Skip to content

Commit 3e26402

Browse files
clay-goodclaude
andcommitted
fix(rules): RISK-008 detect one-sided consequential-damages waiver for any party label (v1.1.0)
What was wrong: RISK-008 (one-sided consequential-damages waiver) detected the one-sided clause only when the protected party was labeled Provider/Vendor/ Company, so a waiver protecting only the Supplier, Licensor, Seller, Customer, etc. slipped through entirely and never fired. How it was fixed: broadened the one-sided-trigger party vocabulary to match the beneficiary counter's list (adding Seller to both so the two stay in sync). The existing ">=2 distinct beneficiaries" guard still clears a genuinely mutual "X … and Y shall not be liable …" clause, so a mutual Seller/Buyer waiver stays silent. Proof: a new RISK-008.test.ts pins the one-sided waivers (Supplier / Licensor) firing and the mutual / "neither party" waivers staying silent (7 tests). Full suite green (5649 passing). RISK-008 is a launch rule, so all four golden corpora (integration v2/v3, tests/golden/v3, tests/golden/v4, tests/golden/v4/bundle) were checked with a finding-level delta before regen — zero finding drift across every fixture (version-only hash bump; RISK-008 only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 3af6711 commit 3e26402

367 files changed

Lines changed: 431 additions & 393 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { describe, expect, it } from "vitest";
2+
import { rule as RISK_008 } from "./RISK-008.js";
3+
import { buildContext } from "../../_test-fixtures.js";
4+
5+
describe("RISK-008 — one-sided consequential-damages waiver", () => {
6+
const fires = (b: string) => !!RISK_008.check(buildContext(["Liability", b]) as never);
7+
8+
it.each([
9+
"The Company shall not be liable for any consequential damages.",
10+
"The Supplier shall not be liable for any consequential or special damages.",
11+
"Licensor shall not be liable for any indirect, incidental, or consequential damages.",
12+
])("fires on a one-sided waiver regardless of the protected party's label: %s", (b) => {
13+
expect(fires(b)).toBe(true);
14+
});
15+
16+
it.each([
17+
"Neither party shall be liable for any consequential damages.",
18+
"The Company shall not be liable for consequential damages, and the Customer shall not be liable for consequential damages.",
19+
"The Seller shall not be liable for consequential damages, and the Buyer shall not be liable for consequential damages.",
20+
])("stays silent on a mutual waiver: %s", (b) => {
21+
expect(fires(b)).toBe(false);
22+
});
23+
24+
it("emits a warning-severity finding", () => {
25+
const f = RISK_008.check(
26+
buildContext([
27+
"Liability",
28+
"The Supplier shall not be liable for consequential damages.",
29+
]) as never,
30+
);
31+
expect(f?.severity).toBe("warning");
32+
});
33+
});

src/engine/rules/risk-allocation/RISK-008.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { emit, firstParagraphMatch } from "../_helpers.js";
33

44
/** Every party named as protected by a consequential-damages waiver in `text`. */
55
const WAIVER_BENEFICIARY =
6-
/\b(Provider|Vendor|Company|Supplier|Contractor|Licensor|Customer|Client|Licensee|Subscriber|Buyer|Purchaser)\s+(?:will|shall)\s+not\s+be\s+liable[\s\S]{0,200}?\b(?:consequential|special|incidental|punitive)\s+damages?\b/gi;
6+
/\b(Provider|Vendor|Company|Supplier|Contractor|Licensor|Customer|Client|Licensee|Subscriber|Buyer|Purchaser|Seller)\s+(?:will|shall)\s+not\s+be\s+liable[\s\S]{0,200}?\b(?:consequential|special|incidental|punitive)\s+damages?\b/gi;
77

