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,1889 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tests for the Chinese-literature resolver client (#595).
|
|
3
|
+
|
|
4
|
+
Per `deep-research/references/chinese_literature_api_protocol.md`. Structure
|
|
5
|
+
mirrors `test_arxiv_client.py` (per-client unit suite) but uses the
|
|
6
|
+
transport-fixture discipline of `test_transport_fixture_citation_gate.py`: the
|
|
7
|
+
REAL `ChineseLiteratureClient` builds the URLs and parses the bodies, and a
|
|
8
|
+
URL-dispatch fake stands in for the socket. Every body is checked in under
|
|
9
|
+
`scripts/fixtures/transport_bodies/chinese_literature/` and is fully synthetic
|
|
10
|
+
(see that directory's README).
|
|
11
|
+
|
|
12
|
+
ZERO live network. The protocol doc's live examples are a record of a manual
|
|
13
|
+
verification; nothing here contacts doi.org, hdl.handle.net, or NCBI.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import http.client
|
|
18
|
+
import io
|
|
19
|
+
import sys
|
|
20
|
+
import traceback
|
|
21
|
+
import urllib.error
|
|
22
|
+
import urllib.parse
|
|
23
|
+
import urllib.request
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from unittest.mock import MagicMock, patch
|
|
26
|
+
|
|
27
|
+
import pytest
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
31
|
+
if str(REPO_ROOT / "scripts") not in sys.path:
|
|
32
|
+
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
|
33
|
+
|
|
34
|
+
FIXTURES = REPO_ROOT / "scripts" / "fixtures" / "transport_bodies" / "chinese_literature"
|
|
35
|
+
|
|
36
|
+
# Synthetic identifiers pinned to the fixture bodies (see the fixtures README):
|
|
37
|
+
# These suffixes are synthetic fixture namespaces. The 10.5555 prefix itself
|
|
38
|
+
# can contain real Crossref records, so tests make no claim about live minting.
|
|
39
|
+
ISTIC_DOI = "10.5555/ars.cn.istic.2026.0042"
|
|
40
|
+
CNKI_DOI = "10.5555/ars.cn.cnki.2026.0042"
|
|
41
|
+
FABRICATED_DOI = "10.5555/ars.cn.istic.9999.9999"
|
|
42
|
+
|
|
43
|
+
CN_TITLE = "合成引文核验闸的确定性评估方法研究"
|
|
44
|
+
CN_JOURNAL = "合成测试医学杂志"
|
|
45
|
+
|
|
46
|
+
# Injected through the client's documented extension point, so no test depends
|
|
47
|
+
# on the shipped seed table's contents.
|
|
48
|
+
TEST_JOURNAL_MAP = {
|
|
49
|
+
CN_JOURNAL: {
|
|
50
|
+
"issn": "0000-0000",
|
|
51
|
+
"nlm_ta": "Hecheng Ceshi Yi Xue Za Zhi",
|
|
52
|
+
"nlm_id": "999999901",
|
|
53
|
+
"display_name": CN_JOURNAL,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _body(name: str) -> bytes:
|
|
59
|
+
return FIXTURES.joinpath(name).read_bytes()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.fixture(autouse=True)
|
|
63
|
+
def _no_real_sleep(monkeypatch):
|
|
64
|
+
"""Pacing must never burn wall-clock in CI. Patched on the client module so
|
|
65
|
+
a `time.sleep` that leaks in from elsewhere still fails loudly."""
|
|
66
|
+
monkeypatch.setattr("chinese_literature_client.time.sleep", lambda seconds: None)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _client(**kwargs):
|
|
70
|
+
from chinese_literature_client import ChineseLiteratureClient
|
|
71
|
+
|
|
72
|
+
kwargs.setdefault("ncbi_email", "ars-tests@example.invalid")
|
|
73
|
+
return ChineseLiteratureClient(**kwargs)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class FakeTransport:
|
|
77
|
+
"""urlopen stand-in dispatching on the REAL URL the client built.
|
|
78
|
+
|
|
79
|
+
`routes` is an ordered list of `(predicate, action)`; predicates receive
|
|
80
|
+
`(urlsplit_result, parse_qs_dict)`. An unrouted URL is an immediate test
|
|
81
|
+
failure — the dispatch table doubles as an assertion that the client
|
|
82
|
+
contacted exactly the documented endpoints. AssertionError is not in the
|
|
83
|
+
client's degradation except-list, so it propagates instead of being
|
|
84
|
+
laundered into ChineseLiteratureUnavailable.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self, routes):
|
|
88
|
+
self.routes = routes
|
|
89
|
+
self.requests: list[str] = []
|
|
90
|
+
|
|
91
|
+
def __call__(self, req, timeout=None):
|
|
92
|
+
url = req.full_url
|
|
93
|
+
self.requests.append(url)
|
|
94
|
+
parsed = urllib.parse.urlsplit(url)
|
|
95
|
+
query = (
|
|
96
|
+
urllib.parse.parse_qs(
|
|
97
|
+
parsed.query, keep_blank_values=True, strict_parsing=True)
|
|
98
|
+
if parsed.query else {}
|
|
99
|
+
)
|
|
100
|
+
for predicate, action in self.routes:
|
|
101
|
+
if predicate(parsed, query):
|
|
102
|
+
return action(url)
|
|
103
|
+
raise AssertionError(f"client contacted an unrouted URL: {url}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _ok(body: bytes):
|
|
107
|
+
def action(url):
|
|
108
|
+
resp = MagicMock()
|
|
109
|
+
resp.status = 200
|
|
110
|
+
resp.headers = {}
|
|
111
|
+
resp.geturl.return_value = url
|
|
112
|
+
resp.read.return_value = body
|
|
113
|
+
resp.__enter__ = MagicMock(return_value=resp)
|
|
114
|
+
resp.__exit__ = MagicMock(return_value=None)
|
|
115
|
+
return resp
|
|
116
|
+
|
|
117
|
+
return action
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _http_error(code: int, body: bytes = b""):
|
|
121
|
+
def action(url):
|
|
122
|
+
raise urllib.error.HTTPError(
|
|
123
|
+
url=url, code=code, msg="synthetic", hdrs={}, fp=io.BytesIO(body),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return action
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# Route predicates, written against host+path so an accidental endpoint change
|
|
130
|
+
# in the client fails routing rather than silently following along.
|
|
131
|
+
def _is_ra(parsed, query):
|
|
132
|
+
return parsed.netloc == "doi.org" and parsed.path.startswith("/doiRA/")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _is_doi_resolve(parsed, query):
|
|
136
|
+
return parsed.netloc == "doi.org" and not parsed.path.startswith("/doiRA/")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _is_handle(parsed, query):
|
|
140
|
+
return parsed.netloc == "hdl.handle.net" and parsed.path.startswith("/api/handles/")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _is_esearch(parsed, query):
|
|
144
|
+
return parsed.path.endswith("/esearch.fcgi")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _is_esummary(parsed, query):
|
|
148
|
+
return parsed.path.endswith("/esummary.fcgi")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------- pure helpers
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_normalize_folds_the_three_measured_chinese_variants():
|
|
155
|
+
"""The shared #431 normalizer rejects three legitimate spellings of one
|
|
156
|
+
identical Chinese title (measured 2026-07-27: fullwidth 0.577/False, CJK
|
|
157
|
+
terminal punctuation 0.981/False, interior spaces 0.929/False). The
|
|
158
|
+
Chinese-aware normalizer must fold all three to one string."""
|
|
159
|
+
from chinese_literature_client import normalize_cn_title
|
|
160
|
+
|
|
161
|
+
canonical = normalize_cn_title("宫颈腺癌中ProEXC和PRMT5的表达及其临床意义")
|
|
162
|
+
assert normalize_cn_title("宫颈腺癌中ProEXC和PRMT5的表达及其临床意义") == canonical
|
|
163
|
+
assert normalize_cn_title("宫颈腺癌中ProEXC和PRMT5的表达及其临床意义。") == canonical
|
|
164
|
+
assert normalize_cn_title("宫颈腺癌中 ProEXC 和 PRMT5 的表达及其临床意义") == canonical
|
|
165
|
+
assert normalize_cn_title("《宫颈腺癌中ProEXC和PRMT5的表达及其临床意义》") == canonical
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_fuzzy_similarity_alone_never_promotes_a_different_paper():
|
|
169
|
+
"""Han-character overlap gives unrelated Chinese papers a high baseline
|
|
170
|
+
ratio (0.510 measured between two genuinely different papers), so the
|
|
171
|
+
shared fuzzy ratio is not sufficient — exact equality after Chinese-aware
|
|
172
|
+
normalization is the rule."""
|
|
173
|
+
from chinese_literature_client import _cn_titles_match
|
|
174
|
+
|
|
175
|
+
a = "宫颈腺癌中ProEXC和PRMT5的表达及其临床意义"
|
|
176
|
+
b = "宫颈癌及癌前病变组织hTERC基因表达及其临床意义"
|
|
177
|
+
assert _cn_titles_match(a, a) is True
|
|
178
|
+
assert _cn_titles_match(b, a) is False
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_legitimate_variants_match_despite_a_sub_threshold_fuzzy_ratio():
|
|
182
|
+
"""Regression for a live-smoke near-miss: the shared 0.70 ratio scores a
|
|
183
|
+
legitimate FULLWIDTH spelling of the identical title at 0.577, so ANDing it
|
|
184
|
+
in as an extra necessary condition would veto a correct match and file a
|
|
185
|
+
real paper at P0 next to the word 'fabricated'. On CJK the ratio separates
|
|
186
|
+
almost nothing (0.510 unrelated vs 0.577 identical), so it is excluded."""
|
|
187
|
+
from _text_similarity import _TITLE_SIMILARITY_THRESHOLD, _similarity
|
|
188
|
+
from chinese_literature_client import _cn_titles_match
|
|
189
|
+
|
|
190
|
+
canonical = "宫颈腺癌中ProEXC和PRMT5的表达及其临床意义"
|
|
191
|
+
fullwidth = "宫颈腺癌中ProEXC和PRMT5的表达及其临床意义。"
|
|
192
|
+
|
|
193
|
+
# The premise: the shared ratio really is below the floor for this pair.
|
|
194
|
+
assert _similarity(canonical, fullwidth) < _TITLE_SIMILARITY_THRESHOLD
|
|
195
|
+
# ...and the Chinese-aware rule still matches it.
|
|
196
|
+
assert _cn_titles_match(fullwidth, canonical) is True
|
|
197
|
+
assert _cn_titles_match(canonical, fullwidth) is True
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_doi_keyed_exact_generic_title_can_match():
|
|
201
|
+
"""The generic-title veto is for identifier-free title searches. A real,
|
|
202
|
+
DOI-keyed work may legitimately be titled Editorial."""
|
|
203
|
+
from chinese_literature_client import _cn_titles_match
|
|
204
|
+
|
|
205
|
+
assert _cn_titles_match("Editorial", "Editorial") is True
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@pytest.mark.parametrize(
|
|
209
|
+
"left,right",
|
|
210
|
+
[
|
|
211
|
+
("ER+ 乳腺癌患者的预后分析", "ER- 乳腺癌患者的预后分析"),
|
|
212
|
+
("CD4+ T细胞计数与预后", "CD4− T细胞计数与预后"),
|
|
213
|
+
("生存率提高 4.5% 的临床意义", "生存率提高 45% 的临床意义"),
|
|
214
|
+
("剂量2²的研究", "剂量22的研究"),
|
|
215
|
+
("Straße治疗研究", "Strasse治疗研究"),
|
|
216
|
+
("治疗是否有效?", "治疗是否有效"),
|
|
217
|
+
("PD L1蛋白表达", "PDL 1蛋白表达"),
|
|
218
|
+
("P53蛋白表达", "p53蛋白表达"),
|
|
219
|
+
],
|
|
220
|
+
)
|
|
221
|
+
def test_normalizer_preserves_scientifically_meaningful_distinctions(left, right):
|
|
222
|
+
from chinese_literature_client import normalize_cn_title
|
|
223
|
+
|
|
224
|
+
assert normalize_cn_title(left) != normalize_cn_title(right)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@pytest.mark.parametrize(
|
|
228
|
+
"variant",
|
|
229
|
+
[
|
|
230
|
+
CN_TITLE + ".",
|
|
231
|
+
CN_TITLE + ".",
|
|
232
|
+
"“" + CN_TITLE + "”",
|
|
233
|
+
"‘" + CN_TITLE + "’",
|
|
234
|
+
],
|
|
235
|
+
)
|
|
236
|
+
def test_normalizer_folds_conventional_terminal_periods_and_outer_quotes(variant):
|
|
237
|
+
from chinese_literature_client import normalize_cn_title
|
|
238
|
+
|
|
239
|
+
assert normalize_cn_title(variant) == normalize_cn_title(CN_TITLE)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_applicability_gate_skips_non_chinese_citations():
|
|
243
|
+
"""Mirrors _run_arxiv's skip semantics — an English citation must be
|
|
244
|
+
`skipped`, never `unmatched`, so English corpora are untouched."""
|
|
245
|
+
client = _client()
|
|
246
|
+
assert client.is_applicable({"title": "Attention Is All You Need"}) is False
|
|
247
|
+
assert client.is_applicable({"title": CN_TITLE}) is True
|
|
248
|
+
assert client.is_applicable({"container_title": CN_JOURNAL}) is True
|
|
249
|
+
assert client.is_applicable({"title": "A Study", "language": "zh"}) is True
|
|
250
|
+
# A Chinese RA on the DOI is sufficient even with a romanized title.
|
|
251
|
+
assert client.is_applicable({"title": "A Study"}, ra="ISTIC") is True
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_resolve_skips_non_chinese_without_touching_the_network():
|
|
255
|
+
"""An English citation must cost ZERO requests — including when it carries
|
|
256
|
+
a DOI. Establishing applicability from the registration agency would mean
|
|
257
|
+
one doi.org round-trip per English reference in every bibliography."""
|
|
258
|
+
client = _client()
|
|
259
|
+
with patch("chinese_literature_client._safe_urlopen", side_effect=AssertionError("no network")):
|
|
260
|
+
result = client.resolve({
|
|
261
|
+
"citation_key": "smith2020",
|
|
262
|
+
"title": "An English Paper",
|
|
263
|
+
"doi": "10.5555/english.2020.1",
|
|
264
|
+
})
|
|
265
|
+
assert result["status"] == "skipped"
|
|
266
|
+
assert result["queried_by"] is None
|
|
267
|
+
assert result["reason_code"] == "NOT_CHINESE_LITERATURE"
|
|
268
|
+
assert result["checklist_item"] is None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ------------------------------------------------------- RA triage (3 branches)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@pytest.mark.parametrize(
|
|
275
|
+
"fixture,expected",
|
|
276
|
+
[
|
|
277
|
+
("ra_istic.json", "ISTIC"),
|
|
278
|
+
("ra_cnki.json", "CNKI"),
|
|
279
|
+
("ra_crossref.json", "Crossref"),
|
|
280
|
+
],
|
|
281
|
+
)
|
|
282
|
+
def test_ra_lookup_routes_all_three_agencies(fixture, expected):
|
|
283
|
+
transport = FakeTransport([(_is_ra, _ok(_body(fixture)))])
|
|
284
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
285
|
+
assert _client().ra_for(ISTIC_DOI) == expected
|
|
286
|
+
assert transport.requests == ["https://doi.org/doiRA/10.5555"]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def test_ra_lookup_returns_none_for_an_unknown_prefix():
|
|
290
|
+
"""The endpoint does not error on an unknown prefix: it answers 200 with a
|
|
291
|
+
`status` field in place of `RA` (verified 2026-07-27: `/doiRA/10.99999` ->
|
|
292
|
+
`[{"DOI": "10.99999", "status": "DOI does not exist"}]`). The missing RA
|
|
293
|
+
key must yield None, never a degradation or a fabricated agency name."""
|
|
294
|
+
transport = FakeTransport([(_is_ra, _ok(_body("ra_unknown_prefix.json")))])
|
|
295
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
296
|
+
assert _client().ra_for("10.99999/ars.cn.unknown.2026.1") is None
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@pytest.mark.parametrize(
|
|
300
|
+
"body",
|
|
301
|
+
[
|
|
302
|
+
b"{}",
|
|
303
|
+
b"[]",
|
|
304
|
+
b"null",
|
|
305
|
+
b'[{"DOI":"10.other","RA":"ISTIC"}]',
|
|
306
|
+
b'[{"DOI":"10.5555","RA":false}]',
|
|
307
|
+
b'[{"DOI":"10.5555"}]',
|
|
308
|
+
b'[{"DOI":"10.5555","RA":"ISTIC"},{"DOI":"10.5555","RA":"CNKI"}]',
|
|
309
|
+
],
|
|
310
|
+
)
|
|
311
|
+
def test_ra_lookup_parseable_malformed_shape_is_typed_unavailable(body):
|
|
312
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
313
|
+
|
|
314
|
+
transport = FakeTransport([(_is_ra, _ok(body))])
|
|
315
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
316
|
+
with pytest.raises(ChineseLiteratureUnavailable):
|
|
317
|
+
_client().ra_for(ISTIC_DOI)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def test_ra_lookup_short_circuits_a_non_doi_without_a_request():
|
|
321
|
+
transport = FakeTransport([])
|
|
322
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
323
|
+
assert _client().ra_for("not-a-doi") is None
|
|
324
|
+
assert transport.requests == []
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def test_crossref_registered_chinese_doi_is_not_claimed():
|
|
328
|
+
"""Division of labour: a Crossref-registered Chinese DOI belongs to the
|
|
329
|
+
existing crossref resolver. It must stop without discarding the supplied
|
|
330
|
+
DOI or attempting the DOI-less coordinate branch."""
|
|
331
|
+
transport = FakeTransport([
|
|
332
|
+
(_is_ra, _ok(_body("ra_crossref.json"))),
|
|
333
|
+
(_is_doi_resolve, _http_error(500)), # must never be reached
|
|
334
|
+
(_is_handle, _http_error(500)), # must never be reached
|
|
335
|
+
])
|
|
336
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
337
|
+
result = _client(journal_map=TEST_JOURNAL_MAP).resolve({
|
|
338
|
+
"citation_key": "chen2026",
|
|
339
|
+
"title": CN_TITLE,
|
|
340
|
+
"container_title": CN_JOURNAL,
|
|
341
|
+
"doi": "10.5555/crossref.zh.2026.1",
|
|
342
|
+
})
|
|
343
|
+
assert result["reason_code"] == "DOI_RA_OUT_OF_SCOPE"
|
|
344
|
+
assert result["status"] == "skipped"
|
|
345
|
+
assert result["checklist_item"]["priority"] == "P3"
|
|
346
|
+
assert len(transport.requests) == 1 # RA lookup only
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def test_unknown_doi_ra_does_not_fall_through_to_coordinates():
|
|
350
|
+
transport = FakeTransport([
|
|
351
|
+
(_is_ra, _ok(_body("ra_unknown_prefix.json"))),
|
|
352
|
+
(_is_esearch, _http_error(500)), # must never be reached
|
|
353
|
+
])
|
|
354
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
355
|
+
result = _client(journal_map=TEST_JOURNAL_MAP).resolve(
|
|
356
|
+
_istic_entry(doi="10.99999/ars.cn.unknown.2026.1")
|
|
357
|
+
)
|
|
358
|
+
assert result["status"] == "skipped"
|
|
359
|
+
assert result["reason_code"] == "DOI_RA_UNRESOLVED"
|
|
360
|
+
assert result["checklist_item"]["priority"] == "P2"
|
|
361
|
+
assert len(transport.requests) == 1
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# ------------------------------------------------------------ ISTIC DOI branch
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _istic_entry(**overrides):
|
|
368
|
+
entry = {
|
|
369
|
+
"citation_key": "hecheng2026",
|
|
370
|
+
"title": CN_TITLE,
|
|
371
|
+
"container_title": CN_JOURNAL,
|
|
372
|
+
"year": 2026,
|
|
373
|
+
"volume": "88",
|
|
374
|
+
"pages": "8801-8809",
|
|
375
|
+
"doi": ISTIC_DOI,
|
|
376
|
+
}
|
|
377
|
+
entry.update(overrides)
|
|
378
|
+
return entry
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def test_istic_doi_hit_with_matching_chinese_title_is_matched_by_id():
|
|
382
|
+
transport = FakeTransport([
|
|
383
|
+
(_is_ra, _ok(_body("ra_istic.json"))),
|
|
384
|
+
(_is_doi_resolve, _ok(_body("istic_csl_hit.json"))),
|
|
385
|
+
])
|
|
386
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
387
|
+
result = _client().resolve(_istic_entry())
|
|
388
|
+
|
|
389
|
+
assert result["status"] == "matched"
|
|
390
|
+
assert result["queried_by"] == "id"
|
|
391
|
+
assert result["reason_code"] == "DOI_TITLE_VERIFIED"
|
|
392
|
+
assert result["evidence"]["title"] == CN_TITLE
|
|
393
|
+
assert result["evidence"]["container_title"] == CN_JOURNAL
|
|
394
|
+
assert result["evidence"]["year"] == 2026
|
|
395
|
+
assert result["checklist_item"] is None
|
|
396
|
+
# Exactly two requests: no Handle probe is needed once the title verifies.
|
|
397
|
+
assert len(transport.requests) == 2
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def test_istic_doi_hit_matches_a_typeset_variant_of_the_cited_title():
|
|
401
|
+
"""End-to-end companion to the unit regression: a citation whose title
|
|
402
|
+
differs from the record only by fullwidth forms, CJK punctuation and
|
|
403
|
+
interior spaces must MATCH, not land at P0. Typesetting differences between
|
|
404
|
+
a reference list and a metadata record are ordinary, not evidence."""
|
|
405
|
+
variant = "合成 引文核验闸 的确定性评估方法研究。"
|
|
406
|
+
transport = FakeTransport([
|
|
407
|
+
(_is_ra, _ok(_body("ra_istic.json"))),
|
|
408
|
+
(_is_doi_resolve, _ok(_body("istic_csl_hit.json"))),
|
|
409
|
+
])
|
|
410
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
411
|
+
result = _client().resolve(_istic_entry(title=variant))
|
|
412
|
+
|
|
413
|
+
assert result["status"] == "matched"
|
|
414
|
+
assert result["queried_by"] == "id"
|
|
415
|
+
assert result["checklist_item"] is None
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def test_istic_doi_resolving_to_a_different_title_is_id_keyed_unmatched():
|
|
419
|
+
"""Chimeric citation (real identifier, someone else's title). ID-keyed
|
|
420
|
+
unmatched is the C-V6(a) shape that licenses a `false`, and it is a P0 row."""
|
|
421
|
+
transport = FakeTransport([
|
|
422
|
+
(_is_ra, _ok(_body("ra_istic.json"))),
|
|
423
|
+
(_is_doi_resolve, _ok(_body("istic_csl_other_title.json"))),
|
|
424
|
+
(_is_handle, _ok(_body("handle_exists.json"))),
|
|
425
|
+
])
|
|
426
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
427
|
+
result = _client().resolve(_istic_entry())
|
|
428
|
+
|
|
429
|
+
assert result["status"] == "unmatched"
|
|
430
|
+
assert result["queried_by"] == "id"
|
|
431
|
+
assert result["reason_code"] == "DOI_TITLE_MISMATCH"
|
|
432
|
+
item = result["checklist_item"]
|
|
433
|
+
assert item["priority"] == "P0"
|
|
434
|
+
assert item["verdict_contribution"] == "false"
|
|
435
|
+
assert item["human_result"] is None
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def test_istic_english_shadow_title_is_unverifiable_never_false():
|
|
439
|
+
"""An English translation cannot refute the cited Chinese original.
|
|
440
|
+
|
|
441
|
+
PubMed and some DOI metadata surfaces expose English shadow titles for
|
|
442
|
+
Chinese articles. Without a translation oracle, a cross-script difference
|
|
443
|
+
is not a machine-verifiable chimeric citation.
|
|
444
|
+
"""
|
|
445
|
+
transport = FakeTransport([
|
|
446
|
+
(_is_ra, _ok(_body("ra_istic.json"))),
|
|
447
|
+
(_is_doi_resolve, _ok(
|
|
448
|
+
b'{"title":"Deterministic Evaluation of a Synthetic Citation Gate"}'
|
|
449
|
+
)),
|
|
450
|
+
])
|
|
451
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
452
|
+
result = _client().resolve(_istic_entry())
|
|
453
|
+
|
|
454
|
+
assert result["status"] == "unmatched"
|
|
455
|
+
assert result["queried_by"] == "title"
|
|
456
|
+
assert result["reason_code"] == "DOI_EXISTS_TITLE_UNVERIFIABLE"
|
|
457
|
+
assert result["evidence"]["resolved_title"].startswith("Deterministic")
|
|
458
|
+
assert result["checklist_item"]["verdict_contribution"] == "unresolvable"
|
|
459
|
+
assert len(transport.requests) == 2
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def test_two_different_latin_titles_are_unverifiable_not_mismatch():
|
|
463
|
+
"""Matching script alone cannot make romanized titles comparable."""
|
|
464
|
+
from chinese_literature_client import DoiTitleState
|
|
465
|
+
|
|
466
|
+
transport = FakeTransport([
|
|
467
|
+
(_is_doi_resolve, _ok(b'{"title":"Romanized title alpha"}')),
|
|
468
|
+
])
|
|
469
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
470
|
+
outcome = _client().doi_lookup_with_title_check(
|
|
471
|
+
ISTIC_DOI, "Romanized title beta",
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
assert outcome.state is DoiTitleState.UNVERIFIABLE
|
|
475
|
+
assert outcome.record["title"] == "Romanized title alpha"
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def test_lone_surrogate_doi_is_typed_unavailable_after_ra_lookup():
|
|
479
|
+
"""A schema-valid JSON string still may not be UTF-8 encodable."""
|
|
480
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
481
|
+
|
|
482
|
+
entry = _istic_entry(doi="10.5555/" + chr(0xD800))
|
|
483
|
+
transport = FakeTransport([
|
|
484
|
+
(_is_ra, _ok(_body("ra_istic.json"))),
|
|
485
|
+
])
|
|
486
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
487
|
+
with pytest.raises(ChineseLiteratureUnavailable, match="invalid Unicode"):
|
|
488
|
+
_client().resolve(entry)
|
|
489
|
+
|
|
490
|
+
assert len(transport.requests) == 1
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def test_fabricated_doi_is_refuted_by_404_plus_handle_100():
|
|
494
|
+
"""The fabrication canary: these prefixes carry no wildcard catch-all, so a
|
|
495
|
+
404 + responseCode 100 is positive evidence the identifier does not exist."""
|
|
496
|
+
transport = FakeTransport([
|
|
497
|
+
(_is_ra, _ok(_body("ra_istic.json"))),
|
|
498
|
+
(_is_doi_resolve, _http_error(404)),
|
|
499
|
+
(_is_handle, _ok(_body("handle_absent.json"))),
|
|
500
|
+
])
|
|
501
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
502
|
+
result = _client().resolve(_istic_entry(doi=FABRICATED_DOI))
|
|
503
|
+
|
|
504
|
+
assert result["status"] == "unmatched"
|
|
505
|
+
assert result["queried_by"] == "id"
|
|
506
|
+
assert result["reason_code"] == "DOI_REFUTED"
|
|
507
|
+
item = result["checklist_item"]
|
|
508
|
+
assert item["priority"] == "P0"
|
|
509
|
+
assert item["verdict_contribution"] == "false"
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def test_doi_404_is_a_negative_not_a_degradation():
|
|
513
|
+
"""A 404 on the DOI path must be reported as data. Turning it into an
|
|
514
|
+
outage would throw away the resolver's strongest signal."""
|
|
515
|
+
from chinese_literature_client import (
|
|
516
|
+
ChineseLiteratureUnavailable,
|
|
517
|
+
DoiTitleState,
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
transport = FakeTransport([(_is_doi_resolve, _http_error(404))])
|
|
521
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
522
|
+
client = _client()
|
|
523
|
+
try:
|
|
524
|
+
outcome = client.doi_lookup_with_title_check(FABRICATED_DOI, CN_TITLE)
|
|
525
|
+
assert outcome.state is DoiTitleState.NOT_FOUND
|
|
526
|
+
assert outcome.record is None
|
|
527
|
+
except ChineseLiteratureUnavailable: # pragma: no cover - failure path
|
|
528
|
+
pytest.fail("a DOI 404 must be a negative, not a degradation")
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def test_doi_keyed_editorial_title_verifies_end_to_end():
|
|
532
|
+
from chinese_literature_client import DoiTitleState
|
|
533
|
+
|
|
534
|
+
transport = FakeTransport([
|
|
535
|
+
(_is_doi_resolve, _ok(b'{"title": "Editorial"}')),
|
|
536
|
+
])
|
|
537
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
538
|
+
outcome = _client().doi_lookup_with_title_check(ISTIC_DOI, "Editorial")
|
|
539
|
+
assert outcome.state is DoiTitleState.MATCH
|
|
540
|
+
assert outcome.record["title"] == "Editorial"
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def test_doi_404_plus_existing_handle_is_title_unverifiable_never_false():
|
|
544
|
+
transport = FakeTransport([
|
|
545
|
+
(_is_ra, _ok(_body("ra_istic.json"))),
|
|
546
|
+
(_is_doi_resolve, _http_error(404)),
|
|
547
|
+
(_is_handle, _ok(_body("handle_exists.json"))),
|
|
548
|
+
])
|
|
549
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
550
|
+
result = _client().resolve(_istic_entry())
|
|
551
|
+
|
|
552
|
+
assert result["status"] == "unmatched"
|
|
553
|
+
assert result["queried_by"] == "title"
|
|
554
|
+
assert result["reason_code"] == "DOI_EXISTS_TITLE_UNVERIFIABLE"
|
|
555
|
+
assert result["checklist_item"]["verdict_contribution"] == "unresolvable"
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def test_empty_expected_title_is_unverifiable_never_mismatch_or_false():
|
|
559
|
+
from chinese_literature_client import DoiTitleState
|
|
560
|
+
|
|
561
|
+
direct = FakeTransport([
|
|
562
|
+
(_is_doi_resolve, _ok(_body("istic_csl_hit.json"))),
|
|
563
|
+
])
|
|
564
|
+
with patch("chinese_literature_client._safe_urlopen", direct):
|
|
565
|
+
outcome = _client().doi_lookup_with_title_check(ISTIC_DOI, "")
|
|
566
|
+
assert outcome.state is DoiTitleState.UNVERIFIABLE
|
|
567
|
+
assert outcome.record["title"] == CN_TITLE
|
|
568
|
+
|
|
569
|
+
transport = FakeTransport([
|
|
570
|
+
(_is_ra, _ok(_body("ra_istic.json"))),
|
|
571
|
+
(_is_doi_resolve, _ok(_body("istic_csl_hit.json"))),
|
|
572
|
+
])
|
|
573
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
574
|
+
result = _client().resolve(_istic_entry(title=""))
|
|
575
|
+
assert result["reason_code"] == "DOI_EXISTS_TITLE_UNVERIFIABLE"
|
|
576
|
+
assert result["queried_by"] == "title"
|
|
577
|
+
assert result["checklist_item"]["verdict_contribution"] == "unresolvable"
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def test_normalized_empty_resolved_title_is_unverifiable_not_mismatch():
|
|
581
|
+
from chinese_literature_client import DoiTitleState
|
|
582
|
+
|
|
583
|
+
transport = FakeTransport([
|
|
584
|
+
(_is_doi_resolve, _ok('{"title": "。"}'.encode())),
|
|
585
|
+
])
|
|
586
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
587
|
+
outcome = _client().doi_lookup_with_title_check(ISTIC_DOI, CN_TITLE)
|
|
588
|
+
assert outcome.state is DoiTitleState.UNVERIFIABLE
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
# ------------------------------------------------------------- CNKI DOI branch
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def test_cnki_doi_that_exists_is_never_matched():
|
|
595
|
+
"""The CNKI RA serves no CSL-JSON and we refuse to parse its HTML, so
|
|
596
|
+
existence alone must degrade to title-keyed unmatched (-> unresolvable),
|
|
597
|
+
never `matched` — otherwise a real DOI with a fabricated title passes."""
|
|
598
|
+
transport = FakeTransport([
|
|
599
|
+
(_is_ra, _ok(_body("ra_cnki.json"))),
|
|
600
|
+
(_is_handle, _ok(_body("handle_exists.json"))),
|
|
601
|
+
(_is_doi_resolve, _http_error(500)), # must never be reached
|
|
602
|
+
])
|
|
603
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
604
|
+
result = _client().resolve(_istic_entry(doi=CNKI_DOI))
|
|
605
|
+
|
|
606
|
+
assert result["status"] == "unmatched"
|
|
607
|
+
assert result["queried_by"] == "title" # NOT "id" -> reduces to unresolvable
|
|
608
|
+
assert result["reason_code"] == "DOI_EXISTS_TITLE_UNVERIFIABLE"
|
|
609
|
+
item = result["checklist_item"]
|
|
610
|
+
assert item["priority"] == "P2"
|
|
611
|
+
assert item["verdict_contribution"] == "unresolvable"
|
|
612
|
+
assert item["verification_urls"] == ["https://doi.org/" + CNKI_DOI]
|
|
613
|
+
# Content negotiation is never attempted for a CNKI prefix.
|
|
614
|
+
assert len(transport.requests) == 2
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def test_cnki_doi_that_does_not_exist_is_still_refuted():
|
|
618
|
+
transport = FakeTransport([
|
|
619
|
+
(_is_ra, _ok(_body("ra_cnki.json"))),
|
|
620
|
+
(_is_handle, _ok(_body("handle_absent.json"))),
|
|
621
|
+
])
|
|
622
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
623
|
+
result = _client().resolve(_istic_entry(doi=CNKI_DOI))
|
|
624
|
+
|
|
625
|
+
assert result["queried_by"] == "id"
|
|
626
|
+
assert result["reason_code"] == "DOI_REFUTED"
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def test_non_csl_200_body_is_typed_unavailable():
|
|
630
|
+
"""A 200 non-JSON body can be a proxy/CDN error page. It must fail closed,
|
|
631
|
+
not become routine human-check evidence."""
|
|
632
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
633
|
+
|
|
634
|
+
transport = FakeTransport([
|
|
635
|
+
(_is_ra, _ok(_body("ra_istic.json"))),
|
|
636
|
+
(_is_doi_resolve, _ok(_body("cnki_landing_page.html"))),
|
|
637
|
+
])
|
|
638
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
639
|
+
with pytest.raises(ChineseLiteratureUnavailable):
|
|
640
|
+
_client().resolve(_istic_entry())
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def test_parseable_csl_with_malformed_author_shape_is_typed_unavailable():
|
|
644
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
645
|
+
|
|
646
|
+
malformed = ('{"title": "' + CN_TITLE + '", "author": 42}').encode()
|
|
647
|
+
transport = FakeTransport([(_is_doi_resolve, _ok(malformed))])
|
|
648
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
649
|
+
with pytest.raises(ChineseLiteratureUnavailable):
|
|
650
|
+
_client().doi_lookup_with_title_check(ISTIC_DOI, CN_TITLE)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def test_parseable_csl_with_malformed_root_is_typed_unavailable():
|
|
654
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
655
|
+
|
|
656
|
+
transport = FakeTransport([(_is_doi_resolve, _ok(b'[{"title": "x"}]'))])
|
|
657
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
658
|
+
with pytest.raises(ChineseLiteratureUnavailable):
|
|
659
|
+
_client().doi_lookup_with_title_check(ISTIC_DOI, CN_TITLE)
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
@pytest.mark.parametrize("endpoint", ["ra", "csl"])
|
|
663
|
+
def test_deeply_nested_json_is_typed_unavailable(endpoint):
|
|
664
|
+
"""A small but adversarial upstream body must not leak RecursionError."""
|
|
665
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
666
|
+
|
|
667
|
+
body = b"[" * 2000 + b"]" * 2000
|
|
668
|
+
predicate = _is_ra if endpoint == "ra" else _is_doi_resolve
|
|
669
|
+
transport = FakeTransport([(predicate, _ok(body))])
|
|
670
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
671
|
+
with pytest.raises(ChineseLiteratureUnavailable):
|
|
672
|
+
if endpoint == "ra":
|
|
673
|
+
_client().ra_for(ISTIC_DOI)
|
|
674
|
+
else:
|
|
675
|
+
_client().doi_lookup_with_title_check(ISTIC_DOI, CN_TITLE)
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
@pytest.mark.parametrize(
|
|
679
|
+
"body",
|
|
680
|
+
[
|
|
681
|
+
b'{"title": {"value": "x"}}',
|
|
682
|
+
b'{"title": [42]}',
|
|
683
|
+
b'{"title": []}',
|
|
684
|
+
b'{"title": "x", "author": [42]}',
|
|
685
|
+
b'{"title": "x", "author": false}',
|
|
686
|
+
b'{"title": "x", "author": [{"given": 42}]}',
|
|
687
|
+
b'{"title": "x", "issued": []}',
|
|
688
|
+
b'{"title": "x", "issued": {"date-parts": {}}}',
|
|
689
|
+
b'{"title": "x", "issued": {"date-parts": [[true]]}}',
|
|
690
|
+
b'{"title": "x", "issued": {"date-parts": [[-1]]}}',
|
|
691
|
+
'{"title": "x", "issued": {"date-parts": [["٢٠٢٦"]]}}'.encode(),
|
|
692
|
+
b'{"title": "x", "issued": {"date-parts": [["99999"]]}}',
|
|
693
|
+
b'{"title": "x", "volume": {"value": "1"}}',
|
|
694
|
+
b'{"title": "x", "DOI": ["10.5555/x"]}',
|
|
695
|
+
(b'{"title": "' + CN_TITLE.encode() + b'", "DOI": ""}'),
|
|
696
|
+
(b'{"title": "' + CN_TITLE.encode()
|
|
697
|
+
+ b'", "DOI": "10.5555/a.different.record"}'),
|
|
698
|
+
],
|
|
699
|
+
)
|
|
700
|
+
def test_parseable_csl_with_malformed_fields_is_typed_unavailable(body):
|
|
701
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
702
|
+
|
|
703
|
+
transport = FakeTransport([(_is_doi_resolve, _ok(body))])
|
|
704
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
705
|
+
with pytest.raises(ChineseLiteratureUnavailable):
|
|
706
|
+
_client().doi_lookup_with_title_check(ISTIC_DOI, CN_TITLE)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def test_handle_unknown_response_code_degrades():
|
|
710
|
+
"""responseCode 2 (internal error) is an unknown state — degrade, do not
|
|
711
|
+
guess an existence answer."""
|
|
712
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
713
|
+
|
|
714
|
+
transport = FakeTransport([(_is_handle, _ok(_body("handle_internal_error.json")))])
|
|
715
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
716
|
+
with pytest.raises(ChineseLiteratureUnavailable):
|
|
717
|
+
_client().handle_exists(ISTIC_DOI)
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@pytest.mark.parametrize(
|
|
721
|
+
"body",
|
|
722
|
+
[b'{"responseCode": true}', b'{"responseCode": 100.0}'],
|
|
723
|
+
)
|
|
724
|
+
def test_handle_bool_or_float_response_code_degrades(body):
|
|
725
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
726
|
+
|
|
727
|
+
transport = FakeTransport([(_is_handle, _ok(body))])
|
|
728
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
729
|
+
with pytest.raises(ChineseLiteratureUnavailable):
|
|
730
|
+
_client().handle_exists(ISTIC_DOI)
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
# --------------------------------------------------- PubMed coordinate branch
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def _coordinate_entry(**overrides):
|
|
737
|
+
entry = {
|
|
738
|
+
"citation_key": "hecheng2026",
|
|
739
|
+
"title": CN_TITLE,
|
|
740
|
+
"container_title": CN_JOURNAL,
|
|
741
|
+
"year": 2026,
|
|
742
|
+
"volume": "88",
|
|
743
|
+
"pages": "8801-8809",
|
|
744
|
+
}
|
|
745
|
+
entry.update(overrides)
|
|
746
|
+
return entry
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def _coordinate_client():
|
|
750
|
+
return _client(journal_map=TEST_JOURNAL_MAP)
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def test_missing_coordinate_tuple_is_p3_without_a_pubmed_request():
|
|
754
|
+
with patch(
|
|
755
|
+
"chinese_literature_client._safe_urlopen",
|
|
756
|
+
side_effect=AssertionError("no PubMed request"),
|
|
757
|
+
):
|
|
758
|
+
result = _coordinate_client().resolve(
|
|
759
|
+
_coordinate_entry(volume=None, pages=None, year=None)
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
assert result["status"] == "skipped"
|
|
763
|
+
assert result["queried_by"] is None
|
|
764
|
+
assert result["reason_code"] == "INSUFFICIENT_PUBMED_COORDINATES"
|
|
765
|
+
assert result["checklist_item"]["priority"] == "P3"
|
|
766
|
+
assert result["checklist_item"]["verdict_contribution"] == "unresolvable"
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def test_lone_surrogate_eutils_parameter_is_typed_before_transport():
|
|
770
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
771
|
+
|
|
772
|
+
with patch(
|
|
773
|
+
"chinese_literature_client._safe_urlopen",
|
|
774
|
+
side_effect=AssertionError("no request for unencodable EUtils term"),
|
|
775
|
+
):
|
|
776
|
+
with pytest.raises(ChineseLiteratureUnavailable, match="invalid Unicode"):
|
|
777
|
+
_coordinate_client().journal_is_indexed("Journal " + chr(0xD800))
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def test_volume_page_zero_hit_never_falls_back_to_author_year():
|
|
781
|
+
"""A valid author/year candidate must not wash a cited wrong page into a
|
|
782
|
+
match after the stronger volume/page query returned zero."""
|
|
783
|
+
esearch_bodies = [
|
|
784
|
+
_body("esearch_coverage_hit.json"),
|
|
785
|
+
_body("esearch_coordinate_zero.json"),
|
|
786
|
+
]
|
|
787
|
+
transport = FakeTransport([
|
|
788
|
+
(_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
|
|
789
|
+
])
|
|
790
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
791
|
+
result = _coordinate_client().resolve(
|
|
792
|
+
_coordinate_entry(pages="9999", first_author_pinyin="Hecheng Ceshi")
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
assert result["reason_code"] == "PUBMED_INDEXED_BUT_COORDINATE_MISS"
|
|
796
|
+
assert len(transport.requests) == 2 # coverage + volume/page, no fallback
|
|
797
|
+
assert result["checklist_item"]["attempts"][-1]["detail"] == "volume_page"
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def test_pubmed_candidate_page_conflict_cannot_be_promoted():
|
|
801
|
+
esearch_bodies = [
|
|
802
|
+
_body("esearch_coverage_hit.json"),
|
|
803
|
+
_body("esearch_coordinate_hit.json"),
|
|
804
|
+
]
|
|
805
|
+
transport = FakeTransport([
|
|
806
|
+
(_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
|
|
807
|
+
(_is_esummary, _ok(_body("esummary_hit.json"))),
|
|
808
|
+
])
|
|
809
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
810
|
+
result = _coordinate_client().resolve(_coordinate_entry(pages="9999"))
|
|
811
|
+
|
|
812
|
+
assert result["reason_code"] == "PUBMED_COORDINATE_CANDIDATE_UNVERIFIED"
|
|
813
|
+
assert result["status"] == "unmatched"
|
|
814
|
+
assert "first page" in result["checklist_item"]["human_action"]
|
|
815
|
+
assert len(transport.requests) == 3 # no DOI/RA lookup after conflict
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def test_pubmed_candidate_volume_conflict_cannot_be_promoted():
|
|
819
|
+
"""A unique hit with a different volume stops before DOI/title binding."""
|
|
820
|
+
esearch_bodies = [
|
|
821
|
+
_body("esearch_coverage_hit.json"),
|
|
822
|
+
_body("esearch_coordinate_hit.json"),
|
|
823
|
+
]
|
|
824
|
+
transport = FakeTransport([
|
|
825
|
+
(_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
|
|
826
|
+
(_is_esummary, _ok(_body("esummary_hit.json"))),
|
|
827
|
+
])
|
|
828
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
829
|
+
result = _coordinate_client().resolve(_coordinate_entry(volume="99"))
|
|
830
|
+
|
|
831
|
+
assert result["reason_code"] == "PUBMED_COORDINATE_CANDIDATE_UNVERIFIED"
|
|
832
|
+
assert result["status"] == "unmatched"
|
|
833
|
+
assert "volume" in result["checklist_item"]["human_action"]
|
|
834
|
+
assert len(transport.requests) == 3 # no DOI/RA lookup after conflict
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
def test_pubmed_coordinate_candidate_is_matched_only_after_istic_title_binding():
|
|
838
|
+
"""Coordinates nominate a candidate; its ISTIC DOI and Chinese CSL title
|
|
839
|
+
provide the evidence that upgrades it to a title-keyed match."""
|
|
840
|
+
esearch_bodies = [_body("esearch_coverage_hit.json"),
|
|
841
|
+
_body("esearch_coordinate_hit.json")]
|
|
842
|
+
transport = FakeTransport([
|
|
843
|
+
(_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
|
|
844
|
+
(_is_esummary, _ok(_body("esummary_hit.json"))),
|
|
845
|
+
(_is_ra, _ok(_body("ra_istic.json"))),
|
|
846
|
+
(_is_doi_resolve, _ok(_body("istic_csl_hit.json"))),
|
|
847
|
+
])
|
|
848
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
849
|
+
result = _coordinate_client().resolve(_coordinate_entry())
|
|
850
|
+
|
|
851
|
+
assert result["status"] == "matched"
|
|
852
|
+
assert result["queried_by"] == "title"
|
|
853
|
+
assert result["reason_code"] == "PUBMED_COORDINATE_VERIFIED"
|
|
854
|
+
assert result["evidence"]["pmid"] == "999999901"
|
|
855
|
+
assert result["evidence"]["doi"] == ISTIC_DOI
|
|
856
|
+
assert result["evidence"]["doi_metadata"]["title"] == CN_TITLE
|
|
857
|
+
# PubMed stores the ENGLISH bracketed shadow title for Chinese articles, so
|
|
858
|
+
# a Chinese title cross-check is structurally impossible on this branch —
|
|
859
|
+
# the shadow title is carried for the human instead.
|
|
860
|
+
assert result["evidence"]["english_title"].startswith("[")
|
|
861
|
+
assert result["checklist_item"] is None
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def test_pubmed_coordinate_miss_is_p1_never_false():
|
|
865
|
+
"""The single most important self-restraint in the design: coverage is
|
|
866
|
+
confirmed and the coordinates return nothing, yet the outcome stays
|
|
867
|
+
title-keyed (-> unresolvable). The strength goes into the P1 priority, not
|
|
868
|
+
into the verdict — the journal bridge is heuristic and PubMed indexes
|
|
869
|
+
Chinese journals selectively."""
|
|
870
|
+
esearch_bodies = [_body("esearch_coverage_hit.json"),
|
|
871
|
+
_body("esearch_coordinate_zero.json")]
|
|
872
|
+
transport = FakeTransport([
|
|
873
|
+
(_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
|
|
874
|
+
])
|
|
875
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
876
|
+
result = _coordinate_client().resolve(_coordinate_entry(pages="9999"))
|
|
877
|
+
|
|
878
|
+
assert result["status"] == "unmatched"
|
|
879
|
+
assert result["queried_by"] == "title"
|
|
880
|
+
assert result["reason_code"] == "PUBMED_INDEXED_BUT_COORDINATE_MISS"
|
|
881
|
+
item = result["checklist_item"]
|
|
882
|
+
assert item["priority"] == "P1"
|
|
883
|
+
assert item["verdict_contribution"] == "unresolvable"
|
|
884
|
+
# Wording discipline: fabrication vocabulary is P0-only.
|
|
885
|
+
assert "pending human check" in item["human_action"].lower()
|
|
886
|
+
assert "fabricat" not in item["human_action"].lower()
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
def test_pubmed_coordinate_ambiguity_never_picks_one():
|
|
890
|
+
esearch_bodies = [_body("esearch_coverage_hit.json"),
|
|
891
|
+
_body("esearch_coordinate_ambiguous.json")]
|
|
892
|
+
transport = FakeTransport([
|
|
893
|
+
(_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
|
|
894
|
+
])
|
|
895
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
896
|
+
result = _coordinate_client().resolve(_coordinate_entry())
|
|
897
|
+
|
|
898
|
+
assert result["reason_code"] == "PUBMED_COORDINATE_AMBIGUOUS"
|
|
899
|
+
assert result["queried_by"] == "title"
|
|
900
|
+
assert result["evidence"] is None
|
|
901
|
+
assert result["checklist_item"]["priority"] == "P1"
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
def test_pubmed_year_disagreement_demotes_the_hit():
|
|
905
|
+
esearch_bodies = [_body("esearch_coverage_hit.json"),
|
|
906
|
+
_body("esearch_coordinate_hit.json")]
|
|
907
|
+
transport = FakeTransport([
|
|
908
|
+
(_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
|
|
909
|
+
(_is_esummary, _ok(_body("esummary_year_mismatch.json"))),
|
|
910
|
+
])
|
|
911
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
912
|
+
result = _coordinate_client().resolve(_coordinate_entry())
|
|
913
|
+
|
|
914
|
+
assert result["status"] == "unmatched"
|
|
915
|
+
assert result["queried_by"] == "title"
|
|
916
|
+
assert result["reason_code"] == "PUBMED_COORDINATE_CANDIDATE_UNVERIFIED"
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
def test_pubmed_issn_disagreement_demotes_the_hit():
|
|
920
|
+
"""The coordinate hit's corroboration is structural, so the ISSN the record
|
|
921
|
+
echoes must be the ISSN we bridged through — a disagreement means the
|
|
922
|
+
journal-name mapping, not the citation, is likely at fault."""
|
|
923
|
+
esearch_bodies = [_body("esearch_coverage_hit.json"),
|
|
924
|
+
_body("esearch_coordinate_hit.json")]
|
|
925
|
+
transport = FakeTransport([
|
|
926
|
+
(_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
|
|
927
|
+
(_is_esummary, _ok(_body("esummary_issn_mismatch.json"))),
|
|
928
|
+
])
|
|
929
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
930
|
+
result = _coordinate_client().resolve(_coordinate_entry())
|
|
931
|
+
|
|
932
|
+
assert result["status"] == "unmatched"
|
|
933
|
+
assert result["queried_by"] == "title"
|
|
934
|
+
assert result["reason_code"] == "PUBMED_COORDINATE_CANDIDATE_UNVERIFIED"
|
|
935
|
+
assert "ISSN" in result["checklist_item"]["human_action"]
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def test_absent_year_does_not_block_doi_title_binding():
|
|
939
|
+
"""Absence is not mismatch: exact Chinese title binding still verifies."""
|
|
940
|
+
esearch_bodies = [_body("esearch_coverage_hit.json"),
|
|
941
|
+
_body("esearch_coordinate_hit.json")]
|
|
942
|
+
transport = FakeTransport([
|
|
943
|
+
(_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
|
|
944
|
+
(_is_esummary, _ok(_body("esummary_hit.json"))),
|
|
945
|
+
(_is_ra, _ok(_body("ra_istic.json"))),
|
|
946
|
+
(_is_doi_resolve, _ok(_body("istic_csl_hit.json"))),
|
|
947
|
+
])
|
|
948
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
949
|
+
result = _coordinate_client().resolve(_coordinate_entry(year=None))
|
|
950
|
+
|
|
951
|
+
assert result["status"] == "matched"
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def test_pubmed_coordinate_candidate_without_doi_is_never_matched():
|
|
955
|
+
esearch_bodies = [_body("esearch_coverage_hit.json"),
|
|
956
|
+
_body("esearch_coordinate_hit.json")]
|
|
957
|
+
transport = FakeTransport([
|
|
958
|
+
(_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
|
|
959
|
+
(_is_esummary, _ok(_body("esummary_no_doi.json"))),
|
|
960
|
+
])
|
|
961
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
962
|
+
result = _coordinate_client().resolve(_coordinate_entry())
|
|
963
|
+
|
|
964
|
+
assert result["status"] == "unmatched"
|
|
965
|
+
assert result["queried_by"] == "title"
|
|
966
|
+
assert result["reason_code"] == "PUBMED_COORDINATE_CANDIDATE_UNVERIFIED"
|
|
967
|
+
assert result["checklist_item"]["verdict_contribution"] == "unresolvable"
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
def test_pubmed_candidate_doi_title_mismatch_never_washes_fake_chinese_title():
|
|
971
|
+
esearch_bodies = [_body("esearch_coverage_hit.json"),
|
|
972
|
+
_body("esearch_coordinate_hit.json")]
|
|
973
|
+
transport = FakeTransport([
|
|
974
|
+
(_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
|
|
975
|
+
(_is_esummary, _ok(_body("esummary_hit.json"))),
|
|
976
|
+
(_is_ra, _ok(_body("ra_istic.json"))),
|
|
977
|
+
(_is_doi_resolve, _ok(_body("istic_csl_other_title.json"))),
|
|
978
|
+
])
|
|
979
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
980
|
+
result = _coordinate_client().resolve(_coordinate_entry())
|
|
981
|
+
|
|
982
|
+
assert result["status"] == "unmatched"
|
|
983
|
+
assert result["queried_by"] == "title"
|
|
984
|
+
assert result["reason_code"] == "PUBMED_COORDINATE_CANDIDATE_UNVERIFIED"
|
|
985
|
+
assert result["checklist_item"]["verdict_contribution"] == "unresolvable"
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
@pytest.mark.parametrize(
|
|
989
|
+
"esummary_fixture,ra_fixture",
|
|
990
|
+
[
|
|
991
|
+
("esummary_hit.json", "ra_crossref.json"),
|
|
992
|
+
("esummary_unknown_ra.json", "ra_unknown_prefix.json"),
|
|
993
|
+
],
|
|
994
|
+
)
|
|
995
|
+
def test_pubmed_candidate_out_of_scope_or_unknown_ra_is_never_matched(
|
|
996
|
+
esummary_fixture, ra_fixture,
|
|
997
|
+
):
|
|
998
|
+
esearch_bodies = [_body("esearch_coverage_hit.json"),
|
|
999
|
+
_body("esearch_coordinate_hit.json")]
|
|
1000
|
+
transport = FakeTransport([
|
|
1001
|
+
(_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
|
|
1002
|
+
(_is_esummary, _ok(_body(esummary_fixture))),
|
|
1003
|
+
(_is_ra, _ok(_body(ra_fixture))),
|
|
1004
|
+
])
|
|
1005
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
1006
|
+
result = _coordinate_client().resolve(_coordinate_entry())
|
|
1007
|
+
|
|
1008
|
+
assert result["status"] == "unmatched"
|
|
1009
|
+
assert result["queried_by"] == "title"
|
|
1010
|
+
assert result["reason_code"] == "PUBMED_COORDINATE_CANDIDATE_UNVERIFIED"
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
def test_pubmed_candidate_cnki_handle_existence_is_never_matched():
|
|
1014
|
+
esearch_bodies = [_body("esearch_coverage_hit.json"),
|
|
1015
|
+
_body("esearch_coordinate_hit.json")]
|
|
1016
|
+
transport = FakeTransport([
|
|
1017
|
+
(_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
|
|
1018
|
+
(_is_esummary, _ok(_body("esummary_hit.json"))),
|
|
1019
|
+
(_is_ra, _ok(_body("ra_cnki.json"))),
|
|
1020
|
+
(_is_handle, _ok(_body("handle_exists.json"))),
|
|
1021
|
+
])
|
|
1022
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
1023
|
+
result = _coordinate_client().resolve(_coordinate_entry())
|
|
1024
|
+
|
|
1025
|
+
assert result["status"] == "unmatched"
|
|
1026
|
+
assert result["reason_code"] == "PUBMED_COORDINATE_CANDIDATE_UNVERIFIED"
|
|
1027
|
+
assert any(
|
|
1028
|
+
attempt["stage"] == "pubmed_candidate_handle"
|
|
1029
|
+
and attempt["outcome"] == "exists"
|
|
1030
|
+
for attempt in result["checklist_item"]["attempts"]
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def test_journal_catalogued_but_not_indexed_is_skipped_not_unmatched():
|
|
1035
|
+
"""'In the NLM Catalog' != 'indexed in PubMed' (中国全科医学 is catalogued
|
|
1036
|
+
with zero PubMed articles). A journal PubMed never indexed must produce
|
|
1037
|
+
`skipped` + a P3 row, because a coordinate miss against it says nothing."""
|
|
1038
|
+
transport = FakeTransport([
|
|
1039
|
+
(_is_esearch, _ok(_body("esearch_coverage_zero.json"))),
|
|
1040
|
+
])
|
|
1041
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
1042
|
+
result = _coordinate_client().resolve(_coordinate_entry())
|
|
1043
|
+
|
|
1044
|
+
assert result["status"] == "skipped"
|
|
1045
|
+
assert result["queried_by"] is None
|
|
1046
|
+
assert result["reason_code"] == "JOURNAL_NOT_INDEXED"
|
|
1047
|
+
assert result["checklist_item"]["priority"] == "P3"
|
|
1048
|
+
# Coverage confirmation only — the coordinate query is never issued.
|
|
1049
|
+
assert len(transport.requests) == 1
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def test_unmapped_journal_is_skipped_with_a_p3_row():
|
|
1053
|
+
"""A gap in OUR bridge table is never evidence about the citation."""
|
|
1054
|
+
transport = FakeTransport([])
|
|
1055
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
1056
|
+
result = _coordinate_client().resolve(
|
|
1057
|
+
_coordinate_entry(container_title="某本尚未映射的中文期刊"))
|
|
1058
|
+
|
|
1059
|
+
assert result["status"] == "skipped"
|
|
1060
|
+
assert result["reason_code"] == "NO_ISSN_MAPPING"
|
|
1061
|
+
assert result["checklist_item"]["priority"] == "P3"
|
|
1062
|
+
assert transport.requests == []
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
def test_seed_journal_map_rows_are_lookup_reachable():
|
|
1066
|
+
"""The shipped seed rows (all verified live 2026-07-27) must be reachable
|
|
1067
|
+
through the same normalization the resolver applies, including 《》 wrapping."""
|
|
1068
|
+
client = _client()
|
|
1069
|
+
row = client.journal_bridge("中华医学杂志")
|
|
1070
|
+
assert row is not None and row["nlm_ta"] == "Zhonghua Yi Xue Za Zhi"
|
|
1071
|
+
assert client.journal_bridge("《中华医学杂志》") == row
|
|
1072
|
+
assert client.journal_bridge("中华内科杂志")["issn"] == "0578-1426"
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
def test_caller_journal_map_extends_the_seed():
|
|
1076
|
+
"""Documented user extension point: caller rows merge over the seed."""
|
|
1077
|
+
client = _client(journal_map=TEST_JOURNAL_MAP)
|
|
1078
|
+
assert client.journal_bridge(CN_JOURNAL)["nlm_ta"] == "Hecheng Ceshi Yi Xue Za Zhi"
|
|
1079
|
+
assert client.journal_bridge("中华医学杂志") is not None # seed still present
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def test_ncbi_api_key_is_passed_through_but_does_not_relax_pacing():
|
|
1083
|
+
from chinese_literature_client import _EUTILS_MIN_INTERVAL
|
|
1084
|
+
|
|
1085
|
+
transport = FakeTransport([(_is_esearch, _ok(_body("esearch_coverage_hit.json")))])
|
|
1086
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
1087
|
+
client = _client(ncbi_api_key="test-ncbi-key")
|
|
1088
|
+
client.journal_is_indexed("Hecheng Ceshi Yi Xue Za Zhi")
|
|
1089
|
+
|
|
1090
|
+
assert "api_key=test-ncbi-key" in transport.requests[0]
|
|
1091
|
+
assert "tool=academic-research-skills" in transport.requests[0]
|
|
1092
|
+
assert "email=ars-tests%40example.invalid" in transport.requests[0]
|
|
1093
|
+
assert _EUTILS_MIN_INTERVAL == 0.34 # the keyless NCBI floor, unconditionally
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def test_ncbi_email_is_required_before_any_eutils_request():
|
|
1097
|
+
from chinese_literature_client import (
|
|
1098
|
+
ChineseLiteratureClient,
|
|
1099
|
+
ChineseLiteratureUnavailable,
|
|
1100
|
+
)
|
|
1101
|
+
|
|
1102
|
+
with patch("chinese_literature_client._safe_urlopen", side_effect=AssertionError("no request")):
|
|
1103
|
+
with pytest.raises(ChineseLiteratureUnavailable, match="ncbi_email"):
|
|
1104
|
+
ChineseLiteratureClient().journal_is_indexed("Hecheng Ceshi Yi Xue Za Zhi")
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def test_pubmed_author_fallback_uses_first_author_tag():
|
|
1108
|
+
bodies = [_body("esearch_coordinate_zero.json")]
|
|
1109
|
+
transport = FakeTransport([
|
|
1110
|
+
(_is_esearch, lambda url: _ok(bodies.pop(0))(url)),
|
|
1111
|
+
])
|
|
1112
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
1113
|
+
_client().pubmed_coordinate_lookup(
|
|
1114
|
+
nlm_ta="Hecheng Ceshi Yi Xue Za Zhi",
|
|
1115
|
+
first_author="Hecheng Ceshi",
|
|
1116
|
+
year=2026,
|
|
1117
|
+
)
|
|
1118
|
+
term = urllib.parse.parse_qs(
|
|
1119
|
+
urllib.parse.urlsplit(transport.requests[0]).query
|
|
1120
|
+
)["term"][0]
|
|
1121
|
+
assert '"Hecheng Ceshi"[1au]' in term
|
|
1122
|
+
assert "[au]" not in term
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
@pytest.mark.parametrize(
|
|
1126
|
+
"kwargs",
|
|
1127
|
+
[
|
|
1128
|
+
{"volume": "88[vi] OR 1", "pages": "8801"},
|
|
1129
|
+
{"volume": "88", "pages": "8801[pg] OR 1"},
|
|
1130
|
+
{"volume": "1:9999", "pages": "8801"},
|
|
1131
|
+
{"first_author": 'Chen" OR cancer[ti]', "year": 2026},
|
|
1132
|
+
],
|
|
1133
|
+
)
|
|
1134
|
+
def test_untrusted_pubmed_fields_cannot_inject_entrez_grammar(kwargs):
|
|
1135
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
1136
|
+
|
|
1137
|
+
with patch("chinese_literature_client._safe_urlopen", side_effect=AssertionError("no request")):
|
|
1138
|
+
with pytest.raises(ChineseLiteratureUnavailable):
|
|
1139
|
+
_client().pubmed_coordinate_lookup(
|
|
1140
|
+
nlm_ta="Hecheng Ceshi Yi Xue Za Zhi", **kwargs
|
|
1141
|
+
)
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
# -------------------------------------------------- URL hygiene / credentials
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
def test_malicious_doi_is_encoded_and_never_crashes():
|
|
1148
|
+
"""A DOI carrying CRLF / query-injection characters must be percent-encoded
|
|
1149
|
+
before it reaches the transport (sibling quote discipline) — no raw control
|
|
1150
|
+
character or `?`/`#` may survive into the request line, and the client must
|
|
1151
|
+
never escape its own degradation contract over a hostile identifier."""
|
|
1152
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
1153
|
+
|
|
1154
|
+
evil = "10.5555/evil\r\nHost: attacker?x=1#frag"
|
|
1155
|
+
transport = FakeTransport([
|
|
1156
|
+
(_is_handle, _ok(_body("handle_absent.json"))),
|
|
1157
|
+
])
|
|
1158
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
1159
|
+
assert _client().handle_exists(evil) is False
|
|
1160
|
+
|
|
1161
|
+
url = transport.requests[0]
|
|
1162
|
+
assert "\r" not in url and "\n" not in url
|
|
1163
|
+
assert "?" not in url and "#" not in url # no query/fragment injection
|
|
1164
|
+
assert "%0D%0A" in url # CRLF arrived encoded, not stripped
|
|
1165
|
+
|
|
1166
|
+
# Defense in depth: even if a malformed request slips past encoding and
|
|
1167
|
+
# urlopen rejects it (http.client.InvalidURL), the client degrades cleanly.
|
|
1168
|
+
def reject(req, timeout=None):
|
|
1169
|
+
raise http.client.InvalidURL("URL can't contain control characters")
|
|
1170
|
+
|
|
1171
|
+
with patch("chinese_literature_client._safe_urlopen", reject):
|
|
1172
|
+
with pytest.raises(ChineseLiteratureUnavailable):
|
|
1173
|
+
_client().handle_exists(evil)
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
def test_api_key_never_lands_in_exception_text():
|
|
1177
|
+
"""#495 red line (mirrors crossref/openalex): refusal/degradation messages
|
|
1178
|
+
strip the query string so a configured NCBI api_key can never reach logs or
|
|
1179
|
+
raised-exception text — checked across the 5xx, network-error and
|
|
1180
|
+
unparseable-body paths."""
|
|
1181
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
1182
|
+
|
|
1183
|
+
key = "test-ncbi-key"
|
|
1184
|
+
|
|
1185
|
+
scenarios = [
|
|
1186
|
+
FakeTransport([(_is_esearch, _http_error(503))]),
|
|
1187
|
+
FakeTransport([(_is_esearch, _ok(_body("error_5xx.html")))]),
|
|
1188
|
+
]
|
|
1189
|
+
for transport in scenarios:
|
|
1190
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
1191
|
+
with pytest.raises(ChineseLiteratureUnavailable) as excinfo:
|
|
1192
|
+
_client(ncbi_api_key=key).journal_is_indexed("Hecheng Ceshi")
|
|
1193
|
+
assert key not in str(excinfo.value)
|
|
1194
|
+
assert "api_key" not in str(excinfo.value)
|
|
1195
|
+
assert "?" not in str(excinfo.value) # whole query stripped, not just the key
|
|
1196
|
+
|
|
1197
|
+
def net_error(req, timeout=None):
|
|
1198
|
+
raise urllib.error.URLError("connection refused")
|
|
1199
|
+
|
|
1200
|
+
with patch("chinese_literature_client._safe_urlopen", net_error):
|
|
1201
|
+
with pytest.raises(ChineseLiteratureUnavailable) as excinfo:
|
|
1202
|
+
_client(ncbi_api_key=key).journal_is_indexed("Hecheng Ceshi")
|
|
1203
|
+
assert key not in str(excinfo.value)
|
|
1204
|
+
assert "?" not in str(excinfo.value)
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
@pytest.mark.parametrize("failure_kind", ["http", "url"])
|
|
1208
|
+
def test_reflected_request_url_is_absent_from_full_exception_traceback(failure_kind):
|
|
1209
|
+
"""Removing a secret from the wrapper message is insufficient when the
|
|
1210
|
+
original exception remains as an explicit cause: logging the traceback
|
|
1211
|
+
would print that cause and its full query string."""
|
|
1212
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
1213
|
+
|
|
1214
|
+
key = "SECRET123"
|
|
1215
|
+
|
|
1216
|
+
def reflected(req, timeout=None):
|
|
1217
|
+
if failure_kind == "http":
|
|
1218
|
+
raise urllib.error.HTTPError(
|
|
1219
|
+
url=req.full_url,
|
|
1220
|
+
code=503,
|
|
1221
|
+
msg=f"upstream reflected {req.full_url}",
|
|
1222
|
+
hdrs={},
|
|
1223
|
+
fp=io.BytesIO(),
|
|
1224
|
+
)
|
|
1225
|
+
raise urllib.error.URLError(f"upstream reflected {req.full_url}")
|
|
1226
|
+
|
|
1227
|
+
with patch("chinese_literature_client._safe_urlopen", reflected):
|
|
1228
|
+
with pytest.raises(ChineseLiteratureUnavailable) as excinfo:
|
|
1229
|
+
_client(ncbi_api_key=key).journal_is_indexed("Hecheng Ceshi")
|
|
1230
|
+
|
|
1231
|
+
rendered = "".join(traceback.format_exception(
|
|
1232
|
+
excinfo.type, excinfo.value, excinfo.tb,
|
|
1233
|
+
))
|
|
1234
|
+
assert key not in rendered
|
|
1235
|
+
assert f"api_key={key}" not in rendered
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
@pytest.mark.parametrize("error_url", ["", "http://evil.invalid/stolen"])
|
|
1239
|
+
def test_rejected_http_error_url_does_not_retain_reflected_secret(error_url):
|
|
1240
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
1241
|
+
|
|
1242
|
+
key = "SECRET123"
|
|
1243
|
+
|
|
1244
|
+
def reflected(req, timeout=None):
|
|
1245
|
+
raise urllib.error.HTTPError(
|
|
1246
|
+
url=error_url,
|
|
1247
|
+
code=503,
|
|
1248
|
+
msg=f"upstream reflected {req.full_url}",
|
|
1249
|
+
hdrs={},
|
|
1250
|
+
fp=io.BytesIO(),
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1253
|
+
with patch("chinese_literature_client._safe_urlopen", reflected):
|
|
1254
|
+
with pytest.raises(ChineseLiteratureUnavailable) as excinfo:
|
|
1255
|
+
_client(ncbi_api_key=key).journal_is_indexed("Hecheng Ceshi")
|
|
1256
|
+
|
|
1257
|
+
rendered = "".join(traceback.format_exception(
|
|
1258
|
+
excinfo.type, excinfo.value, excinfo.tb,
|
|
1259
|
+
))
|
|
1260
|
+
assert key not in rendered
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
def test_malformed_port_does_not_retain_secret_as_a_cause():
|
|
1264
|
+
from chinese_literature_client import ChineseLiteratureUnavailable, _require_api_url
|
|
1265
|
+
|
|
1266
|
+
key = "SECRET123"
|
|
1267
|
+
with pytest.raises(ChineseLiteratureUnavailable) as excinfo:
|
|
1268
|
+
_require_api_url(f"https://doi.org:{key}/path?api_key={key}")
|
|
1269
|
+
|
|
1270
|
+
rendered = "".join(traceback.format_exception(
|
|
1271
|
+
excinfo.type, excinfo.value, excinfo.tb,
|
|
1272
|
+
))
|
|
1273
|
+
assert key not in rendered
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
def test_host_allowlist_guard_refuses_and_redacts():
|
|
1277
|
+
"""Multi-host `_require_api_url` (sibling defense-in-depth): a non-HTTPS or
|
|
1278
|
+
non-allowlisted host is refused before any request, and the refusal text is
|
|
1279
|
+
query-stripped."""
|
|
1280
|
+
from chinese_literature_client import (
|
|
1281
|
+
ChineseLiteratureUnavailable,
|
|
1282
|
+
_require_api_url,
|
|
1283
|
+
)
|
|
1284
|
+
|
|
1285
|
+
_require_api_url("https://doi.org/doiRA/10.5555") # allowlisted: no raise
|
|
1286
|
+
_require_api_url("https://eutils.ncbi.nlm.nih.gov/x") # allowlisted: no raise
|
|
1287
|
+
for bad in (
|
|
1288
|
+
"https://www.cnki.net/search?q=x", # zero-scraping red line
|
|
1289
|
+
"http://doi.org/doiRA/10.5555", # https only
|
|
1290
|
+
"https://evil.invalid/a?api_key=test-ncbi-key",
|
|
1291
|
+
"https://127.0.0.1/doiRA/10.5555", # no bare IPs
|
|
1292
|
+
"https://user@doi.org/doiRA/10.5555", # no userinfo
|
|
1293
|
+
"https://doi.org:443/doiRA/10.5555", # exact origin, no ports
|
|
1294
|
+
):
|
|
1295
|
+
with pytest.raises(ChineseLiteratureUnavailable) as excinfo:
|
|
1296
|
+
_require_api_url(bad)
|
|
1297
|
+
assert "api_key" not in str(excinfo.value)
|
|
1298
|
+
assert "?" not in str(excinfo.value)
|
|
1299
|
+
|
|
1300
|
+
|
|
1301
|
+
def test_redirect_handler_revalidates_every_destination_before_following():
|
|
1302
|
+
from chinese_literature_client import (
|
|
1303
|
+
ChineseLiteratureUnavailable,
|
|
1304
|
+
_SafeRedirectHandler,
|
|
1305
|
+
)
|
|
1306
|
+
|
|
1307
|
+
handler = _SafeRedirectHandler()
|
|
1308
|
+
request = urllib.request.Request("https://doi.org/10.5555/example")
|
|
1309
|
+
for target in (
|
|
1310
|
+
"http://doi.org/10.5555/example", # downgrade
|
|
1311
|
+
"https://127.0.0.1/10.5555/example", # bare IP
|
|
1312
|
+
"https://user@doi.org/10.5555/example", # userinfo
|
|
1313
|
+
"https://doi.org:443/10.5555/example", # explicit port
|
|
1314
|
+
"https://doi.org.evil.invalid/example", # suffix confusion
|
|
1315
|
+
"https://evil.invalid/example", # external host
|
|
1316
|
+
):
|
|
1317
|
+
with pytest.raises(ChineseLiteratureUnavailable):
|
|
1318
|
+
handler.redirect_request(request, None, 302, "Found", {}, target)
|
|
1319
|
+
|
|
1320
|
+
redirected = handler.redirect_request(
|
|
1321
|
+
request, None, 302, "Found", {},
|
|
1322
|
+
"https://hdl.handle.net/api/handles/10.5555/example",
|
|
1323
|
+
)
|
|
1324
|
+
assert redirected.full_url.startswith("https://hdl.handle.net/")
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
@pytest.mark.parametrize("code", [301, 302, 303, 307, 308])
|
|
1328
|
+
def test_redirect_response_body_is_never_read_before_following(code):
|
|
1329
|
+
from chinese_literature_client import _SafeRedirectHandler
|
|
1330
|
+
|
|
1331
|
+
handler = _SafeRedirectHandler()
|
|
1332
|
+
handler.parent = MagicMock()
|
|
1333
|
+
request = urllib.request.Request("https://doi.org/10.5555/example")
|
|
1334
|
+
request.timeout = 30
|
|
1335
|
+
response = MagicMock()
|
|
1336
|
+
redirected_response = object()
|
|
1337
|
+
handler.parent.open.return_value = redirected_response
|
|
1338
|
+
|
|
1339
|
+
result = getattr(handler, f"http_error_{code}")(
|
|
1340
|
+
request,
|
|
1341
|
+
response,
|
|
1342
|
+
code,
|
|
1343
|
+
"Found",
|
|
1344
|
+
{"location": "/10.5555/next"},
|
|
1345
|
+
)
|
|
1346
|
+
|
|
1347
|
+
assert result is redirected_response
|
|
1348
|
+
response.read.assert_not_called()
|
|
1349
|
+
response.close.assert_called_once_with()
|
|
1350
|
+
followed = handler.parent.open.call_args.args[0]
|
|
1351
|
+
assert followed.full_url == "https://doi.org/10.5555/next"
|
|
1352
|
+
|
|
1353
|
+
|
|
1354
|
+
@pytest.mark.parametrize(
|
|
1355
|
+
"location",
|
|
1356
|
+
["http://doi.org/downgrade", "https://evil.invalid/escape", 42],
|
|
1357
|
+
)
|
|
1358
|
+
def test_rejected_redirect_is_closed_without_reading(location):
|
|
1359
|
+
from chinese_literature_client import (
|
|
1360
|
+
ChineseLiteratureUnavailable,
|
|
1361
|
+
_SafeRedirectHandler,
|
|
1362
|
+
)
|
|
1363
|
+
|
|
1364
|
+
handler = _SafeRedirectHandler()
|
|
1365
|
+
request = urllib.request.Request("https://doi.org/10.5555/example")
|
|
1366
|
+
request.timeout = 30
|
|
1367
|
+
response = MagicMock()
|
|
1368
|
+
|
|
1369
|
+
with pytest.raises(ChineseLiteratureUnavailable):
|
|
1370
|
+
handler.http_error_302(
|
|
1371
|
+
request, response, 302, "Found", {"location": location},
|
|
1372
|
+
)
|
|
1373
|
+
|
|
1374
|
+
response.read.assert_not_called()
|
|
1375
|
+
response.close.assert_called_once_with()
|
|
1376
|
+
|
|
1377
|
+
|
|
1378
|
+
def test_redirect_without_location_is_closed_without_reading():
|
|
1379
|
+
from chinese_literature_client import (
|
|
1380
|
+
ChineseLiteratureUnavailable,
|
|
1381
|
+
_SafeRedirectHandler,
|
|
1382
|
+
)
|
|
1383
|
+
|
|
1384
|
+
handler = _SafeRedirectHandler()
|
|
1385
|
+
request = urllib.request.Request("https://doi.org/10.5555/example")
|
|
1386
|
+
request.timeout = 30
|
|
1387
|
+
response = MagicMock()
|
|
1388
|
+
|
|
1389
|
+
with pytest.raises(ChineseLiteratureUnavailable, match="missing a Location"):
|
|
1390
|
+
handler.http_error_302(request, response, 302, "Found", {})
|
|
1391
|
+
|
|
1392
|
+
response.read.assert_not_called()
|
|
1393
|
+
response.close.assert_called_once_with()
|
|
1394
|
+
|
|
1395
|
+
|
|
1396
|
+
def test_istic_plaintext_redirect_stops_resolve_before_any_verdict():
|
|
1397
|
+
"""End-to-end regression for the maintainer's live ISTIC observation.
|
|
1398
|
+
|
|
1399
|
+
RA triage may identify ISTIC, but the subsequent DOI request currently has
|
|
1400
|
+
the shape ``HTTPS doi.org -> HTTP bare IP``. Exercise the real resolver and
|
|
1401
|
+
redirect handler together: the unsafe hop must be closed unread, propagate
|
|
1402
|
+
as typed degradation, and never return a MATCH/MISMATCH-shaped result.
|
|
1403
|
+
"""
|
|
1404
|
+
from chinese_literature_client import (
|
|
1405
|
+
ChineseLiteratureUnavailable,
|
|
1406
|
+
_SafeRedirectHandler,
|
|
1407
|
+
)
|
|
1408
|
+
|
|
1409
|
+
requests: list[str] = []
|
|
1410
|
+
redirect_response = MagicMock()
|
|
1411
|
+
|
|
1412
|
+
def transport(req, timeout=None):
|
|
1413
|
+
url = req.full_url
|
|
1414
|
+
requests.append(url)
|
|
1415
|
+
parsed = urllib.parse.urlsplit(url)
|
|
1416
|
+
query = (
|
|
1417
|
+
urllib.parse.parse_qs(
|
|
1418
|
+
parsed.query, keep_blank_values=True, strict_parsing=True,
|
|
1419
|
+
)
|
|
1420
|
+
if parsed.query else {}
|
|
1421
|
+
)
|
|
1422
|
+
if _is_ra(parsed, query):
|
|
1423
|
+
return _ok(_body("ra_istic.json"))(url)
|
|
1424
|
+
if _is_doi_resolve(parsed, query):
|
|
1425
|
+
handler = _SafeRedirectHandler()
|
|
1426
|
+
return handler.http_error_302(
|
|
1427
|
+
req,
|
|
1428
|
+
redirect_response,
|
|
1429
|
+
302,
|
|
1430
|
+
"Found",
|
|
1431
|
+
{"location": "http://122.115.55.36:8000/doi/synthetic"},
|
|
1432
|
+
)
|
|
1433
|
+
raise AssertionError(f"client contacted an unrouted URL: {url}")
|
|
1434
|
+
|
|
1435
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
1436
|
+
with pytest.raises(ChineseLiteratureUnavailable):
|
|
1437
|
+
_client().resolve(_istic_entry())
|
|
1438
|
+
|
|
1439
|
+
assert len(requests) == 2 # RA triage, then the refused DOI request
|
|
1440
|
+
redirect_response.read.assert_not_called()
|
|
1441
|
+
redirect_response.close.assert_called_once_with()
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
def test_final_response_url_is_revalidated_even_after_transport_returns():
|
|
1445
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
1446
|
+
|
|
1447
|
+
response = MagicMock()
|
|
1448
|
+
response.status = 200
|
|
1449
|
+
response.headers = {}
|
|
1450
|
+
response.geturl.return_value = "http://127.0.0.1/stolen"
|
|
1451
|
+
response.read.return_value = _body("ra_istic.json")
|
|
1452
|
+
response.__enter__ = MagicMock(return_value=response)
|
|
1453
|
+
response.__exit__ = MagicMock(return_value=None)
|
|
1454
|
+
|
|
1455
|
+
with patch("chinese_literature_client._safe_urlopen", return_value=response):
|
|
1456
|
+
with pytest.raises(ChineseLiteratureUnavailable):
|
|
1457
|
+
_client().ra_for(ISTIC_DOI)
|
|
1458
|
+
response.read.assert_not_called()
|
|
1459
|
+
|
|
1460
|
+
|
|
1461
|
+
def test_http_error_final_url_is_revalidated_before_404_becomes_data():
|
|
1462
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
1463
|
+
|
|
1464
|
+
def escaped_404(req, timeout=None):
|
|
1465
|
+
raise urllib.error.HTTPError(
|
|
1466
|
+
url="http://127.0.0.1/not-found",
|
|
1467
|
+
code=404,
|
|
1468
|
+
msg="synthetic",
|
|
1469
|
+
hdrs={},
|
|
1470
|
+
fp=io.BytesIO(b""),
|
|
1471
|
+
)
|
|
1472
|
+
|
|
1473
|
+
with patch("chinese_literature_client._safe_urlopen", escaped_404):
|
|
1474
|
+
with pytest.raises(ChineseLiteratureUnavailable):
|
|
1475
|
+
_client().doi_lookup_with_title_check(FABRICATED_DOI, CN_TITLE)
|
|
1476
|
+
|
|
1477
|
+
|
|
1478
|
+
@pytest.mark.parametrize("code", [404, 503])
|
|
1479
|
+
def test_http_error_response_is_closed_without_reading(code):
|
|
1480
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
1481
|
+
|
|
1482
|
+
error_body = MagicMock()
|
|
1483
|
+
|
|
1484
|
+
def fail(req, timeout=None):
|
|
1485
|
+
raise urllib.error.HTTPError(
|
|
1486
|
+
url=req.full_url,
|
|
1487
|
+
code=code,
|
|
1488
|
+
msg="synthetic",
|
|
1489
|
+
hdrs={},
|
|
1490
|
+
fp=error_body,
|
|
1491
|
+
)
|
|
1492
|
+
|
|
1493
|
+
with patch("chinese_literature_client._safe_urlopen", fail):
|
|
1494
|
+
if code == 404:
|
|
1495
|
+
assert _client().handle_exists(FABRICATED_DOI) is False
|
|
1496
|
+
else:
|
|
1497
|
+
with pytest.raises(ChineseLiteratureUnavailable, match="HTTP 503"):
|
|
1498
|
+
_client().handle_exists(FABRICATED_DOI)
|
|
1499
|
+
|
|
1500
|
+
error_body.read.assert_not_called()
|
|
1501
|
+
error_body.close.assert_called_once_with()
|
|
1502
|
+
|
|
1503
|
+
|
|
1504
|
+
@pytest.mark.parametrize("status", [None, 0, 503, "200"])
|
|
1505
|
+
def test_returned_invalid_or_non_success_status_is_rejected_before_read(status):
|
|
1506
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
1507
|
+
|
|
1508
|
+
response = MagicMock()
|
|
1509
|
+
response.status = status
|
|
1510
|
+
response.headers = {}
|
|
1511
|
+
response.geturl.return_value = "https://doi.org/doiRA/10.5555"
|
|
1512
|
+
response.__enter__ = MagicMock(return_value=response)
|
|
1513
|
+
response.__exit__ = MagicMock(return_value=None)
|
|
1514
|
+
|
|
1515
|
+
with patch("chinese_literature_client._safe_urlopen", return_value=response):
|
|
1516
|
+
with pytest.raises(ChineseLiteratureUnavailable):
|
|
1517
|
+
_client().ra_for(ISTIC_DOI)
|
|
1518
|
+
response.read.assert_not_called()
|
|
1519
|
+
|
|
1520
|
+
|
|
1521
|
+
def test_returned_404_is_data_only_on_an_allow_404_path():
|
|
1522
|
+
response = MagicMock()
|
|
1523
|
+
response.status = 404
|
|
1524
|
+
response.headers = {}
|
|
1525
|
+
response.geturl.return_value = "https://doi.org/doiRA/10.5555"
|
|
1526
|
+
response.__enter__ = MagicMock(return_value=response)
|
|
1527
|
+
response.__exit__ = MagicMock(return_value=None)
|
|
1528
|
+
|
|
1529
|
+
with patch("chinese_literature_client._safe_urlopen", return_value=response):
|
|
1530
|
+
assert _client().ra_for(ISTIC_DOI) is None
|
|
1531
|
+
response.read.assert_not_called()
|
|
1532
|
+
|
|
1533
|
+
|
|
1534
|
+
def test_content_length_precheck_rejects_oversized_body_without_reading():
|
|
1535
|
+
from chinese_literature_client import ChineseLiteratureUnavailable, MAX_BODY_BYTES
|
|
1536
|
+
|
|
1537
|
+
response = MagicMock()
|
|
1538
|
+
response.status = 200
|
|
1539
|
+
response.headers = {"Content-Length": str(MAX_BODY_BYTES + 1)}
|
|
1540
|
+
response.geturl.return_value = "https://doi.org/doiRA/10.5555"
|
|
1541
|
+
response.__enter__ = MagicMock(return_value=response)
|
|
1542
|
+
response.__exit__ = MagicMock(return_value=None)
|
|
1543
|
+
|
|
1544
|
+
with patch("chinese_literature_client._safe_urlopen", return_value=response):
|
|
1545
|
+
with pytest.raises(ChineseLiteratureUnavailable, match="exceeds"):
|
|
1546
|
+
_client().ra_for(ISTIC_DOI)
|
|
1547
|
+
response.read.assert_not_called()
|
|
1548
|
+
|
|
1549
|
+
|
|
1550
|
+
@pytest.mark.parametrize(
|
|
1551
|
+
"value",
|
|
1552
|
+
["+1", "-0", "1_0", "١", " 1 x ", "\u00a01\u00a0", "9" * 5000],
|
|
1553
|
+
)
|
|
1554
|
+
def test_malformed_content_length_is_rejected_before_read(value):
|
|
1555
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
1556
|
+
|
|
1557
|
+
response = MagicMock()
|
|
1558
|
+
response.status = 200
|
|
1559
|
+
response.headers = {"Content-Length": value}
|
|
1560
|
+
response.geturl.return_value = "https://doi.org/doiRA/10.5555"
|
|
1561
|
+
response.__enter__ = MagicMock(return_value=response)
|
|
1562
|
+
response.__exit__ = MagicMock(return_value=None)
|
|
1563
|
+
|
|
1564
|
+
with patch("chinese_literature_client._safe_urlopen", return_value=response):
|
|
1565
|
+
with pytest.raises(ChineseLiteratureUnavailable, match="Content-Length"):
|
|
1566
|
+
_client().ra_for(ISTIC_DOI)
|
|
1567
|
+
response.read.assert_not_called()
|
|
1568
|
+
|
|
1569
|
+
|
|
1570
|
+
def test_body_read_is_bounded_and_rejects_stream_over_limit():
|
|
1571
|
+
from chinese_literature_client import ChineseLiteratureUnavailable, MAX_BODY_BYTES
|
|
1572
|
+
|
|
1573
|
+
response = MagicMock()
|
|
1574
|
+
response.status = 200
|
|
1575
|
+
response.headers = {}
|
|
1576
|
+
response.geturl.return_value = "https://doi.org/doiRA/10.5555"
|
|
1577
|
+
response.read.return_value = b"x" * (MAX_BODY_BYTES + 1)
|
|
1578
|
+
response.__enter__ = MagicMock(return_value=response)
|
|
1579
|
+
response.__exit__ = MagicMock(return_value=None)
|
|
1580
|
+
|
|
1581
|
+
with patch("chinese_literature_client._safe_urlopen", return_value=response):
|
|
1582
|
+
with pytest.raises(ChineseLiteratureUnavailable, match="exceeds"):
|
|
1583
|
+
_client().ra_for(ISTIC_DOI)
|
|
1584
|
+
response.read.assert_called_once_with(MAX_BODY_BYTES + 1)
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
# ------------------------------------------------------- degradation contract
|
|
1588
|
+
|
|
1589
|
+
|
|
1590
|
+
def test_5xx_does_not_retry():
|
|
1591
|
+
"""Per protocol: 5xx -> fail fast, exactly one request."""
|
|
1592
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
1593
|
+
|
|
1594
|
+
transport = FakeTransport([(_is_ra, _http_error(503, _body("error_5xx.html")))])
|
|
1595
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
1596
|
+
with pytest.raises(ChineseLiteratureUnavailable):
|
|
1597
|
+
_client().ra_for(ISTIC_DOI)
|
|
1598
|
+
|
|
1599
|
+
assert len(transport.requests) == 1
|
|
1600
|
+
|
|
1601
|
+
|
|
1602
|
+
def test_429_backs_off_then_raises_after_the_shared_retry_budget(monkeypatch):
|
|
1603
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
1604
|
+
from _text_similarity import _MAX_RETRIES
|
|
1605
|
+
|
|
1606
|
+
sleeps: list[float] = []
|
|
1607
|
+
monkeypatch.setattr("chinese_literature_client.time.sleep", sleeps.append)
|
|
1608
|
+
|
|
1609
|
+
transport = FakeTransport([(_is_ra, _http_error(429))])
|
|
1610
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
1611
|
+
with pytest.raises(ChineseLiteratureUnavailable):
|
|
1612
|
+
_client().ra_for(ISTIC_DOI)
|
|
1613
|
+
|
|
1614
|
+
assert len(transport.requests) == _MAX_RETRIES + 1
|
|
1615
|
+
# Growing backoff, floored at 2s so a retry cannot re-violate the limit the
|
|
1616
|
+
# 429 is enforcing (arxiv_client.py's rule).
|
|
1617
|
+
assert sleeps == [2.0, 4.0, 6.0]
|
|
1618
|
+
assert all(delay >= 2.0 for delay in sleeps)
|
|
1619
|
+
|
|
1620
|
+
|
|
1621
|
+
def test_network_error_raises_unavailable():
|
|
1622
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
1623
|
+
|
|
1624
|
+
def boom(req, timeout=None):
|
|
1625
|
+
raise urllib.error.URLError("connection refused")
|
|
1626
|
+
|
|
1627
|
+
with patch("chinese_literature_client._safe_urlopen", boom):
|
|
1628
|
+
with pytest.raises(ChineseLiteratureUnavailable):
|
|
1629
|
+
_client().ra_for(ISTIC_DOI)
|
|
1630
|
+
|
|
1631
|
+
|
|
1632
|
+
@pytest.mark.parametrize("failure_point", ["open", "close"])
|
|
1633
|
+
def test_raw_oserror_from_transport_or_response_close_is_typed(failure_point):
|
|
1634
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
1635
|
+
|
|
1636
|
+
if failure_point == "open":
|
|
1637
|
+
transport = MagicMock(side_effect=OSError("socket failure"))
|
|
1638
|
+
else:
|
|
1639
|
+
response = MagicMock()
|
|
1640
|
+
response.status = 200
|
|
1641
|
+
response.headers = {}
|
|
1642
|
+
response.geturl.return_value = "https://doi.org/doiRA/10.5555"
|
|
1643
|
+
response.read.return_value = _body("ra_istic.json")
|
|
1644
|
+
response.__enter__ = MagicMock(return_value=response)
|
|
1645
|
+
response.__exit__ = MagicMock(side_effect=OSError("close failure"))
|
|
1646
|
+
transport = MagicMock(return_value=response)
|
|
1647
|
+
|
|
1648
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
1649
|
+
with pytest.raises(ChineseLiteratureUnavailable, match="network error"):
|
|
1650
|
+
_client().ra_for(ISTIC_DOI)
|
|
1651
|
+
|
|
1652
|
+
|
|
1653
|
+
def test_truncated_body_raises_unavailable():
|
|
1654
|
+
"""IncompleteRead inherits HTTPException, not OSError — a truncated body
|
|
1655
|
+
must degrade, never become a miss."""
|
|
1656
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
1657
|
+
|
|
1658
|
+
resp = MagicMock()
|
|
1659
|
+
resp.status = 200
|
|
1660
|
+
resp.headers = {}
|
|
1661
|
+
resp.geturl.return_value = "https://doi.org/doiRA/10.5555"
|
|
1662
|
+
resp.read.side_effect = http.client.IncompleteRead(partial=b"{", expected=200)
|
|
1663
|
+
resp.__enter__ = MagicMock(return_value=resp)
|
|
1664
|
+
resp.__exit__ = MagicMock(return_value=None)
|
|
1665
|
+
|
|
1666
|
+
with patch("chinese_literature_client._safe_urlopen", return_value=resp):
|
|
1667
|
+
with pytest.raises(ChineseLiteratureUnavailable, match="read failed"):
|
|
1668
|
+
_client().ra_for(ISTIC_DOI)
|
|
1669
|
+
|
|
1670
|
+
|
|
1671
|
+
def test_unparseable_200_body_raises_unavailable():
|
|
1672
|
+
"""#331: an HTML error page served with 200 by a proxy/CDN must degrade, not
|
|
1673
|
+
be recorded as an empty result."""
|
|
1674
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
1675
|
+
|
|
1676
|
+
transport = FakeTransport([(_is_esearch, _ok(_body("error_5xx.html")))])
|
|
1677
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
1678
|
+
with pytest.raises(ChineseLiteratureUnavailable):
|
|
1679
|
+
_client().journal_is_indexed("Hecheng Ceshi Yi Xue Za Zhi")
|
|
1680
|
+
|
|
1681
|
+
|
|
1682
|
+
@pytest.mark.parametrize(
|
|
1683
|
+
"body",
|
|
1684
|
+
[
|
|
1685
|
+
b'{"unexpected": true}',
|
|
1686
|
+
b'{"esearchresult":{"idlist":[null]}}',
|
|
1687
|
+
b'{"esearchresult":{"idlist":[42]}}',
|
|
1688
|
+
b'{"esearchresult":{"idlist":[""]}}',
|
|
1689
|
+
b'{"esearchresult":{"idlist":["0"]}}',
|
|
1690
|
+
b'{"esearchresult":{"count":false,"idlist":[]}}',
|
|
1691
|
+
b'{"esearchresult":{"count":"0","idlist":["999999901"]}}',
|
|
1692
|
+
b'{"esearchresult":{"count":"1","idlist":[]}}',
|
|
1693
|
+
(b'{"esearchresult":{"count":"' + b"9" * 5000 + b'","idlist":[]}}'),
|
|
1694
|
+
b'{"esearchresult":{"count":"2","idlist":["999999901","999999901"]}}',
|
|
1695
|
+
],
|
|
1696
|
+
)
|
|
1697
|
+
def test_unexpected_esearch_shape_raises_unavailable(body):
|
|
1698
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
1699
|
+
|
|
1700
|
+
transport = FakeTransport([(_is_esearch, _ok(body))])
|
|
1701
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
1702
|
+
with pytest.raises(ChineseLiteratureUnavailable):
|
|
1703
|
+
_client().journal_is_indexed("Hecheng Ceshi Yi Xue Za Zhi")
|
|
1704
|
+
|
|
1705
|
+
|
|
1706
|
+
@pytest.mark.parametrize(
|
|
1707
|
+
"body",
|
|
1708
|
+
[
|
|
1709
|
+
b'{"result": {"999999901": 42}}',
|
|
1710
|
+
b'{"result": {"999999901": {"articleids": {}}}}',
|
|
1711
|
+
b'{"result": {"999999901": {"articleids": [], "issn": {}}}}',
|
|
1712
|
+
b'{"result": {"999999901": {"articleids": [], "title": false}}}',
|
|
1713
|
+
b'{"result": {"999999901": {"articleids": [], "pubdate": false}}}',
|
|
1714
|
+
b'{"result": {"999999901": {"articleids": [], "pubdate": "garbage"}}}',
|
|
1715
|
+
'{"result": {"999999901": {"articleids": [], "pubdate": "٢٠٢٦"}}}'.encode(),
|
|
1716
|
+
b'{"result": {"999999901": {"articleids": [], "source": []}}}',
|
|
1717
|
+
b'{"result": {"999999901": {"articleids": [], "lang": false}}}',
|
|
1718
|
+
b'{"result": {"999999901": {"articleids": [{"idtype": false, "value": "x"}]}}}',
|
|
1719
|
+
b'{"result": {"999999901": {"articleids": [{"idtype": "doi", "value": false}]}}}',
|
|
1720
|
+
b'{"result": {"999999901": {"articleids": [{"idtype": "doi", "value": "10.a/x"}, {"idtype": "doi", "value": "10.b/y"}]}}}',
|
|
1721
|
+
b'{"result": {"uids": ["888888888"], "999999901": {"articleids": []}}}',
|
|
1722
|
+
b'{"result": {"uids": [null], "999999901": {"articleids": []}}}',
|
|
1723
|
+
b'{"result": {"uids": ["999999901"], "999999901": {"uid": "888888888", "articleids": []}}}',
|
|
1724
|
+
b'{"result": {"uids": ["999999901"], "999999901": {"uid": 999999901, "articleids": []}}}',
|
|
1725
|
+
],
|
|
1726
|
+
)
|
|
1727
|
+
def test_unexpected_esummary_shape_raises_unavailable(body):
|
|
1728
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
1729
|
+
|
|
1730
|
+
transport = FakeTransport([(_is_esummary, _ok(body))])
|
|
1731
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
1732
|
+
with pytest.raises(ChineseLiteratureUnavailable):
|
|
1733
|
+
_client()._esummary("999999901")
|
|
1734
|
+
|
|
1735
|
+
|
|
1736
|
+
def test_resolve_propagates_unavailable_rather_than_returning_a_verdict():
|
|
1737
|
+
"""fail-closed: `resolve` has no `unreachable` return value by design. An
|
|
1738
|
+
outage must reach the caller as an exception so it can never be silently
|
|
1739
|
+
rendered as a lookup result."""
|
|
1740
|
+
from chinese_literature_client import ChineseLiteratureUnavailable
|
|
1741
|
+
|
|
1742
|
+
transport = FakeTransport([(_is_ra, _http_error(503))])
|
|
1743
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
1744
|
+
with pytest.raises(ChineseLiteratureUnavailable):
|
|
1745
|
+
_client().resolve(_istic_entry())
|
|
1746
|
+
|
|
1747
|
+
|
|
1748
|
+
# ------------------------------------------------------------------ throttling
|
|
1749
|
+
|
|
1750
|
+
|
|
1751
|
+
def test_throttle_uses_monotonic_clock(monkeypatch):
|
|
1752
|
+
"""time.monotonic for elapsed measurement (NTP-safe), never time.time
|
|
1753
|
+
(#128 §6). Aligns with arxiv/crossref/openalex/S2."""
|
|
1754
|
+
monotonic_calls: list[int] = []
|
|
1755
|
+
time_calls: list[int] = []
|
|
1756
|
+
monkeypatch.setattr("chinese_literature_client.time.monotonic",
|
|
1757
|
+
lambda: (monotonic_calls.append(1), 100.0)[1])
|
|
1758
|
+
monkeypatch.setattr("chinese_literature_client.time.time",
|
|
1759
|
+
lambda: (time_calls.append(1), 100.0)[1])
|
|
1760
|
+
|
|
1761
|
+
client = _client()
|
|
1762
|
+
client._throttle("_last_doi_at", 0.2) # no prior request -> short-circuit
|
|
1763
|
+
client._last_doi_at = 99.9
|
|
1764
|
+
client._throttle("_last_doi_at", 0.2)
|
|
1765
|
+
|
|
1766
|
+
assert monotonic_calls, "throttle must read time.monotonic"
|
|
1767
|
+
assert time_calls == [], "throttle must NOT read time.time (NTP-unsafe)"
|
|
1768
|
+
|
|
1769
|
+
|
|
1770
|
+
def test_doi_and_ncbi_have_independent_throttle_anchors(monkeypatch):
|
|
1771
|
+
"""Sharing one anchor would either over-throttle doi.org or under-throttle
|
|
1772
|
+
NCBI, whose published expectations differ."""
|
|
1773
|
+
sleeps: list[float] = []
|
|
1774
|
+
monkeypatch.setattr("chinese_literature_client.time.sleep", sleeps.append)
|
|
1775
|
+
clock = [1000.0]
|
|
1776
|
+
monkeypatch.setattr("chinese_literature_client.time.monotonic", lambda: clock[0])
|
|
1777
|
+
|
|
1778
|
+
esearch_bodies = [_body("esearch_coverage_hit.json"),
|
|
1779
|
+
_body("esearch_coverage_hit.json")]
|
|
1780
|
+
transport = FakeTransport([
|
|
1781
|
+
(_is_ra, _ok(_body("ra_istic.json"))),
|
|
1782
|
+
(_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
|
|
1783
|
+
])
|
|
1784
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
1785
|
+
client = _client()
|
|
1786
|
+
client.ra_for(ISTIC_DOI)
|
|
1787
|
+
# A DOI request must not make the (independent) NCBI anchor wait.
|
|
1788
|
+
client.journal_is_indexed("Hecheng Ceshi Yi Xue Za Zhi")
|
|
1789
|
+
assert sleeps == []
|
|
1790
|
+
# A second NCBI request on a frozen clock does pace at the NCBI floor.
|
|
1791
|
+
client.journal_is_indexed("Hecheng Ceshi Yi Xue Za Zhi")
|
|
1792
|
+
assert sleeps == [pytest.approx(0.34)]
|
|
1793
|
+
|
|
1794
|
+
|
|
1795
|
+
# ------------------------------------------------------------ checklist shape
|
|
1796
|
+
|
|
1797
|
+
|
|
1798
|
+
def test_every_applicable_non_matched_scenario_emits_exactly_one_checklist_row():
|
|
1799
|
+
"""The checklist is a first-class deliverable, not a log side effect: it is
|
|
1800
|
+
what turns 'nobody ever actually checked this reference' from an invisible
|
|
1801
|
+
default into an item somebody has to sign off on."""
|
|
1802
|
+
from chinese_literature_client import REASON_CODES
|
|
1803
|
+
|
|
1804
|
+
scenarios = [
|
|
1805
|
+
([(_is_ra, _ok(_body("ra_istic.json"))),
|
|
1806
|
+
(_is_doi_resolve, _http_error(404)),
|
|
1807
|
+
(_is_handle, _ok(_body("handle_absent.json")))], _istic_entry(), {}),
|
|
1808
|
+
([(_is_ra, _ok(_body("ra_cnki.json"))),
|
|
1809
|
+
(_is_handle, _ok(_body("handle_exists.json")))],
|
|
1810
|
+
_istic_entry(doi=CNKI_DOI), {}),
|
|
1811
|
+
([(_is_esearch, _ok(_body("esearch_coverage_zero.json")))],
|
|
1812
|
+
_coordinate_entry(), {"journal_map": TEST_JOURNAL_MAP}),
|
|
1813
|
+
]
|
|
1814
|
+
for routes, entry, kwargs in scenarios:
|
|
1815
|
+
transport = FakeTransport(routes)
|
|
1816
|
+
with patch("chinese_literature_client._safe_urlopen", transport):
|
|
1817
|
+
result = _client(**kwargs).resolve(entry)
|
|
1818
|
+
item = result["checklist_item"]
|
|
1819
|
+
assert item is not None
|
|
1820
|
+
assert item["reason_code"] in REASON_CODES
|
|
1821
|
+
assert item["priority"] in {"P0", "P1", "P2", "P3"}
|
|
1822
|
+
assert item["citation_key"] == entry["citation_key"]
|
|
1823
|
+
assert item["human_result"] is None # the tool never fills the verdict
|
|
1824
|
+
assert item["attempts"], "a row must record which sources were tried"
|
|
1825
|
+
|
|
1826
|
+
|
|
1827
|
+
def test_priority_is_workload_ordering_not_a_suspicion_score():
|
|
1828
|
+
"""Fabrication vocabulary is permitted only at P0. Mislabeling a real paper
|
|
1829
|
+
by a real author costs far more than missing one bad citation."""
|
|
1830
|
+
from chinese_literature_client import _PRIORITY_BY_REASON
|
|
1831
|
+
|
|
1832
|
+
assert _PRIORITY_BY_REASON["DOI_REFUTED"] == "P0"
|
|
1833
|
+
assert _PRIORITY_BY_REASON["DOI_TITLE_MISMATCH"] == "P0"
|
|
1834
|
+
assert _PRIORITY_BY_REASON["DOI_EXISTS_TITLE_UNVERIFIABLE"] == "P2"
|
|
1835
|
+
assert _PRIORITY_BY_REASON["NO_ISSN_MAPPING"] == "P3"
|
|
1836
|
+
non_p0 = {code for code, priority in _PRIORITY_BY_REASON.items()
|
|
1837
|
+
if priority != "P0"}
|
|
1838
|
+
assert "DOI_REFUTED" not in non_p0
|
|
1839
|
+
|
|
1840
|
+
|
|
1841
|
+
def test_unknown_reason_code_is_rejected():
|
|
1842
|
+
"""The reason-code set is closed; adding a member is a protocol-doc change."""
|
|
1843
|
+
from chinese_literature_client import _checklist_item
|
|
1844
|
+
|
|
1845
|
+
with pytest.raises(ValueError):
|
|
1846
|
+
_checklist_item(
|
|
1847
|
+
reason_code="MADE_UP_CODE",
|
|
1848
|
+
verdict_contribution="unresolvable",
|
|
1849
|
+
entry={"citation_key": "x"},
|
|
1850
|
+
attempts=[],
|
|
1851
|
+
human_action="",
|
|
1852
|
+
)
|
|
1853
|
+
|
|
1854
|
+
|
|
1855
|
+
def test_no_forbidden_upstream_host_appears_in_client_code():
|
|
1856
|
+
"""Zero-scraping red line, pinned executably: CNKI / Wanfang / VIP / chndoi
|
|
1857
|
+
hosts must never become upstreams here. Guards against a well-intentioned
|
|
1858
|
+
later edit 'just adding a title lookup'.
|
|
1859
|
+
|
|
1860
|
+
Scanned over the AST's non-docstring string constants rather than raw text:
|
|
1861
|
+
prose EXPLAINING why we refuse to scrape chndoi.org is exactly the comment
|
|
1862
|
+
a future editor most needs to read, so a raw-text scan would pressure
|
|
1863
|
+
someone to delete the warning in order to pass the test."""
|
|
1864
|
+
import ast
|
|
1865
|
+
|
|
1866
|
+
source = (REPO_ROOT / "scripts" / "chinese_literature_client.py").read_text(
|
|
1867
|
+
encoding="utf-8")
|
|
1868
|
+
tree = ast.parse(source)
|
|
1869
|
+
docstrings = set()
|
|
1870
|
+
for node in ast.walk(tree):
|
|
1871
|
+
if isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef,
|
|
1872
|
+
ast.AsyncFunctionDef)):
|
|
1873
|
+
body = getattr(node, "body", None)
|
|
1874
|
+
if (body and isinstance(body[0], ast.Expr)
|
|
1875
|
+
and isinstance(body[0].value, ast.Constant)
|
|
1876
|
+
and isinstance(body[0].value.value, str)):
|
|
1877
|
+
docstrings.add(id(body[0].value))
|
|
1878
|
+
|
|
1879
|
+
literals = [
|
|
1880
|
+
node.value for node in ast.walk(tree)
|
|
1881
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str)
|
|
1882
|
+
and id(node) not in docstrings
|
|
1883
|
+
]
|
|
1884
|
+
# Sanity: the scan must actually see the real endpoint constants, otherwise
|
|
1885
|
+
# it would pass vacuously.
|
|
1886
|
+
assert any("hdl.handle.net" in literal for literal in literals)
|
|
1887
|
+
for host in ("cnki.net", "wanfangdata", "cqvip.com", "chndoi.org"):
|
|
1888
|
+
for literal in literals:
|
|
1889
|
+
assert host not in literal, f"forbidden upstream host {host!r} in client"
|