nx_obligation_lib.nx source
↩ module page · 112 lines · 5541 B
1// nx_obligation_lib.nx -- POST-SIGNATURE OBLIGATION MANAGEMENT (the DocuSign IAM / Ironclad Jurist class).
2// nx_clm catches the auto-renewal trap BEFORE a term ends. This is the other 95% of a contract's life:
3// the obligations it created. Most contract value leaks after signature, because the executed document
4// goes into a folder and the duties inside it are tracked by memory.
5//
6// u2605THE TOOTH MOST TRACKERS MISS -- THE CURE PERIOD. An obligation past its due date is NOT automatically
7// a breach. Commercial contracts routinely grant a cure window (often 10-30 days after written notice),
8// and during it the counterparty can still perform. A tracker that flags overdue AS breach manufactures
9// disputes that do not exist; one that ignores the cure period entirely misses the moment the right to
10// terminate actually vests. Both are expensive, so OVERDUE and BREACH are separate states here and the
11// cure window sits explicitly between them.
12//
13// u2605THE SECOND TOOTH -- NOTICE STARTS THE CLOCK, NOT THE DUE DATE. A cure period typically runs from
14// written notice of the failure, not from the missed deadline. An obligation that is overdue but where
15// notice was NEVER given has an unexpired (indeed unstarted) cure window, so it cannot be a breach yet.
16//
17// u2605FAIL-CLOSED: an obligation with no due date is OBL_UNKNOWN -- never "satisfied", never "fine". A
18// duty nobody dated is a duty nobody is tracking, which is precisely the failure mode.
19//
20// STRUCTURE: pure decision core, zero I/O, per [[feedback-pure-core-not-hostage-to-storage-2026-07-31]].
21// license_tier: ORIGINAL LIB.
22
23import "nx_matter_lib.nx"
24
25const OBL_UNKNOWN: i64 = 0 - 1
26const OBL_PENDING: i64 = 0
27const OBL_DUE: i64 = 1
28const OBL_OVERDUE: i64 = 2
29const OBL_BREACH: i64 = 3
30const OBL_SATISFIED: i64 = 4
31const OBL_WAIVED: i64 = 5
32
33const OBL_UNSET: i64 = 0 - 2000000002
34
35func obl_is1(v: i64) -> i64 {
36 if v == 1 { return 1 }
37 return 0
38}
39
40// u2605THE STATE MACHINE, resolved in priority order. Waiver and satisfaction are terminal and beat every
41// timing consideration; only then does the calendar matter.
42// notice_day = day written notice of failure was given (OBL_UNSET if never), cure_days = the contractual
43// window that runs FROM that notice.
44func obl_status_pure(due: i64, asof: i64, satisfied: i64, waived: i64, notice_day: i64, cure_days: i64) -> i64 {
45 if obl_is1(waived) == 1 { return OBL_WAIVED }
46 if obl_is1(satisfied) == 1 { return OBL_SATISFIED }
47 if due == OBL_UNSET { return OBL_UNKNOWN }
48 if asof == OBL_UNSET { return OBL_UNKNOWN }
49 if asof < due { return OBL_PENDING }
50 if asof == due { return OBL_DUE }
51 // past due. Breach requires that notice was given AND its cure window has expired.
52 if notice_day == OBL_UNSET { return OBL_OVERDUE }
53 if cure_days == OBL_UNSET { return OBL_OVERDUE }
54 if asof > (notice_day + cure_days) { return OBL_BREACH }
55 return OBL_OVERDUE
56}
57
58// Days remaining in the cure window; OBL_UNSET when no notice has started it.
59func obl_cure_remaining_pure(asof: i64, notice_day: i64, cure_days: i64) -> i64 {
60 if notice_day == OBL_UNSET { return OBL_UNSET }
61 if cure_days == OBL_UNSET { return OBL_UNSET }
62 if asof == OBL_UNSET { return OBL_UNSET }
63 return (notice_day + cure_days) - asof
64}
65
66// 1 only when the right to terminate has actually vested -- i.e. a true breach, not merely overdue.
67func obl_termination_right_pure(status: i64) -> i64 {
68 if status == OBL_BREACH { return 1 }
69 return 0
70}
71
72// An obligation performed AFTER its due date is still satisfied, but it was LATE. Tracking that
73// separately is what makes a pattern of lateness visible before it becomes a termination event.
74func obl_late_pure(due: i64, performed: i64) -> i64 {
75 if due == OBL_UNSET { return 0 }
76 if performed == OBL_UNSET { return 0 }
77 if performed > due { return 1 }
78 return 0
79}
80
81// Days late; 0 when on time, OBL_UNSET when undated.
82func obl_days_late_pure(due: i64, performed: i64) -> i64 {
83 if due == OBL_UNSET { return OBL_UNSET }
84 if performed == OBL_UNSET { return OBL_UNSET }
85 if performed <= due { return 0 }
86 return performed - due
87}
88
89// u2605PORTFOLIO EXPOSURE: only true breaches carry termination risk, but overdue items are the leading
90// indicator. Reported separately so a dashboard cannot blur one into the other.
91func obl_exposure_pure(overdue_count: i64, breach_count: i64, unknown_count: i64) -> i64 {
92 return overdue_count + breach_count + unknown_count
93}
94
95// A portfolio is CLEAN only when nothing is overdue, in breach, or undated. Undated counts against it
96// on purpose -- an untracked obligation is not a safe obligation.
97func obl_portfolio_clean_pure(overdue_count: i64, breach_count: i64, unknown_count: i64) -> i64 {
98 if obl_exposure_pure(overdue_count, breach_count, unknown_count) == 0 { return 1 }
99 return 0
100}
101
102func obl_status_label(s: i64, out: *u8) -> i64 {
103 if s == OBL_WAIVED { mt_catcopy(out, 0, "WAIVED" as *u8); out[6] = 0 as u8; return 6 }
104 if s == OBL_SATISFIED { mt_catcopy(out, 0, "SATISFIED" as *u8); out[9] = 0 as u8; return 9 }
105 if s == OBL_BREACH { mt_catcopy(out, 0, "BREACH" as *u8); out[6] = 0 as u8; return 6 }
106 if s == OBL_OVERDUE { mt_catcopy(out, 0, "OVERDUE" as *u8); out[7] = 0 as u8; return 7 }
107 if s == OBL_DUE { mt_catcopy(out, 0, "DUE" as *u8); out[3] = 0 as u8; return 3 }
108 if s == OBL_PENDING { mt_catcopy(out, 0, "PENDING" as *u8); out[7] = 0 as u8; return 7 }
109 mt_catcopy(out, 0, "UNKNOWN" as *u8)
110 out[7] = 0 as u8
111 return 7
112}