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.
- package/README.md +11 -5
- package/dist/apps/panel/index.html +2 -2
- package/dist/src/adapters/pi/extension.js +225 -92
- package/dist/src/adapters/pi/extension.js.map +1 -1
- package/dist/src/agents/import.js +44 -32
- package/dist/src/agents/import.js.map +1 -1
- package/dist/src/analysis/hooks.d.ts +1 -0
- package/dist/src/analysis/hooks.js +20 -1
- package/dist/src/analysis/hooks.js.map +1 -1
- package/dist/src/ars/bridge.d.ts +20 -0
- package/dist/src/ars/bridge.js +132 -0
- package/dist/src/ars/bridge.js.map +1 -0
- package/dist/src/ars/contracts.d.ts +55 -0
- package/dist/src/ars/contracts.js +2 -0
- package/dist/src/ars/contracts.js.map +1 -0
- package/dist/src/ars/panel-plan.d.ts +4 -0
- package/dist/src/ars/panel-plan.js +11 -0
- package/dist/src/ars/panel-plan.js.map +1 -0
- package/dist/src/ars/pi-panel-executor.d.ts +30 -0
- package/dist/src/ars/pi-panel-executor.js +140 -0
- package/dist/src/ars/pi-panel-executor.js.map +1 -0
- package/dist/src/ars/profile.d.ts +36 -0
- package/dist/src/ars/profile.js +119 -0
- package/dist/src/ars/profile.js.map +1 -0
- package/dist/src/ars/re-review.d.ts +22 -0
- package/dist/src/ars/re-review.js +172 -0
- package/dist/src/ars/re-review.js.map +1 -0
- package/dist/src/branding.d.ts +2 -4
- package/dist/src/branding.js +3 -5
- package/dist/src/branding.js.map +1 -1
- package/dist/src/bundled-tools.d.ts +3 -0
- package/dist/src/bundled-tools.js +21 -0
- package/dist/src/bundled-tools.js.map +1 -0
- package/dist/src/chat.js +6 -5
- package/dist/src/chat.js.map +1 -1
- package/dist/src/creation/contracts.d.ts +38 -0
- package/dist/src/creation/contracts.js +2 -0
- package/dist/src/creation/contracts.js.map +1 -0
- package/dist/src/creation/service.d.ts +7 -0
- package/dist/src/creation/service.js +196 -0
- package/dist/src/creation/service.js.map +1 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +10 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/install/installer.js +32 -10
- package/dist/src/install/installer.js.map +1 -1
- package/dist/src/orchestration/personas.d.ts +15 -0
- package/dist/src/orchestration/personas.js +51 -0
- package/dist/src/orchestration/personas.js.map +1 -0
- package/dist/src/orchestration/pi-executor.d.ts +1 -0
- package/dist/src/orchestration/pi-executor.js +1 -1
- package/dist/src/orchestration/pi-executor.js.map +1 -1
- package/dist/src/panel/extension.js +29 -8
- package/dist/src/panel/extension.js.map +1 -1
- package/dist/src/panel/server.js +14 -5
- package/dist/src/panel/server.js.map +1 -1
- package/dist/src/project/paths.d.ts +3 -0
- package/dist/src/project/paths.js +7 -0
- package/dist/src/project/paths.js.map +1 -1
- package/dist/src/rules/user-rules.d.ts +8 -0
- package/dist/src/rules/user-rules.js +36 -0
- package/dist/src/rules/user-rules.js.map +1 -0
- package/dist/src/skills/contracts.d.ts +4 -4
- package/dist/src/skills/recommended.js +1 -1
- package/dist/src/skills/registry.js +49 -28
- package/dist/src/skills/registry.js.map +1 -1
- package/dist/src/style/cli-ui.js +0 -1
- package/dist/src/style/cli-ui.js.map +1 -1
- package/package.json +14 -3
- package/scripts/rebrand-pi.mjs +6 -0
- package/skills/recommended/catalog.json +2 -11
- package/vendor/ars/.claude/CLAUDE.md +371 -0
- package/vendor/ars/.command-invariants.toml +24 -0
- package/vendor/ars/CITATION.cff +35 -0
- package/vendor/ars/LICENSE +417 -0
- package/vendor/ars/MODE_REGISTRY.md +76 -0
- package/vendor/ars/NOTICE.md +26 -0
- package/vendor/ars/POSITIONING.md +99 -0
- package/vendor/ars/PSYCLAW_SOURCE.json +10 -0
- package/vendor/ars/README.md +751 -0
- package/vendor/ars/SECURITY.md +52 -0
- package/vendor/ars/THIRD_PARTY.md +70 -0
- package/vendor/ars/academic-paper/SKILL.md +542 -0
- package/vendor/ars/academic-paper/agents/abstract_bilingual_agent.md +171 -0
- package/vendor/ars/academic-paper/agents/argument_builder_agent.md +276 -0
- package/vendor/ars/academic-paper/agents/citation_compliance_agent.md +422 -0
- package/vendor/ars/academic-paper/agents/draft_writer_agent.md +656 -0
- package/vendor/ars/academic-paper/agents/formatter_agent.md +999 -0
- package/vendor/ars/academic-paper/agents/intake_agent.md +393 -0
- package/vendor/ars/academic-paper/agents/literature_strategist_agent.md +626 -0
- package/vendor/ars/academic-paper/agents/peer_reviewer_agent.md +516 -0
- package/vendor/ars/academic-paper/agents/revision_coach_agent.md +334 -0
- package/vendor/ars/academic-paper/agents/socratic_mentor_agent.md +527 -0
- package/vendor/ars/academic-paper/agents/structure_architect_agent.md +401 -0
- package/vendor/ars/academic-paper/agents/visualization_agent.md +441 -0
- package/vendor/ars/academic-paper/examples/chinese_paper_example.md +278 -0
- package/vendor/ars/academic-paper/examples/clinical_citation_verification_checklist.md +95 -0
- package/vendor/ars/academic-paper/examples/clinical_epistemic_status_example.md +100 -0
- package/vendor/ars/academic-paper/examples/commitment_ledger_example.md +147 -0
- package/vendor/ars/academic-paper/examples/imrad_hei_example.md +234 -0
- package/vendor/ars/academic-paper/examples/literature_review_example.md +260 -0
- package/vendor/ars/academic-paper/examples/plan_mode_guided_writing.md +600 -0
- package/vendor/ars/academic-paper/examples/revision_mode_example.md +344 -0
- package/vendor/ars/academic-paper/examples/revision_recovery_example.md +506 -0
- package/vendor/ars/academic-paper/examples/version_family_reconciliation_example.md +89 -0
- package/vendor/ars/academic-paper/references/abstract_writing_guide.md +169 -0
- package/vendor/ars/academic-paper/references/academic_writing_style.md +188 -0
- package/vendor/ars/academic-paper/references/anti_leakage_protocol.md +83 -0
- package/vendor/ars/academic-paper/references/apa7_chinese_citation_guide.md +364 -0
- package/vendor/ars/academic-paper/references/apa7_extended_guide.md +198 -0
- package/vendor/ars/academic-paper/references/changelog.md +11 -0
- package/vendor/ars/academic-paper/references/citation_format_switcher.md +228 -0
- package/vendor/ars/academic-paper/references/committee_correspondence_protocol.md +158 -0
- package/vendor/ars/academic-paper/references/credit_authorship_guide.md +308 -0
- package/vendor/ars/academic-paper/references/disclosure_mode_protocol.md +478 -0
- package/vendor/ars/academic-paper/references/domain_evidence_profiles.md +38 -0
- package/vendor/ars/academic-paper/references/failure_paths.md +349 -0
- package/vendor/ars/academic-paper/references/funding_statement_guide.md +319 -0
- package/vendor/ars/academic-paper/references/hei_domain_glossary.md +169 -0
- package/vendor/ars/academic-paper/references/intro_title_rhetoric_guide.md +114 -0
- package/vendor/ars/academic-paper/references/journal_submission_guide.md +249 -0
- package/vendor/ars/academic-paper/references/latex_template_reference.md +378 -0
- package/vendor/ars/academic-paper/references/mode_selection_guide.md +378 -0
- package/vendor/ars/academic-paper/references/paper_structure_patterns.md +330 -0
- package/vendor/ars/academic-paper/references/plan_mode_protocol.md +112 -0
- package/vendor/ars/academic-paper/references/policy_anchor_disclosure_protocol.md +200 -0
- package/vendor/ars/academic-paper/references/policy_anchor_table.md +157 -0
- package/vendor/ars/academic-paper/references/revision_patch_protocol.md +173 -0
- package/vendor/ars/academic-paper/references/statistical_visualization_standards.md +750 -0
- package/vendor/ars/academic-paper/references/venue_disclosure_policies.md +259 -0
- package/vendor/ars/academic-paper/references/vlm_figure_verification.md +126 -0
- package/vendor/ars/academic-paper/references/workflow_phase_details.md +135 -0
- package/vendor/ars/academic-paper/references/writing_judgment_framework.md +59 -0
- package/vendor/ars/academic-paper/references/writing_quality_check.md +173 -0
- package/vendor/ars/academic-paper/templates/bilingual_abstract_template.md +78 -0
- package/vendor/ars/academic-paper/templates/case_study_template.md +129 -0
- package/vendor/ars/academic-paper/templates/conference_paper_template.md +108 -0
- package/vendor/ars/academic-paper/templates/credit_statement_template.md +132 -0
- package/vendor/ars/academic-paper/templates/funding_statement_template.md +290 -0
- package/vendor/ars/academic-paper/templates/imrad_template.md +183 -0
- package/vendor/ars/academic-paper/templates/latex_article_template.tex +199 -0
- package/vendor/ars/academic-paper/templates/literature_review_template.md +135 -0
- package/vendor/ars/academic-paper/templates/policy_brief_template.md +139 -0
- package/vendor/ars/academic-paper/templates/revision_tracking_template.md +199 -0
- package/vendor/ars/academic-paper/templates/theoretical_paper_template.md +119 -0
- package/vendor/ars/academic-paper-reviewer/SKILL.md +491 -0
- package/vendor/ars/academic-paper-reviewer/agents/devils_advocate_reviewer_agent.md +443 -0
- package/vendor/ars/academic-paper-reviewer/agents/domain_reviewer_agent.md +412 -0
- package/vendor/ars/academic-paper-reviewer/agents/editorial_synthesizer_agent.md +478 -0
- package/vendor/ars/academic-paper-reviewer/agents/eic_agent.md +339 -0
- package/vendor/ars/academic-paper-reviewer/agents/field_analyst_agent.md +221 -0
- package/vendor/ars/academic-paper-reviewer/agents/methodology_reviewer_agent.md +449 -0
- package/vendor/ars/academic-paper-reviewer/agents/perspective_reviewer_agent.md +427 -0
- package/vendor/ars/academic-paper-reviewer/examples/hei_paper_review_example.md +391 -0
- package/vendor/ars/academic-paper-reviewer/examples/interdisciplinary_review_example.md +299 -0
- package/vendor/ars/academic-paper-reviewer/examples/subclaim_decomposition_example.md +80 -0
- package/vendor/ars/academic-paper-reviewer/references/calibration_mode_protocol.md +256 -0
- package/vendor/ars/academic-paper-reviewer/references/changelog.md +10 -0
- package/vendor/ars/academic-paper-reviewer/references/editorial_decision_standards.md +236 -0
- package/vendor/ars/academic-paper-reviewer/references/guided_mode_protocol.md +34 -0
- package/vendor/ars/academic-paper-reviewer/references/integration_guide.md +15 -0
- package/vendor/ars/academic-paper-reviewer/references/quality_rubrics.md +84 -0
- package/vendor/ars/academic-paper-reviewer/references/re_review_mode_protocol.md +340 -0
- package/vendor/ars/academic-paper-reviewer/references/review_criteria_framework.md +98 -0
- package/vendor/ars/academic-paper-reviewer/references/review_panel_provenance_protocol.md +197 -0
- package/vendor/ars/academic-paper-reviewer/references/review_quality_thinking.md +58 -0
- package/vendor/ars/academic-paper-reviewer/references/reviewer_sprint_prompt_source.md +324 -0
- package/vendor/ars/academic-paper-reviewer/references/sprint_contract_protocol.md +296 -0
- package/vendor/ars/academic-paper-reviewer/references/statistical_reporting_standards.md +505 -0
- package/vendor/ars/academic-paper-reviewer/references/top_journals_by_field.md +206 -0
- package/vendor/ars/academic-paper-reviewer/templates/editorial_decision_template.md +235 -0
- package/vendor/ars/academic-paper-reviewer/templates/peer_review_report_template.md +305 -0
- package/vendor/ars/academic-paper-reviewer/templates/revision_response_template.md +248 -0
- package/vendor/ars/academic-pipeline/SKILL.md +736 -0
- package/vendor/ars/academic-pipeline/agents/claim_ref_alignment_audit_agent.md +382 -0
- package/vendor/ars/academic-pipeline/agents/collaboration_depth_agent.md +164 -0
- package/vendor/ars/academic-pipeline/agents/integrity_verification_agent.md +870 -0
- package/vendor/ars/academic-pipeline/agents/pipeline_orchestrator_agent.md +1379 -0
- package/vendor/ars/academic-pipeline/agents/state_tracker_agent.md +622 -0
- package/vendor/ars/academic-pipeline/examples/full_pipeline_example.md +482 -0
- package/vendor/ars/academic-pipeline/examples/integrity_failure_recovery.md +389 -0
- package/vendor/ars/academic-pipeline/examples/mid_entry_example.md +414 -0
- package/vendor/ars/academic-pipeline/references/adapters/.gitkeep +0 -0
- package/vendor/ars/academic-pipeline/references/adapters/overview.md +153 -0
- package/vendor/ars/academic-pipeline/references/ai_research_failure_modes.md +185 -0
- package/vendor/ars/academic-pipeline/references/changelog.md +14 -0
- package/vendor/ars/academic-pipeline/references/claim_audit_calibration_protocol.md +175 -0
- package/vendor/ars/academic-pipeline/references/claim_verification_protocol.md +282 -0
- package/vendor/ars/academic-pipeline/references/external_review_protocol.md +131 -0
- package/vendor/ars/academic-pipeline/references/integrity_review_protocol.md +110 -0
- package/vendor/ars/academic-pipeline/references/literature_corpus_consumers.md +193 -0
- package/vendor/ars/academic-pipeline/references/mode_advisor.md +135 -0
- package/vendor/ars/academic-pipeline/references/passport_as_reset_boundary.md +132 -0
- package/vendor/ars/academic-pipeline/references/pipeline_state_machine.md +405 -0
- package/vendor/ars/academic-pipeline/references/plagiarism_detection_protocol.md +239 -0
- package/vendor/ars/academic-pipeline/references/process_summary_protocol.md +209 -0
- package/vendor/ars/academic-pipeline/references/progress_dashboard_template.md +38 -0
- package/vendor/ars/academic-pipeline/references/reinforcement_content.md +15 -0
- package/vendor/ars/academic-pipeline/references/reproducibility_audit.md +55 -0
- package/vendor/ars/academic-pipeline/references/score_trajectory_protocol.md +78 -0
- package/vendor/ars/academic-pipeline/references/team_collaboration_protocol.md +261 -0
- package/vendor/ars/academic-pipeline/references/two_stage_review_protocol.md +27 -0
- package/vendor/ars/academic-pipeline/templates/pipeline_status_template.md +146 -0
- package/vendor/ars/agents/report_compiler_agent.md +341 -0
- package/vendor/ars/agents/research_architect_agent.md +298 -0
- package/vendor/ars/agents/synthesis_agent.md +356 -0
- package/vendor/ars/commands/ars-3w.md +10 -0
- package/vendor/ars/commands/ars-abstract.md +10 -0
- package/vendor/ars/commands/ars-cache-invalidate.md +20 -0
- package/vendor/ars/commands/ars-citation-check.md +10 -0
- package/vendor/ars/commands/ars-disclosure.md +10 -0
- package/vendor/ars/commands/ars-format-convert.md +10 -0
- package/vendor/ars/commands/ars-full.md +9 -0
- package/vendor/ars/commands/ars-lit-review.md +12 -0
- package/vendor/ars/commands/ars-mark-read.md +18 -0
- package/vendor/ars/commands/ars-outline.md +10 -0
- package/vendor/ars/commands/ars-plan.md +10 -0
- package/vendor/ars/commands/ars-rebuttal-audit.md +12 -0
- package/vendor/ars/commands/ars-reviewer.md +9 -0
- package/vendor/ars/commands/ars-revision-coach.md +9 -0
- package/vendor/ars/commands/ars-revision.md +10 -0
- package/vendor/ars/commands/ars-unmark-read.md +16 -0
- package/vendor/ars/deep-research/SKILL.md +600 -0
- package/vendor/ars/deep-research/agents/bibliography_agent.md +473 -0
- package/vendor/ars/deep-research/agents/devils_advocate_agent.md +192 -0
- package/vendor/ars/deep-research/agents/editor_in_chief_agent.md +167 -0
- package/vendor/ars/deep-research/agents/ethics_review_agent.md +267 -0
- package/vendor/ars/deep-research/agents/meta_analysis_agent.md +325 -0
- package/vendor/ars/deep-research/agents/monitoring_agent.md +209 -0
- package/vendor/ars/deep-research/agents/report_compiler_agent.md +341 -0
- package/vendor/ars/deep-research/agents/research_architect_agent.md +298 -0
- package/vendor/ars/deep-research/agents/research_question_agent.md +216 -0
- package/vendor/ars/deep-research/agents/risk_of_bias_agent.md +231 -0
- package/vendor/ars/deep-research/agents/socratic_mentor_agent.md +764 -0
- package/vendor/ars/deep-research/agents/source_verification_agent.md +219 -0
- package/vendor/ars/deep-research/agents/synthesis_agent.md +356 -0
- package/vendor/ars/deep-research/agents/timeline_extraction_agent.md +99 -0
- package/vendor/ars/deep-research/examples/exploratory_research.md +157 -0
- package/vendor/ars/deep-research/examples/fact_check_mode.md +173 -0
- package/vendor/ars/deep-research/examples/handoff_to_paper.md +318 -0
- package/vendor/ars/deep-research/examples/idea_diversity_coverage_gap_advisory.md +64 -0
- package/vendor/ars/deep-research/examples/policy_analysis.md +161 -0
- package/vendor/ars/deep-research/examples/review_mode.md +253 -0
- package/vendor/ars/deep-research/examples/socratic_guided_research.md +331 -0
- package/vendor/ars/deep-research/examples/systematic_review.md +133 -0
- package/vendor/ars/deep-research/references/apa7_style_guide.md +162 -0
- package/vendor/ars/deep-research/references/argumentation_reasoning_framework.md +68 -0
- package/vendor/ars/deep-research/references/arxiv_api_protocol.md +76 -0
- package/vendor/ars/deep-research/references/changelog.md +22 -0
- package/vendor/ars/deep-research/references/chinese_literature_api_protocol.md +317 -0
- package/vendor/ars/deep-research/references/cross_agent_quality_definitions.md +14 -0
- package/vendor/ars/deep-research/references/crossref_api_protocol.md +84 -0
- package/vendor/ars/deep-research/references/equator_reporting_guidelines.md +482 -0
- package/vendor/ars/deep-research/references/ethics_checklist.md +282 -0
- package/vendor/ars/deep-research/references/failure_paths.md +355 -0
- package/vendor/ars/deep-research/references/interdisciplinary_bridges.md +292 -0
- package/vendor/ars/deep-research/references/irb_decision_tree.md +315 -0
- package/vendor/ars/deep-research/references/literature_monitoring_strategies.md +263 -0
- package/vendor/ars/deep-research/references/logical_fallacies.md +192 -0
- package/vendor/ars/deep-research/references/methodology_patterns.md +462 -0
- package/vendor/ars/deep-research/references/mode_selection_guide.md +331 -0
- package/vendor/ars/deep-research/references/openalex_api_protocol.md +82 -0
- package/vendor/ars/deep-research/references/preregistration_guide.md +324 -0
- package/vendor/ars/deep-research/references/semantic_scholar_api_protocol.md +107 -0
- package/vendor/ars/deep-research/references/socratic_mode_protocol.md +99 -0
- package/vendor/ars/deep-research/references/socratic_questioning_framework.md +232 -0
- package/vendor/ars/deep-research/references/source_quality_hierarchy.md +188 -0
- package/vendor/ars/deep-research/references/systematic_review_protocol.md +95 -0
- package/vendor/ars/deep-research/references/systematic_review_toolkit.md +353 -0
- package/vendor/ars/deep-research/templates/evidence_assessment_template.md +127 -0
- package/vendor/ars/deep-research/templates/literature_matrix_template.md +85 -0
- package/vendor/ars/deep-research/templates/preregistration_template.md +318 -0
- package/vendor/ars/deep-research/templates/prisma_protocol_template.md +248 -0
- package/vendor/ars/deep-research/templates/prisma_report_template.md +415 -0
- package/vendor/ars/deep-research/templates/research_brief_template.md +93 -0
- package/vendor/ars/package.json +24 -0
- package/vendor/ars/pi/README.md +161 -0
- package/vendor/ars/pi/package.json +26 -0
- package/vendor/ars/pi/wrapper.js +193 -0
- package/vendor/ars/pi/wrapper.test.mjs +201 -0
- package/vendor/ars/pyproject.toml +2 -0
- package/vendor/ars/requirements-pdf-content-classifier.txt +5 -0
- package/vendor/ars/scripts/_block_parser.py +396 -0
- package/vendor/ars/scripts/_ci_pytest_manifest.toml +661 -0
- package/vendor/ars/scripts/_claim_audit_constants.py +268 -0
- package/vendor/ars/scripts/_e4_evidence.py +110 -0
- package/vendor/ars/scripts/_eval_threshold_gate.py +73 -0
- package/vendor/ars/scripts/_markdown_lint_util.py +224 -0
- package/vendor/ars/scripts/_next_verified_at_ms.py +176 -0
- package/vendor/ars/scripts/_passport_yaml.py +53 -0
- package/vendor/ars/scripts/_skill_lint.py +254 -0
- package/vendor/ars/scripts/_text_similarity.py +141 -0
- package/vendor/ars/scripts/adapters/README.md +89 -0
- package/vendor/ars/scripts/adapters/_common.py +209 -0
- package/vendor/ars/scripts/adapters/examples/folder_scan/expected_passport.yaml +25 -0
- package/vendor/ars/scripts/adapters/examples/folder_scan/expected_rejection_log.yaml +18 -0
- package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/Chen2024_AIAssessment.pdf +0 -0
- package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/Wang_2023_formative_feedback.pdf +0 -0
- package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/paper1.pdf +0 -0
- 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
- package/vendor/ars/scripts/adapters/examples/obsidian/expected_passport.yaml +40 -0
- package/vendor/ars/scripts/adapters/examples/obsidian/expected_rejection_log.yaml +13 -0
- package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/.gitkeep +0 -0
- package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/.obsidian/app.json +1 -0
- package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/_templates/tmpl.md +7 -0
- package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/chen2024ai.md +13 -0
- package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/invalid.md +1 -0
- package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/wang2023formative.md +10 -0
- package/vendor/ars/scripts/adapters/examples/zotero/expected_passport.yaml +34 -0
- package/vendor/ars/scripts/adapters/examples/zotero/expected_rejection_log.yaml +34 -0
- package/vendor/ars/scripts/adapters/examples/zotero/input_fixture/.gitkeep +0 -0
- package/vendor/ars/scripts/adapters/examples/zotero/input_fixture/export.json +43 -0
- package/vendor/ars/scripts/adapters/folder_scan.py +214 -0
- package/vendor/ars/scripts/adapters/obsidian.py +336 -0
- package/vendor/ars/scripts/adapters/tests/.gitkeep +0 -0
- package/vendor/ars/scripts/adapters/tests/conftest.py +77 -0
- package/vendor/ars/scripts/adapters/tests/test_check_corpus_consumer_protocol.py +632 -0
- package/vendor/ars/scripts/adapters/tests/test_check_literature_corpus_schema.py +631 -0
- package/vendor/ars/scripts/adapters/tests/test_common.py +365 -0
- package/vendor/ars/scripts/adapters/tests/test_conftest.py +96 -0
- package/vendor/ars/scripts/adapters/tests/test_folder_scan.py +260 -0
- package/vendor/ars/scripts/adapters/tests/test_literature_corpus_entry_schema.py +745 -0
- package/vendor/ars/scripts/adapters/tests/test_obsidian.py +357 -0
- package/vendor/ars/scripts/adapters/tests/test_rejection_log_schema.py +271 -0
- package/vendor/ars/scripts/adapters/tests/test_sync_adapter_docs.py +88 -0
- package/vendor/ars/scripts/adapters/tests/test_zotero.py +454 -0
- package/vendor/ars/scripts/adapters/zotero.py +318 -0
- package/vendor/ars/scripts/adjudication_activity.py +1592 -0
- package/vendor/ars/scripts/announce-ars-loaded.sh +144 -0
- package/vendor/ars/scripts/ars_anchorize_draft.py +170 -0
- package/vendor/ars/scripts/ars_apply_revision_patch.py +912 -0
- package/vendor/ars/scripts/ars_cache_invalidate.py +40 -0
- package/vendor/ars/scripts/ars_mark_read.py +521 -0
- package/vendor/ars/scripts/ars_phase_scope_manifest.json +33 -0
- package/vendor/ars/scripts/ars_update_check.sh +215 -0
- package/vendor/ars/scripts/ars_write_scope_guard.py +506 -0
- package/vendor/ars/scripts/arxiv_client.py +222 -0
- package/vendor/ars/scripts/audit_snapshot.py +572 -0
- package/vendor/ars/scripts/bibliographic_integrity_signals.py +800 -0
- package/vendor/ars/scripts/bootstrap_timeline_yaml.py +146 -0
- package/vendor/ars/scripts/build_claim_standing_candidate_ledger.py +1238 -0
- package/vendor/ars/scripts/build_claim_standing_query_plan.py +643 -0
- package/vendor/ars/scripts/build_content_coverage_advisory.py +1205 -0
- package/vendor/ars/scripts/build_cross_document_consistency_advisory.py +2332 -0
- package/vendor/ars/scripts/build_review_pathway_rule_trace.py +830 -0
- package/vendor/ars/scripts/build_submission_packet_manifest.py +2310 -0
- package/vendor/ars/scripts/check_215_field_norm.py +173 -0
- package/vendor/ars/scripts/check_216_surface_form.py +250 -0
- package/vendor/ars/scripts/check_268_nested_commitment_ledger.py +180 -0
- package/vendor/ars/scripts/check_390_revision_patch_discipline.py +296 -0
- package/vendor/ars/scripts/check_392_citation_verification_intake.py +126 -0
- package/vendor/ars/scripts/check_394_submission_policy.py +178 -0
- package/vendor/ars/scripts/check_439_format_profile.py +307 -0
- package/vendor/ars/scripts/check_619_disclosure_closeout.py +237 -0
- package/vendor/ars/scripts/check_630_codex_subscription_transport.py +456 -0
- package/vendor/ars/scripts/check_669_review_pathway_rule_trace.py +662 -0
- package/vendor/ars/scripts/check_670_revision_roadmap_integration.py +518 -0
- package/vendor/ars/scripts/check_673_adjudication_activity.py +684 -0
- package/vendor/ars/scripts/check_684_review_criteria_binding.py +557 -0
- package/vendor/ars/scripts/check_agents_mirror_sync.py +115 -0
- package/vendor/ars/scripts/check_audit_artifact_consistency.py +2313 -0
- package/vendor/ars/scripts/check_benchmark_report.py +79 -0
- package/vendor/ars/scripts/check_bibliographic_integrity_signals.py +831 -0
- package/vendor/ars/scripts/check_calibration_tiers.py +235 -0
- package/vendor/ars/scripts/check_changelog_covers_merges.py +289 -0
- package/vendor/ars/scripts/check_ci_pytest_manifest.py +204 -0
- package/vendor/ars/scripts/check_claim_audit_consistency.py +1664 -0
- package/vendor/ars/scripts/check_claim_standing_candidate_ledger_integration.py +500 -0
- package/vendor/ars/scripts/check_claim_standing_freshness.py +253 -0
- package/vendor/ars/scripts/check_claim_standing_transmissions.py +449 -0
- package/vendor/ars/scripts/check_collaboration_depth_rubric.py +180 -0
- package/vendor/ars/scripts/check_command_frontmatter_name.py +116 -0
- package/vendor/ars/scripts/check_committee_correspondence.py +333 -0
- package/vendor/ars/scripts/check_compliance_report.py +108 -0
- package/vendor/ars/scripts/check_content_coverage_advisory_integration.py +796 -0
- package/vendor/ars/scripts/check_control_availability.py +172 -0
- package/vendor/ars/scripts/check_corpus_consumer_protocol.py +404 -0
- package/vendor/ars/scripts/check_cross_document_consistency_advisory_integration.py +1191 -0
- package/vendor/ars/scripts/check_cross_model_handoff_contract.py +234 -0
- package/vendor/ars/scripts/check_cross_model_verification_sync.py +261 -0
- package/vendor/ars/scripts/check_data_access_level.py +131 -0
- package/vendor/ars/scripts/check_data_flows.py +252 -0
- package/vendor/ars/scripts/check_decision_contract.py +464 -0
- package/vendor/ars/scripts/check_degradation_registry.py +326 -0
- package/vendor/ars/scripts/check_distribution_surface_claims.py +226 -0
- package/vendor/ars/scripts/check_domain_evidence_profile.py +538 -0
- package/vendor/ars/scripts/check_e4_promotion.py +195 -0
- package/vendor/ars/scripts/check_evals_gold_set.py +279 -0
- package/vendor/ars/scripts/check_evidence_row_integration.py +396 -0
- package/vendor/ars/scripts/check_experiment_provenance.py +117 -0
- package/vendor/ars/scripts/check_field_norm_severity.py +144 -0
- package/vendor/ars/scripts/check_firm_rules_sync.py +375 -0
- package/vendor/ars/scripts/check_heldout_measurement_report.py +1179 -0
- package/vendor/ars/scripts/check_human_subjects_output_contract.py +139 -0
- package/vendor/ars/scripts/check_human_subjects_reference_migration.py +844 -0
- package/vendor/ars/scripts/check_indirect_prompt_injection_no_call.py +328 -0
- package/vendor/ars/scripts/check_instruction_data_boundary.py +236 -0
- package/vendor/ars/scripts/check_judge_prompt_version.py +125 -0
- package/vendor/ars/scripts/check_literature_corpus_schema.py +402 -0
- package/vendor/ars/scripts/check_model_tiering.py +223 -0
- package/vendor/ars/scripts/check_panel_synthesis.py +1331 -0
- package/vendor/ars/scripts/check_passport_reset_contract.py +214 -0
- package/vendor/ars/scripts/check_pattern_eval_manifest.py +422 -0
- package/vendor/ars/scripts/check_persuasion_invariance_fixtures.py +637 -0
- package/vendor/ars/scripts/check_phase_conformance.py +2180 -0
- package/vendor/ars/scripts/check_pipeline_boundary_semantics.py +600 -0
- package/vendor/ars/scripts/check_pipeline_integrity.py +340 -0
- package/vendor/ars/scripts/check_policy_anchor_protocol.py +159 -0
- package/vendor/ars/scripts/check_policy_anchor_table.py +286 -0
- package/vendor/ars/scripts/check_preprint_venues_consistency.py +123 -0
- package/vendor/ars/scripts/check_prisma_trAIce_freshness.py +84 -0
- package/vendor/ars/scripts/check_promotion_bakeoff_preregistration.py +1303 -0
- package/vendor/ars/scripts/check_ranking_lift.py +323 -0
- package/vendor/ars/scripts/check_re_review_synthesis.py +2719 -0
- package/vendor/ars/scripts/check_receipt_enum_sync.py +203 -0
- package/vendor/ars/scripts/check_repro_lock.py +85 -0
- package/vendor/ars/scripts/check_review_pathway_output.py +276 -0
- package/vendor/ars/scripts/check_reviewer_data_fences.py +224 -0
- package/vendor/ars/scripts/check_reviewer_finding_contract.py +783 -0
- package/vendor/ars/scripts/check_reviewer_role_label.py +447 -0
- package/vendor/ars/scripts/check_reviewer_scoring_honesty.py +292 -0
- package/vendor/ars/scripts/check_reviewer_sprint_prompt_sync.py +409 -0
- package/vendor/ars/scripts/check_revision_claim_drift_suite_v2.py +1622 -0
- package/vendor/ars/scripts/check_revision_token_conservation.py +263 -0
- package/vendor/ars/scripts/check_risk_register.py +280 -0
- package/vendor/ars/scripts/check_role_scoped_contract.py +702 -0
- package/vendor/ars/scripts/check_rq_framing_patterns.py +184 -0
- package/vendor/ars/scripts/check_rubric_weight_consistency.py +32 -0
- package/vendor/ars/scripts/check_seeded_defect_fixtures.py +425 -0
- package/vendor/ars/scripts/check_setup_cross_model_parity.py +139 -0
- package/vendor/ars/scripts/check_spec_consistency.py +1249 -0
- package/vendor/ars/scripts/check_sprint_contract.py +371 -0
- package/vendor/ars/scripts/check_stage_capability_matrix.py +784 -0
- package/vendor/ars/scripts/check_submission_packet_manifest_integration.py +358 -0
- package/vendor/ars/scripts/check_surface_form_parity.py +445 -0
- package/vendor/ars/scripts/check_task_type.py +22 -0
- package/vendor/ars/scripts/check_tools_allowlist.py +538 -0
- package/vendor/ars/scripts/check_tortured_phrase_screening_integration.py +1974 -0
- package/vendor/ars/scripts/check_v3_10_134_write_scope.py +286 -0
- package/vendor/ars/scripts/check_v3_10_policy.py +656 -0
- package/vendor/ars/scripts/check_v3_6_6_ab_manifest.py +364 -0
- package/vendor/ars/scripts/check_v3_6_7_pattern_protection.py +1366 -0
- package/vendor/ars/scripts/check_v3_6_8_audit_scope_block.py +464 -0
- package/vendor/ars/scripts/check_v3_6_8_cite_provenance_pipeline.py +231 -0
- package/vendor/ars/scripts/check_v3_6_8_frontmatter_trust_schema.py +248 -0
- package/vendor/ars/scripts/check_v3_6_8_mark_read_commands.py +79 -0
- package/vendor/ars/scripts/check_v3_6_8_pattern_protection.py +941 -0
- package/vendor/ars/scripts/check_v3_7_3_three_layer_citation.py +318 -0
- package/vendor/ars/scripts/check_v3_8_annotation_literal_sync.py +228 -0
- package/vendor/ars/scripts/check_v3_9_0_triangulation.py +366 -0
- package/vendor/ars/scripts/check_v3_9_2_phase_boundary.py +270 -0
- package/vendor/ars/scripts/check_v3_9_4_temporal_verification.py +139 -0
- package/vendor/ars/scripts/check_venue_disclosure_policies.py +63 -0
- package/vendor/ars/scripts/check_version_consistency.py +826 -0
- package/vendor/ars/scripts/check_workflow_classification.py +223 -0
- package/vendor/ars/scripts/chinese_literature_client.py +1938 -0
- package/vendor/ars/scripts/citation_verification_summary.py +85 -0
- package/vendor/ars/scripts/claim_audit_calibration.py +517 -0
- package/vendor/ars/scripts/claim_audit_finalizer.py +456 -0
- package/vendor/ars/scripts/claim_audit_pipeline.py +1594 -0
- package/vendor/ars/scripts/claim_registry_coverage.py +493 -0
- package/vendor/ars/scripts/claim_standing_discovery.py +784 -0
- package/vendor/ars/scripts/claim_standing_stance_runner.py +758 -0
- package/vendor/ars/scripts/claim_standing_stance_scorer.py +239 -0
- package/vendor/ars/scripts/claim_strength_drift_disposition.py +666 -0
- package/vendor/ars/scripts/contamination_signals.py +689 -0
- package/vendor/ars/scripts/corpus_consumer_manifest.json +19 -0
- package/vendor/ars/scripts/cross_model_codex_transport.py +1374 -0
- package/vendor/ars/scripts/cross_model_codex_verify.sh +6 -0
- package/vendor/ars/scripts/cross_model_handoff.py +359 -0
- package/vendor/ars/scripts/cross_model_smoke_test.sh +183 -0
- package/vendor/ars/scripts/cross_model_smoke_test_codex.sh +35 -0
- package/vendor/ars/scripts/cross_model_verification/gemini_is_grounded.jq +45 -0
- package/vendor/ars/scripts/cross_model_verification/gemini_sources.jq +45 -0
- package/vendor/ars/scripts/cross_model_verification/normalize_compat_verdict.py +57 -0
- package/vendor/ars/scripts/cross_model_verification/openai_has_completed_web_search.jq +15 -0
- package/vendor/ars/scripts/cross_model_verification/openai_sources.jq +17 -0
- package/vendor/ars/scripts/cross_model_verification/openai_text.jq +11 -0
- package/vendor/ars/scripts/crossref_client.py +225 -0
- package/vendor/ars/scripts/dispatch_e4_panel.py +2731 -0
- package/vendor/ars/scripts/evidence_rows.py +2043 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3.json +32 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_input.json +27 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_prime.json +32 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_prime_input.json +27 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_override.json +50 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_override_action.json +14 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_pass.json +44 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/explicit_user_request_log.json +26 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/mandatory_checkpoint_log.json +116 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/README.md +56 -0
- 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
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.verdict.yaml +19 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a7_orphan_completion/2026-04-30T15-22-04Z-d8f3.jsonl +3 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.audit_artifact_entry.json +26 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.verdict.yaml +19 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.audit_artifact_entry.json +24 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.verdict.yaml +12 -0
- package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/retraction.json +46 -0
- package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/retraction_check_attestation.json +44 -0
- package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase.json +44 -0
- package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase_v1_2_abstract_missing.json +107 -0
- package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase_v1_2_detected.json +124 -0
- package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/expected_outcomes.json +77 -0
- package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/manifest.yaml +37 -0
- package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/001-valid-doi-test.json +20 -0
- package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/002-valid-arxiv-test.json +19 -0
- package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/003-fabricated-test.json +20 -0
- package/vendor/ars/scripts/fixtures/claim_audit_calibration/gold_set.json +344 -0
- package/vendor/ars/scripts/fixtures/claim_standing_candidate_ledger/query_plan.json +108 -0
- package/vendor/ars/scripts/fixtures/claim_standing_candidate_ledger/retrieval_input.json +179 -0
- package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/concern_tracker.json +129 -0
- package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/response_skeleton.md +19 -0
- package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/source_letter.txt +9 -0
- package/vendor/ars/scripts/fixtures/content_coverage_advisory/base_draft.json +48 -0
- package/vendor/ars/scripts/fixtures/content_coverage_advisory/base_inventory.json +38 -0
- package/vendor/ars/scripts/fixtures/content_coverage_advisory/packet/consent.txt +1 -0
- package/vendor/ars/scripts/fixtures/content_coverage_advisory/session_sources.json +3 -0
- package/vendor/ars/scripts/fixtures/cross_document_consistency/README.md +6 -0
- package/vendor/ars/scripts/fixtures/cross_document_consistency/accepted_draft.md +35 -0
- package/vendor/ars/scripts/fixtures/cross_document_consistency/cases.json +75 -0
- package/vendor/ars/scripts/fixtures/cross_document_consistency/preregistration.md +5 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/forbidden_event.jsonl +4 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/grounded_verified.jsonl +3 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/malformed.jsonl +3 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/missing_search.jsonl +2 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/multiple_finals.jsonl +4 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/not_found.jsonl +3 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/unbound_source.jsonl +3 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/wrong_search_shape.jsonl +3 -0
- package/vendor/ars/scripts/fixtures/evidence_rows/phase_e_inputs.json +114 -0
- package/vendor/ars/scripts/fixtures/evidence_rows/session_sources.json +5 -0
- package/vendor/ars/scripts/fixtures/human_subjects_authority/cross-border-us-tw-gdpr.json +71 -0
- package/vendor/ars/scripts/fixtures/human_subjects_authority/gdpr-member-state-unresolved.json +62 -0
- package/vendor/ars/scripts/fixtures/human_subjects_authority/missing-data-axis.json +45 -0
- package/vendor/ars/scripts/fixtures/human_subjects_authority/no-profile.json +33 -0
- package/vendor/ars/scripts/fixtures/human_subjects_authority/tw-gdpr-two-axis.json +74 -0
- package/vendor/ars/scripts/fixtures/human_subjects_authority/us-gdpr-two-axis.json +74 -0
- package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/lint-near-misses.json +72 -0
- package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/no-profile-request.json +18 -0
- package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/tw-candidates-request.json +63 -0
- package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/us-candidates-request.json +77 -0
- package/vendor/ars/scripts/fixtures/review_target_context/exact-declaration.json +24 -0
- package/vendor/ars/scripts/fixtures/review_target_context/field-general-declaration.json +24 -0
- package/vendor/ars/scripts/fixtures/review_target_context/msr-2027-technical-full-declaration.json +24 -0
- package/vendor/ars/scripts/fixtures/review_target_context/synthetic-registry.json +92 -0
- package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/README.md +21 -0
- package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_attested_only.json +55 -0
- package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_machine_supported.json +49 -0
- package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_not_isolated.json +56 -0
- package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_unknown.json +49 -0
- package/vendor/ars/scripts/fixtures/submission_package/clean/paper.md +16 -0
- package/vendor/ars/scripts/fixtures/submission_package/clean/references.bib +13 -0
- package/vendor/ars/scripts/fixtures/submission_package/fallback_authoryear/paper.md +11 -0
- package/vendor/ars/scripts/fixtures/submission_package/fallback_authoryear/references.bib +13 -0
- package/vendor/ars/scripts/fixtures/submission_package/fallback_latex/paper.tex +9 -0
- package/vendor/ars/scripts/fixtures/submission_package/fallback_latex/references.bib +13 -0
- package/vendor/ars/scripts/fixtures/submission_package/marker_no_join/paper.md +7 -0
- package/vendor/ars/scripts/fixtures/submission_package/orphan_intext/paper.md +8 -0
- package/vendor/ars/scripts/fixtures/submission_package/orphan_intext/references.bib +6 -0
- package/vendor/ars/scripts/fixtures/submission_package/passports/corpus_only.yaml +9 -0
- package/vendor/ars/scripts/fixtures/submission_package/passports/summary_join.yaml +16 -0
- package/vendor/ars/scripts/fixtures/submission_package/profiles/full.yaml +15 -0
- package/vendor/ars/scripts/fixtures/submission_package/profiles/tight.yaml +15 -0
- package/vendor/ars/scripts/fixtures/submission_package/summary_join/paper.md +7 -0
- package/vendor/ars/scripts/fixtures/submission_package/uncited_reference/paper.md +9 -0
- package/vendor/ars/scripts/fixtures/submission_package/uncited_reference/references.bib +13 -0
- package/vendor/ars/scripts/fixtures/submission_package/venue_clean/paper.md +26 -0
- package/vendor/ars/scripts/fixtures/submission_package/venue_clean/references.bib +13 -0
- package/vendor/ars/scripts/fixtures/submission_package/venue_violations/paper.md +20 -0
- package/vendor/ars/scripts/fixtures/submission_package/venue_violations/references.bib +13 -0
- package/vendor/ars/scripts/fixtures/submission_packet_manifest/base_inventory.json +101 -0
- package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/consent-materials.txt +3 -0
- package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/training-certificate.txt +2 -0
- package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/tw-consent-materials.txt +3 -0
- package/vendor/ars/scripts/fixtures/tortured_phrase_screening/corpus_input.yaml +30 -0
- package/vendor/ars/scripts/fixtures/tortured_phrase_screening/own_draft.md +36 -0
- package/vendor/ars/scripts/fixtures/tortured_phrase_screening/own_draft.tex +21 -0
- package/vendor/ars/scripts/fixtures/tortured_phrase_screening/seed_expectations.json +218 -0
- package/vendor/ars/scripts/fixtures/tortured_phrase_screening/snapshot.json +164 -0
- package/vendor/ars/scripts/fixtures/tortured_phrase_screening/snapshot_manifest.json +29 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/README.md +55 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/empty_feed.xml +10 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/error_5xx.html +7 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/id_hit.xml +26 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/README.md +69 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/cnki_landing_page.html +13 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/error_5xx.html +2 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_ambiguous.json +16 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_hit.json +16 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_zero.json +14 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coverage_hit.json +15 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coverage_zero.json +13 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_hit.json +43 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_issn_mismatch.json +31 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_no_doi.json +31 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_unknown_ra.json +34 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_year_mismatch.json +31 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_absent.json +4 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_exists.json +16 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_internal_error.json +4 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/istic_csl_hit.json +25 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/istic_csl_other_title.json +18 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_cnki.json +6 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_crossref.json +6 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_istic.json +6 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_unknown_prefix.json +6 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/crossref/doi_hit.json +24 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/crossref/error_5xx.html +7 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/crossref/title_search_miss.json +15 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/openalex/doi_hit.json +29 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/openalex/error_5xx.json +4 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/openalex/title_search_miss.json +11 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/doi_hit.json +14 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/error_5xx.json +3 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/title_search_miss.json +5 -0
- package/vendor/ars/scripts/human_read_attestation_resolver.py +467 -0
- package/vendor/ars/scripts/ideation_diversity_assignment_gate.py +430 -0
- package/vendor/ars/scripts/inquiry_branch_ledger.py +2540 -0
- package/vendor/ars/scripts/legacy/ars_apply_revision_patch_v1_0.py +715 -0
- package/vendor/ars/scripts/legacy/check_re_review_synthesis_v1_0.py +2215 -0
- package/vendor/ars/scripts/migrate_literature_corpus_to_v3_10.py +223 -0
- package/vendor/ars/scripts/migrate_literature_corpus_to_v3_7_3.py +277 -0
- package/vendor/ars/scripts/migrate_literature_corpus_to_v3_9_0.py +304 -0
- package/vendor/ars/scripts/model_tiering_manifest.json +46 -0
- package/vendor/ars/scripts/openalex_client.py +232 -0
- package/vendor/ars/scripts/parse_audit_verdict.py +802 -0
- package/vendor/ars/scripts/pdf_content_classifier_worker.py +176 -0
- package/vendor/ars/scripts/pdf_read_preflight.py +1455 -0
- package/vendor/ars/scripts/policy_anchor_disclosure_referee.py +354 -0
- package/vendor/ars/scripts/recompute_receipts.py +1414 -0
- package/vendor/ars/scripts/render_claim_standing_view.py +374 -0
- package/vendor/ars/scripts/render_eval_comment.py +130 -0
- package/vendor/ars/scripts/render_harness_retirement_issue.py +167 -0
- package/vendor/ars/scripts/repro_lock_validation.py +90 -0
- package/vendor/ars/scripts/research_workflow_profile.py +1079 -0
- package/vendor/ars/scripts/resolve_human_subjects_authority.py +1158 -0
- package/vendor/ars/scripts/resolve_review_target_context.py +683 -0
- package/vendor/ars/scripts/resume_e4_record.py +509 -0
- package/vendor/ars/scripts/retraction_status.py +484 -0
- package/vendor/ars/scripts/review_criteria_binding.py +889 -0
- package/vendor/ars/scripts/review_panel_provenance.py +744 -0
- package/vendor/ars/scripts/revision_roadmap.py +1967 -0
- package/vendor/ars/scripts/run_ci_pytest_manifest.py +116 -0
- package/vendor/ars/scripts/run_codex_audit.sh +1191 -0
- package/vendor/ars/scripts/run_evals.py +513 -0
- package/vendor/ars/scripts/run_ideation_diversity_no_call.py +3505 -0
- package/vendor/ars/scripts/run_indirect_prompt_injection_no_call.py +3330 -0
- package/vendor/ars/scripts/run_indirect_prompt_injection_probe.py +399 -0
- package/vendor/ars/scripts/run_review_criteria_constructive_value.py +1895 -0
- package/vendor/ars/scripts/run_role_topology_utility_dry_run.py +606 -0
- package/vendor/ars/scripts/score_review_criteria_constructive_value.py +617 -0
- package/vendor/ars/scripts/semantic_scholar_client.py +291 -0
- package/vendor/ars/scripts/slr_lineage.py +59 -0
- package/vendor/ars/scripts/sync_adapter_docs.py +118 -0
- package/vendor/ars/scripts/temporal_integrity_audit.py +840 -0
- package/vendor/ars/scripts/test_431_exact_or_bust.py +253 -0
- package/vendor/ars/scripts/test__eval_threshold_gate.py +125 -0
- package/vendor/ars/scripts/test__markdown_lint_util.py +129 -0
- package/vendor/ars/scripts/test__next_verified_at_ms.py +256 -0
- package/vendor/ars/scripts/test_adjacent_framing_probe_lint.py +171 -0
- package/vendor/ars/scripts/test_adjudication_activity.py +1515 -0
- package/vendor/ars/scripts/test_ars_anchorize_draft.py +178 -0
- package/vendor/ars/scripts/test_ars_apply_revision_patch.py +1335 -0
- package/vendor/ars/scripts/test_ars_cache_invalidate.py +51 -0
- package/vendor/ars/scripts/test_ars_mark_read.py +910 -0
- package/vendor/ars/scripts/test_ars_update_check.py +816 -0
- package/vendor/ars/scripts/test_ars_write_scope_guard.py +790 -0
- package/vendor/ars/scripts/test_arxiv_client.py +374 -0
- package/vendor/ars/scripts/test_audit_schemas.py +560 -0
- package/vendor/ars/scripts/test_audit_snapshot_render_section_0.py +105 -0
- package/vendor/ars/scripts/test_block_parser.py +259 -0
- package/vendor/ars/scripts/test_bootstrap_timeline_yaml.py +148 -0
- package/vendor/ars/scripts/test_build_claim_standing_candidate_ledger.py +1203 -0
- package/vendor/ars/scripts/test_build_claim_standing_query_plan.py +791 -0
- package/vendor/ars/scripts/test_build_submission_packet_manifest.py +2644 -0
- package/vendor/ars/scripts/test_check_215_field_norm.py +238 -0
- package/vendor/ars/scripts/test_check_216_surface_form.py +341 -0
- package/vendor/ars/scripts/test_check_268_nested_commitment_ledger.py +194 -0
- package/vendor/ars/scripts/test_check_390_revision_patch_discipline.py +279 -0
- package/vendor/ars/scripts/test_check_392_citation_verification_intake.py +144 -0
- package/vendor/ars/scripts/test_check_394_submission_policy.py +200 -0
- package/vendor/ars/scripts/test_check_439_format_profile.py +252 -0
- package/vendor/ars/scripts/test_check_619_disclosure_closeout.py +158 -0
- package/vendor/ars/scripts/test_check_630_codex_subscription_transport.py +245 -0
- package/vendor/ars/scripts/test_check_669_review_pathway_rule_trace.py +304 -0
- package/vendor/ars/scripts/test_check_670_revision_roadmap_integration.py +260 -0
- package/vendor/ars/scripts/test_check_673_adjudication_activity.py +367 -0
- package/vendor/ars/scripts/test_check_684_review_criteria_binding.py +274 -0
- package/vendor/ars/scripts/test_check_agents_mirror_sync.py +137 -0
- package/vendor/ars/scripts/test_check_audit_artifact_consistency.py +2133 -0
- package/vendor/ars/scripts/test_check_benchmark_report.py +117 -0
- package/vendor/ars/scripts/test_check_bibliographic_integrity_signals.py +609 -0
- package/vendor/ars/scripts/test_check_calibration_tiers.py +376 -0
- package/vendor/ars/scripts/test_check_changelog_covers_merges.py +509 -0
- package/vendor/ars/scripts/test_check_ci_pytest_manifest.py +425 -0
- package/vendor/ars/scripts/test_check_claim_standing_candidate_ledger_integration.py +256 -0
- package/vendor/ars/scripts/test_check_claim_standing_freshness.py +277 -0
- package/vendor/ars/scripts/test_check_collaboration_depth_rubric.py +239 -0
- package/vendor/ars/scripts/test_check_command_frontmatter_name.py +178 -0
- package/vendor/ars/scripts/test_check_committee_correspondence.py +298 -0
- package/vendor/ars/scripts/test_check_compliance_report.py +381 -0
- package/vendor/ars/scripts/test_check_content_coverage_advisory_integration.py +308 -0
- package/vendor/ars/scripts/test_check_control_availability.py +361 -0
- package/vendor/ars/scripts/test_check_cross_document_consistency_advisory_integration.py +698 -0
- package/vendor/ars/scripts/test_check_cross_model_handoff_contract.py +301 -0
- package/vendor/ars/scripts/test_check_cross_model_verification_sync.py +203 -0
- package/vendor/ars/scripts/test_check_data_access_level.py +227 -0
- package/vendor/ars/scripts/test_check_data_flows.py +382 -0
- package/vendor/ars/scripts/test_check_decision_contract.py +387 -0
- package/vendor/ars/scripts/test_check_degradation_registry.py +270 -0
- package/vendor/ars/scripts/test_check_distribution_surface_claims.py +225 -0
- package/vendor/ars/scripts/test_check_domain_evidence_profile.py +439 -0
- package/vendor/ars/scripts/test_check_e4_promotion.py +163 -0
- package/vendor/ars/scripts/test_check_evals_gold_set.py +312 -0
- package/vendor/ars/scripts/test_check_evidence_row_integration.py +187 -0
- package/vendor/ars/scripts/test_check_field_norm_severity.py +173 -0
- package/vendor/ars/scripts/test_check_firm_rules_sync.py +342 -0
- package/vendor/ars/scripts/test_check_heldout_measurement_report.py +1508 -0
- package/vendor/ars/scripts/test_check_human_subjects_output_contract.py +129 -0
- package/vendor/ars/scripts/test_check_human_subjects_reference_migration.py +756 -0
- package/vendor/ars/scripts/test_check_instruction_data_boundary.py +204 -0
- package/vendor/ars/scripts/test_check_judge_prompt_version.py +90 -0
- package/vendor/ars/scripts/test_check_model_tiering.py +236 -0
- package/vendor/ars/scripts/test_check_panel_synthesis.py +1658 -0
- package/vendor/ars/scripts/test_check_passport_reset_contract.py +249 -0
- package/vendor/ars/scripts/test_check_pattern_eval_manifest.py +381 -0
- package/vendor/ars/scripts/test_check_persuasion_invariance_fixtures.py +591 -0
- package/vendor/ars/scripts/test_check_phase_conformance.py +4366 -0
- package/vendor/ars/scripts/test_check_pipeline_boundary_semantics.py +931 -0
- package/vendor/ars/scripts/test_check_pipeline_integrity.py +243 -0
- package/vendor/ars/scripts/test_check_policy_anchor_protocol.py +236 -0
- package/vendor/ars/scripts/test_check_policy_anchor_table.py +295 -0
- package/vendor/ars/scripts/test_check_prisma_trAIce_freshness.py +68 -0
- package/vendor/ars/scripts/test_check_promotion_bakeoff_preregistration.py +799 -0
- package/vendor/ars/scripts/test_check_ranking_lift.py +377 -0
- package/vendor/ars/scripts/test_check_re_review_synthesis.py +3398 -0
- package/vendor/ars/scripts/test_check_receipt_enum_sync.py +176 -0
- package/vendor/ars/scripts/test_check_repro_lock.py +107 -0
- package/vendor/ars/scripts/test_check_reviewer_data_fences.py +334 -0
- package/vendor/ars/scripts/test_check_reviewer_finding_contract.py +1107 -0
- package/vendor/ars/scripts/test_check_reviewer_role_label.py +491 -0
- package/vendor/ars/scripts/test_check_reviewer_scoring_honesty.py +222 -0
- package/vendor/ars/scripts/test_check_reviewer_sprint_prompt_sync.py +373 -0
- package/vendor/ars/scripts/test_check_revision_claim_drift_suite_v2.py +1925 -0
- package/vendor/ars/scripts/test_check_revision_token_conservation.py +398 -0
- package/vendor/ars/scripts/test_check_risk_register.py +336 -0
- package/vendor/ars/scripts/test_check_role_scoped_contract.py +1104 -0
- package/vendor/ars/scripts/test_check_rq_framing_patterns.py +110 -0
- package/vendor/ars/scripts/test_check_rubric_weight_consistency.py +32 -0
- package/vendor/ars/scripts/test_check_seeded_defect_fixtures.py +392 -0
- package/vendor/ars/scripts/test_check_setup_cross_model_parity.py +121 -0
- package/vendor/ars/scripts/test_check_spec_consistency.py +1081 -0
- package/vendor/ars/scripts/test_check_sprint_contract.py +458 -0
- package/vendor/ars/scripts/test_check_stage_capability_matrix.py +698 -0
- package/vendor/ars/scripts/test_check_submission_packet_manifest_integration.py +177 -0
- package/vendor/ars/scripts/test_check_surface_form_parity.py +417 -0
- package/vendor/ars/scripts/test_check_task_type.py +116 -0
- package/vendor/ars/scripts/test_check_tools_allowlist.py +835 -0
- package/vendor/ars/scripts/test_check_tortured_phrase_screening_integration.py +1306 -0
- package/vendor/ars/scripts/test_check_v3_10_134_write_scope.py +251 -0
- package/vendor/ars/scripts/test_check_v3_10_policy.py +547 -0
- package/vendor/ars/scripts/test_check_v3_6_7_pattern_protection.py +960 -0
- package/vendor/ars/scripts/test_check_v3_6_8_audit_scope_block.py +1000 -0
- package/vendor/ars/scripts/test_check_v3_6_8_cite_provenance_pipeline.py +454 -0
- package/vendor/ars/scripts/test_check_v3_6_8_frontmatter_trust_schema.py +581 -0
- package/vendor/ars/scripts/test_check_v3_6_8_mark_read_commands.py +120 -0
- package/vendor/ars/scripts/test_check_v3_6_8_pattern_protection.py +1138 -0
- package/vendor/ars/scripts/test_check_v3_7_3_three_layer_citation.py +566 -0
- package/vendor/ars/scripts/test_check_v3_8_annotation_literal_sync.py +262 -0
- package/vendor/ars/scripts/test_check_v3_9_0_triangulation.py +321 -0
- package/vendor/ars/scripts/test_check_v3_9_2_phase_boundary.py +190 -0
- package/vendor/ars/scripts/test_check_v3_9_4_temporal_verification.py +471 -0
- package/vendor/ars/scripts/test_check_version_consistency.py +1462 -0
- package/vendor/ars/scripts/test_check_workflow_classification.py +225 -0
- package/vendor/ars/scripts/test_chinese_literature_client.py +1889 -0
- package/vendor/ars/scripts/test_citation_existence_policy.py +480 -0
- package/vendor/ars/scripts/test_citation_verification_summary.py +342 -0
- package/vendor/ars/scripts/test_claim_audit_calibration.py +882 -0
- package/vendor/ars/scripts/test_claim_audit_finalizer.py +979 -0
- package/vendor/ars/scripts/test_claim_audit_pipeline.py +2398 -0
- package/vendor/ars/scripts/test_claim_audit_schema.py +1778 -0
- package/vendor/ars/scripts/test_claim_intent_manifest.py +666 -0
- package/vendor/ars/scripts/test_claim_registry_coverage.py +246 -0
- package/vendor/ars/scripts/test_claim_standing_discovery.py +579 -0
- package/vendor/ars/scripts/test_claim_standing_pipeline_wiring.py +290 -0
- package/vendor/ars/scripts/test_claim_standing_stance_assets.py +399 -0
- package/vendor/ars/scripts/test_claim_standing_stance_contracts.py +383 -0
- package/vendor/ars/scripts/test_claim_standing_stance_runner.py +349 -0
- package/vendor/ars/scripts/test_claim_standing_transmissions.py +605 -0
- package/vendor/ars/scripts/test_claim_strength_drift_disposition.py +517 -0
- package/vendor/ars/scripts/test_claim_verification_coverage_contract.py +104 -0
- package/vendor/ars/scripts/test_contamination_signals.py +1086 -0
- package/vendor/ars/scripts/test_content_coverage_advisory.py +1767 -0
- package/vendor/ars/scripts/test_cross_document_consistency_advisory.py +1270 -0
- package/vendor/ars/scripts/test_cross_model_codex_transport.py +1152 -0
- package/vendor/ars/scripts/test_cross_model_handoff.py +571 -0
- package/vendor/ars/scripts/test_cross_model_verification_guards.py +792 -0
- package/vendor/ars/scripts/test_crossref_client.py +393 -0
- package/vendor/ars/scripts/test_dispatch_e4_panel.py +4235 -0
- package/vendor/ars/scripts/test_e2e_claim_audit.py +540 -0
- package/vendor/ars/scripts/test_eval_harness_workflow.py +140 -0
- package/vendor/ars/scripts/test_evals_citation_extraction.py +150 -0
- package/vendor/ars/scripts/test_evals_lift_report_schema.py +108 -0
- package/vendor/ars/scripts/test_evidence_rows.py +2593 -0
- package/vendor/ars/scripts/test_experiment_provenance.py +915 -0
- package/vendor/ars/scripts/test_human_read_attestation_resolver.py +477 -0
- package/vendor/ars/scripts/test_ideation_diversity_assignment_gate.py +615 -0
- package/vendor/ars/scripts/test_indirect_prompt_injection_behavior_probe.py +250 -0
- package/vendor/ars/scripts/test_inquiry_branch_ledger.py +2296 -0
- package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_10.py +248 -0
- package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_7_3.py +545 -0
- package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_9_0.py +497 -0
- package/vendor/ars/scripts/test_normalize_compat_verdict.py +149 -0
- package/vendor/ars/scripts/test_openalex_client.py +490 -0
- package/vendor/ars/scripts/test_passport_yaml.py +104 -0
- package/vendor/ars/scripts/test_pattern_eval_runtime.py +1295 -0
- package/vendor/ars/scripts/test_pdf_read_preflight.py +1943 -0
- package/vendor/ars/scripts/test_policy_anchor_disclosure.py +666 -0
- package/vendor/ars/scripts/test_reading_probe_lint.py +218 -0
- package/vendor/ars/scripts/test_recompute_receipts.py +778 -0
- package/vendor/ars/scripts/test_render_claim_standing_view.py +192 -0
- package/vendor/ars/scripts/test_render_eval_comment.py +162 -0
- package/vendor/ars/scripts/test_render_harness_retirement_issue.py +110 -0
- package/vendor/ars/scripts/test_repro_lock_validation_drift.py +100 -0
- package/vendor/ars/scripts/test_research_workflow_profile.py +734 -0
- package/vendor/ars/scripts/test_resolve_human_subjects_authority.py +1219 -0
- package/vendor/ars/scripts/test_resolve_review_target_context.py +703 -0
- package/vendor/ars/scripts/test_resume_e4_record.py +315 -0
- package/vendor/ars/scripts/test_retraction_status.py +456 -0
- package/vendor/ars/scripts/test_review_criteria_binding.py +629 -0
- package/vendor/ars/scripts/test_review_panel_provenance.py +565 -0
- package/vendor/ars/scripts/test_review_pathway_rule_trace.py +821 -0
- package/vendor/ars/scripts/test_revision_roadmap.py +1255 -0
- package/vendor/ars/scripts/test_run_ci_pytest_manifest.py +186 -0
- package/vendor/ars/scripts/test_run_codex_audit_e2e.py +369 -0
- package/vendor/ars/scripts/test_run_evals.py +430 -0
- package/vendor/ars/scripts/test_run_guard_launcher.py +500 -0
- package/vendor/ars/scripts/test_run_ideation_diversity_no_call.py +1833 -0
- package/vendor/ars/scripts/test_run_indirect_prompt_injection_no_call.py +1889 -0
- package/vendor/ars/scripts/test_run_review_criteria_constructive_value.py +586 -0
- package/vendor/ars/scripts/test_run_role_topology_utility_dry_run.py +428 -0
- package/vendor/ars/scripts/test_score_review_criteria_constructive_value.py +340 -0
- package/vendor/ars/scripts/test_semantic_scholar_client.py +554 -0
- package/vendor/ars/scripts/test_slr_lineage_emission.py +230 -0
- package/vendor/ars/scripts/test_socratic_rq_non_generation_contract.py +173 -0
- package/vendor/ars/scripts/test_temporal_integrity_audit.py +438 -0
- package/vendor/ars/scripts/test_text_similarity.py +95 -0
- package/vendor/ars/scripts/test_title_fuzzy_false_positive.py +111 -0
- package/vendor/ars/scripts/test_tortured_phrase_screening.py +2959 -0
- package/vendor/ars/scripts/test_transport_fixture_citation_gate.py +338 -0
- package/vendor/ars/scripts/test_uncited_assertion.py +558 -0
- package/vendor/ars/scripts/test_v3_6_7_phase_6_6.py +1279 -0
- package/vendor/ars/scripts/test_validate_compliance_fixtures.py +36 -0
- package/vendor/ars/scripts/test_validate_ideation_diversity_assets.py +230 -0
- package/vendor/ars/scripts/test_venue_disclosure_contract.py +755 -0
- package/vendor/ars/scripts/test_verification_cache.py +280 -0
- package/vendor/ars/scripts/test_verification_gate.py +461 -0
- package/vendor/ars/scripts/test_verify_passport_cli.py +123 -0
- package/vendor/ars/scripts/test_verify_submission_package.py +1407 -0
- package/vendor/ars/scripts/test_version_records_schema.py +211 -0
- package/vendor/ars/scripts/tortured_phrase_screening.py +3502 -0
- package/vendor/ars/scripts/uncited_assertion_detector.py +254 -0
- package/vendor/ars/scripts/v3_6_7_inversion_manifest.json +9 -0
- package/vendor/ars/scripts/v3_6_8_inversion_manifest.json +10 -0
- package/vendor/ars/scripts/validate_claim_standing_stance_assets.py +249 -0
- package/vendor/ars/scripts/validate_compliance_fixtures.py +56 -0
- package/vendor/ars/scripts/validate_ideation_diversity_assets.py +303 -0
- package/vendor/ars/scripts/venue_disclosure_contract_harness.py +837 -0
- package/vendor/ars/scripts/verification_cache.py +276 -0
- package/vendor/ars/scripts/verification_gate/__init__.py +345 -0
- package/vendor/ars/scripts/verify_passport.py +133 -0
- package/vendor/ars/scripts/verify_submission_package.py +1657 -0
- package/vendor/ars/shared/agents/compliance_agent.md +136 -0
- package/vendor/ars/shared/artifact_reproducibility_pattern.md +173 -0
- package/vendor/ars/shared/benchmark_report.schema.json +81 -0
- package/vendor/ars/shared/benchmark_report_pattern.md +180 -0
- package/vendor/ars/shared/bibliographic_integrity_signals.md +142 -0
- package/vendor/ars/shared/collaboration_depth_rubric.md +154 -0
- package/vendor/ars/shared/compliance_checkpoint_protocol.md +162 -0
- package/vendor/ars/shared/compliance_report.schema.json +187 -0
- package/vendor/ars/shared/contracts/README.md +938 -0
- package/vendor/ars/shared/contracts/activity/adjudication_activity_input.schema.json +555 -0
- package/vendor/ars/shared/contracts/activity/adjudication_activity_store.schema.json +560 -0
- package/vendor/ars/shared/contracts/audit/audit_jsonl.schema.json +128 -0
- package/vendor/ars/shared/contracts/audit/audit_sidecar.schema.json +169 -0
- package/vendor/ars/shared/contracts/audit/audit_verdict.schema.json +133 -0
- package/vendor/ars/shared/contracts/audit/cross_document_consistency_advisory.schema.json +480 -0
- package/vendor/ars/shared/contracts/audit/cross_document_consistency_advisory_draft.schema.json +564 -0
- package/vendor/ars/shared/contracts/audit/cross_document_source_manifest.schema.json +202 -0
- package/vendor/ars/shared/contracts/audit/tortured_phrase_advisory.schema.json +1362 -0
- package/vendor/ars/shared/contracts/audit/tortured_phrase_snapshot.schema.json +208 -0
- package/vendor/ars/shared/contracts/audit/tortured_phrase_snapshot_manifest.schema.json +335 -0
- package/vendor/ars/shared/contracts/capability/stage_capability_matrix.json +434 -0
- package/vendor/ars/shared/contracts/claim_standing/candidate_ledger.schema.json +705 -0
- package/vendor/ars/shared/contracts/claim_standing/query_plan.schema.json +389 -0
- package/vendor/ars/shared/contracts/claim_standing/query_plan_v1_1.schema.json +782 -0
- package/vendor/ars/shared/contracts/claim_standing/retrieval_input.schema.json +440 -0
- package/vendor/ars/shared/contracts/claim_standing/stance_record.schema.json +460 -0
- package/vendor/ars/shared/contracts/claim_standing/transmission_ledger.schema.json +282 -0
- package/vendor/ars/shared/contracts/cross_model/codex_citation_receipt.schema.json +157 -0
- package/vendor/ars/shared/contracts/cross_model/codex_citation_request.schema.json +23 -0
- package/vendor/ars/shared/contracts/cross_model/promotion_bakeoff_sealed_commitment.schema.json +55 -0
- package/vendor/ars/shared/contracts/cross_model/promotion_bakeoff_sealed_reveal.schema.json +50 -0
- package/vendor/ars/shared/contracts/degradation_registry.json +428 -0
- package/vendor/ars/shared/contracts/evaluator/full.json +126 -0
- package/vendor/ars/shared/contracts/evidence/claim_registry.schema.json +50 -0
- package/vendor/ars/shared/contracts/evidence/claim_registry_coverage_report.schema.json +79 -0
- package/vendor/ars/shared/contracts/evidence/evidence_row.schema.json +504 -0
- package/vendor/ars/shared/contracts/evidence/evidence_row_v1_1.schema.json +364 -0
- package/vendor/ars/shared/contracts/evidence/evidence_row_v1_2.schema.json +714 -0
- package/vendor/ars/shared/contracts/evidence/evidence_row_v1_3.schema.json +524 -0
- package/vendor/ars/shared/contracts/human_subjects/authority_profile_registry.schema.json +497 -0
- package/vendor/ars/shared/contracts/human_subjects/committee_correspondence.schema.json +253 -0
- package/vendor/ars/shared/contracts/human_subjects/content_coverage_advisory.schema.json +620 -0
- package/vendor/ars/shared/contracts/human_subjects/irb_context_record.schema.json +329 -0
- package/vendor/ars/shared/contracts/human_subjects/resolved_authority_context.schema.json +347 -0
- package/vendor/ars/shared/contracts/human_subjects/review_pathway_rule_trace.schema.json +265 -0
- package/vendor/ars/shared/contracts/human_subjects/review_pathway_trace_request.schema.json +127 -0
- package/vendor/ars/shared/contracts/human_subjects/submission_packet_inventory.schema.json +240 -0
- package/vendor/ars/shared/contracts/human_subjects/submission_packet_manifest.schema.json +999 -0
- package/vendor/ars/shared/contracts/passport/audit_artifact_entry.schema.json +266 -0
- package/vendor/ars/shared/contracts/passport/bibliographic_integrity_signal.schema.json +1676 -0
- package/vendor/ars/shared/contracts/passport/citation_provenance.schema.json +107 -0
- package/vendor/ars/shared/contracts/passport/citation_verification_summary.schema.json +164 -0
- package/vendor/ars/shared/contracts/passport/claim_audit_result.schema.json +124 -0
- package/vendor/ars/shared/contracts/passport/claim_drift.schema.json +58 -0
- package/vendor/ars/shared/contracts/passport/claim_intent_manifest.schema.json +107 -0
- package/vendor/ars/shared/contracts/passport/constraint_violation.schema.json +65 -0
- package/vendor/ars/shared/contracts/passport/experiment_alignment_result.schema.json +69 -0
- package/vendor/ars/shared/contracts/passport/experiment_provenance_entry.schema.json +169 -0
- package/vendor/ars/shared/contracts/passport/human_read_log.schema.json +86 -0
- package/vendor/ars/shared/contracts/passport/inquiry_ledger_ref.schema.json +22 -0
- package/vendor/ars/shared/contracts/passport/literature_corpus_entry.schema.json +650 -0
- package/vendor/ars/shared/contracts/passport/preregistration_artifact.schema.json +132 -0
- package/vendor/ars/shared/contracts/passport/rejection_log.schema.json +89 -0
- package/vendor/ars/shared/contracts/passport/reset_ledger_entry.schema.json +158 -0
- package/vendor/ars/shared/contracts/passport/temporal_audit_results.schema.json +208 -0
- package/vendor/ars/shared/contracts/passport/terminal_policies.schema.json +50 -0
- package/vendor/ars/shared/contracts/passport/timeline.schema.json +102 -0
- package/vendor/ars/shared/contracts/passport/uncited_assertion.schema.json +56 -0
- package/vendor/ars/shared/contracts/passport/uncited_audit_failure.schema.json +72 -0
- package/vendor/ars/shared/contracts/passport/user_attested_read_resolution.schema.json +87 -0
- package/vendor/ars/shared/contracts/passport/version_records.schema.json +138 -0
- package/vendor/ars/shared/contracts/patch/block_manifest.schema.json +45 -0
- package/vendor/ars/shared/contracts/patch/legacy/v1_0/revision_patch.schema.json +110 -0
- package/vendor/ars/shared/contracts/patch/revision_patch.schema.json +250 -0
- package/vendor/ars/shared/contracts/pdf/pdf_content_classifier_diagnostic.schema.json +39 -0
- package/vendor/ars/shared/contracts/pdf/pdf_content_classifier_worker.schema.json +88 -0
- package/vendor/ars/shared/contracts/pdf/pdf_read_preflight.schema.json +227 -0
- package/vendor/ars/shared/contracts/re_review/input_manifest.schema.json +161 -0
- package/vendor/ars/shared/contracts/re_review/legacy/v1_0/input_manifest.schema.json +134 -0
- package/vendor/ars/shared/contracts/re_review/legacy/v1_0/precommitment.schema.json +193 -0
- package/vendor/ars/shared/contracts/re_review/legacy/v1_0/traceability.schema.json +841 -0
- package/vendor/ars/shared/contracts/re_review/legacy/v1_0/verdict_record.schema.json +244 -0
- package/vendor/ars/shared/contracts/re_review/precommitment.schema.json +193 -0
- package/vendor/ars/shared/contracts/re_review/traceability.schema.json +916 -0
- package/vendor/ars/shared/contracts/re_review/verdict_record.schema.json +245 -0
- package/vendor/ars/shared/contracts/research_workflow/inquiry_branch_ledger.schema.json +449 -0
- package/vendor/ars/shared/contracts/research_workflow/research_workflow_profile.schema.json +222 -0
- package/vendor/ars/shared/contracts/research_workflow/research_workflow_profile_selection_receipt.schema.json +111 -0
- package/vendor/ars/shared/contracts/review_target/constructive_review_findings.schema.json +172 -0
- package/vendor/ars/shared/contracts/review_target/criteria_registry.schema.json +97 -0
- package/vendor/ars/shared/contracts/review_target/review_criteria_binding_manifest.schema.json +235 -0
- package/vendor/ars/shared/contracts/review_target/review_criteria_source_receipt.schema.json +158 -0
- package/vendor/ars/shared/contracts/review_target/review_target_context.schema.json +106 -0
- package/vendor/ars/shared/contracts/review_target/review_target_declaration.schema.json +118 -0
- package/vendor/ars/shared/contracts/reviewer/full.json +114 -0
- package/vendor/ars/shared/contracts/reviewer/methodology_focus.json +75 -0
- package/vendor/ars/shared/contracts/reviewer/review_panel_provenance.schema.json +263 -0
- package/vendor/ars/shared/contracts/reviewer/review_panel_provenance_carrier.schema.json +122 -0
- package/vendor/ars/shared/contracts/reviewer/review_panel_provenance_input.schema.json +120 -0
- package/vendor/ars/shared/contracts/revision/author_adjudication.schema.json +251 -0
- package/vendor/ars/shared/contracts/revision/author_adjudication_input.schema.json +25 -0
- package/vendor/ars/shared/contracts/revision/claim_strength_drift_disposition.schema.json +97 -0
- package/vendor/ars/shared/contracts/revision/claim_strength_drift_disposition_input.schema.json +80 -0
- package/vendor/ars/shared/contracts/revision/claim_strength_drift_findings.schema.json +139 -0
- package/vendor/ars/shared/contracts/revision/claim_surface_manifest.schema.json +99 -0
- package/vendor/ars/shared/contracts/revision/integrity_correction_authorization.schema.json +140 -0
- package/vendor/ars/shared/contracts/revision/integrity_correction_authorization_input.schema.json +28 -0
- package/vendor/ars/shared/contracts/revision/integrity_correction_list.schema.json +48 -0
- package/vendor/ars/shared/contracts/revision/integrity_pass_receipt.schema.json +16 -0
- package/vendor/ars/shared/contracts/revision/revision_evidence_bundle.schema.json +134 -0
- package/vendor/ars/shared/contracts/revision/revision_roadmap.schema.json +334 -0
- package/vendor/ars/shared/contracts/submission/format_profile.example.yaml +33 -0
- package/vendor/ars/shared/contracts/submission/format_profile.schema.json +102 -0
- package/vendor/ars/shared/contracts/submission/submission_verification_report.schema.json +233 -0
- package/vendor/ars/shared/contracts/submission/venue_profile.schema.json +114 -0
- package/vendor/ars/shared/contracts/writer/full.json +87 -0
- package/vendor/ars/shared/cross_model_verification.md +714 -0
- package/vendor/ars/shared/evals_lift_report.schema.json +141 -0
- package/vendor/ars/shared/ground_truth_isolation_pattern.md +275 -0
- package/vendor/ars/shared/handoff_schemas.md +1209 -0
- package/vendor/ars/shared/human_subjects_authority_registry.json +1278 -0
- package/vendor/ars/shared/mode_spectrum.md +57 -0
- package/vendor/ars/shared/model_tiering.md +83 -0
- package/vendor/ars/shared/policy_data/nature_policy.md +56 -0
- package/vendor/ars/shared/prisma_trAIce_protocol.md +157 -0
- package/vendor/ars/shared/raise_framework.md +129 -0
- package/vendor/ars/shared/references/authority_content_coverage_advisory_protocol.md +275 -0
- package/vendor/ars/shared/references/claim_standing_candidate_ledger_protocol.md +66 -0
- package/vendor/ars/shared/references/claim_strength_ladder.md +93 -0
- package/vendor/ars/shared/references/cross_document_consistency_advisory_protocol.md +263 -0
- package/vendor/ars/shared/references/evidence_row_protocol.md +260 -0
- package/vendor/ars/shared/references/firm_rules.md +90 -0
- package/vendor/ars/shared/references/human_subjects_authority_protocol.md +274 -0
- package/vendor/ars/shared/references/intent_clarification_protocol.md +168 -0
- package/vendor/ars/shared/references/irb_terminology_glossary.md +229 -0
- package/vendor/ars/shared/references/protected_hedging_phrases.md +118 -0
- package/vendor/ars/shared/references/psychometric_terminology_glossary.md +109 -0
- package/vendor/ars/shared/references/review_criteria_consumer_protocol.md +238 -0
- package/vendor/ars/shared/references/review_pathway_rule_trace_protocol.md +166 -0
- package/vendor/ars/shared/references/submission_packet_manifest_protocol.md +292 -0
- package/vendor/ars/shared/references/word_count_conventions.md +124 -0
- package/vendor/ars/shared/research_workflow_profiles/field_general.json +1 -0
- package/vendor/ars/shared/review_criteria_registry.json +207 -0
- package/vendor/ars/shared/review_criteria_sources/msr-2027-technical-papers.2026-08-24.json +46 -0
- package/vendor/ars/shared/review_criteria_sources/sigsoft-empirical-standards.2026-08-24.json +31 -0
- package/vendor/ars/shared/sprint_contract.schema.json +482 -0
- package/vendor/ars/shared/style_calibration_protocol.md +151 -0
- package/vendor/ars/shared/templates/codex_audit_multifile_template.md +263 -0
- package/vendor/ars/tools/release-discipline/.toolkit-version +1 -0
- package/vendor/ars/tools/release-discipline/README.md +4 -0
- package/vendor/ars/tools/release-discipline/scripts/_release_doc_alignment_schema.py +1011 -0
- package/vendor/ars/tools/release-discipline/scripts/check_command_invariants.py +497 -0
- package/vendor/ars/tools/release-discipline/scripts/check_release_doc_alignment.py +263 -0
- package/vendor/ars/tools/release-discipline/scripts/sync-toolkit.sh +147 -0
- package/vendor/windows/NOTICE.md +12 -0
- package/vendor/windows/arm64/fd.exe +0 -0
- package/vendor/windows/arm64/licenses/fd/LICENSE-APACHE +201 -0
- package/vendor/windows/arm64/licenses/fd/LICENSE-MIT +21 -0
- package/vendor/windows/arm64/licenses/ripgrep/COPYING +3 -0
- package/vendor/windows/arm64/licenses/ripgrep/LICENSE-MIT +21 -0
- package/vendor/windows/arm64/licenses/ripgrep/UNLICENSE +24 -0
- package/vendor/windows/arm64/rg.exe +0 -0
- package/vendor/windows/x64/fd.exe +0 -0
- package/vendor/windows/x64/licenses/fd/LICENSE-APACHE +201 -0
- package/vendor/windows/x64/licenses/fd/LICENSE-MIT +21 -0
- package/vendor/windows/x64/licenses/ripgrep/COPYING +3 -0
- package/vendor/windows/x64/licenses/ripgrep/LICENSE-MIT +21 -0
- package/vendor/windows/x64/licenses/ripgrep/UNLICENSE +24 -0
- 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())
|