Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

[design] Field runtime value-shape as a first-class contract (spec-owned), typed action handlers, and file-as-reference #2731

Closed as not planned

Description

@os-zhuang

Tracking / design issue — no implementation yet. Design-first: an ADR pins the contract before any code. Foundational work lands in @objectstack/spec + framework; objectui is a consumer. A linked framework issue should be opened when the spec work is scheduled.

Background

Since ADR-0059, every action param (and every object-form field) renders through the shared field-widget renderer, so each emits its widget's own runtime value shape on confirm/submit, and those shapes differ per type. #2714 (merged in #2720) added a value-shape test net in objectui to pin the param contract. That net is useful as a regression guard, but the discussion it triggered surfaced that the value-shape contract is placed at the wrong layer and is not where the real leverage is.

Three facts from @objectstack/spec / framework that frame the design:

  1. The storage layer's canonical file reference is a fileId string (storage.zod.ts: presigned → upload → CompleteUploadRequest { fileId }). The { file_id, name, url, size, mime_type } descriptor that FileField holds is a client-side convenience object; the authoritative reference is the id. → The param boundary (fileId string) is aligned with storage; the form/record boundary storing the full descriptor is the denormalized outlier — and it's structurally identical to how lookup should work (store id, $expand for display).
  2. The spec declares no runtime value shape. FieldType is a flat z.enum; only multiple hints "Stores as Array/JSON". The value shape is not part of the contract.
  3. Action params and the handler have zero type flow. An action binds to a handler by name (target); params is runtime metadata. An AI (or human) writing the handler gets no type contract telling it a param arrives as string[], a FileRef, etc. This is the structural root of AI-authoring errors.

The core correction

The runtime value that reaches the backend is a property of the FieldType (+ multiple), not of the widget — two renderers may pick different widgets for select, but both must produce string/string[]. So:

Long-term design

Spec exports a pure fieldRuntimeType(type, { multiple }) → RuntimeType — the single source of truth — with two named reference types:

  • FileRef (fileId string): file / image / video / audio at every boundary. The descriptor becomes a read-time expansion, exactly like lookup. Storage stays fileId. This dissolves the param-vs-form divergence at the source, and serializeParamValues' file special-casing retires.
  • RecordRef (record id string): lookup / user / owner / master_detail.

Three consumers of that one function:

  • objectui (renderer) produces the declared runtime type; the widgets must conform.
  • framework generates typed handler params from an action's params (AI correctness), and validates action-endpoint inputs against the contract (object CRUD is already validated by ObjectQL; the gap is custom action endpoints).
  • producer (AI / Studio / humans) writes handler/flow/formula code that is tsc-checked against the derived types.

Conceptually file and lookup unify as "a reference to a stored entity" — the model least likely to confuse an AI author.

What existing/planned work becomes

