psyclaw 0.27.22 → 0.28.0

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 (1048) hide show
  1. package/README.md +11 -5
  2. package/dist/apps/panel/index.html +2 -2
  3. package/dist/src/adapters/pi/extension.js +225 -92
  4. package/dist/src/adapters/pi/extension.js.map +1 -1
  5. package/dist/src/agents/import.js +44 -32
  6. package/dist/src/agents/import.js.map +1 -1
  7. package/dist/src/analysis/hooks.d.ts +1 -0
  8. package/dist/src/analysis/hooks.js +20 -1
  9. package/dist/src/analysis/hooks.js.map +1 -1
  10. package/dist/src/ars/bridge.d.ts +20 -0
  11. package/dist/src/ars/bridge.js +132 -0
  12. package/dist/src/ars/bridge.js.map +1 -0
  13. package/dist/src/ars/contracts.d.ts +55 -0
  14. package/dist/src/ars/contracts.js +2 -0
  15. package/dist/src/ars/contracts.js.map +1 -0
  16. package/dist/src/ars/panel-plan.d.ts +4 -0
  17. package/dist/src/ars/panel-plan.js +11 -0
  18. package/dist/src/ars/panel-plan.js.map +1 -0
  19. package/dist/src/ars/pi-panel-executor.d.ts +30 -0
  20. package/dist/src/ars/pi-panel-executor.js +140 -0
  21. package/dist/src/ars/pi-panel-executor.js.map +1 -0
  22. package/dist/src/ars/profile.d.ts +36 -0
  23. package/dist/src/ars/profile.js +119 -0
  24. package/dist/src/ars/profile.js.map +1 -0
  25. package/dist/src/ars/re-review.d.ts +22 -0
  26. package/dist/src/ars/re-review.js +172 -0
  27. package/dist/src/ars/re-review.js.map +1 -0
  28. package/dist/src/branding.d.ts +2 -4
  29. package/dist/src/branding.js +3 -5
  30. package/dist/src/branding.js.map +1 -1
  31. package/dist/src/bundled-tools.d.ts +3 -0
  32. package/dist/src/bundled-tools.js +21 -0
  33. package/dist/src/bundled-tools.js.map +1 -0
  34. package/dist/src/chat.js +6 -5
  35. package/dist/src/chat.js.map +1 -1
  36. package/dist/src/creation/contracts.d.ts +38 -0
  37. package/dist/src/creation/contracts.js +2 -0
  38. package/dist/src/creation/contracts.js.map +1 -0
  39. package/dist/src/creation/service.d.ts +7 -0
  40. package/dist/src/creation/service.js +196 -0
  41. package/dist/src/creation/service.js.map +1 -0
  42. package/dist/src/index.d.ts +10 -0
  43. package/dist/src/index.js +10 -0
  44. package/dist/src/index.js.map +1 -1
  45. package/dist/src/install/installer.js +32 -10
  46. package/dist/src/install/installer.js.map +1 -1
  47. package/dist/src/orchestration/personas.d.ts +15 -0
  48. package/dist/src/orchestration/personas.js +51 -0
  49. package/dist/src/orchestration/personas.js.map +1 -0
  50. package/dist/src/orchestration/pi-executor.d.ts +1 -0
  51. package/dist/src/orchestration/pi-executor.js +1 -1
  52. package/dist/src/orchestration/pi-executor.js.map +1 -1
  53. package/dist/src/panel/extension.js +29 -8
  54. package/dist/src/panel/extension.js.map +1 -1
  55. package/dist/src/panel/server.js +14 -5
  56. package/dist/src/panel/server.js.map +1 -1
  57. package/dist/src/project/paths.d.ts +3 -0
  58. package/dist/src/project/paths.js +7 -0
  59. package/dist/src/project/paths.js.map +1 -1
  60. package/dist/src/rules/user-rules.d.ts +8 -0
  61. package/dist/src/rules/user-rules.js +36 -0
  62. package/dist/src/rules/user-rules.js.map +1 -0
  63. package/dist/src/skills/contracts.d.ts +4 -4
  64. package/dist/src/skills/recommended.js +1 -1
  65. package/dist/src/skills/registry.js +49 -28
  66. package/dist/src/skills/registry.js.map +1 -1
  67. package/dist/src/style/cli-ui.js +0 -1
  68. package/dist/src/style/cli-ui.js.map +1 -1
  69. package/package.json +14 -3
  70. package/scripts/rebrand-pi.mjs +6 -0
  71. package/skills/recommended/catalog.json +2 -11
  72. package/vendor/ars/.claude/CLAUDE.md +371 -0
  73. package/vendor/ars/.command-invariants.toml +24 -0
  74. package/vendor/ars/CITATION.cff +35 -0
  75. package/vendor/ars/LICENSE +417 -0
  76. package/vendor/ars/MODE_REGISTRY.md +76 -0
  77. package/vendor/ars/NOTICE.md +26 -0
  78. package/vendor/ars/POSITIONING.md +99 -0
  79. package/vendor/ars/PSYCLAW_SOURCE.json +10 -0
  80. package/vendor/ars/README.md +751 -0
  81. package/vendor/ars/SECURITY.md +52 -0
  82. package/vendor/ars/THIRD_PARTY.md +70 -0
  83. package/vendor/ars/academic-paper/SKILL.md +542 -0
  84. package/vendor/ars/academic-paper/agents/abstract_bilingual_agent.md +171 -0
  85. package/vendor/ars/academic-paper/agents/argument_builder_agent.md +276 -0
  86. package/vendor/ars/academic-paper/agents/citation_compliance_agent.md +422 -0
  87. package/vendor/ars/academic-paper/agents/draft_writer_agent.md +656 -0
  88. package/vendor/ars/academic-paper/agents/formatter_agent.md +999 -0
  89. package/vendor/ars/academic-paper/agents/intake_agent.md +393 -0
  90. package/vendor/ars/academic-paper/agents/literature_strategist_agent.md +626 -0
  91. package/vendor/ars/academic-paper/agents/peer_reviewer_agent.md +516 -0
  92. package/vendor/ars/academic-paper/agents/revision_coach_agent.md +334 -0
  93. package/vendor/ars/academic-paper/agents/socratic_mentor_agent.md +527 -0
  94. package/vendor/ars/academic-paper/agents/structure_architect_agent.md +401 -0
  95. package/vendor/ars/academic-paper/agents/visualization_agent.md +441 -0
  96. package/vendor/ars/academic-paper/examples/chinese_paper_example.md +278 -0
  97. package/vendor/ars/academic-paper/examples/clinical_citation_verification_checklist.md +95 -0
  98. package/vendor/ars/academic-paper/examples/clinical_epistemic_status_example.md +100 -0
  99. package/vendor/ars/academic-paper/examples/commitment_ledger_example.md +147 -0
  100. package/vendor/ars/academic-paper/examples/imrad_hei_example.md +234 -0
  101. package/vendor/ars/academic-paper/examples/literature_review_example.md +260 -0
  102. package/vendor/ars/academic-paper/examples/plan_mode_guided_writing.md +600 -0
  103. package/vendor/ars/academic-paper/examples/revision_mode_example.md +344 -0
  104. package/vendor/ars/academic-paper/examples/revision_recovery_example.md +506 -0
  105. package/vendor/ars/academic-paper/examples/version_family_reconciliation_example.md +89 -0
  106. package/vendor/ars/academic-paper/references/abstract_writing_guide.md +169 -0
  107. package/vendor/ars/academic-paper/references/academic_writing_style.md +188 -0
  108. package/vendor/ars/academic-paper/references/anti_leakage_protocol.md +83 -0
  109. package/vendor/ars/academic-paper/references/apa7_chinese_citation_guide.md +364 -0
  110. package/vendor/ars/academic-paper/references/apa7_extended_guide.md +198 -0
  111. package/vendor/ars/academic-paper/references/changelog.md +11 -0
  112. package/vendor/ars/academic-paper/references/citation_format_switcher.md +228 -0
  113. package/vendor/ars/academic-paper/references/committee_correspondence_protocol.md +158 -0
  114. package/vendor/ars/academic-paper/references/credit_authorship_guide.md +308 -0
  115. package/vendor/ars/academic-paper/references/disclosure_mode_protocol.md +478 -0
  116. package/vendor/ars/academic-paper/references/domain_evidence_profiles.md +38 -0
  117. package/vendor/ars/academic-paper/references/failure_paths.md +349 -0
  118. package/vendor/ars/academic-paper/references/funding_statement_guide.md +319 -0
  119. package/vendor/ars/academic-paper/references/hei_domain_glossary.md +169 -0
  120. package/vendor/ars/academic-paper/references/intro_title_rhetoric_guide.md +114 -0
  121. package/vendor/ars/academic-paper/references/journal_submission_guide.md +249 -0
  122. package/vendor/ars/academic-paper/references/latex_template_reference.md +378 -0
  123. package/vendor/ars/academic-paper/references/mode_selection_guide.md +378 -0
  124. package/vendor/ars/academic-paper/references/paper_structure_patterns.md +330 -0
  125. package/vendor/ars/academic-paper/references/plan_mode_protocol.md +112 -0
  126. package/vendor/ars/academic-paper/references/policy_anchor_disclosure_protocol.md +200 -0
  127. package/vendor/ars/academic-paper/references/policy_anchor_table.md +157 -0
  128. package/vendor/ars/academic-paper/references/revision_patch_protocol.md +173 -0
  129. package/vendor/ars/academic-paper/references/statistical_visualization_standards.md +750 -0
  130. package/vendor/ars/academic-paper/references/venue_disclosure_policies.md +259 -0
  131. package/vendor/ars/academic-paper/references/vlm_figure_verification.md +126 -0
  132. package/vendor/ars/academic-paper/references/workflow_phase_details.md +135 -0
  133. package/vendor/ars/academic-paper/references/writing_judgment_framework.md +59 -0
  134. package/vendor/ars/academic-paper/references/writing_quality_check.md +173 -0
  135. package/vendor/ars/academic-paper/templates/bilingual_abstract_template.md +78 -0
  136. package/vendor/ars/academic-paper/templates/case_study_template.md +129 -0
  137. package/vendor/ars/academic-paper/templates/conference_paper_template.md +108 -0
  138. package/vendor/ars/academic-paper/templates/credit_statement_template.md +132 -0
  139. package/vendor/ars/academic-paper/templates/funding_statement_template.md +290 -0
  140. package/vendor/ars/academic-paper/templates/imrad_template.md +183 -0
  141. package/vendor/ars/academic-paper/templates/latex_article_template.tex +199 -0
  142. package/vendor/ars/academic-paper/templates/literature_review_template.md +135 -0
  143. package/vendor/ars/academic-paper/templates/policy_brief_template.md +139 -0
  144. package/vendor/ars/academic-paper/templates/revision_tracking_template.md +199 -0
  145. package/vendor/ars/academic-paper/templates/theoretical_paper_template.md +119 -0
  146. package/vendor/ars/academic-paper-reviewer/SKILL.md +491 -0
  147. package/vendor/ars/academic-paper-reviewer/agents/devils_advocate_reviewer_agent.md +443 -0
  148. package/vendor/ars/academic-paper-reviewer/agents/domain_reviewer_agent.md +412 -0
  149. package/vendor/ars/academic-paper-reviewer/agents/editorial_synthesizer_agent.md +478 -0
  150. package/vendor/ars/academic-paper-reviewer/agents/eic_agent.md +339 -0
  151. package/vendor/ars/academic-paper-reviewer/agents/field_analyst_agent.md +221 -0
  152. package/vendor/ars/academic-paper-reviewer/agents/methodology_reviewer_agent.md +449 -0
  153. package/vendor/ars/academic-paper-reviewer/agents/perspective_reviewer_agent.md +427 -0
  154. package/vendor/ars/academic-paper-reviewer/examples/hei_paper_review_example.md +391 -0
  155. package/vendor/ars/academic-paper-reviewer/examples/interdisciplinary_review_example.md +299 -0
  156. package/vendor/ars/academic-paper-reviewer/examples/subclaim_decomposition_example.md +80 -0
  157. package/vendor/ars/academic-paper-reviewer/references/calibration_mode_protocol.md +256 -0
  158. package/vendor/ars/academic-paper-reviewer/references/changelog.md +10 -0
  159. package/vendor/ars/academic-paper-reviewer/references/editorial_decision_standards.md +236 -0
  160. package/vendor/ars/academic-paper-reviewer/references/guided_mode_protocol.md +34 -0
  161. package/vendor/ars/academic-paper-reviewer/references/integration_guide.md +15 -0
  162. package/vendor/ars/academic-paper-reviewer/references/quality_rubrics.md +84 -0
  163. package/vendor/ars/academic-paper-reviewer/references/re_review_mode_protocol.md +340 -0
  164. package/vendor/ars/academic-paper-reviewer/references/review_criteria_framework.md +98 -0
  165. package/vendor/ars/academic-paper-reviewer/references/review_panel_provenance_protocol.md +197 -0
  166. package/vendor/ars/academic-paper-reviewer/references/review_quality_thinking.md +58 -0
  167. package/vendor/ars/academic-paper-reviewer/references/reviewer_sprint_prompt_source.md +324 -0
  168. package/vendor/ars/academic-paper-reviewer/references/sprint_contract_protocol.md +296 -0
  169. package/vendor/ars/academic-paper-reviewer/references/statistical_reporting_standards.md +505 -0
  170. package/vendor/ars/academic-paper-reviewer/references/top_journals_by_field.md +206 -0
  171. package/vendor/ars/academic-paper-reviewer/templates/editorial_decision_template.md +235 -0
  172. package/vendor/ars/academic-paper-reviewer/templates/peer_review_report_template.md +305 -0
  173. package/vendor/ars/academic-paper-reviewer/templates/revision_response_template.md +248 -0
  174. package/vendor/ars/academic-pipeline/SKILL.md +736 -0
  175. package/vendor/ars/academic-pipeline/agents/claim_ref_alignment_audit_agent.md +382 -0
  176. package/vendor/ars/academic-pipeline/agents/collaboration_depth_agent.md +164 -0
  177. package/vendor/ars/academic-pipeline/agents/integrity_verification_agent.md +870 -0
  178. package/vendor/ars/academic-pipeline/agents/pipeline_orchestrator_agent.md +1379 -0
  179. package/vendor/ars/academic-pipeline/agents/state_tracker_agent.md +622 -0
  180. package/vendor/ars/academic-pipeline/examples/full_pipeline_example.md +482 -0
  181. package/vendor/ars/academic-pipeline/examples/integrity_failure_recovery.md +389 -0
  182. package/vendor/ars/academic-pipeline/examples/mid_entry_example.md +414 -0
  183. package/vendor/ars/academic-pipeline/references/adapters/.gitkeep +0 -0
  184. package/vendor/ars/academic-pipeline/references/adapters/overview.md +153 -0
  185. package/vendor/ars/academic-pipeline/references/ai_research_failure_modes.md +185 -0
  186. package/vendor/ars/academic-pipeline/references/changelog.md +14 -0
  187. package/vendor/ars/academic-pipeline/references/claim_audit_calibration_protocol.md +175 -0
  188. package/vendor/ars/academic-pipeline/references/claim_verification_protocol.md +282 -0
  189. package/vendor/ars/academic-pipeline/references/external_review_protocol.md +131 -0
  190. package/vendor/ars/academic-pipeline/references/integrity_review_protocol.md +110 -0
  191. package/vendor/ars/academic-pipeline/references/literature_corpus_consumers.md +193 -0
  192. package/vendor/ars/academic-pipeline/references/mode_advisor.md +135 -0
  193. package/vendor/ars/academic-pipeline/references/passport_as_reset_boundary.md +132 -0
  194. package/vendor/ars/academic-pipeline/references/pipeline_state_machine.md +405 -0
  195. package/vendor/ars/academic-pipeline/references/plagiarism_detection_protocol.md +239 -0
  196. package/vendor/ars/academic-pipeline/references/process_summary_protocol.md +209 -0
  197. package/vendor/ars/academic-pipeline/references/progress_dashboard_template.md +38 -0
  198. package/vendor/ars/academic-pipeline/references/reinforcement_content.md +15 -0
  199. package/vendor/ars/academic-pipeline/references/reproducibility_audit.md +55 -0
  200. package/vendor/ars/academic-pipeline/references/score_trajectory_protocol.md +78 -0
  201. package/vendor/ars/academic-pipeline/references/team_collaboration_protocol.md +261 -0
  202. package/vendor/ars/academic-pipeline/references/two_stage_review_protocol.md +27 -0
  203. package/vendor/ars/academic-pipeline/templates/pipeline_status_template.md +146 -0
  204. package/vendor/ars/agents/report_compiler_agent.md +341 -0
  205. package/vendor/ars/agents/research_architect_agent.md +298 -0
  206. package/vendor/ars/agents/synthesis_agent.md +356 -0
  207. package/vendor/ars/commands/ars-3w.md +10 -0
  208. package/vendor/ars/commands/ars-abstract.md +10 -0
  209. package/vendor/ars/commands/ars-cache-invalidate.md +20 -0
  210. package/vendor/ars/commands/ars-citation-check.md +10 -0
  211. package/vendor/ars/commands/ars-disclosure.md +10 -0
  212. package/vendor/ars/commands/ars-format-convert.md +10 -0
  213. package/vendor/ars/commands/ars-full.md +9 -0
  214. package/vendor/ars/commands/ars-lit-review.md +12 -0
  215. package/vendor/ars/commands/ars-mark-read.md +18 -0
  216. package/vendor/ars/commands/ars-outline.md +10 -0
  217. package/vendor/ars/commands/ars-plan.md +10 -0
  218. package/vendor/ars/commands/ars-rebuttal-audit.md +12 -0
  219. package/vendor/ars/commands/ars-reviewer.md +9 -0
  220. package/vendor/ars/commands/ars-revision-coach.md +9 -0
  221. package/vendor/ars/commands/ars-revision.md +10 -0
  222. package/vendor/ars/commands/ars-unmark-read.md +16 -0
  223. package/vendor/ars/deep-research/SKILL.md +600 -0
  224. package/vendor/ars/deep-research/agents/bibliography_agent.md +473 -0
  225. package/vendor/ars/deep-research/agents/devils_advocate_agent.md +192 -0
  226. package/vendor/ars/deep-research/agents/editor_in_chief_agent.md +167 -0
  227. package/vendor/ars/deep-research/agents/ethics_review_agent.md +267 -0
  228. package/vendor/ars/deep-research/agents/meta_analysis_agent.md +325 -0
  229. package/vendor/ars/deep-research/agents/monitoring_agent.md +209 -0
  230. package/vendor/ars/deep-research/agents/report_compiler_agent.md +341 -0
  231. package/vendor/ars/deep-research/agents/research_architect_agent.md +298 -0
  232. package/vendor/ars/deep-research/agents/research_question_agent.md +216 -0
  233. package/vendor/ars/deep-research/agents/risk_of_bias_agent.md +231 -0
  234. package/vendor/ars/deep-research/agents/socratic_mentor_agent.md +764 -0
  235. package/vendor/ars/deep-research/agents/source_verification_agent.md +219 -0
  236. package/vendor/ars/deep-research/agents/synthesis_agent.md +356 -0
  237. package/vendor/ars/deep-research/agents/timeline_extraction_agent.md +99 -0
  238. package/vendor/ars/deep-research/examples/exploratory_research.md +157 -0
  239. package/vendor/ars/deep-research/examples/fact_check_mode.md +173 -0
  240. package/vendor/ars/deep-research/examples/handoff_to_paper.md +318 -0
  241. package/vendor/ars/deep-research/examples/idea_diversity_coverage_gap_advisory.md +64 -0
  242. package/vendor/ars/deep-research/examples/policy_analysis.md +161 -0
  243. package/vendor/ars/deep-research/examples/review_mode.md +253 -0
  244. package/vendor/ars/deep-research/examples/socratic_guided_research.md +331 -0
  245. package/vendor/ars/deep-research/examples/systematic_review.md +133 -0
  246. package/vendor/ars/deep-research/references/apa7_style_guide.md +162 -0
  247. package/vendor/ars/deep-research/references/argumentation_reasoning_framework.md +68 -0
  248. package/vendor/ars/deep-research/references/arxiv_api_protocol.md +76 -0
  249. package/vendor/ars/deep-research/references/changelog.md +22 -0
  250. package/vendor/ars/deep-research/references/chinese_literature_api_protocol.md +317 -0
  251. package/vendor/ars/deep-research/references/cross_agent_quality_definitions.md +14 -0
  252. package/vendor/ars/deep-research/references/crossref_api_protocol.md +84 -0
  253. package/vendor/ars/deep-research/references/equator_reporting_guidelines.md +482 -0
  254. package/vendor/ars/deep-research/references/ethics_checklist.md +282 -0
  255. package/vendor/ars/deep-research/references/failure_paths.md +355 -0
  256. package/vendor/ars/deep-research/references/interdisciplinary_bridges.md +292 -0
  257. package/vendor/ars/deep-research/references/irb_decision_tree.md +315 -0
  258. package/vendor/ars/deep-research/references/literature_monitoring_strategies.md +263 -0
  259. package/vendor/ars/deep-research/references/logical_fallacies.md +192 -0
  260. package/vendor/ars/deep-research/references/methodology_patterns.md +462 -0
  261. package/vendor/ars/deep-research/references/mode_selection_guide.md +331 -0
  262. package/vendor/ars/deep-research/references/openalex_api_protocol.md +82 -0
  263. package/vendor/ars/deep-research/references/preregistration_guide.md +324 -0
  264. package/vendor/ars/deep-research/references/semantic_scholar_api_protocol.md +107 -0
  265. package/vendor/ars/deep-research/references/socratic_mode_protocol.md +99 -0
  266. package/vendor/ars/deep-research/references/socratic_questioning_framework.md +232 -0
  267. package/vendor/ars/deep-research/references/source_quality_hierarchy.md +188 -0
  268. package/vendor/ars/deep-research/references/systematic_review_protocol.md +95 -0
  269. package/vendor/ars/deep-research/references/systematic_review_toolkit.md +353 -0
  270. package/vendor/ars/deep-research/templates/evidence_assessment_template.md +127 -0
  271. package/vendor/ars/deep-research/templates/literature_matrix_template.md +85 -0
  272. package/vendor/ars/deep-research/templates/preregistration_template.md +318 -0
  273. package/vendor/ars/deep-research/templates/prisma_protocol_template.md +248 -0
  274. package/vendor/ars/deep-research/templates/prisma_report_template.md +415 -0
  275. package/vendor/ars/deep-research/templates/research_brief_template.md +93 -0
  276. package/vendor/ars/package.json +24 -0
  277. package/vendor/ars/pi/README.md +161 -0
  278. package/vendor/ars/pi/package.json +26 -0
  279. package/vendor/ars/pi/wrapper.js +193 -0
  280. package/vendor/ars/pi/wrapper.test.mjs +201 -0
  281. package/vendor/ars/pyproject.toml +2 -0
  282. package/vendor/ars/requirements-pdf-content-classifier.txt +5 -0
  283. package/vendor/ars/scripts/_block_parser.py +396 -0
  284. package/vendor/ars/scripts/_ci_pytest_manifest.toml +661 -0
  285. package/vendor/ars/scripts/_claim_audit_constants.py +268 -0
  286. package/vendor/ars/scripts/_e4_evidence.py +110 -0
  287. package/vendor/ars/scripts/_eval_threshold_gate.py +73 -0
  288. package/vendor/ars/scripts/_markdown_lint_util.py +224 -0
  289. package/vendor/ars/scripts/_next_verified_at_ms.py +176 -0
  290. package/vendor/ars/scripts/_passport_yaml.py +53 -0
  291. package/vendor/ars/scripts/_skill_lint.py +254 -0
  292. package/vendor/ars/scripts/_text_similarity.py +141 -0
  293. package/vendor/ars/scripts/adapters/README.md +89 -0
  294. package/vendor/ars/scripts/adapters/_common.py +209 -0
  295. package/vendor/ars/scripts/adapters/examples/folder_scan/expected_passport.yaml +25 -0
  296. package/vendor/ars/scripts/adapters/examples/folder_scan/expected_rejection_log.yaml +18 -0
  297. package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/Chen2024_AIAssessment.pdf +0 -0
  298. package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/Wang_2023_formative_feedback.pdf +0 -0
  299. package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/paper1.pdf +0 -0
  300. package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture//344/270/255/346/226/207/346/252/224/345/220/215_2024.pdf +0 -0
  301. package/vendor/ars/scripts/adapters/examples/obsidian/expected_passport.yaml +40 -0
  302. package/vendor/ars/scripts/adapters/examples/obsidian/expected_rejection_log.yaml +13 -0
  303. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/.gitkeep +0 -0
  304. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/.obsidian/app.json +1 -0
  305. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/_templates/tmpl.md +7 -0
  306. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/chen2024ai.md +13 -0
  307. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/invalid.md +1 -0
  308. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/wang2023formative.md +10 -0
  309. package/vendor/ars/scripts/adapters/examples/zotero/expected_passport.yaml +34 -0
  310. package/vendor/ars/scripts/adapters/examples/zotero/expected_rejection_log.yaml +34 -0
  311. package/vendor/ars/scripts/adapters/examples/zotero/input_fixture/.gitkeep +0 -0
  312. package/vendor/ars/scripts/adapters/examples/zotero/input_fixture/export.json +43 -0
  313. package/vendor/ars/scripts/adapters/folder_scan.py +214 -0
  314. package/vendor/ars/scripts/adapters/obsidian.py +336 -0
  315. package/vendor/ars/scripts/adapters/tests/.gitkeep +0 -0
  316. package/vendor/ars/scripts/adapters/tests/conftest.py +77 -0
  317. package/vendor/ars/scripts/adapters/tests/test_check_corpus_consumer_protocol.py +632 -0
  318. package/vendor/ars/scripts/adapters/tests/test_check_literature_corpus_schema.py +631 -0
  319. package/vendor/ars/scripts/adapters/tests/test_common.py +365 -0
  320. package/vendor/ars/scripts/adapters/tests/test_conftest.py +96 -0
  321. package/vendor/ars/scripts/adapters/tests/test_folder_scan.py +260 -0
  322. package/vendor/ars/scripts/adapters/tests/test_literature_corpus_entry_schema.py +745 -0
  323. package/vendor/ars/scripts/adapters/tests/test_obsidian.py +357 -0
  324. package/vendor/ars/scripts/adapters/tests/test_rejection_log_schema.py +271 -0
  325. package/vendor/ars/scripts/adapters/tests/test_sync_adapter_docs.py +88 -0
  326. package/vendor/ars/scripts/adapters/tests/test_zotero.py +454 -0
  327. package/vendor/ars/scripts/adapters/zotero.py +318 -0
  328. package/vendor/ars/scripts/adjudication_activity.py +1592 -0
  329. package/vendor/ars/scripts/announce-ars-loaded.sh +144 -0
  330. package/vendor/ars/scripts/ars_anchorize_draft.py +170 -0
  331. package/vendor/ars/scripts/ars_apply_revision_patch.py +912 -0
  332. package/vendor/ars/scripts/ars_cache_invalidate.py +40 -0
  333. package/vendor/ars/scripts/ars_mark_read.py +521 -0
  334. package/vendor/ars/scripts/ars_phase_scope_manifest.json +33 -0
  335. package/vendor/ars/scripts/ars_update_check.sh +215 -0
  336. package/vendor/ars/scripts/ars_write_scope_guard.py +506 -0
  337. package/vendor/ars/scripts/arxiv_client.py +222 -0
  338. package/vendor/ars/scripts/audit_snapshot.py +572 -0
  339. package/vendor/ars/scripts/bibliographic_integrity_signals.py +800 -0
  340. package/vendor/ars/scripts/bootstrap_timeline_yaml.py +146 -0
  341. package/vendor/ars/scripts/build_claim_standing_candidate_ledger.py +1238 -0
  342. package/vendor/ars/scripts/build_claim_standing_query_plan.py +643 -0
  343. package/vendor/ars/scripts/build_content_coverage_advisory.py +1205 -0
  344. package/vendor/ars/scripts/build_cross_document_consistency_advisory.py +2332 -0
  345. package/vendor/ars/scripts/build_review_pathway_rule_trace.py +830 -0
  346. package/vendor/ars/scripts/build_submission_packet_manifest.py +2310 -0
  347. package/vendor/ars/scripts/check_215_field_norm.py +173 -0
  348. package/vendor/ars/scripts/check_216_surface_form.py +250 -0
  349. package/vendor/ars/scripts/check_268_nested_commitment_ledger.py +180 -0
  350. package/vendor/ars/scripts/check_390_revision_patch_discipline.py +296 -0
  351. package/vendor/ars/scripts/check_392_citation_verification_intake.py +126 -0
  352. package/vendor/ars/scripts/check_394_submission_policy.py +178 -0
  353. package/vendor/ars/scripts/check_439_format_profile.py +307 -0
  354. package/vendor/ars/scripts/check_619_disclosure_closeout.py +237 -0
  355. package/vendor/ars/scripts/check_630_codex_subscription_transport.py +456 -0
  356. package/vendor/ars/scripts/check_669_review_pathway_rule_trace.py +662 -0
  357. package/vendor/ars/scripts/check_670_revision_roadmap_integration.py +518 -0
  358. package/vendor/ars/scripts/check_673_adjudication_activity.py +684 -0
  359. package/vendor/ars/scripts/check_684_review_criteria_binding.py +557 -0
  360. package/vendor/ars/scripts/check_agents_mirror_sync.py +115 -0
  361. package/vendor/ars/scripts/check_audit_artifact_consistency.py +2313 -0
  362. package/vendor/ars/scripts/check_benchmark_report.py +79 -0
  363. package/vendor/ars/scripts/check_bibliographic_integrity_signals.py +831 -0
  364. package/vendor/ars/scripts/check_calibration_tiers.py +235 -0
  365. package/vendor/ars/scripts/check_changelog_covers_merges.py +289 -0
  366. package/vendor/ars/scripts/check_ci_pytest_manifest.py +204 -0
  367. package/vendor/ars/scripts/check_claim_audit_consistency.py +1664 -0
  368. package/vendor/ars/scripts/check_claim_standing_candidate_ledger_integration.py +500 -0
  369. package/vendor/ars/scripts/check_claim_standing_freshness.py +253 -0
  370. package/vendor/ars/scripts/check_claim_standing_transmissions.py +449 -0
  371. package/vendor/ars/scripts/check_collaboration_depth_rubric.py +180 -0
  372. package/vendor/ars/scripts/check_command_frontmatter_name.py +116 -0
  373. package/vendor/ars/scripts/check_committee_correspondence.py +333 -0
  374. package/vendor/ars/scripts/check_compliance_report.py +108 -0
  375. package/vendor/ars/scripts/check_content_coverage_advisory_integration.py +796 -0
  376. package/vendor/ars/scripts/check_control_availability.py +172 -0
  377. package/vendor/ars/scripts/check_corpus_consumer_protocol.py +404 -0
  378. package/vendor/ars/scripts/check_cross_document_consistency_advisory_integration.py +1191 -0
  379. package/vendor/ars/scripts/check_cross_model_handoff_contract.py +234 -0
  380. package/vendor/ars/scripts/check_cross_model_verification_sync.py +261 -0
  381. package/vendor/ars/scripts/check_data_access_level.py +131 -0
  382. package/vendor/ars/scripts/check_data_flows.py +252 -0
  383. package/vendor/ars/scripts/check_decision_contract.py +464 -0
  384. package/vendor/ars/scripts/check_degradation_registry.py +326 -0
  385. package/vendor/ars/scripts/check_distribution_surface_claims.py +226 -0
  386. package/vendor/ars/scripts/check_domain_evidence_profile.py +538 -0
  387. package/vendor/ars/scripts/check_e4_promotion.py +195 -0
  388. package/vendor/ars/scripts/check_evals_gold_set.py +279 -0
  389. package/vendor/ars/scripts/check_evidence_row_integration.py +396 -0
  390. package/vendor/ars/scripts/check_experiment_provenance.py +117 -0
  391. package/vendor/ars/scripts/check_field_norm_severity.py +144 -0
  392. package/vendor/ars/scripts/check_firm_rules_sync.py +375 -0
  393. package/vendor/ars/scripts/check_heldout_measurement_report.py +1179 -0
  394. package/vendor/ars/scripts/check_human_subjects_output_contract.py +139 -0
  395. package/vendor/ars/scripts/check_human_subjects_reference_migration.py +844 -0
  396. package/vendor/ars/scripts/check_indirect_prompt_injection_no_call.py +328 -0
  397. package/vendor/ars/scripts/check_instruction_data_boundary.py +236 -0
  398. package/vendor/ars/scripts/check_judge_prompt_version.py +125 -0
  399. package/vendor/ars/scripts/check_literature_corpus_schema.py +402 -0
  400. package/vendor/ars/scripts/check_model_tiering.py +223 -0
  401. package/vendor/ars/scripts/check_panel_synthesis.py +1331 -0
  402. package/vendor/ars/scripts/check_passport_reset_contract.py +214 -0
  403. package/vendor/ars/scripts/check_pattern_eval_manifest.py +422 -0
  404. package/vendor/ars/scripts/check_persuasion_invariance_fixtures.py +637 -0
  405. package/vendor/ars/scripts/check_phase_conformance.py +2180 -0
  406. package/vendor/ars/scripts/check_pipeline_boundary_semantics.py +600 -0
  407. package/vendor/ars/scripts/check_pipeline_integrity.py +340 -0
  408. package/vendor/ars/scripts/check_policy_anchor_protocol.py +159 -0
  409. package/vendor/ars/scripts/check_policy_anchor_table.py +286 -0
  410. package/vendor/ars/scripts/check_preprint_venues_consistency.py +123 -0
  411. package/vendor/ars/scripts/check_prisma_trAIce_freshness.py +84 -0
  412. package/vendor/ars/scripts/check_promotion_bakeoff_preregistration.py +1303 -0
  413. package/vendor/ars/scripts/check_ranking_lift.py +323 -0
  414. package/vendor/ars/scripts/check_re_review_synthesis.py +2719 -0
  415. package/vendor/ars/scripts/check_receipt_enum_sync.py +203 -0
  416. package/vendor/ars/scripts/check_repro_lock.py +85 -0
  417. package/vendor/ars/scripts/check_review_pathway_output.py +276 -0
  418. package/vendor/ars/scripts/check_reviewer_data_fences.py +224 -0
  419. package/vendor/ars/scripts/check_reviewer_finding_contract.py +783 -0
  420. package/vendor/ars/scripts/check_reviewer_role_label.py +447 -0
  421. package/vendor/ars/scripts/check_reviewer_scoring_honesty.py +292 -0
  422. package/vendor/ars/scripts/check_reviewer_sprint_prompt_sync.py +409 -0
  423. package/vendor/ars/scripts/check_revision_claim_drift_suite_v2.py +1622 -0
  424. package/vendor/ars/scripts/check_revision_token_conservation.py +263 -0
  425. package/vendor/ars/scripts/check_risk_register.py +280 -0
  426. package/vendor/ars/scripts/check_role_scoped_contract.py +702 -0
  427. package/vendor/ars/scripts/check_rq_framing_patterns.py +184 -0
  428. package/vendor/ars/scripts/check_rubric_weight_consistency.py +32 -0
  429. package/vendor/ars/scripts/check_seeded_defect_fixtures.py +425 -0
  430. package/vendor/ars/scripts/check_setup_cross_model_parity.py +139 -0
  431. package/vendor/ars/scripts/check_spec_consistency.py +1249 -0
  432. package/vendor/ars/scripts/check_sprint_contract.py +371 -0
  433. package/vendor/ars/scripts/check_stage_capability_matrix.py +784 -0
  434. package/vendor/ars/scripts/check_submission_packet_manifest_integration.py +358 -0
  435. package/vendor/ars/scripts/check_surface_form_parity.py +445 -0
  436. package/vendor/ars/scripts/check_task_type.py +22 -0
  437. package/vendor/ars/scripts/check_tools_allowlist.py +538 -0
  438. package/vendor/ars/scripts/check_tortured_phrase_screening_integration.py +1974 -0
  439. package/vendor/ars/scripts/check_v3_10_134_write_scope.py +286 -0
  440. package/vendor/ars/scripts/check_v3_10_policy.py +656 -0
  441. package/vendor/ars/scripts/check_v3_6_6_ab_manifest.py +364 -0
  442. package/vendor/ars/scripts/check_v3_6_7_pattern_protection.py +1366 -0
  443. package/vendor/ars/scripts/check_v3_6_8_audit_scope_block.py +464 -0
  444. package/vendor/ars/scripts/check_v3_6_8_cite_provenance_pipeline.py +231 -0
  445. package/vendor/ars/scripts/check_v3_6_8_frontmatter_trust_schema.py +248 -0
  446. package/vendor/ars/scripts/check_v3_6_8_mark_read_commands.py +79 -0
  447. package/vendor/ars/scripts/check_v3_6_8_pattern_protection.py +941 -0
  448. package/vendor/ars/scripts/check_v3_7_3_three_layer_citation.py +318 -0
  449. package/vendor/ars/scripts/check_v3_8_annotation_literal_sync.py +228 -0
  450. package/vendor/ars/scripts/check_v3_9_0_triangulation.py +366 -0
  451. package/vendor/ars/scripts/check_v3_9_2_phase_boundary.py +270 -0
  452. package/vendor/ars/scripts/check_v3_9_4_temporal_verification.py +139 -0
  453. package/vendor/ars/scripts/check_venue_disclosure_policies.py +63 -0
  454. package/vendor/ars/scripts/check_version_consistency.py +826 -0
  455. package/vendor/ars/scripts/check_workflow_classification.py +223 -0
  456. package/vendor/ars/scripts/chinese_literature_client.py +1938 -0
  457. package/vendor/ars/scripts/citation_verification_summary.py +85 -0
  458. package/vendor/ars/scripts/claim_audit_calibration.py +517 -0
  459. package/vendor/ars/scripts/claim_audit_finalizer.py +456 -0
  460. package/vendor/ars/scripts/claim_audit_pipeline.py +1594 -0
  461. package/vendor/ars/scripts/claim_registry_coverage.py +493 -0
  462. package/vendor/ars/scripts/claim_standing_discovery.py +784 -0
  463. package/vendor/ars/scripts/claim_standing_stance_runner.py +758 -0
  464. package/vendor/ars/scripts/claim_standing_stance_scorer.py +239 -0
  465. package/vendor/ars/scripts/claim_strength_drift_disposition.py +666 -0
  466. package/vendor/ars/scripts/contamination_signals.py +689 -0
  467. package/vendor/ars/scripts/corpus_consumer_manifest.json +19 -0
  468. package/vendor/ars/scripts/cross_model_codex_transport.py +1374 -0
  469. package/vendor/ars/scripts/cross_model_codex_verify.sh +6 -0
  470. package/vendor/ars/scripts/cross_model_handoff.py +359 -0
  471. package/vendor/ars/scripts/cross_model_smoke_test.sh +183 -0
  472. package/vendor/ars/scripts/cross_model_smoke_test_codex.sh +35 -0
  473. package/vendor/ars/scripts/cross_model_verification/gemini_is_grounded.jq +45 -0
  474. package/vendor/ars/scripts/cross_model_verification/gemini_sources.jq +45 -0
  475. package/vendor/ars/scripts/cross_model_verification/normalize_compat_verdict.py +57 -0
  476. package/vendor/ars/scripts/cross_model_verification/openai_has_completed_web_search.jq +15 -0
  477. package/vendor/ars/scripts/cross_model_verification/openai_sources.jq +17 -0
  478. package/vendor/ars/scripts/cross_model_verification/openai_text.jq +11 -0
  479. package/vendor/ars/scripts/crossref_client.py +225 -0
  480. package/vendor/ars/scripts/dispatch_e4_panel.py +2731 -0
  481. package/vendor/ars/scripts/evidence_rows.py +2043 -0
  482. package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3.json +32 -0
  483. package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_input.json +27 -0
  484. package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_prime.json +32 -0
  485. package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_prime_input.json +27 -0
  486. package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_override.json +50 -0
  487. package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_override_action.json +14 -0
  488. package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_pass.json +44 -0
  489. package/vendor/ars/scripts/fixtures/adjudication_activity/explicit_user_request_log.json +26 -0
  490. package/vendor/ars/scripts/fixtures/adjudication_activity/mandatory_checkpoint_log.json +116 -0
  491. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/README.md +56 -0
  492. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.audit_artifact_entry.json +24 -0
  493. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
  494. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
  495. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.verdict.yaml +19 -0
  496. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a7_orphan_completion/2026-04-30T15-22-04Z-d8f3.jsonl +3 -0
  497. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.audit_artifact_entry.json +26 -0
  498. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
  499. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
  500. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.verdict.yaml +19 -0
  501. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.audit_artifact_entry.json +24 -0
  502. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
  503. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
  504. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.verdict.yaml +12 -0
  505. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/retraction.json +46 -0
  506. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/retraction_check_attestation.json +44 -0
  507. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase.json +44 -0
  508. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase_v1_2_abstract_missing.json +107 -0
  509. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase_v1_2_detected.json +124 -0
  510. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/expected_outcomes.json +77 -0
  511. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/manifest.yaml +37 -0
  512. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/001-valid-doi-test.json +20 -0
  513. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/002-valid-arxiv-test.json +19 -0
  514. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/003-fabricated-test.json +20 -0
  515. package/vendor/ars/scripts/fixtures/claim_audit_calibration/gold_set.json +344 -0
  516. package/vendor/ars/scripts/fixtures/claim_standing_candidate_ledger/query_plan.json +108 -0
  517. package/vendor/ars/scripts/fixtures/claim_standing_candidate_ledger/retrieval_input.json +179 -0
  518. package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/concern_tracker.json +129 -0
  519. package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/response_skeleton.md +19 -0
  520. package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/source_letter.txt +9 -0
  521. package/vendor/ars/scripts/fixtures/content_coverage_advisory/base_draft.json +48 -0
  522. package/vendor/ars/scripts/fixtures/content_coverage_advisory/base_inventory.json +38 -0
  523. package/vendor/ars/scripts/fixtures/content_coverage_advisory/packet/consent.txt +1 -0
  524. package/vendor/ars/scripts/fixtures/content_coverage_advisory/session_sources.json +3 -0
  525. package/vendor/ars/scripts/fixtures/cross_document_consistency/README.md +6 -0
  526. package/vendor/ars/scripts/fixtures/cross_document_consistency/accepted_draft.md +35 -0
  527. package/vendor/ars/scripts/fixtures/cross_document_consistency/cases.json +75 -0
  528. package/vendor/ars/scripts/fixtures/cross_document_consistency/preregistration.md +5 -0
  529. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/forbidden_event.jsonl +4 -0
  530. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/grounded_verified.jsonl +3 -0
  531. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/malformed.jsonl +3 -0
  532. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/missing_search.jsonl +2 -0
  533. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/multiple_finals.jsonl +4 -0
  534. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/not_found.jsonl +3 -0
  535. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/unbound_source.jsonl +3 -0
  536. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/wrong_search_shape.jsonl +3 -0
  537. package/vendor/ars/scripts/fixtures/evidence_rows/phase_e_inputs.json +114 -0
  538. package/vendor/ars/scripts/fixtures/evidence_rows/session_sources.json +5 -0
  539. package/vendor/ars/scripts/fixtures/human_subjects_authority/cross-border-us-tw-gdpr.json +71 -0
  540. package/vendor/ars/scripts/fixtures/human_subjects_authority/gdpr-member-state-unresolved.json +62 -0
  541. package/vendor/ars/scripts/fixtures/human_subjects_authority/missing-data-axis.json +45 -0
  542. package/vendor/ars/scripts/fixtures/human_subjects_authority/no-profile.json +33 -0
  543. package/vendor/ars/scripts/fixtures/human_subjects_authority/tw-gdpr-two-axis.json +74 -0
  544. package/vendor/ars/scripts/fixtures/human_subjects_authority/us-gdpr-two-axis.json +74 -0
  545. package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/lint-near-misses.json +72 -0
  546. package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/no-profile-request.json +18 -0
  547. package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/tw-candidates-request.json +63 -0
  548. package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/us-candidates-request.json +77 -0
  549. package/vendor/ars/scripts/fixtures/review_target_context/exact-declaration.json +24 -0
  550. package/vendor/ars/scripts/fixtures/review_target_context/field-general-declaration.json +24 -0
  551. package/vendor/ars/scripts/fixtures/review_target_context/msr-2027-technical-full-declaration.json +24 -0
  552. package/vendor/ars/scripts/fixtures/review_target_context/synthetic-registry.json +92 -0
  553. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/README.md +21 -0
  554. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_attested_only.json +55 -0
  555. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_machine_supported.json +49 -0
  556. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_not_isolated.json +56 -0
  557. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_unknown.json +49 -0
  558. package/vendor/ars/scripts/fixtures/submission_package/clean/paper.md +16 -0
  559. package/vendor/ars/scripts/fixtures/submission_package/clean/references.bib +13 -0
  560. package/vendor/ars/scripts/fixtures/submission_package/fallback_authoryear/paper.md +11 -0
  561. package/vendor/ars/scripts/fixtures/submission_package/fallback_authoryear/references.bib +13 -0
  562. package/vendor/ars/scripts/fixtures/submission_package/fallback_latex/paper.tex +9 -0
  563. package/vendor/ars/scripts/fixtures/submission_package/fallback_latex/references.bib +13 -0
  564. package/vendor/ars/scripts/fixtures/submission_package/marker_no_join/paper.md +7 -0
  565. package/vendor/ars/scripts/fixtures/submission_package/orphan_intext/paper.md +8 -0
  566. package/vendor/ars/scripts/fixtures/submission_package/orphan_intext/references.bib +6 -0
  567. package/vendor/ars/scripts/fixtures/submission_package/passports/corpus_only.yaml +9 -0
  568. package/vendor/ars/scripts/fixtures/submission_package/passports/summary_join.yaml +16 -0
  569. package/vendor/ars/scripts/fixtures/submission_package/profiles/full.yaml +15 -0
  570. package/vendor/ars/scripts/fixtures/submission_package/profiles/tight.yaml +15 -0
  571. package/vendor/ars/scripts/fixtures/submission_package/summary_join/paper.md +7 -0
  572. package/vendor/ars/scripts/fixtures/submission_package/uncited_reference/paper.md +9 -0
  573. package/vendor/ars/scripts/fixtures/submission_package/uncited_reference/references.bib +13 -0
  574. package/vendor/ars/scripts/fixtures/submission_package/venue_clean/paper.md +26 -0
  575. package/vendor/ars/scripts/fixtures/submission_package/venue_clean/references.bib +13 -0
  576. package/vendor/ars/scripts/fixtures/submission_package/venue_violations/paper.md +20 -0
  577. package/vendor/ars/scripts/fixtures/submission_package/venue_violations/references.bib +13 -0
  578. package/vendor/ars/scripts/fixtures/submission_packet_manifest/base_inventory.json +101 -0
  579. package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/consent-materials.txt +3 -0
  580. package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/training-certificate.txt +2 -0
  581. package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/tw-consent-materials.txt +3 -0
  582. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/corpus_input.yaml +30 -0
  583. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/own_draft.md +36 -0
  584. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/own_draft.tex +21 -0
  585. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/seed_expectations.json +218 -0
  586. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/snapshot.json +164 -0
  587. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/snapshot_manifest.json +29 -0
  588. package/vendor/ars/scripts/fixtures/transport_bodies/README.md +55 -0
  589. package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/empty_feed.xml +10 -0
  590. package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/error_5xx.html +7 -0
  591. package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/id_hit.xml +26 -0
  592. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/README.md +69 -0
  593. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/cnki_landing_page.html +13 -0
  594. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/error_5xx.html +2 -0
  595. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_ambiguous.json +16 -0
  596. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_hit.json +16 -0
  597. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_zero.json +14 -0
  598. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coverage_hit.json +15 -0
  599. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coverage_zero.json +13 -0
  600. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_hit.json +43 -0
  601. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_issn_mismatch.json +31 -0
  602. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_no_doi.json +31 -0
  603. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_unknown_ra.json +34 -0
  604. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_year_mismatch.json +31 -0
  605. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_absent.json +4 -0
  606. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_exists.json +16 -0
  607. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_internal_error.json +4 -0
  608. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/istic_csl_hit.json +25 -0
  609. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/istic_csl_other_title.json +18 -0
  610. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_cnki.json +6 -0
  611. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_crossref.json +6 -0
  612. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_istic.json +6 -0
  613. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_unknown_prefix.json +6 -0
  614. package/vendor/ars/scripts/fixtures/transport_bodies/crossref/doi_hit.json +24 -0
  615. package/vendor/ars/scripts/fixtures/transport_bodies/crossref/error_5xx.html +7 -0
  616. package/vendor/ars/scripts/fixtures/transport_bodies/crossref/title_search_miss.json +15 -0
  617. package/vendor/ars/scripts/fixtures/transport_bodies/openalex/doi_hit.json +29 -0
  618. package/vendor/ars/scripts/fixtures/transport_bodies/openalex/error_5xx.json +4 -0
  619. package/vendor/ars/scripts/fixtures/transport_bodies/openalex/title_search_miss.json +11 -0
  620. package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/doi_hit.json +14 -0
  621. package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/error_5xx.json +3 -0
  622. package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/title_search_miss.json +5 -0
  623. package/vendor/ars/scripts/human_read_attestation_resolver.py +467 -0
  624. package/vendor/ars/scripts/ideation_diversity_assignment_gate.py +430 -0
  625. package/vendor/ars/scripts/inquiry_branch_ledger.py +2540 -0
  626. package/vendor/ars/scripts/legacy/ars_apply_revision_patch_v1_0.py +715 -0
  627. package/vendor/ars/scripts/legacy/check_re_review_synthesis_v1_0.py +2215 -0
  628. package/vendor/ars/scripts/migrate_literature_corpus_to_v3_10.py +223 -0
  629. package/vendor/ars/scripts/migrate_literature_corpus_to_v3_7_3.py +277 -0
  630. package/vendor/ars/scripts/migrate_literature_corpus_to_v3_9_0.py +304 -0
  631. package/vendor/ars/scripts/model_tiering_manifest.json +46 -0
  632. package/vendor/ars/scripts/openalex_client.py +232 -0
  633. package/vendor/ars/scripts/parse_audit_verdict.py +802 -0
  634. package/vendor/ars/scripts/pdf_content_classifier_worker.py +176 -0
  635. package/vendor/ars/scripts/pdf_read_preflight.py +1455 -0
  636. package/vendor/ars/scripts/policy_anchor_disclosure_referee.py +354 -0
  637. package/vendor/ars/scripts/recompute_receipts.py +1414 -0
  638. package/vendor/ars/scripts/render_claim_standing_view.py +374 -0
  639. package/vendor/ars/scripts/render_eval_comment.py +130 -0
  640. package/vendor/ars/scripts/render_harness_retirement_issue.py +167 -0
  641. package/vendor/ars/scripts/repro_lock_validation.py +90 -0
  642. package/vendor/ars/scripts/research_workflow_profile.py +1079 -0
  643. package/vendor/ars/scripts/resolve_human_subjects_authority.py +1158 -0
  644. package/vendor/ars/scripts/resolve_review_target_context.py +683 -0
  645. package/vendor/ars/scripts/resume_e4_record.py +509 -0
  646. package/vendor/ars/scripts/retraction_status.py +484 -0
  647. package/vendor/ars/scripts/review_criteria_binding.py +889 -0
  648. package/vendor/ars/scripts/review_panel_provenance.py +744 -0
  649. package/vendor/ars/scripts/revision_roadmap.py +1967 -0
  650. package/vendor/ars/scripts/run_ci_pytest_manifest.py +116 -0
  651. package/vendor/ars/scripts/run_codex_audit.sh +1191 -0
  652. package/vendor/ars/scripts/run_evals.py +513 -0
  653. package/vendor/ars/scripts/run_ideation_diversity_no_call.py +3505 -0
  654. package/vendor/ars/scripts/run_indirect_prompt_injection_no_call.py +3330 -0
  655. package/vendor/ars/scripts/run_indirect_prompt_injection_probe.py +399 -0
  656. package/vendor/ars/scripts/run_review_criteria_constructive_value.py +1895 -0
  657. package/vendor/ars/scripts/run_role_topology_utility_dry_run.py +606 -0
  658. package/vendor/ars/scripts/score_review_criteria_constructive_value.py +617 -0
  659. package/vendor/ars/scripts/semantic_scholar_client.py +291 -0
  660. package/vendor/ars/scripts/slr_lineage.py +59 -0
  661. package/vendor/ars/scripts/sync_adapter_docs.py +118 -0
  662. package/vendor/ars/scripts/temporal_integrity_audit.py +840 -0
  663. package/vendor/ars/scripts/test_431_exact_or_bust.py +253 -0
  664. package/vendor/ars/scripts/test__eval_threshold_gate.py +125 -0
  665. package/vendor/ars/scripts/test__markdown_lint_util.py +129 -0
  666. package/vendor/ars/scripts/test__next_verified_at_ms.py +256 -0
  667. package/vendor/ars/scripts/test_adjacent_framing_probe_lint.py +171 -0
  668. package/vendor/ars/scripts/test_adjudication_activity.py +1515 -0
  669. package/vendor/ars/scripts/test_ars_anchorize_draft.py +178 -0
  670. package/vendor/ars/scripts/test_ars_apply_revision_patch.py +1335 -0
  671. package/vendor/ars/scripts/test_ars_cache_invalidate.py +51 -0
  672. package/vendor/ars/scripts/test_ars_mark_read.py +910 -0
  673. package/vendor/ars/scripts/test_ars_update_check.py +816 -0
  674. package/vendor/ars/scripts/test_ars_write_scope_guard.py +790 -0
  675. package/vendor/ars/scripts/test_arxiv_client.py +374 -0
  676. package/vendor/ars/scripts/test_audit_schemas.py +560 -0
  677. package/vendor/ars/scripts/test_audit_snapshot_render_section_0.py +105 -0
  678. package/vendor/ars/scripts/test_block_parser.py +259 -0
  679. package/vendor/ars/scripts/test_bootstrap_timeline_yaml.py +148 -0
  680. package/vendor/ars/scripts/test_build_claim_standing_candidate_ledger.py +1203 -0
  681. package/vendor/ars/scripts/test_build_claim_standing_query_plan.py +791 -0
  682. package/vendor/ars/scripts/test_build_submission_packet_manifest.py +2644 -0
  683. package/vendor/ars/scripts/test_check_215_field_norm.py +238 -0
  684. package/vendor/ars/scripts/test_check_216_surface_form.py +341 -0
  685. package/vendor/ars/scripts/test_check_268_nested_commitment_ledger.py +194 -0
  686. package/vendor/ars/scripts/test_check_390_revision_patch_discipline.py +279 -0
  687. package/vendor/ars/scripts/test_check_392_citation_verification_intake.py +144 -0
  688. package/vendor/ars/scripts/test_check_394_submission_policy.py +200 -0
  689. package/vendor/ars/scripts/test_check_439_format_profile.py +252 -0
  690. package/vendor/ars/scripts/test_check_619_disclosure_closeout.py +158 -0
  691. package/vendor/ars/scripts/test_check_630_codex_subscription_transport.py +245 -0
  692. package/vendor/ars/scripts/test_check_669_review_pathway_rule_trace.py +304 -0
  693. package/vendor/ars/scripts/test_check_670_revision_roadmap_integration.py +260 -0
  694. package/vendor/ars/scripts/test_check_673_adjudication_activity.py +367 -0
  695. package/vendor/ars/scripts/test_check_684_review_criteria_binding.py +274 -0
  696. package/vendor/ars/scripts/test_check_agents_mirror_sync.py +137 -0
  697. package/vendor/ars/scripts/test_check_audit_artifact_consistency.py +2133 -0
  698. package/vendor/ars/scripts/test_check_benchmark_report.py +117 -0
  699. package/vendor/ars/scripts/test_check_bibliographic_integrity_signals.py +609 -0
  700. package/vendor/ars/scripts/test_check_calibration_tiers.py +376 -0
  701. package/vendor/ars/scripts/test_check_changelog_covers_merges.py +509 -0
  702. package/vendor/ars/scripts/test_check_ci_pytest_manifest.py +425 -0
  703. package/vendor/ars/scripts/test_check_claim_standing_candidate_ledger_integration.py +256 -0
  704. package/vendor/ars/scripts/test_check_claim_standing_freshness.py +277 -0
  705. package/vendor/ars/scripts/test_check_collaboration_depth_rubric.py +239 -0
  706. package/vendor/ars/scripts/test_check_command_frontmatter_name.py +178 -0
  707. package/vendor/ars/scripts/test_check_committee_correspondence.py +298 -0
  708. package/vendor/ars/scripts/test_check_compliance_report.py +381 -0
  709. package/vendor/ars/scripts/test_check_content_coverage_advisory_integration.py +308 -0
  710. package/vendor/ars/scripts/test_check_control_availability.py +361 -0
  711. package/vendor/ars/scripts/test_check_cross_document_consistency_advisory_integration.py +698 -0
  712. package/vendor/ars/scripts/test_check_cross_model_handoff_contract.py +301 -0
  713. package/vendor/ars/scripts/test_check_cross_model_verification_sync.py +203 -0
  714. package/vendor/ars/scripts/test_check_data_access_level.py +227 -0
  715. package/vendor/ars/scripts/test_check_data_flows.py +382 -0
  716. package/vendor/ars/scripts/test_check_decision_contract.py +387 -0
  717. package/vendor/ars/scripts/test_check_degradation_registry.py +270 -0
  718. package/vendor/ars/scripts/test_check_distribution_surface_claims.py +225 -0
  719. package/vendor/ars/scripts/test_check_domain_evidence_profile.py +439 -0
  720. package/vendor/ars/scripts/test_check_e4_promotion.py +163 -0
  721. package/vendor/ars/scripts/test_check_evals_gold_set.py +312 -0
  722. package/vendor/ars/scripts/test_check_evidence_row_integration.py +187 -0
  723. package/vendor/ars/scripts/test_check_field_norm_severity.py +173 -0
  724. package/vendor/ars/scripts/test_check_firm_rules_sync.py +342 -0
  725. package/vendor/ars/scripts/test_check_heldout_measurement_report.py +1508 -0
  726. package/vendor/ars/scripts/test_check_human_subjects_output_contract.py +129 -0
  727. package/vendor/ars/scripts/test_check_human_subjects_reference_migration.py +756 -0
  728. package/vendor/ars/scripts/test_check_instruction_data_boundary.py +204 -0
  729. package/vendor/ars/scripts/test_check_judge_prompt_version.py +90 -0
  730. package/vendor/ars/scripts/test_check_model_tiering.py +236 -0
  731. package/vendor/ars/scripts/test_check_panel_synthesis.py +1658 -0
  732. package/vendor/ars/scripts/test_check_passport_reset_contract.py +249 -0
  733. package/vendor/ars/scripts/test_check_pattern_eval_manifest.py +381 -0
  734. package/vendor/ars/scripts/test_check_persuasion_invariance_fixtures.py +591 -0
  735. package/vendor/ars/scripts/test_check_phase_conformance.py +4366 -0
  736. package/vendor/ars/scripts/test_check_pipeline_boundary_semantics.py +931 -0
  737. package/vendor/ars/scripts/test_check_pipeline_integrity.py +243 -0
  738. package/vendor/ars/scripts/test_check_policy_anchor_protocol.py +236 -0
  739. package/vendor/ars/scripts/test_check_policy_anchor_table.py +295 -0
  740. package/vendor/ars/scripts/test_check_prisma_trAIce_freshness.py +68 -0
  741. package/vendor/ars/scripts/test_check_promotion_bakeoff_preregistration.py +799 -0
  742. package/vendor/ars/scripts/test_check_ranking_lift.py +377 -0
  743. package/vendor/ars/scripts/test_check_re_review_synthesis.py +3398 -0
  744. package/vendor/ars/scripts/test_check_receipt_enum_sync.py +176 -0
  745. package/vendor/ars/scripts/test_check_repro_lock.py +107 -0
  746. package/vendor/ars/scripts/test_check_reviewer_data_fences.py +334 -0
  747. package/vendor/ars/scripts/test_check_reviewer_finding_contract.py +1107 -0
  748. package/vendor/ars/scripts/test_check_reviewer_role_label.py +491 -0
  749. package/vendor/ars/scripts/test_check_reviewer_scoring_honesty.py +222 -0
  750. package/vendor/ars/scripts/test_check_reviewer_sprint_prompt_sync.py +373 -0
  751. package/vendor/ars/scripts/test_check_revision_claim_drift_suite_v2.py +1925 -0
  752. package/vendor/ars/scripts/test_check_revision_token_conservation.py +398 -0
  753. package/vendor/ars/scripts/test_check_risk_register.py +336 -0
  754. package/vendor/ars/scripts/test_check_role_scoped_contract.py +1104 -0
  755. package/vendor/ars/scripts/test_check_rq_framing_patterns.py +110 -0
  756. package/vendor/ars/scripts/test_check_rubric_weight_consistency.py +32 -0
  757. package/vendor/ars/scripts/test_check_seeded_defect_fixtures.py +392 -0
  758. package/vendor/ars/scripts/test_check_setup_cross_model_parity.py +121 -0
  759. package/vendor/ars/scripts/test_check_spec_consistency.py +1081 -0
  760. package/vendor/ars/scripts/test_check_sprint_contract.py +458 -0
  761. package/vendor/ars/scripts/test_check_stage_capability_matrix.py +698 -0
  762. package/vendor/ars/scripts/test_check_submission_packet_manifest_integration.py +177 -0
  763. package/vendor/ars/scripts/test_check_surface_form_parity.py +417 -0
  764. package/vendor/ars/scripts/test_check_task_type.py +116 -0
  765. package/vendor/ars/scripts/test_check_tools_allowlist.py +835 -0
  766. package/vendor/ars/scripts/test_check_tortured_phrase_screening_integration.py +1306 -0
  767. package/vendor/ars/scripts/test_check_v3_10_134_write_scope.py +251 -0
  768. package/vendor/ars/scripts/test_check_v3_10_policy.py +547 -0
  769. package/vendor/ars/scripts/test_check_v3_6_7_pattern_protection.py +960 -0
  770. package/vendor/ars/scripts/test_check_v3_6_8_audit_scope_block.py +1000 -0
  771. package/vendor/ars/scripts/test_check_v3_6_8_cite_provenance_pipeline.py +454 -0
  772. package/vendor/ars/scripts/test_check_v3_6_8_frontmatter_trust_schema.py +581 -0
  773. package/vendor/ars/scripts/test_check_v3_6_8_mark_read_commands.py +120 -0
  774. package/vendor/ars/scripts/test_check_v3_6_8_pattern_protection.py +1138 -0
  775. package/vendor/ars/scripts/test_check_v3_7_3_three_layer_citation.py +566 -0
  776. package/vendor/ars/scripts/test_check_v3_8_annotation_literal_sync.py +262 -0
  777. package/vendor/ars/scripts/test_check_v3_9_0_triangulation.py +321 -0
  778. package/vendor/ars/scripts/test_check_v3_9_2_phase_boundary.py +190 -0
  779. package/vendor/ars/scripts/test_check_v3_9_4_temporal_verification.py +471 -0
  780. package/vendor/ars/scripts/test_check_version_consistency.py +1462 -0
  781. package/vendor/ars/scripts/test_check_workflow_classification.py +225 -0
  782. package/vendor/ars/scripts/test_chinese_literature_client.py +1889 -0
  783. package/vendor/ars/scripts/test_citation_existence_policy.py +480 -0
  784. package/vendor/ars/scripts/test_citation_verification_summary.py +342 -0
  785. package/vendor/ars/scripts/test_claim_audit_calibration.py +882 -0
  786. package/vendor/ars/scripts/test_claim_audit_finalizer.py +979 -0
  787. package/vendor/ars/scripts/test_claim_audit_pipeline.py +2398 -0
  788. package/vendor/ars/scripts/test_claim_audit_schema.py +1778 -0
  789. package/vendor/ars/scripts/test_claim_intent_manifest.py +666 -0
  790. package/vendor/ars/scripts/test_claim_registry_coverage.py +246 -0
  791. package/vendor/ars/scripts/test_claim_standing_discovery.py +579 -0
  792. package/vendor/ars/scripts/test_claim_standing_pipeline_wiring.py +290 -0
  793. package/vendor/ars/scripts/test_claim_standing_stance_assets.py +399 -0
  794. package/vendor/ars/scripts/test_claim_standing_stance_contracts.py +383 -0
  795. package/vendor/ars/scripts/test_claim_standing_stance_runner.py +349 -0
  796. package/vendor/ars/scripts/test_claim_standing_transmissions.py +605 -0
  797. package/vendor/ars/scripts/test_claim_strength_drift_disposition.py +517 -0
  798. package/vendor/ars/scripts/test_claim_verification_coverage_contract.py +104 -0
  799. package/vendor/ars/scripts/test_contamination_signals.py +1086 -0
  800. package/vendor/ars/scripts/test_content_coverage_advisory.py +1767 -0
  801. package/vendor/ars/scripts/test_cross_document_consistency_advisory.py +1270 -0
  802. package/vendor/ars/scripts/test_cross_model_codex_transport.py +1152 -0
  803. package/vendor/ars/scripts/test_cross_model_handoff.py +571 -0
  804. package/vendor/ars/scripts/test_cross_model_verification_guards.py +792 -0
  805. package/vendor/ars/scripts/test_crossref_client.py +393 -0
  806. package/vendor/ars/scripts/test_dispatch_e4_panel.py +4235 -0
  807. package/vendor/ars/scripts/test_e2e_claim_audit.py +540 -0
  808. package/vendor/ars/scripts/test_eval_harness_workflow.py +140 -0
  809. package/vendor/ars/scripts/test_evals_citation_extraction.py +150 -0
  810. package/vendor/ars/scripts/test_evals_lift_report_schema.py +108 -0
  811. package/vendor/ars/scripts/test_evidence_rows.py +2593 -0
  812. package/vendor/ars/scripts/test_experiment_provenance.py +915 -0
  813. package/vendor/ars/scripts/test_human_read_attestation_resolver.py +477 -0
  814. package/vendor/ars/scripts/test_ideation_diversity_assignment_gate.py +615 -0
  815. package/vendor/ars/scripts/test_indirect_prompt_injection_behavior_probe.py +250 -0
  816. package/vendor/ars/scripts/test_inquiry_branch_ledger.py +2296 -0
  817. package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_10.py +248 -0
  818. package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_7_3.py +545 -0
  819. package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_9_0.py +497 -0
  820. package/vendor/ars/scripts/test_normalize_compat_verdict.py +149 -0
  821. package/vendor/ars/scripts/test_openalex_client.py +490 -0
  822. package/vendor/ars/scripts/test_passport_yaml.py +104 -0
  823. package/vendor/ars/scripts/test_pattern_eval_runtime.py +1295 -0
  824. package/vendor/ars/scripts/test_pdf_read_preflight.py +1943 -0
  825. package/vendor/ars/scripts/test_policy_anchor_disclosure.py +666 -0
  826. package/vendor/ars/scripts/test_reading_probe_lint.py +218 -0
  827. package/vendor/ars/scripts/test_recompute_receipts.py +778 -0
  828. package/vendor/ars/scripts/test_render_claim_standing_view.py +192 -0
  829. package/vendor/ars/scripts/test_render_eval_comment.py +162 -0
  830. package/vendor/ars/scripts/test_render_harness_retirement_issue.py +110 -0
  831. package/vendor/ars/scripts/test_repro_lock_validation_drift.py +100 -0
  832. package/vendor/ars/scripts/test_research_workflow_profile.py +734 -0
  833. package/vendor/ars/scripts/test_resolve_human_subjects_authority.py +1219 -0
  834. package/vendor/ars/scripts/test_resolve_review_target_context.py +703 -0
  835. package/vendor/ars/scripts/test_resume_e4_record.py +315 -0
  836. package/vendor/ars/scripts/test_retraction_status.py +456 -0
  837. package/vendor/ars/scripts/test_review_criteria_binding.py +629 -0
  838. package/vendor/ars/scripts/test_review_panel_provenance.py +565 -0
  839. package/vendor/ars/scripts/test_review_pathway_rule_trace.py +821 -0
  840. package/vendor/ars/scripts/test_revision_roadmap.py +1255 -0
  841. package/vendor/ars/scripts/test_run_ci_pytest_manifest.py +186 -0
  842. package/vendor/ars/scripts/test_run_codex_audit_e2e.py +369 -0
  843. package/vendor/ars/scripts/test_run_evals.py +430 -0
  844. package/vendor/ars/scripts/test_run_guard_launcher.py +500 -0
  845. package/vendor/ars/scripts/test_run_ideation_diversity_no_call.py +1833 -0
  846. package/vendor/ars/scripts/test_run_indirect_prompt_injection_no_call.py +1889 -0
  847. package/vendor/ars/scripts/test_run_review_criteria_constructive_value.py +586 -0
  848. package/vendor/ars/scripts/test_run_role_topology_utility_dry_run.py +428 -0
  849. package/vendor/ars/scripts/test_score_review_criteria_constructive_value.py +340 -0
  850. package/vendor/ars/scripts/test_semantic_scholar_client.py +554 -0
  851. package/vendor/ars/scripts/test_slr_lineage_emission.py +230 -0
  852. package/vendor/ars/scripts/test_socratic_rq_non_generation_contract.py +173 -0
  853. package/vendor/ars/scripts/test_temporal_integrity_audit.py +438 -0
  854. package/vendor/ars/scripts/test_text_similarity.py +95 -0
  855. package/vendor/ars/scripts/test_title_fuzzy_false_positive.py +111 -0
  856. package/vendor/ars/scripts/test_tortured_phrase_screening.py +2959 -0
  857. package/vendor/ars/scripts/test_transport_fixture_citation_gate.py +338 -0
  858. package/vendor/ars/scripts/test_uncited_assertion.py +558 -0
  859. package/vendor/ars/scripts/test_v3_6_7_phase_6_6.py +1279 -0
  860. package/vendor/ars/scripts/test_validate_compliance_fixtures.py +36 -0
  861. package/vendor/ars/scripts/test_validate_ideation_diversity_assets.py +230 -0
  862. package/vendor/ars/scripts/test_venue_disclosure_contract.py +755 -0
  863. package/vendor/ars/scripts/test_verification_cache.py +280 -0
  864. package/vendor/ars/scripts/test_verification_gate.py +461 -0
  865. package/vendor/ars/scripts/test_verify_passport_cli.py +123 -0
  866. package/vendor/ars/scripts/test_verify_submission_package.py +1407 -0
  867. package/vendor/ars/scripts/test_version_records_schema.py +211 -0
  868. package/vendor/ars/scripts/tortured_phrase_screening.py +3502 -0
  869. package/vendor/ars/scripts/uncited_assertion_detector.py +254 -0
  870. package/vendor/ars/scripts/v3_6_7_inversion_manifest.json +9 -0
  871. package/vendor/ars/scripts/v3_6_8_inversion_manifest.json +10 -0
  872. package/vendor/ars/scripts/validate_claim_standing_stance_assets.py +249 -0
  873. package/vendor/ars/scripts/validate_compliance_fixtures.py +56 -0
  874. package/vendor/ars/scripts/validate_ideation_diversity_assets.py +303 -0
  875. package/vendor/ars/scripts/venue_disclosure_contract_harness.py +837 -0
  876. package/vendor/ars/scripts/verification_cache.py +276 -0
  877. package/vendor/ars/scripts/verification_gate/__init__.py +345 -0
  878. package/vendor/ars/scripts/verify_passport.py +133 -0
  879. package/vendor/ars/scripts/verify_submission_package.py +1657 -0
  880. package/vendor/ars/shared/agents/compliance_agent.md +136 -0
  881. package/vendor/ars/shared/artifact_reproducibility_pattern.md +173 -0
  882. package/vendor/ars/shared/benchmark_report.schema.json +81 -0
  883. package/vendor/ars/shared/benchmark_report_pattern.md +180 -0
  884. package/vendor/ars/shared/bibliographic_integrity_signals.md +142 -0
  885. package/vendor/ars/shared/collaboration_depth_rubric.md +154 -0
  886. package/vendor/ars/shared/compliance_checkpoint_protocol.md +162 -0
  887. package/vendor/ars/shared/compliance_report.schema.json +187 -0
  888. package/vendor/ars/shared/contracts/README.md +938 -0
  889. package/vendor/ars/shared/contracts/activity/adjudication_activity_input.schema.json +555 -0
  890. package/vendor/ars/shared/contracts/activity/adjudication_activity_store.schema.json +560 -0
  891. package/vendor/ars/shared/contracts/audit/audit_jsonl.schema.json +128 -0
  892. package/vendor/ars/shared/contracts/audit/audit_sidecar.schema.json +169 -0
  893. package/vendor/ars/shared/contracts/audit/audit_verdict.schema.json +133 -0
  894. package/vendor/ars/shared/contracts/audit/cross_document_consistency_advisory.schema.json +480 -0
  895. package/vendor/ars/shared/contracts/audit/cross_document_consistency_advisory_draft.schema.json +564 -0
  896. package/vendor/ars/shared/contracts/audit/cross_document_source_manifest.schema.json +202 -0
  897. package/vendor/ars/shared/contracts/audit/tortured_phrase_advisory.schema.json +1362 -0
  898. package/vendor/ars/shared/contracts/audit/tortured_phrase_snapshot.schema.json +208 -0
  899. package/vendor/ars/shared/contracts/audit/tortured_phrase_snapshot_manifest.schema.json +335 -0
  900. package/vendor/ars/shared/contracts/capability/stage_capability_matrix.json +434 -0
  901. package/vendor/ars/shared/contracts/claim_standing/candidate_ledger.schema.json +705 -0
  902. package/vendor/ars/shared/contracts/claim_standing/query_plan.schema.json +389 -0
  903. package/vendor/ars/shared/contracts/claim_standing/query_plan_v1_1.schema.json +782 -0
  904. package/vendor/ars/shared/contracts/claim_standing/retrieval_input.schema.json +440 -0
  905. package/vendor/ars/shared/contracts/claim_standing/stance_record.schema.json +460 -0
  906. package/vendor/ars/shared/contracts/claim_standing/transmission_ledger.schema.json +282 -0
  907. package/vendor/ars/shared/contracts/cross_model/codex_citation_receipt.schema.json +157 -0
  908. package/vendor/ars/shared/contracts/cross_model/codex_citation_request.schema.json +23 -0
  909. package/vendor/ars/shared/contracts/cross_model/promotion_bakeoff_sealed_commitment.schema.json +55 -0
  910. package/vendor/ars/shared/contracts/cross_model/promotion_bakeoff_sealed_reveal.schema.json +50 -0
  911. package/vendor/ars/shared/contracts/degradation_registry.json +428 -0
  912. package/vendor/ars/shared/contracts/evaluator/full.json +126 -0
  913. package/vendor/ars/shared/contracts/evidence/claim_registry.schema.json +50 -0
  914. package/vendor/ars/shared/contracts/evidence/claim_registry_coverage_report.schema.json +79 -0
  915. package/vendor/ars/shared/contracts/evidence/evidence_row.schema.json +504 -0
  916. package/vendor/ars/shared/contracts/evidence/evidence_row_v1_1.schema.json +364 -0
  917. package/vendor/ars/shared/contracts/evidence/evidence_row_v1_2.schema.json +714 -0
  918. package/vendor/ars/shared/contracts/evidence/evidence_row_v1_3.schema.json +524 -0
  919. package/vendor/ars/shared/contracts/human_subjects/authority_profile_registry.schema.json +497 -0
  920. package/vendor/ars/shared/contracts/human_subjects/committee_correspondence.schema.json +253 -0
  921. package/vendor/ars/shared/contracts/human_subjects/content_coverage_advisory.schema.json +620 -0
  922. package/vendor/ars/shared/contracts/human_subjects/irb_context_record.schema.json +329 -0
  923. package/vendor/ars/shared/contracts/human_subjects/resolved_authority_context.schema.json +347 -0
  924. package/vendor/ars/shared/contracts/human_subjects/review_pathway_rule_trace.schema.json +265 -0
  925. package/vendor/ars/shared/contracts/human_subjects/review_pathway_trace_request.schema.json +127 -0
  926. package/vendor/ars/shared/contracts/human_subjects/submission_packet_inventory.schema.json +240 -0
  927. package/vendor/ars/shared/contracts/human_subjects/submission_packet_manifest.schema.json +999 -0
  928. package/vendor/ars/shared/contracts/passport/audit_artifact_entry.schema.json +266 -0
  929. package/vendor/ars/shared/contracts/passport/bibliographic_integrity_signal.schema.json +1676 -0
  930. package/vendor/ars/shared/contracts/passport/citation_provenance.schema.json +107 -0
  931. package/vendor/ars/shared/contracts/passport/citation_verification_summary.schema.json +164 -0
  932. package/vendor/ars/shared/contracts/passport/claim_audit_result.schema.json +124 -0
  933. package/vendor/ars/shared/contracts/passport/claim_drift.schema.json +58 -0
  934. package/vendor/ars/shared/contracts/passport/claim_intent_manifest.schema.json +107 -0
  935. package/vendor/ars/shared/contracts/passport/constraint_violation.schema.json +65 -0
  936. package/vendor/ars/shared/contracts/passport/experiment_alignment_result.schema.json +69 -0
  937. package/vendor/ars/shared/contracts/passport/experiment_provenance_entry.schema.json +169 -0
  938. package/vendor/ars/shared/contracts/passport/human_read_log.schema.json +86 -0
  939. package/vendor/ars/shared/contracts/passport/inquiry_ledger_ref.schema.json +22 -0
  940. package/vendor/ars/shared/contracts/passport/literature_corpus_entry.schema.json +650 -0
  941. package/vendor/ars/shared/contracts/passport/preregistration_artifact.schema.json +132 -0
  942. package/vendor/ars/shared/contracts/passport/rejection_log.schema.json +89 -0
  943. package/vendor/ars/shared/contracts/passport/reset_ledger_entry.schema.json +158 -0
  944. package/vendor/ars/shared/contracts/passport/temporal_audit_results.schema.json +208 -0
  945. package/vendor/ars/shared/contracts/passport/terminal_policies.schema.json +50 -0
  946. package/vendor/ars/shared/contracts/passport/timeline.schema.json +102 -0
  947. package/vendor/ars/shared/contracts/passport/uncited_assertion.schema.json +56 -0
  948. package/vendor/ars/shared/contracts/passport/uncited_audit_failure.schema.json +72 -0
  949. package/vendor/ars/shared/contracts/passport/user_attested_read_resolution.schema.json +87 -0
  950. package/vendor/ars/shared/contracts/passport/version_records.schema.json +138 -0
  951. package/vendor/ars/shared/contracts/patch/block_manifest.schema.json +45 -0
  952. package/vendor/ars/shared/contracts/patch/legacy/v1_0/revision_patch.schema.json +110 -0
  953. package/vendor/ars/shared/contracts/patch/revision_patch.schema.json +250 -0
  954. package/vendor/ars/shared/contracts/pdf/pdf_content_classifier_diagnostic.schema.json +39 -0
  955. package/vendor/ars/shared/contracts/pdf/pdf_content_classifier_worker.schema.json +88 -0
  956. package/vendor/ars/shared/contracts/pdf/pdf_read_preflight.schema.json +227 -0
  957. package/vendor/ars/shared/contracts/re_review/input_manifest.schema.json +161 -0
  958. package/vendor/ars/shared/contracts/re_review/legacy/v1_0/input_manifest.schema.json +134 -0
  959. package/vendor/ars/shared/contracts/re_review/legacy/v1_0/precommitment.schema.json +193 -0
  960. package/vendor/ars/shared/contracts/re_review/legacy/v1_0/traceability.schema.json +841 -0
  961. package/vendor/ars/shared/contracts/re_review/legacy/v1_0/verdict_record.schema.json +244 -0
  962. package/vendor/ars/shared/contracts/re_review/precommitment.schema.json +193 -0
  963. package/vendor/ars/shared/contracts/re_review/traceability.schema.json +916 -0
  964. package/vendor/ars/shared/contracts/re_review/verdict_record.schema.json +245 -0
  965. package/vendor/ars/shared/contracts/research_workflow/inquiry_branch_ledger.schema.json +449 -0
  966. package/vendor/ars/shared/contracts/research_workflow/research_workflow_profile.schema.json +222 -0
  967. package/vendor/ars/shared/contracts/research_workflow/research_workflow_profile_selection_receipt.schema.json +111 -0
  968. package/vendor/ars/shared/contracts/review_target/constructive_review_findings.schema.json +172 -0
  969. package/vendor/ars/shared/contracts/review_target/criteria_registry.schema.json +97 -0
  970. package/vendor/ars/shared/contracts/review_target/review_criteria_binding_manifest.schema.json +235 -0
  971. package/vendor/ars/shared/contracts/review_target/review_criteria_source_receipt.schema.json +158 -0
  972. package/vendor/ars/shared/contracts/review_target/review_target_context.schema.json +106 -0
  973. package/vendor/ars/shared/contracts/review_target/review_target_declaration.schema.json +118 -0
  974. package/vendor/ars/shared/contracts/reviewer/full.json +114 -0
  975. package/vendor/ars/shared/contracts/reviewer/methodology_focus.json +75 -0
  976. package/vendor/ars/shared/contracts/reviewer/review_panel_provenance.schema.json +263 -0
  977. package/vendor/ars/shared/contracts/reviewer/review_panel_provenance_carrier.schema.json +122 -0
  978. package/vendor/ars/shared/contracts/reviewer/review_panel_provenance_input.schema.json +120 -0
  979. package/vendor/ars/shared/contracts/revision/author_adjudication.schema.json +251 -0
  980. package/vendor/ars/shared/contracts/revision/author_adjudication_input.schema.json +25 -0
  981. package/vendor/ars/shared/contracts/revision/claim_strength_drift_disposition.schema.json +97 -0
  982. package/vendor/ars/shared/contracts/revision/claim_strength_drift_disposition_input.schema.json +80 -0
  983. package/vendor/ars/shared/contracts/revision/claim_strength_drift_findings.schema.json +139 -0
  984. package/vendor/ars/shared/contracts/revision/claim_surface_manifest.schema.json +99 -0
  985. package/vendor/ars/shared/contracts/revision/integrity_correction_authorization.schema.json +140 -0
  986. package/vendor/ars/shared/contracts/revision/integrity_correction_authorization_input.schema.json +28 -0
  987. package/vendor/ars/shared/contracts/revision/integrity_correction_list.schema.json +48 -0
  988. package/vendor/ars/shared/contracts/revision/integrity_pass_receipt.schema.json +16 -0
  989. package/vendor/ars/shared/contracts/revision/revision_evidence_bundle.schema.json +134 -0
  990. package/vendor/ars/shared/contracts/revision/revision_roadmap.schema.json +334 -0
  991. package/vendor/ars/shared/contracts/submission/format_profile.example.yaml +33 -0
  992. package/vendor/ars/shared/contracts/submission/format_profile.schema.json +102 -0
  993. package/vendor/ars/shared/contracts/submission/submission_verification_report.schema.json +233 -0
  994. package/vendor/ars/shared/contracts/submission/venue_profile.schema.json +114 -0
  995. package/vendor/ars/shared/contracts/writer/full.json +87 -0
  996. package/vendor/ars/shared/cross_model_verification.md +714 -0
  997. package/vendor/ars/shared/evals_lift_report.schema.json +141 -0
  998. package/vendor/ars/shared/ground_truth_isolation_pattern.md +275 -0
  999. package/vendor/ars/shared/handoff_schemas.md +1209 -0
  1000. package/vendor/ars/shared/human_subjects_authority_registry.json +1278 -0
  1001. package/vendor/ars/shared/mode_spectrum.md +57 -0
  1002. package/vendor/ars/shared/model_tiering.md +83 -0
  1003. package/vendor/ars/shared/policy_data/nature_policy.md +56 -0
  1004. package/vendor/ars/shared/prisma_trAIce_protocol.md +157 -0
  1005. package/vendor/ars/shared/raise_framework.md +129 -0
  1006. package/vendor/ars/shared/references/authority_content_coverage_advisory_protocol.md +275 -0
  1007. package/vendor/ars/shared/references/claim_standing_candidate_ledger_protocol.md +66 -0
  1008. package/vendor/ars/shared/references/claim_strength_ladder.md +93 -0
  1009. package/vendor/ars/shared/references/cross_document_consistency_advisory_protocol.md +263 -0
  1010. package/vendor/ars/shared/references/evidence_row_protocol.md +260 -0
  1011. package/vendor/ars/shared/references/firm_rules.md +90 -0
  1012. package/vendor/ars/shared/references/human_subjects_authority_protocol.md +274 -0
  1013. package/vendor/ars/shared/references/intent_clarification_protocol.md +168 -0
  1014. package/vendor/ars/shared/references/irb_terminology_glossary.md +229 -0
  1015. package/vendor/ars/shared/references/protected_hedging_phrases.md +118 -0
  1016. package/vendor/ars/shared/references/psychometric_terminology_glossary.md +109 -0
  1017. package/vendor/ars/shared/references/review_criteria_consumer_protocol.md +238 -0
  1018. package/vendor/ars/shared/references/review_pathway_rule_trace_protocol.md +166 -0
  1019. package/vendor/ars/shared/references/submission_packet_manifest_protocol.md +292 -0
  1020. package/vendor/ars/shared/references/word_count_conventions.md +124 -0
  1021. package/vendor/ars/shared/research_workflow_profiles/field_general.json +1 -0
  1022. package/vendor/ars/shared/review_criteria_registry.json +207 -0
  1023. package/vendor/ars/shared/review_criteria_sources/msr-2027-technical-papers.2026-08-24.json +46 -0
  1024. package/vendor/ars/shared/review_criteria_sources/sigsoft-empirical-standards.2026-08-24.json +31 -0
  1025. package/vendor/ars/shared/sprint_contract.schema.json +482 -0
  1026. package/vendor/ars/shared/style_calibration_protocol.md +151 -0
  1027. package/vendor/ars/shared/templates/codex_audit_multifile_template.md +263 -0
  1028. package/vendor/ars/tools/release-discipline/.toolkit-version +1 -0
  1029. package/vendor/ars/tools/release-discipline/README.md +4 -0
  1030. package/vendor/ars/tools/release-discipline/scripts/_release_doc_alignment_schema.py +1011 -0
  1031. package/vendor/ars/tools/release-discipline/scripts/check_command_invariants.py +497 -0
  1032. package/vendor/ars/tools/release-discipline/scripts/check_release_doc_alignment.py +263 -0
  1033. package/vendor/ars/tools/release-discipline/scripts/sync-toolkit.sh +147 -0
  1034. package/vendor/windows/NOTICE.md +12 -0
  1035. package/vendor/windows/arm64/fd.exe +0 -0
  1036. package/vendor/windows/arm64/licenses/fd/LICENSE-APACHE +201 -0
  1037. package/vendor/windows/arm64/licenses/fd/LICENSE-MIT +21 -0
  1038. package/vendor/windows/arm64/licenses/ripgrep/COPYING +3 -0
  1039. package/vendor/windows/arm64/licenses/ripgrep/LICENSE-MIT +21 -0
  1040. package/vendor/windows/arm64/licenses/ripgrep/UNLICENSE +24 -0
  1041. package/vendor/windows/arm64/rg.exe +0 -0
  1042. package/vendor/windows/x64/fd.exe +0 -0
  1043. package/vendor/windows/x64/licenses/fd/LICENSE-APACHE +201 -0
  1044. package/vendor/windows/x64/licenses/fd/LICENSE-MIT +21 -0
  1045. package/vendor/windows/x64/licenses/ripgrep/COPYING +3 -0
  1046. package/vendor/windows/x64/licenses/ripgrep/LICENSE-MIT +21 -0
  1047. package/vendor/windows/x64/licenses/ripgrep/UNLICENSE +24 -0
  1048. package/vendor/windows/x64/rg.exe +0 -0
