Repository navigation
A write can put a company or office in a contract's department — the department-kind filter shapes the picker, not the write - #130
Merged
Conversation
clm_contract.department's lookupFilters shape the picker only: a REST PATCH or POST, /api/v1/batch, an import, or a headless launch_contract stored a company or an office as the requesting department (#129). A conditional rule, the #125 pattern: `when` admits only a write that sets or changes department to a value (null and "" are skipped, since a hop through an empty reference has no row and the engine refuses that write as unevaluable), and the cross_field `then` refuses a unit whose kind is not department. It does not judge `active`, so a renewal that copies a department retired since still inserts. Message in en and zh-CN under _validations. 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 #129; assignee zhuangjianguo; mergeable clean against main abd5401 |
| CI on the head | both Validate check-runs success on 32eef5b = PR head.sha |
| scope | 3 files (+47/−5), exactly the claim's surface: one conditional validation (contract_department_set → contract_department_is_department), its message in both bundles, the department comment. Nothing governed |
| the card | kind judged, active not — as the card set it. Refused on every door measured (PATCH, POST, batch, import insert/update, headless launch_contract), as a requester persona and as the admin; department, null and "" accepted; a retired department neither blocks other edits nor an F12 renewal; a row holding a pre-rule company stays editable on other fields. The "" guard in when is measured, not guessed (without it a clear-the-field PATCH was refused) |
| not in this PR | the flow door answers in English under zh-CN — the same platform gap #125 hit, already filed as objectstack-ai/objectstack#25809; a REST "" on a lookup is stored as "", not null (platform behaviour, observation only) |
Landing: under the maintainer's merge authorization (project thread: 「你可以自己审核的就自己合并」).
Generated by Claude Code
zhuangjianguo
marked this pull request as ready for review
October 25, 2026 15:56
This was referenced Oct 25, 2026
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 #129
Clause-②: no
Metadata-protocol: none
clm_contract.department'slookupFilters(kind = department,active = true) only shaped the picker. A REST write,/api/v1/batch, an import or a headlesslaunch_contractcould store a company or an office as the requesting department. This PR adds the write-side rule, the #125 pattern with the card's one difference: it judgeskindonly, neveractive.What changed
Metadata only, three files: one validation rule on
clm_contract(src/objects/contract.object.ts) and its message in both bundles.contract_department_set(conditional).whenisrecord.department != null && record.department != "" && (previous == null || previous.department != record.department). It is true on an insert or an update that sets the department to a unit, or changes it to a different one.contract_department_is_department(cross_field,fields: ['department']) is thethenbranch. Its condition isrecord.department.kind != "department". The error is attached to thedepartmentfield.objects.clm_contract._validations.contract_department_is_department.messageis in en and zh-CN, next to A write can make a department the signing entity — our_entity's company-only filter shapes the picker, not the write #125's key.departmentfield comment no longer says that no write-side rule exists. It names the rule, and says whyactiveis not judged.Why the empty guard sits in
when(measured on 18.1.0). A hop through an empty reference has no row to read, and the engine refuses that write rather than passing it.nulland""both need the guard, because the engine stores a REST""on this lookup as given. I first booted the rule with only thenullhalf.PATCH {department: ""}then answered 400rule_violation"Validation rule 'contract_department_is_department' could not be evaluated (cannot read 'kind' throughdepartment(object 'sys_business_unit'): the related record was not found)". With both halves it answers 200, the same as on main.Why
activeis not judged. The card says so: the department is an optional reporting field, and F12 / F13 renewals insert a new contract that copies it. The renewal leg below shows that a retired department still renews.Evidence: real boots on 18.1.0,
pnpm demo --port 3129, clean.objectstack/dataeachSeed counts. Both runs gave
{"inserted":830,"updated":0,"skipped":0,"errored":404,"referencesDropped":0}: mainabd5401, and this branch with the rule (head32eef5b). The 404 errors are the same unresolved persona names on both sides.Boot warnings. I de-duplicated the WARN/ERROR/⚠/✗ signatures, with timestamps, ids and timings stripped:
contract_numberread-only drop that the import-update probe caused, not a boot line.Requesting Departmentmatches 0 lines of the after boot, so no seed row trips the rule.degraded,no such table,failed to loadandFailed to expandmatch 0 lines in both logs.The persona, created the README way.
POST /api/v1/auth/admin/create-userBusiness Requester 1→ 200 (mustChangePassword: true).POST /api/v1/auth/change-password→ 200, then sign in → 200,role=user.Every row marked requester below ran signed in as that account. Rows marked admin ran as the dev admin.
Probe units. Two units were created through the API as admin:
PROBE-OFFICE(kind office, 201) andPROBE-RETIRE-DEPT(kind department, active, 201). Companies are the seededARC-EU, and departments are the seededARC-LEGAL/ARC-SALES/ARC-FIN.abd5401launch_contract, headlessFLOW_FAILED"… create_record(clm_contract) failed: Requesting Department must be a department …", 0 rows with that titlelaunch_contract, headlesslaunch_contract, headlessnulllaunch_contract, headlessPATCH(requester's own draft)VALIDATION_FAILED,fields[0] = {field: "department", code: "rule_violation"}, department unchangedPATCH,accept-language: zh-CN发起部门必须是部门,即类型为部门的业务单元。……PATCHnull/""/ ARC-LEGAL / summary onlyPOST /api/v1/batchupdate (the form's write path)POST /api/v1/batchupdatePOSTPOSTPOSTnull/""PATCH(seeded draft SOW-2026-0004)PATCH(seeded draft SOW-2026-0004)nullPOST /api/v1/batchcreateformat: json)ok=1 created=1eachok:false, code: rule_violation, field: department, nothing createdok=1 created=1发起部门必须是部门……ok=1 created=1ok=1 created=1writeMode: updateoncontract_number)ok=1 updated=1, company storedok:false rule_violation, department unchangedok=1 updated=1ok=1 updated=1A department retired after it was set. This ran on the requester's own draft:
PATCH {department: PROBE-RETIRE-DEPT}→ 200 while the unit is still active.PATCH sys_business_unit {active:false}→ 200.PATCH {title}→ 200;PATCH {department: SAME_RETIRED_ID, summary}→ 200 (re-sending it is not a change); batch{summary}→ 200. The department still reads PROBE-RETIRE-DEPT throughout. Main gives the same four answers.F12 renewal carrying a retired department. As admin,
PATCH sys_business_unit ARC-PEOPLE {active:false}→ 200. That is the department of active contract ICA-2026-0002. ThenPOST /api/v1/actions/clm_contract/start_renewal {recordId}→ 200, and the drafts carrying ARC-PEOPLE went from 0 to 1. The renewal draft inserts with the retired department, the same as on main.A company stored before the rule is not re-judged. This ran on the after server through its watcher.
I reverted the three files to
abd5401and confirmed that the compiled artifact held 0 references to the rule. As the requester,PATCH NDA-2026-0022 {department: ARC-EU}→ 200, so the company is stored.I restored them with
git checkout HEAD -- ….git diff HEADwas empty, each blob hash matched HEAD, and the artifact held the rule again.As the requester:
PATCH {title}→ 200;PATCH {department: ARC-EU (same), summary}→ 200;{summary}→ 200;PATCH {department: PROBE-OFFICE}→ 400.The company stays stored until a write changes it.
An incidental reading from the first try of that revert: with the rule removed but its bundle keys left in place, the compile is refused by
translation-target-unknown("Translations are keyed to validation rule "contract_department_is_department", which object "clm_contract" does not declare"). So the bundle key is bound to the rule's name by a gate.Browser (Chromium
/opt/pw-browsers/chromium, signed in as Business Requester 1, en). This ran on NDA-2026-0022, whose department is the company ARC-EU stored before the rule:REQUESTING DEPARTMENT | Arcadia Europe GmbH.POST /api/v1/batch {"operations":[{"object":"clm_contract","action":"update","id":…,"data":{"title":"P129 browser edit — department untouched"}}],"atomic":true}→ 200. The toast reads "Contract updated successfully". The form sends only the changed field, so a legacy department does not block a form save.GET /api/v1/data/sys_business_unit?…&filter=["and",["kind","=","department"],["active","=",true]]→ 200. The picker offered exactly Finance · Procurement · IT · Operations · Legal · Sales · People & HR. It offered no company, not PROBE-OFFICE and not the retired PROBE-RETIRE-DEPT.POST /api/v1/batch … {"department":"…"}→ 200 with the toast "Contract updated successfully". The record then readsREQUESTING DEPARTMENT | Legal.get-session401, one resource 404, and the three pre-existingrecord.statusheader-action predicate warnings, the same asclm_contract.departmentis a fixed option list — make it a lookup to the platform'ssys_business_unit, as #20 ruled forour_entity#128 recorded. There was no new error.NOT MEASURED
RENEWAL_DRAFT_FIELDScopy as the F12 path measured above, and the rule does not judgeactive.executed_upload). It needs the records persona'sexecute_contractcapability, which was not set up. Itsdepartmentparam is field-backed, and its create goes through the engine like the other doors.Gates (head
32eef5b, each exit captured to a file before any pipe)These are the same commands
ci.ymlruns. The i18n gate does not walk validation messages, so the zh-CN message was proven at runtime: REST PATCH and POST, batch and import each return the zh text to a zh-CN caller.Acceptance notes
launch_contractwithaccept-language: zh-CNreturnedFLOW_FAILEDcarrying the English rule message, while the record doors return the zh text. A write can make a department the signing entity — our_entity's company-only filter shapes the picker, not the write #125 recorded the same behaviour forcontract_entity_is_active_company: the flow'screate_recorddoes not carry the caller's locale. This is platform behaviour, and nothing changes here. Reach is low, because the launch dialog's picker offers departments only.""on this lookup is stored as"", notnull. That is engine behaviour on main and on this branch alike. The rule passes it as "empty", per the card. Whether a lookup should normalise""tonullis not this card's question.clm_contractwrite whose row names a department now makes one more batched, one-column read ofsys_business_unit(kind). The engine collects a rule's hop per schema, so the read happens even whenwhenis false, as A write can make a department the signing entity — our_entity's company-only filter shapes the picker, not the write #125 noted forour_entity. A write whose department is empty reads nothing for it.messageis never surfaced. As in A write can make a department the signing entity — our_entity's company-only filter shapes the picker, not the write #125, the engine returns thethenbranch's violation, so onlycontract_department_is_departmentis translated.lookupFilters,contract_entity_set, and every existing row. No data is rewritten. Thedepartmentfield'sdescriptionand help are untouched, since the Implementation notes are shown to users as help text — field and dashboard-widget descriptions cite hook files, card numbers, DESIGN.md sections and PRs #95 remainder is on descriptions.🤖 Generated with Claude Code
https://claude.ai/code/session_017nzWFRUo2U7evMkPdVniAn