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 - #173

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-169-transition-preconditions
Oct 27, 2026
Merged

zhuangjianguo merged 1 commit into
mainfrom
claude/issue-169-transition-preconditions

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #169

Clause-②: no
Metadata-protocol: none

Send for Approval (送审), Start Signing (发起签署) and Activate (生效) were shown whenever the status matched. If a precondition was unmet, the press answered 422 INVALID_STATE. Each button now declares disabled, so it is greyed out until the record says the press can succeed. The hook guards are unchanged and still refuse the press on the server.

Why Part of: one precondition is still checked only after the press. Activate also needs a completed signature round whose formalities_done covers the contract's execution_formalities. That compares a child's multiselect with a field on the parent. A summary filter cannot express that comparison, so no roll-up can carry it. Measured below as F4a. The card's table lists four refusals. All four are now mirrored.

What changed

  • src/objects/contract.object.ts: three new roll-up summary fields on clm_contract. Each copies the version_count / open_deviation_count pattern (readonly, scale: 0, min: 0, group rollup, explicit relationshipField, no max). Each uses the same child filter as the hook guard it mirrors.
field summaryOperations hook guard it mirrors (contract.hook.ts)
approved_legal_review_count count clm_review where { stage: 'legal', decision: 'approved' } in_review to in_approval, "An approved legal review is required"
current_clean_version_count count clm_contract_version where { kind: 'clean', is_current: true } approved to signing, "A current clean version is required"
final_signed_version_count count clm_contract_version where { kind: 'final_signed' } signing to active, "A final_signed version is required"
  • src/actions/contract-lifecycle.actions.ts: transition(...) takes an optional disabled predicate.

    • send_for_approval: record.open_deviation_count > 0 || record.approved_legal_review_count == 0
    • start_signing: record.current_clean_version_count == 0
    • activate_contract: record.final_signed_version_count == 0

    The descriptions of send_for_approval and start_signing now say when the action becomes possible.

  • src/translations/{en,zh-CN}/objects.contract.ts: the three fields' label and help text in both bundles, and the two updated descriptions.

Platform facts this relies on, read at @objectstack/spec@18.2.0:

  • summaryOperations.filter is a full FilterConditionSchema. aggregateSummaryValue (packages/objectql/src/summary-aggregate.ts) ANDs it with the parent foreign-key match, so a filter on two child fields works.
  • ActionSchema.disabled takes the same three forms as visible. The liveness ledger lists it as live on all six rendering surfaces.
  • The console shows only a generic reason: 「对此记录不可用」, as a tooltip on hover and as an aria-describedby description. There is no authored reason. That is the platform half the card leaves out (objectstack#24018 / #24034 B').

is_current moves between versions in contract-version.hook.ts through the engine. Unsetting the old version's flag is a child update, so the roll-up is recomputed then too. Measured in F5.

Measured in the browser (Chromium /opt/pw-browsers/chromium, zh-CN, pnpm demo:zh on a throwaway DB, port 3150)

Before: origin/main e7b56da, with README-way personas (create-user, sys_user_position, set-user-manager, change-password). After: 8b79798, which is this branch rebased on 0159171, signed in as the declared demo staff from #171 (requesterN / counselN / records @example.com). The commits in between (#170, #171) touch none of these files. Every reading was taken signed in as the persona. Setup writes (a review, a deviation decision, version uploads, a signature) went through the REST API, as the persona who would make them.

# persona · contract before after
1 Legal Counsel 2 · DPA-2026-0005, in_review, no approved review 送审 enabled; press, then 422, toast "An approved legal review is required before the contract enters approval." 送审 disabled, tooltip 「对此记录不可用」
2 Legal Counsel 1 · MSA-2026-0001, in_review, 1 open deviation 送审 enabled; press, then 422 "1 deviation(s) are still open; ..." 送审 disabled
3 Records Manager · NDA-2026-0010, approved, no version 发起签署 enabled; press, then 422 "A current clean version is required before signing." 发起签署 disabled
4 Records Manager · NDA-2026-0005, signing, no final_signed 生效 enabled; press, then 422 "A completed signature round is required before activation." 生效 disabled

Flips, after:

# precondition met by reading press
F1 LC2 records a legal/approved review on DPA-2026-0005 (201) 送审 enabled POST .../send_for_approval 200, toast 「已送审,进入审批。」, status in_approval, approved_legal_review_count = 1
F2 LC1 accepts the open deviation on MSA-2026-0001 (200) 送审 enabled 200, 「已送审,进入审批。」
F3 LC2 uploads a clean, current v1 on NDA-2026-0010 (201; current_clean_version_count 0 to 1) 发起签署 enabled 200, 「已进入签署。」. The header then shows 生效 disabled (no final_signed yet)
F4a LC1 uploads a final_signed version on NDA-2026-0005 (201; final_signed_version_count 0 to 1) 生效 enabled 422 "A completed signature round is required before activation." This is the hook-only guard named above
F4b LC1 completes the signature round (sent to completed, 200) 生效 enabled 200, 「合同已生效。」, header now 发起续签
F5 LC2 uploads clean v1 (current) on DPA-2026-0001, then counterparty_redline v2 (current) count 1 after v1, 0 after v2 (v1 is_current unset by the version hook); 发起签署 disabled none
C1 control: LC1 · SUP-2026-0001, seed already has an approved legal review and no open deviation 送审 enabled none

The hook is unchanged and still refuses. A REST press that bypasses the greyed button answers 422 as before:

  • Records Manager POST /api/v1/actions/clm_contract/start_signing on DPA-2026-0001 (current version is a redline): 422 INVALID_STATE "A current clean version is required before signing."
  • Legal Counsel 1 send_for_approval on MSA-2026-0003 (1 open deviation): 422 INVALID_STATE "1 deviation(s) are still open; ...".

Seed values compared with SQL: for all 120 contracts, each roll-up equals the SQL count of its filtered children:

field matching rows
approved_legal_review_count 120 / 120 (total 26 = 26 legal/approved reviews)
current_clean_version_count 120 / 120
final_signed_version_count 120 / 120
open_deviation_count 120 / 120

No column is null. The seed holds no versions (README), so both version counts read an honest 0. After all the writes above, all 120 still match. The server log has no "Roll-up summary recompute failed" line.

Browser console: no disabled predicate warning. The only warnings are the visible "conditional predicate failed ... safe default" ones, which appear on main as well (before run).

Gates (head 8b79798)

pnpm install                 EXIT=0
pnpm validate                EXIT=0   ✓ Validation passed
pnpm lint                    EXIT=0   6 suggestion(s), 0 warnings
pnpm typecheck               EXIT=0
pnpm lint:i18n-gate          EXIT=0   COVERAGE : 0 missing keys across 2 locale(s)
CLM_DEMO_SEED=1 pnpm validate EXIT=0  ✓ Validation passed

Governed text, for the seat's design-doc PR #120 (not touched here)

DESIGN.md §03, the clm_contract row, ending: "· 汇总: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".

Proposed sentence under the §03 state-machine table: "送审 / 发起签署 / 生效 三个按钮以 disabled 读上述汇总,前置条件未满足时置灰;守卫仍在 hook 中直接读子记录并拒绝,汇总只是界面便利。「签署记录 completed 且 formalities_done 覆盖类型 execution_formalities」无法写成汇总过滤,仍只在按下后由 hook 拒绝。"

Acceptance notes

🤖 Generated with Claude Code

https://claude.ai/code/session_017nzWFRUo2U7evMkPdVniAn

…while the hook would refuse them

The three transition buttons were gated on status only, so legal counsel and
the records manager were offered a primary button that answered 422 after the
press. Each now declares `disabled` over roll-ups on the contract itself:
open_deviation_count (existing) plus three new summary counts that mirror the
hook guards' own child filters — approved_legal_review_count,
current_clean_version_count and final_signed_version_count. The hook guards are
unchanged and keep refusing as the server-side control.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017nzWFRUo2U7evMkPdVniAn
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — ACCEPT · head 8b79798 · 2026-10-27T10:46Z

check reading
shape draft → main, rebased on 0159171; mergeable clean
CI on the head both Validate check-runs success on 8b79798 = PR head.sha
scope 4 files (+120/−14): three roll-up summaries on clm_contract (approved_legal_review_count, current_clean_version_count, final_signed_version_count), each with the child filter its hook guard uses, readonly in the rollup group with en/zh-CN labels and help; disabled on 送审 / 发起签署 / 生效. Within the claim's surface. contract.hook.ts untouched, so the server-side refusal stands (REST still 422). Lint 0 warnings
the defect measured in zh-CN with the declared staff. Before: all four presses in the card's table answered 422. After: each button is greyed (「对此记录不可用」) until its precondition holds, then the press succeeds (F1–F4b). Switching the current version from clean to redline greys 发起签署 again (F5). Seed roll-ups equal their SQL child counts on 120/120 contracts
seat call — the remainder C: close #169 on this merge (commit message Fixes #169). All four rows of the card's table are mirrored. Activation's other guard, "a completed round whose formalities cover the type's", compares a child multiselect with a parent field, which no summary filter can express. So 生效 can still be enabled and then refused for that one reason, with the hook's sentence. The authored disabled reason is the platform half the card already routes to the maintainer (objectstack#24018 / #24034 B′, ruled not planned); the seat raises it there, not in this lane
governed text §03 roll-up list and the state-machine note go to seat PR #120
observation the archive-freeze comment in contract.hook.ts still says "five summary"; there are now eight. Comment drift only, no card

Landing: under the maintainer's merge authorization (project thread: 「你可以自己审核的就自己合并」).


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review October 27, 2026 10:47
@zhuangjianguo
zhuangjianguo merged commit 475106f into main Oct 27, 2026
2 checks passed
zhuangjianguo pushed a commit that referenced this pull request Oct 27, 2026
…can succeed (#169, PR #173)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017nzWFRUo2U7evMkPdVniAn
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants