Skip to content

[Decision] Click a stage to advance a deal: the capability exists but a default record page cannot turn it on #30075

Closed

Description

@objectstack-fleet

epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff

The maintainer: 「等我决定的都要立卡,并且从业务的角度给我具体解释和我沟通」 (2026-11-04). Measured by the seat on main 63b86b385d, on the example CRM unless noted. No domain:* label is set: triage routes this card.

The question

Moving a deal to the next stage is the most frequent update in a CRM. Today it takes 编辑 → change 阶段 → 更新. The platform can already advance by clicking the stage path, but only on a page that sets allowStageChange: true on its path block. A default record page has no block to set it on, and most apps use the default page.

Governing text: #28061 and #28303 (opt-in, off by default), and the 2026-08-23 ruling cited in record-path.inertReadout.test.tsx: a click-to-advance control is "a separate card". packages/spec/src/ui/component.zod.ts:2103.

Measured

  • CRM opportunity page: the stage segments are read-only. Nothing in ui/packages/app-shell/src or examples sets the key (git grep allowStageChange).
  • Saving through the dialog offers only the moves the state machine allows.

Options

  • A. On by default on the default record page, for a reader who may edit the field, with a confirmation, judged by the field's validation and state machine. A declared page keeps its own setting.
  • B. A new object-level key the author turns on.
  • C. Declared pages only, as today.

Recommendation: A

四轴分析

实际业务需求。 销售每天推进商机好几次,每次多点三下,还要打开一个大表单只改一个字段。这是记录页最常用的操作。

项目长远合理性。 A 不加新键,默认页本来就是平台决定的。按钮之前被定为只读,是因为它「看着能点其实不能」,现在真能点了,那个顾虑就消失了。

防 AI 写错。 A 没有新键可写错。B 多一个键,AI 容易漏写,功能就悄悄不在。保存仍走同一套校验和状态机,不会绕过规则。

创业阶段不扩散。 能力已建成(#28303),只差默认页接上。风险是误点,所以要二次确认。

棱 A B C
业务 强 中 弱
长远 强 中 中
防错 强 中 强
不扩散 强 弱 强

Not measured

How a stage move behaves when the move needs another required field. The press refuses in place per #28303; the fill-in path is untested. A reader without edit permission keeps the read-only path, per the code comment.

Re-check

git grep -n "allowStageChange" origin/main -- ui/packages/app-shell/src examples

Activity

  1. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff

    The maintainer, 2026-11-04: 「30079这种不是应该在元数据应用中自己去开发自定义操作吗?你的意见是批量换负责人放到内核吗?其他全部同意」.

    Ruling: option A. The default record page turns on click-to-advance for a reader who may edit the field, with a confirmation, judged by the field's validation and state machine. A declared page keeps its own setting, and no new metadata key is added. needs-user-decision is removed; triage routes this card.

  2. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: first-touch audit · p2 kept · domain:ui · 2026-11-04T13:01Z

    ⛔ Not a claim.

    • Read fresh. The ruling recorded on this card (A, 「其他全部同意」) is the scope. The default record page turns on click-to-advance for a reader who may edit the stage field, with a confirmation, and adds no new key. p2 kept.

    Generated by Claude Code

  3. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#3 · os-justin · session_01QSeKYpFBMr917PmVkG9up7

    Claim: PM loop round 2
    Session: session_01QSeKYpFBMr917PmVkG9up7
    Account: os-justin
    Branch: claude/issue-30075-default-page-stage-advance
    Worktree: objectstack-issue-30075
    Domain: domain:ui
    Seat: domain:ui#3
    File surface: ui/packages/plugin-detail/src/renderers/record-path.tsx, ui/packages/plugin-detail/src/renderers/recordPathStageChange.tsx, the default record page's path-block composition (in ui/packages/plugin-detail or ui/packages/app-shell, wherever the default page is built), and their tests (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate; default tier), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: n/a — not a defect card
    Thread-read: 6631324040
    Premise: holds (git grep allowStageChange hits nothing in ui/packages/app-shell/src or examples on origin/main 31fe289fc; the capability lives in plugin-detail recordPathStageChange.tsx)
    Serial constraints cleared: none (no live domain:ui claim names the record-path renderers; this seat's #30069 PR #30156 touches plugin-detail RelatedList.tsx and useDetailTranslation.ts only, in the merge queue)
    Ruling: option A (6631057378), the maintainer's words 「其他全部同意」 (2026-11-04). No new metadata key.


    Generated by Claude Code

  4. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#3 · os-justin · session_01QSeKYpFBMr917PmVkG9up7
    os-dev-report

    {
      "issue": 30075,
      "status": "done",
      "branch": "claude/issue-30075-default-page-stage-advance",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30221",
      "session": "session_01QSeKYpFBMr917PmVkG9up7",
      "premise_still_valid": true,
      "summary": "Premise held on origin/main 31fe289fc: `git grep allowStageChange` hits only plugin-detail (record-path.tsx, recordPathStageChange.tsx, index.tsx registry input, useDetailTranslation.ts) and nothing in ui/packages/app-shell/src or examples. The default record page is built by `buildDefaultHighlights` in ui/packages/plugin-detail/src/synth/buildDefaultPageSchema.ts (rendered by RecordDetailView when no page is assigned); its `record:path` node now carries `allowStageChange: true`, the existing spec key. No key added, nothing under packages/** edited. Who may press is the unchanged opt-in gate (edit session canEdit, field not readonly/computed/system, recordEdit field grant on the stored row); every press confirms, then saves through updateVia with ifMatch, so server validation and state_machine judge it and a refusal shows in place. One renderer change because the press now reaches every app: a stage the field's blocking state_machine cannot reach from the stored value renders as a readout, not a button, using the same `stateMachineNextValues` the record form, grid inline editor and board use (applies to every opted-in path). Authored pages and authored `highlights` slots keep true/false/absent as declared. The inert-readout pin's assertions are unchanged (every mount is a node without the opt-in); only its docblock now names the default page's opt-in. Fill-in path measured: a stage move needing another required field refuses in place, naming the field by label (client pin) over the server's VALIDATION_FAILED fields[] (existing objectql pin, 18/18); no in-place fill-in exists, nothing worse than #28303.",
      "tests": "Local HEAD f9feca33a. Whole plugin-detail isolated: `pnpm exec vitest run --maxWorkers=2 --isolate packages/plugin-detail/` -> Test Files 369 passed | 1 skipped (370), Tests 3270 passed | 18 skipped (3288), lock VERDICT command-exit 0. This ran on 9e94890da, before the origin/main merge f9feca33a; the merge touched plugin-detail RelatedList.tsx, record-quick-actions.tsx and useDetailTranslation.ts only, not these files. record-path.stageChange-28199.test.tsx verbose: 24/24, 10 new #30075 cases. record-path* + synth/: 30 files / 320 tests passed. objectql validate-update-stored-row.test.ts 18/18 (server half of the fill-in path). Ablation 1, via scripts/ablation-replace.mjs with source-resolved subjects so no dist or rebuild is involved: dropping `allowStageChange: true` from the synthesizer gives 5 failed | 19 passed (24); restored, blob 416cc901 == HEAD, git diff HEAD empty. Ablation 2: replacing `&& stageChange.admits(o.stage.value)` gives exactly 1 failed (the state-machine readout pin); restored, blob 1f3e6e82 == HEAD. Its first attempt, anchored on a trailing bare `;`, was refused by the tool (replacement count could not rise) and changed nothing; the anchor was respelled. NOT MEASURED locally: app-shell RecordDetailView/PagePreview/recordDetailSections subset. Reason: four lock attempts on slot issue-30075-dev timed out (exit 99) behind holder pid 15771, alive, a full `--isolate packages/app-shell/` run held for 65+ min. Covered by CI on f9feca33a: Test UI, all 16 shards success.",
      "gates": "Derived with `node scripts/pm/dispatch-gates.mjs --commands` after merging origin/main (merge base 31fe289fc, 6 paths). The first derivation flagged a stale tree, so origin/main was merged in and the run repeated. 33 of 35 families exited 0 locally (exit captured per command before any pipe): adr-0087-registration (+self-test), changeset-no-major, closing-keyword-parity (+self-test), comment-mask-corpus, empty-changeset (+self-test), sdui-manifest (+self-test), release-rehearsal-clone --self-test, release-pending-publish --self-test, spec check:duration-unit-keys, changeset-gate-self-tests, closed-ceremony-text, cross-package-test-inputs, dispatcher-error-vocabulary, driver-memory-census, engine-double-contract, gitlink-declared, nul-bytes, published-files, query-options-erasure, refd-timer-probe, registry-log-declared, rest-log-spy-declared, skill-identifier-liveness, test-source-alias, tier-file-adoption, type-check-coverage, undeclared-dep-imports, watch-hint-literal, where-matcher. NOT MEASURED: `pnpm check:type-check-debt` exit 3 PREREQUISITE NOT MET (its closure build never got the lock); `pnpm --dir ui exec vitest run scripts/` not run (lock). `--ran` reconciliation: 35 derived, 34 run, 1 UNRUN (ui scripts vitest). Local turbo type-check of @object-ui/plugin-detail: NOT MEASURED, lock exit 99. PR check-runs on f9feca33a: Lint & Repo Gates success, TypeScript Type Check success, Test Core success, Build Core skipped, Governed Surface Queue Guard success, Lint UI success, Type Check UI success, Test UI success (16/16 shards), Build & E2E UI success, Live E2E UI success, Check Changeset success.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "0",
      "api_writes": "2 — POST /repos/objectstack-ai/objectstack/dispatches (fleet-write pr_create, #30221, read-back identical 7414/7414 bytes); this os-dev-report comment on #30075 via post-stamped.mjs. Plus git push (not REST).",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": []
    }
  5. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#3 · os-justin · session_01QSeKYpFBMr917PmVkG9up7

    ACCEPT · PR #30221 · head f9feca33a · 2026-11-05T02:39Z

    Seat review of the diff (GET /pulls/30221 diff), against ruling 6631057378 (option A):

    • Shape: buildDefaultHighlights (ui/packages/plugin-detail/src/synth/buildDefaultPageSchema.ts) writes the existing allowStageChange: true on the synthesized page's record:path. No key is added and packages/** is untouched. Authored pages and authored highlights slots keep what they declare. Who may press is unchanged: the edit session, the read-only/computed markers and the field edit grant. Every press confirms, then saves through updateVia with ifMatch, so validation and the state machine judge it (ruling clauses all met).
    • Accepted extension: useStageChange now exposes admits (stateMachineNextValues, the helper the form, grid and board already use). A stage the blocking state machine cannot reach renders as a readout, not a button. This applies to every opted-in path, so the press never offers a move the server must refuse. It narrows a UI offer and changes no contract → Clause-②: no stands.
    • Tests: 10 new cases in record-path.stageChange-28199.test.tsx. The inert-readout pin's assertions are unchanged (docblock only). Two ablations went red and were restored (5/24 and 1/24). The fill-in path is now measured: a move that needs another required field is refused in place and names the field by label (pinned).
    • Gates: check-runs on f9feca33a show 55 runs: 44 success, 11 skipped, 0 red (Test UI 16/16). check-governed-merges --pr 30221 says NOT governed, 254 changed lines. The dev declared these NOT MEASURED locally because of lock contention: the app-shell subset, check:type-check-debt, and the ui scripts/ vitest. CI covered them, green.
    • Out of scope: none.
    • Landing: not governed, under 3000 lines → pr_ready + automerge_enable by the seat. Fixes #30075 closes the card at MERGED.

    Generated by Claude Code

  6. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#3 · os-justin · session_01QSeKYpFBMr917PmVkG9up7

    Landed · PR #30221 · 2026-11-05T02:56Z

    probe reading
    merge MERGED 5fa9375ec at 2026-11-05T02:56Z, via the merge queue; Fixes #30075 closes this card
    content on origin/main git grep -c "allowStageChange: true" origin/main -- ui/packages/plugin-detail/src/synth/buildDefaultPageSchema.ts → origin/main:ui/packages/plugin-detail/src/synth/buildDefaultPageSchema.ts:1

    State: delivered in full, per ruling 6631057378 (option A). pm:dispatched is removed in this stroke.


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions