Skip to content

record:path ticks a stage the record skipped — a contract type with no legal review shows 「审查中 ✓」 after approval #26581

Closed

Description

@objectstack-fleet

Ruled: 6442747364 · letter A (with the current_user guardrail) · 2026-10-27T13:14Z

Filing class: ① user-visible defect (misleading status) — reach: public door, measured in a browser on the 18.1.0 Console with objectstack-ai/hotclm main. Reader: the UI lane (record:path renderer) and the spec owner of RecordPathProps (packages/spec/src/ui/component.zod.ts:2187).

Business reason

In a contract system, 「审查中 ✓」 reads as "legal reviewed this". hotclm order contracts (ORD) skip legal review by design and go straight to approval. Yet their page path shows 审查中 as a passed step. An auditor or a requester reading the page is told something that did not happen.

Measured

ORD-2026-0017 (submitted → in_approval → approved, never in_review): the path shows 草稿 ✓ 已提交 ✓ 审查中 ✓ 审批中 ✓ 已批准. The renderer ticks every stage before the current one by position. A stage takes only value, label and terminal; there is no way to say a stage applies conditionally or was skipped.

Expected (either is enough)

  • A per-stage predicate (e.g. visibleWhen over the record) so an app can drop 审查中 for a type that skips it; or
  • the renderer reads the record's history and shows a skipped stage as skipped (grey, no tick), not passed.

Dedupe: objectstack issues (2,000 most recent) and archived objectui issues grepped for record:path, skip, stage + path — none.


Filed from the hotclm UI/UX review (2026-10-27); maintainer: 「平台的问题应该提交issue到平台」.

