Skip to content

H1 reslice: close feed grammar and kind states - #44

Closed
Pigbibi wants to merge 2 commits into
mainfrom
feat/pert-upstream-feed-primitive-status-h1-r3
Closed

H1 reslice: close feed grammar and kind states#44
Pigbibi wants to merge 2 commits into
mainfrom
feat/pert-upstream-feed-primitive-status-h1-r3

Conversation

@Pigbibi

@Pigbibi Pigbibi commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Scope

Fresh reslice from latest origin/main@294e30b8eed36f070299b1dc01d34955fb262918; PR #43 is closed/frozen evidence and was not reopened or reused.

This producer-boundary slice:

  • requires an RSS2 root to contain exactly one unqualified channel child and rejects every extra root child;
  • validates RSS/Atom direct children with closed allowlists, rejecting near-match or potential item/entry containers instead of silently ignoring them;
  • preserves explicit rss2/atom for zero-entry feeds;
  • allows unknown feed kind only for failed outcomes; accepted/quarantined outcomes require rss2 or atom and contribute zero rows/digest when quarantined;
  • keeps canonical primitive/status bytes and sanitized fail-closed validation.

No workflow, publish, QAR, weekly, permission, or automation changes are included.

Validation

  • focused pytest: 25 passed
  • full pytest: 133 passed
  • compileall: passed
  • git diff --check: passed
  • ruff: unavailable in environment

Co-Authored-By: Codex <noreply@openai.com>
@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

🤖 Codex PR Review

🚫 Merge blocked: 1 serious issue(s) found in high-risk files

⚖️ Codex Review Arbitration

🚫 block: parse_feed_snapshot() now preserves empty RSS/Atom feeds as parsed feeds, and fetch_rss_sources() records them as {"state": "quarantined", "error_code": "zero_entries"} rather than failed. The new status contract in feed_primitives.build_status() also treats quarantined as a distinct state from failed. But fetch_rss_sources() still executes if feed_records and not any(record["state"] == "accepted" ...) and then raises RuntimeError("all configured RSS/Atom feeds failed") whenever any(record["state"] == "failed" ...) is true. With one quarantined feed and one failed feed, that branch raises even though not every configured feed is in failed state. That contradicts the new state model and changes the prior producer behavior where zero-entry feeds counted as successful enough to avoid the all-failed error. The prior blocking finding about blank author handling is separately addressed by PrimitiveRow.from_mapping(... allow_empty=key == "author"), so there is no contract conflict.

🚫 Blocking Issues

These issues must be fixed before this PR can be merged:

1. 🟠 [HIGH] Logic in src/political_event_tracking_research/rss_source_fetch.py

The new if feed_records and not any(record["state"] == "accepted" ...) branch raises RuntimeError("all configured RSS/Atom feeds failed") whenever there are no accepted rows and at least one failed feed. That incorrectly turns a run with only quarantined feeds plus one failure into a hard total-failure, even though quarantined zero-entry feeds are a distinct successful parse outcome in this PR. The previous code treated zero-entry feeds as successful, so this changes producer behavior and can fail publication/jobs on otherwise valid empty feeds. (line 309)

Suggestion: Only raise the all configured RSS/Atom feeds failed error when every configured feed is actually in failed state. If there are quarantined feeds but no accepted rows, keep writing the empty CSV/status and return normally.

ℹ️ Other Findings

1. 🟡 [MEDIUM] Logic in src/political_event_tracking_research/feed_primitives.py

_validate_wire() range-checks rejected_row_count but never enforces its semantic value. As a result, serialize_status() and parse_status_bytes() will accept payloads with arbitrary non-zero rejected_row_count values at both feed and top level, even though build_status() always emits zero and the PR describes this validator as fail-closed/canonical. That allows inconsistent status JSON to be blessed as canonical. (line 220)

Suggestion: Add explicit invariants for rejected_row_count: in this slice it should be 0 for every feed and for the top-level status, or at minimum the top-level value should equal the sum of per-feed rejected counts and the per-feed values should be constrained by state.


Review by Codex PR Review bot • PR

Co-Authored-By: Codex <noreply@openai.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 491350b9c6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +245 to +249
error_code = item["error_code"]
if error_code is None and state != "accepted":
_error("feed_error_invalid")
if error_code is not None and (type(error_code) is not str or not _ERROR.fullmatch(error_code)):
_error("feed_error_invalid")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject error codes on accepted feeds

When validating a wire status supplied to serialize_status() or parse_status_bytes(), this only requires an error for non-accepted feeds and never rejects error_code on an accepted feed. That admits a canonical status that says a feed is successful, increments successful_feed_count, and can set publication_complete/eligible_for_live_publication to true while still carrying error_code: "fetch_failed"; build_status() itself forbids that combination, so external or mutated status artifacts can become internally contradictory and mislead downstream health gates.

Useful? React with 👍 / 👎.

Comment on lines +299 to +303
DefusedXmlException,
ET.ParseError,
LookupError,
OSError,
RuntimeError,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep HTTPException failures continuable

In the RSS workflow I checked, .github/workflows/rss_source_pipeline.yml passes --continue-on-feed-error; with a truncated or malformed HTTP response, urllib/http.client can surface exceptions such as IncompleteRead or BadStatusLine, which are HTTPException subclasses rather than OSError in Python 3.11. Because this narrowed handler does not include that family, one flaky feed now aborts the whole scheduled pipeline instead of being recorded as a failed feed, regressing the previous except Exception behavior.

Useful? React with 👍 / 👎.

ValueError,
UnicodeError,
) as exc:
code = getattr(exc, "code", "fetch_failed")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve parser error codes in feed status

When a feed is malformed, oversized, or schema-invalid and --continue-on-feed-error is used, this looks for exc.code, but FeedXmlError is only a ValueError raised with messages like feed_schema_invalid and never defines a code attribute. The status therefore records parser rejections as generic fetch_failed, making producer/schema problems indistinguishable from network failures and defeating the new sanitized error_code contract.

Useful? React with 👍 / 👎.

error=f"{type(exc).__name__}: {exc}",
)
parsed = parse_feed_snapshot(fetcher(feed.feed_url), feed, max_items=max_items_per_feed)
feed_rows = [PrimitiveRow.from_mapping(row).to_mapping() for row in parsed.entries]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep optional feed authors from failing rows

For feed CSVs that omit or leave author blank, load_feed_config() still defaults feed.author to "", which previously produced valid rows with an empty author. This new PrimitiveRow conversion rejects empty strings for every row field, so an otherwise valid feed with a blank author now raises row_invalid or is marked failed under --continue-on-feed-error; either preserve the optional author behavior or validate it when loading config.

Useful? React with 👍 / 👎.

if state == "failed" and item["accepted_row_count"] != 0:
_error("feed_state_invalid")
accepted_count = _counter(item["accepted_row_count"], "feed_counter_invalid")
_counter(item["rejected_row_count"], "feed_counter_invalid")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Account for rejected rows before marking publishable

A wire status can set a feed-level and top-level rejected_row_count to a positive value while every feed is otherwise accepted; this only type-checks the count, and the later completeness calculation ignores rejected rows entirely, so serialize_status()/parse_status_bytes() can bless a canonical status with publication_complete and eligible_for_live_publication still true despite producer rows being rejected. Since build_status() never emits rejected rows today, mutated or external status artifacts with rejections should either be rejected or made ineligible instead of passing as complete.

Useful? React with 👍 / 👎.

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