Skip to content

Add producer source freshness evidence#52

Closed
Pigbibi wants to merge 2 commits into
mainfrom
feat/pert-source-freshness-evidence-pr0
Closed

Add producer source freshness evidence#52
Pigbibi wants to merge 2 commits into
mainfrom
feat/pert-source-freshness-evidence-pr0

Conversation

@Pigbibi

@Pigbibi Pigbibi commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Scope

Fresh producer-boundary source freshness evidence only.

  • Adds bounded fetch_url_with_metadata returning body plus only Date/Last-Modified evidence; existing fetch_url behavior remains compatible.
  • Adds canonical pert.source_freshness_evidence.v1 bytes and JSON-native readback.
  • Fixed signal priority: Atom feed updated > RSS channel lastBuildDate > HTTP Last-Modified.
  • HTTP Date is retrieval evidence only; item dates are period projection only.
  • Explicit UTC reference, future tolerance 5 minutes, maximum age 8 days.
  • Missing/invalid/high-priority/future/stale/tampered/noncanonical evidence fails closed.
  • Source URL and body are digest-bound; raw URL/request headers are not persisted.
  • Zero-entry feed body can still be freshness-eligible; event count is not freshness.
  • No workflow, weekly artifact, QAR, Pages, permissions, new data source, or parser dependency changes.

Validation

  • focused: 25 passed
  • full: 140 passed
  • compileall and diff-check: passed
  • ruff: unavailable in environment

Review mode: preflight then one concentrated review; no merge/rerun/poll requested.

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

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🤖 Codex PR Review

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

⚖️ Codex Review Arbitration

🚫 block: Both current findings remain valid against the cumulative diff. First, src/political_event_tracking_research/rss_source_fetch.py changes the existing public fetch_url(url) -> bytes path to call fetch_url_with_metadata(url)[0], and fetch_url_with_metadata() now raises FeedXmlError("feed_header_duplicate") when Date or Last-Modified appears more than once. In the prior implementation shown by the diff, fetch_url() only read the body and never inspected headers, so legacy body-only callers gain a new failure mode with no contract evidence in the current docs/tests that this API was intentionally tightened. Second, src/political_event_tracking_research/source_freshness.py still has _parse_date_signal() fall back to datetime.fromisoformat(...) after email.utils.parsedate_to_datetime(...); build_freshness_evidence() uses that helper for HTTP Last-Modified and Date, so non-HTTP ISO-8601 strings like 2026-07-13T11:00:00+00:00 are accepted as valid HTTP freshness evidence instead of failing closed. That contradicts the documented contract in docs/source_freshness_evidence.md that an invalid present higher-priority signal fails closed. There is no contract conflict with the prior blocking finding: the prior finding required stricter per-signal parsing in source_freshness.py, and the current finding preserves that same required behavior rather than reversing it.

🚫 Blocking Issues

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

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

fetch_url() now delegates to fetch_url_with_metadata(), so legacy callers that only need the response body will start failing with feed_header_duplicate whenever a server sends duplicate Date or Last-Modified headers. Previously those headers were ignored entirely, so this changes observable behavior and breaks the PR's stated compatibility guarantee for the existing API. (line 73)

Suggestion: Keep fetch_url() on its previous body-only path, or make duplicate-header enforcement opt-in only for callers that explicitly request freshness metadata.

2. 🟠 [HIGH] Logic in src/political_event_tracking_research/source_freshness.py

_parse_date_signal() falls back to datetime.fromisoformat() for every signal type, so malformed HTTP Date/Last-Modified headers such as ISO-8601 strings are accepted as valid freshness evidence. That violates the documented fail-closed behavior for invalid higher-priority signals and can incorrectly mark a source as eligible based on a non-HTTP timestamp format. (line 79)

Suggestion: Use strict parsers per signal type: RFC 3339 for Atom, the RSS format you intend to support for lastBuildDate, and HTTP-date parsing only for Date/Last-Modified without the generic ISO fallback.

ℹ️ Other Findings

1. 🟡 [MEDIUM] Reliability in src/political_event_tracking_research/source_freshness.py

read_freshness_evidence() has no upper bound on wire size before decoding and json.loads(). A tampered or corrupted evidence blob can therefore force the process to allocate arbitrarily large strings and JSON structures before schema validation rejects it, creating an avoidable memory-exhaustion path in the public wire reader. (line 283)

Suggestion: Reject wire above a fixed maximum size derived from the schema before UTF-8 decoding or JSON parsing, and keep that limit in sync with the bounded producer-side payload rules.


Review by Codex PR Review bot • PR

@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: 72af71565c

ℹ️ 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 +89 to +93
headers = {
name: value
for name in ("Date", "Last-Modified")
if (value := response_headers.get(name)) is not None
}

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 duplicate freshness headers at fetch time

When an HTTP server returns duplicate Date or Last-Modified headers, urllib exposes them via HTTPMessage.get_all(), but this code calls get() and keeps only the first value. That bypasses the downstream duplicate-header guard in _headers(), so a response with a fresh first Last-Modified and a stale/invalid second one can still be marked eligible instead of failing closed; the fetch boundary should preserve or reject duplicates before building evidence.

Useful? React with 👍 / 👎.

if len(channels) != 1:
_fail("source_freshness_invalid")
if kind == "rss_channel_last_build_date":
values = [child.text.strip() for child in channels[0] if child.tag == "lastBuildDate" and child.text]

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 Treat empty freshness elements as invalid

When an RSS feed contains an empty <lastBuildDate> element and also has a fresh HTTP Last-Modified, this filter drops the element because child.text is false, records the higher-priority RSS signal as absent, and allows fallback to HTTP evidence. The contract says present-but-invalid higher-priority signals fail closed, so a malformed feed can be marked eligible instead of rejected; detect the element's presence before parsing its text.

Useful? React with 👍 / 👎.

Comment on lines +91 to +93
parsed = dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=dt.UTC)

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 Fail closed on timezone-less date signals

If a freshness signal supplies an ISO timestamp with no timezone, such as Atom <updated>2026-07-13T11:00:00</updated> or a Last-Modified value in that form, this fallback treats it as UTC and can mark the source eligible. These provider freshness signals need an explicit timezone to be interpreted unambiguously; otherwise a malformed/local-time higher-priority signal is accepted instead of failing closed as invalid.

Useful? React with 👍 / 👎.

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

Pigbibi commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

PERMANENT_FREEZE: the absolute-final closure is exhausted and both current HIGH findings are valid. Preserve the legacy body-only fetch API and require strict per-signal timestamp grammars in any fresh successor. No second closure, rerun, thread bypass, or merge on this PR.

@Pigbibi Pigbibi closed this Jul 16, 2026

@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: 8464a9ebe1

ℹ️ 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".

if type(value) is not str or not value or len(value) > MAX_HEADER_VALUE_BYTES:
return False, None
try:
parsed = email.utils.parsedate_to_datetime(value)

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 malformed date text before selecting freshness

When a provider sends a Last-Modified or RSS lastBuildDate value that starts with a valid date but includes trailing garbage, such as Sun, 12 Jul 2026 11:00:00 GMT junk, email.utils.parsedate_to_datetime() still returns the leading timestamp. That lets a present-but-invalid freshness signal be canonicalized and selected, so the source can be marked eligible instead of failing closed; validate that the whole signal matches an accepted date format before using it.

Useful? React with 👍 / 👎.



def fetch_url(url: str) -> bytes:
return fetch_url_with_metadata(url)[0]

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 legacy fetch_url header tolerance

In the existing RSS ingestion path, fetch_rss_sources() still uses fetch_url as its default fetcher, so this delegation now makes a bytes-only fetch fail on duplicate Date or Last-Modified headers before parsing a feed that otherwise could be consumed. For feeds or proxies that duplicate these freshness headers, the legacy source pipeline becomes a failed fetch even though it does not use the freshness evidence; keep the duplicate-header rejection on the metadata/freshness path without changing fetch_url semantics.

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