Skip to content

M4: calendar arithmetic in the rule IR; the TILA pack computes its deadline - #17

Merged
kjpatel merged 2 commits into
mainfrom
m4/calendar-arithmetic
Jul 30, 2026
Merged

M4: calendar arithmetic in the rule IR; the TILA pack computes its deadline#17
kjpatel merged 2 commits into
mainfrom
m4/calendar-arithmetic

Conversation

@kjpatel

@kjpatel kjpatel commented Jul 30, 2026

Copy link
Copy Markdown
Owner

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 as pack-embedded data: excluded weekdays + explicit holiday dates (each with a source comment) + a hard coverage window, inside the pack's own 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.
  • One companion the rewrite genuinely needed: an { asOf: effective } given-binding so rules can compare against the evaluation date (records no premise — the receipt's asOf already pins it; a now() function was the rejected alternative).
  • The TILA pack at 2026.2.0: RESC-DL-01 computes midnight of the third precise business day after the latest trigger; funding rides the computed deadline instead of effective-dated window facts (now gone from rules, generator, starter, and corpus). The printed notice date survives as a cross-check — the demo warns on mismatch while the computed date stands. The MODELING BOUNDARY header 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).
  • One behavior change of record, deliberate and fixture-proven: a misprinted notice deadline used to force a loud no-decision; now the computed deadline stands and the misprint is flagged for correction — a new fixture pins both sides.

Churn proof (verified independently at review)

66 golden files changed, all resc-*; every other series byte-untouched. impact over 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.py executed 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.0 describes the old certify-don't-compute behavior in one slot description; a 0.2.0 would correct the wording and could add a printedDeadlineMatches slot. Noted in the pack header.

🤖 Generated with Claude Code

kjpatel and others added 2 commits July 30, 2026 09:40
…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>
@github-actions

Copy link
Copy Markdown

Rule-change impact analysis

0 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)

@kjpatel
kjpatel merged commit 7a87da4 into main Jul 30, 2026
2 checks passed
@kjpatel
kjpatel deleted the m4/calendar-arithmetic branch July 30, 2026 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant