Repository navigation
Contract domain: clm_contract and its four review-time children (card 02, M1) - #5
Merged
Merged
Conversation
Contract domain of DESIGN.md §03: clm_contract (private, title as nameField, hook-generated contract_number per §13 Q5) plus the four master-detail children clm_contract_version, clm_review, clm_deviation and clm_signature (controlled_by_parent, stored display_name mirrors). Type-derived stamps, stage timestamps, routing flags, the ai_* fields and is_backfilled are readonly with no writer here; the hooks that stamp them land in the next commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcrVDXSptwDukFsHPHPR1V
…hooks contract.hook.ts: a contract is born draft (system writes exempt, for the F16 backfill and seed replay); category, direction and execution formalities are copied from the type; the number is TYPE_CODE-YYYY-0000 per type per year (max existing + 1, scoped to the organization, DESIGN.md §13 Q5). The status transition table of §03 and every guard — intake fields, blocked party, first version or template, legal owner, open deviations, approved legal review, current clean version, completed signature covering the type's formalities plus a final_signed version — refuse with code INVALID_STATE / status 422 and stamp the stage timestamp on entry. Deviation and signature machines in the same file. mirror.hook.ts: stored display_name mirrors for the four children. hooks.ts + objectstack.config.ts: hooks are a top-level stack key, so the barrel and the one wiring line are what registers them; every one of the 8 handlers lowers to a metadata-only body (no runtime bundle). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcrVDXSptwDukFsHPHPR1V
This was referenced Sep 7, 2026
hotlong
marked this pull request as ready for review
September 7, 2026 12:34
This was referenced Sep 7, 2026
Closed
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 #2
What changed
The contract domain of DESIGN.md §03: five objects, two hook files, and the one line in
objectstack.config.tsthat registers the hooks.Objects —
src/objects/contract.object.ts—clm_contract:sharingModel: 'private',nameField: 'title', iconfile-signature, 58 fields: every field of §03 plus the card'sgoverning_law/jurisdiction/contract_language. Readonly with no writer in this card: the type-derived stamps (contract_number,category,direction,execution_formalities— written by the hook below), the stage timestampssubmitted_at…closed_at,route_*/approval_status/is_expiring/executed_at/archived_at(flows), the fourai_*fields (the adopt-suggestion action only, §07) andis_backfilled(F16, card 09; §13 Q8). Unique index(contract_number)scopeorganization. No roll-ups (card 03).contract-version.object.ts·review.object.ts·deviation.object.ts·signature.object.ts—controlled_by_parent,nameField: 'display_name'(a stored text mirror, never a formula),contract=Field.masterDetail('clm_contract')withdeleteBehavior: 'cascade',inlineEdit: 'grid'and the inline titles from the card (Versions / Reviews / Deviations / Signatures); iconsfile-stack/gavel/git-branch/pen-line. Fields and enums as §03;clm_signature.formalities_donecarries the same value set asclm_contract_type.execution_formalities, so sealing stays a formality value and not a module (§13).index.ts— the five exports under the "Contract domain" comment,Contractfirst.Hooks
contract.hook.tscontract_type_stamp(beforeInsert / beforeUpdate,runAs: 'system'for the sequence read — the object isprivate, so an inherited context would number per user): a non-system insert must be borndraft(F16 backfill and seed replay are system writes and exempt);category,direction,execution_formalitiesare copied from the type;contract_number=TYPE_CODE-YYYY-0000, one sequence per type per year, next = max existing + 1 scoped to the organization (§13 Q5 —autonumbercannot express it). Changing the type is allowed on a draft only and re-stamps all four.contract_state_machine(beforeUpdate onstatus): the §03 transition table and every guard — required intake fields of the type, party set and notblocked, a first version or a type template,requires_legal_reviewrouting,legal_ownerbefore review, noopendeviation and anapprovedlegal review before approval, a currentcleanversion before signing, acompletedsignature whoseformalities_donecovers the contract'sexecution_formalitiesplus afinal_signedversion before activation,expiredreserved for the system (F13),closed_aton termination. Every refusal is a structured errorcode: 'INVALID_STATE',status: 422(the ledger's code for "understood, but the record is not in a state that allows this"); nothing is coerced. Stage timestamps are stamped on entry.deviation_state_machine(open → accepted / rejected / withdrawn, decided states final; stampsdecided_at/decided_bywhen empty) andsignature_state_machine(draft → sent / completed / voided, sent → completed / declined / voided, declined → draft; an e-signature round must besentbeforecompleted, wet ink may complete from draft; stampscompleted_atwhen empty).mirror.hook.ts— the fourdisplay_namemirrors:v3 · Clean,Legal · Lena Legal(reviewer name read fromsys_user, id fallback),Limitation of liability · Accepted(clause title),E-signature · Completed.hooks.ts— the flat hook barrel;_hook-api.ts— the type-only shape ofctx.api(HotCRM's key sets, measured on 17.3.0). Every helper lives inside its handler:objectstack buildreports "all 8 callables are body-only" and emits no runtime bundle.objectstack.config.ts—hooks: allHooks. One import and one key outside thesrc/objects/file surface of the claim: a metadataHookis registered only from the top-levelhooksstack key (ObjectSchemarefuses ahookskey with a prescriptive message), so the wiring line is the difference between the state machine existing and not.Gates — HEAD
31a80f3pnpm validate:pnpm lint:The one suggestion (info level, exit 0) is the
version_countroll-up — card 03. Notitle-unresolvable,missing-name-field,security-role-word, orhook-body/not-lowerable.pnpm typecheck:(exit 0.)
pnpm build(not a gate, run for the lowering evidence):→ Skipping legacy runtime bundle (all 8 callables are body-only);dist/objectstack.jsoncarries 8 hooks withbody.language: 'js',runAs: 'system'oncontract_type_stamp, inferred capabilityapi.readon the four hooks that read.Real-engine run (one-off evidence, not a repo file)
A scratch script booted a real ObjectQL engine on the memory driver with the nine objects (plus a minimal
sys_user), bound the hooks withbindHooksToEngine, and walked the whole lifecycle twice: once with the in-process handlers fromobjectstack.config.ts, once with the LOWERED bodies fromdist/objectstack.jsonexecuted by the real QuickJS sandbox (hookBodyRunnerFactory(new QuickJSScriptRunner()), the production shape). Both surfaces: 42 / 42 PASS. Both card acceptance lines are in it, refused withcode=INVALID_STATE status=422:(The NOTE line: on 17.3.0 the INSERT-side
readonlystrip runs at the DataProtocol ingress —stripReadonlyForInsertin metadata-protocol / rest — which an engine-direct harness bypasses; the objectql copy of that strip is newer than the pinned release. The hook-written stamps on readonly fields survive on both paths:contract_numberoverrode a forged caller value on insert,submitted_atetc. landed on update.)The REST door itself was not driven:
objectstack startrefuses to boot a data API on a stack that mounts noauthtier (requires: ['ui']here; addingauthis a capability expansion the card does not grant). What REST adds over the engine is the envelope mapping, which reads exactlystatusand a ledger-registeredcodeoff the thrown error — both set on every refusal.Measurements against the dispatch assumptions
crm_contractcross-package lookup — the assumption "validate will refuse" is falsified, but so is the card's condition. Withcrm_contract: Field.lookup('crm_contract', …)declared,pnpm validateandpnpm lintboth exit 0 with no diagnostic (144 fields). Control probe: a lookup tozzz_object_that_does_not_existalso passes both gates with no diagnostic. Sovalidatedoes not evaluateField.lookup.referencetargets at all (@objectstack/lint'svalidate-object-referencescovers action params, dashboards, datasets and navigation;defineStack's cross-reference check covers hooks, views, seeds, mappings, grants, nav and actions — not field references). "Accepts" is therefore the absence of a check, not a verdict on cross-package references, and a dangling lookup is a silent runtime failure (record picker 404,$expandfailure) in the standalone composition. The field is left out — the same choice card 01 made forclm_party.crm_account— and the question is in the report'sopen_questionswith a recommendation (declare it in card 12 behindCLM_COMPOSITION). Adding it later is a five-line change.rollup/missing-summary, info level, exit 0).code+statuspropagate through both the in-process wrapper and the QuickJSSandboxError;INVALID_STATEis a ledger member, so the REST mapper keeps it (an unregistered spelling would be demoted todeclaredCodeandcodederived from the status).Data: 9 Objects 143 Fields.Decisions taken where the card left room — please confirm in review
our_entity: one placeholderhead_office(a group's legal entities are configuration, not a taxonomy the standard product can guess — the description says so).department: sales / procurement / legal / finance / operations / people / it / other.contract_language:endefault + zh / ja / de / fr / es (mirrors the currency list's spread).approval_status: not_required (default) / pending / approved / rejected — HotCRM's approval mirror.payment_terms: HotCRM's set (net_15 / net_30 / net_60 / net_90 / due_on_receipt) so the F15 hand-off maps 1:1.provider: docusign / adobe_sign / dropbox_sign. NoteField.selectlowercases every option value, soUSDis stored asusd(AGENTS.md's lowercase rule).draft(the FSM entry point; otherwise a REST create could be bornactive). System writes are exempt, which is how F16 and seed replay enteractivedirectly.in_review → draftrequires a review with decisionchanges_requested(orrejected) — my reading of §03's "审查 changes_requested" as a checkable guard rather than an actor note.in_approval → approved / rejected / draftcarry no guard here: the approval ladder (F5, card 06) owns those decisions.signing → active: §03 also requiressigned_atnon-empty;signed_atis readonly and no flow stamps it yet, so the hook stampssigned_atandexecuted_atfrom the completed signature'scompleted_at(andactivated_at= now) on activation. F7 (card 06) may stamp them earlier; the guard stays satisfiable without it.active → expiredis refused for a non-system caller (§03: "仅日任务 F13"); terminate is the manual exit.(contract, version_no)onclm_contract_version— an integrity constraint implied byversion_no, not pinned in §03.lookupFilters): active types, non-blocked parties, active clauses — convenience only; the hook is the rule.验收备注
active → terminatedguard says "closed_at 与终止原因必填", but the §03 field list declares no termination-reason field; this card stampsclosed_atonly. Atermination_reasonfield (or a note thatsummarycarries it) is a DESIGN.md §03 edit — outside a code PR without a decision.clm_party.crm_accountlookup as "已落地", butparty.object.tsdeclares no such field (card 01 left the cross-package lookup out, as this card does forcrm_contract).packages/specships a declarativestate_machinevalidation rule (transitionsmap,initialStates,legalNextStates()introspection). The table lives in the hook per the card; mirroring it declaratively later would let the UI grey out illegal transitions — worth a card, not a rider, and only with one source of truth.objectstack validate/lint/buildaccept aField.lookup/Field.masterDetailwhosereferencenames an object that exists nowhere (control probe above), so the metadata fails only at runtime. Per AGENTS.md "Platform gaps: report, never patch". Search-before-file is recorded in the report.rollup/missing-summarysuggestion is card 03'sversion_count.🤖 Generated with Claude Code
https://claude.ai/code/session_01KcrVDXSptwDukFsHPHPR1V
Generated by Claude Code