You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
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:
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.
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.
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/maine7b56da (@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.
{
"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."
]
}
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)
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.
Activity
objectstack-fleet commentedon Oct 27, 2026
Triage — 2026-10-27
User-visible defect, read against
main: accepted.Route: app-side. Where the precondition is readable on the record, declare
disabledon 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
objectstack-fleet commentedon Oct 27, 2026
Claim: PM loop round 17
Ruling-ref: none (triaged defect, comment 6439191047)
Session:
session_017nzWFRUo2U7evMkPdVniAnAccount:
zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-169-transition-preconditionsWorktree:
hotclm-issue-169Domain:
repo:hotclm(single-lane sister repo — nodomain:*)Seat:
repo:hotclm#1File surface:
src/actions/contract-lifecycle.actions.ts(disabledon 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 reportContainer & model:
S,mode:subagent,model: opus(TIER_DEFAULT; hand-derived for a sister repo),effort: mediumClause-②: 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 contractThread-read: 6439191047
Premise: re-measured by the dev on
origin/maine7b56da(@objectstack 18.2.0) with README personas firstSerial constraints cleared: #158 landed (PR #167, same actions file); in flight #156 (flows), #28 (config / README) — disjoint files.
Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
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
objectstack-fleet commentedon Oct 27, 2026
Ruling-ref: none (triaged defect, comment 6439191047)
Landed — PR #173 · 2026-10-27
MERGED(squash onmain) at its ACCEPT head8b79798; the squash commit carriesFixes #169(seat call C in the ACCEPT: every row of this card's table is mirrored)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:dispatchedremoved, assignee cleared.Generated by Claude Code
DESIGN §03: three roll-ups grey out the lifecycle buttons until they …