Skip to content

fix(#39): resolve X-Article inline + cover images on the real X payload shape#71

Merged
VGonPa merged 5 commits into
developfrom
fix/39-article-image-resolution
Jul 4, 2026
Merged

fix(#39): resolve X-Article inline + cover images on the real X payload shape#71
VGonPa merged 5 commits into
developfrom
fix/39-article-image-resolution

Conversation

@VGonPa

@VGonPa VGonPa commented Jul 4, 2026

Copy link
Copy Markdown
Owner

What & why

#39 shipped X-Article structured capture pinned to a constructed fixture. Validated against 3 real bookmarked Articles (#66), the parser resolved body text correctly but zero images — the live X shape differs in three ways the constructed fixture didn't model:

  • entityMap is a LIST of {key, value} keyed by entry.key (not a dict) — the old helper returned {}, blinding all image resolution.
  • a MEDIA entity's URL is indirect: data.mediaItems[].mediaId → sibling media_entities[].media_info.original_img_url (never on the entity itself).
  • the lead cover_media image lives in a separate sibling.

The fix (extract/article.py, fetch_x.py)

  • List-entityMap indexing by entry.key (dict shape kept as a defensive path).
  • mediaIdmedia_entities resolution; multi-image galleries emit one block per resolvable item.
  • cover_media prepended as the lead image (deduped vs inline).
  • header-two## , lists→- /1. , blockquote> baked into text runs (flattened-text invariant preserved).
  • Observability: WARN when media indicators are present but 0 images resolve (the exact silent regression Capture X long-form Articles as blogposts (body text + inline multimedia) #39 had); richest-of-multiple-captured-payloads selection so a preview can't ship as complete; partial-gallery + dropped-block WARNs.
  • Degrades to text-only (never crashes) on any shape miss; no model change.

Validation

  • 3 real captured payloads committed as ground-truth fixtures (tests/fixtures/art-*.json); acceptance locked: OpenWiki cover+inline, Wiki Memory cover-only, Headcount none.
  • Reviewed by a 4-lens agent panel (correctness, silent-failure/data-safety, test-quality, spec-conformance); every finding addressed.
  • Quality gate green: 1088 tests, 95% coverage; ruff/mypy/radon A-B/bandit/vulture/interrogate/detect-secrets/deptry all pass.

Addresses #66 (real-payload validation of #39). Follow-up #67 (fetch --force backfill of the older text-only x_article) still applies.

🤖 Generated with Claude Code

VGonPa and others added 5 commits July 4, 2026 08:03
Ground-truth fixtures trimmed verbatim from the 3 real captured Articles
(article_results.result subtree). First failing test: art-OpenWiki must
resolve its atomic MEDIA block to the inline CDN image via the LIST
entityMap + media_entities indirection (currently yields zero images).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ad shape

The #39 parser was pinned to a constructed fixture and resolved ZERO images on
live X Articles (validated against 3 real bookmarked Articles, #66). Three
real-shape divergences fixed in extract/article.py:
- entityMap is a LIST of {key, value} keyed by entry.key (not a dict) — index by
  key, with the old dict shape kept as a defensive path.
- a MEDIA entity's URL is indirect: data.mediaItems[].mediaId resolves against
  the sibling media_entities[] (media_id -> media_info.original_img_url).
- cover_media is a separate lead image, prepended as the first ArticleImageBlock
  (deduped against inline).
Also render header-two -> "## " and unordered-list-item -> "- " by baking the
markdown prefix into the text run (flattened-text invariant preserved). Degrades
to text-only on any shape miss; no model change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Real-payload acceptance tests (tests/test_article_real.py): exact image
inventory per Article (OpenWiki cover+inline, Wiki Memory cover-only, Headcount
none), cover-is-lead-block, LINK-never-image, headings/bullets markdown,
flattened-text invariant via the model validator, and degradation (missing
media siblings drop images but keep all text; foreign payloads -> (None, [])).

Baseline the 3 fixtures' base64 node-ids: each decodes to
"ArticleEntity:<public rest_id>" — benign GraphQL global IDs in real-payload
ground-truth fixtures, not credentials.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… selection

Review panel (silent-failure + test-quality + spec-conformance) hardening of the
real-payload image fix:

- Multi-image gallery: a MEDIA block's `mediaItems` now yields ONE ArticleImageBlock
  per resolvable item (was first-only), so a gallery is no longer silently truncated
  to its first image; a partial resolve WARNs for the lost remainder.
- Image tripwire: WARN when the content_state carries media indicators (atomic
  blocks / media_entities / cover_media) but resolves 0 images — the exact regression
  the original #39 defect produced silently. Text-only articles are not flagged.
- Richest-payload selection (fetch_x._structured_article): parse ALL captured
  payloads and pick the one with the most blocks (was first-with-blocks), so a
  preview/skeleton article response can't ship as the complete body; WARN if >1 parsed.
- Atomic-ness decided from the entity, not blank text: a caption-bearing atomic
  image that fails to resolve is logged as a drop, not demoted to a text run.
- Failed gallery no longer falls back to a stray click-through `url` on the entity
  data (only the no-mediaItems defensive shape uses the URL-in-entity fallback).
- Per-item alt text; DEBUG log on an unparseable content_state string; blockquote/
  header-one/three/four prefixes; TypeGuard on _is_image_entity.
- Rewrote the stale module docstring (was "pinned against a constructed fixture,
  validate before production") to reflect the now-completed real-payload validation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…h, titles

Close the review-flagged test gaps:

- Multi-image gallery (one block per item), partial-gallery WARN, int/str media_id
  coercion, list-shape MEDIA drop WARN, caption-atomic-drop, stray-URL guard,
  ordered-list/header-one/three/blockquote prefixes (constructed, test_article.py).
- Real payloads (test_article_real.py): title extraction per fixture; cover==inline
  dedup; media-tripwire WARN vs no-warn on a text-only article; replaced the
  TAUTOLOGICAL invariant test with an independent text-run separator-structure check
  plus a model-validator check via fetch_x._flatten_blocks.
- Production wrapper (test_fetch_x.py): _structured_article on the REAL OpenWiki
  payload (cover-first + inline, title, off the sibling media_entities/cover_media),
  and richest-of-multiple-payloads selection with the ambiguity WARN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@VGonPa VGonPa merged commit 93bf592 into develop Jul 4, 2026
1 check passed
@VGonPa VGonPa deleted the fix/39-article-image-resolution branch July 4, 2026 11:05
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