Thing Retired (short-term band-aid) Adopted (long-term design)
Value-shape source of truth objectui-owned, widget-keyed (#2714) @objectstack/spec fieldRuntimeType, FieldType-keyed
#2714 param net local table as source conformance check: "renderer emits the spec-declared shape" (derives from spec)
Form value-shape net light-vs-heavy debate not a separate net; form & param are both "conforms to the same spec contract"
file / image FileRef vs FileDescriptor as a transitional dual type file-as-reference: widget emits FileRef, add file expansion, records stop storing the descriptor
AI correctness a docs table framework derives typed handler params from params; tsc backs it

Plan (dependency order; each step a real down-payment, not a band-aid)

  • ADR (in framework/docs/adr): pin fieldRuntimeType, the FileRef/RecordRef unification, typed handlers, and the three-consumer model. (design-first, blocks the rest)
  • @objectstack/spec: fieldRuntimeType(type, { multiple }) + FileRef / RecordRef named types; declare file/image/video/audio → FileRef, lookup/user/owner/master_detail → RecordRef. (pure, additive, no migration — the foundation)
  • framework (a): defineActionHandler<typeof action>() / codegen deriving handler param types from params via fieldRuntimeType. (AI-correctness win; do first among framework work)
  • framework (b): action-endpoint input validation against the contract.
  • framework (c): file-as-reference storage + read-time expansion (the only real data/display migration — schedule separately; requires solid $expand so list cells cheaply get name/size).
  • objectui: FileField/ImageField emit FileRef + file expansion for display; retire serializeParamValues' file special-casing; re-point the [P2] Action-param value-shape parity net: assert every FORM_FIELD_TYPES param round-trips to the endpoint contract #2714 net to consume the spec fieldRuntimeType (conformance). mapFieldTypeToFormType (widget selection) stays renderer-local — it is genuinely the renderer's concern.

Notes / risks

  • Cross-repo & ordered: spec/framework changes must merge first; objectui picks them up after (spec dist staleness — rebuild @objectstack/spec before consuming).
  • file-as-reference is the only migration (existing records store descriptors; FileCellRenderer/ImageCellRenderer read .url/.name; seed data). Gate it behind good file expansion.
  • Not a lenient renderer fallback (Commandment #0.1): this moves the contract to the spec and makes both ends conform — the opposite of accreting renderer-side dialects.

Refs

Activity

  1. self-assigned this
    on Jul 22, 2026
  2. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    Blocked-by: objectstack-ai/objectstack#5867

    分诊:跨座位移交 + pm:blocked

    ⛔ objectui 仓内单不打 domain:*(#5468)。

    为什么移交而不是在本仓入队

    本单自己的第一句就是判据:"Design-first:an ADR pins the contract before any code. Foundational work lands in @objectstack/spec + framework;objectui 是消费者"。按 One-main-backlog 与「shared contract surfaces have one owner」两条规则,任何触及 packages/spec 的工作一律归 domain:spec 座位,无论谁需要它 —— 只有那个座位看得见 spec 队列的在飞批次与生成基线冲突。

    本单六步计划里前五步全在上游(ADR → spec 的 fieldRuntimeType → framework 的类型化 handler → 端点校验 → file-as-reference 迁移),objectui 只承担第六步。在本仓入队会让一个 dev 拿到一张它无权改的契约卡。

    已做的动作

    1. 已在主 backlog 立 objectstack#5867(domain:spec + needs-user-decision + Part of objectstack-ai/objectui#2731),把三个待拍板问题(值形状契约层次 / FileRef+RecordRef / 类型化 handler)与那段唯一的真实数据迁移(file-as-reference)分开陈述,迁移建议单独定 appetite;
    2. 本单转 pm:blocked,Blocked-by: 见本评论首行。上游 ADR 与 spec 落地后,本单收缩为第六步的执行单(FileField/ImageField 产出 FileRef + 展开显示、退役 serializeParamValues 的 file 特判、把 [P2] Action-param value-shape parity net: assert every FORM_FIELD_TYPES param round-trips to the endpoint contract #2714 的值形状网改为消费 spec 的一致性检查),届时由 objectui 座位重新定价 —— ⛔ 不要沿用立单时的成本估计,上游落地会显著改变这一步的形状。

    ⚠️ 一处诚实声明(协议半状态)

    按 SKILL,pm:blocked 的机器半边(Blocked-by: 行)应当写在正文里,供解锁扫描 grep。本轮未改写正文:经 API 取回的正文里所有 ' / " / > 均以 HTML 实体形式返回,整体回写会把正文里约 40 处标点变成字面实体串(正文被 sanitizer 破坏正是本座位负责识别的一类缺陷,不能自己制造一个)。⇒ 该行落在本评论首行,标签为准。请 objectui 座位下次编辑本单正文时顺手补一行 Blocked-by: objectstack-ai/objectstack#5867。

    查重

    已在三仓搜 fieldRuntimeType / FileRef / value-shape:本仓与 cloud 无同题单;objectstack 最近邻是 #4633(导入 dry-run 不预检结构化值形状,domain:cli,已在队列),是不同的一件事,已在 #5867 里点名两者的潜在复用关系。


    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. claude commented on Aug 14, 2026

    @claude
    Contributor

    Unlock sweep: blocker cleared. pm:blocked → pm:queue.

    Declared blocker: objectstack-ai/objectstack#5867 — verified closed against the GitHub API on 2026-08-14. No other blocker is declared on this card.

    ⚠️ This card's Blocked-by: line lives in a comment, not the body. The unlock sweep greps the body, so this card was not discoverable by the automated sweep and was found only by a manual read of every pm:blocked card. Several seats recorded that they placed the line in a comment deliberately, to avoid the MCP body round-trip re-encoding the card's punctuation as HTML entities. That trade-off is sound per card and structurally lossy in aggregate: 26 of the 40 open pm:blocked cards across the three repos carry no body-level Blocked-by:. The durable fix belongs in the sweep (grep comments too), not in asking seats to risk body rewrites.

    Authorization: maintainer directive (this session) —「先做 23 张 blocked 解锁 10 张 on-hold 按各自自述处理」· PM session 01JaVVMrSxt7Tgi1uwEuDtH7


    Generated by Claude Code

  4. added
    domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lane
    on Aug 21, 2026
  5. removed their assignment
    on Aug 23, 2026
  6. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    清 assignee — objectstack#11196 维护者裁决 2026-08-23「A:本席清 agent 残留」,skills 席 session 757f0dc0-5dae-51d7-9053-c15c57ebfe3b。证据:assignee 自 07-20 立卡起即在;08-06 移交 + pm:blocked,08-14 解锁扫描回 pm:queue 只翻标签(#11196 根因 1 的三条路径之一,本卡即活标本)——原会话早收束,assignee 为残留。标签未动;认领席按协议 assign + 声明即可。


    Generated by Claude Code

  7. yinlianghui commented on Sep 2, 2026

    @yinlianghui
    Contributor

    前提复核:上游决策卡已关但决定从未记录 —— 本卡按「暂时不准备开发 ⇒ 直接关闭」处置(否决窗口,重开免费)

    一句话问题:这张「字段运行时值形状升为 spec 一等契约」的设计卡,在 objectui 只剩第六步(消费端)可做,而地基(spec 的 fieldRuntimeType / FileRef / RecordRef)一行都不存在。上游决策卡 objectstack#5867 于 2026-08-07 被关为 completed:零评论、无裁决文字,关卡人 qq9340100 是 write 权限协作者账号,不是裁决账号。08-14 解锁扫描只看到「blocker 已关」就把本卡翻回 pm:queue;上一班席位标注「不可派」却留在队列 —— 队列标签在撒谎。

    实测(2026-09-02,可复跑):

    选项 × 真实代价

    做什么 客户可感知代价
    A 关闭 not planned(设计留在卡上;地基出现或有客户拉动时重开) 今天零变化;写 action handler 的 AI/人继续拿不到参数类型契约(现状)
    B 重开上游:在 objectstack domain:spec 立 ADR 前置的多周程序(spec 契约 + framework 类型化 handler + 端点校验 + file-as-reference 存量迁移) 一个跨三仓、含存量数据迁移的多周程序进入排期;本卡仍要等它
    C pm:on-hold + Restart-when: git grep -q fieldRuntimeType origin/main -- packages/spec/src(objectstack) 与 A 同零变化,多一张永远要扫的 hold 卡

    业务上:A = 承认现在不做;B = 立项;C = 挂着等。

    <!-- os-decision-facets -->

    • ① 项目长远合理性(≥50%):方向本身(契约归 spec、按 FieldType 键)是对的,但它是一整套程序不是一张卡;A 不损失方向(设计留在卡上,重开免费),B 才是把方向变成排期。
    • ② 实际业务拉动:零实测拉动 —— 44 天无人撞、无客户报告 handler 参数类型错;零拉动荐不扩散 ⇒ A。
    • ③ 防 AI 犯错:B 的收益(typed handler)真实但远;今天 AI 写错值形状由 [P2] Action-param value-shape parity net: assert every FORM_FIELD_TYPES param round-trips to the endpoint contract #2714 测试网与 ObjectQL 校验响亮拦下。
    • ④ 创业阶段不扩散:A。含存量数据迁移(file-as-reference)的多周程序不是现在。
    • 推荐 A;回退 C。
    • 本分析看不见:维护者 08-07 是否口头裁过「做」(关卡人非裁决账号、无文字);如已裁「做」,请重开并回 B 与 appetite。

    出处三件:谁的指令 —— 维护者;原话 —— 「我建议有些暂时不准备开发的应该直接关闭。」(2026-08-16);在哪 —— pm-dispatch SKILL.md 状态模型「标签纪律」。执行:关闭 not planned,同笔摘 pm:queue(domain:spec / enhancement 留下)。异议 ⇒ 重开即回滚。


    Generated by Claude Code

  8. yinlianghui commented on Sep 2, 2026

    @yinlianghui
    Contributor

    Marker repair (audit only): the facets block in comment 5503066235 above lost its machine-findable first line to the sanitizer (an HTML comment inside backticks reads back as an empty code span). Literal marker for grep, escaped spelling: <!-- os-decision-facets --> — the four-facet block it labels is the bulleted ①–④ list in that comment.


    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

No one assigned

    Labels

    domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec laneenhancementNew feature or request

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions