Repository navigation
A signature round can be created already declined or voided — the round's state machine judges updates only - #150
Merged
Conversation
An insert into clm_signature judged nothing: a REST POST, a batch create or an import could create a round already declined, voided, or completed by e-signature without passing through sent. The entry set is draft, sent, and completed for wet ink. It is a declarative state_machine rule (signature_initial_status, insert only) so its refusal is translated in both bundles and seed replay / a historical import skip it, plus the one method-dependent half in the signature_state_machine hook's insert leg: an e-signature round is not created completed, system writes exempt (the demo seeds 24 such rounds). Existing rows and the update leg are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nzWFRUo2U7evMkPdVniAn
Contributor
Author
席位核对 — ACCEPT · head
|
| check | reading |
|---|---|
| shape | draft → main, Fixes #147; assignee zhuangjianguo; mergeable clean |
| CI on the head | both Validate check-runs success on ce42ca5 = PR head.sha |
| scope | 4 files (+95/−12), the claim's surface: a declarative state_machine validation on clm_signature (insert only, initialStates draft / sent / completed) with its message in both bundles, and the hook's insert leg refusing an e-signature round created as completed (system writes exempt, as contract_type_stamp). Nothing governed; lint stays at 0 warnings |
| the card | a round born declined or voided is refused on REST, batch and import, in the caller's locale; draft, sent and the wet-ink completed round still save; the wet-ink path PR #145 measured (form → completed_at → F7 final version → activation) re-measured as Legal Counsel 1 and Records Manager 1; seed (30 rounds) and its replay untouched — the engine skips this rule type on seed replay |
| entry-state assumption | the e-signature connector (#83) creates its round as sent and completes it by update — recorded in the PR, to be re-checked when F8 is built |
| seat call on the open question | A: keep the hook half. Its English-only message under zh-CN is the platform's missing translation seam for hook refusals, already open upstream as objectstack-ai/objectstack#26170 |
| not in this PR | the create form's status picker still offers all five statuses (platform renderer, observation); signature-record.flow.ts's header comment says the machine guards updates only — stale since #145, comment-only, left for the next edit of that file |
Landing: under the maintainer's merge authorization (project thread: 「你可以自己审核的就自己合并」).
Generated by Claude Code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #147
Clause-②: no
Metadata-protocol: none
A signature round could be created in any status. As Legal Counsel 1 on
9248c18, a REST POST, a/api/v1/batchcreate and an import each accepted a round borndeclined,voided, or completed by e-signature (table below). The round's state machine judged updates only. This PR adds the insert check.The entry set, and how it was derived
A round may be created as
draft,sent, orcompletedwhen the method is wet ink. Everything else is refused.draft→sent/completed/voided(湿签直接 completed); sent→completed/declined/voided; declined→draft. The hook's update leg already treats a row with no previous status asdraft. An insert is therefore judged like the first edge out ofdraft, with one exclusion:draft → voidedabandons an existing round, and an insert has nothing to abandon.completedwith an executed copy (PR The signature-round form asks a lawyer to type JSON for signers, and "Completed At" promises a stamp the round never gets #145, measured again below). No flow insrc/flowscreates rounds; F7 only reads them. The demo seed createssent(4 e-signature, 2 wet ink) and 24 e-signaturecompleted.CLM_COMPOSITION(M4) #83 on Electronic-signature connector family:connector-esignwith DocuSign first, three fixed actions, consumed by apps as a declarativeprovider: 'docusign'instance objectstack#25747) is not built, so this is an assumption. Its dispatch registers the round when it sends the envelope, assent(ordraftfollowed by an update). The callback then updates that round byenvelope_id; it does not create a completed one. A callback that does create rounds while running as system is still exempt (see the hook half below).sentfor wet ink is allowed. §03'sdraft → sentedge does not depend on the method, and the seed's two wet-inksentrounds are this state: the counterpart is sealed and posted, and the copy is not back yet.What changed (4 files, metadata only)
signature_initial_status, a declarativestate_machinerule onclm_signature(src/objects/signature.object.ts):events: ['insert'],initialStates: ['draft', 'sent', 'completed']. Its message is in both bundles under_validationsinsrc/translations/{en,zh-CN}/objects.lifecycle.ts.signature_state_machinehook's insert leg (src/objects/contract.hook.ts). An e-signature round is not createdcompleted. System writes are exempt (ctx.session.isSystem), which is the same exemptioncontract_type_stampgives a contract created past draft. Thecompleted_atstamp from The signature-round form asks a lawyer to type JSON for signers, and "Completed At" promises a stamp the round never gets #145 is unchanged.Why a declarative rule, measured on 18.1.0
authoredRuleMessagereads_validations.signature_initial_status.messagefor the caller's locale. An Error thrown from a hook has no translation seam (A write can make a department the signing entity — our_entity's company-only filter shapes the picker, not the write #125 measured the same), so a hook refusal reaches every caller in English.shouldSkipStateMachineisctx.seedReplay || ctx.skipStateMachine, and it skips only thestate_machinerule type. This is the platform's own exemption for established facts (ExecutionContext.seedReplay;treatAsHistoricalimport, system principal only). The seed's 24 e-signaturecompletedrounds therefore still load. Ascriptrule is not skipped on seed replay, so it would have refused them.transitionsrepeats the hook's table exactly. The schema requires the key, andGET /api/v1/meta/object/clm_signature/state/status?from=sentanswers from it: measurednext: ["completed","declined","voided"]. Withevents: ['insert']the engine judges no update with it. The update leg stays in the hook, because one of its edges depends on the method. The comment says to change both together.Why the method half is a hook, and what that costs
A
state_machinerule reads one field, socompleted only when wet_inkcannot be written ininitialStates. Aconditionalthat wraps astate_machinebranch is not skipped on seed replay: the skip filters top-level rule types only. The same is true of ascriptrule. Either one would refuse the 24 seeded e-signature completed rounds. Only the hook can tell a system write apart on 18.1.0. The cost: this one refusal is English for a zh-CN caller, exactly like the existing update refusal for the same guard ("An e-signature round completes from sent, not from draft…"). The open question below asks how to close it.Evidence: real boots on 18.1.0,
pnpm demo --port 3147, clean.objectstack/dataeachSeed. Both boots, before
9248c18and afterce42ca5, gave{"inserted":830,"updated":0,"skipped":0,"errored":404,"referencesDropped":0}.clm_signatureread 30 rows on both:esign/completed 24 · esign/sent 4 · wet_ink/sent 2. The 404 errors are the same unresolved persona names the README describes.Boot warnings. I de-duplicated the WARN/ERROR/⚠/✗ signatures (timestamps and ids normalised): 425 before, 423 after. Nothing appears only after. The two lines only before has came from my own probes (a sign-in before the persona existed, and one malformed
sys_user_positionwrite), not from the boot.degraded,no such tableandfailed to loadmatch 0 lines.Re-running
pnpm demoon the same after database (the seed's upsert replay):{"inserted":0,"updated":74,"skipped":755,"errored":332}. No line namesclm_signature, the rule or the hook message.Persona: Legal Counsel 1, created the README way. As the dev admin:
create-user→ 200, thensys_user_position {position: clm_legal_counsel}→ 201. The account must change its temporary password:change-password→ 200, then sign-in → 200. Probe contract: DPA-2026-0002 (signing).9248c18ce42ca5draft/ status omitteddraft)sent/ wet_inksentcompletedcompleted_atstampedcompleted_atstampeddeclined/ wet_inkdeclinedVALIDATION_FAILED,fields[0] = {field: status, code: invalid_initial_state, constraint: {allowed: "draft, sent, completed"}}voided/ wet_inkvoidedaccept-language: zh-CNdeclined/voided签署记录只能以「草稿」或「已发出」状态新建;……completed(en and zh-CN)INVALID_STATE, "An e-signature round is not created as completed: …" (English on both)POST /api/v1/batchcreatedeclined(en) /voided(zh-CN) /draftPOST /api/v1/batchcreatecompletedINVALID_STATEformat: json, insert)declined, wet_inkvoided, esigndraftok=3 created=3ok=1 errors=2, each failed rowcode: invalid_initial_state, field: statusvoidedok=1ok=0 errors=1, the zh messagecompleted, wet_inkcompleted, esignsentok=2 errors=1, row 1INVALID_STATEExisting rows are not touched. These ran as Legal Counsel 1 on a seeded e-signature
sentround on SUP-2026-0011:PATCH {envelope_id}→ 200.sent → declined→ 200, thendeclined → draft→ 200.draft → completed→ 422, the unchanged update-leg message.draft → sent→ 200.The rule is insert-only, so a row stored in any status is never judged by it.
Browser (Chromium
/opt/pw-browsers/chromium, 1440x900, signed in as Legal Counsel 1):POST /api/v1/data/clm_signature {"method":"esign","status":"declined",…}→ 400VALIDATION_FAILED. The dialog stays open and the message appears under Status. The toast reads "Please check the highlighted fields: Status".POST /api/v1/data/clm_signature {"method":"wet_ink","status":"completed","executed_file":"…"}→ 201. Toast: "Signature created successfully".completed_at 2026-10-26T14:47:32.586Z.final_signedv1 version exists, holding the uploaded file.clm_records_manager):POST /api/v1/actions/clm_contract/activate_contract→ 200. The contract readsstatus active,executed_at = signed_at = 2026-10-26T14:47:32.586Z(the round's completion), andactivated_at 14:47:45.545Z.record.statusheader-action predicate warnings (recorded onclm_contract.departmentis a fixed option list — make it a lookup to the platform'ssys_business_unit, as #20 ruled forour_entity#128), plus the 400 / 422 of the refused POSTs themselves. Nothing else.NOT MEASURED
completedround. Those rounds hang offactivecontracts. As the dev admin,PATCH clm_signature/IDanswered 403, and per the persona rule no other route was taken. The rule isevents: ['insert'], so by construction it never judges an update.treatAsHistorical). It is system principal only, and no signed-in persona can send it. Per the engine source it setsskipStateMachine, and its writes are system writes.runAs: systemflow creating rounds. No such flow exists until F8 (Card 12 — F8 e-signature connector · F15 HotCRM hand-off behindCLM_COMPOSITION(M4) #83).Gates (head
ce42ca5, each exit captured to a file before any pipe)The i18n gate does not walk validation messages, so the zh-CN message was proven at runtime: REST POST, batch, import and the browser form each return the zh text to a zh-CN caller.
Acceptance notes
Open question: the e-signature-completed refusal is English for a zh-CN caller. Three options:
completedbecomes a legal birth for a person, which makes the update leg'sdraft → completedguard bypassable through an insert.scriptrule (translated): it would refuse the 24 seeded rounds, so it needs the seed to change, which §10 does not allow.Recommendation: A, because it keeps §03's method guard on every door, and the language gap is the platform's, the same gap A write can make a department the signing entity — our_entity's company-only filter shapes the picker, not the write #125 recorded.
The create form still offers all five statuses (Draft, Sent, Completed, Declined, Voided). On 18.1.0 the Console's status picker does not read
initialStates. The write refuses, and the message lands under the field.Two copies of the transition table now exist: the hook's (enforcing updates) and the rule's (introspection only). The rule's comment names the hook's table as its source and says to change both; the hook's header names the rule.
The F7 flow header (
src/flows/signature-record.flow.ts) still sayssignature_state_machine"guardsbeforeUpdateonly". That has not been true since The signature-round form asks a lawyer to type JSON for signers, and "Completed At" promises a stamp the round never gets #145. It is not in this card's file surface, so it is left unchanged.No data was rewritten. The probe personas and rows were written to throwaway local databases only.
🤖 Generated with Claude Code
https://claude.ai/code/session_017nzWFRUo2U7evMkPdVniAn