88
function countWaiverBeneficiaries(text: string): number {
99
const seen = new Set<string>();
@@ -16,7 +16,7 @@ function countWaiverBeneficiaries(text: string): number {
1616
/** RISK-008 — Consequential damages waiver mutuality (warning). */
1717
export const rule: Rule = {
1818
id: "RISK-008",
19-
version: "1.0.0",
19+
version: "1.1.0",
2020
name: "Consequential damages waiver mutuality",
2121
category: "risk-allocation",
2222
default_severity: "warning",
@@ -28,9 +28,14 @@ export const rule: Rule = {
2828
/\b(?:neither\s+party|neither\s+\w+\s+nor\s+\w+)\s+(?:will|shall)\s+be\s+liable[\s\S]{0,200}\b(?:consequential|special|incidental|punitive)\s+damages?\b/i,
2929
);
3030
if (hit) return null;
31+
// v1.0.0 detected the one-sided waiver only when Provider/Vendor/Company was
32+
// the protected party, so a waiver protecting only the Supplier / Licensor /
33+
// Seller (etc.) slipped through entirely. Match the same party vocabulary the
34+
// beneficiary counter uses; the >=2-beneficiary guard below still clears a
35+
// genuinely mutual "X … and Y shall not be liable" clause.
3136
const oneSided = firstParagraphMatch(
3237
ctx,
33-
/\b(?:Provider|Vendor|Company)\s+(?:will|shall)\s+not\s+be\s+liable[\s\S]{0,200}\b(?:consequential|special|incidental|punitive)\s+damages?\b/i,
38+
/\b(?:Provider|Vendor|Company|Supplier|Contractor|Licensor|Customer|Client|Licensee|Subscriber|Buyer|Purchaser|Seller)\s+(?:will|shall)\s+not\s+be\s+liable[\s\S]{0,200}\b(?:consequential|special|incidental|punitive)\s+damages?\b/i,
3439
);
3540
if (!oneSided) return null;
3641
// A mutual waiver is just as often drafted as two symmetric grants

tests/fixtures/expected/bad-consulting-success-fee.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@
434434
"elapsed_ms": 0,
435435
"fired": false,
436436
"rule_id": "RISK-008",
437-
"rule_version": "1.0.0"
437+
"rule_version": "1.1.0"
438438
},
439439
{
440440
"elapsed_ms": 0,
@@ -974,7 +974,7 @@
974974
"playbook_id": "consulting-agreement",
975975
"playbook_match_confidence": 0.9,
976976
"playbook_match_reasoning": "Selected consulting-agreement (score 0.9). Title matched: \"advisory agreement\". Distinguishing phrases: \"advisory services\", \"deliverables\", \"independent contractor\".",
977-
"result_hash": "6aa36c5150a1f6290a75c4f4b560c0254cea97d5f4c7a3394822c2d04f49640a",
977+
"result_hash": "a22ce028ed9f1f0c9534e3cacd3ea235e1e152261fd9886298f84996adeaa796",
978978
"source_file": {
979979
"name": "bad-consulting-success-fee.docx",
980980
"sha256": "6bc505eb0c1641cda4c3466a62f0b3eead258e2dc3a385e792e79f4a717ad6a5",

tests/fixtures/expected/bad-consulting.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@
438438
"elapsed_ms": 0,
439439
"fired": false,
440440
"rule_id": "RISK-008",
441-
"rule_version": "1.0.0"
441+
"rule_version": "1.1.0"
442442
},
443443
{
444444
"elapsed_ms": 0,
@@ -1055,7 +1055,7 @@
10551055
"playbook_id": "consulting-agreement",
10561056
"playbook_match_confidence": 0.9,
10571057
"playbook_match_reasoning": "Selected consulting-agreement (score 0.9). Title matched: \"consulting agreement\". Distinguishing phrases: \"Consultant\", \"advisory services\", \"deliverables\", \"independent contractor\", \"the Consulting Services\".",
1058-
"result_hash": "4cb43c3669db3a1a434dd36738a51e29a38a690593f83ec9c0c36ae158cf1c4f",
1058+
"result_hash": "a31e12f3f0d373796d571f2f88f4031729bbbb5e39158ecf717ee405b4d0d813",
10591059
"source_file": {
10601060
"name": "bad-consulting.docx",
10611061
"sha256": "f66f070f4bb768dbc3f21ee6782418bca5ed64451d483dab533b345a8ddbe691",

tests/fixtures/expected/bad-contractor-leaseback.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@
431431
"elapsed_ms": 0,
432432
"fired": false,
433433
"rule_id": "RISK-008",
434-
"rule_version": "1.0.0"
434+
"rule_version": "1.1.0"
435435
},
436436
{
437437
"elapsed_ms": 0,
@@ -882,7 +882,7 @@
882882
"playbook_id": "independent-contractor",
883883
"playbook_match_confidence": 1,
884884
"playbook_match_reasoning": "Selected independent-contractor (score 1). Title matched: \"independent contractor\", \"contractor agreement\". Distinguishing phrases: \"independent contractor\", \"Contractor shall\".",
885-
"result_hash": "a80726082e47ddede6401fe3aa451fdd5ab1dd61315600e11982f6a787c473aa",
885+
"result_hash": "4cdee6928508f7916cccc9caa8f32237f64fca74aec1d2f3ca4878c35d193f51",
886886
"source_file": {
887887
"name": "bad-contractor-leaseback.docx",
888888
"sha256": "82496f664a3abe77b7ed03c7c77b47c9ffbd8c5f883389cb761b7ba68b31f98d",

tests/fixtures/expected/bad-contractor.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@
437437
"elapsed_ms": 0,
438438
"fired": false,
439439
"rule_id": "RISK-008",
440-
"rule_version": "1.0.0"
440+
"rule_version": "1.1.0"
441441
},
442442
{
443443
"elapsed_ms": 0,
@@ -1084,7 +1084,7 @@
10841084
"playbook_id": "independent-contractor",
10851085
"playbook_match_confidence": 1,
10861086
"playbook_match_reasoning": "Selected independent-contractor (score 1). Title matched: \"independent contractor\", \"contractor agreement\". Distinguishing phrases: \"independent contractor\", \"Contractor shall\".",
1087-
"result_hash": "7cb3cd66896d98fd801191694a099ed30b278a078f4a563c0d22595e672b3af1",
1087+
"result_hash": "6f50ba820ce29a8ee2a837a1227ec117f7dd563985c8f164fd0db6a95c5f7b3c",
10881088
"source_file": {
10891089
"name": "bad-contractor.docx",
10901090
"sha256": "7a40a2e53d068557d47cf7690838b907085994f596a0c6e2380b8ac2607543df",

tests/fixtures/expected/bad-employment-choice-of-law.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@
432432
"elapsed_ms": 0,
433433
"fired": false,
434434
"rule_id": "RISK-008",
435-
"rule_version": "1.0.0"
435+
"rule_version": "1.1.0"
436436
},
437437
{
438438
"elapsed_ms": 0,
@@ -928,7 +928,7 @@
928928
"playbook_id": "employment-at-will-us",
929929
"playbook_match_confidence": 0.9,
930930
"playbook_match_reasoning": "Selected employment-at-will-us (score 0.9). Title matched: \"employment agreement\". Distinguishing phrases: \"at-will\", \"exempt\", \"Employee\".",
931-
"result_hash": "8fdda017f5d4922ee0663b6a311527e2cee6da2f2ab403ca922583846c56c602",
931+
"result_hash": "c5e0faceb6c70675b68a6b70fddf2c516a23700bd30b8e6cd81678a4ab0c4e2b",
932932
"source_file": {
933933
"name": "bad-employment-choice-of-law.docx",
934934
"sha256": "432c209852332e09cdc68eec7a183f61c5da1fa8aafe36b92e3b8bf6fd2e6c26",

tests/fixtures/expected/bad-employment-trap.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@
437437
"elapsed_ms": 0,
438438
"fired": false,
439439
"rule_id": "RISK-008",
440-
"rule_version": "1.0.0"
440+
"rule_version": "1.1.0"
441441
},
442442
{
443443
"elapsed_ms": 0,
@@ -1027,7 +1027,7 @@
10271027
"playbook_id": "employment-at-will-us",
10281028
"playbook_match_confidence": 0.9,
10291029
"playbook_match_reasoning": "Selected employment-at-will-us (score 0.9). Title matched: \"employment agreement\". Distinguishing phrases: \"at-will\", \"exempt\", \"salary\", \"Employee\".",
1030-
"result_hash": "f96228c6b1bd5c181efaea41c4316cd5e2162c77fed46502a10e4022665a86b0",
1030+
"result_hash": "a4249103f20389edabec7f24637e28c5ea832ff951c9016d94e54e51290cfcb3",
10311031
"source_file": {
10321032
"name": "bad-employment-trap.docx",
10331033
"sha256": "c0ef11cc8a800daead332bf1bc265f96d72af1c51953958734b1bf18c4058b2a",

tests/fixtures/expected/bad-employment.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@
444444
"elapsed_ms": 0,
445445
"fired": false,
446446
"rule_id": "RISK-008",
447-
"rule_version": "1.0.0"
447+
"rule_version": "1.1.0"
448448
},
449449
{
450450
"elapsed_ms": 0,
@@ -1232,7 +1232,7 @@
12321232
"playbook_id": "employment-at-will-us",
12331233
"playbook_match_confidence": 1,
12341234
"playbook_match_reasoning": "Selected employment-at-will-us (score 1). Title matched: \"employment agreement\". Required clauses present: \"confidentiality-obligation\". Distinguishing phrases: \"at-will\", \"exempt\", \"salary\", \"Employee\".",
1235-
"result_hash": "6dad4cfe04279eb12d7c7ec115592277131035d8d894b1400c3a06cd1083acbd",
1235+
"result_hash": "f67d14b05753d7cfb43c9f5de2c347bd905466d5cdc9a2a0e1c34a2d028452e9",
12361236
"source_file": {
12371237
"name": "bad-employment.docx",
12381238
"sha256": "44e689f2fe4e8767866844995363dda962260fd37d54469e6d0fdf443136013c",

tests/fixtures/expected/bad-lease-cam.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@
429429
"elapsed_ms": 0,
430430
"fired": false,
431431
"rule_id": "RISK-008",
432-
"rule_version": "1.0.0"
432+
"rule_version": "1.1.0"
433433
},
434434
{
435435
"elapsed_ms": 0,
@@ -921,7 +921,7 @@
921921
"playbook_id": "lease-commercial-multitenant",
922922
"playbook_match_confidence": 0.9,
923923
"playbook_match_reasoning": "Selected lease-commercial-multitenant (score 0.9). Title matched: \"office lease\". Distinguishing phrases: \"Landlord\", \"Tenant\", \"Premises\", \"Rentable Square Feet\", \"Base Rent\", \"Operating Expenses\".",
924-
"result_hash": "f377529964aed62b9e9cfc2fed4382cef2429334e83cfbf5e8c2a727fb274e91",
924+
"result_hash": "4c666258dc5d6fcf02938955c0b83a5cb094b14878b20875018b124298a93a81",
925925
"source_file": {
926926
"name": "bad-lease-cam.docx",
927927
"sha256": "1de7804edf376ad66d1a78b0c998aa4a479a18835fa4e104e20057385337a0b6",

0 commit comments

Comments
 (0)