Skip to content

In the demo, the administrator adjudicated every deviation legal ever raised: clm_deviation.decided_by is DEMO_USER on every decided row - #66

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-61-deviation-decided-by
Sep 10, 2026
Merged

zhuangjianguo merged 1 commit into
mainfrom
claude/issue-61-deviation-decided-by

Conversation

@zhuangjianguo

Copy link
Copy Markdown
Collaborator

Fixes #61

clm_deviation.decided_by named the dev admin on every decided row, so the demo's
决定人 column said the administrator adjudicated every clause deviation legal ever
raised. This deals the column to a lawyer, by the same counterparty relationship
#52 (PR #65) established for clm_obligation.owner.

What changed

File Change
src/data/keys.ts new deviationDeciderOf(deviation) — null while open, else counselFor(contract); DEMO_USER's doc block and counselFor's doc block re-enumerated
src/data/negotiation.seed.ts decided_by: deviationDeciderOf(deviation) replaces ... ? null : DEMO_USER
README.md the exact-name table's Legal Counsel row now also names the 17 decided deviations (10 / 7); "all three columns" is now four
scripts/demo.mjs the DEMO_USER mirror's NOT-list gains clm_deviation.decided_by; "all three" is now four

Object, playbook, F2 and the stamping hook are untouched: the field and its
behaviour are correct as built.

The premise, re-measured on this branch first

Card readings were the PM seat's on a stale checkout. Taken again from the
running system at 3d11db8, clean database, one pnpm demo:

GET /api/v1/data/clm_deviation -> 200 {"total":25}
deviations by status: {"open":8,"accepted":11,"rejected":4,"withdrawn":2}
decided_by over decided rows: {"Dev Admin":17}
row census: contract 120 · party 40 · type 9 · clause 30 · rule 6 · review 60 ·
            deviation 25 · signature 30 · obligation 200 · payment_plan 300  = 820

Premise holds: 17 decided, every one of them the dev admin, the 8 open ones NULL.

Why counselFor and not legalOwnerOf — the real choice on this card

Both name a lawyer; they differ exactly on the rows where F2 never assigned one.
Enumerated over the plan (not sampled): the 17 decided deviations sit on 17
distinct contracts, and 3 of those 17 are of a type whose requiresLegalReview
is false
— contracts #17 and #48 (in_approval) and #44 (approved) — so
legalOwnerOf returns null for them.

Dealing by legalOwnerOf would therefore leave 3 rows that are accepted/rejected,
carry a decided_at, and name nobody. The write layer never produces that pair:
deviation_state_machine (src/objects/contract.hook.ts) stamps decided_by with
the acting user on any decision that leaves the column empty. The fixture's standing
rule is that no seeded row sits in a state the write layer would have refused or
would never have written — the same rule that keeps all 8 open deviations on
in_review contracts because F6 would not let an in_approval contract carry one.

counselFor claims only what the row needs — the counterparty's lawyer ruled on the
clause — and not the extra fact legalOwnerOf carries, that the contract went
through a legal review at all. That is the distinction keys.ts already draws, and
the same reason obligationOwnerOf deals compliance work by counselFor.

Grounding for "a lawyer at all": DESIGN.md §04 gives clm_requester only
RC(本人合同) on clm_deviation and clm_legal RCU — the only non-admin set
with U. Moving a deviation out of open is a lawyer's write and nobody else's.

No deliberate unassigned set here, and that is not the #52 half being dropped
carelessly. legalOwnerOf and obligationOwnerOf each keep one row empty because a
flow takes a "nobody to tell" edge over that column (F3, F10). Nothing reads
decided_by: it is written once and displayed. An empty one would exercise nothing
and would instead assert the one thing the column cannot mean.

No new account. The two Legal Counsel names README already asks for, 10 / 7.

Deals by structure — both locales identical, row for row

The deal reads partyIndex only; no locale bundle is consulted.

node locale-rows.mjs > rows-en.txt ; OS_SEED_LOCALE=zh-CN node locale-rows.mjs > rows-zh.txt
diff rows-en.txt rows-zh.txt ; echo $?   ->  0
md5sum   05b0ec3a482bc496db4bbb2646ca8307  rows-en.txt
         05b0ec3a482bc496db4bbb2646ca8307  rows-zh.txt

Gates — exit codes captured before any pipe

pnpm validate       > gate-validate.log  2>&1; V=$?   ->  V=0    ✓ Validation passed (970ms)
pnpm lint           > gate-lint.log      2>&1; L=$?   ->  L=0    21 warning(s), 5 suggestion(s) (918ms)
pnpm typecheck      > gate-typecheck.log 2>&1; T=$?   ->  T=0    tsc --noEmit, no diagnostics
pnpm lint:i18n-gate > gate-i18n.log      2>&1; I=$?   ->  I=0    ✓ i18n gate · 0 missing keys across 2 locale(s)

The 21 lint warnings are the pre-existing field-no-consumers family on
clm_clause, clm_contract_type, clm_contract_version, clm_review,
clm_signature and clm_deviation.deviation_text / .justification — fields this
diff does not touch. decided_by is a page column, so it has a consumer.

Control-character self-scan over the four changed files, beyond the repo gates:
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' -> no match.

Verified in the browser and on the wire

Chromium at /opt/pw-browsers/chromium-1194/chrome-linux/chrome, console locale
zh-CN, demo on a non-default port 3517. Three boots.

Boot 2 — post-change, accounts absent (the #65 check). Clean database:

sys_user total=1: Dev Admin
GET /api/v1/data/clm_deviation -> 200 {"total":25}
decided_by over decided rows: {"NULL":17}      open rows carrying a decided_by: 0
row census sum = 820   (unchanged, row for row)

The row survives, as an optional reference must. The loader says so per row:

ERROR [SeedLoader] Deferred reference UNRESOLVED after pass 2 — clm_deviation.decided_by
stays NULL ... nothing links it to sys_user.name = 'Legal Counsel 1', because no such
sys_user row exists {"object":"clm_deviation","field":"decided_by","target":"sys_user.name"}

Loader ERROR lines: 388 before this change, 405 after, and the delta is exactly the
17 new clm_deviation.decided_by stays NULL lines — same class as the 120 owner_id,
68 legal_owner and 199 obligation.owner lines already there, and the demo's closing
note already frames them.

Boot 3 — the operator step, then the upsert handover. Legal Counsel 1 and
Legal Counsel 2 created through Setup 人员与组织 › 用户 › 创建用户 (no seeded users;
DESIGN.md §10 respected), then pnpm demo again on the same database:

sys_user total=3: Legal Counsel 1 | Legal Counsel 2 | Dev Admin
decided_by over decided rows: {"Legal Counsel 1":10,"Legal Counsel 2":7}
open rows carrying a decided_by: 0     decided rows with no decided_at: 0
row census sum = 820
loader ERROR lines: 405 -> 264 — the 17 decided_by lines gone, the 68 legal_owner
gone, obligation.owner 199 -> 143 (the 56 compliance rows), which is README's
34 / 34 and 29 / 27 arriving exactly as the table promises

The reading a person gets. Signed in, HotCLM › 偏离 › Insurance cover · Accepted
(on Supplier Agreement — Bramble Office Supplies, SUP-2026-0011):

决定  ·  决定人   [LC]  Legal Counsel 1
状态  已接受      决定时间  2026/9/3

Before this change that field read Dev Admin. No scheduled job was run: nothing
sweeps this column.

Enumerated, not sampled

  • grep -rn DEMO_USER src after the change: the declaration, four cross-references
    in keys.ts prose, clm_review.reviewer, and the our-side entry of
    clm_signature.signers (a JSON string, not a reference). decided_by is gone from
    that set, so the doc block that named it was rewritten rather than left stale.
  • README's "clm_review.reviewer is the one reference that still has to name the
    dev admin" — read and confirmed, not assumed. Enumerating every user reference in
    src/objects/: clm_contract.owner_id, clm_contract.legal_owner,
    clm_contract_version.submitted_by, clm_obligation.owner,
    clm_deviation.decided_by, clm_review.reviewer. Only reviewer carries
    required: true + storage.notNull, so the claim stays true.
  • counselFor's own doc said "both columns that name a lawyer". With this change
    they are three, so that sentence now enumerates them.

Acceptance notes

  • Platform gap, reported not patched. In a record:related_list the user columns
    render the raw sys_user id rather than the account name: on the contract page's
    审查与偏离 tab, 决定人 shows 8fDg2i1MlSIuG16syiHbJfHEsPcPBjW3 and the review grid's
    审查人 shows kO22t1Gio3hllWxLo22L3Ejs4FzXfANl, while the same fields on a record
    page render [LC] Legal Counsel 1. Platform @objectstack/* 17.4.0, console
    zh-CN. Pre-existing and independent of this card (it hits reviewer too);
    reported for the PM seat to route to objectstack-ai/objectstack per AGENTS.md.
  • No changeset: this repo has no changeset gate.

🤖 Generated with Claude Code

https://claude.ai/code/session_01R3n3GGzobdegM4HUzah1iR


Generated by Claude Code

Every decided deviation in the demo named the dev admin, so the 决定人 of
each one said the administrator adjudicated the clause playbook. Measured
on 3d11db8, one clean-database `pnpm demo`: 25 deviations, 17 decided,
`decided_by` resolving to `Dev Admin` x17.

`decided_by` is an audit stamp rather than a recipient — nothing is routed
to it — so the cost is not a broken notification path but a column that
contradicts the model: DESIGN.md §04 gives `clm_legal` the only non-admin
`U` on `clm_deviation`, so moving one out of `open` is a lawyer's write.

The deal is `counselFor`, the counterparty relationship the fixture already
uses for `legal_owner` and for compliance obligations, and NOT `legalOwnerOf`:
enumerated on this fixture, 3 of the 17 decided deviations sit on contracts
of a type whose `requiresLegalReview` is false, where `legalOwnerOf` returns
null. That would leave 3 rows decided, carrying a `decided_at`, and naming
nobody — a pair `deviation_state_machine` never writes, since it stamps the
acting user on any decision that leaves the column empty.

No new account: the same two `Legal Counsel` names README already asks for,
10 / 7 across the 17. No deliberate unassigned set either — no flow reads
this column, so an empty one exercises no edge and would assert the one
thing the column cannot mean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3n3GGzobdegM4HUzah1iR
@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 10, 2026 09:36
@zhuangjianguo
zhuangjianguo merged commit 713b25b into main Sep 10, 2026
2 checks passed
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.

In the demo, the administrator adjudicated every deviation legal ever raised: clm_deviation.decided_by is DEMO_USER on every decided row

2 participants