@@ -0,0 +1,1455 @@
1
+ """PDF read-integrity preflight (#512).
2
+
3
+ Guards the LOCAL EXTRACTION CHANNEL behind v3.7.3 `page` anchors: PDF readers silently
4
+ truncate documents with malformed cross-reference tables and misreport page counts, so a
5
+ real, correctly-cited source can acquire an apparently valid page locator derived from a
6
+ truncated or mispaginated read — and pass every downstream gate (the v3.7.3 lint checks
7
+ anchor shape, the #182 gate reduces anchors to a kind-only boolean). This preflight is run
8
+ at the orchestration/retrieval layer (never by Bucket A writer agents, which cannot run
9
+ Bash) BEFORE page numbers from a locally-read PDF are trusted as anchor values.
10
+
11
+ Mechanism (observed in kengo006/alexandria, reshaped per the #512 dual-track review):
12
+ three independent page-count signals must agree —
13
+
14
+ 1. declared_page_count — the root page tree's /Count, read from the raw object;
15
+ 2. enumerated_page_count — this script's own recursive /Kids walk counting /Type /Page
16
+ leaves (cycle-guarded, node-budgeted);
17
+ 3. reader_page_count — pypdf's flattened page list, as a third opinion.
18
+
19
+ Verdict: PASS only when all three agree, the count is positive, and the parse emitted no
20
+ repair warnings. FAIL when the parse completed but counts disagree (the truncation /
21
+ mispagination signal itself). UNAVAILABLE for anything the preflight cannot vouch for:
22
+ unreadable or missing file, encryption, missing/malformed page tree, a /Kids cycle or
23
+ node-budget hit, pypdf absent, or parser-repair warnings even with agreeing counts (a
24
+ repaired read may be complete, but only PASS licenses a page anchor downstream, so the
25
+ conservative bucket is the honest one).
26
+
27
+ Object plumbing rides pypdf (already a repo dependency; `verify_submission_package.py`
28
+ precedent), which handles classic xref tables, xref streams, /Prev incremental-update
29
+ chains, and object streams — this is deliberately NOT a "grep the first /Count" check.
30
+
31
+ Optional content classification is a separate, explicitly requested advisory. The
32
+ parent never imports the optional native classifier: it sends the exact bytes already
33
+ hashed here to a fixed subprocess worker, applies timeout and pipe-size ceilings, and
34
+ accepts only a closed result. The structural verdict remains structural; callers that
35
+ opt in must read the separate `content_advisory` field.
36
+
37
+ CLI: `python scripts/pdf_read_preflight.py FILE [--classify-content]
38
+ [--classifier-diagnostics LOCAL.json] [--output SIDECAR.json]`. Exit 0 whenever a
39
+ verdict was produced (the verdict is data, not an error; orchestration consumes the JSON
40
+ without exit-code branching); exit 2 on usage errors only.
41
+
42
+ Design: docs/design/2026-07-20-512-pdf-read-preflight-spec.md.
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ import argparse
48
+ import errno
49
+ import io
50
+ import json
51
+ import logging
52
+ import math
53
+ import os
54
+ import re
55
+ import secrets
56
+ import signal
57
+ import stat
58
+ import subprocess
59
+ import sys
60
+ import threading
61
+ import time
62
+ import unicodedata
63
+ from datetime import datetime, timezone
64
+ from pathlib import Path
65
+ from typing import Any
66
+
67
+ try:
68
+ from audit_snapshot import sha256_hex
69
+ except ImportError: # pragma: no cover - dual-path import (verify_submission_package precedent)
70
+ from scripts.audit_snapshot import sha256_hex
71
+
72
+ try:
73
+ import pypdf
74
+ except ImportError: # degrade to UNAVAILABLE, mirroring verify_submission_package.py
75
+ pypdf = None
76
+
77
+ TOOL_VERSION = "pdf_read_preflight/1.0.0"
78
+ CONTENT_TOOL_VERSION = "pdf_read_preflight/1.1.0"
79
+ SCHEMA = "pdf_read_preflight/1"
80
+ CONTENT_SCHEMA = "pdf_content_classification/1"
81
+ WORKER_SCHEMA = "pdf_content_classifier_worker/1"
82
+ DIAGNOSTIC_SCHEMA = "pdf_content_classifier_diagnostic/1"
83
+
84
+ CLASSIFIER_WORKER = Path(__file__).with_name("pdf_content_classifier_worker.py")
85
+ CLASSIFIER_TIMEOUT_SECONDS = 5.0
86
+ CLASSIFIER_TEARDOWN_GRACE_SECONDS = 0.2
87
+ CLASSIFIER_STDOUT_LIMIT = 8_192
88
+ CLASSIFIER_STDERR_LIMIT = 4_096
89
+ CLASSIFIER_OPERATOR_DETAIL_LIMIT = 512
90
+ CLASSIFIER_MAX_PAGE_ENTRIES = 50_000
91
+
92
+ # Hard ceiling on page-tree nodes visited by the enumeration walk. Real documents sit
93
+ # far below this; hitting it means a pathological or adversarial tree we must not vouch
94
+ # for (and must not spin on).
95
+ NODE_BUDGET = 50_000
96
+
97
+ PASS, FAIL, UNAVAILABLE = "PASS", "FAIL", "UNAVAILABLE"
98
+
99
+
100
+ class _WarningCollector(logging.Handler):
101
+ """Captures pypdf's parser chatter — repair messages ARE the silent-xref-repair
102
+ signal this preflight exists to surface."""
103
+
104
+ def __init__(self):
105
+ super().__init__(level=logging.WARNING)
106
+ self.messages: list[str] = []
107
+
108
+ def emit(self, record):
109
+ self.messages.append(record.getMessage())
110
+
111
+
112
+ class _TreeProblem(Exception):
113
+ """Structural page-tree problem that forecloses a confident enumeration."""
114
+
115
+
116
+ class _ClassifierProtocolError(Exception):
117
+ """The isolated worker did not satisfy its closed result contract."""
118
+
119
+
120
+ class _CappedPipeReader:
121
+ """Drain one worker pipe while retaining at most ``limit + 1`` bytes."""
122
+
123
+ def __init__(self, stream: Any, limit: int):
124
+ self.stream = stream
125
+ self.limit = limit
126
+ self.buffer = bytearray()
127
+ self.total = 0
128
+ self.exceeded = threading.Event()
129
+ self.done = threading.Event()
130
+ self.error: BaseException | None = None
131
+ self.thread = threading.Thread(target=self._run, daemon=True)
132
+ self.thread.start()
133
+
134
+ def _run(self) -> None:
135
+ try:
136
+ read_chunk = getattr(self.stream, "read1", self.stream.read)
137
+ while True:
138
+ # BufferedReader.read(size) may wait to fill ``size`` even when a
139
+ # worker has already flushed limit + 1 bytes and is now hanging.
140
+ # read1 returns the bytes currently available from the pipe, so the
141
+ # cap wins deterministically over the later wall timeout.
142
+ chunk = read_chunk(4_096)
143
+ if not chunk:
144
+ return
145
+ self.total += len(chunk)
146
+ remaining = self.limit + 1 - len(self.buffer)
147
+ if remaining > 0:
148
+ self.buffer.extend(chunk[:remaining])
149
+ if self.total > self.limit:
150
+ self.exceeded.set()
151
+ return
152
+ except BaseException as exc: # pragma: no cover - OS pipe failure
153
+ self.error = exc
154
+ finally:
155
+ self.done.set()
156
+
157
+ def join(self, timeout: float = 1.0) -> bool:
158
+ self.thread.join(timeout)
159
+ return not self.thread.is_alive()
160
+
161
+
162
+ class _InputWriter:
163
+ """Write the exact already-hashed PDF bytes without blocking the timeout loop."""
164
+
165
+ def __init__(self, stream: Any, data: bytes):
166
+ self.stream = stream
167
+ self.data = data
168
+ self.bytes_written = 0
169
+ self.completed = False
170
+ self.error: BaseException | None = None
171
+ self.unexpected_error: BaseException | None = None
172
+ self.thread = threading.Thread(target=self._run, daemon=True)
173
+ self.thread.start()
174
+
175
+ def _run(self) -> None:
176
+ try:
177
+ view = memoryview(self.data)
178
+ while self.bytes_written < len(view):
179
+ written = self.stream.write(view[self.bytes_written :])
180
+ if not isinstance(written, int) or written <= 0:
181
+ raise OSError("worker stdin made no forward progress")
182
+ self.bytes_written += written
183
+ self.stream.flush()
184
+ self.completed = True
185
+ except (BrokenPipeError, OSError) as exc:
186
+ # A dependency-absent worker intentionally exits without consuming
187
+ # stdin. The closed stdout result, not this expected broken pipe,
188
+ # determines whether that run is valid.
189
+ self.error = exc
190
+ except BaseException as exc: # pragma: no cover - unexpected stream failure
191
+ self.error = exc
192
+ self.unexpected_error = exc
193
+ finally:
194
+ try:
195
+ self.stream.close()
196
+ except OSError:
197
+ pass
198
+
199
+ def join(self, timeout: float = 1.0) -> bool:
200
+ self.thread.join(timeout)
201
+ return not self.thread.is_alive()
202
+
203
+
204
+ def _content_state(
205
+ *,
206
+ requested: bool,
207
+ status: str,
208
+ reason: str,
209
+ classification: str | None = None,
210
+ confidence: float | None = None,
211
+ pages_needing_ocr: list[int] | None = None,
212
+ ) -> dict[str, Any]:
213
+ return {
214
+ "schema": CONTENT_SCHEMA,
215
+ "requested": requested,
216
+ "status": status,
217
+ "reason": reason,
218
+ "classification": classification,
219
+ "confidence": confidence,
220
+ "pages_needing_ocr": pages_needing_ocr,
221
+ }
222
+
223
+
224
+ def _unavailable_content(reason: str) -> dict[str, Any]:
225
+ return _content_state(requested=True, status="UNAVAILABLE", reason=reason)
226
+
227
+
228
+ def _strict_json_object(raw: bytes) -> dict[str, Any]:
229
+ def reject_constant(value: str) -> Any:
230
+ raise ValueError(f"non-finite JSON constant {value}")
231
+
232
+ def reject_duplicate_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
233
+ result: dict[str, Any] = {}
234
+ for key, value in pairs:
235
+ if key in result:
236
+ raise _ClassifierProtocolError("worker result has duplicate keys")
237
+ result[key] = value
238
+ return result
239
+
240
+ value = json.loads(
241
+ raw.decode("utf-8", errors="strict"),
242
+ parse_constant=reject_constant,
243
+ object_pairs_hook=reject_duplicate_keys,
244
+ )
245
+ if not isinstance(value, dict):
246
+ raise _ClassifierProtocolError("worker result is not an object")
247
+ return value
248
+
249
+
250
+ def _validate_worker_result(payload: dict[str, Any], page_count: int) -> dict[str, Any]:
251
+ expected = {
252
+ "schema",
253
+ "status",
254
+ "reason",
255
+ "classification",
256
+ "confidence",
257
+ "pages_needing_ocr",
258
+ }
259
+ if set(payload) != expected or payload.get("schema") != WORKER_SCHEMA:
260
+ raise _ClassifierProtocolError("worker result has unknown or missing fields")
261
+
262
+ status = payload.get("status")
263
+ reason = payload.get("reason")
264
+ classification = payload.get("classification")
265
+ confidence = payload.get("confidence")
266
+ pages = payload.get("pages_needing_ocr")
267
+
268
+ if not isinstance(status, str):
269
+ raise _ClassifierProtocolError("worker status is not a string")
270
+
271
+ if status == "UNAVAILABLE":
272
+ if not isinstance(reason, str) or reason not in {
273
+ "DEPENDENCY_ABSENT",
274
+ "CLASSIFIER_ERROR",
275
+ "INVALID_CLASSIFIER_RESULT",
276
+ }:
277
+ raise _ClassifierProtocolError("unknown unavailable reason")
278
+ if classification is not None or confidence is not None or pages is not None:
279
+ raise _ClassifierProtocolError("unavailable worker result carries values")
280
+ return _unavailable_content(reason)
281
+
282
+ if status != "CLASSIFIED" or not isinstance(reason, str) or reason != "CLASSIFIED":
283
+ raise _ClassifierProtocolError("invalid worker state transition")
284
+ if not isinstance(classification, str) or classification not in {
285
+ "TEXT_AVAILABLE",
286
+ "OCR_RECOMMENDED",
287
+ }:
288
+ raise _ClassifierProtocolError("unknown classification")
289
+ if isinstance(confidence, bool) or not isinstance(confidence, (int, float)):
290
+ raise _ClassifierProtocolError("confidence is not finite and bounded")
291
+ try:
292
+ normalized_confidence = float(confidence)
293
+ except (TypeError, ValueError, OverflowError) as exc:
294
+ raise _ClassifierProtocolError(
295
+ "confidence is not finite and bounded"
296
+ ) from exc
297
+ if (
298
+ not math.isfinite(normalized_confidence)
299
+ or not 0.0 <= normalized_confidence <= 1.0
300
+ ):
301
+ raise _ClassifierProtocolError("confidence is not finite and bounded")
302
+ if not isinstance(pages, list) or len(pages) > CLASSIFIER_MAX_PAGE_ENTRIES:
303
+ raise _ClassifierProtocolError("OCR page list is not bounded")
304
+ if any(isinstance(page, bool) or not isinstance(page, int) for page in pages):
305
+ raise _ClassifierProtocolError("OCR page is not an integer")
306
+ if pages != sorted(set(pages)):
307
+ raise _ClassifierProtocolError("OCR pages are not sorted and unique")
308
+ if any(page < 0 or page >= page_count for page in pages):
309
+ raise _ClassifierProtocolError("OCR page is outside the structural page count")
310
+ if classification == "TEXT_AVAILABLE" and pages:
311
+ raise _ClassifierProtocolError("text-available result carries OCR pages")
312
+
313
+ return _content_state(
314
+ requested=True,
315
+ status="CLASSIFIED",
316
+ reason="CLASSIFIED",
317
+ classification=classification,
318
+ confidence=normalized_confidence,
319
+ pages_needing_ocr=pages,
320
+ )
321
+
322
+
323
+ def _bounded_operator_detail(raw: bytes) -> str:
324
+ # Dropping a partial/invalid trailing code unit preserves the byte ceiling;
325
+ # replacement decoding could expand one clipped byte into a three-byte U+FFFD.
326
+ return raw[:CLASSIFIER_OPERATOR_DETAIL_LIMIT].decode("utf-8", errors="ignore")
327
+
328
+
329
+ def _diagnostic(
330
+ reason: str,
331
+ *,
332
+ detail: bytes = b"",
333
+ stdout_bytes: int = 0,
334
+ stderr_bytes: int = 0,
335
+ ) -> dict[str, Any]:
336
+ return {
337
+ "schema": DIAGNOSTIC_SCHEMA,
338
+ "reason": reason,
339
+ "untrusted_detail": _bounded_operator_detail(detail),
340
+ "stdout_bytes_observed": stdout_bytes,
341
+ "stderr_bytes_observed": stderr_bytes,
342
+ }
343
+
344
+
345
+ def _kill_worker(proc: subprocess.Popen[bytes]) -> None:
346
+ """Terminate the isolated POSIX worker group, or the direct Windows worker.
347
+
348
+ ``start_new_session`` makes the POSIX worker its process-group leader, so the
349
+ saved ``proc.pid`` remains the group identifier even after the leader exits.
350
+ Calling this on every terminal path also removes ordinary descendants that
351
+ inherited the worker's pipes. The portable Windows stdlib path has no
352
+ equivalent process-tree handle and therefore kills only the direct process.
353
+ """
354
+ try:
355
+ if os.name == "posix":
356
+ os.killpg(proc.pid, signal.SIGKILL)
357
+ else: # pragma: no cover - exercised on Windows CI only
358
+ proc.kill()
359
+ except OSError:
360
+ try:
361
+ proc.kill()
362
+ except OSError:
363
+ pass
364
+
365
+
366
+ def _teardown_worker(
367
+ proc: subprocess.Popen[bytes],
368
+ *,
369
+ stdout_reader: _CappedPipeReader | None = None,
370
+ stderr_reader: _CappedPipeReader | None = None,
371
+ input_writer: _InputWriter | None = None,
372
+ ) -> None:
373
+ """Best-effort cleanup within one small, shared post-deadline grace budget."""
374
+ deadline = time.monotonic() + CLASSIFIER_TEARDOWN_GRACE_SECONDS
375
+
376
+ def remaining() -> float:
377
+ return max(0.0, deadline - time.monotonic())
378
+
379
+ _kill_worker(proc)
380
+ try:
381
+ proc.wait(timeout=remaining())
382
+ except (OSError, subprocess.TimeoutExpired):
383
+ _kill_worker(proc)
384
+ for helper in (input_writer, stdout_reader, stderr_reader):
385
+ if helper is not None:
386
+ helper.join(remaining())
387
+
388
+
389
+ def _run_content_classifier(
390
+ data: bytes,
391
+ *,
392
+ page_count: int,
393
+ worker_path: Path = CLASSIFIER_WORKER,
394
+ timeout: float = CLASSIFIER_TIMEOUT_SECONDS,
395
+ worker_env: dict[str, str] | None = None,
396
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
397
+ command = [sys.executable, str(worker_path)]
398
+ stdout_reader: _CappedPipeReader | None = None
399
+ stderr_reader: _CappedPipeReader | None = None
400
+ input_writer: _InputWriter | None = None
401
+ try:
402
+ proc = subprocess.Popen(
403
+ command,
404
+ stdin=subprocess.PIPE,
405
+ stdout=subprocess.PIPE,
406
+ stderr=subprocess.PIPE,
407
+ shell=False,
408
+ env=worker_env,
409
+ start_new_session=(os.name == "posix"),
410
+ )
411
+ except OSError as exc:
412
+ reason = "WORKER_LAUNCH_ERROR"
413
+ detail = f"{type(exc).__name__}: {exc}".encode("utf-8", errors="replace")
414
+ return _unavailable_content(reason), _diagnostic(reason, detail=detail)
415
+
416
+ try:
417
+ deadline = time.monotonic() + timeout
418
+
419
+ def remaining() -> float:
420
+ return max(0.0, deadline - time.monotonic())
421
+
422
+ if proc.stdin is None or proc.stdout is None or proc.stderr is None: # pragma: no cover
423
+ reason = "WORKER_IO_ERROR"
424
+ return _unavailable_content(reason), _diagnostic(reason)
425
+
426
+ try:
427
+ stdout_reader = _CappedPipeReader(proc.stdout, CLASSIFIER_STDOUT_LIMIT)
428
+ stderr_reader = _CappedPipeReader(proc.stderr, CLASSIFIER_STDERR_LIMIT)
429
+ input_writer = _InputWriter(proc.stdin, data)
430
+ except Exception as exc:
431
+ reason = "WORKER_IO_ERROR"
432
+ detail = f"{type(exc).__name__}: {exc}".encode("utf-8", errors="replace")
433
+ return _unavailable_content(reason), _diagnostic(reason, detail=detail)
434
+
435
+ forced_reason: str | None = None
436
+
437
+ returncode: int | None = None
438
+ while True:
439
+ # Poll first, then immediately observe the clock. A poll that blocked
440
+ # or otherwise returned only at/after the boundary is a late-observed
441
+ # exit and must never be accepted as an on-time worker result.
442
+ returncode = proc.poll()
443
+ observed_at = time.monotonic()
444
+ if observed_at >= deadline:
445
+ forced_reason = "WORKER_TIMEOUT"
446
+ break
447
+ if returncode is not None:
448
+ break
449
+ if stdout_reader.exceeded.is_set():
450
+ forced_reason = "WORKER_STDOUT_LIMIT"
451
+ break
452
+ if stderr_reader.exceeded.is_set():
453
+ forced_reason = "WORKER_STDERR_LIMIT"
454
+ break
455
+ time.sleep(0.005)
456
+
457
+ # The direct worker may exit while a descendant still owns inherited pipe
458
+ # handles. Kill the isolated POSIX group before any join so those handles
459
+ # cannot extend the execution deadline. On Windows this is direct-worker
460
+ # cleanup only; no process-tree claim is made.
461
+ _kill_worker(proc)
462
+ if returncode is None and remaining() > 0:
463
+ try:
464
+ returncode = proc.wait(timeout=remaining())
465
+ except subprocess.TimeoutExpired:
466
+ if forced_reason is None:
467
+ forced_reason = "WORKER_TIMEOUT"
468
+
469
+ stdout_closed = stdout_reader.join(remaining())
470
+ stderr_closed = stderr_reader.join(remaining())
471
+ input_closed = input_writer.join(remaining())
472
+ io_closed = stdout_closed and stderr_closed and input_closed
473
+ stdout = bytes(stdout_reader.buffer)
474
+ stderr = bytes(stderr_reader.buffer)
475
+
476
+ # A reader can observe limit + 1 only after the timeout loop has selected
477
+ # WORKER_TIMEOUT. The retained byte evidence is more specific, so cap
478
+ # breaches always take precedence once all helper threads have joined.
479
+ if stdout_reader.exceeded.is_set():
480
+ forced_reason = "WORKER_STDOUT_LIMIT"
481
+ elif stderr_reader.exceeded.is_set():
482
+ forced_reason = "WORKER_STDERR_LIMIT"
483
+ elif forced_reason is None:
484
+ if (
485
+ not io_closed
486
+ or stdout_reader.error
487
+ or stderr_reader.error
488
+ or input_writer.unexpected_error
489
+ ):
490
+ forced_reason = "WORKER_IO_ERROR"
491
+
492
+ if forced_reason is not None:
493
+ return _unavailable_content(forced_reason), _diagnostic(
494
+ forced_reason,
495
+ detail=stderr,
496
+ stdout_bytes=stdout_reader.total,
497
+ stderr_bytes=stderr_reader.total,
498
+ )
499
+ if returncode is None:
500
+ reason = "WORKER_TIMEOUT"
501
+ return _unavailable_content(reason), _diagnostic(
502
+ reason,
503
+ detail=stderr,
504
+ stdout_bytes=stdout_reader.total,
505
+ stderr_bytes=stderr_reader.total,
506
+ )
507
+ if returncode < 0:
508
+ reason = "WORKER_SIGNAL"
509
+ return _unavailable_content(reason), _diagnostic(
510
+ reason,
511
+ detail=f"signal={-returncode}; ".encode("ascii") + stderr,
512
+ stdout_bytes=stdout_reader.total,
513
+ stderr_bytes=stderr_reader.total,
514
+ )
515
+ if returncode != 0:
516
+ reason = "WORKER_NONZERO_EXIT"
517
+ return _unavailable_content(reason), _diagnostic(
518
+ reason,
519
+ detail=f"exit={returncode}; ".encode("ascii") + stderr,
520
+ stdout_bytes=stdout_reader.total,
521
+ stderr_bytes=stderr_reader.total,
522
+ )
523
+
524
+ try:
525
+ payload = _strict_json_object(stdout)
526
+ except (UnicodeError, ValueError, RecursionError, _ClassifierProtocolError):
527
+ reason = "WORKER_MALFORMED_OUTPUT"
528
+ return _unavailable_content(reason), _diagnostic(
529
+ reason,
530
+ detail=stderr,
531
+ stdout_bytes=stdout_reader.total,
532
+ stderr_bytes=stderr_reader.total,
533
+ )
534
+ try:
535
+ state = _validate_worker_result(payload, page_count)
536
+ except _ClassifierProtocolError:
537
+ reason = "WORKER_INVALID_OUTPUT"
538
+ return _unavailable_content(reason), _diagnostic(
539
+ reason,
540
+ detail=stderr,
541
+ stdout_bytes=stdout_reader.total,
542
+ stderr_bytes=stderr_reader.total,
543
+ )
544
+ if state["status"] == "CLASSIFIED" and not input_writer.completed:
545
+ reason = "WORKER_IO_ERROR"
546
+ return _unavailable_content(reason), _diagnostic(
547
+ reason,
548
+ detail=stderr,
549
+ stdout_bytes=stdout_reader.total,
550
+ stderr_bytes=stderr_reader.total,
551
+ )
552
+ return state, _diagnostic(
553
+ state["reason"],
554
+ detail=stderr,
555
+ stdout_bytes=stdout_reader.total,
556
+ stderr_bytes=stderr_reader.total,
557
+ )
558
+ finally:
559
+ # A successful direct child can still leave descendants behind. POSIX
560
+ # cleanup therefore targets its isolated process group on every return,
561
+ # including reasons discovered only after reader/writer joins.
562
+ _teardown_worker(
563
+ proc,
564
+ stdout_reader=stdout_reader,
565
+ stderr_reader=stderr_reader,
566
+ input_writer=input_writer,
567
+ )
568
+
569
+
570
+ def _resolved_cli_path(path: str | os.PathLike[str]) -> Path:
571
+ """Resolve literal, ``..``, and existing-symlink aliases without requiring a leaf."""
572
+ return Path(path).resolve(strict=False)
573
+
574
+
575
+ def _canonical_cli_path_key(path: str | os.PathLike[str]) -> str:
576
+ """Conservative key for case-insensitive and canonically equivalent filesystems."""
577
+ absolute = os.path.abspath(os.fspath(path))
578
+ return unicodedata.normalize("NFC", absolute).casefold()
579
+
580
+
581
+ def _existing_paths_share_inode_or_error(
582
+ parser: argparse.ArgumentParser,
583
+ left: str | os.PathLike[str],
584
+ right: str | os.PathLike[str],
585
+ ) -> bool:
586
+ """Detect hard-link aliases; only an absent leaf is safe to treat as distinct."""
587
+ try:
588
+ return os.path.samefile(left, right)
589
+ except OSError as exc:
590
+ if exc.errno == errno.ENOENT:
591
+ return False
592
+ parser.error(f"cannot safely compare CLI paths: {exc}")
593
+
594
+
595
+ def _reject_cli_write_aliases(
596
+ parser: argparse.ArgumentParser,
597
+ *,
598
+ input_path: str | os.PathLike[str],
599
+ output_path: str | os.PathLike[str] | None,
600
+ diagnostic_path: str | os.PathLike[str] | None,
601
+ ) -> None:
602
+ """Reject any CLI write target that aliases the input or the other target."""
603
+ # The legacy stdout-only CLI must not gain a path-resolution precondition. In
604
+ # particular, an input symlink loop remains an exit-0 UNAVAILABLE verdict.
605
+ if output_path is None and diagnostic_path is None:
606
+ return
607
+
608
+ write_paths = [
609
+ path for path in (output_path, diagnostic_path) if path is not None
610
+ ]
611
+ try:
612
+ resolved_writes = {path: _resolved_cli_path(path) for path in write_paths}
613
+ except (OSError, RuntimeError) as exc:
614
+ parser.error(f"cannot safely resolve CLI write paths: {exc}")
615
+
616
+ # Resolving a malformed/unreadable input is the structural preflight's job. A
617
+ # resolution failure here must not turn its data verdict into a usage error as
618
+ # long as the write targets themselves resolved safely. The lexical key still
619
+ # catches an exactly re-spelled write target.
620
+ try:
621
+ resolved_input: Path | None = _resolved_cli_path(input_path)
622
+ except (OSError, RuntimeError):
623
+ resolved_input = None
624
+
625
+ input_lexical_key = _canonical_cli_path_key(input_path)
626
+ input_resolved_key = (
627
+ _canonical_cli_path_key(resolved_input) if resolved_input is not None else None
628
+ )
629
+
630
+ for label, path in (
631
+ ("--output", output_path),
632
+ ("--classifier-diagnostics", diagnostic_path),
633
+ ):
634
+ if path is None:
635
+ continue
636
+ resolved = resolved_writes[path]
637
+ if (
638
+ _canonical_cli_path_key(path) == input_lexical_key
639
+ or (
640
+ input_resolved_key is not None
641
+ and _canonical_cli_path_key(resolved) == input_resolved_key
642
+ )
643
+ or (
644
+ resolved_input is not None
645
+ and _existing_paths_share_inode_or_error(parser, path, input_path)
646
+ )
647
+ ):
648
+ parser.error(f"{label} must not alias the input PDF")
649
+
650
+ if output_path is not None and diagnostic_path is not None:
651
+ if (
652
+ _canonical_cli_path_key(output_path)
653
+ == _canonical_cli_path_key(diagnostic_path)
654
+ or _canonical_cli_path_key(resolved_writes[output_path])
655
+ == _canonical_cli_path_key(resolved_writes[diagnostic_path])
656
+ or _existing_paths_share_inode_or_error(
657
+ parser, output_path, diagnostic_path
658
+ )
659
+ ):
660
+ parser.error("--output and --classifier-diagnostics must not alias")
661
+
662
+
663
+ def _require_open_inode_at(
664
+ opened: os.stat_result,
665
+ directory_fd: int,
666
+ name: str,
667
+ *,
668
+ require_directory: bool = False,
669
+ ) -> None:
670
+ """Fail if a dirfd-relative name no longer denotes the already-opened inode."""
671
+ current = os.stat(name, dir_fd=directory_fd, follow_symlinks=False)
672
+ expected_kind = stat.S_ISDIR if require_directory else stat.S_ISREG
673
+ if (
674
+ (opened.st_dev, opened.st_ino) != (current.st_dev, current.st_ino)
675
+ or not expected_kind(current.st_mode)
676
+ ):
677
+ raise OSError(errno.ESTALE, "anchored staging inode changed")
678
+
679
+
680
+ class _BoundSidecarOutput:
681
+ """Pre-worker parent binding and private dirfd-relative sidecar publication."""
682
+
683
+ _STAGE_PREFIX = ".ars-pdf-stage-"
684
+ _STAGE_FILE = "payload"
685
+
686
+ def __init__(self, path: Path):
687
+ self.target_name = path.name
688
+ self.parent_fd = -1
689
+ self.parent_identity: os.stat_result | None = None
690
+ self.stage_dir_name: str | None = None
691
+ self.stage_dir_fd = -1
692
+ self.stage_identity: os.stat_result | None = None
693
+ self.stage_file_fd = -1
694
+
695
+ @classmethod
696
+ def bind(cls, path: Path) -> _BoundSidecarOutput:
697
+ """Open and identify the output parent before any optional worker starts."""
698
+ bound = cls(path)
699
+ try:
700
+ if os.name != "posix":
701
+ raise OSError(
702
+ errno.ENOTSUP,
703
+ "anchored sidecar output requires POSIX dirfd operations",
704
+ )
705
+ if not bound.target_name or bound.target_name in {".", ".."}:
706
+ raise OSError(errno.EINVAL, "sidecar output needs a file basename")
707
+ resolved_parent = _resolved_cli_path(path.parent)
708
+ bound.parent_fd = os.open(
709
+ resolved_parent,
710
+ os.O_RDONLY
711
+ | getattr(os, "O_DIRECTORY", 0)
712
+ | getattr(os, "O_NOFOLLOW", 0),
713
+ )
714
+ bound.parent_identity = os.fstat(bound.parent_fd)
715
+ if not stat.S_ISDIR(bound.parent_identity.st_mode):
716
+ raise OSError(errno.ENOTDIR, "sidecar output parent is not a directory")
717
+
718
+ # A fixed-size random name is independent of the destination basename,
719
+ # so a legal 255-byte destination still leaves room for staging.
720
+ for _attempt in range(128):
721
+ candidate = cls._STAGE_PREFIX + secrets.token_hex(8)
722
+ try:
723
+ os.mkdir(candidate, 0o700, dir_fd=bound.parent_fd)
724
+ except FileExistsError:
725
+ continue
726
+ bound.stage_dir_name = candidate
727
+ break
728
+ else: # pragma: no cover - 128 random collisions are not plausible
729
+ raise OSError(errno.EEXIST, "cannot allocate private staging directory")
730
+
731
+ bound.stage_dir_fd = os.open(
732
+ bound.stage_dir_name,
733
+ os.O_RDONLY
734
+ | getattr(os, "O_DIRECTORY", 0)
735
+ | getattr(os, "O_NOFOLLOW", 0),
736
+ dir_fd=bound.parent_fd,
737
+ )
738
+ os.fchmod(bound.stage_dir_fd, 0o700)
739
+ bound.stage_identity = os.fstat(bound.stage_dir_fd)
740
+ _require_open_inode_at(
741
+ bound.stage_identity,
742
+ bound.parent_fd,
743
+ bound.stage_dir_name,
744
+ require_directory=True,
745
+ )
746
+ return bound
747
+ except BaseException:
748
+ bound.cleanup(suppress_errors=True)
749
+ raise
750
+
751
+ def publish(self, raw: bytes) -> None:
752
+ """Install the exact staged inode using only the pre-bound directory fds."""
753
+ if (
754
+ self.parent_fd < 0
755
+ or self.parent_identity is None
756
+ or self.stage_dir_fd < 0
757
+ or self.stage_identity is None
758
+ or self.stage_dir_name is None
759
+ ):
760
+ raise OSError(errno.EBADF, "sidecar output binding is incomplete")
761
+
762
+ current_parent = os.fstat(self.parent_fd)
763
+ if (current_parent.st_dev, current_parent.st_ino) != (
764
+ self.parent_identity.st_dev,
765
+ self.parent_identity.st_ino,
766
+ ): # pragma: no cover
767
+ raise OSError(errno.ESTALE, "sidecar output parent identity changed")
768
+ _require_open_inode_at(
769
+ self.stage_identity,
770
+ self.parent_fd,
771
+ self.stage_dir_name,
772
+ require_directory=True,
773
+ )
774
+
775
+ self.stage_file_fd = os.open(
776
+ self._STAGE_FILE,
777
+ os.O_WRONLY
778
+ | os.O_CREAT
779
+ | os.O_EXCL
780
+ | getattr(os, "O_NOFOLLOW", 0),
781
+ 0o600,
782
+ dir_fd=self.stage_dir_fd,
783
+ )
784
+ opened = os.fstat(self.stage_file_fd)
785
+ offset = 0
786
+ while offset < len(raw):
787
+ written = os.write(self.stage_file_fd, raw[offset:])
788
+ if written <= 0: # pragma: no cover - defensive OS contract check
789
+ raise OSError(errno.EIO, "sidecar staging write made no progress")
790
+ offset += written
791
+ os.fsync(self.stage_file_fd)
792
+
793
+ # The 0700 directory removes cross-user name access. The identity check
794
+ # additionally rejects a same-user/test-time pathname swap before replace.
795
+ _require_open_inode_at(opened, self.stage_dir_fd, self._STAGE_FILE)
796
+ os.replace(
797
+ self._STAGE_FILE,
798
+ self.target_name,
799
+ src_dir_fd=self.stage_dir_fd,
800
+ dst_dir_fd=self.parent_fd,
801
+ )
802
+ installed = os.stat(
803
+ self.target_name,
804
+ dir_fd=self.parent_fd,
805
+ follow_symlinks=False,
806
+ )
807
+ if (opened.st_dev, opened.st_ino) != (installed.st_dev, installed.st_ino):
808
+ # A same-UID actor may have swapped the fixed staging name between the
809
+ # pre-replace identity check and replace. Do not accept that inode as
810
+ # output; remove the observed installed entry before failing closed.
811
+ # Python exposes no compare-and-unlink primitive, so this is best-effort
812
+ # cleanup of the exact observed race, not a general same-UID sandbox.
813
+ try:
814
+ current = os.stat(
815
+ self.target_name,
816
+ dir_fd=self.parent_fd,
817
+ follow_symlinks=False,
818
+ )
819
+ if (current.st_dev, current.st_ino) == (
820
+ installed.st_dev,
821
+ installed.st_ino,
822
+ ):
823
+ os.unlink(self.target_name, dir_fd=self.parent_fd)
824
+ os.fsync(self.parent_fd)
825
+ except FileNotFoundError:
826
+ pass
827
+ raise OSError(errno.ESTALE, "published sidecar inode changed")
828
+ os.fsync(self.parent_fd)
829
+
830
+ def cleanup(self, *, suppress_errors: bool) -> OSError | None:
831
+ """Attempt every cleanup action independently, preserving any primary error."""
832
+ first_error: OSError | None = None
833
+
834
+ def record(action) -> None:
835
+ nonlocal first_error
836
+ try:
837
+ action()
838
+ except FileNotFoundError:
839
+ pass
840
+ except OSError as exc:
841
+ if first_error is None:
842
+ first_error = exc
843
+
844
+ if self.stage_file_fd >= 0:
845
+ fd = self.stage_file_fd
846
+ self.stage_file_fd = -1
847
+ record(lambda: os.close(fd))
848
+ if self.stage_dir_fd >= 0:
849
+ record(
850
+ lambda: os.unlink(
851
+ self._STAGE_FILE,
852
+ dir_fd=self.stage_dir_fd,
853
+ )
854
+ )
855
+ fd = self.stage_dir_fd
856
+ self.stage_dir_fd = -1
857
+ record(lambda: os.close(fd))
858
+ if self.stage_dir_name is not None and self.parent_fd >= 0:
859
+ name = self.stage_dir_name
860
+ self.stage_dir_name = None
861
+ record(lambda: os.rmdir(name, dir_fd=self.parent_fd))
862
+ if self.parent_fd >= 0:
863
+ fd = self.parent_fd
864
+ self.parent_fd = -1
865
+ record(lambda: os.close(fd))
866
+ return None if suppress_errors else first_error
867
+
868
+
869
+ def _write_local_diagnostic(path: Path, payload: dict[str, Any]) -> None:
870
+ if os.name != "posix" or not callable(getattr(os, "fchmod", None)):
871
+ raise OSError("private classifier diagnostics require POSIX fchmod")
872
+ raw = (
873
+ json.dumps(payload, indent=2, ensure_ascii=False, allow_nan=False, sort_keys=True)
874
+ + "\n"
875
+ ).encode("utf-8")
876
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0)
877
+ fd = os.open(path, flags, 0o600)
878
+ try:
879
+ os.fchmod(fd, 0o600)
880
+ offset = 0
881
+ while offset < len(raw):
882
+ offset += os.write(fd, raw[offset:])
883
+ os.fsync(fd)
884
+ finally:
885
+ os.close(fd)
886
+
887
+
888
+ class _BoundDiagnosticOutput:
889
+ """Pre-worker parent binding for the exclusive POSIX diagnostic artifact."""
890
+
891
+ def __init__(self, path: Path):
892
+ self.target_name = path.name
893
+ self.parent_fd = -1
894
+ self.parent_identity: os.stat_result | None = None
895
+
896
+ @classmethod
897
+ def bind(cls, path: Path) -> _BoundDiagnosticOutput:
898
+ bound = cls(path)
899
+ try:
900
+ if os.name != "posix" or not callable(getattr(os, "fchmod", None)):
901
+ raise OSError(
902
+ errno.ENOTSUP,
903
+ "private diagnostics require POSIX dirfd and fchmod",
904
+ )
905
+ if not bound.target_name or bound.target_name in {".", ".."}:
906
+ raise OSError(errno.EINVAL, "diagnostic needs a file basename")
907
+ resolved_parent = _resolved_cli_path(path.parent)
908
+ bound.parent_fd = os.open(
909
+ resolved_parent,
910
+ os.O_RDONLY
911
+ | getattr(os, "O_DIRECTORY", 0)
912
+ | getattr(os, "O_NOFOLLOW", 0),
913
+ )
914
+ bound.parent_identity = os.fstat(bound.parent_fd)
915
+ if not stat.S_ISDIR(bound.parent_identity.st_mode):
916
+ raise OSError(errno.ENOTDIR, "diagnostic parent is not a directory")
917
+ return bound
918
+ except BaseException:
919
+ bound.cleanup(suppress_errors=True)
920
+ raise
921
+
922
+ def publish(self, payload: dict[str, Any]) -> None:
923
+ if self.parent_fd < 0 or self.parent_identity is None:
924
+ raise OSError(errno.EBADF, "diagnostic parent binding is incomplete")
925
+ current_parent = os.fstat(self.parent_fd)
926
+ if (current_parent.st_dev, current_parent.st_ino) != (
927
+ self.parent_identity.st_dev,
928
+ self.parent_identity.st_ino,
929
+ ): # pragma: no cover - an open dirfd cannot normally change identity
930
+ raise OSError(errno.ESTALE, "diagnostic parent identity changed")
931
+
932
+ raw = (
933
+ json.dumps(
934
+ payload,
935
+ indent=2,
936
+ ensure_ascii=False,
937
+ allow_nan=False,
938
+ sort_keys=True,
939
+ )
940
+ + "\n"
941
+ ).encode("utf-8")
942
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0)
943
+ fd = -1
944
+ primary_error: BaseException | None = None
945
+ created: os.stat_result | None = None
946
+ close_succeeded = False
947
+ try:
948
+ fd = os.open(
949
+ self.target_name,
950
+ flags,
951
+ 0o600,
952
+ dir_fd=self.parent_fd,
953
+ )
954
+ # Bind cleanup authority to the inode actually created by this fd.
955
+ # A later no-follow leaf comparison may remove only this inode, never
956
+ # an attacker replacement installed under the same pathname.
957
+ created = os.fstat(fd)
958
+ os.fchmod(fd, 0o600)
959
+ offset = 0
960
+ while offset < len(raw):
961
+ written = os.write(fd, raw[offset:])
962
+ if written <= 0: # pragma: no cover - defensive OS contract check
963
+ raise OSError(errno.EIO, "diagnostic write made no progress")
964
+ offset += written
965
+ os.fsync(fd)
966
+ except BaseException as exc:
967
+ primary_error = exc
968
+ finally:
969
+ if fd >= 0:
970
+ try:
971
+ os.close(fd)
972
+ close_succeeded = True
973
+ except OSError as exc:
974
+ if primary_error is None:
975
+ primary_error = exc
976
+
977
+ if primary_error is None:
978
+ try:
979
+ os.fsync(self.parent_fd)
980
+ except OSError as exc:
981
+ primary_error = exc
982
+
983
+ if primary_error is not None:
984
+ # If close itself failed it may or may not already have closed the fd.
985
+ # Retry independently; EBADF or another cleanup error must not replace
986
+ # the primary fchmod/write/fsync/close/publication error.
987
+ if fd >= 0 and not close_succeeded:
988
+ try:
989
+ os.close(fd)
990
+ except OSError:
991
+ pass
992
+
993
+ if created is not None:
994
+ try:
995
+ current = os.stat(
996
+ self.target_name,
997
+ dir_fd=self.parent_fd,
998
+ follow_symlinks=False,
999
+ )
1000
+ if (current.st_dev, current.st_ino) == (
1001
+ created.st_dev,
1002
+ created.st_ino,
1003
+ ):
1004
+ os.unlink(self.target_name, dir_fd=self.parent_fd)
1005
+ except (FileNotFoundError, OSError):
1006
+ # Cleanup is best-effort and never changes the primary error.
1007
+ pass
1008
+ try:
1009
+ os.fsync(self.parent_fd)
1010
+ except OSError:
1011
+ pass
1012
+ raise primary_error
1013
+
1014
+ def cleanup(self, *, suppress_errors: bool) -> OSError | None:
1015
+ if self.parent_fd < 0:
1016
+ return None
1017
+ fd = self.parent_fd
1018
+ self.parent_fd = -1
1019
+ try:
1020
+ os.close(fd)
1021
+ except OSError as exc:
1022
+ return None if suppress_errors else exc
1023
+ return None
1024
+
1025
+
1026
+ def _kid_key(kid):
1027
+ """Stable identity for a /Kids entry (indirect ref when available)."""
1028
+ ref = getattr(kid, "indirect_reference", None) or (
1029
+ kid if hasattr(kid, "idnum") else None
1030
+ )
1031
+ if ref is not None:
1032
+ return ("ref", ref.idnum, ref.generation)
1033
+ return ("id", id(kid))
1034
+
1035
+
1036
+ def _walk_page_tree(node, visited, budget):
1037
+ """Count /Type /Page leaves under `node`, guarding cycles and runaway trees."""
1038
+ count = 0
1039
+ stack = [node]
1040
+ while stack:
1041
+ if len(visited) > budget:
1042
+ raise _TreeProblem("page-tree node budget exceeded")
1043
+ current = stack.pop()
1044
+ key = _kid_key(current)
1045
+ if key in visited:
1046
+ raise _TreeProblem("page-tree cycle detected")
1047
+ visited.add(key)
1048
+ obj = current.get_object() if hasattr(current, "get_object") else current
1049
+ node_type = str(obj.get("/Type", ""))
1050
+ if node_type == "/Page":
1051
+ count += 1
1052
+ elif node_type == "/Pages":
1053
+ kids = obj.get("/Kids", [])
1054
+ stack.extend(kids)
1055
+ else:
1056
+ raise _TreeProblem(f"unexpected page-tree node type {node_type or '(none)'}")
1057
+ return count
1058
+
1059
+
1060
+ def _run_structural_preflight(path) -> tuple[dict[str, Any], bytes | None]:
1061
+ """Run the unchanged #512 structural preflight and retain its exact input bytes."""
1062
+ path = Path(path)
1063
+ result = {
1064
+ "schema": SCHEMA,
1065
+ "verdict": UNAVAILABLE,
1066
+ "file": str(path),
1067
+ "sha256": None,
1068
+ "declared_page_count": None,
1069
+ "enumerated_page_count": None,
1070
+ "reader_page_count": None,
1071
+ "warnings": [],
1072
+ "generated_at": datetime.now(timezone.utc).isoformat(),
1073
+ "tool": TOOL_VERSION,
1074
+ }
1075
+ warnings = result["warnings"]
1076
+
1077
+ try:
1078
+ data = path.read_bytes()
1079
+ except OSError as exc:
1080
+ warnings.append(f"unreadable: {exc}")
1081
+ return result, None
1082
+ result["sha256"] = sha256_hex(data)
1083
+
1084
+ # Structural check independent of the parser: a PDF truncated partway through an
1085
+ # incremental update keeps an OLDER valid %%EOF, and pypdf silently reads that
1086
+ # previous revision — all three counts then agree on the OLD page tree, which would
1087
+ # PASS the exact truncation case this preflight exists to catch (codex #512 P1).
1088
+ # Non-whitespace bytes after the LAST %%EOF are that signature: record the warning
1089
+ # now, veto PASS at the verdict step. A complete incremental update always ends
1090
+ # with its own %%EOF, so legitimate multi-revision files are not flagged.
1091
+ # PDF whitespace per ISO 32000 §7.2.2 — NOT Python's: NUL is whitespace (common
1092
+ # padding after %%EOF, must not veto), vertical tab 0x0B is NOT (r3 P1).
1093
+ _PDF_WS = b"\x00\x09\x0a\x0c\x0d\x20"
1094
+ trailing_ok = True
1095
+ eof_at = data.rfind(b"%%EOF")
1096
+ if eof_at != -1 and data[eof_at + 5 :].translate(None, _PDF_WS):
1097
+ trailing_ok = False
1098
+ warnings.append(
1099
+ f"trailing-data: {len(data) - (eof_at + 5)} bytes after the final %%EOF "
1100
+ "include non-whitespace content (possible truncated incremental update)"
1101
+ )
1102
+
1103
+ if pypdf is None:
1104
+ warnings.append("pypdf-not-installed: preflight cannot parse the document")
1105
+ return result, data
1106
+
1107
+ collector = _WarningCollector()
1108
+ pypdf_logger = logging.getLogger("pypdf")
1109
+ pypdf_logger.addHandler(collector)
1110
+ try:
1111
+ try:
1112
+ reader = pypdf.PdfReader(io.BytesIO(data)) # bytes already in hand for the hash
1113
+ except Exception as exc: # malformed beyond pypdf's tolerance
1114
+ warnings.append(f"parse-error: {exc}")
1115
+ return result, data
1116
+
1117
+ if getattr(reader, "is_encrypted", False):
1118
+ warnings.append("encrypted: preflight cannot verify an encrypted document")
1119
+ return result, data
1120
+
1121
+ try:
1122
+ root = reader.trailer["/Root"].get_object()
1123
+ pages_node = root["/Pages"]
1124
+ pages_obj = pages_node.get_object()
1125
+ raw_count = pages_obj["/Count"]
1126
+ # Require an actual PDF integer object. `int()` would coerce a float
1127
+ # /Count 2.7 to 2 (or a text string "2") and then agree with two real
1128
+ # leaves — a malformed page tree must be UNAVAILABLE, not PASS (r2 P1).
1129
+ # pypdf NumberObject subclasses int; FloatObject subclasses float.
1130
+ if isinstance(raw_count, bool) or not isinstance(raw_count, int):
1131
+ warnings.append(
1132
+ f"page-tree-unresolvable: /Count is not an integer object "
1133
+ f"({type(raw_count).__name__}: {raw_count!r})"
1134
+ )
1135
+ return result, data
1136
+ declared = int(raw_count)
1137
+ except Exception as exc:
1138
+ warnings.append(f"page-tree-unresolvable: {exc}")
1139
+ return result, data
1140
+ result["declared_page_count"] = declared
1141
+
1142
+ try:
1143
+ enumerated = _walk_page_tree(pages_node, set(), NODE_BUDGET)
1144
+ except Exception as exc: # incl. _TreeProblem — same degradation either way
1145
+ warnings.append(f"page-tree-walk: {exc}")
1146
+ return result, data
1147
+ result["enumerated_page_count"] = enumerated
1148
+
1149
+ # The walk above verified the /Kids tree is cycle-free, so flattening the same
1150
+ # tree cannot spin.
1151
+ try:
1152
+ reader_count = len(reader.pages)
1153
+ except Exception as exc:
1154
+ warnings.append(f"reader-page-list: {exc}")
1155
+ return result, data
1156
+ result["reader_page_count"] = reader_count
1157
+
1158
+ # Xref-coverage check (r2 P1): a malformed incremental update can append new
1159
+ # objects PLUS a syntactically complete startxref that still points at the
1160
+ # PREVIOUS revision's xref, followed by its own %%EOF — the trailing-data
1161
+ # check then sees nothing after the final %%EOF while pypdf silently reads
1162
+ # the old revision. Cross-check: every raw `N M obj` header in the file must
1163
+ # be an object number the parsed xref chain knows about. An unreferenced
1164
+ # object number = a revision the active xref chain cannot see. (Offsets are
1165
+ # deliberately not compared — pypdf normalizes them; object-number coverage
1166
+ # is the stable signal. Best-effort: if pypdf's xref internals are absent,
1167
+ # skip rather than crash.)
1168
+ try:
1169
+ xref_map = getattr(reader, "xref", None)
1170
+ if isinstance(xref_map, dict) and xref_map:
1171
+ known_objs = set()
1172
+ for gen_table in xref_map.values():
1173
+ if isinstance(gen_table, dict):
1174
+ known_objs.update(gen_table.keys())
1175
+ compressed = getattr(reader, "xref_objStm", None)
1176
+ if isinstance(compressed, dict):
1177
+ known_objs.update(compressed.keys())
1178
+ # Header token separators implement the FULL ISO 32000 lexer model,
1179
+ # not Python's \s and not just whitespace: PDF permits bare-CR line
1180
+ # endings (r4 P1), treats NUL as whitespace (r5 P1), and treats
1181
+ # %-comments-to-end-of-line as token separators (r7 P1) — so
1182
+ # `2 0%note\nobj` is a valid header. Anything the PDF lexer accepts
1183
+ # as a separator must not hide a header from the coverage checks.
1184
+ # Numeric tokens carry the full ISO 32000 integer form too (r8 P1):
1185
+ # an optional sign and any leading-zero padding are valid and
1186
+ # accepted by pypdf's int() coercion, so `+2 0 obj` or
1187
+ # `00000000002 0 obj` must not hide from the scan either.
1188
+ _ws = rb"[\x00\t\n\x0c\r ]"
1189
+ _sep = rb"(?:" + _ws + rb"|%[^\r\n]*[\r\n])"
1190
+ _num = rb"[+-]?0*\d{1,10}"
1191
+ raw_offsets: dict[int, list[int]] = {}
1192
+ for m in re.finditer(
1193
+ rb"(?:^|" + _sep + rb")" + _sep + rb"*(" + _num + rb")" + _sep + rb"+" + _num + _sep + rb"+obj\b",
1194
+ data,
1195
+ ):
1196
+ raw_offsets.setdefault(int(m.group(1)), []).append(m.start(1))
1197
+ orphaned = set(raw_offsets) - {int(n) for n in known_objs}
1198
+ if orphaned:
1199
+ warnings.append(
1200
+ "xref-coverage: object number(s) "
1201
+ f"{sorted(orphaned)[:5]} present in the file but absent from "
1202
+ "the active xref chain (possible stale startxref / "
1203
+ "unreachable newer revision)"
1204
+ )
1205
+ trailing_ok = False
1206
+ # Redefined-object variant (r3 P1): a malformed update can append a
1207
+ # REPLACEMENT body for an existing object number plus a stale
1208
+ # startxref — number-membership alone then sees no orphan while
1209
+ # pypdf reads the old copy. The newest raw copy of every directly-
1210
+ # stored object must be the one the active chain references.
1211
+ # Calibration guard: pypdf applies a global delta when a file has
1212
+ # junk before %PDF; if NO active offset matches any raw offset the
1213
+ # comparison is uncalibrated — skip rather than mass-flag.
1214
+ direct_offsets = {}
1215
+ for gen_table in xref_map.values():
1216
+ if isinstance(gen_table, dict):
1217
+ for objnum, off in gen_table.items():
1218
+ if isinstance(off, int) and int(objnum) in raw_offsets:
1219
+ direct_offsets[int(objnum)] = off
1220
+ if direct_offsets and any(
1221
+ off in raw_offsets[n] for n, off in direct_offsets.items()
1222
+ ):
1223
+ superseded = sorted(
1224
+ n
1225
+ for n, off in direct_offsets.items()
1226
+ if max(raw_offsets[n]) > off
1227
+ )
1228
+ if superseded:
1229
+ warnings.append(
1230
+ "xref-coverage: later unreferenced revision(s) of object "
1231
+ f"number(s) {superseded[:5]} exist after the copy the "
1232
+ "active xref chain references (possible stale startxref)"
1233
+ )
1234
+ trailing_ok = False
1235
+ # Compressed-object variant (r5 P1): the active copy of N lives
1236
+ # inside an object stream (no direct offset in reader.xref), so the
1237
+ # loop above never inspects it — but a direct raw replacement of N
1238
+ # appended AFTER its container, with a stale startxref, is exactly
1239
+ # the unreachable-newer-revision case. A raw copy BEFORE the
1240
+ # container is the legitimate superseded-into-objstm update and is
1241
+ # not flagged.
1242
+ if isinstance(compressed, dict):
1243
+ compressed_superseded = []
1244
+ for objnum, ref in compressed.items():
1245
+ n = int(objnum)
1246
+ if n not in raw_offsets or n in direct_offsets:
1247
+ continue
1248
+ container = ref[0] if isinstance(ref, (tuple, list)) and ref else None
1249
+ container_off = None
1250
+ if container is not None:
1251
+ for gen_table in xref_map.values():
1252
+ if (
1253
+ isinstance(gen_table, dict)
1254
+ and container in gen_table
1255
+ and isinstance(gen_table[container], int)
1256
+ ):
1257
+ container_off = gen_table[container]
1258
+ break
1259
+ if container_off is not None and max(raw_offsets[n]) > container_off:
1260
+ compressed_superseded.append(n)
1261
+ if compressed_superseded:
1262
+ warnings.append(
1263
+ "xref-coverage: direct replacement(s) of compressed object "
1264
+ f"number(s) {sorted(compressed_superseded)[:5]} appear after "
1265
+ "their object-stream container (possible stale startxref)"
1266
+ )
1267
+ trailing_ok = False
1268
+ except Exception as exc: # best-effort cross-check, never a crash path
1269
+ warnings.append(f"xref-coverage-skipped: {exc}")
1270
+ finally:
1271
+ pypdf_logger.removeHandler(collector)
1272
+ # Append captured parser chatter HERE so every early return above (encryption,
1273
+ # unresolvable tree, walk problems) still carries it — the repair warning that
1274
+ # preceded a later structural error is part of the sidecar contract too.
1275
+ warnings.extend(f"pypdf: {m}" for m in collector.messages)
1276
+
1277
+ if not (declared == enumerated == reader_count):
1278
+ result["verdict"] = FAIL
1279
+ return result, data
1280
+ if declared <= 0:
1281
+ warnings.append("empty-page-tree: agreeing counts but zero pages")
1282
+ return result, data
1283
+ if collector.messages or not trailing_ok:
1284
+ # Counts agree, but the parse needed repair or the file carries data after its
1285
+ # final %%EOF — cannot vouch, per the spec.
1286
+ return result, data
1287
+ result["verdict"] = PASS
1288
+ return result, data
1289
+
1290
+
1291
+ def _run_preflight(
1292
+ path: str | Path,
1293
+ *,
1294
+ classify_content: bool = False,
1295
+ worker_path: Path = CLASSIFIER_WORKER,
1296
+ classifier_timeout: float = CLASSIFIER_TIMEOUT_SECONDS,
1297
+ worker_env: dict[str, str] | None = None,
1298
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
1299
+ result, data = _run_structural_preflight(path)
1300
+ diagnostic = _diagnostic("NOT_REQUESTED")
1301
+
1302
+ if not classify_content:
1303
+ return result, diagnostic
1304
+ result["tool"] = CONTENT_TOOL_VERSION
1305
+ result["verdict_scope"] = "STRUCTURE_ONLY"
1306
+ result["content_advisory"] = "CONTENT_UNAVAILABLE"
1307
+ result["content_classification"] = _unavailable_content("STRUCTURAL_NOT_PASS")
1308
+ if result["verdict"] != PASS or data is None:
1309
+ reason = "STRUCTURAL_NOT_PASS"
1310
+ result["content_advisory"] = "STRUCTURAL_UNAVAILABLE"
1311
+ result["content_classification"] = _unavailable_content(reason)
1312
+ return result, _diagnostic(reason)
1313
+
1314
+ page_count = result["reader_page_count"]
1315
+ if isinstance(page_count, bool) or not isinstance(page_count, int) or page_count <= 0:
1316
+ # Defensive: PASS already proves this cannot happen, but do not invoke a
1317
+ # native child if that structural invariant ever drifts.
1318
+ reason = "STRUCTURAL_NOT_PASS"
1319
+ result["content_advisory"] = "STRUCTURAL_UNAVAILABLE"
1320
+ result["content_classification"] = _unavailable_content(reason)
1321
+ return result, _diagnostic(reason)
1322
+
1323
+ state, diagnostic = _run_content_classifier(
1324
+ data,
1325
+ page_count=page_count,
1326
+ worker_path=worker_path,
1327
+ timeout=classifier_timeout,
1328
+ worker_env=worker_env,
1329
+ )
1330
+ result["content_classification"] = state
1331
+ if state["status"] == "CLASSIFIED":
1332
+ result["content_advisory"] = state["classification"]
1333
+ else:
1334
+ result["content_advisory"] = "CONTENT_UNAVAILABLE"
1335
+ return result, diagnostic
1336
+
1337
+
1338
+ def run_preflight(path, *, classify_content: bool = False) -> dict[str, Any]:
1339
+ """Return a structural sidecar, optionally with isolated content advisory data."""
1340
+ result, _diagnostic_payload = _run_preflight(
1341
+ path,
1342
+ classify_content=classify_content,
1343
+ )
1344
+ return result
1345
+
1346
+
1347
+ def main(argv=None) -> int:
1348
+ parser = argparse.ArgumentParser(
1349
+ description="PDF read-integrity preflight (#512): PASS/FAIL/UNAVAILABLE sidecar "
1350
+ "for page-anchor trust decisions."
1351
+ )
1352
+ parser.add_argument("pdf", help="path to the locally-read PDF")
1353
+ parser.add_argument(
1354
+ "--output",
1355
+ help="write the JSON sidecar here instead of stdout",
1356
+ )
1357
+ parser.add_argument(
1358
+ "--classify-content",
1359
+ action="store_true",
1360
+ help=(
1361
+ "opt in to the isolated optional text/OCR advisory; the structural "
1362
+ "verdict remains unchanged"
1363
+ ),
1364
+ )
1365
+ parser.add_argument(
1366
+ "--classifier-diagnostics",
1367
+ help=(
1368
+ "POSIX-only exclusive local 0600 JSON for bounded untrusted worker "
1369
+ "detail; requires --classify-content and is never referenced by the "
1370
+ "sidecar"
1371
+ ),
1372
+ )
1373
+ args = parser.parse_args(argv)
1374
+
1375
+ if args.classifier_diagnostics and not args.classify_content:
1376
+ parser.error("--classifier-diagnostics requires --classify-content")
1377
+ if args.classifier_diagnostics and (
1378
+ os.name != "posix" or not callable(getattr(os, "fchmod", None))
1379
+ ):
1380
+ parser.error("--classifier-diagnostics requires POSIX fchmod")
1381
+
1382
+ # File-writing invocations reject conservative spelling, resolved-path, and
1383
+ # existing-inode collisions before structural parsing or worker launch. The
1384
+ # helper deliberately no-ops for the legacy stdout-only invocation.
1385
+ _reject_cli_write_aliases(
1386
+ parser,
1387
+ input_path=args.pdf,
1388
+ output_path=args.output,
1389
+ diagnostic_path=args.classifier_diagnostics,
1390
+ )
1391
+
1392
+ bound_output: _BoundSidecarOutput | None = None
1393
+ bound_diagnostic: _BoundDiagnosticOutput | None = None
1394
+ try:
1395
+ if args.output:
1396
+ # Bind the actual parent directory and allocate its private staging
1397
+ # directory before any optional worker starts.
1398
+ bound_output = _BoundSidecarOutput.bind(Path(args.output))
1399
+ if args.classifier_diagnostics:
1400
+ bound_diagnostic = _BoundDiagnosticOutput.bind(
1401
+ Path(args.classifier_diagnostics)
1402
+ )
1403
+ except OSError as exc:
1404
+ if bound_diagnostic is not None:
1405
+ bound_diagnostic.cleanup(suppress_errors=True)
1406
+ if bound_output is not None:
1407
+ bound_output.cleanup(suppress_errors=True)
1408
+ parser.error(f"cannot bind CLI output: {exc}")
1409
+
1410
+ try:
1411
+ result, diagnostic = _run_preflight(
1412
+ args.pdf,
1413
+ classify_content=args.classify_content,
1414
+ )
1415
+ if bound_diagnostic is not None:
1416
+ try:
1417
+ bound_diagnostic.publish(diagnostic)
1418
+ except OSError as exc:
1419
+ parser.error(f"cannot create classifier diagnostic: {exc}")
1420
+
1421
+ sidecar = json.dumps(
1422
+ result,
1423
+ indent=2,
1424
+ ensure_ascii=False,
1425
+ allow_nan=False,
1426
+ )
1427
+ if bound_output is not None:
1428
+ try:
1429
+ bound_output.publish((sidecar + "\n").encode("utf-8"))
1430
+ except OSError as exc:
1431
+ parser.error(f"cannot publish sidecar output: {exc}")
1432
+ else:
1433
+ print(sidecar)
1434
+ except BaseException:
1435
+ if bound_diagnostic is not None:
1436
+ bound_diagnostic.cleanup(suppress_errors=True)
1437
+ if bound_output is not None:
1438
+ bound_output.cleanup(suppress_errors=True)
1439
+ raise
1440
+
1441
+ if bound_diagnostic is not None:
1442
+ cleanup_error = bound_diagnostic.cleanup(suppress_errors=False)
1443
+ if cleanup_error is not None:
1444
+ if bound_output is not None:
1445
+ bound_output.cleanup(suppress_errors=True)
1446
+ parser.error(f"cannot clean diagnostic binding: {cleanup_error}")
1447
+ if bound_output is not None:
1448
+ cleanup_error = bound_output.cleanup(suppress_errors=False)
1449
+ if cleanup_error is not None:
1450
+ parser.error(f"cannot clean sidecar staging: {cleanup_error}")
1451
+ return 0
1452
+
1453
+
1454
+ if __name__ == "__main__":
1455
+ sys.exit(main())