M4: calendar arithmetic in the rule IR; the TILA pack computes its deadline - #17
Merged
Conversation
…adline The roadmap's acceptance test, verbatim: rewrite the TILA pack to COMPUTE the rescission deadline instead of certifying an extracted one. Before this, the IR could not walk a calendar, so pack <= 2026.1.0 certified the notice's printed date against a calendar-day lower bound and rode extraction-derived rescission-period window facts for the funding flip — the MODELING BOUNDARY header disclaimed exactly this gap. What ships: - expr.py: add_business_days(date, n, "calendar") — deterministic, pure, and convention-free: which days count is entirely the calendar's data (excluded weekdays + explicit holiday dates). n=0 returns the start, negative/fractional n is a type error, and any walk touching a day outside the calendar's declared [from, to) coverage fails loudly (ExprCalendarError), never silently treating an unlisted holiday as a business day. - Calendars are pack-embedded data (spec/rule-ir.md "Calendars"): inside the pack file, so inside rulePack.version on every receipt and replayed trivially — no registry machinery. The duplication-across-packs trade and the future shared-registry shape are documented. ir.py validates the block and statically checks every calendar reference (unknown name, non-literal, wrong arity = PackValidationError). - New `asOf: effective` given-binding: binds the evaluation date as a date value so rules can compare a computed deadline against "now as evaluated". No premise recorded — the receipt's top-level asOf already pins it. Documented with a rejected-alternatives note (no now() function; nothing reads a clock). - Pack 2026.2.0: embeds the 12 CFR 1026.2(a)(6) precise calendar (Sundays + 5 U.S.C. 6103(a) statutory dates for 2026-2027, no 6103(b) observance shifting per Official Staff Commentary 2(a)(6)-2, source comment per date). RESC-DL-01 v2 computes midnight of the third precise business day after the latest trigger; RESC-FUND-STAY/EXP v2 compare the computed deadline against the asOf date; the window facts are gone from the rules. resc:statedRescissionDeadline remains an extracted fact no rule consumes: the demo surfaces the printed-vs- computed cross-check, and a new misprinted-deadline fixture proves a wrong printed date can no longer mislead the deadline decision (it used to force a loud no-decision; now the computation stands). The MODELING BOUNDARY header now says what moved and what still has not: state-law variants, observance shifting, 3-year extension, waivers, and the 2026-2027 coverage limit. - Generator: resc template stops emitting window facts, keeps emitting the printed date, places asOf relative to the true computed deadline; a new test holds the pack's embedded calendar reconciled with the generator's independent 6103(a) derivation AND the kernel's walk over every corpus draw date. - Starter re-targeted (window facts removed; check_facts green), demo resc phrasings updated (deadline computed, not certified; printed-date cross-check detail), spec/calendar_demo.py adjudicates the starter across the Memorial-Day window and prints the funding flip (executed: hold through 5/27, clear on 5/28, protective default without the notice fact). Churn, deliberate and proven: impact over the OLD baseline reported 0 of 351 decisions flip, 2 reasoning-only changes (resc-0023: STAY no longer fires when rescission does not apply; resc-0025: the misprint case now concludes a computed deadline instead of refusing). Corpus regenerated (seed 7): 66 files changed, all resc-* (40 window-fact deletions, 25 receipts, 1 case.yaml asOf re-based to the computed deadline); notice-*, trid-*, ron-*, esign-*, rec-*, review-* are byte-untouched. verify: 351/351 on the new baseline; impact clean. Tests: kernel 204 (38 new calendar/asOf tests, 2 new expectation cases), demo 24, assurance 27 — full suite 469 passed, 6 skipped (docling-gated). Pinned-hash no-policy byte-identity and the review-0001 replay still pass, proving calendar-free evaluation is byte-unchanged. spec/validate.py, check_facts.py, and conformance (1478 facts) all green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ng reshape README: the combined Z3/OR-Tools bullet splits four ways — calendar arithmetic ticked (a function, not a solver; the deterministic evaluator was its right home), Z3 static verifier, Z3 what-if with the examples as consumer, OR-Tools scheduling as reference wiring. The sequencing principle gains its gloss: a consumer is a workload, ours or an adopter's, not a hypothesis. CLAUDE.md gotcha (coverage is a wall), glossary and FAQ entries (holiday dates are legal content, not code). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rule-change impact analysis0 of 351 decisions flip; 0 reasoning-only changes (351 golden cases analyzed) No decisions flip under this change. Reasoning-only changes: 0 (same decision, different rules fired or defeated sets) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fourth M4 piece, and the roadmap's own acceptance test fulfilled: the TILA pack now computes the § 1026.23 rescission deadline it previously only certified from an extracted date. Also carries the agreed constraint-solving reshape: the old combined Z3/OR-Tools bullet conflated a function with a solver — deterministic date math belongs in the evaluator (this PR), and the solver's real jobs (static proof, inverse queries, scheduling) each get their own roadmap item with the examples as first consumer.
What shipped
add_business_days(date, n, calendar)in the expression layer — pure, deterministic, loud at every edge: n=0 returns the start, negative/fractional n rejected, any examined day outside the calendar's coverage window raises rather than silently wrapping.calendars:block — versioned with the pack, inside receipt provenance, replayable forever. "Last Monday in May" is legal content, not engine code; TILA's precise business day deliberately excludes § 6103(b) observance shifting per the Reg Z commentary, and a three-way test reconciles the pack's calendar against the corpus generator's 6103(a) derivation.{ asOf: effective }given-binding so rules can compare against the evaluation date (records no premise — the receipt'sasOfalready pins it; anow()function was the rejected alternative).MODELING BOUNDARYheader was rewritten, not removed: the boundary moved (state-law variants, observance shifting, the 3-year extension remain out, and coverage is 2026–2027 with a loud error outside).Churn proof (verified independently at review)
66 golden files changed, all
resc-*; every other series byte-untouched.impactover the old baseline: 0 of 351 decisions flip (2 reasoning-only changes, both explained). Replay green on the regenerated baseline; post-regeneration impact fully clean.Verification
469 passed, 6 skipped (38 new kernel tests); pinned-hash no-policy byte-identity and review-0001 replay both still pass — the evaluator changes provably perturbed nothing existing.
spec/calendar_demo.pyexecuted at review: deadline midnight-of-2026-05-27, funding held through 5/27 including 5/26 where naive calendar math funds wrongly, clear on 5/28, protective default when the notice fact is stripped.Flagged for later (not in this PR)
The immutable ontology
duly-mortgage-closing@0.1.0describes the old certify-don't-compute behavior in one slot description; a 0.2.0 would correct the wording and could add aprintedDeadlineMatchesslot. Noted in the pack header.🤖 Generated with Claude Code