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,1938 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Chinese-language literature resolver client.
|
|
3
|
+
|
|
4
|
+
Implements the lookup contract documented at
|
|
5
|
+
`deep-research/references/chinese_literature_api_protocol.md`.
|
|
6
|
+
|
|
7
|
+
WHY THIS EXISTS (#595): `api.crossref.org` is ONE DOI registration agency (RA),
|
|
8
|
+
not the DOI system. Real, resolvable Chinese-literature DOIs are registered with
|
|
9
|
+
ISTIC or CNKI and return 404 from the Crossref API while resolving fine through
|
|
10
|
+
`doi.org`. So for a Chinese citation, "not found in Crossref / OpenAlex / S2" is
|
|
11
|
+
weak evidence in BOTH directions, and the existing four resolvers reduce almost
|
|
12
|
+
every Chinese reference to `unresolvable` — indistinguishable from a fabrication.
|
|
13
|
+
|
|
14
|
+
Four legally-open, key-free upstreams. NO scraping of CNKI / Wanfang / VIP:
|
|
15
|
+
|
|
16
|
+
1. doi.org RA lookup -> https://doi.org/doiRA/<prefix> (zero-cost routing)
|
|
17
|
+
2. doi.org content-neg -> Accept: application/vnd.citationstyles.csl+json
|
|
18
|
+
(when safely exposed, ISTIC metadata can carry the
|
|
19
|
+
Chinese title, journal, volume/issue/page)
|
|
20
|
+
3. Handle System REST -> https://hdl.handle.net/api/handles/<doi>
|
|
21
|
+
(binary existence, incl. CNKI-registered DOIs)
|
|
22
|
+
4. NCBI E-utilities -> ISSN -> NLM TA bridge, PubMed coverage confirmation,
|
|
23
|
+
then the coordinate query `[ta]+[vi]+[pg]` for
|
|
24
|
+
DOI-less Chinese medical citations
|
|
25
|
+
|
|
26
|
+
Differences from the Crossref / OpenAlex / S2 / arXiv siblings:
|
|
27
|
+
|
|
28
|
+
- APPLICABILITY GATE (mirrors arxiv's "not applicable != unmatched"): a
|
|
29
|
+
non-Chinese citation is `skipped`, never `unmatched`, so English corpora are
|
|
30
|
+
untouched by this resolver.
|
|
31
|
+
- PRECISION ASYMMETRY: a refuted identifier is strong evidence (`unmatched`
|
|
32
|
+
keyed by `id`), but a resolved-yet-unverifiable identifier is NEVER promoted
|
|
33
|
+
to `matched`. The CNKI RA serves an HTML disambiguation page rather than
|
|
34
|
+
CSL-JSON and we deliberately refuse to parse it (see `handle_exists`), so
|
|
35
|
+
"the DOI exists but its title cannot be machine-checked" degrades to
|
|
36
|
+
`unmatched` keyed by `title` — which the ARS reducer folds into
|
|
37
|
+
`unresolvable`, never `false`.
|
|
38
|
+
- CHINESE-AWARE EXACT-TITLE-OR-BUST: the shared `exact_normalized_title`
|
|
39
|
+
(#431) is ASCII-centric and measurably breaks on legitimate Chinese title
|
|
40
|
+
variants (fullwidth forms, CJK terminal punctuation, interior spaces —
|
|
41
|
+
measured 2026-07-27, see the protocol doc). `_cn_titles_match` normalizes
|
|
42
|
+
those away and then requires EXACT equality. The shared fuzzy `_similarity`
|
|
43
|
+
is excluded from the rule entirely: on CJK titles it separates almost
|
|
44
|
+
nothing (0.510 for an unrelated paper vs 0.577 for a fullwidth spelling of
|
|
45
|
+
the identical one), so it is neither sufficient nor safe as an extra
|
|
46
|
+
necessary condition.
|
|
47
|
+
- Every applicable terminal non-decision produces a human-confirmation
|
|
48
|
+
checklist item (P0-P3 priority) rather than a fabrication verdict.
|
|
49
|
+
|
|
50
|
+
STATUS: standalone client (#595). It is NOT wired into
|
|
51
|
+
`scripts/verification_gate/`, the `resolver_outcomes` schema, the k=0..4
|
|
52
|
+
triangulation matrix, or `shared/contracts/degradation_registry.json`. The
|
|
53
|
+
status vocabulary below deliberately mirrors `citation_verification_summary.py`
|
|
54
|
+
so a future integration carries no SEMANTIC change — the schema-side deltas it
|
|
55
|
+
would still need (the four-key `resolver_outcomes` lock, the `queried_by`
|
|
56
|
+
description text, the `skipped` "did not run" wording) are enumerated in the
|
|
57
|
+
protocol doc's "Three-state semantics" section for the #593 issue-first
|
|
58
|
+
integration.
|
|
59
|
+
"""
|
|
60
|
+
from __future__ import annotations
|
|
61
|
+
|
|
62
|
+
import http.client
|
|
63
|
+
import json
|
|
64
|
+
import re
|
|
65
|
+
import string
|
|
66
|
+
import time
|
|
67
|
+
import urllib.error
|
|
68
|
+
import urllib.parse
|
|
69
|
+
import urllib.request
|
|
70
|
+
from dataclasses import dataclass
|
|
71
|
+
from enum import Enum
|
|
72
|
+
from typing import Any
|
|
73
|
+
|
|
74
|
+
# Dual-path import: see openalex_client.py comment.
|
|
75
|
+
try:
|
|
76
|
+
from _text_similarity import _MAX_RETRIES
|
|
77
|
+
except ImportError: # pragma: no cover - exercised by the package-import path
|
|
78
|
+
from scripts._text_similarity import _MAX_RETRIES
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
_DOI_RA_BASE = "https://doi.org/doiRA/"
|
|
82
|
+
_DOI_RESOLVE_BASE = "https://doi.org/"
|
|
83
|
+
_HANDLE_API_BASE = "https://hdl.handle.net/api/handles/"
|
|
84
|
+
_EUTILS_BASE = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/"
|
|
85
|
+
|
|
86
|
+
# Every host this client is permitted to contact. The zero-scraping red line
|
|
87
|
+
# (no CNKI / Wanfang / VIP) is enforced here as code, not only as prose.
|
|
88
|
+
_ALLOWED_API_HOSTS = frozenset({
|
|
89
|
+
"doi.org",
|
|
90
|
+
"hdl.handle.net",
|
|
91
|
+
"eutils.ncbi.nlm.nih.gov",
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
_CSL_ACCEPT = "application/vnd.citationstyles.csl+json"
|
|
95
|
+
_NCBI_TOOL = "academic-research-skills"
|
|
96
|
+
# Bound every response before parsing it. The largest payload this standalone
|
|
97
|
+
# client requests is an E-utilities JSON response with retmax=5; 2 MiB leaves a
|
|
98
|
+
# generous margin without allowing a redirect target or broken upstream to
|
|
99
|
+
# stream an unbounded body into the process.
|
|
100
|
+
MAX_BODY_BYTES = 2 * 1024 * 1024
|
|
101
|
+
|
|
102
|
+
# NCBI asks for <= 3 req/s without an API key (10 with one). We pace at the
|
|
103
|
+
# no-key floor unconditionally; an api_key, when supplied, is passed through but
|
|
104
|
+
# does NOT relax the interval — staying polite is cheaper than defending a ban.
|
|
105
|
+
_EUTILS_MIN_INTERVAL = 0.34
|
|
106
|
+
# Neither doi.org nor the Handle proxy publishes a rate floor; 0.2s mirrors the
|
|
107
|
+
# anonymous pacing the sibling index clients use.
|
|
108
|
+
_DOI_MIN_INTERVAL = 0.2
|
|
109
|
+
|
|
110
|
+
# CJK Unified Ideographs (U+4E00-U+9FFF). Extension blocks are deliberately not
|
|
111
|
+
# scanned: the base block is sufficient for the applicability gate, and a
|
|
112
|
+
# narrower gate errs toward `skipped`, which is the safe direction.
|
|
113
|
+
_CJK_LO = "一"
|
|
114
|
+
_CJK_HI = "鿿"
|
|
115
|
+
|
|
116
|
+
# Registration agencies whose DOIs this resolver claims. A Crossref-registered
|
|
117
|
+
# Chinese DOI is left to the existing crossref resolver: re-querying it here
|
|
118
|
+
# would burn quota and amplify the Chinese fuzzy-match false positives the
|
|
119
|
+
# protocol doc measures.
|
|
120
|
+
_RA_ISTIC = "ISTIC"
|
|
121
|
+
_RA_CNKI = "CNKI"
|
|
122
|
+
_CHINESE_RAS = frozenset({_RA_ISTIC, _RA_CNKI})
|
|
123
|
+
|
|
124
|
+
# Status vocabulary, byte-identical to citation_verification_summary.py — the
|
|
125
|
+
# verbatim claim is scoped to this status/queried_by layer only; the schema-side
|
|
126
|
+
# deltas a gate wiring would still need are listed in the protocol doc's
|
|
127
|
+
# "Three-state semantics" section. Values are duplicated (not imported) to keep
|
|
128
|
+
# this client standalone and dependency-free at #595 scope.
|
|
129
|
+
STATUS_MATCHED = "matched"
|
|
130
|
+
STATUS_UNMATCHED = "unmatched"
|
|
131
|
+
STATUS_SKIPPED = "skipped"
|
|
132
|
+
|
|
133
|
+
# Closed reason-code set. Adding a member is a protocol-doc change.
|
|
134
|
+
REASON_CODES = frozenset({
|
|
135
|
+
"DOI_REFUTED",
|
|
136
|
+
"DOI_TITLE_MISMATCH",
|
|
137
|
+
"DOI_TITLE_VERIFIED",
|
|
138
|
+
"DOI_EXISTS_TITLE_UNVERIFIABLE",
|
|
139
|
+
"PUBMED_COORDINATE_VERIFIED",
|
|
140
|
+
"PUBMED_COORDINATE_CANDIDATE_UNVERIFIED",
|
|
141
|
+
"PUBMED_INDEXED_BUT_COORDINATE_MISS",
|
|
142
|
+
"PUBMED_COORDINATE_AMBIGUOUS",
|
|
143
|
+
"INSUFFICIENT_PUBMED_COORDINATES",
|
|
144
|
+
"JOURNAL_NOT_INDEXED",
|
|
145
|
+
"NO_ISSN_MAPPING",
|
|
146
|
+
"DOI_RA_OUT_OF_SCOPE",
|
|
147
|
+
"DOI_RA_UNRESOLVED",
|
|
148
|
+
"NOT_CHINESE_LITERATURE",
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
# Priority is workload ordering for the human, NOT a suspicion score.
|
|
152
|
+
# P0 = the identifier is absent, or it resolves to a different title (the
|
|
153
|
+
# only tier where fabrication language is permitted at all)
|
|
154
|
+
# P1 = the journal IS indexed but the cited coordinates return nothing
|
|
155
|
+
# P2 = the identifier resolves but the title cannot be machine-compared
|
|
156
|
+
# P3 = no applicable automated source — the normal case for social-science,
|
|
157
|
+
# non-core-journal, and pre-digital Chinese literature; NOT suspicious
|
|
158
|
+
_PRIORITY_BY_REASON = {
|
|
159
|
+
"DOI_REFUTED": "P0",
|
|
160
|
+
"DOI_TITLE_MISMATCH": "P0",
|
|
161
|
+
"PUBMED_INDEXED_BUT_COORDINATE_MISS": "P1",
|
|
162
|
+
"PUBMED_COORDINATE_AMBIGUOUS": "P1",
|
|
163
|
+
"PUBMED_COORDINATE_CANDIDATE_UNVERIFIED": "P2",
|
|
164
|
+
"DOI_EXISTS_TITLE_UNVERIFIABLE": "P2",
|
|
165
|
+
"INSUFFICIENT_PUBMED_COORDINATES": "P3",
|
|
166
|
+
"JOURNAL_NOT_INDEXED": "P3",
|
|
167
|
+
"NO_ISSN_MAPPING": "P3",
|
|
168
|
+
"DOI_RA_OUT_OF_SCOPE": "P3",
|
|
169
|
+
"DOI_RA_UNRESOLVED": "P2",
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# --------------------------------------------------------------------------
|
|
173
|
+
# Seed journal map: 中文刊名 -> ISSN -> NLM title abbreviation.
|
|
174
|
+
#
|
|
175
|
+
# EVERY row below was verified live on 2026-07-27 against NCBI E-utilities
|
|
176
|
+
# (`db=nlmcatalog` by `[issn]` -> `esummary.medlineta`, then a `"<ta>"[ta]`
|
|
177
|
+
# PubMed search for the record count). Nothing here is guessed: an unverified
|
|
178
|
+
# row would silently mis-route a real citation into a P1 "indexed but not
|
|
179
|
+
# found" checklist row, which is exactly the false-accusation failure mode this
|
|
180
|
+
# resolver is built to avoid.
|
|
181
|
+
#
|
|
182
|
+
# This is a SEED, not a catalogue. The map is a documented user extension
|
|
183
|
+
# point: pass `journal_map=` to the constructor to merge in your own rows. An
|
|
184
|
+
# unmapped journal yields `NO_ISSN_MAPPING` -> `skipped` — a coverage gap in
|
|
185
|
+
# OUR table is never evidence about the citation. Rows must be built only from
|
|
186
|
+
# publicly redistributable sources (NLM Catalog, ISSN Portal); importing a
|
|
187
|
+
# journal list out of CNKI / Wanfang / VIP is out of bounds.
|
|
188
|
+
#
|
|
189
|
+
# Keys are `normalize_cn_title` outputs so 《中华医学杂志》 and 中华医学杂志
|
|
190
|
+
# hit the same row.
|
|
191
|
+
# --------------------------------------------------------------------------
|
|
192
|
+
_SEED_JOURNAL_ROWS: tuple[tuple[str, str, str, int], ...] = (
|
|
193
|
+
# (Chinese journal name, ISSN, NLM title abbreviation, NLM unique ID)
|
|
194
|
+
("中华医学杂志", "0376-2491", "Zhonghua Yi Xue Za Zhi", 7511141),
|
|
195
|
+
("中华内科杂志", "0578-1426", "Zhonghua Nei Ke Za Zhi", 161387),
|
|
196
|
+
("中华外科杂志", "0529-5815", "Zhonghua Wai Ke Za Zhi", 153611),
|
|
197
|
+
("中华儿科杂志", "0578-1310", "Zhonghua Er Ke Za Zhi", 417427),
|
|
198
|
+
("中华流行病学杂志", "0254-6450", "Zhonghua Liu Xing Bing Xue Za Zhi", 8208604),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class ChineseLiteratureUnavailable(Exception):
|
|
203
|
+
"""Chinese-literature upstream degraded.
|
|
204
|
+
|
|
205
|
+
The caller MUST map this to an `unreachable` outcome and MUST NOT interpret
|
|
206
|
+
the absence of a hit as evidence about existence. fail-closed: this is
|
|
207
|
+
raised, never swallowed into a miss. Distinct from a 404 or Handle
|
|
208
|
+
`responseCode: 100`, which are meaningful typed observations the resolver
|
|
209
|
+
reports as data; only their validated combination refutes a DOI (see
|
|
210
|
+
`_fetch(allow_404=True)`).
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _redact_url(url: str) -> str:
|
|
215
|
+
"""scheme + host + path only. Error/refusal text must never carry the
|
|
216
|
+
query string: it can carry api_key, which must never land in logs /
|
|
217
|
+
raised-exception text. Mirrors openalex_client.py / crossref_client.py
|
|
218
|
+
(#495)."""
|
|
219
|
+
parsed = urllib.parse.urlsplit(url)
|
|
220
|
+
# Rebuild from hostname rather than netloc so rejected userinfo is not
|
|
221
|
+
# reflected into logs either. An explicit port is retained for diagnosis.
|
|
222
|
+
host = parsed.hostname or ""
|
|
223
|
+
try:
|
|
224
|
+
port = parsed.port
|
|
225
|
+
except ValueError:
|
|
226
|
+
port = None
|
|
227
|
+
netloc = f"{host}:{port}" if port is not None else host
|
|
228
|
+
return urllib.parse.urlunsplit((parsed.scheme, netloc, parsed.path, "", ""))
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _quote_url_component(value: str, *, safe: str = "") -> str:
|
|
232
|
+
"""Percent-encode one URL component inside the typed failure boundary.
|
|
233
|
+
|
|
234
|
+
JSON Schema strings can contain lone UTF-16 surrogates even though UTF-8
|
|
235
|
+
cannot encode them. Treat that malformed scalar as unavailable input
|
|
236
|
+
rather than leaking a raw ``UnicodeEncodeError`` from ``urllib.parse``.
|
|
237
|
+
"""
|
|
238
|
+
try:
|
|
239
|
+
return urllib.parse.quote(value, safe=safe)
|
|
240
|
+
except (TypeError, UnicodeError, ValueError):
|
|
241
|
+
raise ChineseLiteratureUnavailable(
|
|
242
|
+
"URL component contains invalid Unicode"
|
|
243
|
+
) from None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _require_api_url(url: str) -> None:
|
|
247
|
+
"""Multi-host variant of the siblings' `_require_api_url`: this client
|
|
248
|
+
legitimately talks to three hosts, so the guard is an allowlist rather
|
|
249
|
+
than a single-host equality check."""
|
|
250
|
+
try:
|
|
251
|
+
parsed = urllib.parse.urlsplit(url)
|
|
252
|
+
port = parsed.port
|
|
253
|
+
except ValueError:
|
|
254
|
+
# `parsed.port` includes the rejected port token in its ValueError;
|
|
255
|
+
# never retain that untrusted text as an exception cause.
|
|
256
|
+
raise ChineseLiteratureUnavailable("Refusing malformed API URL") from None
|
|
257
|
+
if (
|
|
258
|
+
parsed.scheme != "https"
|
|
259
|
+
or parsed.hostname not in _ALLOWED_API_HOSTS
|
|
260
|
+
or parsed.netloc != parsed.hostname
|
|
261
|
+
or parsed.username is not None
|
|
262
|
+
or parsed.password is not None
|
|
263
|
+
or port is not None
|
|
264
|
+
):
|
|
265
|
+
raise ChineseLiteratureUnavailable(
|
|
266
|
+
f"Refusing non-allowlisted URL: {_redact_url(url)}"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class _SafeRedirectHandler(urllib.request.HTTPRedirectHandler):
|
|
271
|
+
"""Validate the resolved destination before following every redirect.
|
|
272
|
+
|
|
273
|
+
urllib's default handler follows Location automatically. Validating only
|
|
274
|
+
the initial request would let an allowlisted DOI endpoint downgrade to
|
|
275
|
+
HTTP or escape to an arbitrary host. `newurl` is the absolute URL resolved
|
|
276
|
+
by urllib for this hop; the final response URL is checked independently in
|
|
277
|
+
`_fetch` as defense in depth.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
def redirect_request(
|
|
281
|
+
self,
|
|
282
|
+
req: urllib.request.Request,
|
|
283
|
+
fp: Any,
|
|
284
|
+
code: int,
|
|
285
|
+
msg: str,
|
|
286
|
+
headers: Any,
|
|
287
|
+
newurl: str,
|
|
288
|
+
) -> urllib.request.Request | None:
|
|
289
|
+
_require_api_url(newurl)
|
|
290
|
+
method = req.get_method()
|
|
291
|
+
if not (
|
|
292
|
+
code in (301, 302, 303, 307, 308) and method in ("GET", "HEAD")
|
|
293
|
+
or code in (301, 302, 303) and method == "POST"
|
|
294
|
+
):
|
|
295
|
+
raise ChineseLiteratureUnavailable(
|
|
296
|
+
"redirect response is not permitted for this request method"
|
|
297
|
+
)
|
|
298
|
+
content_headers = {"content-length", "content-type"}
|
|
299
|
+
new_headers = {
|
|
300
|
+
key: value for key, value in req.headers.items()
|
|
301
|
+
if key.lower() not in content_headers
|
|
302
|
+
}
|
|
303
|
+
return urllib.request.Request(
|
|
304
|
+
newurl,
|
|
305
|
+
headers=new_headers,
|
|
306
|
+
origin_req_host=req.origin_req_host,
|
|
307
|
+
unverifiable=True,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
def http_error_302(
|
|
311
|
+
self,
|
|
312
|
+
req: urllib.request.Request,
|
|
313
|
+
fp: Any,
|
|
314
|
+
code: int,
|
|
315
|
+
msg: str,
|
|
316
|
+
headers: Any,
|
|
317
|
+
) -> Any:
|
|
318
|
+
"""Follow one validated redirect without stdlib's unbounded `fp.read()`.
|
|
319
|
+
|
|
320
|
+
CPython's default handler drains every redirect body with an unbounded
|
|
321
|
+
read before following it. Redirect bodies are not evidence this client
|
|
322
|
+
consumes, so close the response without reading after the destination
|
|
323
|
+
and loop budget have been checked.
|
|
324
|
+
"""
|
|
325
|
+
try:
|
|
326
|
+
location = headers.get("location") or headers.get("uri")
|
|
327
|
+
if location is None:
|
|
328
|
+
raise ChineseLiteratureUnavailable(
|
|
329
|
+
"redirect response is missing a Location header"
|
|
330
|
+
)
|
|
331
|
+
if not isinstance(location, str):
|
|
332
|
+
raise ChineseLiteratureUnavailable(
|
|
333
|
+
"redirect response has a malformed Location header"
|
|
334
|
+
)
|
|
335
|
+
try:
|
|
336
|
+
quoted = urllib.parse.quote(
|
|
337
|
+
location, encoding="iso-8859-1", safe=string.punctuation,
|
|
338
|
+
)
|
|
339
|
+
newurl = urllib.parse.urljoin(req.full_url, quoted)
|
|
340
|
+
except (TypeError, UnicodeError, ValueError):
|
|
341
|
+
raise ChineseLiteratureUnavailable(
|
|
342
|
+
"redirect response has a malformed Location header"
|
|
343
|
+
) from None
|
|
344
|
+
|
|
345
|
+
_require_api_url(newurl)
|
|
346
|
+
new = self.redirect_request(req, fp, code, msg, headers, newurl)
|
|
347
|
+
if new is None:
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
if hasattr(req, "redirect_dict"):
|
|
351
|
+
visited = new.redirect_dict = req.redirect_dict
|
|
352
|
+
if (
|
|
353
|
+
visited.get(newurl, 0) >= self.max_repeats
|
|
354
|
+
or len(visited) >= self.max_redirections
|
|
355
|
+
):
|
|
356
|
+
raise ChineseLiteratureUnavailable(
|
|
357
|
+
"redirect loop or budget exhausted"
|
|
358
|
+
)
|
|
359
|
+
else:
|
|
360
|
+
visited = new.redirect_dict = req.redirect_dict = {}
|
|
361
|
+
visited[newurl] = visited.get(newurl, 0) + 1
|
|
362
|
+
except ChineseLiteratureUnavailable:
|
|
363
|
+
try:
|
|
364
|
+
fp.close()
|
|
365
|
+
except (OSError, http.client.HTTPException):
|
|
366
|
+
raise ChineseLiteratureUnavailable(
|
|
367
|
+
"redirect response could not be closed"
|
|
368
|
+
) from None
|
|
369
|
+
raise
|
|
370
|
+
except (TypeError, UnicodeError, ValueError):
|
|
371
|
+
try:
|
|
372
|
+
fp.close()
|
|
373
|
+
except (OSError, http.client.HTTPException):
|
|
374
|
+
pass
|
|
375
|
+
raise ChineseLiteratureUnavailable(
|
|
376
|
+
"redirect response has a malformed Location header"
|
|
377
|
+
) from None
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
fp.close()
|
|
381
|
+
except (OSError, http.client.HTTPException):
|
|
382
|
+
raise ChineseLiteratureUnavailable(
|
|
383
|
+
"redirect response could not be closed"
|
|
384
|
+
) from None
|
|
385
|
+
return self.parent.open(new, timeout=req.timeout)
|
|
386
|
+
|
|
387
|
+
http_error_301 = http_error_303 = http_error_307 = http_error_308 = http_error_302
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _safe_urlopen(req: urllib.request.Request, timeout: float = 30) -> Any:
|
|
391
|
+
"""Production transport and the single hermetic-test injection point."""
|
|
392
|
+
opener = urllib.request.build_opener(_SafeRedirectHandler())
|
|
393
|
+
return opener.open(req, timeout=timeout)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _entrez_quoted(value: Any, field: str) -> str:
|
|
397
|
+
"""Return one quoted Entrez literal or fail closed.
|
|
398
|
+
|
|
399
|
+
URL encoding protects HTTP syntax, not Entrez's own query grammar. Quotes,
|
|
400
|
+
brackets, backslashes and control characters could terminate a literal or
|
|
401
|
+
introduce a new field tag, so untrusted citation fields carrying any of
|
|
402
|
+
them are rejected before a request is built.
|
|
403
|
+
"""
|
|
404
|
+
text = str(value or "").strip()
|
|
405
|
+
if not text or any(
|
|
406
|
+
ord(ch) < 32 or ch in {'"', "[", "]", "\\"} for ch in text
|
|
407
|
+
):
|
|
408
|
+
raise ChineseLiteratureUnavailable(
|
|
409
|
+
f"unsafe Entrez {field} literal"
|
|
410
|
+
)
|
|
411
|
+
return f'"{text}"'
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _entrez_atom(value: Any, field: str) -> str:
|
|
415
|
+
"""Return an unquoted volume/page atom with no Entrez grammar surface."""
|
|
416
|
+
text = str(value or "").strip()
|
|
417
|
+
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._+/-]*", text):
|
|
418
|
+
raise ChineseLiteratureUnavailable(f"unsafe Entrez {field} atom")
|
|
419
|
+
return text
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _entrez_year(value: Any) -> str:
|
|
423
|
+
text = str(value or "").strip()
|
|
424
|
+
if not re.fullmatch(r"[12][0-9]{3}", text):
|
|
425
|
+
raise ChineseLiteratureUnavailable("unsafe Entrez publication year")
|
|
426
|
+
return text
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _valid_ncbi_email(value: Any) -> bool:
|
|
430
|
+
"""Minimal deterministic identity check; NCBI requires a valid email."""
|
|
431
|
+
return isinstance(value, str) and bool(
|
|
432
|
+
re.fullmatch(r"[^@\s]+@[^@\s]+\.[^@\s]+", value)
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _first_page(value: Any) -> str:
|
|
437
|
+
"""Return the first page/e-location atom from common range separators."""
|
|
438
|
+
return re.split(r"[-–—]", str(value or ""), maxsplit=1)[0].strip()
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
class DoiTitleState(str, Enum):
|
|
442
|
+
"""Closed outcome set for one DOI-keyed machine-title lookup."""
|
|
443
|
+
|
|
444
|
+
MATCH = "match"
|
|
445
|
+
MISMATCH = "mismatch"
|
|
446
|
+
NOT_FOUND = "not_found"
|
|
447
|
+
UNVERIFIABLE = "unverifiable"
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
@dataclass(frozen=True)
|
|
451
|
+
class DoiTitleLookupOutcome:
|
|
452
|
+
"""Keep evidence states distinct until orchestration assigns semantics."""
|
|
453
|
+
|
|
454
|
+
state: DoiTitleState
|
|
455
|
+
record: dict[str, Any] | None = None
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def has_cjk(text: str | None) -> bool:
|
|
459
|
+
"""True iff the string contains a CJK Unified Ideograph."""
|
|
460
|
+
return any(_CJK_LO <= ch <= _CJK_HI for ch in text or "")
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def normalize_cn_title(title: str | None) -> str:
|
|
464
|
+
"""Chinese-aware title normalization.
|
|
465
|
+
|
|
466
|
+
The shared `_text_similarity.exact_normalized_title` (#431) is ASCII-centric
|
|
467
|
+
and, measured on real ISTIC metadata 2026-07-27, rejects three legitimate
|
|
468
|
+
variants of one identical Chinese title:
|
|
469
|
+
|
|
470
|
+
- fullwidth latin/digits (ProEXC vs ProEXC): an explicit fullwidth-
|
|
471
|
+
ASCII fold handles these; the shared normalizer does not (measured
|
|
472
|
+
similarity 0.577, exact=False)
|
|
473
|
+
- a trailing CJK full stop (。) and outer title wrappers (《》): not in
|
|
474
|
+
`string.punctuation`, so the shared normalizer keeps them (0.981, False)
|
|
475
|
+
- spaces touching Han characters: Chinese carries no word breaks, so
|
|
476
|
+
these are typesetting noise; whitespace between non-CJK tokens remains
|
|
477
|
+
significant (the measured Chinese/Latin variant scored 0.929, False)
|
|
478
|
+
|
|
479
|
+
Simplified/Traditional folding is deliberately NOT done: it is lossy for
|
|
480
|
+
proper nouns, and a wrong fold would manufacture a false match. The pair is
|
|
481
|
+
surfaced to the human instead.
|
|
482
|
+
"""
|
|
483
|
+
# Fold only the fullwidth ASCII compatibility block that was observed in
|
|
484
|
+
# the motivating metadata. Whole-string NFKC/casefold is too broad for an
|
|
485
|
+
# exact scientific-title key: it collapses e.g. 2² with 22 and Straße with
|
|
486
|
+
# Strasse. U+3000 is the fullwidth/ideographic space and is removed below.
|
|
487
|
+
text = "".join(
|
|
488
|
+
chr(ord(ch) - 0xFEE0) if "!" <= ch <= "~" else " " if ch == " " else ch
|
|
489
|
+
for ch in (title or "")
|
|
490
|
+
).strip()
|
|
491
|
+
|
|
492
|
+
# Only remove wrappers and terminal marks that are demonstrated typesetting
|
|
493
|
+
# noise. Scientific operators and measurements remain byte-significant:
|
|
494
|
+
# ER+ != ER-, CD4+ != CD4−, and 4.5% != 45%. In particular, do not return to
|
|
495
|
+
# a Unicode-category-wide P*/S* deletion rule.
|
|
496
|
+
wrappers = {
|
|
497
|
+
("《", "》"), ("「", "」"), ("『", "』"), ("【", "】"),
|
|
498
|
+
("“", "”"), ("‘", "’"),
|
|
499
|
+
}
|
|
500
|
+
# The fullwidth-ASCII fold above has already mapped `.` to `.`. A final
|
|
501
|
+
# full stop is ordinary title punctuation; keep `?`/`?` because it can
|
|
502
|
+
# distinguish an interrogative title from an otherwise identical one.
|
|
503
|
+
terminal_marks = "。."
|
|
504
|
+
while text:
|
|
505
|
+
previous = text
|
|
506
|
+
text = text.rstrip(terminal_marks).strip()
|
|
507
|
+
if len(text) >= 2 and (text[0], text[-1]) in wrappers:
|
|
508
|
+
text = text[1:-1].strip()
|
|
509
|
+
if text == previous:
|
|
510
|
+
break
|
|
511
|
+
|
|
512
|
+
# Whitespace touching a Han character is ordinary Chinese typesetting
|
|
513
|
+
# noise, including spaces around an embedded Latin abbreviation. Preserve
|
|
514
|
+
# whitespace *between* non-CJK tokens, where deleting it can collapse
|
|
515
|
+
# scientifically distinct names (for example `PD L1` versus `PDL 1`).
|
|
516
|
+
# Case is likewise retained: gene/protein symbols can be case-sensitive,
|
|
517
|
+
# and the measured fullwidth variant already folds to the same case without
|
|
518
|
+
# requiring a broad lowercase transform.
|
|
519
|
+
text = re.sub(r"\s+", " ", text).strip()
|
|
520
|
+
text = re.sub(
|
|
521
|
+
rf"(?<=[{_CJK_LO}-{_CJK_HI}]) +| +(?=[{_CJK_LO}-{_CJK_HI}])",
|
|
522
|
+
"",
|
|
523
|
+
text,
|
|
524
|
+
)
|
|
525
|
+
return text
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _cn_titles_match(candidate: str | None, expected: str | None) -> bool:
|
|
529
|
+
"""Chinese-aware exact-title-or-bust (#431 discipline).
|
|
530
|
+
|
|
531
|
+
The rule is EXACT equality after `normalize_cn_title`. This helper is used
|
|
532
|
+
only after a DOI-keyed lookup, so the #431 `generic_title` veto for
|
|
533
|
+
identifier-free title searches does not apply: a real DOI can legitimately
|
|
534
|
+
resolve to a paper titled "Editorial". The shared fuzzy `_similarity` is
|
|
535
|
+
deliberately NOT part of it, in either direction:
|
|
536
|
+
|
|
537
|
+
- It is not sufficient. Han characters give unrelated papers a high
|
|
538
|
+
baseline overlap — two genuinely different cervical-cancer papers score
|
|
539
|
+
0.510 (measured 2026-07-27), and a Crossref bibliographic query for an
|
|
540
|
+
exact Chinese title returned a completely different paper as its top
|
|
541
|
+
hit. Fuzzy title matching is MORE dangerous in Chinese than in English.
|
|
542
|
+
|
|
543
|
+
- It is not usable as an extra necessary condition either, which is the
|
|
544
|
+
non-obvious half. A legitimate fullwidth spelling of the identical
|
|
545
|
+
title scores 0.577 — BELOW the 0.70 floor — so ANDing the ratio in
|
|
546
|
+
would veto matches that exact normalization correctly established, and
|
|
547
|
+
a real paper would land in the checklist at P0 next to the word
|
|
548
|
+
"fabricated". That miscall costs far more than a missed bad citation,
|
|
549
|
+
so the ratio is excluded. This was caught by a live smoke run against
|
|
550
|
+
real ISTIC metadata, not by reasoning.
|
|
551
|
+
|
|
552
|
+
In short: on CJK titles the 0.70 ratio separates almost nothing (0.510 for
|
|
553
|
+
an unrelated paper vs 0.577 for an identical one), so it earns no place in
|
|
554
|
+
the decision.
|
|
555
|
+
"""
|
|
556
|
+
left, right = normalize_cn_title(candidate), normalize_cn_title(expected)
|
|
557
|
+
if not left or not right:
|
|
558
|
+
return False
|
|
559
|
+
return left == right
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _csl_to_dict(csl: dict[str, Any]) -> dict[str, Any]:
|
|
563
|
+
"""Project CSL-JSON into the shape callers consume.
|
|
564
|
+
|
|
565
|
+
Chinese-DOI metadata quirks defended against here (all observed live on
|
|
566
|
+
ISTIC records, 2026-07-27):
|
|
567
|
+
- `author` frequently collapses a whole name into `given` with no
|
|
568
|
+
family/given split, and mixes pinyin with Han characters -> authors are
|
|
569
|
+
DISPLAY-ONLY and are never a match criterion.
|
|
570
|
+
- `page` frequently carries the first page only.
|
|
571
|
+
- `issued` may be absent -> `year` is None and the year check is SKIPPED
|
|
572
|
+
rather than failed (absence is not mismatch).
|
|
573
|
+
"""
|
|
574
|
+
title = csl.get("title")
|
|
575
|
+
if isinstance(title, list): # some RAs emit title as a single-element list
|
|
576
|
+
if len(title) != 1 or not isinstance(title[0], str):
|
|
577
|
+
raise ChineseLiteratureUnavailable("unexpected CSL title shape")
|
|
578
|
+
title = title[0]
|
|
579
|
+
elif title is not None and not isinstance(title, str):
|
|
580
|
+
raise ChineseLiteratureUnavailable("unexpected CSL title shape")
|
|
581
|
+
container = csl.get("container-title")
|
|
582
|
+
if isinstance(container, list):
|
|
583
|
+
if len(container) != 1 or not isinstance(container[0], str):
|
|
584
|
+
raise ChineseLiteratureUnavailable("unexpected CSL container-title shape")
|
|
585
|
+
container = container[0]
|
|
586
|
+
elif container is not None and not isinstance(container, str):
|
|
587
|
+
raise ChineseLiteratureUnavailable("unexpected CSL container-title shape")
|
|
588
|
+
year = None
|
|
589
|
+
issued = csl.get("issued")
|
|
590
|
+
if issued is not None and not isinstance(issued, dict):
|
|
591
|
+
raise ChineseLiteratureUnavailable("unexpected CSL issued shape")
|
|
592
|
+
if isinstance(issued, dict):
|
|
593
|
+
parts = issued.get("date-parts")
|
|
594
|
+
if parts is not None and not isinstance(parts, list):
|
|
595
|
+
raise ChineseLiteratureUnavailable("unexpected CSL date-parts shape")
|
|
596
|
+
if isinstance(parts, list) and parts:
|
|
597
|
+
if not isinstance(parts[0], list):
|
|
598
|
+
raise ChineseLiteratureUnavailable("unexpected CSL date-parts shape")
|
|
599
|
+
if not parts[0]:
|
|
600
|
+
parts = []
|
|
601
|
+
if isinstance(parts, list) and parts:
|
|
602
|
+
head = parts[0][0]
|
|
603
|
+
if type(head) is int and 1000 <= head <= 2999:
|
|
604
|
+
year = head
|
|
605
|
+
elif isinstance(head, str) and re.fullmatch(r"[12][0-9]{3}", head):
|
|
606
|
+
year = int(head)
|
|
607
|
+
else:
|
|
608
|
+
raise ChineseLiteratureUnavailable("unexpected CSL year shape")
|
|
609
|
+
author_rows = csl.get("author")
|
|
610
|
+
if author_rows is None:
|
|
611
|
+
author_rows = []
|
|
612
|
+
elif not isinstance(author_rows, list):
|
|
613
|
+
raise ChineseLiteratureUnavailable("unexpected CSL author shape")
|
|
614
|
+
authors = []
|
|
615
|
+
for entry in author_rows:
|
|
616
|
+
if not isinstance(entry, dict):
|
|
617
|
+
raise ChineseLiteratureUnavailable("unexpected CSL author entry shape")
|
|
618
|
+
for key in ("given", "family"):
|
|
619
|
+
if entry.get(key) is not None and not isinstance(entry.get(key), str):
|
|
620
|
+
raise ChineseLiteratureUnavailable(
|
|
621
|
+
f"unexpected CSL author {key} shape"
|
|
622
|
+
)
|
|
623
|
+
name = " ".join(
|
|
624
|
+
part for part in (entry.get("given"), entry.get("family"))
|
|
625
|
+
if isinstance(part, str) and part
|
|
626
|
+
).strip()
|
|
627
|
+
if name:
|
|
628
|
+
authors.append(name)
|
|
629
|
+
scalar_values: dict[str, str | None] = {}
|
|
630
|
+
for source_key, output_key in (
|
|
631
|
+
("volume", "volume"),
|
|
632
|
+
("issue", "issue"),
|
|
633
|
+
("page", "page"),
|
|
634
|
+
):
|
|
635
|
+
value = csl.get(source_key)
|
|
636
|
+
if value is not None and type(value) not in (str, int):
|
|
637
|
+
raise ChineseLiteratureUnavailable(
|
|
638
|
+
f"unexpected CSL {source_key} shape"
|
|
639
|
+
)
|
|
640
|
+
scalar_values[output_key] = str(value) if value is not None else None
|
|
641
|
+
|
|
642
|
+
metadata_doi = csl.get("DOI")
|
|
643
|
+
if metadata_doi is not None and not isinstance(metadata_doi, str):
|
|
644
|
+
raise ChineseLiteratureUnavailable("unexpected CSL DOI shape")
|
|
645
|
+
scalar_values["doi"] = metadata_doi
|
|
646
|
+
|
|
647
|
+
return {
|
|
648
|
+
"title": title if isinstance(title, str) else None,
|
|
649
|
+
"year": year,
|
|
650
|
+
"container_title": container if isinstance(container, str) else None,
|
|
651
|
+
**scalar_values,
|
|
652
|
+
"authors": authors,
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _checklist_item(
|
|
657
|
+
*,
|
|
658
|
+
reason_code: str,
|
|
659
|
+
verdict_contribution: str,
|
|
660
|
+
entry: dict[str, Any],
|
|
661
|
+
attempts: list[dict[str, Any]],
|
|
662
|
+
human_action: str,
|
|
663
|
+
verification_urls: list[str] | None = None,
|
|
664
|
+
) -> dict[str, Any]:
|
|
665
|
+
"""Build one human-confirmation row.
|
|
666
|
+
|
|
667
|
+
`human_result` is initialized to None and the tool NEVER fills it: the
|
|
668
|
+
judgement is the human's. Wording discipline: fabrication vocabulary is
|
|
669
|
+
permitted only at P0 (an absent identifier or a DOI-title association that
|
|
670
|
+
resolves to a different title); every other tier reads "pending human
|
|
671
|
+
check", because mislabeling a real paper by a real author as suspected
|
|
672
|
+
fabrication costs far more than missing one bad citation.
|
|
673
|
+
"""
|
|
674
|
+
if reason_code not in REASON_CODES:
|
|
675
|
+
raise ValueError(f"unknown reason_code {reason_code!r}")
|
|
676
|
+
return {
|
|
677
|
+
"citation_key": entry.get("citation_key"),
|
|
678
|
+
"priority": _PRIORITY_BY_REASON.get(reason_code, "P3"),
|
|
679
|
+
"reason_code": reason_code,
|
|
680
|
+
"verdict_contribution": verdict_contribution,
|
|
681
|
+
"cited_as": {
|
|
682
|
+
"title": entry.get("title"),
|
|
683
|
+
"container_title": entry.get("container_title"),
|
|
684
|
+
"year": entry.get("year"),
|
|
685
|
+
"volume": entry.get("volume"),
|
|
686
|
+
"issue": entry.get("issue"),
|
|
687
|
+
"pages": entry.get("pages"),
|
|
688
|
+
"doi": entry.get("doi"),
|
|
689
|
+
},
|
|
690
|
+
"attempts": attempts,
|
|
691
|
+
"human_action": human_action,
|
|
692
|
+
"verification_urls": verification_urls or [],
|
|
693
|
+
"human_result": None,
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
class ChineseLiteratureClient:
|
|
698
|
+
"""Waterfall resolver for Chinese-language citations.
|
|
699
|
+
|
|
700
|
+
Concurrency note: rate-limit pacing is per-instance (matches the siblings).
|
|
701
|
+
Two independent throttle anchors are kept because the DOI/Handle hosts and
|
|
702
|
+
the NCBI host publish different pacing expectations; sharing one anchor
|
|
703
|
+
would either over-throttle DOI lookups or under-throttle NCBI.
|
|
704
|
+
"""
|
|
705
|
+
|
|
706
|
+
def __init__(
|
|
707
|
+
self,
|
|
708
|
+
journal_map: dict[str, dict[str, Any]] | None = None,
|
|
709
|
+
ncbi_api_key: str | None = None,
|
|
710
|
+
ncbi_email: str | None = None,
|
|
711
|
+
) -> None:
|
|
712
|
+
self._journal_map = dict(seed_journal_map())
|
|
713
|
+
if journal_map:
|
|
714
|
+
# User extension point: caller rows override / extend the seed.
|
|
715
|
+
for key, value in journal_map.items():
|
|
716
|
+
self._journal_map[normalize_cn_title(key)] = value
|
|
717
|
+
self._ncbi_api_key = ncbi_api_key
|
|
718
|
+
self._ncbi_email = ncbi_email
|
|
719
|
+
self._last_doi_at: float | None = None
|
|
720
|
+
self._last_eutils_at: float | None = None
|
|
721
|
+
self._user_agent = "ARS-v3.19"
|
|
722
|
+
|
|
723
|
+
# ---------- transport ----------
|
|
724
|
+
|
|
725
|
+
def _throttle(self, attr: str, interval: float) -> None:
|
|
726
|
+
last = getattr(self, attr)
|
|
727
|
+
if last is None:
|
|
728
|
+
return
|
|
729
|
+
# time.monotonic for elapsed measurement: NTP / manual clock adjustments
|
|
730
|
+
# can make time.time run backwards (#128 §6). Aligns with
|
|
731
|
+
# arxiv_client.py / crossref_client.py.
|
|
732
|
+
elapsed = time.monotonic() - last
|
|
733
|
+
if elapsed < interval:
|
|
734
|
+
time.sleep(interval - elapsed)
|
|
735
|
+
|
|
736
|
+
def _fetch(
|
|
737
|
+
self,
|
|
738
|
+
url: str,
|
|
739
|
+
*,
|
|
740
|
+
accept: str,
|
|
741
|
+
throttle_attr: str,
|
|
742
|
+
interval: float,
|
|
743
|
+
allow_404: bool = False,
|
|
744
|
+
) -> tuple[int, bytes]:
|
|
745
|
+
"""One paced HTTP GET returning `(status, body)`.
|
|
746
|
+
|
|
747
|
+
A 404 is returned as data ONLY when `allow_404`: on the DOI/Handle paths
|
|
748
|
+
it is a meaningful NOT_FOUND observation, not a degradation. A DOI CSL
|
|
749
|
+
404 alone does not refute an identifier; orchestration requires an
|
|
750
|
+
independent Handle absence before assigning `DOI_REFUTED`. Everything
|
|
751
|
+
else degrades:
|
|
752
|
+
|
|
753
|
+
- 429: backoff and retry up to `_MAX_RETRIES` (shared budget), then
|
|
754
|
+
raise. The backoff respects the endpoint's own pacing floor so a
|
|
755
|
+
retry cannot itself violate the limit the 429 is enforcing —
|
|
756
|
+
same rule as arxiv_client.py.
|
|
757
|
+
- 5xx: NO retry, raise immediately (fail fast; the sibling clients do
|
|
758
|
+
the same and the tests pin the request count).
|
|
759
|
+
- transport / timeout / truncated body / unparseable body: raise.
|
|
760
|
+
"""
|
|
761
|
+
_require_api_url(url)
|
|
762
|
+
# Every error message below uses the query-stripped URL: the query can
|
|
763
|
+
# carry api_key, which must never land in logs / raised-exception
|
|
764
|
+
# text (#495 discipline, mirrors crossref/openalex).
|
|
765
|
+
redacted = _redact_url(url)
|
|
766
|
+
headers = {"User-Agent": self._user_agent, "Accept": accept}
|
|
767
|
+
req = urllib.request.Request(url, headers=headers)
|
|
768
|
+
|
|
769
|
+
# Pace once before the first attempt; the 429 branch below re-anchors
|
|
770
|
+
# after its own backoff, so a retry never double-sleeps (mirrors
|
|
771
|
+
# arxiv_client.py, whose throttle also sits outside the retry loop).
|
|
772
|
+
self._throttle(throttle_attr, interval)
|
|
773
|
+
setattr(self, throttle_attr, time.monotonic())
|
|
774
|
+
|
|
775
|
+
for attempt in range(_MAX_RETRIES + 1):
|
|
776
|
+
try:
|
|
777
|
+
# `_safe_urlopen` installs the per-hop redirect guard. The
|
|
778
|
+
# response URL is checked again because injected transports,
|
|
779
|
+
# alternate handlers, and future refactors must not bypass the
|
|
780
|
+
# same trust boundary.
|
|
781
|
+
with _safe_urlopen(req, timeout=30) as resp:
|
|
782
|
+
try:
|
|
783
|
+
final_url = resp.geturl()
|
|
784
|
+
if not isinstance(final_url, str) or not final_url:
|
|
785
|
+
raise ChineseLiteratureUnavailable(
|
|
786
|
+
f"response has no final URL for {redacted}"
|
|
787
|
+
)
|
|
788
|
+
_require_api_url(final_url)
|
|
789
|
+
|
|
790
|
+
response_status = getattr(resp, "status", None)
|
|
791
|
+
if type(response_status) is not int:
|
|
792
|
+
raise ChineseLiteratureUnavailable(
|
|
793
|
+
f"response has invalid status for {redacted}"
|
|
794
|
+
)
|
|
795
|
+
if response_status == 404 and allow_404:
|
|
796
|
+
return 404, b""
|
|
797
|
+
if not 200 <= response_status < 300:
|
|
798
|
+
raise ChineseLiteratureUnavailable(
|
|
799
|
+
f"HTTP {response_status} for {redacted}"
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
response_headers = getattr(resp, "headers", None)
|
|
803
|
+
if response_headers is None:
|
|
804
|
+
raise ChineseLiteratureUnavailable(
|
|
805
|
+
f"response has no headers for {redacted}"
|
|
806
|
+
)
|
|
807
|
+
content_length = response_headers.get("Content-Length")
|
|
808
|
+
if content_length is not None:
|
|
809
|
+
normalized_length = (
|
|
810
|
+
content_length.strip(" \t")
|
|
811
|
+
if isinstance(content_length, str)
|
|
812
|
+
else ""
|
|
813
|
+
)
|
|
814
|
+
if (
|
|
815
|
+
not isinstance(content_length, str)
|
|
816
|
+
or len(normalized_length) > 20
|
|
817
|
+
or not re.fullmatch(r"[0-9]+", normalized_length)
|
|
818
|
+
):
|
|
819
|
+
raise ChineseLiteratureUnavailable(
|
|
820
|
+
f"invalid Content-Length for {redacted}"
|
|
821
|
+
)
|
|
822
|
+
declared_length = int(normalized_length)
|
|
823
|
+
if declared_length < 0 or declared_length > MAX_BODY_BYTES:
|
|
824
|
+
raise ChineseLiteratureUnavailable(
|
|
825
|
+
f"response body exceeds {MAX_BODY_BYTES} bytes "
|
|
826
|
+
f"for {redacted}"
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
body = resp.read(MAX_BODY_BYTES + 1)
|
|
830
|
+
if not isinstance(body, bytes):
|
|
831
|
+
raise ChineseLiteratureUnavailable(
|
|
832
|
+
f"non-bytes response body for {redacted}"
|
|
833
|
+
)
|
|
834
|
+
if len(body) > MAX_BODY_BYTES:
|
|
835
|
+
raise ChineseLiteratureUnavailable(
|
|
836
|
+
f"response body exceeds {MAX_BODY_BYTES} bytes "
|
|
837
|
+
f"for {redacted}"
|
|
838
|
+
)
|
|
839
|
+
if (
|
|
840
|
+
content_length is not None
|
|
841
|
+
and declared_length != len(body)
|
|
842
|
+
):
|
|
843
|
+
raise ChineseLiteratureUnavailable(
|
|
844
|
+
f"truncated response body for {redacted}"
|
|
845
|
+
)
|
|
846
|
+
return response_status, body
|
|
847
|
+
except (OSError, http.client.HTTPException):
|
|
848
|
+
# IncompleteRead inherits HTTPException, not OSError: a
|
|
849
|
+
# truncated body must degrade, never become a miss.
|
|
850
|
+
raise ChineseLiteratureUnavailable(
|
|
851
|
+
f"read failed for {redacted}"
|
|
852
|
+
) from None
|
|
853
|
+
except urllib.error.HTTPError as exc:
|
|
854
|
+
try:
|
|
855
|
+
error_url = exc.geturl()
|
|
856
|
+
if not isinstance(error_url, str) or not error_url:
|
|
857
|
+
raise ChineseLiteratureUnavailable(
|
|
858
|
+
f"HTTP error has no final URL for {redacted}"
|
|
859
|
+
) from None
|
|
860
|
+
try:
|
|
861
|
+
_require_api_url(error_url)
|
|
862
|
+
except ChineseLiteratureUnavailable:
|
|
863
|
+
raise ChineseLiteratureUnavailable(
|
|
864
|
+
f"HTTP error final URL refused for {redacted}"
|
|
865
|
+
) from None
|
|
866
|
+
if exc.code == 404 and allow_404:
|
|
867
|
+
return 404, b""
|
|
868
|
+
if exc.code == 429 and attempt < _MAX_RETRIES:
|
|
869
|
+
# Sleep the endpoint's pacing floor (>= 2s), then refresh the
|
|
870
|
+
# throttle anchor so the next call paces from actual wake
|
|
871
|
+
# time rather than from before the sleep.
|
|
872
|
+
time.sleep(max(interval, 2.0) * (attempt + 1))
|
|
873
|
+
setattr(self, throttle_attr, time.monotonic())
|
|
874
|
+
continue
|
|
875
|
+
raise ChineseLiteratureUnavailable(
|
|
876
|
+
f"HTTP {exc.code} for {redacted}"
|
|
877
|
+
) from None
|
|
878
|
+
finally:
|
|
879
|
+
try:
|
|
880
|
+
exc.close()
|
|
881
|
+
except (OSError, http.client.HTTPException):
|
|
882
|
+
raise ChineseLiteratureUnavailable(
|
|
883
|
+
f"HTTP error response could not be closed for {redacted}"
|
|
884
|
+
) from None
|
|
885
|
+
except (urllib.error.URLError, TimeoutError):
|
|
886
|
+
raise ChineseLiteratureUnavailable(
|
|
887
|
+
f"network error for {redacted}"
|
|
888
|
+
) from None
|
|
889
|
+
except OSError:
|
|
890
|
+
# Some alternate transports and response context managers can
|
|
891
|
+
# surface a raw OSError rather than wrapping it in URLError.
|
|
892
|
+
# Keep that close/transport failure inside the same typed
|
|
893
|
+
# degradation boundary.
|
|
894
|
+
raise ChineseLiteratureUnavailable(
|
|
895
|
+
f"network error for {redacted}"
|
|
896
|
+
) from None
|
|
897
|
+
except (http.client.HTTPException, ValueError):
|
|
898
|
+
# http.client.InvalidURL (an HTTPException raised when a
|
|
899
|
+
# control character survives into the request line) and any
|
|
900
|
+
# ValueError-shaped malformed-request rejection must degrade
|
|
901
|
+
# cleanly, never crash resolve() outside its contract.
|
|
902
|
+
# Identifiers are percent-encoded before reaching here, so this
|
|
903
|
+
# is defense in depth, not the primary sanitizer.
|
|
904
|
+
raise ChineseLiteratureUnavailable(
|
|
905
|
+
f"invalid request for {redacted}"
|
|
906
|
+
) from None
|
|
907
|
+
|
|
908
|
+
raise ChineseLiteratureUnavailable(f"rate limit exhausted for {redacted}")
|
|
909
|
+
|
|
910
|
+
def _fetch_json(self, url: str, **kwargs: Any) -> tuple[int, Any]:
|
|
911
|
+
"""`_fetch` + JSON decode. An unparseable 200 body is a degradation, not
|
|
912
|
+
an empty result (#331: a proxy/CDN HTML error page served with 200 must
|
|
913
|
+
never be cached as a false negative)."""
|
|
914
|
+
status, body = self._fetch(url, **kwargs)
|
|
915
|
+
if status == 404:
|
|
916
|
+
return 404, None
|
|
917
|
+
try:
|
|
918
|
+
return status, json.loads(body)
|
|
919
|
+
except (
|
|
920
|
+
json.JSONDecodeError,
|
|
921
|
+
ValueError,
|
|
922
|
+
UnicodeDecodeError,
|
|
923
|
+
RecursionError,
|
|
924
|
+
) as exc:
|
|
925
|
+
raise ChineseLiteratureUnavailable(
|
|
926
|
+
f"unparseable JSON body from {_redact_url(url)}: {exc}"
|
|
927
|
+
) from exc
|
|
928
|
+
|
|
929
|
+
# ---------- stage 1a: registration-agency routing ----------
|
|
930
|
+
|
|
931
|
+
def ra_for(self, doi: str) -> str | None:
|
|
932
|
+
"""`https://doi.org/doiRA/<prefix>` -> RA name ("ISTIC" / "CNKI" /
|
|
933
|
+
"Crossref" / ...), or None when the prefix is unknown to the DOI
|
|
934
|
+
Foundation.
|
|
935
|
+
|
|
936
|
+
This is pure routing metadata and NEVER produces a verdict by itself:
|
|
937
|
+
an unknown prefix could equally be a typo or a very new registrant.
|
|
938
|
+
"""
|
|
939
|
+
prefix = (doi or "").split("/", 1)[0].strip()
|
|
940
|
+
if not prefix.startswith("10.") or len(prefix) <= 3:
|
|
941
|
+
return None
|
|
942
|
+
status, rows = self._fetch_json(
|
|
943
|
+
_DOI_RA_BASE + _quote_url_component(prefix),
|
|
944
|
+
accept="application/json",
|
|
945
|
+
throttle_attr="_last_doi_at",
|
|
946
|
+
interval=_DOI_MIN_INTERVAL,
|
|
947
|
+
allow_404=True,
|
|
948
|
+
)
|
|
949
|
+
if status == 404:
|
|
950
|
+
return None
|
|
951
|
+
if (
|
|
952
|
+
not isinstance(rows, list)
|
|
953
|
+
or len(rows) != 1
|
|
954
|
+
or not isinstance(rows[0], dict)
|
|
955
|
+
):
|
|
956
|
+
raise ChineseLiteratureUnavailable(
|
|
957
|
+
f"unexpected RA lookup shape for {prefix}"
|
|
958
|
+
)
|
|
959
|
+
row = rows[0]
|
|
960
|
+
echoed_prefix = row.get("DOI")
|
|
961
|
+
if not isinstance(echoed_prefix, str) or echoed_prefix != prefix:
|
|
962
|
+
raise ChineseLiteratureUnavailable(
|
|
963
|
+
f"RA lookup returned a mismatched DOI prefix for {prefix}"
|
|
964
|
+
)
|
|
965
|
+
ra = row.get("RA")
|
|
966
|
+
# For an unknown prefix the endpoint answers 200 with a `status` field
|
|
967
|
+
# in place of `RA` (verified 2026-07-27: /doiRA/10.99999 ->
|
|
968
|
+
# [{"DOI": "10.99999", "status": "DOI does not exist"}]), so a missing
|
|
969
|
+
# RA key means "no agency", but only for the endpoint's documented
|
|
970
|
+
# not-found row. Any other missing/malformed RA is degradation rather
|
|
971
|
+
# than an invented unknown-prefix observation.
|
|
972
|
+
if ra is None and row.get("status") == "DOI does not exist":
|
|
973
|
+
return None
|
|
974
|
+
if (
|
|
975
|
+
not isinstance(ra, str)
|
|
976
|
+
or not ra
|
|
977
|
+
or ra != ra.strip()
|
|
978
|
+
or any(ord(ch) < 32 for ch in ra)
|
|
979
|
+
):
|
|
980
|
+
raise ChineseLiteratureUnavailable(
|
|
981
|
+
f"unexpected RA value for {prefix}"
|
|
982
|
+
)
|
|
983
|
+
return ra
|
|
984
|
+
|
|
985
|
+
# ---------- stage 1b: ISTIC content negotiation ----------
|
|
986
|
+
|
|
987
|
+
def doi_lookup_with_title_check(
|
|
988
|
+
self, doi: str, expected_title: str,
|
|
989
|
+
) -> DoiTitleLookupOutcome:
|
|
990
|
+
"""ISTIC path: DOI content negotiation + mandatory title cross-check.
|
|
991
|
+
|
|
992
|
+
The closed result distinguishes a 404, a verified mismatch, and a
|
|
993
|
+
record whose title cannot be compared. Keeping those states separate is
|
|
994
|
+
what prevents a 404 followed by Handle existence, or an empty cited
|
|
995
|
+
title, from becoming a false mismatch verdict.
|
|
996
|
+
|
|
997
|
+
A 200 response that is non-JSON or malformed is upstream degradation
|
|
998
|
+
and raises `ChineseLiteratureUnavailable`; it is not an UNVERIFIABLE
|
|
999
|
+
metadata result and cannot drive a citation verdict.
|
|
1000
|
+
"""
|
|
1001
|
+
# Percent-encode the DOI (sibling discipline). safe="/" rather than
|
|
1002
|
+
# crossref's safe="": doi.org / Handle resolve on the path, where the
|
|
1003
|
+
# prefix/suffix slash must survive; crossref's API demands the fully
|
|
1004
|
+
# encoded form because the DOI sits in a different position there.
|
|
1005
|
+
status, body = self._fetch(
|
|
1006
|
+
_DOI_RESOLVE_BASE + _quote_url_component(doi, safe="/"),
|
|
1007
|
+
accept=_CSL_ACCEPT,
|
|
1008
|
+
throttle_attr="_last_doi_at",
|
|
1009
|
+
interval=_DOI_MIN_INTERVAL,
|
|
1010
|
+
allow_404=True,
|
|
1011
|
+
)
|
|
1012
|
+
if status == 404:
|
|
1013
|
+
return DoiTitleLookupOutcome(DoiTitleState.NOT_FOUND)
|
|
1014
|
+
try:
|
|
1015
|
+
csl = json.loads(body)
|
|
1016
|
+
except (
|
|
1017
|
+
json.JSONDecodeError,
|
|
1018
|
+
ValueError,
|
|
1019
|
+
UnicodeDecodeError,
|
|
1020
|
+
RecursionError,
|
|
1021
|
+
) as exc:
|
|
1022
|
+
# A 200 non-JSON body can be a proxy/CDN error page. It is an
|
|
1023
|
+
# upstream degradation, never ordinary evidence for a human-check
|
|
1024
|
+
# verdict.
|
|
1025
|
+
raise ChineseLiteratureUnavailable(
|
|
1026
|
+
f"unparseable CSL body from {_redact_url(_DOI_RESOLVE_BASE + doi)}"
|
|
1027
|
+
) from exc
|
|
1028
|
+
if not isinstance(csl, dict):
|
|
1029
|
+
raise ChineseLiteratureUnavailable("unexpected CSL root shape")
|
|
1030
|
+
record = _csl_to_dict(csl)
|
|
1031
|
+
metadata_doi = record.get("doi")
|
|
1032
|
+
if metadata_doi is None:
|
|
1033
|
+
# Some ISTIC CSL responses omit the redundant DOI field. The
|
|
1034
|
+
# request path is still DOI-keyed, so retain that key in evidence.
|
|
1035
|
+
record["doi"] = doi
|
|
1036
|
+
elif (
|
|
1037
|
+
not metadata_doi.strip()
|
|
1038
|
+
or metadata_doi.strip().lower() != doi.strip().lower()
|
|
1039
|
+
):
|
|
1040
|
+
raise ChineseLiteratureUnavailable(
|
|
1041
|
+
"CSL DOI does not match the requested DOI"
|
|
1042
|
+
)
|
|
1043
|
+
else:
|
|
1044
|
+
record["doi"] = metadata_doi.strip()
|
|
1045
|
+
if (
|
|
1046
|
+
not normalize_cn_title(record.get("title"))
|
|
1047
|
+
or not normalize_cn_title(expected_title)
|
|
1048
|
+
):
|
|
1049
|
+
return DoiTitleLookupOutcome(DoiTitleState.UNVERIFIABLE, record)
|
|
1050
|
+
if _cn_titles_match(record["title"], expected_title):
|
|
1051
|
+
return DoiTitleLookupOutcome(DoiTitleState.MATCH, record)
|
|
1052
|
+
|
|
1053
|
+
# A different script is not evidence of a different work. Chinese
|
|
1054
|
+
# indexing services can expose an English translation/shadow title for
|
|
1055
|
+
# the same article, and this client has no translation oracle. Only two
|
|
1056
|
+
# non-empty Chinese titles are comparable strongly enough for a
|
|
1057
|
+
# mismatch to contribute `false`; every cross-language or romanized
|
|
1058
|
+
# pair remains human-verifiable evidence.
|
|
1059
|
+
if not has_cjk(record["title"]) or not has_cjk(expected_title):
|
|
1060
|
+
return DoiTitleLookupOutcome(DoiTitleState.UNVERIFIABLE, record)
|
|
1061
|
+
return DoiTitleLookupOutcome(DoiTitleState.MISMATCH, record)
|
|
1062
|
+
|
|
1063
|
+
# ---------- stage 2a: Handle existence (CNKI etc.) ----------
|
|
1064
|
+
|
|
1065
|
+
def handle_exists(self, doi: str) -> bool:
|
|
1066
|
+
"""Handle System REST: `responseCode` 1 = exists, 100 = not found.
|
|
1067
|
+
|
|
1068
|
+
Verified 2026-07-27: the CNKI prefixes carry NO wildcard handler, so a
|
|
1069
|
+
100 is a trustworthy negative — three fabricated DOIs across both ISTIC
|
|
1070
|
+
and CNKI prefixes all returned 100.
|
|
1071
|
+
|
|
1072
|
+
This is existence ONLY. A True must NOT be promoted to `matched`: no
|
|
1073
|
+
title is obtainable for a CNKI-registered DOI without parsing
|
|
1074
|
+
chndoi.org's resolution page, and this project does not scrape. That is
|
|
1075
|
+
a deliberate compliance tradeoff — one extra human click beats a legal
|
|
1076
|
+
risk — and it is why the CNKI branch tops out at `unresolvable`.
|
|
1077
|
+
"""
|
|
1078
|
+
# Percent-encode (safe="/") — see doi_lookup_with_title_check.
|
|
1079
|
+
status, payload = self._fetch_json(
|
|
1080
|
+
_HANDLE_API_BASE + _quote_url_component(doi, safe="/"),
|
|
1081
|
+
accept="application/json",
|
|
1082
|
+
throttle_attr="_last_doi_at",
|
|
1083
|
+
interval=_DOI_MIN_INTERVAL,
|
|
1084
|
+
allow_404=True,
|
|
1085
|
+
)
|
|
1086
|
+
if status == 404:
|
|
1087
|
+
return False
|
|
1088
|
+
code = payload.get("responseCode") if isinstance(payload, dict) else None
|
|
1089
|
+
if type(code) is int and code == 1:
|
|
1090
|
+
return True
|
|
1091
|
+
if type(code) is int and code == 100:
|
|
1092
|
+
return False
|
|
1093
|
+
# 2 (internal error), 200 (values not found), anything else: unknown
|
|
1094
|
+
# state, degrade rather than guess.
|
|
1095
|
+
raise ChineseLiteratureUnavailable(
|
|
1096
|
+
f"handle responseCode {code!r} for {doi}"
|
|
1097
|
+
)
|
|
1098
|
+
|
|
1099
|
+
# ---------- stage 3: ISSN bridge + PubMed coordinate lookup ----------
|
|
1100
|
+
|
|
1101
|
+
def journal_bridge(self, container_title: str | None) -> dict[str, Any] | None:
|
|
1102
|
+
"""中文刊名 -> {issn, nlm_ta, nlm_id}. OFFLINE map only.
|
|
1103
|
+
|
|
1104
|
+
Returns None when unmapped, and the caller then emits `skipped` — never
|
|
1105
|
+
`unmatched`. An unmapped journal is a coverage gap in OUR table, not
|
|
1106
|
+
evidence about the citation. Runtime expansion of the map by fetching a
|
|
1107
|
+
journal list from any site is forbidden: the map changes by PR review.
|
|
1108
|
+
"""
|
|
1109
|
+
return self._journal_map.get(normalize_cn_title(container_title))
|
|
1110
|
+
|
|
1111
|
+
def journal_is_indexed(self, nlm_ta: str) -> bool:
|
|
1112
|
+
"""Coverage confirmation: does the journal have >= 1 PubMed record?
|
|
1113
|
+
|
|
1114
|
+
NOT "is it in the NLM Catalog" — those differ in practice. 中国全科医学
|
|
1115
|
+
(ISSN 1007-9572) is catalogued as NLM 101299195 yet carries no MEDLINE
|
|
1116
|
+
abbreviation and no PubMed articles (verified 2026-07-27), so a catalog
|
|
1117
|
+
hit would license a meaningless coordinate miss against a journal
|
|
1118
|
+
PubMed never indexed.
|
|
1119
|
+
"""
|
|
1120
|
+
return bool(self._esearch(f'{_entrez_quoted(nlm_ta, "journal")}[ta]', retmax=1))
|
|
1121
|
+
|
|
1122
|
+
def pubmed_coordinate_lookup(
|
|
1123
|
+
self,
|
|
1124
|
+
*,
|
|
1125
|
+
nlm_ta: str,
|
|
1126
|
+
volume: str | None = None,
|
|
1127
|
+
pages: str | None = None,
|
|
1128
|
+
first_author: str | None = None,
|
|
1129
|
+
year: int | None = None,
|
|
1130
|
+
) -> tuple[str, dict[str, Any] | None]:
|
|
1131
|
+
"""Coordinate query `[ta]+[vi]+[pg]`, then `[ta]+[1au]+[dp]` fallback.
|
|
1132
|
+
|
|
1133
|
+
The coordinate tuple is a deterministic candidate query rather than a
|
|
1134
|
+
fuzzy title match: a real journal/volume/first-page triple can return one
|
|
1135
|
+
record and a fabricated page can return zero (verified 2026-07-27), but
|
|
1136
|
+
a hit is not promoted until its DOI binds an exact Chinese title.
|
|
1137
|
+
|
|
1138
|
+
Returns `(outcome, record)` where outcome is one of `"hit"` /
|
|
1139
|
+
`"zero_hit"` / `"ambiguous"` / `"insufficient_input"`. A multi-hit is
|
|
1140
|
+
`ambiguous` with no record; a missing tuple is not mislabeled as a
|
|
1141
|
+
search miss. We never pick one of several.
|
|
1142
|
+
"""
|
|
1143
|
+
terms: list[str] = []
|
|
1144
|
+
journal_term = _entrez_quoted(nlm_ta, "journal")
|
|
1145
|
+
if volume and pages:
|
|
1146
|
+
first_page = _first_page(pages)
|
|
1147
|
+
if first_page:
|
|
1148
|
+
volume_term = _entrez_atom(volume, "volume")
|
|
1149
|
+
page_term = _entrez_atom(first_page, "first page")
|
|
1150
|
+
terms.append(
|
|
1151
|
+
f"{journal_term}[ta] AND {volume_term}[vi] AND {page_term}[pg]"
|
|
1152
|
+
)
|
|
1153
|
+
# Volume + first page is the stronger coordinate. Author/year is used
|
|
1154
|
+
# only when that tuple is unavailable; it must never wash a zero-hit or
|
|
1155
|
+
# ambiguous page citation into a positive match.
|
|
1156
|
+
if not terms and first_author and year:
|
|
1157
|
+
author_term = _entrez_quoted(first_author, "first author")
|
|
1158
|
+
year_term = _entrez_year(year)
|
|
1159
|
+
terms.append(
|
|
1160
|
+
f"{journal_term}[ta] AND {author_term}[1au] AND {year_term}[dp]"
|
|
1161
|
+
)
|
|
1162
|
+
if not terms:
|
|
1163
|
+
return "insufficient_input", None
|
|
1164
|
+
outcome = "zero_hit"
|
|
1165
|
+
for term in terms:
|
|
1166
|
+
ids = self._esearch(term)
|
|
1167
|
+
if len(ids) == 1:
|
|
1168
|
+
return "hit", self._esummary(ids[0])
|
|
1169
|
+
if len(ids) > 1:
|
|
1170
|
+
outcome = "ambiguous"
|
|
1171
|
+
return outcome, None
|
|
1172
|
+
|
|
1173
|
+
def _eutils_url(self, endpoint: str, params: dict[str, str]) -> str:
|
|
1174
|
+
query = dict(params)
|
|
1175
|
+
if not self._ncbi_email or not _valid_ncbi_email(self._ncbi_email):
|
|
1176
|
+
raise ChineseLiteratureUnavailable(
|
|
1177
|
+
"NCBI E-utilities requires a valid ncbi_email"
|
|
1178
|
+
)
|
|
1179
|
+
query["tool"] = _NCBI_TOOL
|
|
1180
|
+
query["email"] = self._ncbi_email
|
|
1181
|
+
if self._ncbi_api_key:
|
|
1182
|
+
query["api_key"] = self._ncbi_api_key
|
|
1183
|
+
try:
|
|
1184
|
+
encoded_query = urllib.parse.urlencode(query)
|
|
1185
|
+
except (TypeError, UnicodeError, ValueError):
|
|
1186
|
+
raise ChineseLiteratureUnavailable(
|
|
1187
|
+
"E-utilities parameters contain invalid Unicode"
|
|
1188
|
+
) from None
|
|
1189
|
+
return _EUTILS_BASE + endpoint + "?" + encoded_query
|
|
1190
|
+
|
|
1191
|
+
def _esearch(self, term: str, retmax: int = 5) -> list[str]:
|
|
1192
|
+
url = self._eutils_url(
|
|
1193
|
+
"esearch.fcgi",
|
|
1194
|
+
{"db": "pubmed", "retmode": "json", "retmax": str(retmax), "term": term},
|
|
1195
|
+
)
|
|
1196
|
+
_status, payload = self._fetch_json(
|
|
1197
|
+
url,
|
|
1198
|
+
accept="application/json",
|
|
1199
|
+
throttle_attr="_last_eutils_at",
|
|
1200
|
+
interval=_EUTILS_MIN_INTERVAL,
|
|
1201
|
+
)
|
|
1202
|
+
try:
|
|
1203
|
+
result = payload["esearchresult"]
|
|
1204
|
+
ids = result["idlist"]
|
|
1205
|
+
count = result["count"]
|
|
1206
|
+
except (KeyError, TypeError) as exc:
|
|
1207
|
+
raise ChineseLiteratureUnavailable(
|
|
1208
|
+
f"unexpected esearch shape for term {term!r}"
|
|
1209
|
+
) from exc
|
|
1210
|
+
if (
|
|
1211
|
+
not isinstance(result, dict)
|
|
1212
|
+
or not isinstance(count, str)
|
|
1213
|
+
or len(count) > 20
|
|
1214
|
+
or not re.fullmatch(r"[0-9]+", count)
|
|
1215
|
+
or not isinstance(ids, list)
|
|
1216
|
+
or any(
|
|
1217
|
+
not isinstance(pmid, str)
|
|
1218
|
+
or not re.fullmatch(r"[1-9][0-9]*", pmid)
|
|
1219
|
+
for pmid in ids
|
|
1220
|
+
)
|
|
1221
|
+
):
|
|
1222
|
+
raise ChineseLiteratureUnavailable(
|
|
1223
|
+
f"unexpected esearch idlist shape for term {term!r}"
|
|
1224
|
+
)
|
|
1225
|
+
count_value = int(count)
|
|
1226
|
+
if (
|
|
1227
|
+
len(ids) != min(count_value, retmax)
|
|
1228
|
+
or len(set(ids)) != len(ids)
|
|
1229
|
+
):
|
|
1230
|
+
raise ChineseLiteratureUnavailable(
|
|
1231
|
+
f"inconsistent esearch count/idlist for term {term!r}"
|
|
1232
|
+
)
|
|
1233
|
+
return ids
|
|
1234
|
+
|
|
1235
|
+
def _esummary(self, pmid: str) -> dict[str, Any]:
|
|
1236
|
+
"""Project one esummary record.
|
|
1237
|
+
|
|
1238
|
+
NOTE (measured 2026-07-27): for a Chinese-language article PubMed's
|
|
1239
|
+
`title` is the ENGLISH bracketed shadow title, never the Chinese
|
|
1240
|
+
original. A Chinese-to-English title cross-check is therefore
|
|
1241
|
+
structurally impossible within PubMed; `resolve()` treats this record
|
|
1242
|
+
only as a candidate and follows its DOI to Chinese metadata.
|
|
1243
|
+
"""
|
|
1244
|
+
url = self._eutils_url(
|
|
1245
|
+
"esummary.fcgi", {"db": "pubmed", "retmode": "json", "id": pmid},
|
|
1246
|
+
)
|
|
1247
|
+
_status, payload = self._fetch_json(
|
|
1248
|
+
url,
|
|
1249
|
+
accept="application/json",
|
|
1250
|
+
throttle_attr="_last_eutils_at",
|
|
1251
|
+
interval=_EUTILS_MIN_INTERVAL,
|
|
1252
|
+
)
|
|
1253
|
+
try:
|
|
1254
|
+
result = payload["result"]
|
|
1255
|
+
record = result[pmid]
|
|
1256
|
+
except (KeyError, TypeError) as exc:
|
|
1257
|
+
raise ChineseLiteratureUnavailable(
|
|
1258
|
+
f"unexpected esummary shape for pmid {pmid}"
|
|
1259
|
+
) from exc
|
|
1260
|
+
if not isinstance(record, dict):
|
|
1261
|
+
raise ChineseLiteratureUnavailable(
|
|
1262
|
+
f"unexpected esummary record shape for pmid {pmid}"
|
|
1263
|
+
)
|
|
1264
|
+
echoed_uids = result.get("uids")
|
|
1265
|
+
if echoed_uids is not None and echoed_uids != [pmid]:
|
|
1266
|
+
raise ChineseLiteratureUnavailable(
|
|
1267
|
+
f"unexpected esummary uids for pmid {pmid}"
|
|
1268
|
+
)
|
|
1269
|
+
echoed_uid = record.get("uid")
|
|
1270
|
+
if echoed_uid is not None and (
|
|
1271
|
+
not isinstance(echoed_uid, str) or echoed_uid != pmid
|
|
1272
|
+
):
|
|
1273
|
+
raise ChineseLiteratureUnavailable(
|
|
1274
|
+
f"unexpected esummary uid for pmid {pmid}"
|
|
1275
|
+
)
|
|
1276
|
+
scalar_fields: dict[str, str | None] = {}
|
|
1277
|
+
for field in ("pubdate", "title", "source", "volume", "issue", "pages", "issn"):
|
|
1278
|
+
value = record.get(field)
|
|
1279
|
+
if value is not None and not isinstance(value, str):
|
|
1280
|
+
raise ChineseLiteratureUnavailable(
|
|
1281
|
+
f"unexpected esummary {field} shape for pmid {pmid}"
|
|
1282
|
+
)
|
|
1283
|
+
scalar_fields[field] = value
|
|
1284
|
+
|
|
1285
|
+
languages = record.get("lang")
|
|
1286
|
+
if languages is None:
|
|
1287
|
+
languages = []
|
|
1288
|
+
elif not isinstance(languages, list) or any(
|
|
1289
|
+
not isinstance(language, str) for language in languages
|
|
1290
|
+
):
|
|
1291
|
+
raise ChineseLiteratureUnavailable(
|
|
1292
|
+
f"unexpected esummary lang shape for pmid {pmid}"
|
|
1293
|
+
)
|
|
1294
|
+
|
|
1295
|
+
pubdate = scalar_fields["pubdate"] or ""
|
|
1296
|
+
if pubdate:
|
|
1297
|
+
pubdate_match = re.fullmatch(
|
|
1298
|
+
r"([12][0-9]{3})(?:[ \t./-][\x20-\x7e]*)?",
|
|
1299
|
+
pubdate,
|
|
1300
|
+
)
|
|
1301
|
+
if pubdate_match is None:
|
|
1302
|
+
raise ChineseLiteratureUnavailable(
|
|
1303
|
+
f"unexpected esummary pubdate value for pmid {pmid}"
|
|
1304
|
+
)
|
|
1305
|
+
year = int(pubdate_match.group(1))
|
|
1306
|
+
else:
|
|
1307
|
+
year = None
|
|
1308
|
+
doi = None
|
|
1309
|
+
article_ids = record.get("articleids")
|
|
1310
|
+
if article_ids is None:
|
|
1311
|
+
article_ids = []
|
|
1312
|
+
elif not isinstance(article_ids, list):
|
|
1313
|
+
raise ChineseLiteratureUnavailable(
|
|
1314
|
+
f"unexpected esummary articleids shape for pmid {pmid}"
|
|
1315
|
+
)
|
|
1316
|
+
for article_id in article_ids:
|
|
1317
|
+
if not isinstance(article_id, dict):
|
|
1318
|
+
raise ChineseLiteratureUnavailable(
|
|
1319
|
+
f"unexpected esummary articleid entry shape for pmid {pmid}"
|
|
1320
|
+
)
|
|
1321
|
+
id_type = article_id.get("idtype")
|
|
1322
|
+
value = article_id.get("value")
|
|
1323
|
+
if not isinstance(id_type, str) or not isinstance(value, str):
|
|
1324
|
+
raise ChineseLiteratureUnavailable(
|
|
1325
|
+
f"unexpected esummary articleid fields for pmid {pmid}"
|
|
1326
|
+
)
|
|
1327
|
+
if id_type == "doi":
|
|
1328
|
+
candidate = value.strip() or None
|
|
1329
|
+
if doi is not None and candidate is not None and candidate != doi:
|
|
1330
|
+
raise ChineseLiteratureUnavailable(
|
|
1331
|
+
f"conflicting esummary DOI values for pmid {pmid}"
|
|
1332
|
+
)
|
|
1333
|
+
doi = candidate
|
|
1334
|
+
return {
|
|
1335
|
+
"pmid": pmid,
|
|
1336
|
+
# English shadow title — display/human-confirmation only.
|
|
1337
|
+
"english_title": scalar_fields["title"],
|
|
1338
|
+
"year": year,
|
|
1339
|
+
"container_title": scalar_fields["source"],
|
|
1340
|
+
"volume": scalar_fields["volume"],
|
|
1341
|
+
"issue": scalar_fields["issue"],
|
|
1342
|
+
"pages": scalar_fields["pages"],
|
|
1343
|
+
"issn": scalar_fields["issn"],
|
|
1344
|
+
"languages": languages,
|
|
1345
|
+
"doi": doi,
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
# ---------- stage 0 + orchestration ----------
|
|
1349
|
+
|
|
1350
|
+
def is_applicable(self, entry: dict[str, Any], ra: str | None = None) -> bool:
|
|
1351
|
+
"""Applicability gate (mirrors `_run_arxiv`'s skip semantics).
|
|
1352
|
+
|
|
1353
|
+
A citation is in scope when any of its title / journal name / language
|
|
1354
|
+
field is Chinese, or when a caller has ALREADY established that its DOI
|
|
1355
|
+
belongs to a Chinese RA and passes that in. Everything else is `skipped`
|
|
1356
|
+
with `queried_by=None`, so an English corpus's verdicts are byte-
|
|
1357
|
+
unchanged by this resolver's existence.
|
|
1358
|
+
|
|
1359
|
+
`resolve()` deliberately calls this WITHOUT `ra`: see the comment there
|
|
1360
|
+
for why establishing the RA first would cost one request per English
|
|
1361
|
+
reference.
|
|
1362
|
+
"""
|
|
1363
|
+
if ra in _CHINESE_RAS:
|
|
1364
|
+
return True
|
|
1365
|
+
language = str(entry.get("language") or "").lower()
|
|
1366
|
+
if language in {"zh", "zh-cn", "zh-tw", "chi", "chinese"}:
|
|
1367
|
+
return True
|
|
1368
|
+
return has_cjk(entry.get("title")) or has_cjk(entry.get("container_title"))
|
|
1369
|
+
|
|
1370
|
+
def resolve(self, entry: dict[str, Any]) -> dict[str, Any]:
|
|
1371
|
+
"""Run the waterfall for one citation and return a structured result.
|
|
1372
|
+
|
|
1373
|
+
Return shape::
|
|
1374
|
+
|
|
1375
|
+
{"status": "matched"|"unmatched"|"skipped",
|
|
1376
|
+
"queried_by": "id"|"title"|None,
|
|
1377
|
+
"reason_code": <closed set>,
|
|
1378
|
+
"evidence": {...} | None,
|
|
1379
|
+
"checklist_item": {...} | None}
|
|
1380
|
+
|
|
1381
|
+
`status`/`queried_by` use the `citation_verification_summary.py`
|
|
1382
|
+
vocabulary verbatim — at the semantic layer; the schema-side deltas a
|
|
1383
|
+
gate wiring would still need are in the protocol doc. There is no
|
|
1384
|
+
`unreachable` return value BY DESIGN: degradation raises
|
|
1385
|
+
`ChineseLiteratureUnavailable` and the CALLER decides, so a network
|
|
1386
|
+
outage can never be silently rendered as a lookup result.
|
|
1387
|
+
|
|
1388
|
+
Asymmetry, restated because it is the whole point: only a refuted or
|
|
1389
|
+
title-mismatched IDENTIFIER returns `queried_by="id"` (the shape the ARS
|
|
1390
|
+
reducer turns into `false`). Every PubMed and CNKI outcome returns
|
|
1391
|
+
`queried_by="title"` at most, which reduces to `unresolvable`.
|
|
1392
|
+
"""
|
|
1393
|
+
# Stage 0 FIRST, on local signals only: an English citation must cost
|
|
1394
|
+
# ZERO requests. Deciding applicability from the RA would mean one
|
|
1395
|
+
# doi.org round-trip per English reference in every bibliography — the
|
|
1396
|
+
# exact waste the applicability gate exists to prevent. The price is
|
|
1397
|
+
# that a Chinese work cited with a fully romanized title, no CJK
|
|
1398
|
+
# anywhere and no language field is `skipped`; that is recorded as a
|
|
1399
|
+
# known non-catch in the protocol doc rather than paid for by every
|
|
1400
|
+
# English corpus.
|
|
1401
|
+
if not self.is_applicable(entry):
|
|
1402
|
+
return {
|
|
1403
|
+
"status": STATUS_SKIPPED,
|
|
1404
|
+
"queried_by": None,
|
|
1405
|
+
"reason_code": "NOT_CHINESE_LITERATURE",
|
|
1406
|
+
"evidence": None,
|
|
1407
|
+
"checklist_item": None,
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
doi = (entry.get("doi") or "").strip()
|
|
1411
|
+
attempts: list[dict[str, Any]] = []
|
|
1412
|
+
|
|
1413
|
+
ra = None
|
|
1414
|
+
if doi:
|
|
1415
|
+
ra = self.ra_for(doi)
|
|
1416
|
+
attempts.append({"stage": "ra_lookup", "outcome": ra or "unknown_prefix"})
|
|
1417
|
+
|
|
1418
|
+
if doi:
|
|
1419
|
+
if ra in _CHINESE_RAS:
|
|
1420
|
+
return self._resolve_by_doi(entry, doi, ra, attempts)
|
|
1421
|
+
if ra is None:
|
|
1422
|
+
return self._skipped_with_checklist(
|
|
1423
|
+
entry, attempts, "DOI_RA_UNRESOLVED",
|
|
1424
|
+
human_action=(
|
|
1425
|
+
"The DOI registration agency could not be established. "
|
|
1426
|
+
"This resolver will not ignore the supplied DOI and fall "
|
|
1427
|
+
"back to coordinates. Verify the DOI manually. Pending "
|
|
1428
|
+
"human check."
|
|
1429
|
+
),
|
|
1430
|
+
)
|
|
1431
|
+
return self._skipped_with_checklist(
|
|
1432
|
+
entry, attempts, "DOI_RA_OUT_OF_SCOPE",
|
|
1433
|
+
human_action=(
|
|
1434
|
+
f"This DOI is registered with {ra}, outside this resolver's "
|
|
1435
|
+
"ISTIC/CNKI scope. Route it to the appropriate DOI resolver; "
|
|
1436
|
+
"the supplied DOI was not replaced by a coordinate lookup. "
|
|
1437
|
+
"Pending human check."
|
|
1438
|
+
),
|
|
1439
|
+
)
|
|
1440
|
+
return self._resolve_by_coordinates(entry, attempts)
|
|
1441
|
+
|
|
1442
|
+
def _resolve_by_doi(
|
|
1443
|
+
self,
|
|
1444
|
+
entry: dict[str, Any],
|
|
1445
|
+
doi: str,
|
|
1446
|
+
ra: str,
|
|
1447
|
+
attempts: list[dict[str, Any]],
|
|
1448
|
+
) -> dict[str, Any]:
|
|
1449
|
+
expected_title = entry.get("title") or ""
|
|
1450
|
+
# Display-only (checklist verification_urls): kept human-readable
|
|
1451
|
+
# unencoded on purpose; the network call sites do their own encoding.
|
|
1452
|
+
resolve_url = _DOI_RESOLVE_BASE + doi
|
|
1453
|
+
|
|
1454
|
+
if ra == _RA_CNKI:
|
|
1455
|
+
# CNKI RA: existence only. Content negotiation would return an HTML
|
|
1456
|
+
# disambiguation page and parsing it is out of bounds.
|
|
1457
|
+
exists = self.handle_exists(doi)
|
|
1458
|
+
attempts.append(
|
|
1459
|
+
{"stage": "handle_existence", "outcome": "exists" if exists else "absent"}
|
|
1460
|
+
)
|
|
1461
|
+
if not exists:
|
|
1462
|
+
return self._refuted(entry, attempts, resolve_url)
|
|
1463
|
+
return {
|
|
1464
|
+
"status": STATUS_UNMATCHED,
|
|
1465
|
+
"queried_by": "title",
|
|
1466
|
+
"reason_code": "DOI_EXISTS_TITLE_UNVERIFIABLE",
|
|
1467
|
+
"evidence": {"doi": doi, "registration_agency": ra},
|
|
1468
|
+
"checklist_item": _checklist_item(
|
|
1469
|
+
reason_code="DOI_EXISTS_TITLE_UNVERIFIABLE",
|
|
1470
|
+
verdict_contribution="unresolvable",
|
|
1471
|
+
entry=entry,
|
|
1472
|
+
attempts=attempts,
|
|
1473
|
+
human_action=(
|
|
1474
|
+
"This DOI resolves, but its registration agency serves no "
|
|
1475
|
+
"machine-readable title. Open the link and confirm the "
|
|
1476
|
+
"title matches the citation. Pending human check — this "
|
|
1477
|
+
"is not a finding about the citation's validity."
|
|
1478
|
+
),
|
|
1479
|
+
verification_urls=[resolve_url],
|
|
1480
|
+
),
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
# ISTIC RA: attempt CSL-JSON within the allowlisted HTTPS boundary.
|
|
1484
|
+
lookup = self.doi_lookup_with_title_check(doi, expected_title)
|
|
1485
|
+
attempts.append({
|
|
1486
|
+
"stage": "content_negotiation",
|
|
1487
|
+
"outcome": lookup.state.value,
|
|
1488
|
+
})
|
|
1489
|
+
|
|
1490
|
+
if lookup.state is DoiTitleState.MATCH:
|
|
1491
|
+
assert lookup.record is not None # closed-state construction invariant
|
|
1492
|
+
return {
|
|
1493
|
+
"status": STATUS_MATCHED,
|
|
1494
|
+
"queried_by": "id",
|
|
1495
|
+
"reason_code": "DOI_TITLE_VERIFIED",
|
|
1496
|
+
"evidence": {"registration_agency": ra, **lookup.record},
|
|
1497
|
+
"checklist_item": None,
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
if lookup.state is DoiTitleState.MISMATCH:
|
|
1501
|
+
return {
|
|
1502
|
+
"status": STATUS_UNMATCHED,
|
|
1503
|
+
"queried_by": "id",
|
|
1504
|
+
"reason_code": "DOI_TITLE_MISMATCH",
|
|
1505
|
+
"evidence": {
|
|
1506
|
+
"doi": doi,
|
|
1507
|
+
"registration_agency": ra,
|
|
1508
|
+
"resolved_title": (lookup.record or {}).get("title"),
|
|
1509
|
+
},
|
|
1510
|
+
"checklist_item": _checklist_item(
|
|
1511
|
+
reason_code="DOI_TITLE_MISMATCH",
|
|
1512
|
+
verdict_contribution="false",
|
|
1513
|
+
entry=entry,
|
|
1514
|
+
attempts=attempts,
|
|
1515
|
+
human_action=(
|
|
1516
|
+
"This DOI resolves to a DIFFERENT title than the one cited "
|
|
1517
|
+
"(a chimeric citation pattern). Verify the identifier "
|
|
1518
|
+
"against the source and correct or remove the reference."
|
|
1519
|
+
),
|
|
1520
|
+
verification_urls=[resolve_url],
|
|
1521
|
+
),
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
if lookup.state is DoiTitleState.NOT_FOUND:
|
|
1525
|
+
# A content-negotiation 404 is only refutation evidence when the
|
|
1526
|
+
# independent Handle API also says absent. Some existing handles do
|
|
1527
|
+
# not expose a CSL representation at doi.org.
|
|
1528
|
+
exists = self.handle_exists(doi)
|
|
1529
|
+
attempts.append({
|
|
1530
|
+
"stage": "handle_existence",
|
|
1531
|
+
"outcome": "exists" if exists else "absent",
|
|
1532
|
+
})
|
|
1533
|
+
if not exists:
|
|
1534
|
+
return self._refuted(entry, attempts, resolve_url)
|
|
1535
|
+
|
|
1536
|
+
# Either the 200 record lacks a comparable title (including an empty
|
|
1537
|
+
# cited title), or content negotiation returned 404 while Handle proves
|
|
1538
|
+
# the DOI exists. Neither state permits a mismatch/false verdict.
|
|
1539
|
+
return self._doi_title_unverifiable(
|
|
1540
|
+
entry, attempts, doi, ra, resolve_url,
|
|
1541
|
+
resolved_title=(lookup.record or {}).get("title"),
|
|
1542
|
+
)
|
|
1543
|
+
|
|
1544
|
+
def _doi_title_unverifiable(
|
|
1545
|
+
self,
|
|
1546
|
+
entry: dict[str, Any],
|
|
1547
|
+
attempts: list[dict[str, Any]],
|
|
1548
|
+
doi: str,
|
|
1549
|
+
ra: str,
|
|
1550
|
+
resolve_url: str,
|
|
1551
|
+
*,
|
|
1552
|
+
resolved_title: Any = None,
|
|
1553
|
+
) -> dict[str, Any]:
|
|
1554
|
+
evidence = {"doi": doi, "registration_agency": ra}
|
|
1555
|
+
if isinstance(resolved_title, str) and resolved_title:
|
|
1556
|
+
evidence["resolved_title"] = resolved_title
|
|
1557
|
+
return {
|
|
1558
|
+
"status": STATUS_UNMATCHED,
|
|
1559
|
+
"queried_by": "title",
|
|
1560
|
+
"reason_code": "DOI_EXISTS_TITLE_UNVERIFIABLE",
|
|
1561
|
+
"evidence": evidence,
|
|
1562
|
+
"checklist_item": _checklist_item(
|
|
1563
|
+
reason_code="DOI_EXISTS_TITLE_UNVERIFIABLE",
|
|
1564
|
+
verdict_contribution="unresolvable",
|
|
1565
|
+
entry=entry,
|
|
1566
|
+
attempts=attempts,
|
|
1567
|
+
human_action=(
|
|
1568
|
+
"This DOI exists, but the cited and resolved titles were "
|
|
1569
|
+
"not both comparable Chinese originals. Open the link and "
|
|
1570
|
+
"confirm the title manually. Pending human check — this is "
|
|
1571
|
+
"not a finding about the citation's validity."
|
|
1572
|
+
),
|
|
1573
|
+
verification_urls=[resolve_url],
|
|
1574
|
+
),
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
def _refuted(
|
|
1578
|
+
self,
|
|
1579
|
+
entry: dict[str, Any],
|
|
1580
|
+
attempts: list[dict[str, Any]],
|
|
1581
|
+
resolve_url: str,
|
|
1582
|
+
) -> dict[str, Any]:
|
|
1583
|
+
"""The identifier does not exist anywhere in the global DOI system."""
|
|
1584
|
+
return {
|
|
1585
|
+
"status": STATUS_UNMATCHED,
|
|
1586
|
+
"queried_by": "id",
|
|
1587
|
+
"reason_code": "DOI_REFUTED",
|
|
1588
|
+
"evidence": {"doi": entry.get("doi")},
|
|
1589
|
+
"checklist_item": _checklist_item(
|
|
1590
|
+
reason_code="DOI_REFUTED",
|
|
1591
|
+
verdict_contribution="false",
|
|
1592
|
+
entry=entry,
|
|
1593
|
+
attempts=attempts,
|
|
1594
|
+
human_action=(
|
|
1595
|
+
"This DOI does not exist in the global DOI system (Handle "
|
|
1596
|
+
"responseCode 100 and no content negotiation). There is no "
|
|
1597
|
+
"wildcard catch-all on these prefixes, so this is positive "
|
|
1598
|
+
"evidence the identifier was fabricated or mistyped. "
|
|
1599
|
+
"Verify against the original journal, correct, or remove."
|
|
1600
|
+
),
|
|
1601
|
+
verification_urls=[resolve_url],
|
|
1602
|
+
),
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
def _resolve_by_coordinates(
|
|
1606
|
+
self, entry: dict[str, Any], attempts: list[dict[str, Any]],
|
|
1607
|
+
) -> dict[str, Any]:
|
|
1608
|
+
"""No Chinese DOI: journal -> ISSN -> NLM TA -> PubMed coordinates."""
|
|
1609
|
+
container = entry.get("container_title")
|
|
1610
|
+
bridged = self.journal_bridge(container)
|
|
1611
|
+
if bridged is None:
|
|
1612
|
+
attempts.append({"stage": "issn_bridge", "outcome": "unmapped"})
|
|
1613
|
+
return self._skipped_with_checklist(
|
|
1614
|
+
entry, attempts, "NO_ISSN_MAPPING",
|
|
1615
|
+
human_action=(
|
|
1616
|
+
"This journal is not in the ISSN/NLM bridge table, so no "
|
|
1617
|
+
"automated source applies. Verify the reference by hand. "
|
|
1618
|
+
"Pending human check — an unmapped journal says nothing "
|
|
1619
|
+
"about the citation."
|
|
1620
|
+
),
|
|
1621
|
+
)
|
|
1622
|
+
nlm_ta = bridged["nlm_ta"]
|
|
1623
|
+
attempts.append({
|
|
1624
|
+
"stage": "issn_bridge",
|
|
1625
|
+
"outcome": "mapped",
|
|
1626
|
+
"detail": f"{bridged['issn']} -> {nlm_ta} (NLM {bridged['nlm_id']})",
|
|
1627
|
+
})
|
|
1628
|
+
|
|
1629
|
+
volume = entry.get("volume")
|
|
1630
|
+
pages = entry.get("pages")
|
|
1631
|
+
first_author = entry.get("first_author_pinyin")
|
|
1632
|
+
cited_year = entry.get("year")
|
|
1633
|
+
cited_year_number = (
|
|
1634
|
+
int(_entrez_year(cited_year)) if cited_year is not None else None
|
|
1635
|
+
)
|
|
1636
|
+
has_volume_page = bool(volume and pages and _first_page(pages))
|
|
1637
|
+
has_author_year = bool(first_author and cited_year_number)
|
|
1638
|
+
if not has_volume_page and not has_author_year:
|
|
1639
|
+
attempts.append({
|
|
1640
|
+
"stage": "pubmed_coordinate_input",
|
|
1641
|
+
"outcome": "insufficient",
|
|
1642
|
+
})
|
|
1643
|
+
return self._skipped_with_checklist(
|
|
1644
|
+
entry,
|
|
1645
|
+
attempts,
|
|
1646
|
+
"INSUFFICIENT_PUBMED_COORDINATES",
|
|
1647
|
+
human_action=(
|
|
1648
|
+
"PubMed coordinate lookup needs either volume plus first "
|
|
1649
|
+
"page, or first-author pinyin plus publication year. These "
|
|
1650
|
+
"fields are incomplete, so no coordinate search was run. "
|
|
1651
|
+
"Verify the reference by hand. Pending human check."
|
|
1652
|
+
),
|
|
1653
|
+
)
|
|
1654
|
+
|
|
1655
|
+
if not self.journal_is_indexed(nlm_ta):
|
|
1656
|
+
attempts.append({"stage": "pubmed_coverage", "outcome": "not_indexed"})
|
|
1657
|
+
return self._skipped_with_checklist(
|
|
1658
|
+
entry, attempts, "JOURNAL_NOT_INDEXED",
|
|
1659
|
+
human_action=(
|
|
1660
|
+
"This journal has no PubMed records, so a coordinate miss "
|
|
1661
|
+
"would carry no information. Verify by hand. Pending human "
|
|
1662
|
+
"check."
|
|
1663
|
+
),
|
|
1664
|
+
)
|
|
1665
|
+
attempts.append({"stage": "pubmed_coverage", "outcome": "indexed"})
|
|
1666
|
+
|
|
1667
|
+
outcome, record = self.pubmed_coordinate_lookup(
|
|
1668
|
+
nlm_ta=nlm_ta,
|
|
1669
|
+
volume=volume,
|
|
1670
|
+
pages=pages,
|
|
1671
|
+
first_author=first_author,
|
|
1672
|
+
year=cited_year_number,
|
|
1673
|
+
)
|
|
1674
|
+
attempts.append({
|
|
1675
|
+
"stage": "pubmed_coordinate",
|
|
1676
|
+
"outcome": outcome,
|
|
1677
|
+
"detail": "volume_page" if has_volume_page else "author_year",
|
|
1678
|
+
})
|
|
1679
|
+
|
|
1680
|
+
if outcome == "hit" and record is not None:
|
|
1681
|
+
# PubMed's title is only an English shadow title, so a coordinate
|
|
1682
|
+
# hit is a candidate, never a match. Structural disagreement rejects
|
|
1683
|
+
# even candidacy; otherwise the candidate DOI must route back to an
|
|
1684
|
+
# allowed Chinese RA and return a Chinese machine title that exactly
|
|
1685
|
+
# matches the cited title.
|
|
1686
|
+
record_issn = (record.get("issn") or "").strip()
|
|
1687
|
+
issn_conflict = bool(record_issn) and record_issn != bridged["issn"]
|
|
1688
|
+
cited_volume = str(volume).strip() if volume is not None else ""
|
|
1689
|
+
record_volume = (record.get("volume") or "").strip()
|
|
1690
|
+
volume_conflict = bool(cited_volume and record_volume) and (
|
|
1691
|
+
cited_volume != record_volume
|
|
1692
|
+
)
|
|
1693
|
+
cited_first_page = _first_page(pages)
|
|
1694
|
+
record_first_page = _first_page(record.get("pages"))
|
|
1695
|
+
page_conflict = bool(cited_first_page and record_first_page) and (
|
|
1696
|
+
cited_first_page != record_first_page
|
|
1697
|
+
)
|
|
1698
|
+
year_conflict = bool(
|
|
1699
|
+
cited_year_number and record.get("year")
|
|
1700
|
+
and cited_year_number != record["year"]
|
|
1701
|
+
)
|
|
1702
|
+
structural_conflicts = [
|
|
1703
|
+
label for label, conflict in (
|
|
1704
|
+
("journal ISSN", issn_conflict),
|
|
1705
|
+
("volume", volume_conflict),
|
|
1706
|
+
("first page", page_conflict),
|
|
1707
|
+
("year", year_conflict),
|
|
1708
|
+
) if conflict
|
|
1709
|
+
]
|
|
1710
|
+
if structural_conflicts:
|
|
1711
|
+
attempts.append({
|
|
1712
|
+
"stage": "pubmed_candidate_verification",
|
|
1713
|
+
"outcome": "structural_conflict",
|
|
1714
|
+
"detail": ", ".join(structural_conflicts),
|
|
1715
|
+
})
|
|
1716
|
+
return self._pubmed_candidate_unverified(
|
|
1717
|
+
entry,
|
|
1718
|
+
attempts,
|
|
1719
|
+
record,
|
|
1720
|
+
"The PubMed coordinate candidate's "
|
|
1721
|
+
+ ", ".join(structural_conflicts)
|
|
1722
|
+
+ " disagrees with the citation.",
|
|
1723
|
+
)
|
|
1724
|
+
|
|
1725
|
+
candidate_doi = record.get("doi")
|
|
1726
|
+
if not isinstance(candidate_doi, str) or not candidate_doi.strip():
|
|
1727
|
+
attempts.append({
|
|
1728
|
+
"stage": "pubmed_candidate_verification",
|
|
1729
|
+
"outcome": "no_doi",
|
|
1730
|
+
})
|
|
1731
|
+
return self._pubmed_candidate_unverified(
|
|
1732
|
+
entry,
|
|
1733
|
+
attempts,
|
|
1734
|
+
record,
|
|
1735
|
+
"The unique PubMed coordinate candidate has no DOI, so its "
|
|
1736
|
+
"English shadow title cannot be bound to the cited Chinese title.",
|
|
1737
|
+
)
|
|
1738
|
+
candidate_doi = candidate_doi.strip()
|
|
1739
|
+
|
|
1740
|
+
candidate_ra = self.ra_for(candidate_doi)
|
|
1741
|
+
attempts.append({
|
|
1742
|
+
"stage": "pubmed_candidate_ra",
|
|
1743
|
+
"outcome": candidate_ra or "unknown_prefix",
|
|
1744
|
+
})
|
|
1745
|
+
if candidate_ra == _RA_ISTIC:
|
|
1746
|
+
title_lookup = self.doi_lookup_with_title_check(
|
|
1747
|
+
candidate_doi, entry.get("title") or "",
|
|
1748
|
+
)
|
|
1749
|
+
attempts.append({
|
|
1750
|
+
"stage": "pubmed_candidate_title",
|
|
1751
|
+
"outcome": title_lookup.state.value,
|
|
1752
|
+
})
|
|
1753
|
+
machine_title = (title_lookup.record or {}).get("title")
|
|
1754
|
+
if (
|
|
1755
|
+
title_lookup.state is DoiTitleState.MATCH
|
|
1756
|
+
and has_cjk(machine_title)
|
|
1757
|
+
and has_cjk(entry.get("title"))
|
|
1758
|
+
):
|
|
1759
|
+
return {
|
|
1760
|
+
"status": STATUS_MATCHED,
|
|
1761
|
+
"queried_by": "title",
|
|
1762
|
+
"reason_code": "PUBMED_COORDINATE_VERIFIED",
|
|
1763
|
+
"evidence": {
|
|
1764
|
+
**record,
|
|
1765
|
+
"registration_agency": candidate_ra,
|
|
1766
|
+
"doi_metadata": title_lookup.record,
|
|
1767
|
+
},
|
|
1768
|
+
"checklist_item": None,
|
|
1769
|
+
}
|
|
1770
|
+
return self._pubmed_candidate_unverified(
|
|
1771
|
+
entry,
|
|
1772
|
+
attempts,
|
|
1773
|
+
record,
|
|
1774
|
+
"The PubMed candidate DOI did not yield an exactly matching "
|
|
1775
|
+
"machine-readable Chinese title.",
|
|
1776
|
+
)
|
|
1777
|
+
|
|
1778
|
+
if candidate_ra == _RA_CNKI:
|
|
1779
|
+
exists = self.handle_exists(candidate_doi)
|
|
1780
|
+
attempts.append({
|
|
1781
|
+
"stage": "pubmed_candidate_handle",
|
|
1782
|
+
"outcome": "exists" if exists else "absent",
|
|
1783
|
+
})
|
|
1784
|
+
detail = (
|
|
1785
|
+
"The PubMed candidate DOI exists, but CNKI supplies no "
|
|
1786
|
+
"key-free machine-readable Chinese title."
|
|
1787
|
+
if exists else
|
|
1788
|
+
"The PubMed candidate DOI could not be confirmed by Handle."
|
|
1789
|
+
)
|
|
1790
|
+
return self._pubmed_candidate_unverified(
|
|
1791
|
+
entry, attempts, record, detail,
|
|
1792
|
+
)
|
|
1793
|
+
|
|
1794
|
+
return self._pubmed_candidate_unverified(
|
|
1795
|
+
entry,
|
|
1796
|
+
attempts,
|
|
1797
|
+
record,
|
|
1798
|
+
"The PubMed candidate DOI belongs to an out-of-scope or unknown "
|
|
1799
|
+
"registration agency, so this client cannot bind it to a Chinese title.",
|
|
1800
|
+
)
|
|
1801
|
+
|
|
1802
|
+
if outcome == "ambiguous":
|
|
1803
|
+
return {
|
|
1804
|
+
"status": STATUS_UNMATCHED,
|
|
1805
|
+
"queried_by": "title",
|
|
1806
|
+
"reason_code": "PUBMED_COORDINATE_AMBIGUOUS",
|
|
1807
|
+
"evidence": None,
|
|
1808
|
+
"checklist_item": _checklist_item(
|
|
1809
|
+
reason_code="PUBMED_COORDINATE_AMBIGUOUS",
|
|
1810
|
+
verdict_contribution="unresolvable",
|
|
1811
|
+
entry=entry,
|
|
1812
|
+
attempts=attempts,
|
|
1813
|
+
human_action=(
|
|
1814
|
+
"Several PubMed records share these coordinates, so no "
|
|
1815
|
+
"single record can be attributed. Pending human check."
|
|
1816
|
+
),
|
|
1817
|
+
),
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
# Coverage confirmed, coordinates return nothing. This deliberately does
|
|
1821
|
+
# NOT escalate to `false`: (1) the journal-name -> NLM TA bridge is
|
|
1822
|
+
# heuristic and a wrong row would condemn a real paper; (2) PubMed
|
|
1823
|
+
# indexes Chinese journals selectively, so "the journal is indexed" does
|
|
1824
|
+
# not mean "this volume is indexed"; (3) C-V6(a) defines `false` as
|
|
1825
|
+
# ID-keyed unmatched and a coordinate tuple is not an identifier.
|
|
1826
|
+
# The strength goes into the P1 priority, not into the verdict.
|
|
1827
|
+
if has_volume_page:
|
|
1828
|
+
miss_description = "the cited volume/page"
|
|
1829
|
+
verification_term = (
|
|
1830
|
+
f'"{nlm_ta}"[ta] AND {volume}[vi] AND {_first_page(pages)}[pg]'
|
|
1831
|
+
)
|
|
1832
|
+
else:
|
|
1833
|
+
miss_description = "the cited first-author/year coordinates"
|
|
1834
|
+
verification_term = (
|
|
1835
|
+
f'"{nlm_ta}"[ta] AND "{first_author}"[1au] '
|
|
1836
|
+
f"AND {cited_year_number}[dp]"
|
|
1837
|
+
)
|
|
1838
|
+
return {
|
|
1839
|
+
"status": STATUS_UNMATCHED,
|
|
1840
|
+
"queried_by": "title",
|
|
1841
|
+
"reason_code": "PUBMED_INDEXED_BUT_COORDINATE_MISS",
|
|
1842
|
+
"evidence": None,
|
|
1843
|
+
"checklist_item": _checklist_item(
|
|
1844
|
+
reason_code="PUBMED_INDEXED_BUT_COORDINATE_MISS",
|
|
1845
|
+
verdict_contribution="unresolvable",
|
|
1846
|
+
entry=entry,
|
|
1847
|
+
attempts=attempts,
|
|
1848
|
+
human_action=(
|
|
1849
|
+
"This journal IS indexed in PubMed, but no record exists at "
|
|
1850
|
+
f"{miss_description}. PubMed indexes Chinese journals "
|
|
1851
|
+
"selectively, so this may simply be an unindexed issue — "
|
|
1852
|
+
"check the journal's own site. Pending human check."
|
|
1853
|
+
),
|
|
1854
|
+
verification_urls=[
|
|
1855
|
+
"https://pubmed.ncbi.nlm.nih.gov/?term="
|
|
1856
|
+
+ _quote_url_component(verification_term)
|
|
1857
|
+
],
|
|
1858
|
+
),
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
def _pubmed_candidate_unverified(
|
|
1862
|
+
self,
|
|
1863
|
+
entry: dict[str, Any],
|
|
1864
|
+
attempts: list[dict[str, Any]],
|
|
1865
|
+
record: dict[str, Any],
|
|
1866
|
+
detail: str,
|
|
1867
|
+
) -> dict[str, Any]:
|
|
1868
|
+
"""A coordinate candidate is never negative evidence about a citation."""
|
|
1869
|
+
verification_urls = [
|
|
1870
|
+
f"https://pubmed.ncbi.nlm.nih.gov/{record['pmid']}/"
|
|
1871
|
+
]
|
|
1872
|
+
if isinstance(record.get("doi"), str) and record["doi"]:
|
|
1873
|
+
verification_urls.append(_DOI_RESOLVE_BASE + record["doi"])
|
|
1874
|
+
return {
|
|
1875
|
+
"status": STATUS_UNMATCHED,
|
|
1876
|
+
"queried_by": "title",
|
|
1877
|
+
"reason_code": "PUBMED_COORDINATE_CANDIDATE_UNVERIFIED",
|
|
1878
|
+
"evidence": record,
|
|
1879
|
+
"checklist_item": _checklist_item(
|
|
1880
|
+
reason_code="PUBMED_COORDINATE_CANDIDATE_UNVERIFIED",
|
|
1881
|
+
verdict_contribution="unresolvable",
|
|
1882
|
+
entry=entry,
|
|
1883
|
+
attempts=attempts,
|
|
1884
|
+
human_action=(
|
|
1885
|
+
f"{detail} The coordinate hit remains a candidate only; "
|
|
1886
|
+
"verify the original Chinese title manually. Pending human "
|
|
1887
|
+
"check — this is never a false verdict."
|
|
1888
|
+
),
|
|
1889
|
+
verification_urls=verification_urls,
|
|
1890
|
+
),
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
def _skipped_with_checklist(
|
|
1894
|
+
self,
|
|
1895
|
+
entry: dict[str, Any],
|
|
1896
|
+
attempts: list[dict[str, Any]],
|
|
1897
|
+
reason_code: str,
|
|
1898
|
+
*,
|
|
1899
|
+
human_action: str,
|
|
1900
|
+
) -> dict[str, Any]:
|
|
1901
|
+
"""`skipped` + a checklist row at the reason code's priority.
|
|
1902
|
+
|
|
1903
|
+
`skipped` is not silence: the row is the whole point of this resolver —
|
|
1904
|
+
it turns "nobody ever actually checked this reference" from an invisible
|
|
1905
|
+
default into a visible item somebody has to sign off on.
|
|
1906
|
+
"""
|
|
1907
|
+
return {
|
|
1908
|
+
"status": STATUS_SKIPPED,
|
|
1909
|
+
"queried_by": None,
|
|
1910
|
+
"reason_code": reason_code,
|
|
1911
|
+
"evidence": None,
|
|
1912
|
+
"checklist_item": _checklist_item(
|
|
1913
|
+
reason_code=reason_code,
|
|
1914
|
+
verdict_contribution="unresolvable",
|
|
1915
|
+
entry=entry,
|
|
1916
|
+
attempts=attempts,
|
|
1917
|
+
human_action=human_action,
|
|
1918
|
+
),
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
|
|
1922
|
+
def seed_journal_map() -> dict[str, dict[str, Any]]:
|
|
1923
|
+
"""The verified seed 中文刊名 -> ISSN/NLM-TA rows, keyed by normalized name.
|
|
1924
|
+
|
|
1925
|
+
Every row was confirmed live against NCBI on 2026-07-27 (see
|
|
1926
|
+
`_SEED_JOURNAL_ROWS`). Extend it by passing `journal_map=` to the client
|
|
1927
|
+
rather than by editing this function in a fork, and only from publicly
|
|
1928
|
+
redistributable sources.
|
|
1929
|
+
"""
|
|
1930
|
+
return {
|
|
1931
|
+
normalize_cn_title(name): {
|
|
1932
|
+
"issn": issn,
|
|
1933
|
+
"nlm_ta": nlm_ta,
|
|
1934
|
+
"nlm_id": str(nlm_id),
|
|
1935
|
+
"display_name": name,
|
|
1936
|
+
}
|
|
1937
|
+
for name, issn, nlm_ta, nlm_id in _SEED_JOURNAL_ROWS
|
|
1938
|
+
}
|