Skip to content

Commit 8238c45

Browse files
clay-goodclaude
andcommitted
fix(rules): CHOICE-007 detect passive & long-parenthetical class-action waivers (v1.2.0)
v1.1.0 matched only the noun "class action waiver" or an active "waive … class action" with the object within 40 chars of the verb. It missed the passive "the right to a class action is hereby waived" and the boilerplate "waives, to the fullest extent permitted by law, any right to bring or participate in a class action" (the long parenthetical pushed the object past 40 chars). Widen the active window to a sentence-bounded 100 chars and add a passive branch tempered so a negated "class action is NOT waived" cannot match — the rule's inline-negation guard only inspects text before the match, not a negation between "class action" and "waived". Mirrors the CHOICE-008 v1.2.0 jury-waiver fix; the consumer-heading gate is unchanged. Probe: 5 fire cases (noun + active + passive + long-parenthetical + expressly- waived), 4 silent cases (do-not-waive, nothing-herein, no-waiver, passive-not- waived) plus the non-consumer-scope guard. Finding-delta over the contracts corpus and all golden tiers: zero fired-flips, zero finding-count changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8fb3225 commit 8238c45

367 files changed

Lines changed: 437 additions & 392 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: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { describe, expect, it } from "vitest";
2+
import { rule as CHOICE_007 } from "./CHOICE-007.js";
3+
import { buildContext } from "../../_test-fixtures.js";
4+
5+
const fires = (t: string) =>
6+
CHOICE_007.check(buildContext(["Consumer Terms of Service", t])) !== null;
7+
8+
describe("CHOICE-007 — class-action waiver recognizes passive & long-parenthetical forms (v1.2.0)", () => {
9+
it.each([
10+
"You agree to a class action waiver.",
11+
"You waive any right to a class action.",
12+
"The right to bring a class action is hereby waived by you.",
13+
"You waive, to the fullest extent permitted by law, any right to bring or participate in a class action.",
14+
"Any right to participate in a class action is expressly waived.",
15+
])("fires on a class-action waiver in a consumer contract: %s", (t) => {
16+
expect(fires(t)).toBe(true);
17+
});
18+
19+
it.each([
20+
"You do not waive your right to a class action.",
21+
"Nothing herein waives your right to a class action.",
22+
"This agreement does not contain a class action waiver.",
23+
"The right to a class action is not waived.",
24+
])("stays silent on a preserved / negated class-action right: %s", (t) => {
25+
expect(fires(t)).toBe(false);
26+
});
27+
28+
it("does not fire outside a consumer-facing document", () => {
29+
expect(
30+
CHOICE_007.check(
31+
buildContext([
32+
"Master Services Agreement",
33+
"The parties waive any right to a class action.",
34+
]),
35+
),
36+
).toBeNull();
37+
});
38+
});

src/engine/rules/choice-and-venue/CHOICE-007.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const CONSUMER_HEADINGS = /\b(lease|residential|terms\s+of\s+service|employment|
99
/** CHOICE-007 — Class-action waiver in consumer-facing contract (warning). */
1010
export const rule: Rule = {
1111
id: "CHOICE-007",
12-
version: "1.1.0",
12+
version: "1.2.0",
1313
name: "Class-action waiver in consumer contract",
1414
category: "choice-and-venue",
1515
default_severity: "warning",
@@ -20,7 +20,14 @@ export const rule: Rule = {
2020
if (!isConsumer) return null;
2121
const hit = firstParagraphMatch(
2222
ctx,
23-
/\bclass\s+(?:action\s+)?waiver\b|\bwaive[\s\S]{0,40}class\s+action\b/i,
23+
// v1.2.0 widens the active window from 40 to a sentence-bounded 100 so the
24+
// boilerplate "waives, to the fullest extent permitted by law, any right …
25+
// to bring or participate in a class action" reaches its object, and adds
26+
// a passive branch ("the right to a class action is hereby waived")
27+
// tempered so a negated "class action is NOT waived" cannot match — the
28+
// inline-negation guard below only inspects the text BEFORE the match, not
29+
// a negation sitting between "class action" and "waived".
30+
/\bclass\s+(?:action\s+)?waiver\b|\bwaive[^.]{0,100}class\s+action\b|class\s+action(?:(?!\bnot\b|\bnever\b|\bno\b)[^.;]){0,40}\bwaived\b/i,
2431
);
2532
if (!hit) return null;
2633
// A contract that PRESERVES class rights trips the same words — "you do NOT

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"elapsed_ms": 0,
4444
"fired": false,
4545
"rule_id": "CHOICE-007",
46-
"rule_version": "1.1.0"
46+
"rule_version": "1.2.0"
4747
},
4848
{
4949
"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": "eb95c14e194c15d98919d8928175e34011ad8c877377c37d64f08f33edaa9145",
977+
"result_hash": "adad0c96cfd6f7289969de77d24c003178b1f23d3246e0b4fdbfdec4d6ebc39c",
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
@@ -43,7 +43,7 @@
4343
"elapsed_ms": 0,
4444
"fired": false,
4545
"rule_id": "CHOICE-007",
46-
"rule_version": "1.1.0"
46+
"rule_version": "1.2.0"
4747
},
4848
{
4949
"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": "20869b94587a65c62aec95b0b5d256e7cf5621b569d2ff0d7d45727c9bb2d361",
1058+
"result_hash": "94b8d27e4f3395fef9e3e42fb6770348f65d8fce1580d4b9fd626085082e60c0",
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
@@ -43,7 +43,7 @@
4343
"elapsed_ms": 0,
4444
"fired": false,
4545
"rule_id": "CHOICE-007",
46-
"rule_version": "1.1.0"
46+
"rule_version": "1.2.0"
4747
},
4848
{
4949
"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": "fe8e424cdad76efb7034af019942554f6bcacb41fa2b003321b84bef05d9ab56",
885+
"result_hash": "2a2685d0c57c96680c33725a30d3672364f67872c10ee5c1be1376321fe4bfe9",
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
@@ -43,7 +43,7 @@
4343
"elapsed_ms": 0,
4444
"fired": false,
4545
"rule_id": "CHOICE-007",
46-
"rule_version": "1.1.0"
46+
"rule_version": "1.2.0"
4747
},
4848
{
4949
"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": "970e59bbf68d3908ad73dfca97f5021fd4d400d831b99bd93f8dbcee01d668f6",
1087+
"result_hash": "2f9d959bc03b325a64f7a595de8b1f954c3e79a33ae38ad86062382393736253",
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
@@ -42,7 +42,7 @@
4242
"elapsed_ms": 0,
4343
"fired": false,
4444
"rule_id": "CHOICE-007",
45-
"rule_version": "1.1.0"
45+
"rule_version": "1.2.0"
4646
},
4747
{
4848
"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": "84f0286c6334110df5ccd2cf2e018543704405458e43555ec7c05a28ea5f9531",
931+
"result_hash": "f8fc1b7370319c1cbedacb061745653506d1982cdc69f4cce3e859c8573ddef8",
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
@@ -44,7 +44,7 @@
4444
"elapsed_ms": 0,
4545
"fired": false,
4646
"rule_id": "CHOICE-007",
47-
"rule_version": "1.1.0"
47+
"rule_version": "1.2.0"
4848
},
4949
{
5050
"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": "401df9c40bb20755bb46a594684fa5d56acaab4c60b7f66b70931aada65080e5",
1030+
"result_hash": "d5b1ba1d0c6833fc40deacf002d0924106995495aec3d283ff2166f972bb50dc",
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
@@ -43,7 +43,7 @@
4343
"elapsed_ms": 0,
4444
"fired": false,
4545
"rule_id": "CHOICE-007",
46-
"rule_version": "1.1.0"
46+
"rule_version": "1.2.0"
4747
},
4848
{
4949
"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": "8be7a448b53b38d4d23fbfc1d4520f52493999dd55b44b86f0f7eac7862afdab",
1235+
"result_hash": "1cf5dc7e168f9ecd805d6ae9b723e212d852b25f4bcf035d8d46e2464289d9b3",
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
@@ -43,7 +43,7 @@
4343
"elapsed_ms": 0,
4444
"fired": false,
4545
"rule_id": "CHOICE-007",
46-
"rule_version": "1.1.0"
46+
"rule_version": "1.2.0"
4747
},
4848
{
4949
"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": "843d4747d1a4a8d523ebb6942f03ad59390c88f5805c7c5edf9ee3b0bec3e71f",
924+
"result_hash": "0ad2f89854ba7516f44d336a8e933d121ebb2df5a18eb2d68cef1d9af028c3b2",
925925
"source_file": {
926926
"name": "bad-lease-cam.docx",
927927
"sha256": "1de7804edf376ad66d1a78b0c998aa4a479a18835fa4e104e20057385337a0b6",

0 commit comments

Comments
 (0)