Skip to content

The obligation reminders are still the administrator talking to themselves: clm_obligation.owner is the dev admin on all 200 seeded rows - #65

Merged
zhuangjianguo merged 3 commits into
mainfrom
claude/issue-52-obligation-owner
Sep 10, 2026
Merged

zhuangjianguo merged 3 commits into
mainfrom
claude/issue-52-obligation-owner

Conversation

@claude

@claude claude Bot commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #52

Data only. Five files: three in src/data/, plus README.md (the operator table) and scripts/demo.mjs (its comment and its failure text named columns that stopped naming the dev admin at #47). ⛔ No file under src/flows/, no object, no hook, no DESIGN.md. F10 is untouched — it does exactly what card 09 built it to do.


First: the card's premise, re-measured on main

The card's reading was taken on claude/issue-47-seed-legal-owner. Re-taken here on origin/main @ 2d63324, clean database, pnpm demo on port 3152, README operator setup performed, re-seeded so the upserts hand the rows over, one trigger of each of the six daily jobs through POST /api/v1/automation/NAME/trigger, receipts read from the driver's own SQLite file (node:sqlite, read-only):

clm_obligation.owner:  Dev Admin  x200      (all of them)

receipts by topic x recipient — 45 receipts, 27 notifications
  clm_legal_review_over_30_days  ->  Legal Counsel 2 x3, Legal Counsel 1 x2
  clm_obligation_due_soon        ->  Dev Admin x2                      <- this card
  clm_payment_overdue            ->  Finance Controller x18, Business Requester 1 x7, 2 x6, 3 x5
  clm_turn_stalled               ->  Business Requester 1 x2

The premise holds, exactly as written. 200 of 200, and F10's two are the only receipts in the whole book still addressed to the dev admin now that #26 and #47 have landed.

The deal: route A for the book, route B for one row

owner is optional, so it may name accounts that do not exist yet. It now names them by what the obligation is:

kind rows goes to why
compliance 56 the lawyer who knows that counterparty — Legal Counsel 1 / 2, 29 / 27 §04 gives clm_legal RCU on clm_obligation, the only non-admin set that may create one, and all four seeded compliance titles are legal work: an insurance certificate, a sanctions re-screen, a data-transfer safeguard confirmation, an anti-bribery statement
deliverable · report 143 the requester who launched the contract — Business Requester 1 / 2 / 3, 50 / 53 / 40 §04 gives clm_requester RU(本人负责), and §05 puts 我负责的履约 in the 我的合同 group every employee reaches. A delivery and a quarterly report are the launching desk's own work
(one row) 1 nobody, deliberately see below

Both halves deal by the counterparty relationship — the identical rule ownerOf and legalOwnerOf already use, and structure rather than prose, so demo-en and demo-zh deal identically. It names no new account: all five are already in README.md's exact-name table, which #55 built for exactly this. The table's two rows gain what they now also receive; no sixth name, and the other five positions stay "named however you like".

⚠️ Why it is not simply the contract's owner_id

DESIGN.md §03 declines to default this column to the contract owner, and _daily-sweep.ts repeats that permission where it explains why hasRecipient exists. Putting all 200 on the launching requester would assert by construction the very thing §03 refuses to assert by default — and a reader could then no longer tell from the data that the two columns are independent at all. Dealing the compliance third away from the business owner is what makes that independence visible, and it is why obligation_metrics (§09's third dataset, whose dimensions are status · kind · owner · contract) reads as five bars instead of one.

⚠️ And it does not claim to reproduce a flow

Nothing in the product assigns this column. F9 creates renewal obligations from the type's defaults and leaves the owner to a person; S5's extract_obligations proposes a 负责人 that a person confirms (§07). So the deal states a rule a legal desk would recognise rather than pretending to replay one.

What F10's "nobody to tell" edge still exercises — one row, and it is proved reachable

UNASSIGNED_OBLIGATIONS = 1, and leaveUnassigned in plan-children.ts proves F10 will select it. One, because one is what coverage costs — and one is therefore also the maximum, since every row beyond it is a reminder the demo does not send, which is the defect this PR exists to fix. The constant is a one-line change and the proof re-runs for whatever number it is given.

Why the row's due date is constructed at T+7 rather than drawn — measured on main, one obligation_due run on a freshly seeded database:

week_select     selected=2
today_select    selected=0
arrears_select  selected=0

The two zeroes are structural, not a bad day. Every stage filters status IN (pending, in_progress); the plan draws every such row's due date from 1 + rng*29 or 35 + rng*460, so no open obligation is ever due today (the minimum is +1) or already past due (the seeded arrears are born overdue, which no stage selects). The week stage's one-day [T+7, T+8) window is the only one that can carry this edge on this corpus, and how many rows land in it is otherwise a draw.

The row is taken from the due_soon_pending band — pending, due in [1, 29] — so moving it inside that band leaves §10's "40 due in the next 30 days" exactly where it was. Taking a future row (+35 and out) would have quietly made it 41. deliverable is preferred for the story, not the mechanics: an extracted commitment nobody has been made accountable for yet is precisely the state §07's S5 leaves behind between "AI proposes a 负责人" and "a person confirms".

leaveUnassigned re-proves both columns after the rewrite and throws with the reason if either moves; the eligibility check throws separately if the due-soon band ever empties.

Selected vs notified, before and after — read from the running system

Identical protocol both sides: clean .objectstack/data, pnpm demo on port 3152, README operator setup (3 requesters · all 7 positions · dev admin into clm_admin), re-seed so the upserts hand the rows over, then one trigger of each job. Counts from each run's own node summary; failed=0 on every run, both sides.

Receipts by topic, both sides. This is the measurement the card asks for, and it is complete on both runs:

topic before after
clm_legal_review_over_30_days Legal Counsel 2 x3 · Legal Counsel 1 x2 identical
clm_obligation_due_soon Dev Admin x2 Legal Counsel 2 x1 · Business Requester 3 x1
clm_payment_overdue Finance Controller x18 · BR1 x7 · BR2 x6 · BR3 x5 identical
clm_turn_stalled Business Requester 1 x2 identical
total 45 receipts · 27 notifications 45 receipts · 27 notifications

Stage census. Taken node by node from each run's own summary. The after side covers all six jobs; on the before side only F10's was taken directly, which is the one this card moves:

Job after: selected → notified before
obligation_due (F10) week · today · arrears 3 → 2 · 0 · 0 2 → 2 · 0 · 0 (measured)
legal_review_sla (F3) over 30d · 60d 6 → 5 · 0 → 0 receipts identical (5)
turn_stalled (F4) 2 → 2 receipts identical (2)
payment_overdue (F11) due · arrears 0 · 18 → 18 (acted 18) receipts identical (36)
renewal_notice (F12) 25 → 0 receipts identical (0)
expiration_sweep (F13) 0 · 0 → 0 receipts identical (0)

F10's 3 → 2 is the whole change: three rows selected, two notified, one quiet by design — and failed=0 proves the sweep did not die on the unowned row, which is the edge being kept alive. F12's zero is unchanged and untouched (#47 ruled it correct and out of scope).

Where the notifications land

before                                    after
  Finance Controller     18                 Finance Controller     18
  Business Requester 1    9                 Business Requester 1    9
  Business Requester 2    6                 Business Requester 3    6
  Business Requester 3    5                 Business Requester 2    6
  Legal Counsel 2         4                 Legal Counsel 2         4
  Legal Counsel 1         2                 Legal Counsel 1         2
  Dev Admin               2                 —

clm_obligation_due_soon  ->  Dev Admin x2   BECOMES   Legal Counsel 2 x1, Business Requester 3 x1

45 receipts both sides. Zero now carry the dev admin — the last such receipt in the book is gone. Distinct recipients falls 7 → 6 because the dev admin drops out, which is the point rather than a regression: it holds no clm_* permission set, so it was the one recipient that could not act.

Driven in a browser

Chromium (/opt/pw-browsers/chromium-1194/chrome-linux/chrome), against the same running demo, signed in as each account and clicked through to 我的合同 › My Obligations:

Signed in as navigation groups served my_obligations rows clm console errors
Legal Counsel 2 My Contracts · Legal Desk · Analytics 14 (all Compliance, soonest first, one Overdue) none
Business Requester 3 My Contracts · Analytics 18 (delivery and reporting) none
Dev Admin — 0 none

GET /api/v1/data/clm_obligation?...&filter=[["owner","equals","9srl9g9N…"],["status","in",["pending","in_progress","overdue"]]] -> 200, 14 records, grid footer reads 14 records. The administrator's own obligation queue is now empty, which is the fix stated as a screenshot.

The one action the reminder asks for was driven too: Legal Counsel 2 PATCHed one of its own obligations to in_progress and got 200. The same PATCH as Business Requester 3 is refused — that refusal is not this card and is not caused by it; see Acceptance notes.

Invariants

820 rows total en 820 · zh-CN 820, per-object row counts identical (120 · 60 · 25 · 30 · 200 · 300 · 40 · 9 · 30 · 6)
both locales row-for-row identical clm_obligation compared positionally across the two compiled artifacts: owner mismatches = 0, structural (kind/status/due/completed_at) mismatches = 0
⛔ no seeded users no user row is written; the fixture references accounts by name. Measured on a first boot with the accounts absent: all 200 obligations land owner = NULL and every row survives — 820 rows, none refused
legal_owner untouched 68 contracts carry one before and after (34 / 34); the counselFor extraction is a pure refactor and this is the check on it

Gates

Each redirected to a file with $? read immediately after, no pipe between the command and the exit code.

$ pnpm validate      > g2-validate.txt  2>&1; echo $?   ->  0     ✓ Validation passed (1009ms)
$ pnpm lint          > g2-lint.txt      2>&1; echo $?   ->  0     21 warning(s), 5 suggestion(s) (947ms)
$ pnpm typecheck     > g2-typecheck.txt 2>&1; echo $?   ->  0     tsc --noEmit, no output
$ pnpm lint:i18n-gate> g2-i18n.txt      2>&1; echo $?   ->  0     ✓ i18n gate — 0 missing keys across 2 locale(s)

lint's 21 warnings and 5 suggestions are not new: the same two gates were run on a detached worktree at 2d63324 and the warning set diffs clean against this branch's — the only differing line is the config path. No changeset (this repo has no changeset gate).

Control-character self-scan over the five changed files: grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' — clean.

Acceptance notes

Filed as #64 — DESIGN.md §04 gives clm_requester U(本人负责) on clm_obligation; controlled_by_parent refuses it for all 200. Business Requester 3 owns an obligation and its parent contract, POST /api/v1/security/explain answers allowed: true, and the PATCH is still 403 PERMISSION_DENIED — requires edit access to its master record. clm_requester's contract edit window is ['draft','submitted'] and every obligation hangs off an executed contract, so the two windows are disjoint by construction. ⛔ Not introduced here and not dodged here: the wall stands whoever owns the row, so re-dealing the obligations away from the business desk to avoid it would be widening a consumer around a defect that lives upstream. Legal is unaffected and was measured working.

Already filed as #11 — the three business requesters do not get clm_requester by existing. An account with no position resolves member_default only, and explain read clm_obligation answers allowed: false; the sys_user_permission_set grant that requester.profile.ts names as "the platform's supported path" is what turns it on (measured both ways). Every measurement above was taken with that grant applied. This is #11's decision, untouched here.

Noted, not filed — only three of clm_obligation.kind's six option values are ever seeded: deliverable 72 · report 72 · compliance 56, while payment, renewal and other never occur. planObligations rotates kind once per round-robin round over 72 parents, so 200 rows reach exactly three rounds. Nothing declares otherwise — §10 pins only the 200, the 40 and the 10 — so it breaks no contract, but it is why this deal could not use the card's own suggestion of "the finance controller for the payment ones": there are no payment obligations to give anyone. Whoever next touches the §10 obligation spread or bands a dashboard on kind will meet it; there is no queued card that does.

Noted, not filed — a non-admin console session logs GET /api/v1/data/sys_activity -> 403 PERMISSION_DENIED on the home page. Platform surface, nothing to do with clm_*; not patched (AGENTS.md "Platform gaps"). No queued card picks it up.

Merged origin/main (b667fda) — and what that does to the numbers above

Branched at 2d63324; main moved twice while this was in flight. scripts/demo.mjs conflicted in the two passages #54 / PR #60 (6e534689) also edited — the DEMO_USER doc block and the Dev Admin name-check failure detail. Both edits move the same way: #60 dropped clm_contract.legal_owner from the two reference lists, and this branch takes the same lists one reference further, to clm_review.reviewer alone — the only one required: true forces to resolve at seed time. Resolved to this branch's wording in both hunks, on top of #60, merge commit not rebase, no force-push. README.md merged with no conflict. PR #62 (#59) also landed; it touched only src/dashboards/ and the two bundles.

Every measurement in this PR body still describes the merged tree, and that is checked rather than assumed. Nothing that merged in touches src/data/, src/flows/ or the objects, so the receipt census, the stage census and the browser readings stand as taken. Re-run after the merge:

pnpm validate  -> 0    ✓ Validation passed (972ms)
pnpm lint      -> 0    21 warning(s), 5 suggestion(s) (1032ms)   <- same 21 as before the merge
pnpm typecheck -> 0
pnpm lint:i18n-gate -> 0    0 missing keys across 2 locale(s)

Both locales recompiled from the merged tree: 820 rows each, per-object counts identical, clm_obligation positionally identical (owner mismatches 0, structural mismatches 0), and the deal unchanged — 53 / 50 / 40 / 29 / 27 and one deliberately unassigned. node --check scripts/demo.mjs clean, OPERATOR_SETUP_NOTE item 1 present exactly once, control-character scan clean.

⛔ No re-seed and no re-measure were run, because nothing merged in could have moved them.

REWORK round — one sentence in README.md (7645414)

README.md:121 claimed clm_review.reviewer was "the only user reference in the fixture that still points at the dev admin". That is a universal, and clm_deviation.decided_by kills it: deviation.object.ts declares it Field.user, and negotiation.seed.ts:79 still stamps it DEMO_USER on every decided deviation. Two references name the account; the sentence claimed one. Confirmed both readings against the tree rather than taking them on trust.

Scoped to what the paragraph supports and to what the operator has to act on:

clm_review.reviewer is the one reference that still has to name the dev admin: it is required: true, so a name that resolves to nothing takes the row with it, …

That claim is checkable, and it was checked: of the six user references in the model — submitted_by, owner_id, legal_owner, decided_by, clm_obligation.owner, reviewer — only reviewer carries required: true. The mechanism and the reassign instruction are unchanged.

decided_by is deliberately left unnamed rather than listed beside it: it is an audit stamp, nothing is routed to it, there is nothing for the operator to reassign, and #61 is queued to re-deal it — naming it here would cost that card a second README edit. Omitting a member is incompleteness; the defect was the universal, not the omission. ⛔ decided_by itself is untouched — that is #61's, deliberately queued to adopt this card's route as precedent.

scripts/demo.mjs's "the ONLY reference this boot has to satisfy" is correct as it stands and is untouched: it is scoped to what the priming boot must resolve, and decided_by is optional.

Gates re-run on this commit: pnpm validate → 0 (✓ Validation passed (966ms)), pnpm lint → 0 (21 warning(s), 5 suggestion(s) — the same 21), pnpm typecheck → 0, pnpm lint:i18n-gate → 0 (0 missing keys across 2 locale(s)). Control-character scan on README.md: clean.

⛔ No re-seed, no re-measure, no browser run. A prose-only change to README.md cannot move a fixture measurement — the receipt census, stage census, locale-identity check and browser readings above are the ones taken earlier in this PR and are unchanged, not re-verified against this commit.


Generated by Claude Code

… dev admin

All 200 seeded obligations named `DEMO_USER`, so F10's reminders were the
administrator writing to themselves — the only notification topic still doing
so after #26 dealt `owner_id` and #47 dealt `legal_owner`. Re-measured on
`main` @ 2d63324 before changing anything: 200 of 200 on the dev admin, and
`clm_obligation_due_soon -> Dev Admin x2` was the last dev-admin receipt in
the book.

`owner` is optional, so it may name accounts the operator has yet to create.
It now names them by what the obligation IS: a `compliance` filing goes to the
lawyer who knows that counterparty, a `deliverable` or `report` to the
requester who launched the contract. Both are the counterparty-relationship
rule `ownerOf` and `legalOwnerOf` already use, so the two locales deal
identically, and both accounts are already in the README's exact-name table —
this deal adds no new account for the operator to create.

Deliberately NOT a copy of the contract's `owner_id`: DESIGN.md §03 declines
to default this column to the contract owner, and a fixture that put all 200
on the launching requester would assert by construction what §03 refuses to
assert by default.

One row is left unassigned, and `leaveUnassigned` proves F10 will select it.
All three of F10's stages take `{obligation.owner}` as their sole recipient,
so an unowned obligation is the only way this corpus exercises the partitioned
"nobody to tell" edge that keeps `loop-node.ts`'s bare `await` from ending a
sweep on one unreachable recipient. Its due date is constructed at T+7 rather
than drawn: measured, the `today` and `arrears` stages select 0 on a freshly
seeded database, so the `week` stage is the only one that can carry the edge.
It is taken from the due-soon band so §10's "40 due in the next 30 days" does
not move.

820 rows, both locales row-for-row identical, no seeded users.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3n3GGzobdegM4HUzah1iR
`scripts/demo.mjs` conflicted in the two passages #54 / PR #60 (`6e534689`)
also edited: the `DEMO_USER` doc block and the `Dev Admin` name-check failure
detail. Both edits move the same way — #60 dropped `clm_contract.legal_owner`
from the two reference lists, and this branch takes the same lists one
reference further to `clm_review.reviewer` alone, which is the only one left
that `required: true` forces to resolve at seed time. Resolved to this
branch's wording in both, on top of #60.

Nothing that merged in touches `src/data/`, `src/flows/` or the objects, so
the fixture measurements in the PR body still describe this tree. Re-verified
after the merge anyway: all four gates green, both locales compile to 820 rows
with `clm_obligation.owner` positionally identical and the deal unchanged
(53 / 50 / 40 / 29 / 27 and one deliberately unassigned).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3n3GGzobdegM4HUzah1iR
…versal

"the only user reference in the fixture that still points at the dev admin"
was a universal, and one counterexample kills it: `clm_deviation.decided_by`
is `Field.user` (deviation.object.ts) and `negotiation.seed.ts` still stamps
it with `DEMO_USER` on every decided deviation. Two references name the
account; the sentence claimed one.

Scoped to what the paragraph actually supports and what the operator has to
act on: `clm_review.reviewer` is the reference that still HAS TO name it,
because it is the only `required: true` user reference in the model — checked
against all six (`submitted_by`, `owner_id`, `legal_owner`, `decided_by`,
`clm_obligation.owner`, `reviewer`), and only `reviewer` carries it. The
`required: true` mechanism and the reassign instruction are unchanged.

`decided_by` is deliberately left unnamed rather than listed beside it: it is
an audit stamp with nothing routed to it and nothing for the operator to
reassign, and #61 is queued to re-deal it — naming it here would cost that
card a second README edit. Omitting a member is incompleteness; the defect
being fixed was the universal, not the omission.

`scripts/demo.mjs`'s "the ONLY reference this boot has to satisfy" is correct
as it stands and is untouched: it is scoped to what the priming boot must
resolve, and `decided_by` is optional.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3n3GGzobdegM4HUzah1iR
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.

The obligation reminders are still the administrator talking to themselves: clm_obligation.owner is the dev admin on all 200 seeded rows

2 participants