Skip to content

Python citation block parser + shared citations.json fixture - #230

Open
jat255 wants to merge 7 commits into
mainfrom
jat255/yq4g-citation-block-parser
Open

Python citation block parser + shared citations.json fixture#230
jat255 wants to merge 7 commits into
mainfrom
jat255/yq4g-citation-block-parser

Conversation

@jat255

@jat255 jat255 commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds parse_commons_citation() to the Python package. The citation grammar and the commons.citation.candidates record move into a shared fixture (tests/shared/citations.json) that both suites read.

R changes are tests only — no functional R code changed.

What changed

  • Python: new parse_commons_citation() and a frozen, self-validating CitationDecision dataclass. Status/kind vocabularies come from Literal aliases via get_args(); label/kind exist only for accepted decisions, and only malformed decisions lack a quote.
  • Shared: parser test cases and the candidates-record shape promoted to tests/shared/citations.json.
  • R:
    • test-citation-scan.R now reads parser cases from the fixture via shared_fixture().
    • Three hand-written tests were moved to the shared fixture with identical bahavior
    • Seven new cases added: One new test checks the JSON structure of a citation-decision record. These records are written to trace spans and read back by trajectory_read(); no test previously verified the two sides agree on the field names, so a mismatch would have failed silently.

Citation block format

The shared test fixtures enforce the following behavior:

A citation consists of exactly one contiguous run of > lines holding the verbatim evidence; all other lines are explanation and never verified. Zero or two runs is malformed. Two details worth noting:

  • A wrapped quote parses: the continuation line becomes explanation, never verified.
  • The marker is ^> ? with no indentation allowance; an indented > line makes the body malformed.

Known divergence between R and Python

Explanation trimming uses each language's whitespace class, so a trailing U+00A0 survives in R but not Python. Explanation is never verified, so this is cosmetic; it's documented in the fixture.

Reviewer notes (for R)

  • The new decision test drives all three producers (accepted/rejected/malformed) from the fixture rather than a hand-built list — more indirection than usual, but the point is that the real producers (for both R and Python) agree with the recorded shape. I'm open to feedback on whether those three statuses fully describe what citation_scanner() records.
  • A reminder that any files under tests/testthat/fixtures/shared/ are generated by scripts/sync-shared-fixtures.sh, so they don't need to be separately reviewed.

Testing

  • Python: 68 tests, ruff, pyrefly — all pass.
  • R: test-citation-scan.R passes in full (25 tests, 217 expectations) against the rebased tree. R CMD check not run locally (missing R deps); left to CI.
jat255 added 5 commits August 31, 2026 16:26
Adds parse_commons_citation() and the CitationDecision record to the Python
package, with the grammar pinned as a cross-language fixture that both suites
run.

The plan's Task 4 sketch disagreed with the R implementation on two points, and
the R source wins because this is a contract between the two packages:

- A wrapped quote (a blockquote run followed by an un-prefixed line) parses
  rather than failing as malformed. The continuation becomes explanation, so it
  is never verified. Nothing unverified is presented as verified.
- The marker is `^> ?` with no indent allowance, so an indented `>` line is not
  evidence and its body is malformed.

CitationDecision carries only the statuses R records (accepted, rejected,
malformed); the plan also listed "missing", which nothing produces.

On the R side this is tests only. The three hand-written parser tests are
replaced by a runner over the shared fixture, which covers those cases and
seven more, and a new test pins the commons.citation.candidates record shape
that the trajectory reviewer reads.
roborev job 104 on 2ac4c9b, three low findings, all applied.

CitationDecision is frozen like its siblings and now rejects a record that
misreports its verdict: label and kind belong to an accepted decision only, and
only a malformed one has no quote. The record is what the R trajectory reviewer
reads back out of the span, so a wrong shape fails silently there.

The fixture guard pins the set of statuses rather than a case count, so three
accepted cases can no longer leave the omission logic uncovered. Both suites
assert it.

The explanation is trimmed with each language's own whitespace class, so a
trailing U+00A0 survives in R and not in Python. Recorded in the fixture as an
accepted divergence rather than engineered away: the explanation is never
verified, so the difference is cosmetic.
roborev job 106 on e78039f, two low findings, both applied.

Literal is not enforced at runtime, so a typo in a status reached as_record()
and put a value on the span that the R reviewer cannot interpret. The class is
the only place that can be caught, so it checks the vocabulary before checking
the field combination.

The half-populated accepted shape (a label with no kind) already raised; it is
now pinned, because that is the shape a producer would build by accident when
copying fields off a corpus entry.
roborev job 108 on 9730e3e. The vocabulary was written twice, so the annotation
and the runtime guard could drift in either direction. CitationStatus now sits
next to CitationKind and the guard reads get_args() off it.
roborev job 110 on c326257. kind was the last field of the record left
unconstrained while its sibling CorpusEntry.kind already used CitationKind, so
a kind the R reviewer cannot interpret could be serialized onto the span.

Every field that reaches as_record() is now checked at construction. The guard
belongs on the record rather than further upstream, because the record is what
crosses into the trace.
@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown

Preview deployed to Connect (dogfood.team.pct.posit.it): https://dogfood.team.pct.posit.it/connect/#/apps/d7a36cae-8f27-448b-a478-61b81fbe3942/draft/366423

Deployed from commit cc6a499.

@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown

Preview deployed to Connect (connect.staging.pct.posit.it): https://connect.staging.pct.posit.it/connect/#/apps/ad662e1b-5048-4acc-9ad7-f9478c92274e/draft/2429

Deployed from commit cc6a499.

jat255 added 2 commits August 31, 2026 16:57
Branching on the corpus rather than switching over the whole producer call
expresses the same three assertions in a third less code. Coverage is
unchanged: 217 expectations in the file before and after, four of them from
this test.
The file repeated two different sample passages across ten literals. The tests
that pin shared behavior read their inputs from tests/shared/citations.json;
the rest only need some quote, so they now share one named sample, as the R
suite already does with scanner_test_quote.

test_matching_returns_the_entry_that_verified no longer builds a two-entry
corpus. Which entry wins is pinned by the fixture's first-match-wins case, so
restating it here was a per-language copy of shared behavior. The test keeps
only its own claim, that the matched entry itself comes back.
@jat255 jat255 added r Affects the R implementation py Affects the Python implementation labels Aug 31, 2026
@jat255 jat255 changed the title Strict commons-citation block parser Aug 31, 2026
@jat255
jat255 marked this pull request as ready for review September 1, 2026 00:46
@jat255
jat255 requested a review from simonpcouch September 1, 2026 00:47
@jat255

jat255 commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

@simonpcouch this includes a minor change to R citation tests since they are using a the shared fixture with the Python side. It also added a few new tests to ensure citation formats match in both implementations. There's no functional changes in runtime R code, so it's all additive, but just an FYI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

py Affects the Python implementation r Affects the R implementation

1 participant