55 KiB
Branch:
master| Last Updated: 2026-07-08 Active work: Embedding pipeline overhaul (PR9A-C). Previous: Layer 3 — Plugin UI polish. Layer 2 (OCR Quality Report + Readiness Policy) delivered at96fd9771. Full Python test suite: 41 passed (14 PR9A + 23 PR9B + 4 PR9C), plugin tests: 58 passed.
Current state: Memory/Embedding layer rearchitected. PR9A (correctness) — resume/rebuild deterministic selection,
.donemarkers removed, three-gate embed resume protection. PR9B (performance) — three-stage pipeline (prepare/encode/write) with parallel encode via ThreadPoolExecutor. PR9C (UX) — sliding-window streaming pipeline with continuous EMBED_PROGRESS. Provider switched fromopenaiclient torequeststo fix SiliconFlow NAT hang. Plugin chunk display fixed to sum all three collections. Next: full vault embed build with new pipeline.
1. Architecture
1.1 The problem (pre-v2)
raw_label/text → final role → normalize → rescue → demote/promote → attach → render
Early role guesses caused: title/author/footnote misclassification, body paragraphs typed as references, reference continuations leaking into body, left/right column interleave bugs, duplicate figure captions.
1.2 The solution (v2 target)
raw observations → structural signatures → stable anchors/families → zone inference → role resolution → figure/table validation → render + health
1.3 Core principles
- seed_role is a proposal, never a final role —
assign_block_role()proposes;normalize_document_structure()decides - VERIFY_REQUIRED roles (paper_title, authors, section_heading, reference_item, figure_caption, etc.) must have
role_verification_status == "ACCEPT"with non-emptyrole_sourceandrole_evidence - Zone != Role — being in the body reading environment does not imply
body_paragraph - Reference tail first — reference sections protected from tail contamination
- Frontmatter source-backed — OCR localizes but must not invent/discard canonical frontmatter
Key design documents:
- Architecture spec:
docs/superpowers/specs/2026-06-08-ocr-anchor-first-structured-parsing-design.md - Implementation plan:
docs/superpowers/plans/2026-06-08-ocr-anchor-first-structured-parsing-plan.md - Role gate plan:
docs/superpowers/plans/2026-06-11-ocr-verified-structural-role-gate.md - Latest spec realignment:
docs/superpowers/specs/2026-06-13-ocr-real-paper-regression-and-spec-realignment-design.md
2. Current Status
2.1 Test / Verification Status
| Suite | Result | |-------|--------| | Full OCR regression suite | **1278 passed, 8 pre-existing failures, 275 skipped** ✅ | | Focused merge suite (v3 + tail settlement + writeback + appendix numbering + rendering) | **105 passed, 0 failed** ✅ | | Layer 2 quality + feedback tests | **22 passed, 0 failed** ✅ (17 quality + 5 feedback) | | Full vault corpus diff: legacy vs v3 (555 papers) | **547/555 no diff, 5/555 v3 improvement** ✅ | | 86-paper pre-merge corpus diff | **86/86 no diff** ✅ | | 6 fixture-backed v3 parity gates | **6/6 pass** ✅ |
| Component | Status | |-----------|--------| | Structural gate | Installed | | Role assignment (seed only) | ✅ | | Zone inference + fallback | ✅ | | Figure pipeline vnext | ✅ Shared pairing core + `role_candidate`-aware match-time role resolution | | Table pipeline vnext | ✅ Shared pairing core + `role_candidate`-aware match-time role resolution | | Object writeback seam | ✅ `paperforge/worker/ocr_object_writeback.py` active on default path | | Tail settlement seam | ✅ `paperforge/worker/ocr_tail_settlement.py` active on default path | | V3 normalize split | ✅ ON by default (`OCR_PIPELINE_V3=0` reverts to legacy) | | Rebuild/orchestrator seam | ✅ Public wrappers unchanged | | Cross-domain figure/table conflict resolution | ✅ Still external to pairing core | | **Quality indicators** (Layer 2) | ✅ `paperforge/worker/ocr_quality.py` — `build_quality_indicators()` with 5 normalizers | | **Readiness policy** (Layer 2) | ✅ `evaluate_readiness()` + YAML policy evaluator with deep-merge, hard-red, use-case gates | | **Feedback sidecar** (Layer 2) | ✅ `paperforge/worker/ocr_quality_feedback.py` — per-mark hash, stale detection, no UI |
2.3 Fix Status
| # | Paper | Issue | Type | Fix | Commit |
|---|---|---|---|---|---|
| 1 | — | P21 zone fix (4AG67PBH "Conflict of Interest" heading) | Pipeline code | infer_zones() frontmatter_side_blocks page gate: added first_reference_page is not None and page >= first_reference_page - 1 |
35aabae |
| 2 | 4AG67PBH | Author bio text in post-ref reference_item (b8/b10) | New module | post_ref_bio_cleanup reclassifies bios as backmatter_body; _bio_text_score category-weighted 0-5 scoring |
e2f0c8a |
| 3 | — | author_bio_asset role contract | Pipeline code | author_bio_asset added to render_default=False, index_default=False skip sets |
7810eb1 |
| 4 | — | Pass C pipeline wiring | Pipeline code | Insert post_ref_bio_cleanup + prune_figure_inventory_after_bio after write_back_figure_roles |
7810eb1 |
| 5 | — | P1 residual author bio pass (Pass B) | New function | residual_author_bio_pass detects portrait unmatched_assets/unresolved_clusters with nearby bio text |
7a1cc5e |
| 6 | — | P1 figure_caption support in Pass C | Role expansion | post_ref_bio_cleanup extended for figure_caption role |
7a1cc5e |
| 7 | — | tag_figure_contained_text author_bio guard | Protection | Skip author_bio blocks and author_bio_asset role in figure containment |
7a1cc5e |
| 8 | — | Bio word limit 80→200 | Pipeline code | Real bio text 90-100 words exceeded 80-word limit in _bio_text_score |
ae081a4 |
| 9 | 4AG67PBH | Barbara bio as structured_insert_candidate missed by Pass C | Role expansion | Added structured_insert_candidate to Pass C role list |
ae081a4 |
| 10 | 4AG67PBH | Page 25 portrait id=5 missing from unmatched_assets | Pipeline bug fix | ocr_figures.py 4479/4529 used page-agnostic bare block_id filter, hitting collision when same id exists on different pages. Changed to (page, block_id) tuples. |
ae081a4 |
| 11 | 4AG67PBH | Acknowledgment text absorbed as reference_item (p21) | Regex fix | _is_reference_item_candidate tightened |
fe9cc70 |
| 13 | WV2FF4NV | Fig 10/6 locator caption bridge | New feature | _is_previous_page_legend_locator + bridge in build_figure_inventory: connects locator → previous full legend → current visual group. Recovers misclassified legends from rejected_legends. |
3f61f4a |
| 14 | — | Issue 3: Backfill word leakage beyond bbox | Word-level filter | Added _word_belongs_to_block + _word_center_inside_rect filters after expanded clip |
796e8bb |
| 15 | — | Issue 1A: Validation-first bare table skips same-page asset | Continue guard | Validation-first branch only early-exits when no same-page assets exist | 796e8bb |
| 16 | — | Issue 1B: Split table caption continuation escapes ownership | Continuation materialization | _find_table_caption_continuation + _materialize_table_caption inside build_table_inventory |
796e8bb |
| 17 | — | Issue 4: Short papers (≤2p) incorrectly red + needs_rebuild | Health profile | Added _health_profile(page_count) → short_form waives abstract/heading gates |
796e8bb |
| 18 | — | Issue 2: Demoted-caption figure inner-text leakage | Container bbox regions | Validated _container_bbox regions in tag_figure_contained_text via 3 helpers + containment-only integration |
0e4ecbc |
| 19 | — | Issue 5: Cross-column page_assets groups falsely accepted | Column-homogeneity gate | _column_band_id + rejection in _is_safe_page_assets_group |
4ab227e |
| 20 | — | Issue 6: Same asset consumed by figure AND table | Post-hoc arbitration | resolve_media_asset_conflicts resolves asymmetric cases; weak/weak stays in ownership_conflicts |
4ab227e |
| 21 | — | Pairing framework extraction + figure migration | Refactor | Extracted shared ocr_pairing_* core from figure vnext; preserved public seams and figure behavior |
6229f6c, 7cfbb5f, 32541cf |
| 22 | — | Table vnext on pairing framework + public cutover | Refactor/feature | Added ocr_table_domain.py + ocr_table_passes.py, preserved resolver-consumed fields, switched build_table_inventory(...) to vnext |
db01518, ea6a1f0, a9e68ac, a2e5788 |
| 23 | 37LK5T97 + cutover corpus | Merge-unblock hardening | Moved figure-only rotation enrichment out of generic state, validated 6 runnable real-paper table fixtures semantically, fixed touched-file lint/format issues | fa734f6, 9b285b1 |
|
| 24 | — | Workstream A — object ownership evidence seam | New module | Added ocr_object_writeback.py; unified figure/table asset claims, side-adjacent text claims, consumed-block contract, and renderer consumption skip |
964e05b |
| 25 | — | Workstream B — tail settlement seam | New module | Added ocr_tail_settlement.py, extracted tail/body/backmatter settlement, and attached TailSettlementReport to DocumentStructure |
7bf652c |
| 28 | — | Wire apply_object_writebacks into rebuild path | Fix | Rebuild entry point never called apply_object_writebacks; side-adjacent/contained claims missing from rebuild production path | e8aae2e |
| 29 | M84CTEM9 | Figure inner_text not merged into figure render | Fix | figure_inner_text blocks were correctly recognized but crop bbox excluded variable labels; render_figure_object_markdown emits only image+legend. Fix: expand crop bbox to include owned figure_inner_text blocks | c42da20 |
| 30 | — | tag_figure_contained_text binds owner_id on contained blocks | Fix | Contained text path stamped role but not _object_owner_id; needed for crop bbox expansion to find the block-to-figure link | 34827f2 |
| 31 | — | Enable OCR_PIPELINE_V3 by default | Toggle | Full vault corpus diff (555 papers): 547/555 no diff, 5/555 v3 improvements. Set OCR_PIPELINE_V3=0 for legacy | 914acd6 |
3. Remaining Issues — Release-Readiness Layers
Layer 1: OCR Truth Coverage — Layout-Category Audit
Workstream X complete. 11 papers audited across 6 layout classes, verified via vision subagents. 6 real bug patterns identified. Full report: docs/superpowers/analysis/2026-07-05-layout-truth-audit-findings.md
Bug patterns found:
- A
_is_obviously_formal_figure_captionheuristic — "Fig. X shows..." body text classified as figure caption (ocr_roles.py:193) - B Cross-column figure asset mis-assignment — nearest-neighbor ignores column boundaries (
ocr_figures.py:704) - C Render frontmatter skip — authors/affiliations silently dropped from fulltext (
ocr_render.py) - D Two-column same-page boundary — body pulled to backmatter zone on mixed pages (
ocr_document.py) - E Frontmatter headings ("ARTICLE INFO") misclassified (
ocr_roles.py) - F Supplementary-only PDFs — pipeline not designed for non-standard structure
Non-bugs confirmed: 170 reference_span_error findings = FALSE POSITIVE (high-risk noise). Single-column same-page boundaries = FALSE POSITIVE.
Layer 2: OCR Quality Report + Readiness Policy
DONE at commit 96fd9771. Three modules delivered:
paperforge/worker/ocr_quality.py—build_quality_indicators()(5 normalsers) +evaluate_readiness()(policy evaluator)paperforge/policies/ocr_readiness_v1.yaml— default policy (weights, hard-red, use-case gates)paperforge/worker/ocr_quality_feedback.py— human feedback sidecar (per-mark hash, stale detection) 22 new tests. Contract polish:status/gates/reasonsoutput shape, hash validation, user override bypass.
Layer 3: Plugin UI
PaperForgeStatusView dashboard is 2300+ lines in one file. Status display, OCR maintenance UI, and user-facing health presentation not yet polished.
Layer 4: Downstream Tools
chunker.py uses hardcoded section regex + fixed 3-paragraph groups. OCR has rich structured output (sections, headings, figures, tables with captions) — chunker should consume this structure directly. Figures/tables should support separate embedding (text + future vision).
Remaining legacy OCR issues (carried forward):
4. Active Queue
- ✅ Layout-category truth audit (Workstream X) — 6 bug patterns identified.
- ✅ Layer 2: OCR Quality Report + Readiness Policy —
96fd9771. - 🟡 Plugin UI polish — dashboard cleanup, status clarity
- 🟡 Downstream tooling — section-aware chunking, figure/table separate handling
- ⏳ Architecture cleanup — deferred post-release
- ⏳ Compatibility naming cleanup — deferred post-release
4.1 Immediate Next Steps
- Enter Layer 3: Plugin UI polish (
PaperForgeStatusView) - Archive stale queue docs from pairing-framework phase
5. Key File Map
5.1 Production Code (OCR Pipeline)
| File | Role |
|---|---|
paperforge/worker/ocr_blocks.py |
Raw + structured block generation; legacy normalize path plus normalize_mode=\"seed_only\" for v3 |
paperforge/worker/ocr_roles.py |
assign_block_role() — seed/proposal logic only (NOT final) |
paperforge/worker/ocr_document.py |
normalize_document_structure() — anchor/family/zone + gate + final roles |
paperforge/worker/ocr_pre_match_normalize.py |
V3 candidate-only normalize; fills role_candidate while preserving public role = seed_role |
paperforge/worker/ocr_post_match_normalize.py |
V3 post-match role commit; shadow normalize + rescue equivalence + tail settlement |
paperforge/worker/ocr_object_writeback.py |
Post-inventory ownership-evidence seam for figure/table assets, contained text, and side-adjacent figure text |
paperforge/worker/ocr_tail_settlement.py |
Tail/body/backmatter settlement seam + TailSettlementReport |
paperforge/worker/ocr_structural_gate.py |
VERIFY_REQUIRED role decision + abstract span + reference zone + health |
paperforge/worker/ocr_orchestrator.py |
Body reorder, column validation, layered assembly |
paperforge/worker/ocr_render.py |
render_fulltext_markdown() — skips consumed object-owned blocks and consumes verified artifacts only |
paperforge/worker/ocr_health.py |
Health reporting, merged gate summary |
paperforge/worker/ocr_profiles.py |
Span extraction, profile aggregation, cross-validation |
paperforge/worker/ocr_figures.py |
Figure reader pipeline + role_candidate-aware match-time role resolution |
paperforge/worker/ocr_tables.py |
Table inventory matching + role_candidate-aware match-time role resolution |
paperforge/worker/ocr_scores.py |
Score functions (spatial, structured_insert, etc.) |
paperforge/worker/ocr_rebuild.py |
Derived rebuild entry point |
paperforge/worker/ocr_pdf_spans.py |
PDF span backfill for OCR-missed blocks |
paperforge/worker/ocr_bio.py |
Author biography detection utilities and passes |
paperforge/worker/ocr_quality.py |
Quality indicators builder (5 normalsers) + readiness policy evaluator |
paperforge/worker/ocr_quality_feedback.py |
Human feedback sidecar (per-mark hash, stale detection, append-only) |
paperforge/policies/ocr_readiness_v1.yaml |
Default readiness policy (weights, hard-red rules, use-case gates) |
5.2 Test Files
| File | Purpose |
|---|---|
tests/test_ocr_figures.py |
Figure inventory, matching, ownership |
tests/test_ocr_document.py |
Document structure, normalize, gate |
tests/test_ocr_render.py |
Fulltext render contract |
tests/test_ocr_figure_reader.py |
Reader contract |
tests/test_ocr_trace_vs_expectations.py |
Real-paper trace vs expectations gap report (8 gold papers) |
tests/test_ocr_real_paper_regressions.py |
Page-level + document-level regression on real papers |
tests/test_ocr_real_paper_audit_contracts.py |
Gold-fixture quality gate |
tests/test_ocr_spec_contracts.py |
Architecture contract tests |
tests/test_ocr_structural_gate.py |
Gate unit tests |
tests/test_ocr_v2_structural_regressions.py |
Structural regression guards |
tests/test_ocr_layout_first_regressions.py |
Layout-first behavior guards |
tests/test_ocr_object_writeback.py |
Ownership-evidence seam, consumed-block contract, contained/side-adjacent text, cross-page block_id guard |
tests/test_ocr_tail_settlement.py |
Tail/body/backmatter extraction seam + TailSettlementReport |
tests/test_ocr_pipeline_v3.py |
OCR_PIPELINE_V3 toggle, seed-only mode, pre/post normalize split, parity gate |
tests/test_ocr_quality.py |
Quality indicators (17 tests: shape, thresholds, health_profile, inventory precedence, run_integrity) + readiness policy (7 tests: default, hard-red, gates, YAML, merge) |
tests/test_ocr_quality_feedback.py |
Human feedback sidecar (5 tests: roundtrip, hash validation, append, stale, resolve) |
tests/test_appendix_figure_numbering.py |
Appendix figure/table numbering regressions including M84CTEM9 coverage |
5.3 Test Fixtures
tests/fixtures/ocr_real_papers/{CAQNW9Q2,DWQQK2YB,TSCKAVIS,A8E7SRVS,K7R8PEKW,6FGDBFQN,SAN9AYVR,2GN9LMCW}/
5.4 Design Documents
| File | Purpose |
|---|---|
docs/superpowers/specs/2026-06-08-ocr-anchor-first-structured-parsing-design.md |
Architecture spec |
docs/superpowers/specs/2026-06-13-ocr-real-paper-regression-and-spec-realignment-design.md |
Current phase spec |
docs/superpowers/specs/2026-06-10-ocr-figure-reader-contract-design.md |
Figure reader spec |
docs/superpowers/specs/2026-06-23-ocr-visual-grammar-hardening-design.md |
Visual grammar hardening |
docs/superpowers/specs/2026-06-27-figure-containment-and-backmatter-boundary-design.md |
Current P0/P1 spec |
docs/superpowers/plans/2026-06-18-ocr-v2-readiness-master-plan.md |
Readiness gates master plan |
docs/superpowers/specs/2026-07-05-ocr-quality-report-design.md |
Layer 2 design spec — quality indicators, readiness policy, feedback sidecar |
docs/superpowers/plans/2026-07-05-ocr-quality-report-plan.md |
Layer 2 implementation plan (3 PRs + contract polish) |
docs/superpowers/plans/2026-06-17-ocr-v2-closeout-single-plan.md |
Close-out single plan |
docs/superpowers/plans/2026-06-15-group-first-figure-inventory-plan.md |
Group-first figure refactor (deferred) |
docs/superpowers/specs/README-ocr.md |
OCR design index |
5.5 Active Truth Files
| File | Role |
|---|---|
project/current/ocr-v2-active-queue.md |
Active queue — next-work authorit |
project/current/ocr_rebuild_audit.md |
Evidence source for queue |
project/current/ocr-v2-generalization-boundary.md |
Architecture boundary note |
project/current/ocr-v2-remaining-issues-2026-06-18.md |
Historical readiness residuals |
6. Decision Log
| Date | Decision | Rationale |
|---|---|---|
| 2026-06-08 | Adopt anchor-first OCR architecture | Early role guessing caused cascading errors; needed structural discovery first |
| 2026-06-10 | Separate figure reader from body prose | Figure info must be reader-visible without body pollution |
| 2026-06-11 | Install verified structural role gate | Spec was being bypassed; gate enforces seed≠final contract |
| 2026-06-13 | Dual-gate regression + spec-contract testing | Tests protected helper behavior, not real-paper outcomes |
| 2026-06-13 | Root-cause approach: no renderer patches | Heading merge goes in raw blocks; boundary detection fixed in infer_zones |
| 2026-06-13 | Figure sequential matching as cross-page tradeoff | Caption-asset pairs on different pages get lower confidence |
| 2026-06-15 | Expand deterministic gold set to 8 papers | Needed broader regression surface before changing figure inventory |
| 2026-06-15 | Do not solve AJR side-caption recovery in group-first refactor | Keep scope generic; AJR-specific rescue is later phase |
| 2026-06-15 | Group-first matching is next architectural target | Existing clusters/visual groups too late in pipeline |
| 2026-06-17 | Single-thread close-out note | authoritative queue moved to project/current/ocr-v2-closeout-priority.md |
| 2026-06-21 | Replace greedy region-growth with global distance clustering | Human sees assets as perceptual groups, not competing candidates |
| 2026-06-23 | Pre-ref=body flow, post-ref=backmatter | CRediT/Ethics above References → body_zone |
| 2026-06-23 | Figure containment: render-hygiene pass build_figure_inventory | Containment shouldn't affect matching, only rendering |
| 2026-06-23 | Reference sort: two capture groups | Prevents false matches on plain year numbers |
| 2026-06-26 | P0 before P1 | Fix 2/4/5 are <30 lines fully understood; Fix 1/3 need new specs |
| 2026-06-28 | Pre-ref tail zone: strip from region_bus not re-apply | _apply_zone_labels re-applied stale tail zone from infer_zones() after ref partition. Fix: strip pre_ref block IDs from region_bus before zone re-apply. |
| 2026-06-28 | Author byline: require lowercase letters | _looks_like_initial_lastname_byline matched all-caps journal taglines. Fix: require any lowercase letter in matched text. |
| 2026-06-28 | Page-1 body_start: metadata headings should not trigger | _is_first_page_body_start treated ANY section_heading as body start. Fix: only real body section headings (introduction, methods, etc.) trigger body_start on page 1. |
| 2026-06-28 | Frontmatter heading normalization: no text matching | Metadata sidebar labels rejected by structural gate fell to unknown_structural. Fix: normalize held heading blocks in frontmatter_main_zone to frontmatter_noise using only zone + gate decision + seed_role, no text matching. |
| 2026-06-28 | Author bio detection: three-pass cascade, P0 first | Strong structure first, residual explanation second. Real figures must never be preempted. P0: post-ref text-only. P1: figure residual. P2+: P1 profile card pre-pass. |
| 2026-07-04 | Introduce ownership-evidence writeback seam | Figure/table asset claims, contained text, and side-adjacent figure text needed one post-inventory contract so renderer suppression and future late-stage role logic could consume the same evidence. |
| 2026-07-04 | Extract tail settlement out of normalize/build seam | Tail/body/backmatter settlement had grown into a cross-module implicit contract; isolating it lowered blast radius before attempting the v3 normalize split. |
| 2026-07-04 | Keep OCR_PIPELINE_V3 merged but default-off |
The pre/post normalize split is architecturally correct, but synthetic parity is easier to prove than corpus parity. Merging behind a toggle cuts long-lived branch risk without forcing the new path live. |
| 2026-07-04 | Contained figure claims must be page-qualified | Cross-page block_id reuse and similar figure geometry can silently hide the wrong text. Ownership claims now require same-page qualification before containment is trusted. |
| 2026-06-28 | Category-weighted bio scoring | career=+3, education=+2, research=+2, institution=+1, publication=+1. Returns (score, categories) tuple. Threshold: score ≥ 4 AND categories ≥ 2. |
| 2026-06-28 | author_bio_asset role: non-rendered, non-indexed | Bio artifacts removed from figure_inventory entirely, never returned to unmatched_assets. Clean prune before reader. |
| 2026-07-01 | Asset-internal figure number recovery: metadata-only pass | Recovery must NOT split OCR blocks or mutate chart text — only patches figure_number, figure_id, recovered_label_text on existing matched figures. Coordinate normalization is caller responsibility. |
| 2026-07-01 | Broaden recovery gate to handle normal prematch unknown figures | Synthetic-figure gate (bbox_only_asset flag) excluded figure_unknown_NNN from normal rotated prematch path. Gate now allows figure_unknown figures without synthetic flags. |
| 2026-07-03 | Cutover uses evidence gates, not code confidence | VNext matched or improved on the full cutover corpus with identical consumed asset sets; wrapper switch became a release decision only after diff review + gate verification. |
| 2026-07-03 | Legacy schema tests must be upgraded before wrapper switch | Real-paper behavior was cutover-ready earlier, but test_ocr_figures.py still asserted legacy-only inventory keys. Updating the test contract was required to make wrapper switch honest. |
| 2026-07-03 | Generic state uses a domain hook for figure rotation enrichment | Figure rotation metadata had to stay behaviorally intact without polluting the shared pairing state used by table passes. A pre-match enricher hook keeps the core generic and the figure path exact. |
| 2026-07-03 | Table parity comparison normalizes benign storage drift only | int/str block IDs, ordering differences, and None vs empty unmatched asset IDs are storage-level drift already tolerated downstream. Validation now compares semantic fields rather than raw serialization artifacts. |
| 2026-07-03 | Legacy block_id=0 drop is treated as a legacy bug, not a parity target |
37LK5T97 showed legacy dropping consumed caption id 0 via falsy filtering. The branch keeps vnext truth and documents the legacy defect instead of reproducing it. |
| 2026-07-05 | Expand crop bbox to include figure_inner_text | figure_inner_text IS the figure content (variables labels, y-axis text); text should be IN the cropped jpg, not listed as a separate note section. Crop bbox now unions all owned figure_inner_text block bboxes. |
| 2026-07-05 | Enable OCR_PIPELINE_V3 by default | Full vault corpus diff (555 papers): 547/555 no diff, 5/555 v3 improvements (3 more figures found, 2 boundary corrections). 98.6% parity is sufficient to flip the default; OCR_PIPELINE_V3=0 restores legacy. |
| 2026-07-05 | figure_inner_text must be in the figure render, not dropped | Side-adjacent and contained text blocks correctly identified as figure_inner_text, but had no display outlet. Crop bbox expansion is the correct fix (text is IN the figure), not a text listing below the image. |
| 2026-07-05 | Health = 3 layers: Quality Signal → Quality Indicator → User Readiness | build_ocr_health() preserved raw; build_quality_indicators() adds normalized layer; evaluate_readiness() applies policy. Never merge health and quality report. |
| 2026-07-05 | Readiness weights in external YAML, never hardcoded in Python | Policy evaluator reads from YAML; thresholds tunable without re-running OCR. Default ships in paperforge/policies/ocr_readiness_v1.yaml. |
| 2026-07-05 | Human feedback is a sidecar file, never part of pipeline output | ocr_quality_feedback.json is read/written independently; bound to result_hash for integrity. |
| 2026-07-05 | Field resolution precedence: direct inventory > health aggregates | figure_inventory fields preferred over health.matched_figure_count_v2 etc. health.figure_asset_count is NOT a figure-evidence signal (it's a match count). |
| 2026-07-05 | user_readiness must state "basis": "policy_estimate" |
Pipeline produces signals, not ground truth. Gaps are real, but code doesn't know if a gap is actual missing text or a proper skip. |
| 2026-07-05 | recommended_use output shape: status/gates/reasons |
Contract fixed at 96fd9771 from the initial recommended/gate_results shape. |
| 2026-07-05 | Feedback hashes per-mark, not just root | append_mark() injects result_hash and fulltext_hash into each mark; stale detection compares latest mark's hash with current run. |
7. Agent Instructions
7.1 Project Folder Management
The authoritative prompt for project record management lives in .omp/AGENTS.md.
It is auto-loaded by omp into every session context and defines when and how to
update PROJECT-MANAGEMENT.md, project/current/*, and project/archive/.
TL;DR: PROJECT-MANAGEMENT.md updated every session end. project/current/ updated at milestones only. project/archive/ gets moved-to (not deleted) when stale.
7.2 How to Update PROJECT-MANAGEMENT.md
- Update section 2 (Current Status) — test counts, component status, fix status
- Update section 3 (Remaining Issues) — remove resolved items, add new ones
- Update section 4 (Active Queue) — check/adjust next steps
- Add new decisions to section 6 (Decision Log)
- Add a compressed entry to section 8 (Session Timeline)
- Update "Last Updated" date at top
7.3 Before Starting Any OCR-v2 Work
- Read section 4 (Active Queue) for current priority
- Read the relevant design doc from section 5.4
- Understand what the tests currently expect
- Work one repair at a time; verify before moving on
7.4 Test Commands
# Full OCR test suite
python -m pytest tests/test_ocr_*.py -v --tb=short
# Figure stack only
python -m pytest tests/test_ocr_figures.py tests/test_ocr_figure_reader.py tests/test_ocr_render.py -q
# Real-paper regression only
python -m pytest tests/test_ocr_real_paper_regressions.py -v --tb=short
# Spec-contract tests only
python -m pytest tests/test_ocr_spec_contracts.py -v --tb=short
# Structural gate only
python -m pytest tests/test_ocr_structural_gate.py tests/test_ocr_document.py -v --tb=short
# Rebuild a single paper
python scripts/dev/ocr_rebuild_paper.py DWQQK2YB
# Rebuild + regenerate block_trace
python scripts/dev/ocr_rebuild_paper.py --trace DWQQK2YB
# Lint
python -m ruff check paperforge/worker/ocr_*.py
7.5 Design Doc Reading Order
docs/superpowers/specs/README-ocr.md— indexdocs/superpowers/specs/2026-06-08-ocr-anchor-first-structured-parsing-design.md— architecturedocs/superpowers/specs/2026-06-13-ocr-real-paper-regression-and-spec-realignment-design.md— current phase
8. Session Timeline (Compressed)
| Date | Session | Key Results | Detailed Archive |
|---|---|---|---|
| 2026-06-16 | Gold fixture expansion + 10 pipeline fixes | 98 bug annotations, 8 papers audited, 8 fixes applied (F1-F10) | §9.1 |
| 2026-06-17 | OCR-v2 boundary close-out pass | Zone-boundary fixes, tail/backmatter shrink, correspondence routing. 202P/1F/43S | §9.2 |
| 2026-06-18 | Readiness-gates implementation | Gates 1-4 complete + blind audit protocol. 249/249 figure/health/document pass | §9.3 |
| 2026-06-19 | Blind audit (5 unseen papers) | All PASS — no new failure families. OCR-v2 declared "state healthy" | §9.3 |
| 2026-06-21~22 | Figure merge: greedy → global distance clustering | Union-find clustering, 261 tests. Caption-independent semantic grouping, ownership registry, local pairing, conflict detection | §9.5 |
| 2026-06-22 | Cross-page caption consumption fix | Reader/render contract breach fixed — cross-page matches now consume caption on legend page | §9.4 |
| 2026-06-23 | Visual grammar hardening | Composite parent detection, dense page arbitration, figure/table separator veto, dedup refinement | §9.5 |
| 2026-06-26 | Rebuild production run (699 papers) | --resume checkpoint, glob fallback, N6XCZD25 body text fix |
§9.6 |
| 2026-06-26 | Figure number inference + container admission | Leading [1] gap filled, blue sidebar box rendered as [!NOTE] |
§9.7 |
| 2026-06-26 | UI polish (plugin) | Dashboard CSS, maintenance tab redesign, Vercel-style polish | §9.8 |
| 2026-06-27 | Rebuild audit + index repair | 6 hard-block/high bugs fixed (sync, workspace, field registry) | §9.9 |
| 2026-06-27 | Deep investigation — 5-fix spec | P0 all committed (ref sort, caption insert, figure containment). P1 backmatter in progress | §9.10 |
| 2026-06-28 | P1 backmatter boundary committed | Ref-anchored partition (3e33e5b). Pre-ref=body flow confirmed (9b72783). 16/16 tests pass. All 5 audit papers verified. |
§9.11 |
| 2026-06-28 | Gate 5 blind audit + pre-ref tail zone fix | Gate 5: 24YKLTHQ (13p) + 4KCHGV2Z (9p) rebuilt post-P1. Found pre-ref body pages misclassified as tail_nonref_hold_zone. Root cause: _apply_zone_labels re-applies stale region_bus after ref partition. Fixed by stripping pre_ref block IDs from tail zone. 4KCHGV2Z P7: tail=20 → body=2+disp=5. All 286 figure/backmatter tests green. | §9.12 |
| 2026-06-28 | Gate 5 frontmatter fix series (3 fixes) | 24YKLTHQ: author byline lowercase guard, metadata body_start fix, frontmatter heading normalization (no text matching). All 3 fixes verified on real paper, 461 tests pass, 0 new regressions. | §9.13 |
| 2026-06-28 | Test fix session: 25 tests reconciled | Fixed 10 stale test issues: non_body_insert guard, caffard abstract, legend_like role, structural gate, backmatter heading render, state machine (done_degraded), body_zone anchor, rebuild backfill skip, truth surface docs, trace-vs-expectations (10 assertions). 616 OCR tests, 0 failed. Expectations updated for post-P1 behavior. | §9.14 |
| 2026-07-04 | A/B/C OCR deepening merge + pre-merge blocker cleanup | Merged Workstream A (ocr_object_writeback.py), Workstream B (ocr_tail_settlement.py), and Workstream C (pre_match_normalize / post_match_normalize behind OCR_PIPELINE_V3) to master. Closed pre-merge blockers: page-qualified writeback lookup, contained-text ownership contract, same-page contained-text guard, and v3 rescue equivalence. Verification: 99 focused tests passed on merged master. |
§9.24 |
| 2026-06-28 | Data-driven truth audit (2 papers) | 2HEUD5P9 (27p) + 4AG67PBH (25p) — no vision (model limit). Found 3 pipeline defect patterns: zone_leak_frontmatter_to_body (2 papers), reference_boundary_body_mix (2 papers), title_repeat_page2 (1 paper). 12 ghost unknown_structural blocks in 2HEUD5P9. Findings saved to audit/2026-06-28-data-audit-findings.json. | §9.15 |
| 2026-07-05 | Figure inner_text crop merge + V3 default-on | Figure inner_text identified but silently dropped — crop bbox expanded to include owned figure_inner_text blocks (variables in images retained). Rebuild path wired to apply_object_writebacks. Full vault corpus diff: 547/555 no diff, 5/555 v3 improvements. OCR_PIPELINE_V3 enabled by default. 105 tests + 555-paper diff green. | §9.25 |
| 2026-06-28 | P0 author bio detection implementation | Created ocr_bio.py with category-weighted bio scoring, Pass C (post_ref_bio_cleanup), figure match guards. Wired author_bio_asset role contract + pipeline. 30 new tests pass. 1041 total OCR tests, 0 regressions. Commits: e2f0c8a, 7810eb1. |
§9.16 |
| 2026-06-28 | P1 author bio detection implementation | Added residual_author_bio_pass (figure-residual portrait assets), extended post_ref_bio_cleanup for figure_caption, tag_figure_contained_text protection. 7 new P1 tests. 1018 total OCR tests, 0 regressions. Commit: 7a1cc5e. |
§9.17 |
| 2026-07-01 | Audit fix commits + orientation-aware rotated figure normalization | Commit 1 (2d40ad9) + Commit 2 (21bdfd0) + Commit 4 (7670227) landed, then refactored rotated-caption handling out of synthetic fallback into normal figure pre-match. Added PyMuPDF dir/wmode capture, same-page rotated settlement, rotated crop render. U746UJ7G now matches via same_page_rotated; KUR9PBJC unchanged. 422 regression tests pass. |
§9.18 |
| 2026-07-01 | Asset-internal figure number recovery implementation | Added extract_pdf_lines_normalized helper, _recover_missing_figure_numbers_from_assets pass in build_figure_inventory, 5 gate functions, 2 pattern constants. U746UJ7G figure_unknown_000 → figure_002 with recovered label "Plot of Criteria Time". 6 new tests. 428 regression tests pass. |
§9.19 |
| 2026-07-02 | Round 2 truth audit + 3 targeted bug fixes for 37LK5T97 | Batch-audited 10 fresh papers (5 GREEN / 4 YELLOW / 1 RED). 37LK5T97 found with Figure 1 broken (sidecar caption demoted) + 6 unmatched rotated tables. Fixes: (1) _is_sidecar_candidate guard in candidate_resolution, (2) adjacent_x+y_overlap in score_table_match for rotated captions, (3) rotated table render bbox+270° correction. Also: rotated figure crop quality fix (4x zoom + coordinate normalization in _crop_asset_from_pdf). Commits: 59cd01a, bd3f3b6, 86e0d14. 428 regression tests pass. |
§9.20 |
| 2026-07-02 | Zone/role robustness completion — Figure caption prefix recovery + inline table fix + table matching audit | Figure caption prefix recovery from PDF text layer (_recover_figure_heading_prefix): 5S7UI34M 4→9 matched figures, 33→1 unmatched. Inline <table> HTML role fix: 650 blocks now table_html after rebuild (priority bug: raw_label=table fired before <table> check). Table matching audit: 620 remaining media_asset pending full rebuild. 585 figure/table/role tests pass. |
§9.21 |
| 2026-07-03 | Figure pipeline vnext cutover completed | Implemented all remaining vnext passes (composite parent, group/classic sequential, unresolved consolidation, accounting), expanded compare harness, curated 5-paper cutover corpus covering all 9 spec categories, generated diff review (improvement=2 / equivalent=2 / parity=1 / regression=0), updated legacy figure tests for vnext contract, and switched build_figure_inventory(...) wrapper to vnext on branch feat/figure-pipeline-vnext. Verification: 346 tests passed. |
§9.22 |
| 2026-07-03 | OCR pairing framework merge-unblock pass | Cleared the remaining merge blockers on feat/ocr-pairing-framework: moved figure-only rotation enrichment out of generic state, upgraded table cutover validation to semantic parity across 6 runnable real-paper fixtures including 37LK5T97, and cleaned touched-file lint/format issues. Verification: 357 targeted tests passed; merge-ready. |
§9.23 |
9. Historical Detail Archive
Fixed records and verbose session logs preserved below. The archive is read-only reference — active work is tracked in sections 2-4 above.
9.1 Gold Fixture Expansion + Bug Fixes (2026-06-16)
Full-day debugging session across 8 gold papers. 98 bug annotations, 8 pipeline fixes (F1-F10).
Root cause categories identified:
- Frontmatter noise unrecognized (ISSN, journal citation) → body pollution
- Cross-page text fragmentation
- Tail zone: body prose incorrectly converted to backmatter
- Backmatter heading gaps
- Heading merge/split logic
- Heading rescue from unknown_structural
- Heading prefix by role (not font size)
- Permissive figure matching
- All same-page assets included in group match
- Composite region text requirement removed
Fixes applied: ocr_roles.py, ocr_document.py, ocr_blocks.py, ocr_render.py, ocr_scores.py, ocr_figures.py, ocr.py
Layout-first Phase 1 pass added — table inventory relaxed for media_asset blocks, _should_keep_formal_caption_seed() added, tests/test_ocr_layout_first_regressions.py created.
9.2 Boundary Close-Out Pass (2026-06-17)
Plan: docs/superpowers/plans/2026-06-17-ocr-v2-closeout-single-plan.md
Tasks 1-5 executed:
- Same-page reference/body boundary split (block-level vertical split by heading position)
- False tail backmatter conversion reduction
- Page-1 correspondence line → frontmatter_support
- Preproof page-1 frontmatter: first-surviving-page logic, margin-band watermark detection, figure inner label extension
- Active truth-file cleanup: reconciled P0-P2 close-out
Result: 202P/1F/43S (sole failure = pre-existing DW figure ownership)
Key commits: 6f68bf2, b7d369e, c7a9c93, 827a2cc, 9329843
9.3 Readiness-Gates (2026-06-18 ~ 06-19)
Plan: docs/superpowers/plans/2026-06-18-ocr-v2-readiness-master-plan.md
Gates implemented:
- Gate 1 (Completeness):
_summarize_page_text_coverage()+_classify_region_text_completeness()+audit_rendered_text_coverage() - Gate 2 (Figure ownership): 8 tasks — previous-page sequential fallback, DW Fig 3 xfail→pass, sidecar partition, fallback tightening. Final: 249/249 pass
- Gate 3 (Ordering):
_ref_number_sort_keyregex extended, reference boundary normalizers - Gate 4 (Layout coverage):
audit/coverage_ledger.jsonwith readiness-class taxonomy, contract tests enforce named representatives
Blind audit (5 unseen papers): ALL PASS. Papers: 8VB9ZVQG, U746UJ7G, L6ALWJFP, PZ8B59K4, GU9R8EPE. No new failure families discovered.
Remaining residuals: ~40 stale audit truth blocks, ~50 edge-case misclassifications (low severity).
9.4 Cross-Page Caption Consumption Fix (2026-06-22)
Paper: SAN9AYVR Figure 24 double-emit. Cross-page figure ownership repair — reader/render chain consumed caption blocks on asset page instead legend page.
Fixes applied:
- Reader payload records cross-page caption consumption on legend page
- Render path suppresses original caption block on asset page
_recompute_final_unmatched_assets()added — orphan truth now matches final ownership truth- Cross-page duplicate
![[render/figures/figure_N.md]]eliminated
Test status: 150 passed (figures + reader + render)
9.5 Figure Merge Refactor + Visual Grammar Hardening (2026-06-21 ~ 06-23)
Core change: Replaced greedy region-growth with global distance clustering (union-find):
_cluster_page_assets(): horizontal <12% pw, vertical <8% ph, text-separator-aware- Caption-as-boundary: each legend claims assets by y-band
- Composite parent detection:
_build_composite_parent_figure_groups_visual_only() - Dense page arbitration:
_build_dense_composite_parent_candidates()when ≥4 visual fragments - Ownership registry:
FigureOwnershipRegistrywith conflict detection - Figure/table separation:
asset_family_hint+ table-like veto (confidence ≥0.70) - Dedup refinement:
_normalized_caption_body— internal punctuation preserved, terminal punctuation stripped _ref_number_sort_keywith[N]bracket support
Key fix (merge-gate closeout): Same-number-distinct dedup + grid collapse fix (highest-score selection, not first-match)
Test status: 216→225→261 passed (figure stack)
9.6 Rebuild Production Run (2026-06-26)
Full ocr rebuild --all (699 papers):
--resumecheckpoint support (interrupted rebuilds resume)- ASCII tqdm progress bar fix (Windows)
- Removed rogue
[DEBUG]print inocr_render.py:1295
Fixes found:
- N6XCZD25: Body paragraphs misclassified
structured_insert→body_spine_matchflag inbody_zone - Chinese Windows encoding: glob fallback in
resolve_pdf_path; garbledmeta.jsonauto-corrected on rebuild
9.7 Figure Number Inference + Container Admission (2026-06-26)
Figure number inference (leading [1] gap): 8-step algorithm in _infer_missing_main_figure_numbers(). N6XCZD25 figure_unknown_005 → figure_001.
Container admission rewrite (evidence-driven):
- 7-method container extraction: page-sized/crop-like excluded, line-like→grouping only, vertical component merge
- Three-phase per-page loop replaces lazy-cache
- Blue sidebar box score 0.45→0.80 → rendered as
[!NOTE]callout
Key commits: 45cf65e, bab0167, 5ba1a6d
9.8 UI Polish (2026-06-26)
Plugin dashboard cleanup:
- Vercel-style CSS (cards, collapsible headers, status grid, issue summary funnel)
- Title tooltip with
title_fullfield - Table sorted by time descending
- Click-to-copy on paper paths (pf-copy)
- Global "Start Working" cleanup — Doctor/Repair only in Issues section
- Run OCR button shows pending count
- Redo OCR → Maintenance button (opens settings→maintenance tab)
- DESIGN.md reference file added
9.9 Rebuild Audit + Index Repair (2026-06-27)
6 bugs fixed:
| Issue | Severity | Status |
|---|---|---|
sync_service.py: _time UnboundLocalError |
HARD BLOCK | FIXED |
| Workspace fulltext never synced from OCR output | HIGH | FIXED |
build_index early-return skips _build_entry |
HIGH | FIXED |
| Field registry missing 9 common fields → 6000+ WARN | MEDIUM | FIXED |
| "700 papers missing fulltext" — false alarm | MISLEADING | NOTED |
| Base legacy fields — user choice | INFO | DEFERRED |
Field registry additions: aliases, tags, journal, first_author, pmid, impact_factor, abstract, keywords, ocr_time.
9.10 Deep Investigation — 5 Fix Spec (2026-06-27)
5-paper vision audit: 25K5KZAQ, NC66N4Q3, 9TW98JH8, YGH7VEX6, XD2BPCMG.
Fixes identified and resolved:
| Fix | Issue | Root Cause | Resolution |
|---|---|---|---|
| Fix 1 | Figure-internal text containment | No spatial containment in build_figure_inventory |
Render-hygiene pass: 6 helpers + 19 tests ✅ |
| Fix 2 | Reference sorting [N] bracket gap |
regex only matches N./N), not [N] |
Two capture groups `r"^\s*(?:\d+ |
| Fix 3 | Backmatter boundary (CRediT/Ethics) | 7.97pt < 11pt threshold; no container keywords | Ref-anchored partition design 🔄 |
| Fix 4 | Figure caption → non_body_insert |
figure_caption in _INSERT_CANDIDATE_ROLES |
Removed list ✅ |
| Fix 5 | Demoted body paragraph in figure legends | body_paragraph re-enters matching via legend detection |
Filter before _is_validation_first_legend_candidate ✅ |
Spec: docs/superpowers/specs/2026-06-27-figure-containment-and-backmatter-boundary-design.md
Plan: docs/superpowers/plans/2026-06-27-figure-containment-implementation-plan.md
9.18 10-Paper Truth Audit + GPT Cross-Validation (2026-07-01)
Scope: 10 papers sampled from vault (3 green, 4 yellow, 3 red), batch-audited via ocr_truth_audit.py + annotated page vision agents.
1 complete vision audit: U746UJ7G — 0 matched figures, root cause = vector-rendered Figure 1+2 (52 drawing paths, 0 embedded images).
Key bugs found:
_TABLE_PREFIX_PATTERN3 处只认\d+,不认罗马数字 I/II/III(KUR9PBJC pages 4/5/7)raw_label=figure_title的 Table N caption 无 guard → 进figure_captionvision_footnote里 "This figure..." 被吞成 footnote(U746UJ7G p8:4)unmatched_assets重复计数(matched 后又在 unmatched 列表)- HTML table block 断连(2YW2MJBL)
- Vector figure pipeline 盲区(U746UJ7G)
False positives: reference_span_error ~90% noise, same_page_boundary_error 100% unreliable.
GPT 二审修正 8 处: Roman+S prefix 三处同步、score hard gate、tie-breaking、dedup、_is_near_figure_media helper。
Outputs:
- Spec:
docs/superpowers/specs/2026-07-01-ocr-audit-findings-for-gpt.md - Plan:
docs/superpowers/plans/2026-07-01-ocr-audit-gpt-fix-plan.md - Per-paper vision reports:
local://*-vision-report.mdExecution: 3/4 commits implemented (2d40ad9,21bdfd0,7670227). Commit 3 (unmatched dedup) pre-existing from PR3 (4ab227e). Follow-up refactor moved rotated-caption handling earlier: PyMuPDFdir/wmodenow preserved inspan_metadata; rotatedvision_footnotecandidates enter normal legend matching;same_page_rotatedmatches carryrotation_correction_deginto crop/render. U746UJ7G now produces a normal matched figure (figure_unknown_000,settlement_type=same_page_rotated) instead of synthetic fallback. 422 regression tests pass. - Block reviews:
audit/<KEY>/block_review.jsonl(U746UJ7G, KUR9PBJC, CGGYTEEQ, 7FNV9AW2 etc.)
9.19 Asset-Internal Figure Number Recovery (2026-07-01)
Problem: U746UJ7G rotated figure — "Figure 2. Plot of Criteria Time..." was absorbed into chart asset bbox during OCR assembly. The rotated prematch refactor (9.18) produced a matched figure (figure_unknown_000) but without figure_number. No way to assign Figure 2 to the figure.
Solution: Metadata-only recovery pass after synthetic vector fallback, before dedup.
extract_pdf_lines_normalizedinocr_pdf_spans.pyextracts PDF rawdict text lines page-by-page, normalizing coordinates to OCR space_recover_missing_figure_numbers_from_assetsiterates matched_figures without numbers, scans each matched asset's bbox for internal PDF line labels ("Figure N.", "Fig. N:")_needs_asset_internal_figure_number_recovery: gates by figure_id prefix (figure_unknown_*orsynthetic_figure_*) + text description signal_looks_like_internal_figure_label: rejects full-sentence patterns ("Figure X shows...") via regex match_asset_edge_band_score: geometric rejection for lines in asset center (not label position) or covering >15% of asset area- Coordinate normalization is caller responsibility (done in ocr_rebuild.py)
Files changed:
paperforge/worker/ocr_figures.py: 7 new functions + 2 pattern constants + signature change tobuild_figure_inventory+ recovery pass callpaperforge/worker/ocr_pdf_spans.py:extract_pdf_lines_normalizedhelperpaperforge/worker/ocr_rebuild.py: call helper, passpage_pdf_lines_by_pageto inventory buildertests/test_ocr_figures.py: 6 new tests (basic recovery, duplicate rejection, normal-fig unaffected, multi-label conflict, center rejection, overlap gate)
Spec: docs/superpowers/plans/2026-07-01-ocr-asset-internal-figure-number-recovery-plan.md
Execution:
- U746UJ7G verified:
figure_number == 2,figure_id == "figure_002",recovered_label_textcontains "Plot of Criteria Time", flags containfigure_number_recovered_from_asset_text - 428 regression tests pass (422 existing + 6 new)
9.20 Round 2 Truth Audit + 37LK5T97 Bug Fixes (2026-07-02)
Scope: 10 new papers batch-audited via ocr_truth_audit.py (high-risk mode). 5 GREEN / 4 YELLOW / 1 RED.
RED paper: 37LK5T97 — "Both IM and EC ossification occurs during the bone-healing process"
Three bugs found and fixed:
-
Figure 1 sidecar demotion: Caption (left column, 246px) and image (right column, 693px) had zero x_overlap.
_is_near_figure_media()missed it,_looks_like_figure_narrative_prose()caught the long description, and candidate_resolution demoted tobody_paragraph. Fix: Added_is_sidecar_candidate()guard — checks vertical overlap with media_asset when horizontal overlap is absent. Caption preserved asfigure_caption_candidate.- File:
paperforge/worker/ocr_document.py - Result: Figure 1 matched as
figure_001(caption block_id=2, asset block_id=9)
- File:
-
Rotated table caption matching: Tables 1-3 had rotated captions (span dir=[0,-1], vertical text beside table body).
score_table_matchrequired x_overlap + asset_below_caption, both failed for rotated sidecar layout. Fix: Addedadjacent_x+y_overlap_with_assetscoring branch when caption has rotated text and x_overlap < 0.5.- File:
paperforge/worker/ocr_scores.py - Result: All 6 tables matched (has_asset=true, score >= 0.65)
- File:
-
Rotated table render orientation: Table body also had dir=[0,-1] (rotated 90° content on portrait page). Rendered JPEG showed vertical text. Fix: Added
_table_has_rotated_content()helper; computes unionrender_bbox(caption+asset) andrender_rotation_deg=270in table entry; render loop passesrotation_degto_crop_asset_from_pdf.- Files:
paperforge/worker/ocr_tables.py,paperforge/worker/ocr_objects.py - Result: Tables 1-5 rendered at correct orientation (1908×2858 → 2858×1908)
- Files:
Additional quality fix: Rotated figure crop quality improved in prior session — Matrix(2,2) → Matrix(4,4), PIL rotation from pix.tobytes("png") (single pass, no double JPEG), and OCR→PDF coordinate conversion fix. U746UJ7G figure_002: 2618×1914 px, 5.0M px (4.5x improvement).
Commits: 59cd01a (figure quality+rotation coord fix), bd3f3b6 (sidecar+table match), 86e0d14 (table render rotation)
Tests: 428 regression tests pass (no new tests added, existing cover the affected paths)
Analysis: docs/superpowers/analysis/2026-07-02-37lk5t97-figure1-sidecar-bug-analysis.md
Audit findings: docs/superpowers/specs/2026-07-02-ocr-truth-audit-round2-findings.md
9.21 Figure Caption Prefix Recovery + Inline Table Fix (2026-07-02)
Problem 1: PaddleOCR fails to detect standalone "Figure N" / "FIGURE N" headings in bold/small-caps fonts. Caption body is captured as figure_caption_candidate but lacks the figure number prefix → _is_formal_legend() fails → caption never enters matching pool.
Fix: _recover_figure_heading_prefix() in ocr_figures.py checks the PDF text layer (via existing page_pdf_lines_by_page infrastructure — no extra PDF open) for "Figure N" lines. If the next PDF line (by y-order) shares ≥15 common-prefix chars with the OCR caption text, the heading is prepended. Runs BEFORE the zone/style filter so recovered captions pass through to legend matching.
Result: 5S7UI34M (PVA综述): 4→9 matched figures, 33→1 unmatched (p1 logo). HQAQBSBP: Figure 5 recovered. 372 figure tests pass.
Problem 2: Inline <table> HTML blocks with raw_label=table hit the raw_label="table" → media_asset fallback (ocr_roles.py:1355) before reaching the <table> → table_html check (line 1456, gated by raw_label="text"). Plus ocr_document.py:6120-6121 converted table_html to table_html_candidate — a role with no downstream handler → structural gate downgraded to unknown_structural.
Fix: (1) Moved inline <table> check before raw_label=table fallback in assign_block_role(). (2) Added table_html verifier in structural gate (self-identifying, accepts if text starts with <table>). (3) Removed dead table_html → table_html_candidate conversion.
Result: AH6Q7DLC (worst case, 30 blocks): 29/30 → table_html (1 = reference_item). 585 figure/table/role tests pass.
Files changed:
paperforge/worker/ocr_figures.py—_recover_figure_heading_prefix()+ body_zone filter guardpaperforge/worker/ocr_roles.py— inline<table>before raw_label=tablepaperforge/worker/ocr_document.py— removedtable_html_candidatedead pathpaperforge/worker/ocr_structural_gate.py—table_htmlverifier
9.22 Plan A: OCR Pairing Framework Extraction (2026-07-03)
Goal: Extract generic OCR pairing mechanics from figure vnext and migrate figure onto the framework with no behavior change. Table vnext deferred to Plan B.
Architecture (Option B):
- Generic framework:
ocr_pairing_types.py,ocr_pairing_state.py,ocr_pairing_framework.py - Figure domain:
ocr_figure_domain.py(FigureCorpus, FigureCandidateIndex), 8 pass files (import paths only) - Compatibility shims:
ocr_figure_vnext_types.py,ocr_figure_vnext_state.py,ocr_figure_vnext_corpus.pyeach re-export from framework/domain ocr_figures.py: orchestration loop replaced withrun_pairing_passes(state, pass_classes)from framework
Key decisions:
- Keep
figure_noandFigurePipelineStatenames in Plan A (rename deferred until migration stable) - Plan A extracts pass orchestration only, not full framework-owned arbitration
- Table vnext deferred — no table file changes
Commits (branch feat/ocr-pairing-framework, 6 commits over master):
| # | Commit | Description |
|---|---|---|
| 1 | 0f94123 |
docs: add Plan A implementation document |
| 2 | 96a5ddd |
fix(tests): update stale span_backfill_version constants |
| 3 | 1cc44b5 |
test(ocr): lock figure vnext extraction baseline |
| 4 | 6229f6c |
refactor(ocr): extract generic pairing types and state |
| 5 | 7cfbb5f |
refactor(ocr): add pairing pass runner and figure domain module |
| 6 | 32541cf |
test(ocr): prove rebuild compatibility with pairing framework |
Files changed: 21 files, +1546 -364
Tests: 288 pass in figure + rebuild + pairing suites (0 failed). Pre-existing paperforge.resources ModuleNotFoundError in test_ocr_document.py unrelated.
Verification gates:
- Pre-existing rebuild test fixed (stale version constant)
build_figure_inventory→build_figure_inventory_vnextdelegation test passes- Shim identity tests prove re-exports are the same class (not copies)
- Rebuild compatibility test calls
run_derived_rebuild_for_keys()for real and assertsbuild_figure_inventoryis invoked - No table file changes in diff
Spec: docs/superpowers/specs/2026-07-03-ocr-pairing-framework-design.md
Plan: docs/superpowers/plans/2026-07-03-ocr-pairing-framework-plan-a.md
Branch: feat/ocr-pairing-framework (in worktree .worktrees/feat-ocr-pairing-framework/)