Skip to content

feat: a calendar of the rules the page states, and a list of the rest - #64

Merged
ChelseaKR merged 1 commit into
mainfrom
feat/calendar-rules-where-a-bare-rule-is-stated
Sep 6, 2026
Merged

feat: a calendar of the rules the page states, and a list of the rest#64
ChelseaKR merged 1 commit into
mainfrom
feat/calendar-rules-where-a-bare-rule-is-stated

Conversation

@ChelseaKR

Copy link
Copy Markdown
Owner

What was missing

Downstream tools that compute over a time-of-use schedule need machine-checkable
time rules. The parser reads them and emits them as prose plus, where the
document states a bare range, a start and an end.

This is the first consumer-facing derivation the project ships, which makes
the fence more important than the feature: a calendar that quietly omits the
windows it could not express looks like a complete calendar.

What changed

ca-tariff-parse calendar tests/golden/smud-r-tod.json --dir dist/calendar

writes smud-r-tod.ics and smud-r-tod.refused.json — both, always. A
missing refusal file reads as "no refusals"; an empty list reads as "nothing was
refused". Those are different statements and only one of them is a measurement.

A VEVENT is written only where it re-expresses text the parser already
committed to. Everything else is refused, with the reason, the citation and the
verbatim text it refused:

  • a residual window is refused because the parser already refused to give it
    hours, and deriving them here would undo that refusal;
  • a window defined by exception ("Weekdays between noon and midnight except
    during the Peak hours") is refused — the exception is prose this module cannot
    subtract;
  • a window with hours but no stated day type is refused rather than assumed
    daily;
  • a window whose end is at or before its start crosses midnight, and
    splitting it is a decision the document did not make;
  • a holiday day_rule outside a closed grammar (fixed day, ordinal weekday,
    last weekday) is refused. Day after Thanksgiving is a real rule with a
    defensible date and it is not in the grammar, so it is refused rather than
    rendered as the fourth Friday. A guess in a calendar entry has a calendar
    entry's authority.

Season bounds only where the season names whole months. BYMONTH selects
whole months and nothing else, so Summer (Jun 1 - Sept 30) becomes
BYMONTH=6,7,8,9, while Jun 15 - Sept 30 would have to be widened or narrowed
and is left unbounded and marked X-CA-SEASON-BOUNDS:partial.

No time zone is inferred, and the file says so. Recurrences are anchored to
1970 because a published rule states no year; the file says only the month, day
and time of a DTSTART mean anything, and every DTSTART is a real instance of
its own rule so a reader cannot treat it as an extra occurrence.

Nothing reads the clock. DTSTAMP is the document's own retrieval date, or
the epoch when it states none — a generation time would make two renderings of
one parse differ.

A watch baseline is refused outright: it omits the verbatim prose, and a refusal
that cannot quote what it refused is not one.

On the real documents

document windows rendered holidays rendered refused
smud-r-tod 2 of 5 11 of 11 3
smud-ci-tod1 0 of 5 11 of 11 5
smud-r, smud-ssr 0 of 0 0 of 0 0

smud-ci-tod1 states no bare times at all, so its calendar has no windows in it
and the refusal file says why for each of the five.

How it was verified

ruff check, ruff format --check, mypy (strict, 28 files), pytest — all
green, coverage 94.32% against an 85% floor. 42 new tests. No golden file or
baseline changed.

Negative controls, each read back out of the file before the gate ran, then
restored from a pre-edit byte copy (diff clean) and re-run green:

sabotage gate
residual fence removed (if False:) red — 2 tests
out-of-grammar day rule approximated as the fourth Friday red — 4 tests
a missing day type assumed to be every day red — 2 tests
a partial month range widened to whole months red
the refusal list dropped from the payload red

One of these found a real weakness in my own test and it is worth naming.
The first run of the residual sabotage came back green. In every committed
document a residual window happens to carry no start/end, so removing the
residual fence just let the next fence catch it — the test was asserting the
outcome, not the fence. I added a synthetic residual window that does carry
hours and a day type, plus a test that the refusal names exclusion rather than a
missing time. With those, the sabotage goes red. The fence is now gated; before,
it was decoration that happened to be correct.

Closes #57

Prepared with AI assistance; reviewed before submission.

The first consumer-facing derivation this project ships, so the fence is the
feature. `calendar` writes an .ics beside a refusal file, both always: a
missing refusal file reads as "no refusals" and an empty list reads as
"nothing was refused", and those are different statements.

A VEVENT is written only where it re-expresses text the parser already
committed to. A residual window is refused on the residual flag itself, not on
the absence of times, so it stays refused even carrying hours. A window defined
by exception, a window with hours but no stated day type, and a window whose
end is at or before its start are each refused with the reason and the
citation. A holiday day rule outside a closed grammar is refused rather than
approximated: rendering "Day after Thanksgiving" as the fourth Friday would
give a guess a calendar entry's authority.

Season bounds only where the season names whole months, since BYMONTH can
express nothing else; anything narrower is left unbounded and marked partial.
No time zone is inferred. Recurrences are anchored to 1970 because a published
rule states no year, and every DTSTART is a real instance of its own rule.
Nothing reads the clock, so the same parse renders identical bytes.
@ChelseaKR
ChelseaKR merged commit b4dbda5 into main Sep 6, 2026
3 checks passed
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.

Render TOU windows and holidays as iCalendar rules where a bare rule is stated

1 participant