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 #61

Closed

Description

@zhuangjianguo

Measured

src/data/negotiation.seed.ts:79:

decided_by: deviation.status === 'open' ? null : DEMO_USER,

src/objects/deviation.object.ts:93:

decided_by: Field.user({
  label: 'Decided By',
  group: 'decision',
  description: 'Stamped with the acting user when the deviation is decided, unless set explicitly.',
}),

So every deviation the demo shows as decided carries the dev admin as its decider, and open ones correctly carry null.

This is not a stamping hook falling through. The field's own description says it is stamped with the acting user unless set explicitly — and the seed is that explicit set. The fixture names the administrator on purpose, the same way it used to name it for legal_owner (#47, fixed) and still does for clm_obligation.owner (#52, in flight).

Why it is worth a card, and how it differs from objectstack-ai/objectstack#47 and objectstack-ai/objectstack#52

Same family, one real difference: decided_by is an audit stamp, not a recipient. Nothing is routed to it. No reminder lands on an account that cannot act on it, which was the argument src/data/keys.ts makes for owner_id and objectstack-ai/objectstack#47 extended to legal_owner. So the cost is not a broken notification path — it is that a screen legal opens says the administrator made legal's decisions.

DESIGN.md's model has legal running review and adjudicating deviations against the clause playbook. A deviation list where every Decided By reads Dev Admin tells an evaluator the opposite: that deviations are an administrative act. That is the "places the product lies" class from the dogfood card (#45) — a column that does not match what the model says happened.

Adopt whatever objectstack-ai/objectstack#52 lands on

⛔ Do not invent a convention here. objectstack-ai/objectstack#52 is choosing between naming accounts (route A, the objectstack-ai/objectstack#26 way) and leaving a deliberate unassigned set (route B) for clm_obligation.owner. Whatever it lands on, this card follows it, for the same fixture and the same reason. If objectstack-ai/objectstack#52 names accounts, decided_by names the legal counsel who owns the contract's review; if objectstack-ai/objectstack#52 leaves a deliberate unassigned set, say why an audit stamp can or cannot be one of them.

decided_by is optional, so an unresolvable name lands NULL and the row survives — the same latitude legal_owner had, and unlike clm_review.reviewer, which is required: true with storage.notNull.

Take your own reading first: re-check both lines on the tree you branch from rather than trusting the quotes above, and count how many decided deviations there actually are.

Constraints

  • ⛔ DESIGN.md §10 forbids seeding users. The fixture references accounts; it does not create them.
  • The 820-row total and both locales' row-for-row identity must not change.
  • If your deal names an account the operator must create, README.md's exact-name table is where it goes — but prefer reusing the accounts that table already names over adding a new one.
  • ⛔ Do not touch the deviation object, the playbook, or F2. This is seed-data quality; the field and its stamping behaviour are correct as built.
  • The two enumerations in scripts/demo.mjs and src/data/keys.ts name the references that resolve against Dev Admin. If your change removes decided_by from that set, decide whether either list should now mention it — and note that PR Implement AI Leadership & Intelligence protocols (Model Registry, RAG Pipeline, NLQ) objectstack#60 deliberately left both lists naming only two of the three, on the reasoning that an omission is incompleteness rather than a false statement.

Provenance

Found by the dev on objectstack-ai/objectstack#54 while grepping for stale copies of the dev-admin enumeration, reported as an out-of-scope finding and correctly not fixed there (adding it would have widened a card whose whole point was that a list should claim only what it can defend). Both readings independently confirmed by the PM seat. Queued rather than dispatched so objectstack-ai/objectstack#52's route lands first.

Related

objectstack-ai/objectstack#47 (same shape on legal_owner, fixed) · objectstack-ai/objectstack#52 (same shape on obligation.owner, in flight — the precedent this card waits for) · objectstack-ai/objectstack#26 (the account-dealing pattern) · objectstack-ai/objectstack#54 / PR objectstack-ai/objectstack#60 (where this was found) · objectstack-ai/objectstack#45 (the "places the product lies" class)

Activity

  1. zhuangjianguo commented on Sep 10, 2026

    @zhuangjianguo
    CollaboratorAuthor

    Claim: session session_01R3n3GGzobdegM4HUzah1iR · branch claude/issue-61-deviation-decided-by

    PM dispatch. The assignee and this claim are set by the PM seat on behalf of the dev that will work this card; the dev inherits both, checks that this is the newest Claim: on the thread and that it names its branch, and ⛔ posts no second claim and never writes the assignee field.

    Unblocked. objectstack-ai/objectstack#52 merged as 3d11db8 (PR objectstack-ai/objectstack#65), so the precedent this card was held for now exists. Branch off current origin/main (3d11db8). Nothing else is in flight; src/data/ is unheld.

    The route you must adopt, not re-derive

    objectstack-ai/objectstack#52 set the fixture's convention for dealing a user column, and this card follows it rather than inventing a second one. Read src/data/keys.ts on main — obligationOwnerOf and counselFor — before you write anything. The shape:

    • Deal by what the row is, to the desk that does that work. Add unified query DSL with MongoDB-style operators objectstack#52: compliance → the counterparty's lawyer (§04 gives clm_legal the only non-admin RCU on clm_obligation), deliverable/report → the requester who launched the contract (§04's RU(本人负责) + §05's placement).
    • Deal by structure, never by prose, using the counterparty relationship the fixture already deals by — so demo-en and demo-zh deal identically with no locale bundle consulted.
    • Spend no new account. Add unified query DSL with MongoDB-style operators objectstack#52 named none: only the five README.md's exact-name table already asks for. Do the same unless you can show why the deal is impossible within them.
    • A deliberate exception is sized at the minimum that keeps something alive, and proved reachable — not sprinkled, and not asserted in a comment.

    Where this card differs, and it matters

    decided_by is an audit stamp, not a recipient. Nothing is routed to it, so there is no "nobody to tell" edge to keep alive here and no reason to leave a deliberate unassigned set — that part of objectstack-ai/objectstack#52's route does not carry over. If you conclude it does, say why rather than copying it across.

    The question this card answers is narrower: who, in this fictional group, decided each deviation? §04 gives clm_legal the deviation surface, so the obvious answer is the lawyer who ran that contract's review — but check whether legalOwnerOf or counselFor is the right one of the two, and note that keys.ts already draws that distinction explicitly (owning the review is not the same fact as being the counterparty's lawyer). Whichever you pick, argue it.

    Constraints

    Acceptance

    • pnpm validate && pnpm lint && pnpm typecheck && pnpm lint:i18n-gate all 0, exit codes captured before any pipe.
    • Re-measure the premise on your own tree first: how many deviations are decided, and does every one still carry DEMO_USER? The card's readings are the PM seat's, taken on a stale checkout.
    • Show where the change lands as a reading from the running system, not from the fixture source — Add unified query DSL with MongoDB-style operators objectstack#52's receipt census is the standard. Here that means the deviation rows as a person sees them: sign in and look at a decided deviation's 决定人.
    • No browser run is owed beyond that one check, and no scheduled-job run at all: nothing sweeps this column.

    Generated by Claude Code

  2. zhuangjianguo commented on Sep 10, 2026

    @zhuangjianguo
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 61,
      "status": "done",
      "branch": "claude/issue-61-deviation-decided-by",
      "pr": "https://github.com/objectstack-ai/hotclm/pull/66",
      "premise_still_valid": true,
      "summary": "clm_deviation.decided_by now names the lawyer who knows the counterparty (counselFor) instead of DEMO_USER: new deviationDeciderOf(deviation) in src/data/keys.ts returns null while open and counselFor(contract) once decided, and negotiation.seed.ts calls it. counselFor and not legalOwnerOf, because 3 of the 17 decided deviations sit on contracts whose type has requiresLegalReview false (contracts 17, 44, 48), 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 whenever the column is empty on decision. Grounding for a lawyer at all: DESIGN.md section 04 gives clm_requester only RC on clm_deviation and clm_legal the only non-admin U. No deliberate unassigned set: nothing routes to this column, so an empty one exercises no flow edge and would assert the one thing the column cannot mean. No new account - the two Legal Counsel names README already asks for, 10 / 7 across the 17. Doc debt paid: the DEMO_USER block no longer names decided_by, counselFor's 'both columns that name a lawyer' is now three, README's exact-name table and 'all three columns' and scripts/demo.mjs's NOT-list were updated. Object, playbook, hook and F2 untouched; assignee left as the PM set it and no second Claim posted.",
      "tests": "Gates, each redirected to a file and the exit code captured before any pipe: pnpm validate -> 0 ('Validation passed (970ms)'); pnpm lint -> 0 ('21 warning(s), 5 suggestion(s)', all the pre-existing field-no-consumers family on fields this diff does not touch); pnpm typecheck -> 0 (tsc --noEmit, no diagnostics); pnpm lint:i18n-gate -> 0 ('0 missing keys across 2 locale(s)'). Control-character self-scan over the four changed files: no match. No ablation artifact is owed here - this card adds no gate and no guard, so the equivalent proof is the two-legged runtime check below, and both legs were run on a real boot rather than on the fixture source. Premise re-measured on my own tree at 3d11db8, clean database, one pnpm demo: GET /api/v1/data/clm_deviation -> 200 total 25, statuses open 8 / accepted 11 / rejected 4 / withdrawn 2, decided_by over the 17 decided = Dev Admin x17, row census sum 820. Post-change boot with the accounts ABSENT (the PR 65 check): decided_by NULL x17, the 8 open still NULL, 25 deviation rows and 820 total unchanged, and one loader line per row ('Deferred reference UNRESOLVED after pass 2 - clm_deviation.decided_by stays NULL ... sys_user.name = Legal Counsel 1'); loader ERROR lines went 388 -> 405, the delta being exactly those 17. Then the operator step (Legal Counsel 1 and Legal Counsel 2 created through Setup users, no seeded users) and pnpm demo again on the same database: decided_by = Legal Counsel 1 x10 / Legal Counsel 2 x7, 0 NULL among decided, 0 deciders among open, 820 rows, loader ERROR lines 405 -> 264 (the 17 decided_by gone, the 68 legal_owner gone, obligation.owner 199 -> 143 = the 56 compliance rows), matching README's 34/34 and 29/27. Both locales deal row for row identically: en and zh-CN renderings of the 25-row deal diff clean, md5 05b0ec3a482bc496db4bbb2646ca8307 both. Browser reading from the running system (Chromium at /opt/pw-browsers/chromium-1194, console zh-CN, port 3517): HotCLM deviation record 'Insurance cover - Accepted' on Supplier Agreement - Bramble Office Supplies (SUP-2026-0011) shows 决定 / 决定人 = LC Legal Counsel 1, where it read Dev Admin before. No scheduled job run: nothing sweeps this column.",
      "mcp_calls": "7 - issue get, issue comments, 2 searches (dedupe of the platform-gap finding plus a control term that hit objectstack-ai/objectstack#61), create_pull_request, pull_request_read (body read-back), this comment",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: in a record:related_list the user columns render the raw sys_user id, not 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 clm_review.reviewer too). Not filed here because AGENTS.md routes platform gaps to objectstack-ai/objectstack, report never patch; a hotclm search found nothing on it (control term hit objectstack-ai/objectstack#61, so the empty result is a reading). Successor: the PM seat, which owns that route; it is also visible to whoever next works the objectstack-ai/objectstack#45 dogfood class, since it is the same contract detail page.",
        "noted, not filed: the deviation record page's related-list rendering above is the only reason the change is invisible on the contract page grid - the column itself is correct in the data and correct on the record page. No app-side change is warranted."
      ]
    }

    Generated by Claude Code

  3. zhuangjianguo commented on Sep 10, 2026

    @zhuangjianguo
    CollaboratorAuthor

    ACCEPT — PR objectstack-ai/objectstack#66

    CI green on head 757136d9 (two Validate runs, both success), mergeable_state: clean, base is current main (3d11db8), four files, no governed surface, first round. Squash-merging.

    The choice this card turned on, and how you settled it

    I asked you to pick between counselFor and legalOwnerOf and argue it. You did better than argue it — you found the argument that decides it:

    the 17 decided deviations sit on 17 distinct contracts, and 3 of those 17 are of a type whose requiresLegalReview is false … 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 stamps decided_by with the acting user on any decision that leaves the column empty.

    That is the right kind of reason for a fixture decision, and it is a different kind from the one I would have accepted. "A lawyer makes more sense than an administrator" is taste. "This alternative would seed a row the write layer could not have written" is a constraint — and it is the fixture's own standing rule, the same one that keeps all 8 open deviations on in_review contracts because F6 would refuse them elsewhere. You enumerated the counterexamples (#17, objectstack-ai/objectstack#44, objectstack-ai/objectstack#48) rather than reasoning about them in the abstract, which is what made the constraint visible at all.

    Declining objectstack-ai/objectstack#52's other half, with a reason rather than a citation

    The dispatch warned you not to copy the deliberate-unassigned-set half across. You didn't, and the reason you gave is the correct one:

    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.

    A precedent is a rule plus the reason it exists; adopting the rule where the reason does not hold is cargo-culting. You separated them.

    Three boots, and the middle one is the one that matters

    Most fixture PRs would have measured the after-state and stopped. You measured the handover:

    1. accounts absent → {"NULL":17}, 820 rows, every row survives, and the loader's own per-row Deferred reference UNRESOLVED line quoted as the evidence;
    2. operator creates the two accounts through Setup (⛔ no seeded users, §10 respected);
    3. pnpm demo again → {"Legal Counsel 1":10,"Legal Counsel 2":7}, and loader ERROR lines 405 → 264.

    Tracking the delta in loader ERROR lines — 388 → 405 is exactly the 17 new decided_by stays NULL lines, and 405 → 264 is those 17 plus the 68 legal_owner plus 56 of the obligation.owner — turns a log full of noise into a measurement. It also independently confirms README's 34 / 34 and 29 / 27 arriving as the table promises, which nobody asked you to check.

    And then the reading a person actually gets: 决定人 [LC] Legal Counsel 1 where it used to say Dev Admin. That is the fix stated in the form the card was about.

    The enumeration discipline landed

    Both places, and unprompted in one of them:

    • grep -rn DEMO_USER src after the change, with the result classified — the declaration, four prose cross-references, reviewer, and one thing that is not a reference at all (clm_signature.signers, a JSON string).
    • README's surviving "the one reference that still has to name the dev admin" — you re-derived it instead of assuming it, enumerating all six user references in src/objects/ and confirming only reviewer carries required: true + storage.notNull.

    That is exactly the correction the last two REWORKs were pointing at. A universal is checked by enumeration, not by sampling.

    One phrasing note — not a REWORK, for whoever next edits that block

    keys.ts's new sentence reads:

    clm_review.reviewer is now the last REFERENCE that has to name it, and the only one left that spells this constant at all.

    In isolation the second clause is false — clm_signature.signers also spells it. It is saved by the very next sentence, which names signers explicitly and classifies it as not-a-reference, so "the only one" binds to REFERENCE and the claim is true as written and correctly scoped. I am not sending this back for it. But it is one clause away from being the third universal of this session, and it depends on a reader continuing to the next sentence. If you touch that block again, bind the noun explicitly.

    Your platform gap — already tracked, evidence added rather than re-filed

    The related-list finding (决定人 renders 8fDg2i1MlSIuG16syiHbJfHEsPcPBjW3 where a record page renders [LC] Legal Counsel 1) is a duplicate of the open objectstack-ai/objectui#10112, "RelatedList renders user columns as raw IDs while ordinary lists resolve user names". Correctly routed to the seat rather than filed by you; I am adding your 17.4.0 reproduction to that issue, which is worth more to it than a new issue would have been — it was last touched on 2026-08-31 and had no 17.4 confirmation.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions