Skip to content

Send for Approval, Start Signing and Activate are offered while their precondition is unmet, then refused after the click — grey them out with disabled #169

Closed

Description

@objectstack-fleet

Filing gate: ① user-visible defect, reproduced in the browser on @objectstack 18.2.0 (the 18.2 upgrade pass, hotclm PR #155), zh-CN, real personas.

Symptom. On NDA-2026-0023 in in_review with no approved review yet, legal counsel sees 送审 as an enabled primary button. Pressing it answers 422 INVALID_STATE and the toast reads "An approved legal review is required before the contract enters approval." The button gave no sign beforehand that it could not succeed.

Where. src/actions/contract-lifecycle.actions.ts gates the transition buttons on record.status only (visible), and src/objects/contract.hook.ts refuses the edge after the press:

Action Hook refusal (contract.hook.ts on main e7b56da) Readable on the record today?
send_for_approval L490 open deviations remain yes, open_deviation_count (summary)
send_for_approval L497 no approved legal review no — needs a roll-up (e.g. approved legal review count)
start_signing L519 no current clean version no — needs a roll-up or stamped field
activate_contract L547 no final_signed version no — needs a roll-up

Expected. Where the precondition is readable on the record, the action declares disabled (ActionSchema has carried it since before 18) so the button is greyed out instead of failing on click; the hook keeps refusing as the server-side control. Where it is not readable, a roll-up summary field makes it readable (DESIGN.md §03 already carries version_count / open_deviation_count).

Platform half, not this card. disabled has no authored reason, so a greyed button cannot say why. That key was ruled not planned on objectstack#24018 / #24034 B′ ("re-file after GA when a second page needs it, on a measured ask"); it goes to the maintainer as its own decision, not here.

Business reason. Legal and the records manager press these buttons on every contract; a primary button that fails reads as a broken system, and the English refusal (objectstack#26170) makes it worse in zh-CN.

Reader. hotclm execution seat (PM seat #36). Shares src/actions/contract-lifecycle.actions.ts with #158 (merged as PR #167), so dispatch after it.

Duplicate search. hotclm issues, all states, "disabled / precondition / button refused after click": 0 hits.

Activity

  1. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage — 2026-10-27

    User-visible defect, read against main: accepted.

    Route: app-side. Where the precondition is readable on the record, declare disabled on the action. Where it is not readable, any new roll-up field is a DESIGN.md §03 change, which goes to the seat's design-doc PR. The hook keeps refusing as the server-side control. The platform half stays with the platform card the body names.

    State: → pm:queue + bug.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 17
    Ruling-ref: none (triaged defect, comment 6439191047)
    Session: session_017nzWFRUo2U7evMkPdVniAn
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-169-transition-preconditions
    Worktree: hotclm-issue-169
    Domain: repo:hotclm (single-lane sister repo — no domain:*)
    Seat: repo:hotclm#1
    File surface: src/actions/contract-lifecycle.actions.ts (disabled on send_for_approval / start_signing / activate_contract) · src/objects/contract.object.ts (roll-up summary fields, only if a precondition is otherwise unreadable) · translations for any new field — governed §03 text for a new field rides the seat's design-doc PR #120; stop on breach; explain in the report
    Container & model: S, mode:subagent, model: opus (TIER_DEFAULT; hand-derived for a sister repo), effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: this app's lifecycle buttons (offered only when they can succeed) | possibly a platform half, named in the body | every lawyer and records manager moving a contract
    Thread-read: 6439191047
    Premise: re-measured by the dev on origin/main e7b56da (@objectstack 18.2.0) with README personas first
    Serial constraints cleared: #158 landed (PR #167, same actions file); in flight #156 (flows), #28 (config / README) — disjoint files.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 169,
      "status": "done",
      "branch": "claude/issue-169-transition-preconditions",
      "pr": "https://github.com/objectstack-ai/hotclm/pull/173",
      "session": "session_017nzWFRUo2U7evMkPdVniAn (parent seat session; subagent)",
      "premise_still_valid": true,
      "summary": "Before (origin/main e7b56da, zh-CN, README personas): 送审 / 发起签署 / 生效 were enabled with the precondition unmet, and each press answered 422 INVALID_STATE. Fix: three new roll-up summary fields on clm_contract, each with the hook guard's own child filter: approved_legal_review_count (clm_review {stage: legal, decision: approved}), current_clean_version_count (clm_contract_version {kind: clean, is_current: true}), final_signed_version_count ({kind: final_signed}). With open_deviation_count, they drive a `disabled` predicate on send_for_approval, start_signing and activate_contract. The fields have en + zh-CN labels and help, and are readonly in the rollup group like the existing roll-ups. contract.hook.ts is untouched; a REST press past the grey button still answers 422. PR body says `Part of #169`: activate also needs a completed signature round whose formalities_done covers the contract's execution_formalities. That compares a child multiselect with a parent field, so no summary filter can express it, and the guard stays hook-only (measured F4a). All four refusals in the card's table are now mirrored.",
      "tests": "Browser: Chromium /opt/pw-browsers/chromium, zh-CN, pnpm demo:zh on a throwaway DB, port 3150. Before used README-way personas; after used the #171 declared staff on 8b79798 (rebased on 0159171). BEFORE, all enabled and all 422 on press: LC2 送审 on DPA-2026-0005 (no approved review); LC1 送审 on MSA-2026-0001 (1 open deviation); RM 发起签署 on NDA-2026-0010; RM 生效 on NDA-2026-0005. AFTER, the same four are `disabled`, with aria-describedby and hover tooltip 「对此记录不可用」 (screenshot shots/hover-DPA-2026-0001.png). Flips: F1 approved review recorded → 送审 enabled → 200 「已送审,进入审批。」. F2 deviation accepted → 送审 enabled → 200. F3 clean current v1 uploaded → 发起签署 enabled → 200 「已进入签署。」, after which 生效 shows disabled. F4a final_signed uploaded → 生效 enabled → 422 'A completed signature round is required' (the hook-only guard). F4b signature completed → 200 「合同已生效。」. F5 clean v1 then redline v2 current → current_clean_version_count 1 then 0 (the version hook's is_current unset is recomputed) → 发起签署 disabled. C1 SUP-2026-0001 (seed already meets the precondition) → 送审 enabled. Server control: REST start_signing on DPA-2026-0001 → 422 INVALID_STATE; REST send_for_approval on MSA-2026-0003 → 422 INVALID_STATE. SQL: for all 120 seed contracts each roll-up equals the filtered child count (approved_legal 26 = 26, clean 120/120, final 120/120, open_dev 120/120, 0 nulls), and still 120/120 after the measurement writes. No 'Roll-up summary recompute failed' in the server log. No ablation: no test pins this behaviour (metadata-only app).",
      "gates": "head 8b79798: pnpm install EXIT=0; pnpm validate EXIT=0 (✓ Validation passed); pnpm lint EXIT=0 (6 suggestions, 0 warnings: no new warning); pnpm typecheck EXIT=0; pnpm lint:i18n-gate EXIT=0 (0 missing keys across 2 locales); CLM_DEMO_SEED=1 pnpm validate EXIT=0. Exit codes were captured before any pipe. PR check-runs: not read (in_progress at report time).",
      "dogfood_battery": "n/a — sister repo, no `!` in title, Clause-②: no",
      "mcp_calls": "0",
      "api_writes": "2 via fleet-write relay: pr_create objectstack-ai/hotclm#173; this os-dev-report comment on #169. Plus 1 git push (not REST).",
      "open_questions": [
        {
          "question": "Activate's other guard (a completed signature round whose formalities_done covers execution_formalities) stays hook-only, so 生效 can still be enabled and then refused. Should the card stay open for it?",
          "options": [
            "A: Keep #169 open (PR says Part of) and route the remainder to the platform half with the authored disabled reason: a greyed button that cannot say why is the same gap",
            "B: Add completed_signature_count (summary, {status: completed}) and grey 生效 on it too. That covers the 'no completed round' case only, not formalities coverage; it is one more §03 field",
            "C: Accept the residual and close #169 on merge (the seat edits the body to Fixes)"
          ],
          "recommendation": "A, because no summary filter can express the coverage comparison, B would only half-mirror the guard and add a governed field, and the card's own table is fully covered."
        }
      ],
      "out_of_scope_findings": [
        "class: none (comment drift) · src/objects/contract.hook.ts archive-freeze comment says 'the engine recomputes the five `summary`' — now eight · carrier: #157 (holds contract.hook.ts this round) · noted in PR Acceptance notes, not filed"
      ],
      "governed_text_proposal": {
        "file": "DESIGN.md §03",
        "clm_contract_row_tail": "· 汇总:version_count · open_deviation_count · approved_legal_review_count(`legal` 审查 `approved` 条数)· current_clean_version_count(`kind: clean` 且 `is_current` 的版本条数)· final_signed_version_count(`kind: final_signed` 版本条数)· overdue_obligation_count · planned_amount · actual_amount",
        "state_machine_note": "送审 / 发起签署 / 生效 三个按钮以 `disabled` 读上述汇总,前置条件未满足时置灰;守卫仍在 hook 中直接读子记录并拒绝,汇总只是界面便利。「签署记录 completed 且 formalities_done 覆盖类型 execution_formalities」无法写成汇总过滤,仍只在按下后由 hook 拒绝。"
      },
      "deviations": [
        "The after-measurement used the #171 declared staff (example.com, admin123), as the updated common.md says; the before-measurement used README-way personas on e7b56da, because that is what main offered then. The rebase picked up #170 and #171, which touch none of these files.",
        "Changed two action description strings (send_for_approval, start_signing) in source and both bundles so the copy says when each becomes possible. Same files as the claim's surface."
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling-ref: none (triaged defect, comment 6439191047)

    Landed — PR #173 · 2026-10-27

    reading value
    PR #173 MERGED (squash on main) at its ACCEPT head 8b79798; the squash commit carries Fixes #169 (seat call C in the ACCEPT: every row of this card's table is mirrored)
    authority maintainer, project thread, verbatim: 「你可以自己审核的就自己合并」

    Release: 送审, 发起签署 and 生效 are greyed out until their precondition holds, so a button no longer fails after the press.

    Remaining: 生效 can still be refused after the press for one reason that no roll-up can express, a completed signing round whose formalities do not cover the type's. A greyed button also cannot say why; that is the platform half this card routes to the maintainer.

    State: pm:dispatched removed, assignee cleared.


    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

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions