Skip to content

explain: make each refusal name itself - #74

Draft
ChelseaKR wants to merge 1 commit into
mainfrom
feat/48-explain-verb
Draft

explain: make each refusal name itself#74
ChelseaKR wants to merge 1 commit into
mainfrom
feat/48-explain-verb

Conversation

@ChelseaKR

Copy link
Copy Markdown
Owner

Closes #48.

What was wrong

coverage reports that a line went unread and gives a coarse reason — "3 of 13 lines in a recognized section matched no rule". That is true, and it does not say which recognizer got closest to the line or what stopped it. Reading the source was the only way to find out, which made a refusal legible to whoever wrote the parser and to nobody else. Refusals are this project's deliverable, so that was the wrong reader.

$ ca-tariff-parse explain tests/fixtures/SYNTHETIC-example-unclosed-bracket.txt \
      --profile pge-tariff-book 1:18
document        SYNTHETIC-example-unclosed-bracket
fences          1 of 5 reached on this document
lines explained 1

p.1 L18  section RATES
    | Example Tier 1 (Winter Only $6.6666
    refused by    sheet_rates at sheet_rates.bracket-unclosed (ADR 0014)
                  the row's label opens a bracket it never closes, and the line beneath it does
                  not close exactly one it did not open. A bracket that never closes states
                  nothing that can be read to an end, so the row carrying it is the first half
                  of a name and is refused rather than published
                  saw: Example Tier 1 (Winter Only
    declined      applicability, billing_periods, condition_list, credit, cross_reference,
                  dated_charge, proration, rate_table, transition_table

The fourth state is the design, and it is why there is no "nearest fence"

state what it means
consumed a recognizer claimed the line and emitted from it; it is named
refused a recognizer reached the line and a named fence stopped it — the fence, its ADR, its own reason, and what it saw on the page
examined a recognizer claimed the line's section, read it, and took nothing from this line, with no fence firing
unclaimed no recognizer claimed the line's section at all

refused means the page states something this parser will not read as written. unclaimed means there is no rule here whose shape matches the section, so no fence could have fired. That is the distinction ADR 0018 draws by hand — "unstated on the page" is not "unread by this parser" — and it is the reason explain never offers a closest rule. A nearest fence picked by proximity is a value invented from an absence, which is the defect this project exists to refuse. A control that makes an unclaimed line report refused turns two tests red.

Recording cannot change what parse emits

ca_tariff_parse.trace is the channel. Recording is off unless a caller opens it, and nothing in the package reads a trace back while parsing, so no decision the parser makes can depend on whether it was watched. Recognizers call refuse() unconditionally rather than behind if tracing: — one code path cannot drift from itself, and the call is a single context lookup when nothing is listening.

Asserted rather than argued: every committed fixture is parsed with and without a trace open and the emitted JSON is compared as bytes. A control that lets a recognizer see the trace and act on it reddens exactly those tests.

unparsed[].reason is untouched, and no flag changes it. The issue allows carrying a nearest fence there behind a flag; not done, because it would be a second refusal vocabulary alongside explain's, and the goldens and every baseline under data/parsed/ embed those strings.

Coverage, both numbers

The report prints how many of the parser's registered fences a document reached out of how many exist. A count of refusals says nothing about how much of the vocabulary an input exercised.

fixture lines fences states
SYNTHETIC-example-schedule-complete.txt 40 0/5 40 consumed
SYNTHETIC-example-schedule-unknown-section.txt 45 0/5 41 consumed, 4 unclaimed
SYNTHETIC-example-refused-rows.txt (new) 11 4/5 5 consumed, 3 refused, 1 examined, 2 unclaimed
SYNTHETIC-example-keyword-schedule.txt 32 0/5 14 consumed, 15 examined, 3 unclaimed
SYNTHETIC-example-unclosed-bracket.txt (new) 34 1/5 14 consumed, 19 examined, 1 refused

162 lines explained, 5 of 5 registered fences reached across the corpus, all four states produced. Before the two new fixtures the registry stood at 0 of 5 reached — five names nothing exercised, which is the shape this campaign keeps finding.

A fence was written and removed

rate_table.row-prices-nothing sat on the if not accounted branch at the end of _read_value_row. That function is called only when some word in the value area matched an amount or an explicit n/a, and every path through the loop for such a word either returns early or sets accounted — so the branch cannot be reached from its only caller. A fence there would have been a row explain --fences prints and can never report.

It is deleted, with a comment at the branch saying why rather than leaving the omission silent. test_every_registered_fence_fires_on_a_committed_fixture is what found it, and a control that registers a deliberately unreachable fence turns that one test red — so the census can see the thing it exists for.

Against the issue's "Done when"

  • "A synthetic fixture with a bracket left open explains the ADR 0014 fence by name"done, shown above, with a named test.
  • "Running explain then parse leaves tests/golden/ byte for byte unchanged"done, and stronger: the byte comparison runs on every fixture on every test run rather than being checked once.
  • "For every line of the four SMUD golden documents, explain names exactly one consuming recognizer or at least one examining recognizer with a fence; no line reports 'unknown'" — the second half of that is not achievable honestly, and this is the measured reason. A line in a section every recognizer declined has no fence to name, because no rule reached it; producing one would be the nearest-fence guess the same issue forbids two bullets earlier. What is achievable, and what is asserted over every fixture, is that every line lands in a named state and none reports "unknown" — with unclaimed carrying the recognizers that declined the section, so the answer is still specific. The clause needs amending on the issue and I have not edited it; flagging rather than quietly satisfying a weaker version.
  • The four SMUD documents themselves cannot be run here: sources/ is populated by make fetch and this repository does not redistribute them (ADR 0003). A green CI run is not evidence for that criterion and the fixtures above are what CI can see.

Fence vocabulary

Five fences, each carrying the ADR that decided it and a reason written about the page in the present tense — printed to somebody who has the PDF open and not the source. CONTRIBUTING.md gains the four rules for adding one, including that a fence must be reachable and that an unreachable branch gets a comment rather than a name.

Negative controls

Committed first; every sabotage asserted to have changed the blob and every restore verified by comparing git hash-object with git rev-parse HEAD:<path>.

sabotage predicted measured
register a fence nothing can reach the census only red on test_every_registered_fence_fires_on_a_committed_fixture, nothing else
let a recognizer see the trace and change its emission the byte-identity tests red on test_a_traced_parse_emits_the_same_bytes_as_an_untraced_one for the two fixtures whose output moves, nothing else
report an unclaimed line as refused the nearest-fence guard red on test_a_line_no_recognizer_claimed_says_so_rather_than_naming_a_nearest_fence and test_every_state_is_produced_by_the_committed_corpus — the second because the vocabulary loses a state
stop recording the recognizers that declined a section the same guard, from the other side red on test_a_line_no_recognizer_claimed_says_so_rather_than_naming_a_nearest_fence, on its "examined by nobody either" assertion

make verify: 649 passed, 65 skipped, 93.72% coverage, exit 0. The 65 skips are the real-document spot checks, which need make fetch.

Prepared with AI assistance; reviewed before submission.

A refusal is this project's deliverable and it was legible only in the
source. `coverage` says a line went unread and gives a coarse reason -- "3 of
13 lines in a recognized section matched no rule" -- and nothing says which
recognizer got closest or what stopped it.

`explain <doc> p.3 L11` answers both. Every line lands in one of four states:
consumed, refused at a named fence, examined (a recognizer claimed the
section, read it and took nothing from this line, with no fence firing), or
unclaimed (no recognizer claimed the section at all, so no fence could have
fired).

The fourth state is why this does not offer a nearest fence. A closest rule
picked by proximity is a value invented from an absence, and the difference
between "unstated on the page" and "unread by this parser" is the one ADR
0018 draws by hand.

trace.py is the channel. Recording is off unless a caller opens it, nothing
reads a trace back while parsing, and recognizers call refuse() with no
conditional so there is one code path rather than two that can drift. That is
what makes `parse` byte-identical either way, asserted over every committed
fixture rather than argued.

Five fences are registered, each with the ADR that decided it and a reason
written about the page. A sixth was written and removed: rate_table's
row-prices-nothing sits on a branch its only caller cannot reach, so it would
have been a row `explain --fences` prints and can never report. The census
test names any registered fence no fixture trips, which is what found it.
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.

Add an explain verb naming the recognizer and refusal fence behind any line

1 participant