Activity

  1. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p2 · domain:ui · area:records · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-27T09:01Z

    ⛔ Not a claim, ⛔ not a dispatch.

    • p2: record:path ticks every stage before the current one by position, so a stage the record skipped reads as passed. That is a misleading status on an audit-facing page. First measure whether the record's state history is already readable on the page: if it is, tick only the visited stages. If a stage needs a new declared key on RecordPathProps (packages/spec/src/ui/component.zod.ts:2187), that is a protocol addition, p1, domain:spec, and it comes back to triage before it is built.

    Generated by Claude Code

  2. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    bugSomething isn't working
    domain:uiDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Oct 27, 2026
  3. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01QsR2jTfvjNTaR29ehLk26T
    Account: zhuangjianguo
    Branch: claude/project-thread-ziacft (platform-assigned card-thread branch; a card thread can push only its assigned branch, and the name carries no issue number, so this claim is the identity record)
    Worktree: objectstack-issue-26581
    Domain: domain:ui
    Seat: domain:ui#4
    File surface: ui/packages/plugin-detail/src/renderers/record-path.tsx, its tests, one root changeset; ⛔ packages/spec/** and server packages (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate; reason: the maintainer's standing rule 「只认领不需要 fable 的卡」, 2026-10-27T10:39Z), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: RecordPathRenderer (plugin-detail, this lane) ticks every stage before the current one by position · the platform path that may cover it: the record's status history, if readable on the page (unmeasured) · reached by every reader of a record:path whose flow skips a stage (hotclm ORD contracts)
    Thread-read: 6437783999
    Premise: holds (action, card-reference and work-item surfaces re-read on origin/main 498e8f40 at 2026-10-27T11:40Z)
    Serial constraints cleared: none in flight; #26612 (not yet triaged) may later land on record-path.tsx; no open PR touches it (scan 2026-10-27T11:40Z)

    Dispatched at 2026-10-27T11:43Z to the card thread on that branch; the order lives in the thread's opening message.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 26581,
      "status": "needs_decision",
      "branch": "claude/project-thread-ziacft",
      "pr": null,
      "session": "session_01QsR2jTgHpQBzM5GaoKifNh",
      "premise_still_valid": true,
      "summary": "Stop condition met: no history read is reachable from record:path without a new server read, so nothing was changed and no PR opened. The defect is real on origin/main 498e8f40: record-path.tsx:443 (desktop) and :514 (mobile) tick every stage with idx before currentIdx, and the only record input is ctx.data[statusField] (:231). The renderer's one channel, RecordContextValue (ui/packages/react/src/context/RecordContext.tsx:113), carries objectName, recordId, dataSource, data (current row only), objectSchema, refresh and header members; no status history. The record page does read history, but not into anything record:path can reach: RecordDetailView.tsx:2092-2096 reads sys_audit_log old_value/new_value only when enable.trackHistory === true (opt-in, :1965), only on the synthesized page, only once the History tab is on screen (objectui#13010 deferral), $top 50 desc, and hands the rows only to the record:history node's entries (:3225); the sys_activity read (:2550, metadata {old,new} per audit-writers.ts:2309) is deferred to a feed block on screen and lands in DiscussionContext as FeedItems. Even a full read is not proof of a skip: activities can be opted out (enable.activities false), rows for fields the reader is not served are withheld (activity-field-redaction.ts), unstamped rows are withheld until backfill, and both reads are capped, so a missing in_review row is not evidence the record skipped it. A smaller route exists that needs no history and no spec key: the status field option's existing, server-enforced visibleWhen (open question 1).",
      "tests": "none run: no code change (stop condition)",
      "gates": "none run: empty diff; dispatch-gates --commands not derived (no changeset, no files changed)",
      "dogfood_battery": "n/a — no PR",
      "mcp_calls": "0 — no GitHub MCP calls",
      "api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/26581/comments (this os-dev-report, via scripts/pm/post-stamped.mjs)",
      "line_budget": "n/a — empty diff",
      "deviations": [],
      "files_changed": [],
      "open_questions": [
        {
          "question": "How should record:path stop presenting a stage this record could not have passed?",
          "options": [
            "A — Honor the status field option's declared visibleWhen (packages/spec/src/data/field.zod.ts:431: 'option is offered only when TRUE (else omitted)', and the server refuses a write of a value whose predicate is false, packages/objectql/src/validation/rule-validator.ts:185-200). record:path omits from both rows any stage whose matching option's predicate is FALSE for this record; a faulting predicate keeps the stage (render side stays fail-open, rule-validator.ts D3). Matches authored stages by value, the way declaredStageKinds already reads option kind. Zero spec keys, zero new server reads, file surface record-path.tsx plus tests plus changeset. hotclm then declares visibleWhen on its in_review option (app-side, existing key; hotclm not in this container, so not measured). Does not cover an ad-hoc skip no rule declares.",
            "B — record:path self-fetches sys_activity (or sys_audit_log) for this record and ticks only statusField values seen in metadata old/new. A new server read on every record page that draws a path, against the objectui#13010 deferral; sys_audit_log needs trackHistory opt-in; withheld rows, opt-out and $top caps make absence ambiguous, so it would show a guessed skipped state unless a third 'unknown' presentation is added (product decision).",
            "C — New per-stage key on RecordPathProps.stages[] (e.g. visibleWhen) for authored stages that are not field options. Protocol addition: p1, domain:spec, back to triage per comment 6437783999. No measured pull beyond what A covers."
          ],
          "recommendation": "A. Business need: the measured case (hotclm ORD skips legal review by contract type, by design) is a rule about the record's own columns, which is exactly what an option visibleWhen declares. Long-term: it honors an already-declared, server-enforced contract (declared-but-unhonored is an implementation gap, not a new surface). AI-error: declaration-driven and enforced on write, no consumer-side guessing from incomplete history. Startup focus: no new spec key, no new read, no new gate. B adds a read and an ambiguous state; C expands the protocol without pull. A needs the seat to amend the order, since the order's stop condition said change nothing."
        }
      ],
      "out_of_scope_findings": [
        "class: none — noted, not filed · RecordDetailView.tsx:1951-1963 comment says the audit read requests 'only the safe projection (created_at, action, user_id) so the browser never receives serialized old_value/new_value', but :2096 selects old_value and new_value; stale comment, observation only · carrier: none"
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    待维护者裁决 · record:path 如何不再把「跳过的阶段」画成「已通过」

    席位 domain:ui#4 · session_01QsR2jTfvjNTaR29ehLk26T · 2026-10-27T11:50Z · 依据 os-dev 报告 6441110297(停止条件命中,未改代码、未开 PR)

    一句话问题: 一份订单合同按设计不走法务审查,但合同页上方的阶段条依然显示「审查中 ✓」,审计人员会读成"法务审过了"。

    背景(实测,origin/main 498e8f40): 阶段条 ui/packages/plugin-detail/src/renderers/record-path.tsx(:443 桌面、:514 移动)按位置给当前阶段之前的每一格打勾。它唯一能读到的是当前记录行(RecordContextValue,ui/packages/react/src/context/RecordContext.tsx:113),读不到状态历史。页面上的两条历史读取(sys_audit_log 需 trackHistory 开启、只在历史页签可见时读、最多 50 条;sys_activity 只在动态块可见时读)都到不了阶段条。而且这两条历史天生不完整(可关闭、对读者隐藏的字段行被扣留、有条数上限),所以"历史里没有"也证明不了"跳过了"。

    Governing text: packages/spec/src/data/field.zod.ts(选项 visibleWhen):"option is offered only when TRUE (else omitted) … the rule validator refuses a write of a value whose predicate is false … so a user-gated CHOICE belongs here"。packages/objectql/src/validation/rule-validator.ts:186-188:"That gate is the server's enforcement of who may pick an option (ADR-0124 D1)"。分诊评论 6437783999:新的阶段键属协议新增,p1、domain:spec,须回分诊。

    协议声明: 选项 A 不改协议;C 改协议(新增键)。

    带 re-check 命令的前提:

    • 阶段条按位置打勾:git grep -n "idx < currentIdx\|index < currentIndex" origin/main -- ui/packages/plugin-detail/src/renderers/record-path.tsx(对照词:git grep -c "statusField" origin/main -- ui/packages/plugin-detail/src/renderers/record-path.tsx 必中)。
    • 选项 visibleWhen 由服务端强制:git grep -n "who may pick an option" origin/main -- packages/objectql/src/validation/rule-validator.ts。
    选项 做什么 客户看到的后果 工作量
    A 阶段条复用状态字段选项上已有的 visibleWhen:对本记录为假的选项不画成阶段;谓词出错则照画;谓词引用 current_user 的不据以隐藏(那是"谁能选",不是"记录走哪条路") 应用声明"订单类合同不可选审查中"后,订单合同的阶段条不再出现审查中 ui 车道 · 1 个 PR(record-path.tsx + 测试 + changeset)· 无先例
    B 阶段条自己去读 sys_activity/sys_audit_log,只给历史里出现过的值打勾 每个带阶段条的记录页多一次请求;历史缺失时只能猜,或新增"未知"态(又一个产品决定) ui 车道 · 1 个 PR + 一次新读取 · 无先例
    C RecordPathProps.stages[] 新增每阶段条件键 作者可单独声明阶段显隐,不依赖选项 spec 车道 + ui 消费端 · 2 个 PR · 需契约复审档复核

    业务含义直译: A = "不能选的路就不画";B = "去翻流水账,翻不到就猜";C = "给阶段条单独开一个开关"。

    四轴(业务立场):

    • 项目长远合理性:A 复用一个已声明且服务端强制的契约,不新增面;类比主流平台按"记录类型 → 可用选项"决定路径阶段。B 让渲染器依赖不完整的数据源。C 增加一个与选项可见性重叠的第二处声明。
    • 实际业务拉动:今天撞上的是 hotclm 订单合同(按合同类型跳过法务),正是"按本记录字段决定可选值"的规则,A 正好能表达;C 无 A 之外的实测拉动。
    • 防 AI 犯错:A 由声明驱动、写入时服务端拒绝,出错是响亮的;B 静默猜测;C 让 AI 要在两处声明同一件事,容易不一致。
    • 创业阶段不扩散:A 零新键、零新读取、零新门禁;C 新增永久义务。

    os-decision-facets

    • ① 项目长远合理性:A 缩小特例(复用既有选项契约),C 扩大契约增生。
    • ② 实际业务拉动:hotclm 订单合同今天撞上;A 覆盖该场景,C 零额外拉动。
    • ③ 防 AI 犯错:A 声明驱动且写入强制、响亮;B 静默猜测;C 双处声明易漂移。
    • ④ 创业阶段不扩散:A remove/复用优于 C 的 declare-and-maintain。
      Prior rulings read: option visibleWhen, record path stage → ADR-0124 D1 (who may pick an option), ADR-0137 D2/D3 (render side fail-open); thread: 6437783999

    推荐:A。 只看①选 A;②③④ 是否翻转:否。置信缺口:A 把"谁能选这个值"读成"这条记录走不走这一步",谓词引用 current_user 时两者不同,所以 A 带了"引用 current_user 的谓词不据以隐藏"这条护栏;这个护栏本身就是产品语义的选择,所以呈您裁。hotclm 能否用 visibleWhen 表达"订单类跳过审查"未在本容器实测。B 不取:靠不完整历史只能猜。C 不取:无 A 之外的拉动,且需契约复审档复核。

    裁后执行: 选 A ⇒ 席位改派发令(取消"不改代码"停止条件),同一卡线程实施 record-path.tsx + 测试 + changeset,Clause-②: no;hotclm 侧声明 visibleWhen 由 hotclm 自己跟进。选 C ⇒ 本卡转分诊,按协议新增 p1 走 spec 车道。选 B ⇒ 先另裁"未知"态的呈现。

    请回一个字母:A / B / C。


    Generated by Claude Code

  6. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Oct 27, 2026
  7. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Brief-claimed-by: session_01KrKXQVvgYk2AJ4VkqMobyb · batch #413 · 2026-10-27T12:28Z

    Director seat, summon #51 continuation (ledger objectstack#12708). Presented to the maintainer in batch #413 as item 2 of 3 — the ui seat's question 6441171471 after the card thread's stop (6441110297). Seat increment (re-verified on origin/main fe53769a):

    • Premises hold: ui/packages/plugin-detail/src/renderers/record-path.tsx:443 and :514 tick every stage with idx < currentIdx; the renderer's only record input is the current row. packages/spec/src/data/field.zod.ts option visibleWhen: "option is offered only when TRUE (else omitted)"; packages/objectql/src/validation/rule-validator.ts:188 calls that predicate "the server's enforcement of who may pick an option (ADR-0124 D1)". Triage 6437783999 graded the card p2 domain:ui and read a new per-stage key as a protocol addition (p1, spec).
    • Half-state: needs-user-decision alone (the thread's claim released); the ruling's stroke returns it to pm:queue.
    • Facets (the seat's block, re-read): ① A reuses a declared, server-enforced contract — an option this record cannot take is not a stage this record walks; C adds a second place to declare the same fact; B makes the renderer depend on incomplete history. ② hotclm ORD contracts today, an auditor reads 「审查中 ✓」 as "legal reviewed this". ③ A is declaration-driven and loud on write; B guesses silently; C drifts between two declarations. ④ A adds no key, read or gate.
    • The one product semantic inside A, which is why it is in the box: visibleWhen answers "who may pick this value", the path asks "does this record walk this stage"; they coincide for predicates over the record's own columns and diverge for predicates over current_user. The seat's guardrail: a predicate that references current_user does not hide a stage; a faulting predicate keeps the stage (render side fail-open, ADR-0137 D2/D3). Unmeasured: whether hotclm can express "ORD skips review" as an option visibleWhen — the hotclm seat confirms on its own card.
    • The seat agrees with A with that guardrail. Not B (a new read per record page against the objectui#13010 deferral, and an ambiguous "unknown" state to design) and not C (a protocol key with no pull beyond A). Workload: ui · 1 PR (record-path.tsx + tests + changeset) · S; Clause-②: no.
      Prior rulings read: ADR-0124 D1, ADR-0137 D2/D3, objectui#13010; thread: 6437783999, 6441171471.
  8. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #413 item 2 · letter A · maintainer 「同意」 2026-10-27T13:11Z

    Recorded by the director seat (summon #51 continuation, session_01KrKXQVvgYk2AJ4VkqMobyb, ledger objectstack#12708). The maintainer answered batch #413 with 「同意」 — every seat letter. Brief 6441900906; the ui seat's question 6441171471. Letter A, with the guardrail: record:path omits from both rows any stage whose matching status-field option declares a visibleWhen that evaluates FALSE for this record (the option contract of packages/spec/src/data/field.zod.ts, enforced on write by rule-validator.ts per ADR-0124 D1); a faulting predicate keeps the stage (render side fail-open, ADR-0137 D2/D3); a predicate that references current_user does not hide a stage — that predicate answers "who may pick the value", not "does this record walk this stage". Stages are matched to options by value, as declaredStageKinds already reads option kind. Not B (a new read per record page against objectui#13010, and an "unknown" state to design) and not C (a protocol key with no pull beyond A).

    Workload (card column): ui · 1 PR (ui/packages/plugin-detail/src/renderers/record-path.tsx + tests + changeset) · S; Clause-②: no, Metadata-protocol: none. hotclm declares the option visibleWhen on its contract-type rule on its own card; whether that rule is expressible as an option predicate is the hotclm seat's reading.

    Execution: needs-user-decision → pm:queue in this act (domain:ui, p2); the ui seat amends its dispatch order (the "change nothing" stop condition lifts) and the same card thread implements. Ruling-ref: names this comment.

  9. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Oct 27, 2026
  10. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim amendment · domain:ui#4 · 2026-10-27T13:20Z

    Ruling-ref: 6442747364
    Thread-read: 6442747364

    The "change nothing" stop condition of claim 6441053217 lifts. Letter A with the guardrail is the order. The same card thread (claude/project-thread-ziacft, session_01QsR2jTgHpQBzM5GaoKifNh) implements it.

    File surface: ui/packages/plugin-detail/src/renderers/record-path.tsx, its tests, one root changeset; ⛔ packages/spec/**, server packages, new keys or reads (stop on breach)
    Clause-②: no
    Metadata-protocol: none
    Container & model: M, mode:thread, model: TIER_DEFAULT, effort: high

    Order (per the ruling): record:path omits from both rows (desktop and mobile) every stage whose matching status-field option declares a visibleWhen that evaluates FALSE for this record. Stages match options by value, the way declaredStageKinds already reads option kind. A faulting predicate keeps the stage (fail-open, ADR-0137 D2/D3). A predicate that references current_user does not hide a stage.

    Serial constraint: PR #26689 (#26612) is in the merge queue on the same file. The thread starts from origin/main after #26689 merges.
    Out of scope: the lost-record "last open step" stays on #26694.

  11. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Oct 27, 2026
  12. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 26581,
      "status": "done",
      "branch": "claude/project-thread-ziacft",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/26784",
      "session": "session_01QsR2jTgHpQBzM5GaoKifNh",
      "premise_still_valid": true,
      "summary": "Implemented ruling 6442747364 letter A with its guardrail, per claim amendment 6442931479. record:path filters its stages through stagesThisRecordWalks before #26689's kind-based classify/layout, so both rows read one list: a stage whose status-field option (matched by value, authored stages included) declares a visibleWhen that is FALSE for the bound record is omitted. A predicate reading an acting-user root (current_user, user, ctx, os, the server option gate's USER_SCOPE_ROOTS) hides nothing; a faulting predicate keeps its stage (evalFieldPredicate fallback true, ADR-0137 D3); the current stage is always kept. No spec key, no new read. Head a5c3c69f on base 94bab5a2 (carries #26689); no merge-main owed.",
      "tests": "head a5c3c69f, from ui/ under os-verify-lock: new record-path.optionVisibleWhen-26581.test.tsx 7/7; all record:path tests 13 files 102/102; full packages/plugin-detail/ Test Files 315 passed 1 skipped (316), Tests 2911 passed 18 skipped (2929), VERDICT command-exit 0; pnpm --filter @object-ui/plugin-detail run type-check exit 0. Ablations via ablation-replace.mjs (source-resolved test, no dist), each restored to HEAD blob 35a646591faa with git diff HEAD empty: filter off -> 3 failed/4 passed (a, e, f); acting-user guard off -> 1 failed (b); current-stage guard off -> 1 failed (d).",
      "gates": "local: dispatch-gates --commands derived 34 commands from 3 paths on a5c3c69f; all 34 exit 0 (captured before any pipe); --ran: 34 derived, 34 run, 0 NOT-MEASURED (a DERIVED zero). PR-body gates re-run on the final body: check-adr-0087-registration, check-closing-keyword-parity, check-changeset-no-major all exit 0. CI on a5c3c69f at report time: 19 check-runs, 17 in_progress, 2 skipped, none red.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "0 — no GitHub MCP calls",
      "api_writes": "3 — POST issues/26581/comments (os-dev-report 6441110297, earlier round); pr_create objectstack-ai/objectstack#26784 via fleet-write relay run 40531227715 (read-back identical, assignee zhuangjianguo); POST issues/26581/comments (this report)",
      "line_budget": "283 insertions, 2 deletions over 3 files (record-path.tsx +76/-2, test +189, changeset +18); no skills/** touched",
      "deviations": [
        "Acting-user detection is lexical (source with string literals blanked, roots not preceded by a dot), not the server's AST reader collectCelRootIdentifiers: @objectstack/formula is not a plugin-detail dependency and package.json is outside the file surface. It can only over-match, which keeps a stage.",
        "A reviewer request on the PR was not made: the relay pr_create row carries assignees only."
      ],
      "files_changed": [
        ".changeset/26581-record-path-option-visiblewhen.md",
        "ui/packages/plugin-detail/src/renderers/__tests__/record-path.optionVisibleWhen-26581.test.tsx",
        "ui/packages/plugin-detail/src/renderers/record-path.tsx"
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "class: none · hotclm still has to declare visibleWhen on its in_review option for the ORD case to change on screen; ruling names the hotclm seat's card · carrier: hotclm seat",
        "class: none · RecordDetailView.tsx:1951-1963 comment says old_value/new_value are not fetched, :2096 selects them; stale comment, observation only · carrier: none"
      ]
    }

    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:uiDomain lane — seat card indexed by label:pm:seatpm:dispatchedDispatched to a dev agent by /pm-dispatchpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions