Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,14 @@ references them by name:
| Name it exactly | Give it | It receives |
|---|---|---|
| `Business Requester 1` · `Business Requester 2` · `Business Requester 3` | the default `clm_requester` set | the 120 contracts' `owner_id`, dealt by counterparty — 43 / 43 / 34 · and the 143 delivery and reporting obligations under them — 50 / 53 / 40 |
| `Legal Counsel 1` · `Legal Counsel 2` | the `clm_legal_counsel` position | the `legal_owner` of every contract legal has accepted — 34 / 34 · and all 56 compliance obligations, by the same counterparty relationship — 29 / 27 |
| `Legal Counsel 1` · `Legal Counsel 2` | the `clm_legal_counsel` position | the `legal_owner` of every contract legal has accepted — 34 / 34 · all 56 compliance obligations, by the same counterparty relationship — 29 / 27 · and the `decided_by` of all 17 decided deviations — 10 / 7 |

Each requester's **我的合同 › Launched by Me** and **› My Obligations**, and each
lawyer's **法务工作台 › 审查中** and their own **My Obligations**, fill the moment
the account exists. All three columns are optional references, so naming an
account that is not there yet costs nothing and no row: it simply stays empty.
the account exists; a decided deviation's **Decided By** shows its lawyer on the
contract page's Deviations grid. All four columns are optional references, so
naming an account that is not there yet costs nothing and no row: it simply
stays empty.
Every dataset is an upsert, so **create the accounts and run `pnpm demo` again**
and their contracts and obligations are handed over. Get a name wrong by one
character and the column stays empty with no error — that is the one thing to
Expand Down
14 changes: 7 additions & 7 deletions scripts/demo.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,13 @@ const SEED_LOCALE_ENV_VAR = 'OS_SEED_LOCALE';
* whole row, so it must name an account that exists while the seed runs, and
* on a first boot the dev admin is the only account there is.
*
* NOT `clm_contract.owner_id`, NOT `clm_contract.legal_owner` and NOT
* `clm_obligation.owner`. All three name accounts of DESIGN.md §10's persona
* mix — three business requesters and two legal counsel — which no seed may
* create and this script does not mint. They can afford to be missing: all
* three are optional, so a name with no account lands NULL and the row
* survives, and re-running `pnpm demo` once the operator has created them
* hands the rows over. See `src/data/keys.ts`.
* NOT `clm_contract.owner_id`, NOT `clm_contract.legal_owner`, NOT
* `clm_obligation.owner` and NOT `clm_deviation.decided_by`. All four name
* accounts of DESIGN.md §10's persona mix — three business requesters and two
* legal counsel — which no seed may create and this script does not mint. They
* can afford to be missing: all four are optional, so a name with no account
* lands NULL and the row survives, and re-running `pnpm demo` once the operator
* has created them hands the rows over. See `src/data/keys.ts`.
*
* ⚠️ This MIRRORS `DEMO_USER` in `src/data/keys.ts`. They have to agree, and
* they cannot be one constant: this file is plain `.mjs` that runs before
Expand Down
112 changes: 98 additions & 14 deletions src/data/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { STRINGS } from './demo-locale.js';
import type { ObligationPlan } from './plan-children.js';
import { CONTRACT_PLAN, type ContractPlan } from './plan-contracts.js';
import { CONTRACT_PLAN, type ContractPlan, type DeviationPlan } from './plan-contracts.js';
import { assertUniqueKeys } from './_shared.js';

/**
Expand Down Expand Up @@ -32,14 +32,16 @@ import { assertUniqueKeys } from './_shared.js';
* REQUIRED user reference has to name: `clm_review.reviewer` is
* `required: true`, and a name resolving to nothing costs the whole row.
*
* `clm_review.reviewer` is now the last REFERENCE that has to name it.
* `clm_contract.legal_owner` left for {@link LEGAL_OWNERS} and
* `clm_obligation.owner` for {@link obligationOwnerOf} — both optional, so
* both may name accounts that do not exist yet. What still spells this
* constant besides `reviewer` is `clm_deviation.decided_by` (the
* administrator did decide those, on this database) and the `our`-side entry
* of `clm_signature.signers`, which is a JSON string rather than a reference
* and resolves against nothing at all.
* `clm_review.reviewer` is now the last REFERENCE that has to name it, and
* the only one left that spells this constant at all.
* `clm_contract.legal_owner` left for {@link LEGAL_OWNERS},
* `clm_obligation.owner` for {@link obligationOwnerOf} and
* `clm_deviation.decided_by` for {@link deviationDeciderOf} — all three
* optional, so all three may name accounts that do not exist yet. Enumerated
* rather than sampled: `grep -rn DEMO_USER src` over this tree returns the
* declaration, this file's four cross-references, `reviewer`, and one thing
* that is not a reference at all — the `our`-side entry of
* `clm_signature.signers`, a JSON string that resolves against nothing.
*/
export const DEMO_USER = 'Dev Admin';

Expand Down Expand Up @@ -139,14 +141,20 @@ export const LEGAL_OWNERS: readonly string[] = [
];

/**
* The lawyer who knows this counterparty — the one rule both columns that
* name a lawyer are dealt by, extracted so there is one of it.
* The lawyer who knows this counterparty — the one rule every column that
* names a lawyer is dealt by, extracted so there is one of it. Enumerated,
* those columns are three: `clm_contract.legal_owner` ({@link legalOwnerOf}),
* the compliance share of `clm_obligation.owner`
* ({@link obligationOwnerOf}) and `clm_deviation.decided_by`
* ({@link deviationDeciderOf}).
*
* ⚠️ Being the counterparty's lawyer is NOT the same fact as owning that
* contract's review: {@link legalOwnerOf} additionally requires that F2 would
* have assigned one at all, and {@link obligationOwnerOf} deliberately does
* not — a compliance filing under a signed contract needs a lawyer whether or
* not that particular contract ever went through legal review.
* have assigned one at all, and {@link obligationOwnerOf} and
* {@link deviationDeciderOf} deliberately do not — a compliance filing under a
* signed contract needs a lawyer, and an off-playbook clause needs a ruling
* from one, whether or not that particular contract ever went through legal
* review.
*/
const counselFor = (contract: ContractPlan): string =>
LEGAL_OWNERS[contract.partyIndex % LEGAL_OWNERS.length]!;
Expand Down Expand Up @@ -231,6 +239,82 @@ export const obligationOwnerOf = (obligation: ObligationPlan): string | null =>
return obligation.kind === 'compliance' ? counselFor(contract) : ownerOf(contract);
};

/**
* Who decided this deviation — the lawyer who knows the counterparty — or
* `null` while it is still open.
*
* ## Why not {@link DEMO_USER}, which is what this column held before
*
* Measured on `main` @ `3d11db8`, clean database, one `pnpm demo`: **25
* deviations, 17 of them decided, and every one of the 17 carried the dev
* admin** (`GET /api/v1/data/clm_deviation` → `{"total":25}`, `decided_by`
* resolving to `Dev Admin` ×17, the 8 open ones NULL). So the 决定人 column of
* every decided deviation said the administrator adjudicated it.
*
* That is a different cost from the one {@link CONTRACT_OWNERS},
* {@link LEGAL_OWNERS} and {@link obligationOwnerOf} answer, and the
* difference is worth stating rather than copying their argument across.
* `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, and no screen is keyed
* on it. What it does is claim who performed an act — and §04 gives the act to
* legal, not to the administrator: on `clm_deviation`, `clm_requester` holds
* `RC(本人合同)` and `clm_legal` is the only non-admin set with **U**, so
* moving a deviation out of `open` is a lawyer's write and nobody else's.
* A demo whose every deviation was decided by the dev admin tells an evaluator
* that adjudicating the playbook is an administrative act.
*
* ## {@link counselFor} and not {@link legalOwnerOf}, which is the real choice
*
* Both name a lawyer; they differ on the rows where F2 never assigned one.
* Enumerated on this fixture: the 17 decided deviations sit on 17 distinct
* contracts, and **3 of those 17 are of a type whose `requiresLegalReview` is
* false**, so `legalOwnerOf` returns `null` for them (contracts #17, #44, #48
* — two `in_approval`, one `approved`).
*
* Dealing by {@link legalOwnerOf} would therefore leave 3 rows that are
* `accepted`/`rejected`/`withdrawn`, carry a `decided_at`, and name nobody.
* The write layer never produces that pair: `deviation_state_machine`
* (`contract.hook.ts`) stamps `decided_by` with the acting user on any
* decision that leaves it empty, so a decided deviation always has a decider.
* This fixture's standing rule is that no seeded row is 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.
*
* {@link counselFor} claims only what a lawyer's involvement needs: the
* counterparty's lawyer looked at the clause and ruled on it. It does NOT
* claim that contract went through a legal review, which is the extra fact
* {@link legalOwnerOf} carries and the distinction `counselFor`'s own note
* draws — the same reason {@link obligationOwnerOf} deals compliance work by
* `counselFor` and not by `legalOwnerOf`. A deviation exists precisely where
* somebody wanted off-playbook wording, and someone in legal answered, whether
* or not the contract's type routes through review at all.
*
* ## No deliberate unassigned set here, and that is not an oversight
*
* {@link legalOwnerOf} and {@link obligationOwnerOf} both keep one deliberately
* empty because a FLOW takes a "nobody to tell" edge over that column — F3 over
* `legal_owner`, F10 over `owner` — and an empty column is what exercises it.
* No flow reads `decided_by`: it is written once and displayed. An empty one
* would exercise nothing and would instead assert the one thing this column
* cannot mean, that a decided deviation was decided by nobody. So every decided
* row names a lawyer and every open row is `null` — which is not an unassigned
* set but the column's own semantics, and matches what the hook writes.
*
* ## Structure, and no new account
*
* The deal is {@link counselFor}, so it is the counterparty relationship the
* fixture already deals `owner_id`, `legal_owner` and the compliance
* obligations by: no locale bundle is consulted and `demo-en` and `demo-zh`
* deal identically. It spends NO new account — the same two `Legal Counsel`
* names `README.md` already asks the operator to create, 10 / 7 across the 17.
* Like the other optional references it may name an account that does not
* exist yet: measured on 17.4.0, an unresolvable name lands NULL and never
* refuses the row, and the next `pnpm demo` upsert hands the rows over.
*/
export const deviationDeciderOf = (deviation: DeviationPlan): string | null =>
deviation.status === 'open' ? null : counselFor(CONTRACT_PLAN[deviation.contractIndex]!);

/**
* `<type> — <counterparty>`, with a qualifier when a counterparty has more
* than one contract of the same type.
Expand Down
10 changes: 8 additions & 2 deletions src/data/negotiation.seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Signature } from '../objects/signature.object.js';

import { dayOffset } from './_shared.js';
import { STRINGS } from './demo-locale.js';
import { DEMO_USER, titleOfIndex } from './keys.js';
import { DEMO_USER, deviationDeciderOf, titleOfIndex } from './keys.js';
import { CONTRACT_PLAN, DEVIATION_PLAN, typeOf } from './plan-contracts.js';
import { REVIEW_PLAN, SIGNATURE_PLAN } from './plan-children.js';
import { contactEmailFor } from './plan.js';
Expand Down Expand Up @@ -65,6 +65,12 @@ export const reviewSeed = defineSeed(Review, {
* `in_approval` while any deviation is open. A seed is exempt from that gate;
* the plan applies it anyway, so no seeded row is in a state the write layer
* would have refused.
*
* The same rule decides `decided_by`: `deviation_state_machine` stamps the
* acting user on any decision that leaves the column empty, so a decided
* deviation always names a decider and an open one never does. WHICH lawyer it
* names is {@link deviationDeciderOf}, and it is a lawyer because §04 gives
* `clm_legal` the only non-admin `U` on this object.
*/
export const deviationSeed = defineSeed(Deviation, {
externalId: ['contract', 'clause'],
Expand All @@ -76,7 +82,7 @@ export const deviationSeed = defineSeed(Deviation, {
requested_position: deviation.requestedPosition,
justification: STRINGS.deviationJustifications[deviation.justificationIndex]!,
status: deviation.status,
decided_by: deviation.status === 'open' ? null : DEMO_USER,
decided_by: deviationDeciderOf(deviation),
decided_at: deviation.decidedAt === null ? null : dayOffset(deviation.decidedAt),
})),
});
Expand Down