docpluck 2.4.95__tar.gz → 2.4.97__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (436) hide show
  1. {docpluck-2.4.95 → docpluck-2.4.97}/.claude/skills/docpluck-deploy/SKILL.md +12 -11
  2. {docpluck-2.4.95 → docpluck-2.4.97}/.claude/skills/docpluck-iterate/LEARNINGS.md +52 -0
  3. {docpluck-2.4.95 → docpluck-2.4.97}/.claude/skills/docpluck-qa/SKILL.md +15 -2
  4. {docpluck-2.4.95 → docpluck-2.4.97}/.claude/skills/docpluck-qa/references/check-11-hard-rules.md +13 -6
  5. {docpluck-2.4.95 → docpluck-2.4.97}/.claude/skills/docpluck-review/SKILL.md +7 -0
  6. {docpluck-2.4.95 → docpluck-2.4.97}/CHANGELOG.md +26 -0
  7. {docpluck-2.4.95 → docpluck-2.4.97}/CLAUDE.md +13 -3
  8. {docpluck-2.4.95 → docpluck-2.4.97}/PKG-INFO +1 -1
  9. {docpluck-2.4.95 → docpluck-2.4.97}/TODO.md +18 -0
  10. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/__init__.py +1 -1
  11. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/extract_structured.py +105 -2
  12. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/render.py +39 -0
  13. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/tables/cell_cleaning.py +20 -1
  14. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/tables/flatten.py +118 -2
  15. docpluck-2.4.97/docs/TRIAGE_2026-06-21_head_v2.4.95_assessment.md +121 -0
  16. docpluck-2.4.97/docs/superpowers/handoffs/2026-06-21-rc-t-table-region-implementation.md +67 -0
  17. docpluck-2.4.97/docs/superpowers/handoffs/2026-06-22-dp2-dp5-flatten-fixes-commit.md +71 -0
  18. docpluck-2.4.97/docs/superpowers/specs/2026-06-21-rc-t-table-region-prose-contamination.md +85 -0
  19. {docpluck-2.4.95 → docpluck-2.4.97}/pyproject.toml +1 -1
  20. docpluck-2.4.97/scripts/check_app_pin_sync.py +182 -0
  21. docpluck-2.4.97/tests/test_rc_t_degenerate_table_real_pdf.py +201 -0
  22. docpluck-2.4.97/tests/test_rc_t_layer2_raw_text_real_pdf.py +163 -0
  23. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_tables_flatten_blank_header_recovery.py +31 -1
  24. docpluck-2.4.97/tests/test_tables_superheader_alignment_real_pdf.py +168 -0
  25. {docpluck-2.4.95 → docpluck-2.4.97}/.claude/skills/_project/canary.json +0 -0
  26. {docpluck-2.4.95 → docpluck-2.4.97}/.claude/skills/_project/lessons.md +0 -0
  27. {docpluck-2.4.95 → docpluck-2.4.97}/.claude/skills/docpluck-cleanup/SKILL.md +0 -0
  28. {docpluck-2.4.95 → docpluck-2.4.97}/.claude/skills/docpluck-iterate/SKILL.md +0 -0
  29. {docpluck-2.4.95 → docpluck-2.4.97}/.claude/skills/docpluck-iterate/references/ai-full-doc-verify.md +0 -0
  30. {docpluck-2.4.95 → docpluck-2.4.97}/.claude/skills/docpluck-iterate/references/cycle-report-template.md +0 -0
  31. {docpluck-2.4.95 → docpluck-2.4.97}/.claude/skills/docpluck-iterate/references/local-verification.md +0 -0
  32. {docpluck-2.4.95 → docpluck-2.4.97}/.claude/skills/docpluck-iterate/references/rationalizations.md +0 -0
  33. {docpluck-2.4.95 → docpluck-2.4.97}/.claude/skills/docpluck-iterate/references/real-library-real-pdf.md +0 -0
  34. {docpluck-2.4.95 → docpluck-2.4.97}/.claude/skills/docpluck-iterate/references/release-flow.md +0 -0
  35. {docpluck-2.4.95 → docpluck-2.4.97}/.claude/skills/docpluck-iterate/references/self-improvement.md +0 -0
  36. {docpluck-2.4.95 → docpluck-2.4.97}/.claude/skills/docpluck-iterate/references/subagent-parallelization.md +0 -0
  37. {docpluck-2.4.95 → docpluck-2.4.97}/.claude/skills/docpluck-iterate/references/three-tier-parity.md +0 -0
  38. {docpluck-2.4.95 → docpluck-2.4.97}/.claude/skills/docpluck-qa/references/benchmark-mode.md +0 -0
  39. {docpluck-2.4.95 → docpluck-2.4.97}/.claude/skills/docpluck-qa/references/check-13-escicheck-production.md +0 -0
  40. {docpluck-2.4.95 → docpluck-2.4.97}/.claude/skills/docpluck-qa/references/check-5-escicheck-library.md +0 -0
  41. {docpluck-2.4.95 → docpluck-2.4.97}/.claude/skills/docpluck-qa/references/check-6-escicheck-local-webapp.md +0 -0
  42. {docpluck-2.4.95 → docpluck-2.4.97}/.claude/skills/docpluck-qa/references/check-7-batch-smoke.md +0 -0
  43. {docpluck-2.4.95 → docpluck-2.4.97}/.github/workflows/bump-app-pin.yml +0 -0
  44. {docpluck-2.4.95 → docpluck-2.4.97}/.github/workflows/publish.yml +0 -0
  45. {docpluck-2.4.95 → docpluck-2.4.97}/.github/workflows/test.yml +0 -0
  46. {docpluck-2.4.95 → docpluck-2.4.97}/.gitignore +0 -0
  47. {docpluck-2.4.95 → docpluck-2.4.97}/CUSTOMER_UPDATE_2026-06-19_tables_sections_api.md +0 -0
  48. {docpluck-2.4.95 → docpluck-2.4.97}/HANDOFF_SECTIONS_APP_INTEGRATION.md +0 -0
  49. {docpluck-2.4.95 → docpluck-2.4.97}/LESSONS.md +0 -0
  50. {docpluck-2.4.95 → docpluck-2.4.97}/LICENSE +0 -0
  51. {docpluck-2.4.95 → docpluck-2.4.97}/README.md +0 -0
  52. {docpluck-2.4.95 → docpluck-2.4.97}/REPLY_FROM_DOCPLUCK_v1.4.5.md +0 -0
  53. {docpluck-2.4.95 → docpluck-2.4.97}/REPLY_FROM_DOCPLUCK_v1.5.0.md +0 -0
  54. {docpluck-2.4.95 → docpluck-2.4.97}/REPLY_FROM_DOCPLUCK_v2.4.93.md +0 -0
  55. {docpluck-2.4.95 → docpluck-2.4.97}/REPLY_FROM_DOCPLUCK_v2.4.94.md +0 -0
  56. {docpluck-2.4.95 → docpluck-2.4.97}/REPLY_FROM_DOCPLUCK_v2.4.95.md +0 -0
  57. {docpluck-2.4.95 → docpluck-2.4.97}/REQUEST_08_CHUNKING_ENDPOINT.md +0 -0
  58. {docpluck-2.4.95 → docpluck-2.4.97}/REQUEST_09_REFERENCE_LIST_NORMALIZATION.md +0 -0
  59. {docpluck-2.4.95 → docpluck-2.4.97}/REQUEST_10_TABLE_FLATTEN_HTTP_EXPOSURE.md +0 -0
  60. {docpluck-2.4.95 → docpluck-2.4.97}/REQUEST_10_TIER2_ORPHANED_LABEL_ROW_RECOVERY.md +0 -0
  61. {docpluck-2.4.95 → docpluck-2.4.97}/REQUEST_11_FLATTEN_FIELDS_NONCLINICAL_TABLES.md +0 -0
  62. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/__main__.py +0 -0
  63. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/batch.py +0 -0
  64. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/cli.py +0 -0
  65. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/extract.py +0 -0
  66. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/extract_columns.py +0 -0
  67. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/extract_docx.py +0 -0
  68. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/extract_html.py +0 -0
  69. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/extract_layout.py +0 -0
  70. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/figures/__init__.py +0 -0
  71. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/figures/detect.py +0 -0
  72. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/normalize.py +0 -0
  73. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/quality.py +0 -0
  74. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/sections/__init__.py +0 -0
  75. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/sections/annotators/__init__.py +0 -0
  76. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/sections/annotators/docx.py +0 -0
  77. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/sections/annotators/html.py +0 -0
  78. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/sections/annotators/pdf.py +0 -0
  79. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/sections/annotators/text.py +0 -0
  80. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/sections/blocks.py +0 -0
  81. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/sections/boundaries.py +0 -0
  82. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/sections/core.py +0 -0
  83. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/sections/taxonomy.py +0 -0
  84. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/sections/types.py +0 -0
  85. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/tables/__init__.py +0 -0
  86. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/tables/bbox_utils.py +0 -0
  87. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/tables/camelot_extract.py +0 -0
  88. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/tables/captions.py +0 -0
  89. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/tables/cluster.py +0 -0
  90. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/tables/confidence.py +0 -0
  91. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/tables/detect.py +0 -0
  92. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/tables/render.py +0 -0
  93. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/tables/whitespace.py +0 -0
  94. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/telemetry.py +0 -0
  95. {docpluck-2.4.95 → docpluck-2.4.97}/docpluck/version.py +0 -0
  96. {docpluck-2.4.95 → docpluck-2.4.97}/docs/BENCHMARKS.md +0 -0
  97. {docpluck-2.4.95 → docpluck-2.4.97}/docs/BENCHMARKS_liteparse_2026-06.md +0 -0
  98. {docpluck-2.4.95 → docpluck-2.4.97}/docs/DESIGN.md +0 -0
  99. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-07_sections_strict_iteration.md +0 -0
  100. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-09_session_state_and_followups.md +0 -0
  101. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-09_unified_extraction_brainstorm.md +0 -0
  102. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-10_table_rendering_iteration.md +0 -0
  103. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-10_table_rendering_iteration_2.md +0 -0
  104. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-10_table_rendering_iteration_3.md +0 -0
  105. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-10_table_rendering_iteration_4.md +0 -0
  106. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-10_table_rendering_iteration_5.md +0 -0
  107. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-10_table_rendering_iteration_6.md +0 -0
  108. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-10_table_rendering_iteration_7.md +0 -0
  109. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-11_PROMOTE_SPIKE_TO_LIBRARY.md +0 -0
  110. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-11_table_rendering_iteration_8.md +0 -0
  111. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-11_visual_review_findings.md +0 -0
  112. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-12_phase2_101pdf_corpus.md +0 -0
  113. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-12_remaining_ui_and_chrome_verification.md +0 -0
  114. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-12_visual_verify_results.md +0 -0
  115. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-13_apa_50_expansion.md +0 -0
  116. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-13_apa_50_expansion_iter_1.md +0 -0
  117. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-13_apa_50_expansion_iter_2.md +0 -0
  118. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-13_iterate_skill_first_use.md +0 -0
  119. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-13_iterative_1.md +0 -0
  120. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-13_iterative_library_improvement.md +0 -0
  121. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-13_table_extraction_next_iteration.md +0 -0
  122. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-14_continue_iterations_v2_4_30_to_15n.md +0 -0
  123. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-14_full_corpus_iteration_v2_4_30.md +0 -0
  124. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-14_iterate_6_cycles_complete.md +0 -0
  125. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-14_iterate_9_cycle_run.md +0 -0
  126. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-14_iterate_resume_4_cycles.md +0 -0
  127. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-14_iterate_v2_4_31_cycle_15n.md +0 -0
  128. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-14_phase_5d_gold_audit_v2_4_29.md +0 -0
  129. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-15_autonomous_apa_first_10h.md +0 -0
  130. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-15_iterate_apa_run_1.md +0 -0
  131. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-16_ai-gold-instructions.md +0 -0
  132. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-16_iterate_apa_run_2.md +0 -0
  133. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-16_iterate_apa_run_3.md +0 -0
  134. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-16_iterate_run_4_final.md +0 -0
  135. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-16_iterate_run_4_fix_and_continue.md +0 -0
  136. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-16_iterate_run_5.md +0 -0
  137. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-16_iterate_run_6.md +0 -0
  138. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-17_iterate_run_7.md +0 -0
  139. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-17_iterate_run_8.md +0 -0
  140. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-17_iterate_run_9.md +0 -0
  141. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-18_iterate_run_9_cont.md +0 -0
  142. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-18_iterate_run_9_cont2.md +0 -0
  143. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-20_iterate_run_9_cont3.md +0 -0
  144. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-22_iterate_run_9_session4_final.md +0 -0
  145. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-22_iterate_run_9_session5_close.md +0 -0
  146. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-25_haiku-orchestration-pretest.md +0 -0
  147. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-05-25_pretest-followups.md +0 -0
  148. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-06-08_iterate_splice-wordintegrity-runningheader.md +0 -0
  149. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-06-08_untested_sweep_v2.4.81.md +0 -0
  150. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-06-13_sciencearena_grobid_liteparse.md +0 -0
  151. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-06-15_docpluck-iterate-resume.md +0 -0
  152. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-06-15_rc1-step2-continue.md +0 -0
  153. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-06-16_docpluck-iterate-resume.md +0 -0
  154. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-06-17_iterate_resume-cycle1.md +0 -0
  155. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-06-17_iterate_v2491_shipped.md +0 -0
  156. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-06-18_iterate_v2492_affiliation_caption-revert.md +0 -0
  157. {docpluck-2.4.95 → docpluck-2.4.97}/docs/HANDOFF_2026-06-20_request11_flatten_nonclinical_tables.md +0 -0
  158. {docpluck-2.4.95 → docpluck-2.4.97}/docs/ITERATION_VERIFICATION_LESSONS.md +0 -0
  159. {docpluck-2.4.95 → docpluck-2.4.97}/docs/LIBRARY_APP_SYNC.md +0 -0
  160. {docpluck-2.4.95 → docpluck-2.4.97}/docs/NORMALIZATION.md +0 -0
  161. {docpluck-2.4.95 → docpluck-2.4.97}/docs/README.md +0 -0
  162. {docpluck-2.4.95 → docpluck-2.4.97}/docs/TRIAGE_2026-05-10_corpus_assessment.md +0 -0
  163. {docpluck-2.4.95 → docpluck-2.4.97}/docs/TRIAGE_2026-05-14_phase_5d_gold_audit.md +0 -0
  164. {docpluck-2.4.95 → docpluck-2.4.97}/docs/TRIAGE_2026-06-08_untested_corpus_sweep.md +0 -0
  165. {docpluck-2.4.95 → docpluck-2.4.97}/docs/TRIAGE_2026-06-15_head_v2.4.88_assessment.md +0 -0
  166. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/handoffs/2026-05-22-b1-next-iteration.md +0 -0
  167. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/handoffs/2026-05-22-b2-remaining-halluc-head.md +0 -0
  168. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/handoffs/2026-05-22-b3-b7-structural-defects.md +0 -0
  169. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/handoffs/2026-05-22-residual-after-locally-doable-pass.md +0 -0
  170. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/handoffs/2026-05-23-bundled-residual-cycle-CLOSED.md +0 -0
  171. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/handoffs/2026-05-23-residual-after-iterate-spine-cycles-1-3.md +0 -0
  172. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/handoffs/2026-05-25-canary-audit-architecture-and-cluster-A-B-C-landed.md +0 -0
  173. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/handoffs/2026-05-25-wrapup-r4-cycle.md +0 -0
  174. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/handoffs/2026-05-26-run-11-cluster-A-ter-and-C-bis-landed.md +0 -0
  175. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/handoffs/2026-05-26-text-extraction-defects-from-citationguard-audit.md +0 -0
  176. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/handoffs/2026-06-07-text-extraction-defects-from-citationguard-iterate.md +0 -0
  177. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/handoffs/2026-06-07-v2.4.78-landed-canary-iterate.md +0 -0
  178. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/handoffs/2026-06-07-v2.4.79-findings-1-2-cleared.md +0 -0
  179. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/handoffs/2026-06-20_docpluck-skill-file-edits-from-app-cron-fix.md +0 -0
  180. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/2026-05-06-section-identification.md +0 -0
  181. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/2026-05-06-table-extraction.md +0 -0
  182. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/2026-05-07-sections-strict-iteration-progress.md +0 -0
  183. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/2026-05-08-unified-extraction-phase-0-splice-spike.md +0 -0
  184. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/2026-05-23-haiku-orchestration-pretest.md +0 -0
  185. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/sections-deferred-items.md +0 -0
  186. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/sections-issues-backlog.md +0 -0
  187. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/2026-05-07_spot-01_apa.md +0 -0
  188. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/2026-05-07_spot-02_pattern-A-shipped.md +0 -0
  189. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/2026-05-08_spot-final_all-styles.md +0 -0
  190. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/COMPARISON.md +0 -0
  191. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-a/korbmacher_table1.md +0 -0
  192. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-a/option-a.py +0 -0
  193. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-a/ziano_table1.md +0 -0
  194. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-b/korbmacher_notes_raw.txt +0 -0
  195. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-b/korbmacher_table1.md +0 -0
  196. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-b/notes.md +0 -0
  197. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-b/option-b.py +0 -0
  198. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-b/ziano_notes_raw.txt +0 -0
  199. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-b/ziano_table1.md +0 -0
  200. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-c/korbmacher_table1.md +0 -0
  201. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-c/notes.md +0 -0
  202. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-c/option-c.py +0 -0
  203. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-c/sample-pdftotext-bbox.html +0 -0
  204. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-c/ziano_table1.md +0 -0
  205. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-d/korbmacher_table1.md +0 -0
  206. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-d/notes.md +0 -0
  207. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-d/option-d.py +0 -0
  208. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-d/ziano_table1.md +0 -0
  209. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-e/korbmacher_2022_kruger_bbox.html +0 -0
  210. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-e/korbmacher_bbox.html +0 -0
  211. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-e/korbmacher_table1.md +0 -0
  212. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-e/option-e.py +0 -0
  213. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-e/sample-bbox.html +0 -0
  214. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-e/ziano_2021_joep_bbox.html +0 -0
  215. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-e/ziano_bbox.html +0 -0
  216. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/experiments/option-e/ziano_table1.md +0 -0
  217. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/html-fallback-demo.md +0 -0
  218. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs/chandrashekar_2023_mp.err +0 -0
  219. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs/chandrashekar_2023_mp.md +0 -0
  220. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs/efendic_2022_affect.err +0 -0
  221. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs/efendic_2022_affect.md +0 -0
  222. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs/ieee_access_2.err +0 -0
  223. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs/ieee_access_2.md +0 -0
  224. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs/ip_feldman_2025_pspb.err +0 -0
  225. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs/ip_feldman_2025_pspb.md +0 -0
  226. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs/korbmacher_2022_kruger.err +0 -0
  227. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs/korbmacher_2022_kruger.md +0 -0
  228. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs/nat_comms_1.err +0 -0
  229. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs/nat_comms_1.md +0 -0
  230. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs/ziano_2021_joep.err +0 -0
  231. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs/ziano_2021_joep.md +0 -0
  232. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/am_sociol_rev_3.err +0 -0
  233. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/am_sociol_rev_3.md +0 -0
  234. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/amc_1.err +0 -0
  235. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/amc_1.md +0 -0
  236. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/amj_1.err +0 -0
  237. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/amj_1.md +0 -0
  238. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/amle_1.err +0 -0
  239. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/amle_1.md +0 -0
  240. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/ar_apa_j_jesp_2009_12_010.err +0 -0
  241. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/ar_apa_j_jesp_2009_12_010.md +0 -0
  242. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/ar_royal_society_rsos_140066.err +0 -0
  243. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/ar_royal_society_rsos_140066.md +0 -0
  244. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/ar_royal_society_rsos_140072.err +0 -0
  245. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/ar_royal_society_rsos_140072.md +0 -0
  246. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/bjps_1.err +0 -0
  247. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/bjps_1.md +0 -0
  248. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/chan_feldman_2025_cogemo.err +0 -0
  249. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/chan_feldman_2025_cogemo.md +0 -0
  250. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/chen_2021_jesp.err +0 -0
  251. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/chen_2021_jesp.md +0 -0
  252. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/demography_1.err +0 -0
  253. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/demography_1.md +0 -0
  254. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/ieee_access_3.err +0 -0
  255. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/ieee_access_3.md +0 -0
  256. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/ieee_access_4.err +0 -0
  257. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/ieee_access_4.md +0 -0
  258. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/jama_open_1.err +0 -0
  259. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/jama_open_1.md +0 -0
  260. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/jama_open_2.err +0 -0
  261. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/jama_open_2.md +0 -0
  262. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/jmf_1.err +0 -0
  263. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/jmf_1.md +0 -0
  264. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/nat_comms_2.err +0 -0
  265. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/nat_comms_2.md +0 -0
  266. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/sci_rep_1.err +0 -0
  267. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/sci_rep_1.md +0 -0
  268. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/social_forces_1.err +0 -0
  269. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/outputs-new/social_forces_1.md +0 -0
  270. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/papers.md +0 -0
  271. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/report.md +0 -0
  272. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/splice_spike.py +0 -0
  273. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/plans/spot-checks/splice-spike/test_splice_spike.py +0 -0
  274. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/specs/2026-04-27-request-09-reference-normalization-design.md +0 -0
  275. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/specs/2026-05-06-section-identification-design.md +0 -0
  276. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/specs/2026-05-06-table-extraction-design.md +0 -0
  277. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/specs/2026-05-08-unified-extraction-design.md +0 -0
  278. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/specs/2026-05-23-haiku-orchestration-pretest-design.md +0 -0
  279. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/specs/2026-06-07-ip_feldman-B4-R4-column-interleave-diagnosis.md +0 -0
  280. {docpluck-2.4.95 → docpluck-2.4.97}/docs/superpowers/specs/2026-06-08-rc1-region-aware-column-architecture.md +0 -0
  281. {docpluck-2.4.95 → docpluck-2.4.97}/scripts/__init__.py +0 -0
  282. {docpluck-2.4.95 → docpluck-2.4.97}/scripts/check_docs_consistency.py +0 -0
  283. {docpluck-2.4.95 → docpluck-2.4.97}/scripts/harness/README.md +0 -0
  284. {docpluck-2.4.95 → docpluck-2.4.97}/scripts/harness/VERIFIER_PROMPT.md +0 -0
  285. {docpluck-2.4.95 → docpluck-2.4.97}/scripts/harness/__init__.py +0 -0
  286. {docpluck-2.4.95 → docpluck-2.4.97}/scripts/harness/baseline_matrix.json +0 -0
  287. {docpluck-2.4.95 → docpluck-2.4.97}/scripts/harness/checks.py +0 -0
  288. {docpluck-2.4.95 → docpluck-2.4.97}/scripts/harness/corpus.py +0 -0
  289. {docpluck-2.4.95 → docpluck-2.4.97}/scripts/harness/corpus_manifest.json +0 -0
  290. {docpluck-2.4.95 → docpluck-2.4.97}/scripts/harness/extract.py +0 -0
  291. {docpluck-2.4.95 → docpluck-2.4.97}/scripts/harness/gold_keys.json +0 -0
  292. {docpluck-2.4.95 → docpluck-2.4.97}/scripts/harness/inspect.py +0 -0
  293. {docpluck-2.4.95 → docpluck-2.4.97}/scripts/lint_rendered_corpus.py +0 -0
  294. {docpluck-2.4.95 → docpluck-2.4.97}/scripts/pretest_capture_tokens.py +0 -0
  295. {docpluck-2.4.95 → docpluck-2.4.97}/scripts/verify_corpus.py +0 -0
  296. {docpluck-2.4.95 → docpluck-2.4.97}/scripts/verify_corpus_full.py +0 -0
  297. {docpluck-2.4.95 → docpluck-2.4.97}/tests/__init__.py +0 -0
  298. {docpluck-2.4.95 → docpluck-2.4.97}/tests/conftest.py +0 -0
  299. {docpluck-2.4.95 → docpluck-2.4.97}/tests/fixtures/__init__.py +0 -0
  300. {docpluck-2.4.95 → docpluck-2.4.97}/tests/fixtures/sections/__init__.py +0 -0
  301. {docpluck-2.4.95 → docpluck-2.4.97}/tests/fixtures/sections/builders.py +0 -0
  302. {docpluck-2.4.95 → docpluck-2.4.97}/tests/fixtures/structured/.gitkeep +0 -0
  303. {docpluck-2.4.95 → docpluck-2.4.97}/tests/fixtures/structured/MANIFEST.json +0 -0
  304. {docpluck-2.4.95 → docpluck-2.4.97}/tests/fixtures/structured/README.md +0 -0
  305. {docpluck-2.4.95 → docpluck-2.4.97}/tests/golden/sections/apa_multi_study_pdf.json +0 -0
  306. {docpluck-2.4.95 → docpluck-2.4.97}/tests/golden/sections/apa_single_study_pdf.json +0 -0
  307. {docpluck-2.4.95 → docpluck-2.4.97}/tests/golden/sections/html_real_headings.json +0 -0
  308. {docpluck-2.4.95 → docpluck-2.4.97}/tests/snapshots/amj_lattice.txt +0 -0
  309. {docpluck-2.4.95 → docpluck-2.4.97}/tests/snapshots/apa_chan_feldman_lineless.txt +0 -0
  310. {docpluck-2.4.95 → docpluck-2.4.97}/tests/snapshots/apa_chen_jesp_lineless.txt +0 -0
  311. {docpluck-2.4.95 → docpluck-2.4.97}/tests/snapshots/apa_efendic_affect.txt +0 -0
  312. {docpluck-2.4.95 → docpluck-2.4.97}/tests/snapshots/apa_ip_feldman_pspb.txt +0 -0
  313. {docpluck-2.4.95 → docpluck-2.4.97}/tests/snapshots/bmc_lattice.txt +0 -0
  314. {docpluck-2.4.95 → docpluck-2.4.97}/tests/snapshots/ieee_figure_heavy.txt +0 -0
  315. {docpluck-2.4.95 → docpluck-2.4.97}/tests/snapshots/ieee_lattice.txt +0 -0
  316. {docpluck-2.4.95 → docpluck-2.4.97}/tests/snapshots/jama_lattice.txt +0 -0
  317. {docpluck-2.4.95 → docpluck-2.4.97}/tests/snapshots/nat_comms_figure_only.txt +0 -0
  318. {docpluck-2.4.95 → docpluck-2.4.97}/tests/snapshots/nature_minimal_rule.txt +0 -0
  319. {docpluck-2.4.95 → docpluck-2.4.97}/tests/snapshots/scirep_minimal_rule.txt +0 -0
  320. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_a3c_leading_zero_decimal_real_pdf.py +0 -0
  321. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_a4_ci_period_to_comma.py +0 -0
  322. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_affiliation_heading_promote_guard_real_pdf.py +0 -0
  323. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_all_caps_section_promote_real_pdf.py +0 -0
  324. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_bbox_utils.py +0 -0
  325. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_benchmark_docx_html.py +0 -0
  326. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_cambridge_footer_strip_real_pdf.py +0 -0
  327. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_camelot_lattice_augment.py +0 -0
  328. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_camelot_temp_cleanup.py +0 -0
  329. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_canary_provenance.py +0 -0
  330. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_caption_only_table_heading_real_pdf.py +0 -0
  331. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_caption_regex.py +0 -0
  332. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_chart_data_trim_real_pdf.py +0 -0
  333. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_cid_minus_recovery_real_pdf.py +0 -0
  334. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_cli_sections.py +0 -0
  335. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_cli_structured.py +0 -0
  336. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_confidence.py +0 -0
  337. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_corpus_smoke.py +0 -0
  338. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_d5_normalization_audit.py +0 -0
  339. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_dropped_minus_layout_recovery_real_pdf.py +0 -0
  340. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_edge_cases.py +0 -0
  341. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_elsevier_footer_strip_real_pdf.py +0 -0
  342. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_equation_page_header_strip_real_pdf.py +0 -0
  343. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_extract_columns.py +0 -0
  344. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_extract_docx.py +0 -0
  345. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_extract_filter_sugar.py +0 -0
  346. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_extract_html.py +0 -0
  347. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_extract_layout.py +0 -0
  348. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_extract_pdf_structured.py +0 -0
  349. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_extraction.py +0 -0
  350. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_f0_table_region_aware.py +0 -0
  351. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_fffd_comparison_recovery_real_pdf.py +0 -0
  352. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_figure_caption_trim_real_pdf.py +0 -0
  353. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_figure_detect.py +0 -0
  354. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_fixtures_manifest.py +0 -0
  355. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_hallucinated_heading_continuation_guard.py +0 -0
  356. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_harness_text_loss_reflow.py +0 -0
  357. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_harvard_refs_pagebreak_stitch.py +0 -0
  358. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_jama_open_cluster_real_pdf.py +0 -0
  359. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_lattice_cluster.py +0 -0
  360. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_letterspaced_label_real_pdf.py +0 -0
  361. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_ligature_decomposition_real_pdf.py +0 -0
  362. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_lt_operator_recovery_real_pdf.py +0 -0
  363. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_mathitalic_greek_real_pdf.py +0 -0
  364. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_metaesci_followups.py +0 -0
  365. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_minus_sign_recovery_real_pdf.py +0 -0
  366. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_normalization.py +0 -0
  367. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_normalize_a3_r2_body_integer_real_pdf.py +0 -0
  368. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_normalize_f0_footnote_strip.py +0 -0
  369. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_normalize_idempotent_real_pdf.py +0 -0
  370. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_normalize_layout_param.py +0 -0
  371. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_normalize_metadata_leak_real_pdf.py +0 -0
  372. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_normalize_report_layout_fields.py +0 -0
  373. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_normalize_soft_hyphen_dehyphenation.py +0 -0
  374. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_normalize_v18_strips.py +0 -0
  375. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_numbered_heading_promotion_real_pdf.py +0 -0
  376. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_numbered_section_promotion_real_pdf.py +0 -0
  377. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_o5_reference_inversion_real_pdf.py +0 -0
  378. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_orphan_multilevel_number_real_pdf.py +0 -0
  379. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_orphan_section_number_real_pdf.py +0 -0
  380. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_p0r_recurring_running_header_strip.py +0 -0
  381. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_preserve_math_glyphs_real_pdf.py +0 -0
  382. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_pretest_capture_tokens.py +0 -0
  383. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_pua_glyph_recovery_real_pdf.py +0 -0
  384. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_quality.py +0 -0
  385. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_r1_whitespace_cells_wiring_real_pdf.py +0 -0
  386. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_r4_column_correction_real_pdf.py +0 -0
  387. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_rc1_banded_column_real_pdf.py +0 -0
  388. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_rc1_general_column_correction_real_pdf.py +0 -0
  389. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_render.py +0 -0
  390. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_render_frontmatter_masthead.py +0 -0
  391. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_render_html.py +0 -0
  392. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_render_subsection_chain_promotion.py +0 -0
  393. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_request_09_reference_normalization.py +0 -0
  394. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_residual_2026_05_23_bundled.py +0 -0
  395. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_roman_numeral_section_promote_real_pdf.py +0 -0
  396. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_section_row_label_no_merge_real_pdf.py +0 -0
  397. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_sections_boundaries.py +0 -0
  398. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_sections_boundary_truncation.py +0 -0
  399. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_sections_core_partition.py +0 -0
  400. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_sections_docx_annotator.py +0 -0
  401. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_sections_extract_text.py +0 -0
  402. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_sections_footnote_section.py +0 -0
  403. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_sections_golden.py +0 -0
  404. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_sections_html_annotator.py +0 -0
  405. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_sections_pdf_annotator.py +0 -0
  406. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_sections_public_api.py +0 -0
  407. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_sections_real_corpus.py +0 -0
  408. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_sections_taxonomy.py +0 -0
  409. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_sections_text_annotator.py +0 -0
  410. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_sections_types.py +0 -0
  411. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_sections_unit_corpus.py +0 -0
  412. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_sections_v161_coalesce.py +0 -0
  413. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_sections_v161_subheadings.py +0 -0
  414. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_sections_v161_taxonomy.py +0 -0
  415. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_sections_v161_text_annotator.py +0 -0
  416. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_sections_version.py +0 -0
  417. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_single_column_subsection_promote_real_pdf.py +0 -0
  418. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_smoke_fixtures.py +0 -0
  419. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_structured_result_type.py +0 -0
  420. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_structured_types.py +0 -0
  421. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_structured_version.py +0 -0
  422. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_table_caption_cell_region_real_pdf.py +0 -0
  423. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_table_detect.py +0 -0
  424. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_tables_cell_cleaning.py +0 -0
  425. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_tables_flatten.py +0 -0
  426. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_text_mode.py +0 -0
  427. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_v23_1_fixes.py +0 -0
  428. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_v23_bug_fixes.py +0 -0
  429. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_v23_post_corpus.py +0 -0
  430. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_v23_post_corpus_v2.py +0 -0
  431. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_v2_backwards_compat.py +0 -0
  432. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_v2_top_level_exports.py +0 -0
  433. {docpluck-2.4.95 → docpluck-2.4.97}/tests/test_whitespace_cluster.py +0 -0
  434. {docpluck-2.4.95 → docpluck-2.4.97}/tools/canary_provenance.py +0 -0
  435. {docpluck-2.4.95 → docpluck-2.4.97}/tools/fix_python_env.ps1 +0 -0
  436. {docpluck-2.4.95 → docpluck-2.4.97}/tools/render_for_audit.py +0 -0
@@ -84,28 +84,29 @@ print(f'All imports OK; docpluck=={info[\"version\"]} normalize={info[\"normaliz
84
84
  "
85
85
  ```
86
86
 
87
- ### 4. Cross-Repo Library Version Sync (CRITICAL)
87
+ ### 4. Cross-Repo Library Version Sync (CRITICAL — "when we bump the package, we bump the app")
88
88
 
89
- Verify the app's `service/requirements.txt` git pin matches the library's latest tag. Mismatches mean the deploy will silently ship the OLD library to prod.
89
+ Verify the app's `service/requirements.txt` git pin matches the library's latest released tag. A mismatch means the deploy silently ships the OLD library to prod. **The pin is read from docpluckapp `origin/master` (what Railway deploys), NOT the local clone — a stale local checkout shows an old pin even when prod is correctly synced, which almost causes a phantom "fix".** The shared gate (also run by `/docpluck-qa` check 11b and `/docpluck-review` rule 22) is the single source of truth:
90
90
 
91
91
  ```bash
92
- LIB_VERSION=$(grep '^__version__' C:/Users/filin/Dropbox/Vibe/MetaScienceTools/docpluck/docpluck/__init__.py | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
93
- APP_PIN=$(grep -oE 'docpluck.*@v[0-9]+\.[0-9]+\.[0-9]+' C:/Users/filin/Dropbox/Vibe/MetaScienceTools/PDFextractor/service/requirements.txt | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
94
- echo "Library __version__: $LIB_VERSION"
95
- echo "App requirements.txt pin: v$APP_PIN"
96
- if [ "$LIB_VERSION" != "$APP_PIN" ]; then
97
- echo "❌ MISMATCH — bump PDFextractor/service/requirements.txt to docpluck @ git+https://github.com/giladfeldman/docpluck.git@v$LIB_VERSION before deploying"
92
+ cd C:/Users/filin/Dropbox/Vibe/MetaScienceTools/docpluck && python scripts/check_app_pin_sync.py || {
93
+ echo "Cross-repo pin sync FAILED recover before deploying:"
94
+ echo " - re-push the tag: git push origin v<VERSION> (re-fires bump-app-pin.yml), OR"
95
+ echo " - hand-bump PDFextractor/service/requirements.txt to @v<VERSION> and push to docpluckapp master."
98
96
  exit 1
99
- fi
97
+ }
98
+ # Note: a working-tree __version__ ahead of the latest tag is reported UNRELEASED (not a failure) —
99
+ # that is the normal pre-flight state; the "Library Release Step" below tags+pushes it, which
100
+ # fires the auto-bump, and post-deploy check 3 confirms Railway /health reports the new version.
100
101
 
101
102
  # Also verify the API.md examples are not stale beyond a major version
103
+ LIB_VERSION=$(grep '^__version__' C:/Users/filin/Dropbox/Vibe/MetaScienceTools/docpluck/docpluck/__init__.py | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
102
104
  API_DOC_VERSION=$(grep -oE 'docpluck_version["\s:]+[0-9]+\.[0-9]+\.[0-9]+' C:/Users/filin/Dropbox/Vibe/MetaScienceTools/PDFextractor/API.md | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
103
105
  LIB_MAJOR_MINOR=$(echo "$LIB_VERSION" | cut -d. -f1,2)
104
106
  DOC_MAJOR_MINOR=$(echo "$API_DOC_VERSION" | cut -d. -f1,2)
105
107
  if [ "$LIB_MAJOR_MINOR" != "$DOC_MAJOR_MINOR" ]; then
106
- echo "⚠️ API.md examples reference docpluck_version $API_DOC_VERSION; library is at $LIB_VERSION. Update PDFextractor/API.md."
108
+ echo "WARN: API.md examples reference docpluck_version $API_DOC_VERSION; library is at $LIB_VERSION. Update PDFextractor/API.md."
107
109
  fi
108
- echo "✅ Library version sync OK"
109
110
  ```
110
111
 
111
112
  ### 5. Verify Vercel Environment Variables
@@ -1275,3 +1275,55 @@ aren't skipped.
1275
1275
  2. **A bounded sample gives FALSE CONFIDENCE — the full-corpus regression gate (rule 19) is non-negotiable and earned its keep here.** The 11-paper diff said "only the target changed, ship it"; the 48-paper diff revealed 4 real-heading false positives. I nearly shipped a regression off a green bounded sample. ALWAYS run the guard-live-vs-bypassed diff over the WHOLE corpus before trusting a heading-promotion change — bounded samples miss the long tail where the FP pattern lives.
1276
1276
 
1277
1277
  **Open queue (run stays OPEN — standing verdict FAIL):** the cell-label cases are table-content-as-prose → fold into the table cluster, NOT a standalone render guard; the TABLE cluster (highest impact, architectural bbox decision outstanding since 2026-05-22 — needs user scope decision); RC-1 band path (multi-session, riskiest); residual metadata-leaks. The clean render-layer slice that DID ship this run (affiliation v2.4.92) plus the prior single-column v2.4.91 are done; the remainder is architectural.
1278
+
1279
+ ---
1280
+
1281
+ ## 2026-06-21 · Resume · cycle 3 · full real AI-verify @ v2.4.95 = 7/7 canary FAIL; corpus at an ARCHITECTURAL boundary (3 root causes, all need sign-off); 2 open_findings adjudicated NOT-defects
1282
+
1283
+ **Target:** "keep addressing todo, iterating and improving." Found the run half-open after the v2.4.95 Request-11 ship (cycles 1-2), TRIAGE stale @ v2.4.88, 2 carried-over open_findings. **Verdict: cycle 3 = VERIFY + ADJUDICATE + SURFACE-DECISION, NO code shipped.** Standing FAIL.
1284
+
1285
+ **The canary-audit clobber masked a fully-broken corpus AGAIN (memory `feedback_canary_audit_clobbers_phase5d`, re-confirmed live).** The fresh HEAD canary render (`canary-2dbdd98`) carried 5 `verdict:PASS` files — but every one was `raw_verdicts:[AUDIT_DEFERRED_TO_AGENT,AUDIT_DEFERRED_TO_AGENT] → union PASS`. `AUDIT_DEFERRED` means the headless Sonnet deferred to the in-session agent and the hook recorded the *non-verdict* as PASS. Re-running 7 real in-session Sonnet verifiers vs the article-finder golds → **7/7 FAIL.** **Never trust a canary-audit PASS whose `raw_verdicts` are `AUDIT_DEFERRED`; it is a placeholder, not a verification.** Re-verify manually after a commit before trusting I3-green.
1286
+
1287
+ **Narrow-scope verification hides broad defects (Phase-0.8 cross-output lesson, re-proven).** maier_2023_collabra was recorded PASS in cycles 1-2 — but those only checked the *specific* Request-11 flatten fields (T8/T10). The full-document verify this cycle shows maier broadly FAIL: T5 unstructured fallback, T7 garbled with body text, T8/T9/T11 empty headers, section displacement. The narrow check wasn't wrong for its scope; it never looked at the rest of the doc. **A per-feature "PASS" is not a per-document PASS — when the canary rotation comes around, verify the WHOLE document, not the fields the last cycle touched.**
1288
+
1289
+ **All 7 papers' defects cluster to 3 ARCHITECTURAL root causes (TRIAGE_2026-06-21):**
1290
+ 1. **RC-T table-bbox** (widest — all 7, single + two-column): Camelot grabs furniture/adjacent prose → empty shells, garbled cells, missing headers, duplicate dumps, orphan `### Table N`. Proof: ip_feldman Table-10 cells = running-header `Ip and Feldman` + page `15` + `Discussion` heading + Discussion prose + 1 real row, all bbox `(0,0,0,0)` → render *correctly* drops the `<table>`. Bbox decision open since 2026-05-22.
1291
+ 2. **RC-1 column/sidebar interleave** (chan_feldman, chandrashekar, plos_med sidebar): the furniture-strip is *defeated by* the interleave (plos_med's whole front-matter sidebar lands before the Abstract; `## Abstract Published: <date>` weld). Spec ready (2026-06-08 region-aware).
1292
+ 3. **RC-B7 deleted-minus glyph** (ar_apa): 5 body-prose betas sign-flipped `β=−.022`→`b=.022`. 3-path decision pending.
1293
+
1294
+ **No clean non-architectural win remains — chased the 3 best leads, each bottomed out in a root cause above** (plos_med masthead-strip = interleave; ip_feldman Table-10 splice = bbox-garbage; abstract-date weld = interleave). Per the skill (avoid C4 without sign-off) + must-stop ("fix needs an architectural decision"), surfaced the 3-way decision to the user rather than diving in blind.
1295
+
1296
+ **2 open_findings adjudicated NOT docpluck defects (LEAVE NOTHING BEHIND — inspected locally, did not defer):**
1297
+ - `collabra_77859` "Table 3" vs gold "Table 2": source text-channel caption (line 866) is verbatim `Table 3. Study 4: Dish sets`; docpluck correct, **gold mis-numbered** → article-finder.
1298
+ - `collabra_90203` Table 10 r=.59 vs .63: pdftotext literally emits `.59` (text-line 1706), Camelot agrees; `.63` is visual-only → source text-layer/visual divergence, OCR-only. Documented limitation.
1299
+
1300
+ **Addendum (same session, user authorized "do all three, 1-3"): RC-B7 was ALREADY DONE; RC-T root-caused; checkpointed before the big multi-session table work.**
1301
+
1302
+ **RC-B7 (authorized #1) = already implemented — verify the codebase before re-solving an "architectural" item.** I set out to build the B7 layout-channel minus recovery the old TRIAGE called for — and found it already exists as **W0h** (`normalize.recover_dropped_minus_via_layout`), wired (render.py:5079→sections→normalize.py:3170) and regression-tested (`tests/test_dropped_minus_layout_recovery_real_pdf.py`). HEAD renders `b=-.022 / -.88 / -.428` correctly (4/5 ar_apa betas). My own cycle-3 FAIL was a **verifier over-flag** — it quoted the W0h-recovered `-.022`/`-.428` and still called them GLYPH defects. **Lesson: before treating a TRIAGE "architectural, needs sign-off" item as open, grep the library for an existing implementation — a prior session may have already shipped it. The verifier's FAIL is a hypothesis, not a fact; reproduce + read the code at HEAD first.** Residuals (`.245` pixel-minus, β→b) confirmed OCR-tier: probed BOTH channels — pdfplumber also extracts `b` (font AdvPSMP10) and shows no `.245` minus glyph; outside docpluck's MIT text+layout architecture, already documented in the W0h comment.
1303
+
1304
+ **RC-T (authorized #2) root cause = the FULL-PAGE-BBOX signature.** ip_feldman Table 10's region bbox is `(53, 53, 577, 800)` — top→bottom spans the whole of page 15 (vs Tables 1-9 = tight sub-region bands), so the "table" swallowed the running header `Ip and Feldman` + page `15` + `Discussion` heading + Discussion prose + 1 real data row (cells all bbox `(0,0,0,0)`). **The fix must key on CELL CONTENT (furniture/prose signatures), NOT bbox-size** — legitimate landscape Tables 6/7/8 also have tall bboxes; a degenerate region → clean unstructured fallback (no orphan `### Table N`, no prose-as-cells). This is a multi-session, high-regression-surface change.
1305
+
1306
+ **Pacing decision (per LEARNINGS rule: full-corpus gate is non-negotiable; don't rush heading/table changes at session-tail).** After a long verification+investigation session, I deliberately did NOT start the RC-T/RC-1 implementation — a table-bbox change rushed without a careful full-corpus regression pass is exactly how the cycle-3 caption-follows revert happened. Checkpointed with the characterization done so RC-T can be a focused dedicated effort. Standing verdict stays FAIL (RC-T + RC-1 open).
1307
+
1308
+ ---
1309
+
1310
+ ## Run: 2026-06-21 (PM) · cycle 1 · RC-T Option A implemented → v2.4.96
1311
+
1312
+ ### Outcome
1313
+ - **SHIPPED (incremental):** RC-T degenerate prose-table strip — `render.py::_strip_phantom_camelot_tables`. Net corpus impact: **exactly 2 tables** now fail-clean (maier_2023 Table 7, chan_feldman Table 6), all else byte-identical. ip_feldman Table 10 (the RC-T canonical "orphan") was **already** fail-clean at HEAD; the real fixable garbage was elsewhere. Standing verdict remains **PARTIAL/FAIL** — RC-T Layer-1 recovery + RC-1 interleave are deferred (user-scoped to Option A), so canaries still FAIL on those classes.
1314
+
1315
+ ### Blind Spots (corrections to the prior session's RC-T characterization)
1316
+ - **The prior entry framed RC-T as "full-page-bbox → emit a cell-content degenerate guard / route to unstructured fallback / no orphan `### Table N`."** Reproduction at HEAD corrected this: ip_feldman T10 is **already stripped** at HEAD (its folded-prose `<th>` already trips `_strip_phantom_camelot_tables` via the fn≥3 path), rendering `### Table 10` + caption + no table = already fail-clean. The actual open gap was much smaller: **"the" — the single most common English function word — was missing from `_FUNCTION_WORDS_IN_PROSE`**, so a body-prose `<th>` with `fn==2 (+the)` slipped under the bar (maier T7 "Following the analyses conducted in Study 1 of Small": fn=in,of=2; verb=following,conducted=2). Lesson: a "build a whole new guard" item can collapse to a one-word set fix once you reproduce + read the EXISTING guard at HEAD (compare RC-B7's "already implemented as W0h").
1317
+ - **Cell-content "majority prose/furniture" is NOT a safe degenerate discriminator** — my first attempt (a data-layer `_table_cells_are_degenerate` keyed on majority prose) FP'd immediately on chan_feldman Table 5, a **legitimate comparison table whose cells are full descriptive sentences by design** (prose-in-cells is normal for comparison/design/instrument tables). Reverted. The reliable signature is narrower: a **≥8-word sentence-shaped `<th>`** (real column headers are short noun phrases; a sentence in the header == Camelot folded body prose). Legit prose-bearing tables keep short headers.
1318
+
1319
+ ### Edge Cases (the title-leak FP — caught only by the full-corpus scan)
1320
+ - **A real table can leak its own TITLE into the `<th>`** (Camelot whole-page bbox grabs the caption row as the header). `aom/amp_1` Table 5's `<th>` is its caption "Improving Scholarly Impact … Practice" over a REAL grid (Domains/Policymaking/Practice + data). Adding "the" naively strips this real table = regression. **Discriminator: caption-token overlap.** A title-leak `<th>` shares ≥60% of its tokens with a caption line; a body-prose `<th>` (maier T7, chan_feldman T6) shares ~none. Gate the new strip on `not is_title_leak`.
1321
+ - **A relaxation can secretly be a broadening.** My first title-leak exclusion skipped title-leak `<th>`s for ALL fn-paths — which would have changed the verdict for **37 tables already stripped at HEAD** (many with title-shaped `<th>`s like jama-open-1 T3 "Effect of Time-Restricted Eating…", bmc-pub-health-2 "Table 2 Collection of equations…"). Whether each of those 37 is correct-strip vs wrongly-stripped-real-table is a **pre-existing, unverified question**. Fix: **scope the new behavior to the marginal case only** — fire only when "the" crosses fn 2→3 (`fn_count<3 and fn_count+the_count>=3`), so every HEAD-stripped table is byte-identical and only genuinely-new body-prose strips are added.
1322
+
1323
+ ### Improvements (verification methodology)
1324
+ - **th-level full-corpus FP scan beats a bounded render sample AND a full render-diff for a surgical guard change.** `tmp/repro/fp_scan.py` extracted all 152 corpus PDFs once and, per table, computed the exact `_strip_phantom` th counts with/without "the" — pinpointing the precise change set (3 fn-flip tables; 37 HEAD-stripped candidates for un-strip regression) in ~13 min, far cheaper than 304 renders. For a change confined to one code path, analyze THAT path corpus-wide rather than diffing whole renders. (Still confirmed the deciding cases with real renders.)
1325
+ - **The full-corpus gate earned its keep AGAIN.** The 4-paper sample (ip_feldman/maier/chan_feldman/plos_med) looked clean at every step; the FP (amp_1 T5) and the 37-table over-broad-exclusion lived ONLY in the long tail — exactly the cycle-3-caption-follows trap. Never trust a bounded sample for a table/heading guard.
1326
+
1327
+ ### Verification Gaps / Deferred (queued, not dropped)
1328
+ - **RC-T Layer-1 recovery** — actually RECOVERING lost table data via tight `table_areas` (plos_med T5's 13 SAE rows; chan_feldman T2 column-squish) — out of Option-A scope, deferred by the user.
1329
+ - **Audit of the ~37 `_strip_phantom` th-stripped tables** — some title-shaped `<th>` strips may be wrongly-stripped REAL tables (pre-existing, predates this cycle). Needs its own verification cycle (render each, judge real-vs-phantom). Surfaced explicitly; NOT silently shipped around.
@@ -25,7 +25,7 @@ If QA surfaces an issue — any issue, however small, whether pre-existing, alre
25
25
  ## Project Context
26
26
 
27
27
  - **App repo (private):** `C:\Users\filin\Dropbox\Vibe\MetaScienceTools\PDFextractor` (GitHub: giladfeldman/docpluckapp)
28
- - **Library repo (public):** `C:\Users\filin\Dropbox\Vibe\docpluck` (GitHub: giladfeldman/docpluck, PyPI: docpluck)
28
+ - **Library repo (public):** `C:\Users\filin\Dropbox\Vibe\MetaScienceTools\docpluck` (GitHub: giladfeldman/docpluck, PyPI: docpluck)
29
29
  - **Frontend:** Next.js 16 + Auth.js + Drizzle (in `frontend/`), port 6116
30
30
  - **Service:** Python FastAPI importing `docpluck` library (in `service/`), port 6117
31
31
  - **Database:** Neon Postgres (docpluck project)
@@ -607,6 +607,18 @@ print('F0 sentinel (preserved): PASS')
607
607
  "
608
608
  ```
609
609
 
610
+ ### 11b. Cross-Repo Library ↔ App Version Sync (CRITICAL — "when we bump the package, we bump the app")
611
+
612
+ Asserts the app's docpluck git pin equals the library's latest released tag, so production never silently runs an old library. Reads the pin from docpluckapp **origin/master** (production-authoritative — a stale local clone shows an old pin even when prod is synced), so it fetches first.
613
+
614
+ ```bash
615
+ cd C:\Users\filin\Dropbox\Vibe\MetaScienceTools\docpluck && python scripts/check_app_pin_sync.py
616
+ ```
617
+
618
+ **Gate:** exit 0 (`PASS: in sync ...`). Exit 1 = the app pin lags the latest library tag — the `bump-app-pin.yml` auto-bump missed; recover NOW (re-push the tag to re-fire the workflow, or hand-bump `PDFextractor/service/requirements.txt` to `@v<latest>` and push to docpluckapp `master`), then re-run until PASS — never report the run clean while it lags. Exit 2 = could not reach docpluckapp `origin/master` (treat as FAIL, not PASS; offline dev only may pass `--allow-local-fallback`, which reads the local clone and prints a stale-clone warning). A working-tree `__version__` ahead of the latest tag is reported as UNRELEASED (not a failure) — tag + push it so the app auto-bumps.
619
+
620
+ See CLAUDE.md "Two-Repo Architecture → Library ↔ app version sync".
621
+
610
622
  ### 12. Production Deployment (Vercel + Railway)
611
623
  ```bash
612
624
  # Vercel frontend
@@ -658,10 +670,11 @@ Opt-in cross-format benchmark suite --- DOCX corpus integrity, DOCX↔PDF parity
658
670
  | 9 | Database connectivity | PASS/FAIL | 7/7 tables |
659
671
  | 10 | Admin API | PASS/FAIL | health + stats |
660
672
  | 11 | Hard rules (4+2 checks) | PASS/FAIL | no -layout, no AGPL, U+2212, version, new modules, F0 sentinel |
673
+ | 11b | Cross-repo lib↔app version sync | PASS/FAIL | app pin (origin/master) == latest library tag |
661
674
  | 12 | Production health | PASS/FAIL | HTTP codes |
662
675
  | 13 | ESCIcheck 10-PDF (production) | PASS/FAIL/SKIP | X/10 passed |
663
676
 
664
- **Overall: X/15 checks passed**
677
+ **Overall: X/16 checks passed**
665
678
 
666
679
  ### Issues Found
667
680
  - [list any failures with exact error messages and file:line]
@@ -3,7 +3,7 @@
3
3
  _Extracted from [../SKILL.md](../SKILL.md). Full procedure lives here._
4
4
 
5
5
  ```bash
6
- cd C:\Users\filin\Dropbox\Vibe\PDFextractor\service && python -c "
6
+ cd C:\Users\filin\Dropbox\Vibe\MetaScienceTools\PDFextractor\service && python -c "
7
7
  import re
8
8
 
9
9
  # Rule 1: No -layout flag in pdftotext calls (check library)
@@ -30,15 +30,22 @@ print('Rule 2 (no AGPL): PASS')
30
30
  import docpluck.normalize as _nm_mod
31
31
  with open(_nm_mod.__file__, 'rb') as _f:
32
32
  _norm_bytes = _f.read()
33
- assert b'\\u2212' in _norm_bytes or b'\xe2\x88\x92' in _norm_bytes, 'U+2212 normalization missing'
33
+ assert b'\xe2\x88\x92' in _norm_bytes, 'U+2212 normalization missing' # U+2212 as UTF-8 bytes
34
34
  print('Rule 3 (U+2212 norm): PASS')
35
35
 
36
- # Rule 4: Library version is consistent
37
- import docpluck
38
- assert docpluck.__version__ == '1.4.5', f'Version mismatch: {docpluck.__version__}'
39
- print(f'Rule 4 (version=1.4.5): PASS')
36
+ # Rule 4: Library version is internally consistent (__init__.py == pyproject.toml).
37
+ # (Do NOT freeze a literal version here — it rots. This checks the two in-repo
38
+ # sources agree; the cross-repo app-pin sync is qa check 11b, see note below.)
39
+ import docpluck, re, pathlib
40
+ init_ver = docpluck.__version__
41
+ pyproject = (pathlib.Path(docpluck.__file__).resolve().parent.parent / 'pyproject.toml').read_text(encoding='utf-8')
42
+ proj_ver = re.search(r'(?m)^version\s*=\s*.(\d+\.\d+\.\d+)', pyproject).group(1)
43
+ assert init_ver == proj_ver, f'Version mismatch: __init__={init_ver} pyproject={proj_ver}'
44
+ print(f'Rule 4 (version consistency, __init__==pyproject=={init_ver}): PASS')
40
45
  "
41
46
  ```
42
47
 
48
+ > **Cross-repo pin sync** (the app's `@v<VERSION>` pin == the library's latest released tag) is a *separate* gate — qa check **11b** and the review hard rule, both via `python scripts/check_app_pin_sync.py` (reads docpluckapp `origin/master`). Rule 4 above only checks the library's *internal* version consistency (`__init__.py` == `pyproject.toml`).
49
+
43
50
  ---
44
51
 
@@ -213,6 +213,12 @@ The product principle is "one nightly server-resource-bounded cron per concern."
213
213
  - **Check:** parse `frontend/vercel.json`; assert exactly two cron entries, namely `/api/admin/blob-cleanup` at `0 3 * * *` AND `/api/cron/daily-digest` at `0 9 * * *`.
214
214
  - **Severity:** BLOCKER on any third cron — REQUEST CHANGES with the question "why isn't daily-digest enough?"
215
215
 
216
+ ### 22. Library ↔ app version pin sync (cross-repo, 2026-06-20 — "when we bump the package, we bump the app")
217
+ The app imports the library via a git pin in `PDFextractor/service/requirements.txt` (`docpluck[all] @ git+...@v<VERSION>`). This pin — on docpluckapp **origin/master**, the production-authoritative source Railway deploys — MUST always equal the library's latest released `v*` tag. A lagging pin silently runs the OLD library in production. The `bump-app-pin.yml` workflow auto-bumps it on tag push, but it is best-effort and has missed silently, so review VERIFIES rather than assumes.
218
+ - **Check:** run `python scripts/check_app_pin_sync.py` (from the docpluck repo). Exit 0 = synced. It fetches docpluckapp `origin/master` and compares the pin to the latest library tag — a stale local clone is NOT trusted (it shows an old pin even when prod is correctly synced).
219
+ - **Check:** whenever the diff bumps `docpluck/__init__.py:__version__` / `pyproject.toml:version`, confirm a matching `v<VERSION>` tag will be / has been pushed so the auto-bump can fire — an untagged version bump leaves the app pinned behind. The script reports this as UNRELEASED.
220
+ - **Severity:** BLOCKER if the app pin lags the latest released library tag (production runs the old library). WARN if a working-tree version bump is not yet tagged (release step pending).
221
+
216
222
  ## Review Checklist
217
223
 
218
224
  ### Python Service (`service/`)
@@ -266,6 +272,7 @@ The product principle is "one nightly server-resource-bounded cron per concern."
266
272
  - [ ] Changes reflected in CLAUDE.md if architectural
267
273
  - [ ] LESSONS.md updated if a new pitfall was discovered
268
274
  - [ ] TODO.md updated if features were added/completed
275
+ - [ ] App pin in sync with library (hard rule 22): `python scripts/check_app_pin_sync.py` exits 0 (app pin on docpluckapp `origin/master` == latest library tag). BLOCKER if it lags.
269
276
 
270
277
  ## Output Format
271
278
 
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.4.97] — 2026-06-22
4
+
5
+ **Three table fixes shipped together (combined from two concurrent sessions): type the skipped p+df columns (DP-2), stop dropping / mis-binding two-header-row tables (DP-5), and stop the table raw_text fallback swallowing body prose (RC-T Layer-2).** `TABLE_EXTRACTION_VERSION` → `2.4.2`; no `NORMALIZATION_VERSION` / `SECTIONING_VERSION` change. DP-2/DP-5 are render-visible in the inline flattened-table blocks + the `.tables.jsonl` sidecar `fields` (the `<table>` HTML gains the previously-dropped data rows); RC-T Layer-2 is render-visible in the `unstructured-table` fallback blocks. DP-2/DP-5 filed in `ESCIcheckapp/docs/DOCPLUCK_HANDOFF_2026-06-21.md`; RC-T Layer-2 per `docs/superpowers/specs/2026-06-21-rc-t-table-region-prose-contamination.md`.
6
+
7
+ - **DP-2 — type the unlabeled p and df columns.** `tables.flatten._recover_blank_roles` recovered the leading test statistic and the `d [CI]` column of a header-stripped result table but left the bare p-value and df columns between them untyped, so `collabra.77859` Table 3 emitted `fields: {group, t, d, CI}` and dropped the `p` (`.551`) and `df` (`260.54`). A new Pass 4.5 types a still-blank column that is a bare `.XXX` with no comparison op as `p`, and a bare integer / Welch-decimal sitting between the test statistic and its `est/CI` column as `df` — keyed on data shape + position relative to the already-recovered roles, never bare position. The four Table-3 rows now carry `p` and `df`.
8
+
9
+ - **DP-5 — two-row-header parallel-arm tables: recover the first data row and align centered super-headers.** `collabra.90203` Table 10 delivered only 5 of its 6 correlation rows (the Identifiable/Explicit-learning row was silently dropped), and the Original/Replication arms of `xiao_2021` Table 4 were swapped. Three coupled root-cause fixes: (a) `cell_cleaning._is_header_like_row` now counts APA value shapes (leading-dot decimal, bracketed CI, operator-prefixed p, `N/A`) as data via `_DATA_VALUE_CELL_RE`, so a real first data row is no longer mis-read as a third header row (the bracket branch requires a digit and no letters inside, so a genuine `[95% CI]` header stays a header); (b) `tables.flatten._detect_column_groups` re-derives arm boundaries from equal-width blocks of the data region — each must contain exactly one super-label — so a *centered* super-label (camelot stream loses colspan and folds it mid-span) no longer swaps arm values or pushes a stat column into the label region; left-aligned super-headers stay byte-identical; (c) `tables.flatten._classify_column` reads a folded super-header cell's role from its sub-part so a folded `…<sep>95% CI` column is still typed `CI`. Table 10 now emits all 6 conditions split into Target-article / Replication arms with correct `r` / `n` / `CI` / `p`; xiao Table 4 arms are no longer swapped; incidentally recovers `chan_feldman` Table 8 arm labels and `jama_open_2` Table 3 HR estimates + CIs.
10
+
11
+ - **RC-T Layer-2 — stop the table raw_text fallback swallowing body prose.** When Camelot recovers no cells, `extract_structured._extract_table_body_text` linearises the text after a caption as the `unstructured-table` fallback; its per-line prose gate (`_line_is_body_prose`, len ≥ 80) misses prose that pdftotext WRAPPED into short (~48-char) lines, so a short table's caption-anchored region overshot the table end and swallowed Results/Discussion prose. Two FP-safe structural fixes: **(a) Note-anchor** — a table's `Note:` footnote is, by convention, its last element, so trim everything after the note paragraph (`chan_feldman` T1/T3 + `efendic_2022` T5 trailing Discussion prose removed; the stat rows + the note are kept); **(b) degenerate-prose guard** — suppress a fallback block that STARTS mid-sentence with a lowercase multi-letter word AND is majority sentence-shaped prose, so the renderer emits a clean caption-only table (`chan_feldman` T9 was an entire verbatim duplicate of `## Discussion` — now caption-only, no duplication). FP-safe by construction: real table cells start with a header / label / number / single-letter item marker, never a wrapped mid-sentence continuation — hypotheses ("a There is a positive association…"), descriptive rows ("Median age"), and instrument fragments are preserved. Keyed on the structural overshoot signature, never paper identity.
12
+
13
+ Verification: new real-PDF + contract regression tests (`tests/test_tables_superheader_alignment_real_pdf.py`) — collabra.90203 T10 six-conditions/correct-arms + xiao T4 not-swapped (each FAILS at HEAD, PASSES after), plus `_is_header_like_row` / `_detect_column_groups` contract cases; `tests/test_tables_flatten_blank_header_recovery.py` extended for DP-2. A full-corpus (101-PDF) cached-table flatten diff confirms no clean-table regression — every changed table is a recovered row, a correct arm split, a recovered field, or a removed stat-less spurious row; already-garbage tables shuffle without a clean table regressing. Broad pytest green (real-PDF Camelot tests run serially per file — non-deterministic under cumulative load). RC-T Layer-2 adds `tests/test_rc_t_layer2_raw_text_real_pdf.py` (6 contract + 4 real-PDF: chan T1 Note-anchor, T9 suppress-no-duplication, T3 preserved) and an independent full-corpus 101-PDF guard-live-vs-bypassed raw_text diff (`grew=0 changed=0`; 4 trims + 8 prose-suppressions only). A 7-canary Sonnet AI-gold verify confirms every table this release touched is correct (chan T1/T3/T9, maier T10 six-conditions, xiao T4 arms) with no new TEXT-LOSS / HALLUCINATION.
14
+
15
+ **Deferred (pre-existing, user decision 2026-06-22):** the remaining canary AI-verify FAILs are the architectural backlog, NOT regressions from this release — RC-T **Layer-1** table-data recovery (`table_areas`; e.g. plos_med Table 5's SAE rows, chan_feldman / chandrashekar under-extraction) and RC-1 two-column / sidebar column-interleave. Tracked in `docs/TRIAGE_2026-06-21_head_v2.4.95_assessment.md`; intentionally not addressed here.
16
+
17
+ ## [2.4.96] — 2026-06-21
18
+
19
+ **RC-T (Option A): strip Camelot "tables" that are absorbed body prose, not data.** Render-only — `render.py::_strip_phantom_camelot_tables`; no `TABLE_EXTRACTION_VERSION` / `NORMALIZATION_VERSION` / `SECTIONING_VERSION` change.
20
+
21
+ Camelot runs free-form (`flavor="stream"`, no `table_areas`), so on a text-heavy page it returns a whole-page bbox and folds body prose into the `<thead>`. `_strip_phantom_camelot_tables` already drops such a table when a `<th>` is sentence-shaped prose (≥8 words, ≥3 function words, ≥2 verb-shape words) — but its function-word set was **missing `"the"`**, the single most common English function word, so a body-prose `<th>` with `fn=2 + "the"` slipped under the `fn≥3` bar and a garbage prose `<table>` survived. Two corpus cases: `10.1525/collabra.90203` (maier) Table 7 (`<th>` "Following the analyses conducted in Study 1 of Small") and `10.1080/02699931.2024.2434156` (chan_feldman) Table 6 ("associations between the six measures of interest: …").
22
+
23
+ The fix counts `"the"`, but is **scoped to the marginal case** (a `<th>` that crosses fn 2→3 only because of "the") so every table already stripped at HEAD via the `fn≥3` path stays byte-identical, and is **gated on NOT being a title-leak**: `aom/amp_1` Table 5 leaks its own caption ("Improving Scholarly Impact … Practice") into the `<th>` over a REAL grid (Domains / Policymaking / Practice + data) — a caption-token-overlap test (≥60%) keeps such title-leak tables. **Net corpus impact: exactly 2 tables stripped, all others byte-identical** (verified by a full-corpus th-level scan over all 152 corpus PDFs + render diffs). Fail-clean: the `### Table N` heading + caption remain (table_parity preserved); the stripped prose is a Camelot duplicate, so the clean original survives in the body (no TEXT-LOSS). ip_feldman Table 10 — the RC-T canonical "orphan" — was already fail-clean at HEAD and is unchanged.
24
+
25
+ Verification: 8 new real-PDF regression tests (`tests/test_rc_t_degenerate_table_real_pdf.py`): maier T7 + chan_feldman T6 stripped (each FAILS at HEAD, PASSES after), amp_1 T5 + chan_feldman T2/T5 (real tables) preserved, maier prose survives in body (no TEXT-LOSS), ip_feldman T10 prose stays out of `<table>`. Broad pytest green; 7-canary AI-verify shows the touched tables fail-clean with no new TEXT-LOSS / HALLUCINATION.
26
+
27
+ **Deferred (separate cycles, surfaced not dropped):** (1) RC-T **Layer-1 recovery** — actually RECOVERING lost table data via tight `table_areas` (plos_med Table 5's 13 SAE rows; chan_feldman Table 2 column-squish) — out of Option-A scope. (2) **Audit of the ~37 corpus tables** stripped by the existing `_strip_phantom_camelot_tables` th-prose / section-token paths — some title-shaped `<th>` strips may be wrongly-stripped REAL tables (pre-existing; predates this change), needs its own verify-each-render cycle. (3) RC-1 two-column / sidebar interleave.
28
+
3
29
  ## [2.4.95] — 2026-06-20
4
30
 
5
31
  **Flatten now populates `fields` for non-clinical result tables (REQUEST_11).** `TABLE_EXTRACTION_VERSION` → `2.4.0`; no `NORMALIZATION_VERSION` / `SECTIONING_VERSION` change. v2.4.94 solved the clinical PROSECCO table (labelled headers); this closes the two reproducers whose `fields` still came back `{}` — header-stripped result tables and tables packing parallel arms into single cells.
@@ -50,16 +50,25 @@ docpluck[all] @ git+https://github.com/giladfeldman/docpluck.git@v<VERSION>
50
50
 
51
51
  When this library releases a new version, the app's `requirements.txt` git pin must be bumped or production silently keeps running the old library. The `/docpluck-deploy` skill's pre-flight check 4 enforces this.
52
52
 
53
+ ### Library ↔ app version sync (HARD RULE — when we bump the package, we bump the app; verify, don't assume)
54
+
55
+ **Invariant: the app's docpluck pin and the library version are ALWAYS in sync.** The `@v<VERSION>` pin in `PDFextractor/service/requirements.txt` on **docpluckapp `origin/master`** (what Railway deploys) MUST equal the library's latest released `v*` tag. A lagging pin = production silently runs the old library. Bumping the package therefore *is* bumping the app — the two are never released independently.
56
+
57
+ - **Mechanism (best-effort, automated):** `.github/workflows/bump-app-pin.yml` fires on every `v*.*.*` tag push to this repo and auto-commits the pin bump to docpluckapp `master`, which triggers the Railway redeploy. This usually "just works" — but it is **best-effort**: an Actions outage, an expired `APP_REPO_TOKEN`, or a regex drift can let it miss *silently* (it has happened). **Never assume the bump landed.**
58
+ - **Verification (mandatory, deterministic):** run `python scripts/check_app_pin_sync.py` — it reads the pin from docpluckapp `origin/master` (production-authoritative, NOT your local clone) and compares it to the latest library tag. Exit 0 = synced. This gate is wired into `/docpluck-qa`, `/docpluck-review`, and `/docpluck-deploy` (pre-flight check 4); every release MUST pass it.
59
+ - **A stale LOCAL clone lies.** A local `PDFextractor` checkout that hasn't fetched shows an *old* pin even when production is correctly synced — and almost causes a phantom "fix". ALWAYS verify against `origin/master` (the script does this for you); never judge sync from a local working-tree file.
60
+ - **Recovery when it drifted:** re-push the tag (`git push origin v<VERSION>` re-fires the workflow), or hand-bump `service/requirements.txt` to `@v<VERSION>` and push to docpluckapp `master` (triggers Railway redeploy). Then re-run the gate and confirm Railway `/health` reports `docpluck_version == <VERSION>`.
61
+
53
62
  ## Release flow (library → production)
54
63
 
55
64
  1. Make + commit changes in this repo. Bump `__version__` (in `docpluck/__init__.py`), `version` (in `pyproject.toml`), and `NORMALIZATION_VERSION` (in `docpluck/normalize.py`) consistently.
56
65
  2. Update `CHANGELOG.md`.
57
66
  3. Push to `main`, then tag: `git tag v<VERSION> && git push --tags`.
58
67
  4. (Optional) Publish to PyPI: `python -m build && twine upload dist/*`.
59
- 5. In `PDFextractor/service/requirements.txt`, bump the `@v<VERSION>` git pin and update any frozen version examples in `PDFextractor/API.md`.
60
- 6. Run `/docpluck-deploy` from the docpluck repo — pre-flight check 4 verifies the pin matches.
68
+ 5. The tag push auto-fires `bump-app-pin.yml`, which bumps the `@v<VERSION>` pin in `PDFextractor/service/requirements.txt` on docpluckapp `master` and triggers the Railway redeploy. **This is best-effort — verify it landed:** run `python scripts/check_app_pin_sync.py` (exit 0 = synced against `origin/master`). If it drifted, recover per "Library ↔ app version sync" above. Also update any frozen version examples in `PDFextractor/API.md`.
69
+ 6. Run `/docpluck-deploy` from the docpluck repo — pre-flight check 4 runs the same sync gate and the post-deploy step confirms Railway `/health` reports the new version.
61
70
 
62
- Skipping step 5 is the most common failure mode. The deploy skill catches it.
71
+ The most common failure mode is assuming the auto-bump landed when it silently missed — step 5's `check_app_pin_sync.py` gate catches it. The deploy skill, qa, and review all run it.
63
72
 
64
73
  ## Spike work queue (table-rendering iteration)
65
74
 
@@ -79,6 +88,7 @@ Skipping step 5 is the most common failure mode. The deploy skill catches it.
79
88
 
80
89
  - **NEVER call the Anthropic API. ALL Claude model calls go through Claude Max via Claude Code.** Allowed: `Agent` tool in-session (with `model="sonnet"` for the audit subagent); headless `claude -p --model sonnet` from `.git/hooks/*` and `tools/canary_audit.sh`; `mcp__scheduled-tasks__create_scheduled_task` invoking Claude Code. Forbidden: `import anthropic`, `ANTHROPIC_API_KEY` anywhere in this repo or any related repo (`docpluckapp`, `escicheck`, `2Rmarkdown`, `CitationGuard`), `.github/workflows/*` containing Anthropic-API calls. The canary-audit architecture (Sonnet-watches-Opus) is designed around this constraint: external enforcement is local git hooks + scheduled tasks invoking headless Claude Code, NOT GitHub Actions calling the API. Source: user directive 2026-05-25 (memory `feedback_no_apis_only_claude_max`), re-affirming previous statements across multiple sessions. Failure to follow this rule is the same severity as failing "LEAVE NOTHING BEHIND."
81
90
  - **LEAVE NOTHING BEHIND.** If you see an issue — any issue, however small, whether pre-existing, already-known, "out of scope", or unrelated to the task at hand — you fix it in the same run. "Pre-existing", "known", "not introduced by this change", and "out of scope" are NEVER grounds to leave a defect in place; noticing a defect and walking past it is itself a defect. Two — and only two — exceptions: **(a)** the fix needs a product or architecture decision only the user can make — surface it explicitly and immediately, never bury it; **(b)** the fix is genuinely too entangled to land in the current change — then it is queued as an *immediate next cycle in the same run*, never as "later", never as a handoff-doc footnote. Never end a task, cycle, or run with a known issue unaddressed. Established by user directive 2026-05-14, re-affirmed 2026-05-15, 2026-05-17, and **2026-05-19** ("doesn't matter pre-existing or not; this directive holds for all future runs, every skill"). This generalizes and strengthens the rule-0e family (memory `feedback_fix_every_bug_found`). See the prominent top-of-file statement under "Working directive — LEAVE NOTHING BEHIND".
91
+ - **KEEP THE APP PIN IN SYNC WITH THE LIBRARY — when we bump the package, we bump the app.** The `@v<VERSION>` docpluck pin in `PDFextractor/service/requirements.txt` (on docpluckapp `origin/master`, the production-authoritative source) MUST always equal the library's latest released `v*` tag; a lagging pin silently runs the old library in production. `bump-app-pin.yml` auto-bumps it on tag push but is **best-effort and has missed silently** — never assume, always verify with `python scripts/check_app_pin_sync.py` (exit 0 = synced). A stale LOCAL clone shows an old pin even when prod is synced, so the gate reads `origin/master`, never a local file. Wired into `/docpluck-qa`, `/docpluck-review`, `/docpluck-deploy`. Full mechanism + recovery under "Two-Repo Architecture → Library ↔ app version sync". Established by user directive 2026-06-20.
82
92
  - **EVERY FIX MUST BE GENERAL — serve all future PDFs, never a one-PDF quick-hack.** docpluck is a meta-science tool that processes arbitrary academic PDFs across many publishers. Every change must be keyed on a STRUCTURAL SIGNATURE — a typographic pattern, layout invariant, glyph-corruption shape, section-structure rule — never on paper identity, filename, or a string hard-coded from one PDF. A change that resolves one paper's quirk but risks regressions on others is the WRONG fix; find the general root cause. Regression tests use specific PDF fixtures, but the fix *logic* must generalize to any PDF with the same structural signature. Always run the full 26-paper baseline to confirm no regression; widen verification (broad-read, more AI-golds) when a fix touches a shared code path. Established by user directive 2026-05-15. See memory `feedback_general_fixes_not_pdf_specific`.
83
93
  - **NEVER swap the PDF text-extraction tool as a fix for downstream problems.** The TEXT channel is `extract_pdf` (pdftotext default mode); the LAYOUT channel is `extract_pdf_layout` (pdfplumber). They are not interchangeable text sources. Sections / normalize / batch consume the text channel; tables / figures / F0-layout-strip consume the layout channel. Real-world-paper bugs (watermarks in body, abstract not detected, column interleaving) must be fixed in the layer that owns the artifact (`normalize.py` W0, `sections/annotators/text.py`, `sections/taxonomy.py`, `sections/core.py`) — not by switching extraction tools. See [LESSONS.md L-001](./LESSONS.md#l-001--never-swap-the-pdf-text-extraction-tool-as-a-fix-for-downstream-problems) for the full incident record.
84
94
  - **NEVER use pdftotext with `-layout` flag** — causes column interleaving. See `docpluck/extract.py:13–16` and [LESSONS.md L-002](./LESSONS.md#l-002--never-use-pdftotext--layout-flag).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docpluck
3
- Version: 2.4.95
3
+ Version: 2.4.97
4
4
  Summary: PDF, DOCX, and HTML text extraction and normalization for academic papers
5
5
  Project-URL: Homepage, https://docpluck.app
6
6
  Project-URL: Documentation, https://docpluck.app/api-docs
@@ -2,6 +2,24 @@
2
2
 
3
3
  This file tracks future-aim items that are scoped out of the current milestone but should not be lost. See `docs/superpowers/specs/` for active specs.
4
4
 
5
+ ## 2026-06-21 — v2.4.95 corpus assessment (7/7 canary FAIL) — RC-B7 done, RC-T spec'd + handed off, RC-1 next
6
+
7
+ > Full real AI-verify of 7 canaries at HEAD v2.4.95 = **7/7 FAIL** on 3 architectural root causes (the canary-audit hook's `AUDIT_DEFERRED→union PASS` had masked it again — `feedback_canary_audit_clobbers_phase5d`). Canonical queue: [`docs/TRIAGE_2026-06-21_head_v2.4.95_assessment.md`](docs/TRIAGE_2026-06-21_head_v2.4.95_assessment.md). Branch `feat/rc-t-table-region-guard` (commits `db7192b`, `927d869`).
8
+
9
+ - [x] **RC-B7 deleted-minus glyph — DONE (W0h).** Already implemented as `normalize.recover_dropped_minus_via_layout` (wired render.py:5079→normalize.py:3170, tested `tests/test_dropped_minus_layout_recovery_real_pdf.py`); HEAD recovers 4/5 ar_apa betas. Residuals (`.245` pixel-minus, β→b) are OCR-tier won't-fix (both pdftotext AND pdfplumber agree on the wrong glyph). The cycle-3 ar_apa "FAIL" was a verifier over-flag.
10
+ - [ ] **RC-T table-region prose contamination — SPEC'd, implementation handed off.** Widest defect (all 7 papers): a small table among prose gets a near-full-page region (ip_feldman T10 region 71→331 reaches into the Discussion prose) and the whitespace clusterer turns prose into "rows" → garbage cells, orphan `### Table N`, empty shells, duplicate dumps. Two layers: (1) Camelot free-form (no `table_areas`) → whole-page bbox; (2) `caption + SEARCH_BELOW_PT(250)` region with no table-END detection. Fix = region prose-trim + degenerate-region guard, keyed on **cell content not bbox-size**. Spec: [`docs/superpowers/specs/2026-06-21-rc-t-table-region-prose-contamination.md`](docs/superpowers/specs/2026-06-21-rc-t-table-region-prose-contamination.md). Handoff: see `docs/superpowers/handoffs/2026-06-21-*`.
11
+ - [ ] **RC-1 region-aware columns — after RC-T.** Two-column + PLOS-sidebar interleave (chan_feldman, chandrashekar, plos_med front-matter-before-Abstract). Step-2 band path exists ship-dark; see the existing RC-1 entry below + `docs/superpowers/specs/2026-06-08-rc1-region-aware-column-architecture.md`.
12
+ - [ ] **article-finder: correct the `collabra.77859` gold table-numbering** (gold says "Table 2"; the source PDF caption + docpluck + the consumer all say "Table 3" — gold error, see below).
13
+
14
+ ## 2026-06-20 — v2.4.95 SHIPPED (REQUEST_11: flatten fields for non-clinical result tables) — deferred follow-ups
15
+
16
+ > ✅ **Shipped + verified in prod.** main `370b89c` + tag `v2.4.95` (canary PASS), PyPI published, app pin auto-bumped to `@v2.4.95`, Railway `/health` reports `docpluck_version: 2.4.95`. Closes ESCImate REQUEST_11 (blank-header column-role recovery + packed parallel-arm split + general U+2212/bracket-CI fixes). All 4 acceptance criteria met, both target papers AI-gold-verified. Details: `REPLY_FROM_DOCPLUCK_v2.4.95.md`, `docs/HANDOFF_2026-06-20_request11_flatten_nonclinical_tables.md`, `CHANGELOG.md`.
17
+
18
+ - [ ] **`fields.effect_type` — opt-in, only if ESCImate requests it.** REQUEST_11 §2.4 asked for `effect_type` (cohens_d / pearson_r / partial_eta_squared / mean_difference) but called it "not a blocker." Deferred because emitting it would add a key to PROSECCO's 6 rows, conflicting with acceptance #4 (PROSECCO byte-identical). Offered as an opt-in in the reply; implement (grounded in key-present + effect vocab) only if the consumer accepts the PROSECCO field-set change.
19
+ - [x] **(ADJUDICATED 2026-06-21 — docpluck CORRECT, gold mis-numbered) `collabra.77859` caption-number "Table 3" vs gold "Table 2".** Resolved via the source text channel: the PDF's own caption at text-line 866 reads verbatim `Table 3. Study 4: Dish sets` (tables run 1→5 sequentially: L282 T1, L680 T2, **L866 T3**, L869 T4, L1034 T5). docpluck binds "Table 3" exactly matching the source; the consumer also calls it Table 3. **The AI gold mis-numbered it "Table 2"** — gold error, not a docpluck defect. → surface to article-finder for gold correction (logged above).
20
+ - [x] **(no action) `collabra.90203` Table 10 "Joint/No-explicit" r=.59 vs gold .63** — the PDF *text layer* encodes `.59` (pdftotext AND Camelot agree); only the AI-visual gold sees `.63`. Source text-layer corruption, undetectable without OCR (not allowed). Documented for the consumer; nothing docpluck can fix.
21
+ - [x] **(handled) Value-exact real-PDF flatten tests flake under `-n10`** — Camelot extraction is non-deterministic under parallel load; the 4 `*_real_pdf` tests now skip under `PYTEST_XDIST_WORKER` (run serially in canonical QA), matching the `test_benchmark_docx_html.py` convention. Synthetic-grid contract tests cover the logic under `-n10`.
22
+
5
23
  ## 2026-06-16 — deferred for investigation before code changes
6
24
 
7
25
  - [ ] **Investigate `sections=` extraction de-dup (no behavior change yet).** `extract_pdf(..., sections=...)`, `extract_docx(..., sections=...)`, and `extract_html(..., sections=...)` currently do one extraction pass and then call `extract_sections(...)`, which can re-run extraction/annotation internally by design. Before optimizing, document invariants proving parity with direct `extract_sections(...)` outputs, then run corpus/harness verification to confirm zero regressions. No implementation change until those proofs are in place.
@@ -78,7 +78,7 @@ from .figures import Figure
78
78
  from .extract_structured import TABLE_EXTRACTION_VERSION, StructuredResult, extract_pdf_structured
79
79
  from .render import render_pdf_to_markdown
80
80
 
81
- __version__ = "2.4.95"
81
+ __version__ = "2.4.97"
82
82
  __author__ = "Gilad Feldman"
83
83
  __license__ = "MIT"
84
84
 
@@ -37,7 +37,7 @@ from .tables.render import cells_to_html
37
37
  from .telemetry import record_fallback
38
38
 
39
39
 
40
- TABLE_EXTRACTION_VERSION = "2.4.0" # v2.4.0 (REQUEST_11): flatten now populates fields for NON-clinical result tables — (a) blank-header column-role recovery (tables.flatten._recover_blank_roles): assign a stat role to a header-stripped column from its data-token SHAPE (CI brackets, df1/df2 pair, estimate-adjacent-CI, p-with-operator) AND caption/footnote/all-header-rows vocabulary, never bare position; recovers collabra.77859 T5 (t/df/d/CI) + collabra.90203 T8/T9 (F/df/p/BF01/eta²p-as-est/CI). (b) packed parallel-arm split (tables.flatten._detect_packed_arms/_flatten_packed_arms): tables packing k≥2 arms into single cells ("Separate Joint" + space-joined values) emit one typed record per arm (group=arm) — collabra.77859 T3 Separate/Joint, xiao_2021 T7 Regret/Justifiability. (c) new BF01 role; validity guards drop r∉[-1,1] / non-monotone CI / non-int n / p∉[0,1]. (d) GENERAL L-004 fixes: _parse_number + _parse_ci_cell fold U+2212 MINUS (negative t/d/CI bounds in Camelot cells were dropped/sign-lost); _VALUE_GROUP_RE handles bracket-led CI groups. Default render + PROSECCO output byte-identical. # v2.3.0 (Tier-2, REQUEST_10): cross-flavor lattice-augmentation — recover data rows a lattice extraction vertically TRUNCATED by appending the rows a same-page, same-column-count stream table captured below the lattice bbox (camelot_extract._augment_lattice_with_stream_rows), gated on equal-col-count + bbox overlap + extends-below; PLUS numeric/parenthetical continuation merge (cell_cleaning._merge_continuation_rows) rejoining stream's stacked value/parenthetical cells. Fixes PROSECCO Table 2 R2-R6. v2.2.0: EC-T1 docpluck.tables.flatten — per-row FlattenedRow records (sentence + structured fields) for downstream stat-verification consumers (effectcheck/escimate/scimeto) + opt-in inline "rendered as text" block below each <table> via render_pdf_to_markdown(flatten_tables_inline=True). v2.1.5: cell-cleaning recovers CMEX10 extensible-bracket PUA glyphs (U+F8EE-F8FB). v2.1.4: cell-cleaning recovers Adobe-Symbol-font PUA glyphs (beta/chi/bullet as U+F0xx). v2.1.3: cell-cleaning recovers '<'-as-backslash glyph corruption. v2.1.2: cell-cleaning recovers descending-CI '2'-for-minus corruption. v2.1.1: cell-cleaning recovers (cid:0) corrupted minus signs + strips math-alphanumeric styling. v2.1.0: cell-cleaning pipeline ported from splice spike (multi-row header detection, continuation merging, leader-dot strip, mash-split, group separators, sig-marker attach)
40
+ TABLE_EXTRACTION_VERSION = "2.4.2" # v2.4.2 (RC-T Layer-2): _extract_table_body_text now (a) Note-anchor — a table's "Note:" footnote is its last element, so trim body prose bled past it (chan_feldman T1/T3, efendic_2022 T5); and (b) degenerate-prose guard — suppress a raw_text fallback that STARTS mid-sentence with a lowercase multi-letter word AND is majority sentence-shaped prose, so render emits a clean caption-only table instead of an unstructured-table dump duplicating Results/Discussion prose (chan_feldman T9 was a verbatim ## Discussion duplicate). FP-safe (real cells start with header/label/number/single-letter marker, never a wrapped continuation); full-corpus 101-PDF guard-diff only trims+suppresses (grew=0 changed=0). # v2.4.1 (DP-2/DP-5): (DP-2) blank-header role recovery now types the unlabeled p-value (a bare `.XXX` after the test stat, no comparison op) and df (a bare integer/Welch-decimal between the stat and the d[CI] column) columns it previously skipped — collabra.77859 T3 fields gain p+df (tables.flatten._recover_blank_roles Pass 4.5). (DP-5) parallel-arm tables with a TWO-ROW header no longer drop their first data row, and a CENTERED super-header is aligned to its arm block instead of its visual-center column: (a) cell_cleaning._is_header_like_row counts APA value shapes (leading-dot decimal, bracketed CI, operator-prefixed p, N/A) as data via _DATA_VALUE_CELL_RE so a real first data row isn't read as a 3rd header row (collabra.90203 T10 recovered the Identifiable/Explicit-learning correlation); (b) tables.flatten._detect_column_groups re-derives arm boundaries from equal-width blocks of the data region (each must hold one super-label) so a centered super-label folded mid-span no longer swaps arm values (xiao_2021 T4 Original/Replication F) or pushes a stat column into the label region; (c) tables.flatten._classify_column reads a folded super-header cell's role from its sub-part (collabra.90203 T10 CI). Full-corpus cached-table flatten diff: no clean-table regression. # v2.4.0 (REQUEST_11): flatten now populates fields for NON-clinical result tables — (a) blank-header column-role recovery (tables.flatten._recover_blank_roles): assign a stat role to a header-stripped column from its data-token SHAPE (CI brackets, df1/df2 pair, estimate-adjacent-CI, p-with-operator) AND caption/footnote/all-header-rows vocabulary, never bare position; recovers collabra.77859 T5 (t/df/d/CI) + collabra.90203 T8/T9 (F/df/p/BF01/eta²p-as-est/CI). (b) packed parallel-arm split (tables.flatten._detect_packed_arms/_flatten_packed_arms): tables packing k≥2 arms into single cells ("Separate Joint" + space-joined values) emit one typed record per arm (group=arm) — collabra.77859 T3 Separate/Joint, xiao_2021 T7 Regret/Justifiability. (c) new BF01 role; validity guards drop r∉[-1,1] / non-monotone CI / non-int n / p∉[0,1]. (d) GENERAL L-004 fixes: _parse_number + _parse_ci_cell fold U+2212 MINUS (negative t/d/CI bounds in Camelot cells were dropped/sign-lost); _VALUE_GROUP_RE handles bracket-led CI groups. Default render + PROSECCO output byte-identical. # v2.3.0 (Tier-2, REQUEST_10): cross-flavor lattice-augmentation — recover data rows a lattice extraction vertically TRUNCATED by appending the rows a same-page, same-column-count stream table captured below the lattice bbox (camelot_extract._augment_lattice_with_stream_rows), gated on equal-col-count + bbox overlap + extends-below; PLUS numeric/parenthetical continuation merge (cell_cleaning._merge_continuation_rows) rejoining stream's stacked value/parenthetical cells. Fixes PROSECCO Table 2 R2-R6. v2.2.0: EC-T1 docpluck.tables.flatten — per-row FlattenedRow records (sentence + structured fields) for downstream stat-verification consumers (effectcheck/escimate/scimeto) + opt-in inline "rendered as text" block below each <table> via render_pdf_to_markdown(flatten_tables_inline=True). v2.1.5: cell-cleaning recovers CMEX10 extensible-bracket PUA glyphs (U+F8EE-F8FB). v2.1.4: cell-cleaning recovers Adobe-Symbol-font PUA glyphs (beta/chi/bullet as U+F0xx). v2.1.3: cell-cleaning recovers '<'-as-backslash glyph corruption. v2.1.2: cell-cleaning recovers descending-CI '2'-for-minus corruption. v2.1.1: cell-cleaning recovers (cid:0) corrupted minus signs + strips math-alphanumeric styling. v2.1.0: cell-cleaning pipeline ported from splice spike (multi-row header detection, continuation merging, leader-dot strip, mash-split, group separators, sig-marker attach)
41
41
 
42
42
  TableTextMode = Literal["raw", "placeholder"]
43
43
 
@@ -1306,6 +1306,74 @@ def _line_is_body_prose(line: str) -> bool:
1306
1306
  return stopwords_hit >= 4
1307
1307
 
1308
1308
 
1309
+ def _join_wrapped_lines(lines: list[str]) -> list[str]:
1310
+ """Merge pdftotext-wrapped lines into logical paragraphs.
1311
+
1312
+ pdftotext linearizes a flowing prose paragraph into several short
1313
+ (~45-60 char) lines; the per-line ``_line_is_body_prose`` gate
1314
+ (len >= 80) cannot see prose in that wrapped form. Joining a line with
1315
+ the next whenever it does not end on sentence-terminal punctuation
1316
+ reconstructs the paragraph so prose can be measured at paragraph scale.
1317
+ """
1318
+ paras: list[str] = []
1319
+ cur = ""
1320
+ for ln in lines:
1321
+ s = ln.strip()
1322
+ if not s:
1323
+ continue
1324
+ cur = (cur + " " + s).strip() if cur else s
1325
+ if s.endswith((".", "!", "?", ":")):
1326
+ paras.append(cur)
1327
+ cur = ""
1328
+ if cur:
1329
+ paras.append(cur)
1330
+ return paras
1331
+
1332
+
1333
+ def _raw_text_is_degenerate_prose(text: str) -> bool:
1334
+ """True if a table raw_text fallback is dominated by flowing body prose.
1335
+
1336
+ RC-T Layer-2 (v2.4.97). When Camelot recovers no cells AND the
1337
+ caption-anchored region has no extractable table text near the caption,
1338
+ the body_start walk lands INSIDE a prose paragraph and the fallback
1339
+ swallows Results/Discussion prose (which is then duplicated under its
1340
+ real section heading). Such a block must be suppressed (render then
1341
+ emits a clean caption-only table) rather than dumped verbatim.
1342
+
1343
+ FP-safe by construction — fires only when BOTH hold:
1344
+ (a) the block STARTS mid-sentence: its first line begins with a
1345
+ lowercase multi-letter continuation word. A real table's
1346
+ linearized cells start with a column header, label, number, or a
1347
+ single-letter item marker (``a``/``b``/``c``) — never a wrapped
1348
+ mid-paragraph continuation like "than empathy. We provided ...".
1349
+ (b) the joined block is majority (>= 60% of chars) sentence-shaped
1350
+ body prose.
1351
+
1352
+ Legitimate degraded tables are preserved: hypotheses ("a There is a
1353
+ positive association ..."), descriptive rows ("Median age (years)"),
1354
+ instrument items ("h et al., 1997)") all fail (a). Keyed purely on the
1355
+ structural overshoot signature, never on paper identity.
1356
+ """
1357
+ lines = [ln for ln in text.split("\n") if ln.strip()]
1358
+ if len(lines) < 4:
1359
+ return False
1360
+ first_tokens = lines[0].split()
1361
+ first_word = first_tokens[0] if first_tokens else ""
1362
+ starts_midsentence = (
1363
+ len(first_word) >= 2
1364
+ and first_word[0].islower()
1365
+ and first_word[0].isalpha()
1366
+ )
1367
+ if not starts_midsentence:
1368
+ return False
1369
+ paragraphs = _join_wrapped_lines(lines)
1370
+ total = sum(len(p) for p in paragraphs)
1371
+ if total == 0:
1372
+ return False
1373
+ prose = sum(len(p) for p in paragraphs if _line_is_body_prose(p))
1374
+ return prose >= 0.6 * total
1375
+
1376
+
1309
1377
  def _extract_table_body_text(
1310
1378
  raw_text: str,
1311
1379
  cap: CaptionMatch,
@@ -1379,6 +1447,31 @@ def _extract_table_body_text(
1379
1447
  break
1380
1448
  kept.append(ln)
1381
1449
 
1450
+ # Note-anchor table-end (RC-T Layer-2, v2.4.97). A table's "Note:" /
1451
+ # "Notes:" footnote is, by academic-table convention, its LAST element.
1452
+ # Any text after the note paragraph is body prose that bled past the
1453
+ # table boundary — the caption-anchored region overshot the table end
1454
+ # and the per-line `_line_is_body_prose` gate (len >= 80) misses prose
1455
+ # that pdftotext WRAPPED into short (~48-char) lines, so it accumulates
1456
+ # here. Trim everything after the note's (possibly wrapped) paragraph.
1457
+ # This is FP-safe: legitimate table cells (hypotheses a/b/c, instrument
1458
+ # items) appear BEFORE the note; nothing legitimate follows it. Keyed on
1459
+ # the structural "Note: ... <sentence end>" signature, never paper
1460
+ # identity. `^Notes?[.:]` requires punctuation so body prose that merely
1461
+ # starts with the word "Note that ..." does not false-trigger.
1462
+ note_idx = next(
1463
+ (i for i, ln in enumerate(kept)
1464
+ if re.match(r"^\s*Notes?[.:]", ln.strip())),
1465
+ None,
1466
+ )
1467
+ if note_idx is not None and not os.environ.get("DOCPLUCK_RCT_L2_BYPASS"):
1468
+ note_end = note_idx
1469
+ for k in range(note_idx, len(kept)):
1470
+ note_end = k
1471
+ if kept[k].strip().endswith((".", "!", "?")):
1472
+ break
1473
+ kept = kept[: note_end + 1]
1474
+
1382
1475
  # Trim trailing heading-like short lines that don't belong to this table
1383
1476
  # (the start of the next section). Two patterns are trimmed:
1384
1477
  # * Title-Case headings without a sentence terminator
@@ -1414,7 +1507,17 @@ def _extract_table_body_text(
1414
1507
  s = re.sub(r"[ \t]+", " ", ln).strip()
1415
1508
  if s:
1416
1509
  cleaned_lines.append(s)
1417
- return "\n".join(cleaned_lines).strip()
1510
+ result = "\n".join(cleaned_lines).strip()
1511
+ # Degenerate-prose guard (RC-T Layer-2, v2.4.97): drop a raw_text
1512
+ # fallback that is really body prose the region overshot into, so the
1513
+ # renderer emits a clean caption-only table instead of an
1514
+ # ``unstructured-table`` dump that duplicates Results/Discussion prose.
1515
+ # ``DOCPLUCK_RCT_L2_BYPASS`` reverts both Layer-2 additions (Note-anchor
1516
+ # + this guard) to HEAD behavior — used only by the FP-scan harness to
1517
+ # diff guard-live vs guard-bypassed over the full corpus.
1518
+ if not os.environ.get("DOCPLUCK_RCT_L2_BYPASS") and _raw_text_is_degenerate_prose(result):
1519
+ return ""
1520
+ return result
1418
1521
 
1419
1522
 
1420
1523
  def _figure_from_caption(
@@ -2696,6 +2696,20 @@ def _strip_phantom_camelot_tables(text: str) -> str:
2696
2696
  # Process each <table>...</table> block independently.
2697
2697
  pattern = re.compile(r"<table\b[^>]*>.*?</table>\s*", re.DOTALL | re.IGNORECASE)
2698
2698
 
2699
+ # RC-T (v2.4.96): token sets of the document's italic caption lines (``*…*``).
2700
+ # Used by the scoped "the" body-prose path below to EXCLUDE a <th> that is
2701
+ # the table's own TITLE leaked into the header (a title-leak on a REAL table,
2702
+ # e.g. amp_1 Table 5) rather than absorbed body prose — high caption-token
2703
+ # overlap == title-leak == keep. Built once per render; empty when a paper
2704
+ # has no captions (then the scoped path simply never excludes).
2705
+ _caption_tok_sets = [
2706
+ toks for toks in (
2707
+ set(re.findall(r"[a-z]{2,}", c.lower()))
2708
+ for c in re.findall(r"\*([^*\n]{12,}?)\*", text)
2709
+ )
2710
+ if len(toks) >= 4
2711
+ ]
2712
+
2699
2713
  def is_phantom(block: str) -> bool:
2700
2714
  # Extract <th> contents.
2701
2715
  th_cells = re.findall(r"<th[^>]*>(.*?)</th>", block, re.DOTALL | re.IGNORECASE)
@@ -2752,6 +2766,31 @@ def _strip_phantom_camelot_tables(text: str) -> str:
2752
2766
  if fn_count >= 3 and verb_count >= 2:
2753
2767
  th_section_leak = True
2754
2768
  break
2769
+ # RC-T (v2.4.96, 2026-06-21): "the" — the single most common
2770
+ # English function word — is absent from _FUNCTION_WORDS_IN_PROSE,
2771
+ # so a body-prose <th> with exactly fn_count==2 + "the" slipped
2772
+ # under the bar: maier_2023 Table 7 "Following the analyses
2773
+ # conducted in Study 1 of Small" (fn=in,of; verb=following,
2774
+ # conducted) and chan_feldman Table 6 "associations between the
2775
+ # six measures of interest: …". This NEW path counts "the" to push
2776
+ # such ths over — but is SCOPED to the marginal case (fn_count<3
2777
+ # without "the", >=3 with it) so every table already stripped at
2778
+ # HEAD via the fn>=3 path stays byte-identical, AND gated on NOT
2779
+ # being a title-leak: amp_1 Table 5 leaks its own caption
2780
+ # ("Improving Scholarly Impact … Practice") into the <th> over a
2781
+ # REAL grid — stripping it would destroy a real table. A title
2782
+ # leak shares most of its tokens with a caption; body prose
2783
+ # shares ~none.
2784
+ the_count = sum(1 for w in words if w == "the")
2785
+ if fn_count < 3 and (fn_count + the_count) >= 3 and verb_count >= 2:
2786
+ th_toks = set(re.findall(r"[a-z]{2,}", cleaned_th.lower()))
2787
+ is_title_leak = bool(th_toks) and any(
2788
+ len(th_toks & cap) / len(th_toks) >= 0.6
2789
+ for cap in _caption_tok_sets
2790
+ )
2791
+ if not is_title_leak:
2792
+ th_section_leak = True
2793
+ break
2755
2794
  word_set = set(words)
2756
2795
  if any(t.lower() in word_set for t in _PHANTOM_TABLE_BODY_LEAK_TOKENS):
2757
2796
  if verb_count >= 1:
@@ -393,13 +393,32 @@ _NUMERIC_CELL_RE = re.compile(
393
393
  r"^[-−–]?\d+(?:[.,]\d+)*(?:[%∗*]+)?(?:\s*\([^)]*\))?$"
394
394
  )
395
395
 
396
+ # A cell carrying a statistic VALUE (vs a header label). Broader than
397
+ # _NUMERIC_CELL_RE: also matches APA leading-dot decimals (".34"), operator-
398
+ # prefixed p-values ("< .001"), bracketed numeric intervals ("[0.53, 0.72]"),
399
+ # and the "N/A" filler — all DATA, not header text. The interval branch requires
400
+ # a digit and NO letters inside the brackets so a genuine header cell like
401
+ # "[95% CI]" (letters present) is NOT counted as data and stays a header. Used by
402
+ # `_is_header_like_row` so a real data row whose APA-formatted values the bare
403
+ # numeric pattern under-counted is not mistaken for an extra header row — the
404
+ # bug that silently dropped the FIRST data row of two-header-row correlation
405
+ # tables (collabra.90203 Table 10, DP-5).
406
+ _DATA_VALUE_CELL_RE = re.compile(
407
+ r"^(?:"
408
+ r"[<>=]?\s*[-−–]?\d*[.,]?\d+(?:[.,]\d+)*(?:[%∗*]+)?(?:\s*\([^)]*\))?"
409
+ r"|\[[^\]A-Za-z]*\d[^\]A-Za-z]*\]"
410
+ r"|n\s*/?\s*a"
411
+ r")$",
412
+ re.I,
413
+ )
414
+
396
415
 
397
416
  def _is_header_like_row(row: list[str]) -> bool:
398
417
  """Heuristic: a row that looks like part of a header rather than data."""
399
418
  nonempty = [c.strip() for c in row if (c or "").strip()]
400
419
  if not nonempty:
401
420
  return False
402
- numeric = sum(1 for c in nonempty if _NUMERIC_CELL_RE.match(c))
421
+ numeric = sum(1 for c in nonempty if _DATA_VALUE_CELL_RE.match(c))
403
422
  if numeric / len(nonempty) > 0.3:
404
423
  return False
405
424
  avg_len = sum(len(c) for c in nonempty) / len(nonempty)