psyclaw 0.27.22 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1048) hide show
  1. package/README.md +11 -5
  2. package/dist/apps/panel/index.html +2 -2
  3. package/dist/src/adapters/pi/extension.js +225 -92
  4. package/dist/src/adapters/pi/extension.js.map +1 -1
  5. package/dist/src/agents/import.js +44 -32
  6. package/dist/src/agents/import.js.map +1 -1
  7. package/dist/src/analysis/hooks.d.ts +1 -0
  8. package/dist/src/analysis/hooks.js +20 -1
  9. package/dist/src/analysis/hooks.js.map +1 -1
  10. package/dist/src/ars/bridge.d.ts +20 -0
  11. package/dist/src/ars/bridge.js +132 -0
  12. package/dist/src/ars/bridge.js.map +1 -0
  13. package/dist/src/ars/contracts.d.ts +55 -0
  14. package/dist/src/ars/contracts.js +2 -0
  15. package/dist/src/ars/contracts.js.map +1 -0
  16. package/dist/src/ars/panel-plan.d.ts +4 -0
  17. package/dist/src/ars/panel-plan.js +11 -0
  18. package/dist/src/ars/panel-plan.js.map +1 -0
  19. package/dist/src/ars/pi-panel-executor.d.ts +30 -0
  20. package/dist/src/ars/pi-panel-executor.js +140 -0
  21. package/dist/src/ars/pi-panel-executor.js.map +1 -0
  22. package/dist/src/ars/profile.d.ts +36 -0
  23. package/dist/src/ars/profile.js +119 -0
  24. package/dist/src/ars/profile.js.map +1 -0
  25. package/dist/src/ars/re-review.d.ts +22 -0
  26. package/dist/src/ars/re-review.js +172 -0
  27. package/dist/src/ars/re-review.js.map +1 -0
  28. package/dist/src/branding.d.ts +2 -4
  29. package/dist/src/branding.js +3 -5
  30. package/dist/src/branding.js.map +1 -1
  31. package/dist/src/bundled-tools.d.ts +3 -0
  32. package/dist/src/bundled-tools.js +21 -0
  33. package/dist/src/bundled-tools.js.map +1 -0
  34. package/dist/src/chat.js +6 -5
  35. package/dist/src/chat.js.map +1 -1
  36. package/dist/src/creation/contracts.d.ts +38 -0
  37. package/dist/src/creation/contracts.js +2 -0
  38. package/dist/src/creation/contracts.js.map +1 -0
  39. package/dist/src/creation/service.d.ts +7 -0
  40. package/dist/src/creation/service.js +196 -0
  41. package/dist/src/creation/service.js.map +1 -0
  42. package/dist/src/index.d.ts +10 -0
  43. package/dist/src/index.js +10 -0
  44. package/dist/src/index.js.map +1 -1
  45. package/dist/src/install/installer.js +32 -10
  46. package/dist/src/install/installer.js.map +1 -1
  47. package/dist/src/orchestration/personas.d.ts +15 -0
  48. package/dist/src/orchestration/personas.js +51 -0
  49. package/dist/src/orchestration/personas.js.map +1 -0
  50. package/dist/src/orchestration/pi-executor.d.ts +1 -0
  51. package/dist/src/orchestration/pi-executor.js +1 -1
  52. package/dist/src/orchestration/pi-executor.js.map +1 -1
  53. package/dist/src/panel/extension.js +29 -8
  54. package/dist/src/panel/extension.js.map +1 -1
  55. package/dist/src/panel/server.js +14 -5
  56. package/dist/src/panel/server.js.map +1 -1
  57. package/dist/src/project/paths.d.ts +3 -0
  58. package/dist/src/project/paths.js +7 -0
  59. package/dist/src/project/paths.js.map +1 -1
  60. package/dist/src/rules/user-rules.d.ts +8 -0
  61. package/dist/src/rules/user-rules.js +36 -0
  62. package/dist/src/rules/user-rules.js.map +1 -0
  63. package/dist/src/skills/contracts.d.ts +4 -4
  64. package/dist/src/skills/recommended.js +1 -1
  65. package/dist/src/skills/registry.js +49 -28
  66. package/dist/src/skills/registry.js.map +1 -1
  67. package/dist/src/style/cli-ui.js +0 -1
  68. package/dist/src/style/cli-ui.js.map +1 -1
  69. package/package.json +14 -3
  70. package/scripts/rebrand-pi.mjs +6 -0
  71. package/skills/recommended/catalog.json +2 -11
  72. package/vendor/ars/.claude/CLAUDE.md +371 -0
  73. package/vendor/ars/.command-invariants.toml +24 -0
  74. package/vendor/ars/CITATION.cff +35 -0
  75. package/vendor/ars/LICENSE +417 -0
  76. package/vendor/ars/MODE_REGISTRY.md +76 -0
  77. package/vendor/ars/NOTICE.md +26 -0
  78. package/vendor/ars/POSITIONING.md +99 -0
  79. package/vendor/ars/PSYCLAW_SOURCE.json +10 -0
  80. package/vendor/ars/README.md +751 -0
  81. package/vendor/ars/SECURITY.md +52 -0
  82. package/vendor/ars/THIRD_PARTY.md +70 -0
  83. package/vendor/ars/academic-paper/SKILL.md +542 -0
  84. package/vendor/ars/academic-paper/agents/abstract_bilingual_agent.md +171 -0
  85. package/vendor/ars/academic-paper/agents/argument_builder_agent.md +276 -0
  86. package/vendor/ars/academic-paper/agents/citation_compliance_agent.md +422 -0
  87. package/vendor/ars/academic-paper/agents/draft_writer_agent.md +656 -0
  88. package/vendor/ars/academic-paper/agents/formatter_agent.md +999 -0
  89. package/vendor/ars/academic-paper/agents/intake_agent.md +393 -0
  90. package/vendor/ars/academic-paper/agents/literature_strategist_agent.md +626 -0
  91. package/vendor/ars/academic-paper/agents/peer_reviewer_agent.md +516 -0
  92. package/vendor/ars/academic-paper/agents/revision_coach_agent.md +334 -0
  93. package/vendor/ars/academic-paper/agents/socratic_mentor_agent.md +527 -0
  94. package/vendor/ars/academic-paper/agents/structure_architect_agent.md +401 -0
  95. package/vendor/ars/academic-paper/agents/visualization_agent.md +441 -0
  96. package/vendor/ars/academic-paper/examples/chinese_paper_example.md +278 -0
  97. package/vendor/ars/academic-paper/examples/clinical_citation_verification_checklist.md +95 -0
  98. package/vendor/ars/academic-paper/examples/clinical_epistemic_status_example.md +100 -0
  99. package/vendor/ars/academic-paper/examples/commitment_ledger_example.md +147 -0
  100. package/vendor/ars/academic-paper/examples/imrad_hei_example.md +234 -0
  101. package/vendor/ars/academic-paper/examples/literature_review_example.md +260 -0
  102. package/vendor/ars/academic-paper/examples/plan_mode_guided_writing.md +600 -0
  103. package/vendor/ars/academic-paper/examples/revision_mode_example.md +344 -0
  104. package/vendor/ars/academic-paper/examples/revision_recovery_example.md +506 -0
  105. package/vendor/ars/academic-paper/examples/version_family_reconciliation_example.md +89 -0
  106. package/vendor/ars/academic-paper/references/abstract_writing_guide.md +169 -0
  107. package/vendor/ars/academic-paper/references/academic_writing_style.md +188 -0
  108. package/vendor/ars/academic-paper/references/anti_leakage_protocol.md +83 -0
  109. package/vendor/ars/academic-paper/references/apa7_chinese_citation_guide.md +364 -0
  110. package/vendor/ars/academic-paper/references/apa7_extended_guide.md +198 -0
  111. package/vendor/ars/academic-paper/references/changelog.md +11 -0
  112. package/vendor/ars/academic-paper/references/citation_format_switcher.md +228 -0
  113. package/vendor/ars/academic-paper/references/committee_correspondence_protocol.md +158 -0
  114. package/vendor/ars/academic-paper/references/credit_authorship_guide.md +308 -0
  115. package/vendor/ars/academic-paper/references/disclosure_mode_protocol.md +478 -0
  116. package/vendor/ars/academic-paper/references/domain_evidence_profiles.md +38 -0
  117. package/vendor/ars/academic-paper/references/failure_paths.md +349 -0
  118. package/vendor/ars/academic-paper/references/funding_statement_guide.md +319 -0
  119. package/vendor/ars/academic-paper/references/hei_domain_glossary.md +169 -0
  120. package/vendor/ars/academic-paper/references/intro_title_rhetoric_guide.md +114 -0
  121. package/vendor/ars/academic-paper/references/journal_submission_guide.md +249 -0
  122. package/vendor/ars/academic-paper/references/latex_template_reference.md +378 -0
  123. package/vendor/ars/academic-paper/references/mode_selection_guide.md +378 -0
  124. package/vendor/ars/academic-paper/references/paper_structure_patterns.md +330 -0
  125. package/vendor/ars/academic-paper/references/plan_mode_protocol.md +112 -0
  126. package/vendor/ars/academic-paper/references/policy_anchor_disclosure_protocol.md +200 -0
  127. package/vendor/ars/academic-paper/references/policy_anchor_table.md +157 -0
  128. package/vendor/ars/academic-paper/references/revision_patch_protocol.md +173 -0
  129. package/vendor/ars/academic-paper/references/statistical_visualization_standards.md +750 -0
  130. package/vendor/ars/academic-paper/references/venue_disclosure_policies.md +259 -0
  131. package/vendor/ars/academic-paper/references/vlm_figure_verification.md +126 -0
  132. package/vendor/ars/academic-paper/references/workflow_phase_details.md +135 -0
  133. package/vendor/ars/academic-paper/references/writing_judgment_framework.md +59 -0
  134. package/vendor/ars/academic-paper/references/writing_quality_check.md +173 -0
  135. package/vendor/ars/academic-paper/templates/bilingual_abstract_template.md +78 -0
  136. package/vendor/ars/academic-paper/templates/case_study_template.md +129 -0
  137. package/vendor/ars/academic-paper/templates/conference_paper_template.md +108 -0
  138. package/vendor/ars/academic-paper/templates/credit_statement_template.md +132 -0
  139. package/vendor/ars/academic-paper/templates/funding_statement_template.md +290 -0
  140. package/vendor/ars/academic-paper/templates/imrad_template.md +183 -0
  141. package/vendor/ars/academic-paper/templates/latex_article_template.tex +199 -0
  142. package/vendor/ars/academic-paper/templates/literature_review_template.md +135 -0
  143. package/vendor/ars/academic-paper/templates/policy_brief_template.md +139 -0
  144. package/vendor/ars/academic-paper/templates/revision_tracking_template.md +199 -0
  145. package/vendor/ars/academic-paper/templates/theoretical_paper_template.md +119 -0
  146. package/vendor/ars/academic-paper-reviewer/SKILL.md +491 -0
  147. package/vendor/ars/academic-paper-reviewer/agents/devils_advocate_reviewer_agent.md +443 -0
  148. package/vendor/ars/academic-paper-reviewer/agents/domain_reviewer_agent.md +412 -0
  149. package/vendor/ars/academic-paper-reviewer/agents/editorial_synthesizer_agent.md +478 -0
  150. package/vendor/ars/academic-paper-reviewer/agents/eic_agent.md +339 -0
  151. package/vendor/ars/academic-paper-reviewer/agents/field_analyst_agent.md +221 -0
  152. package/vendor/ars/academic-paper-reviewer/agents/methodology_reviewer_agent.md +449 -0
  153. package/vendor/ars/academic-paper-reviewer/agents/perspective_reviewer_agent.md +427 -0
  154. package/vendor/ars/academic-paper-reviewer/examples/hei_paper_review_example.md +391 -0
  155. package/vendor/ars/academic-paper-reviewer/examples/interdisciplinary_review_example.md +299 -0
  156. package/vendor/ars/academic-paper-reviewer/examples/subclaim_decomposition_example.md +80 -0
  157. package/vendor/ars/academic-paper-reviewer/references/calibration_mode_protocol.md +256 -0
  158. package/vendor/ars/academic-paper-reviewer/references/changelog.md +10 -0
  159. package/vendor/ars/academic-paper-reviewer/references/editorial_decision_standards.md +236 -0
  160. package/vendor/ars/academic-paper-reviewer/references/guided_mode_protocol.md +34 -0
  161. package/vendor/ars/academic-paper-reviewer/references/integration_guide.md +15 -0
  162. package/vendor/ars/academic-paper-reviewer/references/quality_rubrics.md +84 -0
  163. package/vendor/ars/academic-paper-reviewer/references/re_review_mode_protocol.md +340 -0
  164. package/vendor/ars/academic-paper-reviewer/references/review_criteria_framework.md +98 -0
  165. package/vendor/ars/academic-paper-reviewer/references/review_panel_provenance_protocol.md +197 -0
  166. package/vendor/ars/academic-paper-reviewer/references/review_quality_thinking.md +58 -0
  167. package/vendor/ars/academic-paper-reviewer/references/reviewer_sprint_prompt_source.md +324 -0
  168. package/vendor/ars/academic-paper-reviewer/references/sprint_contract_protocol.md +296 -0
  169. package/vendor/ars/academic-paper-reviewer/references/statistical_reporting_standards.md +505 -0
  170. package/vendor/ars/academic-paper-reviewer/references/top_journals_by_field.md +206 -0
  171. package/vendor/ars/academic-paper-reviewer/templates/editorial_decision_template.md +235 -0
  172. package/vendor/ars/academic-paper-reviewer/templates/peer_review_report_template.md +305 -0
  173. package/vendor/ars/academic-paper-reviewer/templates/revision_response_template.md +248 -0
  174. package/vendor/ars/academic-pipeline/SKILL.md +736 -0
  175. package/vendor/ars/academic-pipeline/agents/claim_ref_alignment_audit_agent.md +382 -0
  176. package/vendor/ars/academic-pipeline/agents/collaboration_depth_agent.md +164 -0
  177. package/vendor/ars/academic-pipeline/agents/integrity_verification_agent.md +870 -0
  178. package/vendor/ars/academic-pipeline/agents/pipeline_orchestrator_agent.md +1379 -0
  179. package/vendor/ars/academic-pipeline/agents/state_tracker_agent.md +622 -0
  180. package/vendor/ars/academic-pipeline/examples/full_pipeline_example.md +482 -0
  181. package/vendor/ars/academic-pipeline/examples/integrity_failure_recovery.md +389 -0
  182. package/vendor/ars/academic-pipeline/examples/mid_entry_example.md +414 -0
  183. package/vendor/ars/academic-pipeline/references/adapters/.gitkeep +0 -0
  184. package/vendor/ars/academic-pipeline/references/adapters/overview.md +153 -0
  185. package/vendor/ars/academic-pipeline/references/ai_research_failure_modes.md +185 -0
  186. package/vendor/ars/academic-pipeline/references/changelog.md +14 -0
  187. package/vendor/ars/academic-pipeline/references/claim_audit_calibration_protocol.md +175 -0
  188. package/vendor/ars/academic-pipeline/references/claim_verification_protocol.md +282 -0
  189. package/vendor/ars/academic-pipeline/references/external_review_protocol.md +131 -0
  190. package/vendor/ars/academic-pipeline/references/integrity_review_protocol.md +110 -0
  191. package/vendor/ars/academic-pipeline/references/literature_corpus_consumers.md +193 -0
  192. package/vendor/ars/academic-pipeline/references/mode_advisor.md +135 -0
  193. package/vendor/ars/academic-pipeline/references/passport_as_reset_boundary.md +132 -0
  194. package/vendor/ars/academic-pipeline/references/pipeline_state_machine.md +405 -0
  195. package/vendor/ars/academic-pipeline/references/plagiarism_detection_protocol.md +239 -0
  196. package/vendor/ars/academic-pipeline/references/process_summary_protocol.md +209 -0
  197. package/vendor/ars/academic-pipeline/references/progress_dashboard_template.md +38 -0
  198. package/vendor/ars/academic-pipeline/references/reinforcement_content.md +15 -0
  199. package/vendor/ars/academic-pipeline/references/reproducibility_audit.md +55 -0
  200. package/vendor/ars/academic-pipeline/references/score_trajectory_protocol.md +78 -0
  201. package/vendor/ars/academic-pipeline/references/team_collaboration_protocol.md +261 -0
  202. package/vendor/ars/academic-pipeline/references/two_stage_review_protocol.md +27 -0
  203. package/vendor/ars/academic-pipeline/templates/pipeline_status_template.md +146 -0
  204. package/vendor/ars/agents/report_compiler_agent.md +341 -0
  205. package/vendor/ars/agents/research_architect_agent.md +298 -0
  206. package/vendor/ars/agents/synthesis_agent.md +356 -0
  207. package/vendor/ars/commands/ars-3w.md +10 -0
  208. package/vendor/ars/commands/ars-abstract.md +10 -0
  209. package/vendor/ars/commands/ars-cache-invalidate.md +20 -0
  210. package/vendor/ars/commands/ars-citation-check.md +10 -0
  211. package/vendor/ars/commands/ars-disclosure.md +10 -0
  212. package/vendor/ars/commands/ars-format-convert.md +10 -0
  213. package/vendor/ars/commands/ars-full.md +9 -0
  214. package/vendor/ars/commands/ars-lit-review.md +12 -0
  215. package/vendor/ars/commands/ars-mark-read.md +18 -0
  216. package/vendor/ars/commands/ars-outline.md +10 -0
  217. package/vendor/ars/commands/ars-plan.md +10 -0
  218. package/vendor/ars/commands/ars-rebuttal-audit.md +12 -0
  219. package/vendor/ars/commands/ars-reviewer.md +9 -0
  220. package/vendor/ars/commands/ars-revision-coach.md +9 -0
  221. package/vendor/ars/commands/ars-revision.md +10 -0
  222. package/vendor/ars/commands/ars-unmark-read.md +16 -0
  223. package/vendor/ars/deep-research/SKILL.md +600 -0
  224. package/vendor/ars/deep-research/agents/bibliography_agent.md +473 -0
  225. package/vendor/ars/deep-research/agents/devils_advocate_agent.md +192 -0
  226. package/vendor/ars/deep-research/agents/editor_in_chief_agent.md +167 -0
  227. package/vendor/ars/deep-research/agents/ethics_review_agent.md +267 -0
  228. package/vendor/ars/deep-research/agents/meta_analysis_agent.md +325 -0
  229. package/vendor/ars/deep-research/agents/monitoring_agent.md +209 -0
  230. package/vendor/ars/deep-research/agents/report_compiler_agent.md +341 -0
  231. package/vendor/ars/deep-research/agents/research_architect_agent.md +298 -0
  232. package/vendor/ars/deep-research/agents/research_question_agent.md +216 -0
  233. package/vendor/ars/deep-research/agents/risk_of_bias_agent.md +231 -0
  234. package/vendor/ars/deep-research/agents/socratic_mentor_agent.md +764 -0
  235. package/vendor/ars/deep-research/agents/source_verification_agent.md +219 -0
  236. package/vendor/ars/deep-research/agents/synthesis_agent.md +356 -0
  237. package/vendor/ars/deep-research/agents/timeline_extraction_agent.md +99 -0
  238. package/vendor/ars/deep-research/examples/exploratory_research.md +157 -0
  239. package/vendor/ars/deep-research/examples/fact_check_mode.md +173 -0
  240. package/vendor/ars/deep-research/examples/handoff_to_paper.md +318 -0
  241. package/vendor/ars/deep-research/examples/idea_diversity_coverage_gap_advisory.md +64 -0
  242. package/vendor/ars/deep-research/examples/policy_analysis.md +161 -0
  243. package/vendor/ars/deep-research/examples/review_mode.md +253 -0
  244. package/vendor/ars/deep-research/examples/socratic_guided_research.md +331 -0
  245. package/vendor/ars/deep-research/examples/systematic_review.md +133 -0
  246. package/vendor/ars/deep-research/references/apa7_style_guide.md +162 -0
  247. package/vendor/ars/deep-research/references/argumentation_reasoning_framework.md +68 -0
  248. package/vendor/ars/deep-research/references/arxiv_api_protocol.md +76 -0
  249. package/vendor/ars/deep-research/references/changelog.md +22 -0
  250. package/vendor/ars/deep-research/references/chinese_literature_api_protocol.md +317 -0
  251. package/vendor/ars/deep-research/references/cross_agent_quality_definitions.md +14 -0
  252. package/vendor/ars/deep-research/references/crossref_api_protocol.md +84 -0
  253. package/vendor/ars/deep-research/references/equator_reporting_guidelines.md +482 -0
  254. package/vendor/ars/deep-research/references/ethics_checklist.md +282 -0
  255. package/vendor/ars/deep-research/references/failure_paths.md +355 -0
  256. package/vendor/ars/deep-research/references/interdisciplinary_bridges.md +292 -0
  257. package/vendor/ars/deep-research/references/irb_decision_tree.md +315 -0
  258. package/vendor/ars/deep-research/references/literature_monitoring_strategies.md +263 -0
  259. package/vendor/ars/deep-research/references/logical_fallacies.md +192 -0
  260. package/vendor/ars/deep-research/references/methodology_patterns.md +462 -0
  261. package/vendor/ars/deep-research/references/mode_selection_guide.md +331 -0
  262. package/vendor/ars/deep-research/references/openalex_api_protocol.md +82 -0
  263. package/vendor/ars/deep-research/references/preregistration_guide.md +324 -0
  264. package/vendor/ars/deep-research/references/semantic_scholar_api_protocol.md +107 -0
  265. package/vendor/ars/deep-research/references/socratic_mode_protocol.md +99 -0
  266. package/vendor/ars/deep-research/references/socratic_questioning_framework.md +232 -0
  267. package/vendor/ars/deep-research/references/source_quality_hierarchy.md +188 -0
  268. package/vendor/ars/deep-research/references/systematic_review_protocol.md +95 -0
  269. package/vendor/ars/deep-research/references/systematic_review_toolkit.md +353 -0
  270. package/vendor/ars/deep-research/templates/evidence_assessment_template.md +127 -0
  271. package/vendor/ars/deep-research/templates/literature_matrix_template.md +85 -0
  272. package/vendor/ars/deep-research/templates/preregistration_template.md +318 -0
  273. package/vendor/ars/deep-research/templates/prisma_protocol_template.md +248 -0
  274. package/vendor/ars/deep-research/templates/prisma_report_template.md +415 -0
  275. package/vendor/ars/deep-research/templates/research_brief_template.md +93 -0
  276. package/vendor/ars/package.json +24 -0
  277. package/vendor/ars/pi/README.md +161 -0
  278. package/vendor/ars/pi/package.json +26 -0
  279. package/vendor/ars/pi/wrapper.js +193 -0
  280. package/vendor/ars/pi/wrapper.test.mjs +201 -0
  281. package/vendor/ars/pyproject.toml +2 -0
  282. package/vendor/ars/requirements-pdf-content-classifier.txt +5 -0
  283. package/vendor/ars/scripts/_block_parser.py +396 -0
  284. package/vendor/ars/scripts/_ci_pytest_manifest.toml +661 -0
  285. package/vendor/ars/scripts/_claim_audit_constants.py +268 -0
  286. package/vendor/ars/scripts/_e4_evidence.py +110 -0
  287. package/vendor/ars/scripts/_eval_threshold_gate.py +73 -0
  288. package/vendor/ars/scripts/_markdown_lint_util.py +224 -0
  289. package/vendor/ars/scripts/_next_verified_at_ms.py +176 -0
  290. package/vendor/ars/scripts/_passport_yaml.py +53 -0
  291. package/vendor/ars/scripts/_skill_lint.py +254 -0
  292. package/vendor/ars/scripts/_text_similarity.py +141 -0
  293. package/vendor/ars/scripts/adapters/README.md +89 -0
  294. package/vendor/ars/scripts/adapters/_common.py +209 -0
  295. package/vendor/ars/scripts/adapters/examples/folder_scan/expected_passport.yaml +25 -0
  296. package/vendor/ars/scripts/adapters/examples/folder_scan/expected_rejection_log.yaml +18 -0
  297. package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/Chen2024_AIAssessment.pdf +0 -0
  298. package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/Wang_2023_formative_feedback.pdf +0 -0
  299. package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/paper1.pdf +0 -0
  300. package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture//344/270/255/346/226/207/346/252/224/345/220/215_2024.pdf +0 -0
  301. package/vendor/ars/scripts/adapters/examples/obsidian/expected_passport.yaml +40 -0
  302. package/vendor/ars/scripts/adapters/examples/obsidian/expected_rejection_log.yaml +13 -0
  303. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/.gitkeep +0 -0
  304. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/.obsidian/app.json +1 -0
  305. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/_templates/tmpl.md +7 -0
  306. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/chen2024ai.md +13 -0
  307. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/invalid.md +1 -0
  308. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/wang2023formative.md +10 -0
  309. package/vendor/ars/scripts/adapters/examples/zotero/expected_passport.yaml +34 -0
  310. package/vendor/ars/scripts/adapters/examples/zotero/expected_rejection_log.yaml +34 -0
  311. package/vendor/ars/scripts/adapters/examples/zotero/input_fixture/.gitkeep +0 -0
  312. package/vendor/ars/scripts/adapters/examples/zotero/input_fixture/export.json +43 -0
  313. package/vendor/ars/scripts/adapters/folder_scan.py +214 -0
  314. package/vendor/ars/scripts/adapters/obsidian.py +336 -0
  315. package/vendor/ars/scripts/adapters/tests/.gitkeep +0 -0
  316. package/vendor/ars/scripts/adapters/tests/conftest.py +77 -0
  317. package/vendor/ars/scripts/adapters/tests/test_check_corpus_consumer_protocol.py +632 -0
  318. package/vendor/ars/scripts/adapters/tests/test_check_literature_corpus_schema.py +631 -0
  319. package/vendor/ars/scripts/adapters/tests/test_common.py +365 -0
  320. package/vendor/ars/scripts/adapters/tests/test_conftest.py +96 -0
  321. package/vendor/ars/scripts/adapters/tests/test_folder_scan.py +260 -0
  322. package/vendor/ars/scripts/adapters/tests/test_literature_corpus_entry_schema.py +745 -0
  323. package/vendor/ars/scripts/adapters/tests/test_obsidian.py +357 -0
  324. package/vendor/ars/scripts/adapters/tests/test_rejection_log_schema.py +271 -0
  325. package/vendor/ars/scripts/adapters/tests/test_sync_adapter_docs.py +88 -0
  326. package/vendor/ars/scripts/adapters/tests/test_zotero.py +454 -0
  327. package/vendor/ars/scripts/adapters/zotero.py +318 -0
  328. package/vendor/ars/scripts/adjudication_activity.py +1592 -0
  329. package/vendor/ars/scripts/announce-ars-loaded.sh +144 -0
  330. package/vendor/ars/scripts/ars_anchorize_draft.py +170 -0
  331. package/vendor/ars/scripts/ars_apply_revision_patch.py +912 -0
  332. package/vendor/ars/scripts/ars_cache_invalidate.py +40 -0
  333. package/vendor/ars/scripts/ars_mark_read.py +521 -0
  334. package/vendor/ars/scripts/ars_phase_scope_manifest.json +33 -0
  335. package/vendor/ars/scripts/ars_update_check.sh +215 -0
  336. package/vendor/ars/scripts/ars_write_scope_guard.py +506 -0
  337. package/vendor/ars/scripts/arxiv_client.py +222 -0
  338. package/vendor/ars/scripts/audit_snapshot.py +572 -0
  339. package/vendor/ars/scripts/bibliographic_integrity_signals.py +800 -0
  340. package/vendor/ars/scripts/bootstrap_timeline_yaml.py +146 -0
  341. package/vendor/ars/scripts/build_claim_standing_candidate_ledger.py +1238 -0
  342. package/vendor/ars/scripts/build_claim_standing_query_plan.py +643 -0
  343. package/vendor/ars/scripts/build_content_coverage_advisory.py +1205 -0
  344. package/vendor/ars/scripts/build_cross_document_consistency_advisory.py +2332 -0
  345. package/vendor/ars/scripts/build_review_pathway_rule_trace.py +830 -0
  346. package/vendor/ars/scripts/build_submission_packet_manifest.py +2310 -0
  347. package/vendor/ars/scripts/check_215_field_norm.py +173 -0
  348. package/vendor/ars/scripts/check_216_surface_form.py +250 -0
  349. package/vendor/ars/scripts/check_268_nested_commitment_ledger.py +180 -0
  350. package/vendor/ars/scripts/check_390_revision_patch_discipline.py +296 -0
  351. package/vendor/ars/scripts/check_392_citation_verification_intake.py +126 -0
  352. package/vendor/ars/scripts/check_394_submission_policy.py +178 -0
  353. package/vendor/ars/scripts/check_439_format_profile.py +307 -0
  354. package/vendor/ars/scripts/check_619_disclosure_closeout.py +237 -0
  355. package/vendor/ars/scripts/check_630_codex_subscription_transport.py +456 -0
  356. package/vendor/ars/scripts/check_669_review_pathway_rule_trace.py +662 -0
  357. package/vendor/ars/scripts/check_670_revision_roadmap_integration.py +518 -0
  358. package/vendor/ars/scripts/check_673_adjudication_activity.py +684 -0
  359. package/vendor/ars/scripts/check_684_review_criteria_binding.py +557 -0
  360. package/vendor/ars/scripts/check_agents_mirror_sync.py +115 -0
  361. package/vendor/ars/scripts/check_audit_artifact_consistency.py +2313 -0
  362. package/vendor/ars/scripts/check_benchmark_report.py +79 -0
  363. package/vendor/ars/scripts/check_bibliographic_integrity_signals.py +831 -0
  364. package/vendor/ars/scripts/check_calibration_tiers.py +235 -0
  365. package/vendor/ars/scripts/check_changelog_covers_merges.py +289 -0
  366. package/vendor/ars/scripts/check_ci_pytest_manifest.py +204 -0
  367. package/vendor/ars/scripts/check_claim_audit_consistency.py +1664 -0
  368. package/vendor/ars/scripts/check_claim_standing_candidate_ledger_integration.py +500 -0
  369. package/vendor/ars/scripts/check_claim_standing_freshness.py +253 -0
  370. package/vendor/ars/scripts/check_claim_standing_transmissions.py +449 -0
  371. package/vendor/ars/scripts/check_collaboration_depth_rubric.py +180 -0
  372. package/vendor/ars/scripts/check_command_frontmatter_name.py +116 -0
  373. package/vendor/ars/scripts/check_committee_correspondence.py +333 -0
  374. package/vendor/ars/scripts/check_compliance_report.py +108 -0
  375. package/vendor/ars/scripts/check_content_coverage_advisory_integration.py +796 -0
  376. package/vendor/ars/scripts/check_control_availability.py +172 -0
  377. package/vendor/ars/scripts/check_corpus_consumer_protocol.py +404 -0
  378. package/vendor/ars/scripts/check_cross_document_consistency_advisory_integration.py +1191 -0
  379. package/vendor/ars/scripts/check_cross_model_handoff_contract.py +234 -0
  380. package/vendor/ars/scripts/check_cross_model_verification_sync.py +261 -0
  381. package/vendor/ars/scripts/check_data_access_level.py +131 -0
  382. package/vendor/ars/scripts/check_data_flows.py +252 -0
  383. package/vendor/ars/scripts/check_decision_contract.py +464 -0
  384. package/vendor/ars/scripts/check_degradation_registry.py +326 -0
  385. package/vendor/ars/scripts/check_distribution_surface_claims.py +226 -0
  386. package/vendor/ars/scripts/check_domain_evidence_profile.py +538 -0
  387. package/vendor/ars/scripts/check_e4_promotion.py +195 -0
  388. package/vendor/ars/scripts/check_evals_gold_set.py +279 -0
  389. package/vendor/ars/scripts/check_evidence_row_integration.py +396 -0
  390. package/vendor/ars/scripts/check_experiment_provenance.py +117 -0
  391. package/vendor/ars/scripts/check_field_norm_severity.py +144 -0
  392. package/vendor/ars/scripts/check_firm_rules_sync.py +375 -0
  393. package/vendor/ars/scripts/check_heldout_measurement_report.py +1179 -0
  394. package/vendor/ars/scripts/check_human_subjects_output_contract.py +139 -0
  395. package/vendor/ars/scripts/check_human_subjects_reference_migration.py +844 -0
  396. package/vendor/ars/scripts/check_indirect_prompt_injection_no_call.py +328 -0
  397. package/vendor/ars/scripts/check_instruction_data_boundary.py +236 -0
  398. package/vendor/ars/scripts/check_judge_prompt_version.py +125 -0
  399. package/vendor/ars/scripts/check_literature_corpus_schema.py +402 -0
  400. package/vendor/ars/scripts/check_model_tiering.py +223 -0
  401. package/vendor/ars/scripts/check_panel_synthesis.py +1331 -0
  402. package/vendor/ars/scripts/check_passport_reset_contract.py +214 -0
  403. package/vendor/ars/scripts/check_pattern_eval_manifest.py +422 -0
  404. package/vendor/ars/scripts/check_persuasion_invariance_fixtures.py +637 -0
  405. package/vendor/ars/scripts/check_phase_conformance.py +2180 -0
  406. package/vendor/ars/scripts/check_pipeline_boundary_semantics.py +600 -0
  407. package/vendor/ars/scripts/check_pipeline_integrity.py +340 -0
  408. package/vendor/ars/scripts/check_policy_anchor_protocol.py +159 -0
  409. package/vendor/ars/scripts/check_policy_anchor_table.py +286 -0
  410. package/vendor/ars/scripts/check_preprint_venues_consistency.py +123 -0
  411. package/vendor/ars/scripts/check_prisma_trAIce_freshness.py +84 -0
  412. package/vendor/ars/scripts/check_promotion_bakeoff_preregistration.py +1303 -0
  413. package/vendor/ars/scripts/check_ranking_lift.py +323 -0
  414. package/vendor/ars/scripts/check_re_review_synthesis.py +2719 -0
  415. package/vendor/ars/scripts/check_receipt_enum_sync.py +203 -0
  416. package/vendor/ars/scripts/check_repro_lock.py +85 -0
  417. package/vendor/ars/scripts/check_review_pathway_output.py +276 -0
  418. package/vendor/ars/scripts/check_reviewer_data_fences.py +224 -0
  419. package/vendor/ars/scripts/check_reviewer_finding_contract.py +783 -0
  420. package/vendor/ars/scripts/check_reviewer_role_label.py +447 -0
  421. package/vendor/ars/scripts/check_reviewer_scoring_honesty.py +292 -0
  422. package/vendor/ars/scripts/check_reviewer_sprint_prompt_sync.py +409 -0
  423. package/vendor/ars/scripts/check_revision_claim_drift_suite_v2.py +1622 -0
  424. package/vendor/ars/scripts/check_revision_token_conservation.py +263 -0
  425. package/vendor/ars/scripts/check_risk_register.py +280 -0
  426. package/vendor/ars/scripts/check_role_scoped_contract.py +702 -0
  427. package/vendor/ars/scripts/check_rq_framing_patterns.py +184 -0
  428. package/vendor/ars/scripts/check_rubric_weight_consistency.py +32 -0
  429. package/vendor/ars/scripts/check_seeded_defect_fixtures.py +425 -0
  430. package/vendor/ars/scripts/check_setup_cross_model_parity.py +139 -0
  431. package/vendor/ars/scripts/check_spec_consistency.py +1249 -0
  432. package/vendor/ars/scripts/check_sprint_contract.py +371 -0
  433. package/vendor/ars/scripts/check_stage_capability_matrix.py +784 -0
  434. package/vendor/ars/scripts/check_submission_packet_manifest_integration.py +358 -0
  435. package/vendor/ars/scripts/check_surface_form_parity.py +445 -0
  436. package/vendor/ars/scripts/check_task_type.py +22 -0
  437. package/vendor/ars/scripts/check_tools_allowlist.py +538 -0
  438. package/vendor/ars/scripts/check_tortured_phrase_screening_integration.py +1974 -0
  439. package/vendor/ars/scripts/check_v3_10_134_write_scope.py +286 -0
  440. package/vendor/ars/scripts/check_v3_10_policy.py +656 -0
  441. package/vendor/ars/scripts/check_v3_6_6_ab_manifest.py +364 -0
  442. package/vendor/ars/scripts/check_v3_6_7_pattern_protection.py +1366 -0
  443. package/vendor/ars/scripts/check_v3_6_8_audit_scope_block.py +464 -0
  444. package/vendor/ars/scripts/check_v3_6_8_cite_provenance_pipeline.py +231 -0
  445. package/vendor/ars/scripts/check_v3_6_8_frontmatter_trust_schema.py +248 -0
  446. package/vendor/ars/scripts/check_v3_6_8_mark_read_commands.py +79 -0
  447. package/vendor/ars/scripts/check_v3_6_8_pattern_protection.py +941 -0
  448. package/vendor/ars/scripts/check_v3_7_3_three_layer_citation.py +318 -0
  449. package/vendor/ars/scripts/check_v3_8_annotation_literal_sync.py +228 -0
  450. package/vendor/ars/scripts/check_v3_9_0_triangulation.py +366 -0
  451. package/vendor/ars/scripts/check_v3_9_2_phase_boundary.py +270 -0
  452. package/vendor/ars/scripts/check_v3_9_4_temporal_verification.py +139 -0
  453. package/vendor/ars/scripts/check_venue_disclosure_policies.py +63 -0
  454. package/vendor/ars/scripts/check_version_consistency.py +826 -0
  455. package/vendor/ars/scripts/check_workflow_classification.py +223 -0
  456. package/vendor/ars/scripts/chinese_literature_client.py +1938 -0
  457. package/vendor/ars/scripts/citation_verification_summary.py +85 -0
  458. package/vendor/ars/scripts/claim_audit_calibration.py +517 -0
  459. package/vendor/ars/scripts/claim_audit_finalizer.py +456 -0
  460. package/vendor/ars/scripts/claim_audit_pipeline.py +1594 -0
  461. package/vendor/ars/scripts/claim_registry_coverage.py +493 -0
  462. package/vendor/ars/scripts/claim_standing_discovery.py +784 -0
  463. package/vendor/ars/scripts/claim_standing_stance_runner.py +758 -0
  464. package/vendor/ars/scripts/claim_standing_stance_scorer.py +239 -0
  465. package/vendor/ars/scripts/claim_strength_drift_disposition.py +666 -0
  466. package/vendor/ars/scripts/contamination_signals.py +689 -0
  467. package/vendor/ars/scripts/corpus_consumer_manifest.json +19 -0
  468. package/vendor/ars/scripts/cross_model_codex_transport.py +1374 -0
  469. package/vendor/ars/scripts/cross_model_codex_verify.sh +6 -0
  470. package/vendor/ars/scripts/cross_model_handoff.py +359 -0
  471. package/vendor/ars/scripts/cross_model_smoke_test.sh +183 -0
  472. package/vendor/ars/scripts/cross_model_smoke_test_codex.sh +35 -0
  473. package/vendor/ars/scripts/cross_model_verification/gemini_is_grounded.jq +45 -0
  474. package/vendor/ars/scripts/cross_model_verification/gemini_sources.jq +45 -0
  475. package/vendor/ars/scripts/cross_model_verification/normalize_compat_verdict.py +57 -0
  476. package/vendor/ars/scripts/cross_model_verification/openai_has_completed_web_search.jq +15 -0
  477. package/vendor/ars/scripts/cross_model_verification/openai_sources.jq +17 -0
  478. package/vendor/ars/scripts/cross_model_verification/openai_text.jq +11 -0
  479. package/vendor/ars/scripts/crossref_client.py +225 -0
  480. package/vendor/ars/scripts/dispatch_e4_panel.py +2731 -0
  481. package/vendor/ars/scripts/evidence_rows.py +2043 -0
  482. package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3.json +32 -0
  483. package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_input.json +27 -0
  484. package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_prime.json +32 -0
  485. package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_prime_input.json +27 -0
  486. package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_override.json +50 -0
  487. package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_override_action.json +14 -0
  488. package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_pass.json +44 -0
  489. package/vendor/ars/scripts/fixtures/adjudication_activity/explicit_user_request_log.json +26 -0
  490. package/vendor/ars/scripts/fixtures/adjudication_activity/mandatory_checkpoint_log.json +116 -0
  491. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/README.md +56 -0
  492. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.audit_artifact_entry.json +24 -0
  493. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
  494. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
  495. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.verdict.yaml +19 -0
  496. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a7_orphan_completion/2026-04-30T15-22-04Z-d8f3.jsonl +3 -0
  497. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.audit_artifact_entry.json +26 -0
  498. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
  499. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
  500. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.verdict.yaml +19 -0
  501. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.audit_artifact_entry.json +24 -0
  502. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
  503. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
  504. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.verdict.yaml +12 -0
  505. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/retraction.json +46 -0
  506. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/retraction_check_attestation.json +44 -0
  507. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase.json +44 -0
  508. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase_v1_2_abstract_missing.json +107 -0
  509. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase_v1_2_detected.json +124 -0
  510. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/expected_outcomes.json +77 -0
  511. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/manifest.yaml +37 -0
  512. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/001-valid-doi-test.json +20 -0
  513. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/002-valid-arxiv-test.json +19 -0
  514. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/003-fabricated-test.json +20 -0
  515. package/vendor/ars/scripts/fixtures/claim_audit_calibration/gold_set.json +344 -0
  516. package/vendor/ars/scripts/fixtures/claim_standing_candidate_ledger/query_plan.json +108 -0
  517. package/vendor/ars/scripts/fixtures/claim_standing_candidate_ledger/retrieval_input.json +179 -0
  518. package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/concern_tracker.json +129 -0
  519. package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/response_skeleton.md +19 -0
  520. package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/source_letter.txt +9 -0
  521. package/vendor/ars/scripts/fixtures/content_coverage_advisory/base_draft.json +48 -0
  522. package/vendor/ars/scripts/fixtures/content_coverage_advisory/base_inventory.json +38 -0
  523. package/vendor/ars/scripts/fixtures/content_coverage_advisory/packet/consent.txt +1 -0
  524. package/vendor/ars/scripts/fixtures/content_coverage_advisory/session_sources.json +3 -0
  525. package/vendor/ars/scripts/fixtures/cross_document_consistency/README.md +6 -0
  526. package/vendor/ars/scripts/fixtures/cross_document_consistency/accepted_draft.md +35 -0
  527. package/vendor/ars/scripts/fixtures/cross_document_consistency/cases.json +75 -0
  528. package/vendor/ars/scripts/fixtures/cross_document_consistency/preregistration.md +5 -0
  529. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/forbidden_event.jsonl +4 -0
  530. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/grounded_verified.jsonl +3 -0
  531. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/malformed.jsonl +3 -0
  532. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/missing_search.jsonl +2 -0
  533. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/multiple_finals.jsonl +4 -0
  534. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/not_found.jsonl +3 -0
  535. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/unbound_source.jsonl +3 -0
  536. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/wrong_search_shape.jsonl +3 -0
  537. package/vendor/ars/scripts/fixtures/evidence_rows/phase_e_inputs.json +114 -0
  538. package/vendor/ars/scripts/fixtures/evidence_rows/session_sources.json +5 -0
  539. package/vendor/ars/scripts/fixtures/human_subjects_authority/cross-border-us-tw-gdpr.json +71 -0
  540. package/vendor/ars/scripts/fixtures/human_subjects_authority/gdpr-member-state-unresolved.json +62 -0
  541. package/vendor/ars/scripts/fixtures/human_subjects_authority/missing-data-axis.json +45 -0
  542. package/vendor/ars/scripts/fixtures/human_subjects_authority/no-profile.json +33 -0
  543. package/vendor/ars/scripts/fixtures/human_subjects_authority/tw-gdpr-two-axis.json +74 -0
  544. package/vendor/ars/scripts/fixtures/human_subjects_authority/us-gdpr-two-axis.json +74 -0
  545. package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/lint-near-misses.json +72 -0
  546. package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/no-profile-request.json +18 -0
  547. package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/tw-candidates-request.json +63 -0
  548. package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/us-candidates-request.json +77 -0
  549. package/vendor/ars/scripts/fixtures/review_target_context/exact-declaration.json +24 -0
  550. package/vendor/ars/scripts/fixtures/review_target_context/field-general-declaration.json +24 -0
  551. package/vendor/ars/scripts/fixtures/review_target_context/msr-2027-technical-full-declaration.json +24 -0
  552. package/vendor/ars/scripts/fixtures/review_target_context/synthetic-registry.json +92 -0
  553. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/README.md +21 -0
  554. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_attested_only.json +55 -0
  555. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_machine_supported.json +49 -0
  556. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_not_isolated.json +56 -0
  557. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_unknown.json +49 -0
  558. package/vendor/ars/scripts/fixtures/submission_package/clean/paper.md +16 -0
  559. package/vendor/ars/scripts/fixtures/submission_package/clean/references.bib +13 -0
  560. package/vendor/ars/scripts/fixtures/submission_package/fallback_authoryear/paper.md +11 -0
  561. package/vendor/ars/scripts/fixtures/submission_package/fallback_authoryear/references.bib +13 -0
  562. package/vendor/ars/scripts/fixtures/submission_package/fallback_latex/paper.tex +9 -0
  563. package/vendor/ars/scripts/fixtures/submission_package/fallback_latex/references.bib +13 -0
  564. package/vendor/ars/scripts/fixtures/submission_package/marker_no_join/paper.md +7 -0
  565. package/vendor/ars/scripts/fixtures/submission_package/orphan_intext/paper.md +8 -0
  566. package/vendor/ars/scripts/fixtures/submission_package/orphan_intext/references.bib +6 -0
  567. package/vendor/ars/scripts/fixtures/submission_package/passports/corpus_only.yaml +9 -0
  568. package/vendor/ars/scripts/fixtures/submission_package/passports/summary_join.yaml +16 -0
  569. package/vendor/ars/scripts/fixtures/submission_package/profiles/full.yaml +15 -0
  570. package/vendor/ars/scripts/fixtures/submission_package/profiles/tight.yaml +15 -0
  571. package/vendor/ars/scripts/fixtures/submission_package/summary_join/paper.md +7 -0
  572. package/vendor/ars/scripts/fixtures/submission_package/uncited_reference/paper.md +9 -0
  573. package/vendor/ars/scripts/fixtures/submission_package/uncited_reference/references.bib +13 -0
  574. package/vendor/ars/scripts/fixtures/submission_package/venue_clean/paper.md +26 -0
  575. package/vendor/ars/scripts/fixtures/submission_package/venue_clean/references.bib +13 -0
  576. package/vendor/ars/scripts/fixtures/submission_package/venue_violations/paper.md +20 -0
  577. package/vendor/ars/scripts/fixtures/submission_package/venue_violations/references.bib +13 -0
  578. package/vendor/ars/scripts/fixtures/submission_packet_manifest/base_inventory.json +101 -0
  579. package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/consent-materials.txt +3 -0
  580. package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/training-certificate.txt +2 -0
  581. package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/tw-consent-materials.txt +3 -0
  582. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/corpus_input.yaml +30 -0
  583. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/own_draft.md +36 -0
  584. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/own_draft.tex +21 -0
  585. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/seed_expectations.json +218 -0
  586. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/snapshot.json +164 -0
  587. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/snapshot_manifest.json +29 -0
  588. package/vendor/ars/scripts/fixtures/transport_bodies/README.md +55 -0
  589. package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/empty_feed.xml +10 -0
  590. package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/error_5xx.html +7 -0
  591. package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/id_hit.xml +26 -0
  592. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/README.md +69 -0
  593. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/cnki_landing_page.html +13 -0
  594. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/error_5xx.html +2 -0
  595. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_ambiguous.json +16 -0
  596. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_hit.json +16 -0
  597. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_zero.json +14 -0
  598. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coverage_hit.json +15 -0
  599. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coverage_zero.json +13 -0
  600. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_hit.json +43 -0
  601. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_issn_mismatch.json +31 -0
  602. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_no_doi.json +31 -0
  603. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_unknown_ra.json +34 -0
  604. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_year_mismatch.json +31 -0
  605. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_absent.json +4 -0
  606. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_exists.json +16 -0
  607. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_internal_error.json +4 -0
  608. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/istic_csl_hit.json +25 -0
  609. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/istic_csl_other_title.json +18 -0
  610. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_cnki.json +6 -0
  611. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_crossref.json +6 -0
  612. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_istic.json +6 -0
  613. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_unknown_prefix.json +6 -0
  614. package/vendor/ars/scripts/fixtures/transport_bodies/crossref/doi_hit.json +24 -0
  615. package/vendor/ars/scripts/fixtures/transport_bodies/crossref/error_5xx.html +7 -0
  616. package/vendor/ars/scripts/fixtures/transport_bodies/crossref/title_search_miss.json +15 -0
  617. package/vendor/ars/scripts/fixtures/transport_bodies/openalex/doi_hit.json +29 -0
  618. package/vendor/ars/scripts/fixtures/transport_bodies/openalex/error_5xx.json +4 -0
  619. package/vendor/ars/scripts/fixtures/transport_bodies/openalex/title_search_miss.json +11 -0
  620. package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/doi_hit.json +14 -0
  621. package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/error_5xx.json +3 -0
  622. package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/title_search_miss.json +5 -0
  623. package/vendor/ars/scripts/human_read_attestation_resolver.py +467 -0
  624. package/vendor/ars/scripts/ideation_diversity_assignment_gate.py +430 -0
  625. package/vendor/ars/scripts/inquiry_branch_ledger.py +2540 -0
  626. package/vendor/ars/scripts/legacy/ars_apply_revision_patch_v1_0.py +715 -0
  627. package/vendor/ars/scripts/legacy/check_re_review_synthesis_v1_0.py +2215 -0
  628. package/vendor/ars/scripts/migrate_literature_corpus_to_v3_10.py +223 -0
  629. package/vendor/ars/scripts/migrate_literature_corpus_to_v3_7_3.py +277 -0
  630. package/vendor/ars/scripts/migrate_literature_corpus_to_v3_9_0.py +304 -0
  631. package/vendor/ars/scripts/model_tiering_manifest.json +46 -0
  632. package/vendor/ars/scripts/openalex_client.py +232 -0
  633. package/vendor/ars/scripts/parse_audit_verdict.py +802 -0
  634. package/vendor/ars/scripts/pdf_content_classifier_worker.py +176 -0
  635. package/vendor/ars/scripts/pdf_read_preflight.py +1455 -0
  636. package/vendor/ars/scripts/policy_anchor_disclosure_referee.py +354 -0
  637. package/vendor/ars/scripts/recompute_receipts.py +1414 -0
  638. package/vendor/ars/scripts/render_claim_standing_view.py +374 -0
  639. package/vendor/ars/scripts/render_eval_comment.py +130 -0
  640. package/vendor/ars/scripts/render_harness_retirement_issue.py +167 -0
  641. package/vendor/ars/scripts/repro_lock_validation.py +90 -0
  642. package/vendor/ars/scripts/research_workflow_profile.py +1079 -0
  643. package/vendor/ars/scripts/resolve_human_subjects_authority.py +1158 -0
  644. package/vendor/ars/scripts/resolve_review_target_context.py +683 -0
  645. package/vendor/ars/scripts/resume_e4_record.py +509 -0
  646. package/vendor/ars/scripts/retraction_status.py +484 -0
  647. package/vendor/ars/scripts/review_criteria_binding.py +889 -0
  648. package/vendor/ars/scripts/review_panel_provenance.py +744 -0
  649. package/vendor/ars/scripts/revision_roadmap.py +1967 -0
  650. package/vendor/ars/scripts/run_ci_pytest_manifest.py +116 -0
  651. package/vendor/ars/scripts/run_codex_audit.sh +1191 -0
  652. package/vendor/ars/scripts/run_evals.py +513 -0
  653. package/vendor/ars/scripts/run_ideation_diversity_no_call.py +3505 -0
  654. package/vendor/ars/scripts/run_indirect_prompt_injection_no_call.py +3330 -0
  655. package/vendor/ars/scripts/run_indirect_prompt_injection_probe.py +399 -0
  656. package/vendor/ars/scripts/run_review_criteria_constructive_value.py +1895 -0
  657. package/vendor/ars/scripts/run_role_topology_utility_dry_run.py +606 -0
  658. package/vendor/ars/scripts/score_review_criteria_constructive_value.py +617 -0
  659. package/vendor/ars/scripts/semantic_scholar_client.py +291 -0
  660. package/vendor/ars/scripts/slr_lineage.py +59 -0
  661. package/vendor/ars/scripts/sync_adapter_docs.py +118 -0
  662. package/vendor/ars/scripts/temporal_integrity_audit.py +840 -0
  663. package/vendor/ars/scripts/test_431_exact_or_bust.py +253 -0
  664. package/vendor/ars/scripts/test__eval_threshold_gate.py +125 -0
  665. package/vendor/ars/scripts/test__markdown_lint_util.py +129 -0
  666. package/vendor/ars/scripts/test__next_verified_at_ms.py +256 -0
  667. package/vendor/ars/scripts/test_adjacent_framing_probe_lint.py +171 -0
  668. package/vendor/ars/scripts/test_adjudication_activity.py +1515 -0
  669. package/vendor/ars/scripts/test_ars_anchorize_draft.py +178 -0
  670. package/vendor/ars/scripts/test_ars_apply_revision_patch.py +1335 -0
  671. package/vendor/ars/scripts/test_ars_cache_invalidate.py +51 -0
  672. package/vendor/ars/scripts/test_ars_mark_read.py +910 -0
  673. package/vendor/ars/scripts/test_ars_update_check.py +816 -0
  674. package/vendor/ars/scripts/test_ars_write_scope_guard.py +790 -0
  675. package/vendor/ars/scripts/test_arxiv_client.py +374 -0
  676. package/vendor/ars/scripts/test_audit_schemas.py +560 -0
  677. package/vendor/ars/scripts/test_audit_snapshot_render_section_0.py +105 -0
  678. package/vendor/ars/scripts/test_block_parser.py +259 -0
  679. package/vendor/ars/scripts/test_bootstrap_timeline_yaml.py +148 -0
  680. package/vendor/ars/scripts/test_build_claim_standing_candidate_ledger.py +1203 -0
  681. package/vendor/ars/scripts/test_build_claim_standing_query_plan.py +791 -0
  682. package/vendor/ars/scripts/test_build_submission_packet_manifest.py +2644 -0
  683. package/vendor/ars/scripts/test_check_215_field_norm.py +238 -0
  684. package/vendor/ars/scripts/test_check_216_surface_form.py +341 -0
  685. package/vendor/ars/scripts/test_check_268_nested_commitment_ledger.py +194 -0
  686. package/vendor/ars/scripts/test_check_390_revision_patch_discipline.py +279 -0
  687. package/vendor/ars/scripts/test_check_392_citation_verification_intake.py +144 -0
  688. package/vendor/ars/scripts/test_check_394_submission_policy.py +200 -0
  689. package/vendor/ars/scripts/test_check_439_format_profile.py +252 -0
  690. package/vendor/ars/scripts/test_check_619_disclosure_closeout.py +158 -0
  691. package/vendor/ars/scripts/test_check_630_codex_subscription_transport.py +245 -0
  692. package/vendor/ars/scripts/test_check_669_review_pathway_rule_trace.py +304 -0
  693. package/vendor/ars/scripts/test_check_670_revision_roadmap_integration.py +260 -0
  694. package/vendor/ars/scripts/test_check_673_adjudication_activity.py +367 -0
  695. package/vendor/ars/scripts/test_check_684_review_criteria_binding.py +274 -0
  696. package/vendor/ars/scripts/test_check_agents_mirror_sync.py +137 -0
  697. package/vendor/ars/scripts/test_check_audit_artifact_consistency.py +2133 -0
  698. package/vendor/ars/scripts/test_check_benchmark_report.py +117 -0
  699. package/vendor/ars/scripts/test_check_bibliographic_integrity_signals.py +609 -0
  700. package/vendor/ars/scripts/test_check_calibration_tiers.py +376 -0
  701. package/vendor/ars/scripts/test_check_changelog_covers_merges.py +509 -0
  702. package/vendor/ars/scripts/test_check_ci_pytest_manifest.py +425 -0
  703. package/vendor/ars/scripts/test_check_claim_standing_candidate_ledger_integration.py +256 -0
  704. package/vendor/ars/scripts/test_check_claim_standing_freshness.py +277 -0
  705. package/vendor/ars/scripts/test_check_collaboration_depth_rubric.py +239 -0
  706. package/vendor/ars/scripts/test_check_command_frontmatter_name.py +178 -0
  707. package/vendor/ars/scripts/test_check_committee_correspondence.py +298 -0
  708. package/vendor/ars/scripts/test_check_compliance_report.py +381 -0
  709. package/vendor/ars/scripts/test_check_content_coverage_advisory_integration.py +308 -0
  710. package/vendor/ars/scripts/test_check_control_availability.py +361 -0
  711. package/vendor/ars/scripts/test_check_cross_document_consistency_advisory_integration.py +698 -0
  712. package/vendor/ars/scripts/test_check_cross_model_handoff_contract.py +301 -0
  713. package/vendor/ars/scripts/test_check_cross_model_verification_sync.py +203 -0
  714. package/vendor/ars/scripts/test_check_data_access_level.py +227 -0
  715. package/vendor/ars/scripts/test_check_data_flows.py +382 -0
  716. package/vendor/ars/scripts/test_check_decision_contract.py +387 -0
  717. package/vendor/ars/scripts/test_check_degradation_registry.py +270 -0
  718. package/vendor/ars/scripts/test_check_distribution_surface_claims.py +225 -0
  719. package/vendor/ars/scripts/test_check_domain_evidence_profile.py +439 -0
  720. package/vendor/ars/scripts/test_check_e4_promotion.py +163 -0
  721. package/vendor/ars/scripts/test_check_evals_gold_set.py +312 -0
  722. package/vendor/ars/scripts/test_check_evidence_row_integration.py +187 -0
  723. package/vendor/ars/scripts/test_check_field_norm_severity.py +173 -0
  724. package/vendor/ars/scripts/test_check_firm_rules_sync.py +342 -0
  725. package/vendor/ars/scripts/test_check_heldout_measurement_report.py +1508 -0
  726. package/vendor/ars/scripts/test_check_human_subjects_output_contract.py +129 -0
  727. package/vendor/ars/scripts/test_check_human_subjects_reference_migration.py +756 -0
  728. package/vendor/ars/scripts/test_check_instruction_data_boundary.py +204 -0
  729. package/vendor/ars/scripts/test_check_judge_prompt_version.py +90 -0
  730. package/vendor/ars/scripts/test_check_model_tiering.py +236 -0
  731. package/vendor/ars/scripts/test_check_panel_synthesis.py +1658 -0
  732. package/vendor/ars/scripts/test_check_passport_reset_contract.py +249 -0
  733. package/vendor/ars/scripts/test_check_pattern_eval_manifest.py +381 -0
  734. package/vendor/ars/scripts/test_check_persuasion_invariance_fixtures.py +591 -0
  735. package/vendor/ars/scripts/test_check_phase_conformance.py +4366 -0
  736. package/vendor/ars/scripts/test_check_pipeline_boundary_semantics.py +931 -0
  737. package/vendor/ars/scripts/test_check_pipeline_integrity.py +243 -0
  738. package/vendor/ars/scripts/test_check_policy_anchor_protocol.py +236 -0
  739. package/vendor/ars/scripts/test_check_policy_anchor_table.py +295 -0
  740. package/vendor/ars/scripts/test_check_prisma_trAIce_freshness.py +68 -0
  741. package/vendor/ars/scripts/test_check_promotion_bakeoff_preregistration.py +799 -0
  742. package/vendor/ars/scripts/test_check_ranking_lift.py +377 -0
  743. package/vendor/ars/scripts/test_check_re_review_synthesis.py +3398 -0
  744. package/vendor/ars/scripts/test_check_receipt_enum_sync.py +176 -0
  745. package/vendor/ars/scripts/test_check_repro_lock.py +107 -0
  746. package/vendor/ars/scripts/test_check_reviewer_data_fences.py +334 -0
  747. package/vendor/ars/scripts/test_check_reviewer_finding_contract.py +1107 -0
  748. package/vendor/ars/scripts/test_check_reviewer_role_label.py +491 -0
  749. package/vendor/ars/scripts/test_check_reviewer_scoring_honesty.py +222 -0
  750. package/vendor/ars/scripts/test_check_reviewer_sprint_prompt_sync.py +373 -0
  751. package/vendor/ars/scripts/test_check_revision_claim_drift_suite_v2.py +1925 -0
  752. package/vendor/ars/scripts/test_check_revision_token_conservation.py +398 -0
  753. package/vendor/ars/scripts/test_check_risk_register.py +336 -0
  754. package/vendor/ars/scripts/test_check_role_scoped_contract.py +1104 -0
  755. package/vendor/ars/scripts/test_check_rq_framing_patterns.py +110 -0
  756. package/vendor/ars/scripts/test_check_rubric_weight_consistency.py +32 -0
  757. package/vendor/ars/scripts/test_check_seeded_defect_fixtures.py +392 -0
  758. package/vendor/ars/scripts/test_check_setup_cross_model_parity.py +121 -0
  759. package/vendor/ars/scripts/test_check_spec_consistency.py +1081 -0
  760. package/vendor/ars/scripts/test_check_sprint_contract.py +458 -0
  761. package/vendor/ars/scripts/test_check_stage_capability_matrix.py +698 -0
  762. package/vendor/ars/scripts/test_check_submission_packet_manifest_integration.py +177 -0
  763. package/vendor/ars/scripts/test_check_surface_form_parity.py +417 -0
  764. package/vendor/ars/scripts/test_check_task_type.py +116 -0
  765. package/vendor/ars/scripts/test_check_tools_allowlist.py +835 -0
  766. package/vendor/ars/scripts/test_check_tortured_phrase_screening_integration.py +1306 -0
  767. package/vendor/ars/scripts/test_check_v3_10_134_write_scope.py +251 -0
  768. package/vendor/ars/scripts/test_check_v3_10_policy.py +547 -0
  769. package/vendor/ars/scripts/test_check_v3_6_7_pattern_protection.py +960 -0
  770. package/vendor/ars/scripts/test_check_v3_6_8_audit_scope_block.py +1000 -0
  771. package/vendor/ars/scripts/test_check_v3_6_8_cite_provenance_pipeline.py +454 -0
  772. package/vendor/ars/scripts/test_check_v3_6_8_frontmatter_trust_schema.py +581 -0
  773. package/vendor/ars/scripts/test_check_v3_6_8_mark_read_commands.py +120 -0
  774. package/vendor/ars/scripts/test_check_v3_6_8_pattern_protection.py +1138 -0
  775. package/vendor/ars/scripts/test_check_v3_7_3_three_layer_citation.py +566 -0
  776. package/vendor/ars/scripts/test_check_v3_8_annotation_literal_sync.py +262 -0
  777. package/vendor/ars/scripts/test_check_v3_9_0_triangulation.py +321 -0
  778. package/vendor/ars/scripts/test_check_v3_9_2_phase_boundary.py +190 -0
  779. package/vendor/ars/scripts/test_check_v3_9_4_temporal_verification.py +471 -0
  780. package/vendor/ars/scripts/test_check_version_consistency.py +1462 -0
  781. package/vendor/ars/scripts/test_check_workflow_classification.py +225 -0
  782. package/vendor/ars/scripts/test_chinese_literature_client.py +1889 -0
  783. package/vendor/ars/scripts/test_citation_existence_policy.py +480 -0
  784. package/vendor/ars/scripts/test_citation_verification_summary.py +342 -0
  785. package/vendor/ars/scripts/test_claim_audit_calibration.py +882 -0
  786. package/vendor/ars/scripts/test_claim_audit_finalizer.py +979 -0
  787. package/vendor/ars/scripts/test_claim_audit_pipeline.py +2398 -0
  788. package/vendor/ars/scripts/test_claim_audit_schema.py +1778 -0
  789. package/vendor/ars/scripts/test_claim_intent_manifest.py +666 -0
  790. package/vendor/ars/scripts/test_claim_registry_coverage.py +246 -0
  791. package/vendor/ars/scripts/test_claim_standing_discovery.py +579 -0
  792. package/vendor/ars/scripts/test_claim_standing_pipeline_wiring.py +290 -0
  793. package/vendor/ars/scripts/test_claim_standing_stance_assets.py +399 -0
  794. package/vendor/ars/scripts/test_claim_standing_stance_contracts.py +383 -0
  795. package/vendor/ars/scripts/test_claim_standing_stance_runner.py +349 -0
  796. package/vendor/ars/scripts/test_claim_standing_transmissions.py +605 -0
  797. package/vendor/ars/scripts/test_claim_strength_drift_disposition.py +517 -0
  798. package/vendor/ars/scripts/test_claim_verification_coverage_contract.py +104 -0
  799. package/vendor/ars/scripts/test_contamination_signals.py +1086 -0
  800. package/vendor/ars/scripts/test_content_coverage_advisory.py +1767 -0
  801. package/vendor/ars/scripts/test_cross_document_consistency_advisory.py +1270 -0
  802. package/vendor/ars/scripts/test_cross_model_codex_transport.py +1152 -0
  803. package/vendor/ars/scripts/test_cross_model_handoff.py +571 -0
  804. package/vendor/ars/scripts/test_cross_model_verification_guards.py +792 -0
  805. package/vendor/ars/scripts/test_crossref_client.py +393 -0
  806. package/vendor/ars/scripts/test_dispatch_e4_panel.py +4235 -0
  807. package/vendor/ars/scripts/test_e2e_claim_audit.py +540 -0
  808. package/vendor/ars/scripts/test_eval_harness_workflow.py +140 -0
  809. package/vendor/ars/scripts/test_evals_citation_extraction.py +150 -0
  810. package/vendor/ars/scripts/test_evals_lift_report_schema.py +108 -0
  811. package/vendor/ars/scripts/test_evidence_rows.py +2593 -0
  812. package/vendor/ars/scripts/test_experiment_provenance.py +915 -0
  813. package/vendor/ars/scripts/test_human_read_attestation_resolver.py +477 -0
  814. package/vendor/ars/scripts/test_ideation_diversity_assignment_gate.py +615 -0
  815. package/vendor/ars/scripts/test_indirect_prompt_injection_behavior_probe.py +250 -0
  816. package/vendor/ars/scripts/test_inquiry_branch_ledger.py +2296 -0
  817. package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_10.py +248 -0
  818. package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_7_3.py +545 -0
  819. package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_9_0.py +497 -0
  820. package/vendor/ars/scripts/test_normalize_compat_verdict.py +149 -0
  821. package/vendor/ars/scripts/test_openalex_client.py +490 -0
  822. package/vendor/ars/scripts/test_passport_yaml.py +104 -0
  823. package/vendor/ars/scripts/test_pattern_eval_runtime.py +1295 -0
  824. package/vendor/ars/scripts/test_pdf_read_preflight.py +1943 -0
  825. package/vendor/ars/scripts/test_policy_anchor_disclosure.py +666 -0
  826. package/vendor/ars/scripts/test_reading_probe_lint.py +218 -0
  827. package/vendor/ars/scripts/test_recompute_receipts.py +778 -0
  828. package/vendor/ars/scripts/test_render_claim_standing_view.py +192 -0
  829. package/vendor/ars/scripts/test_render_eval_comment.py +162 -0
  830. package/vendor/ars/scripts/test_render_harness_retirement_issue.py +110 -0
  831. package/vendor/ars/scripts/test_repro_lock_validation_drift.py +100 -0
  832. package/vendor/ars/scripts/test_research_workflow_profile.py +734 -0
  833. package/vendor/ars/scripts/test_resolve_human_subjects_authority.py +1219 -0
  834. package/vendor/ars/scripts/test_resolve_review_target_context.py +703 -0
  835. package/vendor/ars/scripts/test_resume_e4_record.py +315 -0
  836. package/vendor/ars/scripts/test_retraction_status.py +456 -0
  837. package/vendor/ars/scripts/test_review_criteria_binding.py +629 -0
  838. package/vendor/ars/scripts/test_review_panel_provenance.py +565 -0
  839. package/vendor/ars/scripts/test_review_pathway_rule_trace.py +821 -0
  840. package/vendor/ars/scripts/test_revision_roadmap.py +1255 -0
  841. package/vendor/ars/scripts/test_run_ci_pytest_manifest.py +186 -0
  842. package/vendor/ars/scripts/test_run_codex_audit_e2e.py +369 -0
  843. package/vendor/ars/scripts/test_run_evals.py +430 -0
  844. package/vendor/ars/scripts/test_run_guard_launcher.py +500 -0
  845. package/vendor/ars/scripts/test_run_ideation_diversity_no_call.py +1833 -0
  846. package/vendor/ars/scripts/test_run_indirect_prompt_injection_no_call.py +1889 -0
  847. package/vendor/ars/scripts/test_run_review_criteria_constructive_value.py +586 -0
  848. package/vendor/ars/scripts/test_run_role_topology_utility_dry_run.py +428 -0
  849. package/vendor/ars/scripts/test_score_review_criteria_constructive_value.py +340 -0
  850. package/vendor/ars/scripts/test_semantic_scholar_client.py +554 -0
  851. package/vendor/ars/scripts/test_slr_lineage_emission.py +230 -0
  852. package/vendor/ars/scripts/test_socratic_rq_non_generation_contract.py +173 -0
  853. package/vendor/ars/scripts/test_temporal_integrity_audit.py +438 -0
  854. package/vendor/ars/scripts/test_text_similarity.py +95 -0
  855. package/vendor/ars/scripts/test_title_fuzzy_false_positive.py +111 -0
  856. package/vendor/ars/scripts/test_tortured_phrase_screening.py +2959 -0
  857. package/vendor/ars/scripts/test_transport_fixture_citation_gate.py +338 -0
  858. package/vendor/ars/scripts/test_uncited_assertion.py +558 -0
  859. package/vendor/ars/scripts/test_v3_6_7_phase_6_6.py +1279 -0
  860. package/vendor/ars/scripts/test_validate_compliance_fixtures.py +36 -0
  861. package/vendor/ars/scripts/test_validate_ideation_diversity_assets.py +230 -0
  862. package/vendor/ars/scripts/test_venue_disclosure_contract.py +755 -0
  863. package/vendor/ars/scripts/test_verification_cache.py +280 -0
  864. package/vendor/ars/scripts/test_verification_gate.py +461 -0
  865. package/vendor/ars/scripts/test_verify_passport_cli.py +123 -0
  866. package/vendor/ars/scripts/test_verify_submission_package.py +1407 -0
  867. package/vendor/ars/scripts/test_version_records_schema.py +211 -0
  868. package/vendor/ars/scripts/tortured_phrase_screening.py +3502 -0
  869. package/vendor/ars/scripts/uncited_assertion_detector.py +254 -0
  870. package/vendor/ars/scripts/v3_6_7_inversion_manifest.json +9 -0
  871. package/vendor/ars/scripts/v3_6_8_inversion_manifest.json +10 -0
  872. package/vendor/ars/scripts/validate_claim_standing_stance_assets.py +249 -0
  873. package/vendor/ars/scripts/validate_compliance_fixtures.py +56 -0
  874. package/vendor/ars/scripts/validate_ideation_diversity_assets.py +303 -0
  875. package/vendor/ars/scripts/venue_disclosure_contract_harness.py +837 -0
  876. package/vendor/ars/scripts/verification_cache.py +276 -0
  877. package/vendor/ars/scripts/verification_gate/__init__.py +345 -0
  878. package/vendor/ars/scripts/verify_passport.py +133 -0
  879. package/vendor/ars/scripts/verify_submission_package.py +1657 -0
  880. package/vendor/ars/shared/agents/compliance_agent.md +136 -0
  881. package/vendor/ars/shared/artifact_reproducibility_pattern.md +173 -0
  882. package/vendor/ars/shared/benchmark_report.schema.json +81 -0
  883. package/vendor/ars/shared/benchmark_report_pattern.md +180 -0
  884. package/vendor/ars/shared/bibliographic_integrity_signals.md +142 -0
  885. package/vendor/ars/shared/collaboration_depth_rubric.md +154 -0
  886. package/vendor/ars/shared/compliance_checkpoint_protocol.md +162 -0
  887. package/vendor/ars/shared/compliance_report.schema.json +187 -0
  888. package/vendor/ars/shared/contracts/README.md +938 -0
  889. package/vendor/ars/shared/contracts/activity/adjudication_activity_input.schema.json +555 -0
  890. package/vendor/ars/shared/contracts/activity/adjudication_activity_store.schema.json +560 -0
  891. package/vendor/ars/shared/contracts/audit/audit_jsonl.schema.json +128 -0
  892. package/vendor/ars/shared/contracts/audit/audit_sidecar.schema.json +169 -0
  893. package/vendor/ars/shared/contracts/audit/audit_verdict.schema.json +133 -0
  894. package/vendor/ars/shared/contracts/audit/cross_document_consistency_advisory.schema.json +480 -0
  895. package/vendor/ars/shared/contracts/audit/cross_document_consistency_advisory_draft.schema.json +564 -0
  896. package/vendor/ars/shared/contracts/audit/cross_document_source_manifest.schema.json +202 -0
  897. package/vendor/ars/shared/contracts/audit/tortured_phrase_advisory.schema.json +1362 -0
  898. package/vendor/ars/shared/contracts/audit/tortured_phrase_snapshot.schema.json +208 -0
  899. package/vendor/ars/shared/contracts/audit/tortured_phrase_snapshot_manifest.schema.json +335 -0
  900. package/vendor/ars/shared/contracts/capability/stage_capability_matrix.json +434 -0
  901. package/vendor/ars/shared/contracts/claim_standing/candidate_ledger.schema.json +705 -0
  902. package/vendor/ars/shared/contracts/claim_standing/query_plan.schema.json +389 -0
  903. package/vendor/ars/shared/contracts/claim_standing/query_plan_v1_1.schema.json +782 -0
  904. package/vendor/ars/shared/contracts/claim_standing/retrieval_input.schema.json +440 -0
  905. package/vendor/ars/shared/contracts/claim_standing/stance_record.schema.json +460 -0
  906. package/vendor/ars/shared/contracts/claim_standing/transmission_ledger.schema.json +282 -0
  907. package/vendor/ars/shared/contracts/cross_model/codex_citation_receipt.schema.json +157 -0
  908. package/vendor/ars/shared/contracts/cross_model/codex_citation_request.schema.json +23 -0
  909. package/vendor/ars/shared/contracts/cross_model/promotion_bakeoff_sealed_commitment.schema.json +55 -0
  910. package/vendor/ars/shared/contracts/cross_model/promotion_bakeoff_sealed_reveal.schema.json +50 -0
  911. package/vendor/ars/shared/contracts/degradation_registry.json +428 -0
  912. package/vendor/ars/shared/contracts/evaluator/full.json +126 -0
  913. package/vendor/ars/shared/contracts/evidence/claim_registry.schema.json +50 -0
  914. package/vendor/ars/shared/contracts/evidence/claim_registry_coverage_report.schema.json +79 -0
  915. package/vendor/ars/shared/contracts/evidence/evidence_row.schema.json +504 -0
  916. package/vendor/ars/shared/contracts/evidence/evidence_row_v1_1.schema.json +364 -0
  917. package/vendor/ars/shared/contracts/evidence/evidence_row_v1_2.schema.json +714 -0
  918. package/vendor/ars/shared/contracts/evidence/evidence_row_v1_3.schema.json +524 -0
  919. package/vendor/ars/shared/contracts/human_subjects/authority_profile_registry.schema.json +497 -0
  920. package/vendor/ars/shared/contracts/human_subjects/committee_correspondence.schema.json +253 -0
  921. package/vendor/ars/shared/contracts/human_subjects/content_coverage_advisory.schema.json +620 -0
  922. package/vendor/ars/shared/contracts/human_subjects/irb_context_record.schema.json +329 -0
  923. package/vendor/ars/shared/contracts/human_subjects/resolved_authority_context.schema.json +347 -0
  924. package/vendor/ars/shared/contracts/human_subjects/review_pathway_rule_trace.schema.json +265 -0
  925. package/vendor/ars/shared/contracts/human_subjects/review_pathway_trace_request.schema.json +127 -0
  926. package/vendor/ars/shared/contracts/human_subjects/submission_packet_inventory.schema.json +240 -0
  927. package/vendor/ars/shared/contracts/human_subjects/submission_packet_manifest.schema.json +999 -0
  928. package/vendor/ars/shared/contracts/passport/audit_artifact_entry.schema.json +266 -0
  929. package/vendor/ars/shared/contracts/passport/bibliographic_integrity_signal.schema.json +1676 -0
  930. package/vendor/ars/shared/contracts/passport/citation_provenance.schema.json +107 -0
  931. package/vendor/ars/shared/contracts/passport/citation_verification_summary.schema.json +164 -0
  932. package/vendor/ars/shared/contracts/passport/claim_audit_result.schema.json +124 -0
  933. package/vendor/ars/shared/contracts/passport/claim_drift.schema.json +58 -0
  934. package/vendor/ars/shared/contracts/passport/claim_intent_manifest.schema.json +107 -0
  935. package/vendor/ars/shared/contracts/passport/constraint_violation.schema.json +65 -0
  936. package/vendor/ars/shared/contracts/passport/experiment_alignment_result.schema.json +69 -0
  937. package/vendor/ars/shared/contracts/passport/experiment_provenance_entry.schema.json +169 -0
  938. package/vendor/ars/shared/contracts/passport/human_read_log.schema.json +86 -0
  939. package/vendor/ars/shared/contracts/passport/inquiry_ledger_ref.schema.json +22 -0
  940. package/vendor/ars/shared/contracts/passport/literature_corpus_entry.schema.json +650 -0
  941. package/vendor/ars/shared/contracts/passport/preregistration_artifact.schema.json +132 -0
  942. package/vendor/ars/shared/contracts/passport/rejection_log.schema.json +89 -0
  943. package/vendor/ars/shared/contracts/passport/reset_ledger_entry.schema.json +158 -0
  944. package/vendor/ars/shared/contracts/passport/temporal_audit_results.schema.json +208 -0
  945. package/vendor/ars/shared/contracts/passport/terminal_policies.schema.json +50 -0
  946. package/vendor/ars/shared/contracts/passport/timeline.schema.json +102 -0
  947. package/vendor/ars/shared/contracts/passport/uncited_assertion.schema.json +56 -0
  948. package/vendor/ars/shared/contracts/passport/uncited_audit_failure.schema.json +72 -0
  949. package/vendor/ars/shared/contracts/passport/user_attested_read_resolution.schema.json +87 -0
  950. package/vendor/ars/shared/contracts/passport/version_records.schema.json +138 -0
  951. package/vendor/ars/shared/contracts/patch/block_manifest.schema.json +45 -0
  952. package/vendor/ars/shared/contracts/patch/legacy/v1_0/revision_patch.schema.json +110 -0
  953. package/vendor/ars/shared/contracts/patch/revision_patch.schema.json +250 -0
  954. package/vendor/ars/shared/contracts/pdf/pdf_content_classifier_diagnostic.schema.json +39 -0
  955. package/vendor/ars/shared/contracts/pdf/pdf_content_classifier_worker.schema.json +88 -0
  956. package/vendor/ars/shared/contracts/pdf/pdf_read_preflight.schema.json +227 -0
  957. package/vendor/ars/shared/contracts/re_review/input_manifest.schema.json +161 -0
  958. package/vendor/ars/shared/contracts/re_review/legacy/v1_0/input_manifest.schema.json +134 -0
  959. package/vendor/ars/shared/contracts/re_review/legacy/v1_0/precommitment.schema.json +193 -0
  960. package/vendor/ars/shared/contracts/re_review/legacy/v1_0/traceability.schema.json +841 -0
  961. package/vendor/ars/shared/contracts/re_review/legacy/v1_0/verdict_record.schema.json +244 -0
  962. package/vendor/ars/shared/contracts/re_review/precommitment.schema.json +193 -0
  963. package/vendor/ars/shared/contracts/re_review/traceability.schema.json +916 -0
  964. package/vendor/ars/shared/contracts/re_review/verdict_record.schema.json +245 -0
  965. package/vendor/ars/shared/contracts/research_workflow/inquiry_branch_ledger.schema.json +449 -0
  966. package/vendor/ars/shared/contracts/research_workflow/research_workflow_profile.schema.json +222 -0
  967. package/vendor/ars/shared/contracts/research_workflow/research_workflow_profile_selection_receipt.schema.json +111 -0
  968. package/vendor/ars/shared/contracts/review_target/constructive_review_findings.schema.json +172 -0
  969. package/vendor/ars/shared/contracts/review_target/criteria_registry.schema.json +97 -0
  970. package/vendor/ars/shared/contracts/review_target/review_criteria_binding_manifest.schema.json +235 -0
  971. package/vendor/ars/shared/contracts/review_target/review_criteria_source_receipt.schema.json +158 -0
  972. package/vendor/ars/shared/contracts/review_target/review_target_context.schema.json +106 -0
  973. package/vendor/ars/shared/contracts/review_target/review_target_declaration.schema.json +118 -0
  974. package/vendor/ars/shared/contracts/reviewer/full.json +114 -0
  975. package/vendor/ars/shared/contracts/reviewer/methodology_focus.json +75 -0
  976. package/vendor/ars/shared/contracts/reviewer/review_panel_provenance.schema.json +263 -0
  977. package/vendor/ars/shared/contracts/reviewer/review_panel_provenance_carrier.schema.json +122 -0
  978. package/vendor/ars/shared/contracts/reviewer/review_panel_provenance_input.schema.json +120 -0
  979. package/vendor/ars/shared/contracts/revision/author_adjudication.schema.json +251 -0
  980. package/vendor/ars/shared/contracts/revision/author_adjudication_input.schema.json +25 -0
  981. package/vendor/ars/shared/contracts/revision/claim_strength_drift_disposition.schema.json +97 -0
  982. package/vendor/ars/shared/contracts/revision/claim_strength_drift_disposition_input.schema.json +80 -0
  983. package/vendor/ars/shared/contracts/revision/claim_strength_drift_findings.schema.json +139 -0
  984. package/vendor/ars/shared/contracts/revision/claim_surface_manifest.schema.json +99 -0
  985. package/vendor/ars/shared/contracts/revision/integrity_correction_authorization.schema.json +140 -0
  986. package/vendor/ars/shared/contracts/revision/integrity_correction_authorization_input.schema.json +28 -0
  987. package/vendor/ars/shared/contracts/revision/integrity_correction_list.schema.json +48 -0
  988. package/vendor/ars/shared/contracts/revision/integrity_pass_receipt.schema.json +16 -0
  989. package/vendor/ars/shared/contracts/revision/revision_evidence_bundle.schema.json +134 -0
  990. package/vendor/ars/shared/contracts/revision/revision_roadmap.schema.json +334 -0
  991. package/vendor/ars/shared/contracts/submission/format_profile.example.yaml +33 -0
  992. package/vendor/ars/shared/contracts/submission/format_profile.schema.json +102 -0
  993. package/vendor/ars/shared/contracts/submission/submission_verification_report.schema.json +233 -0
  994. package/vendor/ars/shared/contracts/submission/venue_profile.schema.json +114 -0
  995. package/vendor/ars/shared/contracts/writer/full.json +87 -0
  996. package/vendor/ars/shared/cross_model_verification.md +714 -0
  997. package/vendor/ars/shared/evals_lift_report.schema.json +141 -0
  998. package/vendor/ars/shared/ground_truth_isolation_pattern.md +275 -0
  999. package/vendor/ars/shared/handoff_schemas.md +1209 -0
  1000. package/vendor/ars/shared/human_subjects_authority_registry.json +1278 -0
  1001. package/vendor/ars/shared/mode_spectrum.md +57 -0
  1002. package/vendor/ars/shared/model_tiering.md +83 -0
  1003. package/vendor/ars/shared/policy_data/nature_policy.md +56 -0
  1004. package/vendor/ars/shared/prisma_trAIce_protocol.md +157 -0
  1005. package/vendor/ars/shared/raise_framework.md +129 -0
  1006. package/vendor/ars/shared/references/authority_content_coverage_advisory_protocol.md +275 -0
  1007. package/vendor/ars/shared/references/claim_standing_candidate_ledger_protocol.md +66 -0
  1008. package/vendor/ars/shared/references/claim_strength_ladder.md +93 -0
  1009. package/vendor/ars/shared/references/cross_document_consistency_advisory_protocol.md +263 -0
  1010. package/vendor/ars/shared/references/evidence_row_protocol.md +260 -0
  1011. package/vendor/ars/shared/references/firm_rules.md +90 -0
  1012. package/vendor/ars/shared/references/human_subjects_authority_protocol.md +274 -0
  1013. package/vendor/ars/shared/references/intent_clarification_protocol.md +168 -0
  1014. package/vendor/ars/shared/references/irb_terminology_glossary.md +229 -0
  1015. package/vendor/ars/shared/references/protected_hedging_phrases.md +118 -0
  1016. package/vendor/ars/shared/references/psychometric_terminology_glossary.md +109 -0
  1017. package/vendor/ars/shared/references/review_criteria_consumer_protocol.md +238 -0
  1018. package/vendor/ars/shared/references/review_pathway_rule_trace_protocol.md +166 -0
  1019. package/vendor/ars/shared/references/submission_packet_manifest_protocol.md +292 -0
  1020. package/vendor/ars/shared/references/word_count_conventions.md +124 -0
  1021. package/vendor/ars/shared/research_workflow_profiles/field_general.json +1 -0
  1022. package/vendor/ars/shared/review_criteria_registry.json +207 -0
  1023. package/vendor/ars/shared/review_criteria_sources/msr-2027-technical-papers.2026-08-24.json +46 -0
  1024. package/vendor/ars/shared/review_criteria_sources/sigsoft-empirical-standards.2026-08-24.json +31 -0
  1025. package/vendor/ars/shared/sprint_contract.schema.json +482 -0
  1026. package/vendor/ars/shared/style_calibration_protocol.md +151 -0
  1027. package/vendor/ars/shared/templates/codex_audit_multifile_template.md +263 -0
  1028. package/vendor/ars/tools/release-discipline/.toolkit-version +1 -0
  1029. package/vendor/ars/tools/release-discipline/README.md +4 -0
  1030. package/vendor/ars/tools/release-discipline/scripts/_release_doc_alignment_schema.py +1011 -0
  1031. package/vendor/ars/tools/release-discipline/scripts/check_command_invariants.py +497 -0
  1032. package/vendor/ars/tools/release-discipline/scripts/check_release_doc_alignment.py +263 -0
  1033. package/vendor/ars/tools/release-discipline/scripts/sync-toolkit.sh +147 -0
  1034. package/vendor/windows/NOTICE.md +12 -0
  1035. package/vendor/windows/arm64/fd.exe +0 -0
  1036. package/vendor/windows/arm64/licenses/fd/LICENSE-APACHE +201 -0
  1037. package/vendor/windows/arm64/licenses/fd/LICENSE-MIT +21 -0
  1038. package/vendor/windows/arm64/licenses/ripgrep/COPYING +3 -0
  1039. package/vendor/windows/arm64/licenses/ripgrep/LICENSE-MIT +21 -0
  1040. package/vendor/windows/arm64/licenses/ripgrep/UNLICENSE +24 -0
  1041. package/vendor/windows/arm64/rg.exe +0 -0
  1042. package/vendor/windows/x64/fd.exe +0 -0
  1043. package/vendor/windows/x64/licenses/fd/LICENSE-APACHE +201 -0
  1044. package/vendor/windows/x64/licenses/fd/LICENSE-MIT +21 -0
  1045. package/vendor/windows/x64/licenses/ripgrep/COPYING +3 -0
  1046. package/vendor/windows/x64/licenses/ripgrep/LICENSE-MIT +21 -0
  1047. package/vendor/windows/x64/licenses/ripgrep/UNLICENSE +24 -0
  1048. package/vendor/windows/x64/rg.exe +0 -0
@@ -0,0 +1,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
+ }