psyclaw 0.27.23 → 0.28.1

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 (1069) hide show
  1. package/README.md +11 -5
  2. package/agents/recommended/catalog.json +99 -0
  3. package/dist/apps/panel/index.html +5 -5
  4. package/dist/src/adapters/pi/extension.js +339 -94
  5. package/dist/src/adapters/pi/extension.js.map +1 -1
  6. package/dist/src/adapters/pi/rpc.d.ts +2 -0
  7. package/dist/src/adapters/pi/rpc.js +9 -2
  8. package/dist/src/adapters/pi/rpc.js.map +1 -1
  9. package/dist/src/agents/import.js +44 -32
  10. package/dist/src/agents/import.js.map +1 -1
  11. package/dist/src/agents/recommended-personas.d.ts +42 -0
  12. package/dist/src/agents/recommended-personas.js +135 -0
  13. package/dist/src/agents/recommended-personas.js.map +1 -0
  14. package/dist/src/analysis/hooks.d.ts +1 -0
  15. package/dist/src/analysis/hooks.js +20 -1
  16. package/dist/src/analysis/hooks.js.map +1 -1
  17. package/dist/src/ars/bridge.d.ts +20 -0
  18. package/dist/src/ars/bridge.js +132 -0
  19. package/dist/src/ars/bridge.js.map +1 -0
  20. package/dist/src/ars/contracts.d.ts +55 -0
  21. package/dist/src/ars/contracts.js +2 -0
  22. package/dist/src/ars/contracts.js.map +1 -0
  23. package/dist/src/ars/panel-plan.d.ts +4 -0
  24. package/dist/src/ars/panel-plan.js +11 -0
  25. package/dist/src/ars/panel-plan.js.map +1 -0
  26. package/dist/src/ars/pi-panel-executor.d.ts +30 -0
  27. package/dist/src/ars/pi-panel-executor.js +140 -0
  28. package/dist/src/ars/pi-panel-executor.js.map +1 -0
  29. package/dist/src/ars/profile.d.ts +36 -0
  30. package/dist/src/ars/profile.js +119 -0
  31. package/dist/src/ars/profile.js.map +1 -0
  32. package/dist/src/ars/re-review.d.ts +22 -0
  33. package/dist/src/ars/re-review.js +172 -0
  34. package/dist/src/ars/re-review.js.map +1 -0
  35. package/dist/src/branding.d.ts +2 -4
  36. package/dist/src/branding.js +3 -5
  37. package/dist/src/branding.js.map +1 -1
  38. package/dist/src/bundled-tools.d.ts +3 -0
  39. package/dist/src/bundled-tools.js +21 -0
  40. package/dist/src/bundled-tools.js.map +1 -0
  41. package/dist/src/chat.js +6 -5
  42. package/dist/src/chat.js.map +1 -1
  43. package/dist/src/cli.js +6 -3
  44. package/dist/src/cli.js.map +1 -1
  45. package/dist/src/creation/contracts.d.ts +41 -0
  46. package/dist/src/creation/contracts.js +2 -0
  47. package/dist/src/creation/contracts.js.map +1 -0
  48. package/dist/src/creation/service.d.ts +7 -0
  49. package/dist/src/creation/service.js +208 -0
  50. package/dist/src/creation/service.js.map +1 -0
  51. package/dist/src/index.d.ts +11 -0
  52. package/dist/src/index.js +11 -0
  53. package/dist/src/index.js.map +1 -1
  54. package/dist/src/install/installer.js +32 -10
  55. package/dist/src/install/installer.js.map +1 -1
  56. package/dist/src/orchestration/bundled-personas.d.ts +16 -0
  57. package/dist/src/orchestration/bundled-personas.js +101 -0
  58. package/dist/src/orchestration/bundled-personas.js.map +1 -0
  59. package/dist/src/orchestration/effects.d.ts +8 -0
  60. package/dist/src/orchestration/effects.js +39 -0
  61. package/dist/src/orchestration/effects.js.map +1 -0
  62. package/dist/src/orchestration/personas.d.ts +16 -0
  63. package/dist/src/orchestration/personas.js +64 -0
  64. package/dist/src/orchestration/personas.js.map +1 -0
  65. package/dist/src/orchestration/pi-executor.d.ts +9 -4
  66. package/dist/src/orchestration/pi-executor.js +55 -22
  67. package/dist/src/orchestration/pi-executor.js.map +1 -1
  68. package/dist/src/orchestration/research-agents.d.ts +3 -7
  69. package/dist/src/orchestration/research-agents.js +5 -69
  70. package/dist/src/orchestration/research-agents.js.map +1 -1
  71. package/dist/src/panel/server.js +19 -4
  72. package/dist/src/panel/server.js.map +1 -1
  73. package/dist/src/project/paths.d.ts +3 -0
  74. package/dist/src/project/paths.js +7 -0
  75. package/dist/src/project/paths.js.map +1 -1
  76. package/dist/src/rules/user-rules.d.ts +8 -0
  77. package/dist/src/rules/user-rules.js +36 -0
  78. package/dist/src/rules/user-rules.js.map +1 -0
  79. package/dist/src/skills/contracts.d.ts +4 -4
  80. package/dist/src/skills/recommended.js +1 -1
  81. package/dist/src/skills/registry.js +49 -28
  82. package/dist/src/skills/registry.js.map +1 -1
  83. package/dist/src/style/cli-ui.d.ts +14 -0
  84. package/dist/src/style/cli-ui.js +45 -1
  85. package/dist/src/style/cli-ui.js.map +1 -1
  86. package/dist/src/tui/skill-manager.js +1 -1
  87. package/dist/src/tui/skill-manager.js.map +1 -1
  88. package/dist/src/updates/update.js +9 -3
  89. package/dist/src/updates/update.js.map +1 -1
  90. package/package.json +15 -3
  91. package/scripts/rebrand-pi.mjs +6 -0
  92. package/skills/recommended/catalog.json +2 -11
  93. package/vendor/ars/.claude/CLAUDE.md +371 -0
  94. package/vendor/ars/.command-invariants.toml +24 -0
  95. package/vendor/ars/CITATION.cff +35 -0
  96. package/vendor/ars/LICENSE +417 -0
  97. package/vendor/ars/MODE_REGISTRY.md +76 -0
  98. package/vendor/ars/NOTICE.md +26 -0
  99. package/vendor/ars/POSITIONING.md +99 -0
  100. package/vendor/ars/PSYCLAW_SOURCE.json +10 -0
  101. package/vendor/ars/README.md +751 -0
  102. package/vendor/ars/SECURITY.md +52 -0
  103. package/vendor/ars/THIRD_PARTY.md +70 -0
  104. package/vendor/ars/academic-paper/SKILL.md +542 -0
  105. package/vendor/ars/academic-paper/agents/abstract_bilingual_agent.md +171 -0
  106. package/vendor/ars/academic-paper/agents/argument_builder_agent.md +276 -0
  107. package/vendor/ars/academic-paper/agents/citation_compliance_agent.md +422 -0
  108. package/vendor/ars/academic-paper/agents/draft_writer_agent.md +656 -0
  109. package/vendor/ars/academic-paper/agents/formatter_agent.md +999 -0
  110. package/vendor/ars/academic-paper/agents/intake_agent.md +393 -0
  111. package/vendor/ars/academic-paper/agents/literature_strategist_agent.md +626 -0
  112. package/vendor/ars/academic-paper/agents/peer_reviewer_agent.md +516 -0
  113. package/vendor/ars/academic-paper/agents/revision_coach_agent.md +334 -0
  114. package/vendor/ars/academic-paper/agents/socratic_mentor_agent.md +527 -0
  115. package/vendor/ars/academic-paper/agents/structure_architect_agent.md +401 -0
  116. package/vendor/ars/academic-paper/agents/visualization_agent.md +441 -0
  117. package/vendor/ars/academic-paper/examples/chinese_paper_example.md +278 -0
  118. package/vendor/ars/academic-paper/examples/clinical_citation_verification_checklist.md +95 -0
  119. package/vendor/ars/academic-paper/examples/clinical_epistemic_status_example.md +100 -0
  120. package/vendor/ars/academic-paper/examples/commitment_ledger_example.md +147 -0
  121. package/vendor/ars/academic-paper/examples/imrad_hei_example.md +234 -0
  122. package/vendor/ars/academic-paper/examples/literature_review_example.md +260 -0
  123. package/vendor/ars/academic-paper/examples/plan_mode_guided_writing.md +600 -0
  124. package/vendor/ars/academic-paper/examples/revision_mode_example.md +344 -0
  125. package/vendor/ars/academic-paper/examples/revision_recovery_example.md +506 -0
  126. package/vendor/ars/academic-paper/examples/version_family_reconciliation_example.md +89 -0
  127. package/vendor/ars/academic-paper/references/abstract_writing_guide.md +169 -0
  128. package/vendor/ars/academic-paper/references/academic_writing_style.md +188 -0
  129. package/vendor/ars/academic-paper/references/anti_leakage_protocol.md +83 -0
  130. package/vendor/ars/academic-paper/references/apa7_chinese_citation_guide.md +364 -0
  131. package/vendor/ars/academic-paper/references/apa7_extended_guide.md +198 -0
  132. package/vendor/ars/academic-paper/references/changelog.md +11 -0
  133. package/vendor/ars/academic-paper/references/citation_format_switcher.md +228 -0
  134. package/vendor/ars/academic-paper/references/committee_correspondence_protocol.md +158 -0
  135. package/vendor/ars/academic-paper/references/credit_authorship_guide.md +308 -0
  136. package/vendor/ars/academic-paper/references/disclosure_mode_protocol.md +478 -0
  137. package/vendor/ars/academic-paper/references/domain_evidence_profiles.md +38 -0
  138. package/vendor/ars/academic-paper/references/failure_paths.md +349 -0
  139. package/vendor/ars/academic-paper/references/funding_statement_guide.md +319 -0
  140. package/vendor/ars/academic-paper/references/hei_domain_glossary.md +169 -0
  141. package/vendor/ars/academic-paper/references/intro_title_rhetoric_guide.md +114 -0
  142. package/vendor/ars/academic-paper/references/journal_submission_guide.md +249 -0
  143. package/vendor/ars/academic-paper/references/latex_template_reference.md +378 -0
  144. package/vendor/ars/academic-paper/references/mode_selection_guide.md +378 -0
  145. package/vendor/ars/academic-paper/references/paper_structure_patterns.md +330 -0
  146. package/vendor/ars/academic-paper/references/plan_mode_protocol.md +112 -0
  147. package/vendor/ars/academic-paper/references/policy_anchor_disclosure_protocol.md +200 -0
  148. package/vendor/ars/academic-paper/references/policy_anchor_table.md +157 -0
  149. package/vendor/ars/academic-paper/references/revision_patch_protocol.md +173 -0
  150. package/vendor/ars/academic-paper/references/statistical_visualization_standards.md +750 -0
  151. package/vendor/ars/academic-paper/references/venue_disclosure_policies.md +259 -0
  152. package/vendor/ars/academic-paper/references/vlm_figure_verification.md +126 -0
  153. package/vendor/ars/academic-paper/references/workflow_phase_details.md +135 -0
  154. package/vendor/ars/academic-paper/references/writing_judgment_framework.md +59 -0
  155. package/vendor/ars/academic-paper/references/writing_quality_check.md +173 -0
  156. package/vendor/ars/academic-paper/templates/bilingual_abstract_template.md +78 -0
  157. package/vendor/ars/academic-paper/templates/case_study_template.md +129 -0
  158. package/vendor/ars/academic-paper/templates/conference_paper_template.md +108 -0
  159. package/vendor/ars/academic-paper/templates/credit_statement_template.md +132 -0
  160. package/vendor/ars/academic-paper/templates/funding_statement_template.md +290 -0
  161. package/vendor/ars/academic-paper/templates/imrad_template.md +183 -0
  162. package/vendor/ars/academic-paper/templates/latex_article_template.tex +199 -0
  163. package/vendor/ars/academic-paper/templates/literature_review_template.md +135 -0
  164. package/vendor/ars/academic-paper/templates/policy_brief_template.md +139 -0
  165. package/vendor/ars/academic-paper/templates/revision_tracking_template.md +199 -0
  166. package/vendor/ars/academic-paper/templates/theoretical_paper_template.md +119 -0
  167. package/vendor/ars/academic-paper-reviewer/SKILL.md +491 -0
  168. package/vendor/ars/academic-paper-reviewer/agents/devils_advocate_reviewer_agent.md +443 -0
  169. package/vendor/ars/academic-paper-reviewer/agents/domain_reviewer_agent.md +412 -0
  170. package/vendor/ars/academic-paper-reviewer/agents/editorial_synthesizer_agent.md +478 -0
  171. package/vendor/ars/academic-paper-reviewer/agents/eic_agent.md +339 -0
  172. package/vendor/ars/academic-paper-reviewer/agents/field_analyst_agent.md +221 -0
  173. package/vendor/ars/academic-paper-reviewer/agents/methodology_reviewer_agent.md +449 -0
  174. package/vendor/ars/academic-paper-reviewer/agents/perspective_reviewer_agent.md +427 -0
  175. package/vendor/ars/academic-paper-reviewer/examples/hei_paper_review_example.md +391 -0
  176. package/vendor/ars/academic-paper-reviewer/examples/interdisciplinary_review_example.md +299 -0
  177. package/vendor/ars/academic-paper-reviewer/examples/subclaim_decomposition_example.md +80 -0
  178. package/vendor/ars/academic-paper-reviewer/references/calibration_mode_protocol.md +256 -0
  179. package/vendor/ars/academic-paper-reviewer/references/changelog.md +10 -0
  180. package/vendor/ars/academic-paper-reviewer/references/editorial_decision_standards.md +236 -0
  181. package/vendor/ars/academic-paper-reviewer/references/guided_mode_protocol.md +34 -0
  182. package/vendor/ars/academic-paper-reviewer/references/integration_guide.md +15 -0
  183. package/vendor/ars/academic-paper-reviewer/references/quality_rubrics.md +84 -0
  184. package/vendor/ars/academic-paper-reviewer/references/re_review_mode_protocol.md +340 -0
  185. package/vendor/ars/academic-paper-reviewer/references/review_criteria_framework.md +98 -0
  186. package/vendor/ars/academic-paper-reviewer/references/review_panel_provenance_protocol.md +197 -0
  187. package/vendor/ars/academic-paper-reviewer/references/review_quality_thinking.md +58 -0
  188. package/vendor/ars/academic-paper-reviewer/references/reviewer_sprint_prompt_source.md +324 -0
  189. package/vendor/ars/academic-paper-reviewer/references/sprint_contract_protocol.md +296 -0
  190. package/vendor/ars/academic-paper-reviewer/references/statistical_reporting_standards.md +505 -0
  191. package/vendor/ars/academic-paper-reviewer/references/top_journals_by_field.md +206 -0
  192. package/vendor/ars/academic-paper-reviewer/templates/editorial_decision_template.md +235 -0
  193. package/vendor/ars/academic-paper-reviewer/templates/peer_review_report_template.md +305 -0
  194. package/vendor/ars/academic-paper-reviewer/templates/revision_response_template.md +248 -0
  195. package/vendor/ars/academic-pipeline/SKILL.md +736 -0
  196. package/vendor/ars/academic-pipeline/agents/claim_ref_alignment_audit_agent.md +382 -0
  197. package/vendor/ars/academic-pipeline/agents/collaboration_depth_agent.md +164 -0
  198. package/vendor/ars/academic-pipeline/agents/integrity_verification_agent.md +870 -0
  199. package/vendor/ars/academic-pipeline/agents/pipeline_orchestrator_agent.md +1379 -0
  200. package/vendor/ars/academic-pipeline/agents/state_tracker_agent.md +622 -0
  201. package/vendor/ars/academic-pipeline/examples/full_pipeline_example.md +482 -0
  202. package/vendor/ars/academic-pipeline/examples/integrity_failure_recovery.md +389 -0
  203. package/vendor/ars/academic-pipeline/examples/mid_entry_example.md +414 -0
  204. package/vendor/ars/academic-pipeline/references/adapters/.gitkeep +0 -0
  205. package/vendor/ars/academic-pipeline/references/adapters/overview.md +153 -0
  206. package/vendor/ars/academic-pipeline/references/ai_research_failure_modes.md +185 -0
  207. package/vendor/ars/academic-pipeline/references/changelog.md +14 -0
  208. package/vendor/ars/academic-pipeline/references/claim_audit_calibration_protocol.md +175 -0
  209. package/vendor/ars/academic-pipeline/references/claim_verification_protocol.md +282 -0
  210. package/vendor/ars/academic-pipeline/references/external_review_protocol.md +131 -0
  211. package/vendor/ars/academic-pipeline/references/integrity_review_protocol.md +110 -0
  212. package/vendor/ars/academic-pipeline/references/literature_corpus_consumers.md +193 -0
  213. package/vendor/ars/academic-pipeline/references/mode_advisor.md +135 -0
  214. package/vendor/ars/academic-pipeline/references/passport_as_reset_boundary.md +132 -0
  215. package/vendor/ars/academic-pipeline/references/pipeline_state_machine.md +405 -0
  216. package/vendor/ars/academic-pipeline/references/plagiarism_detection_protocol.md +239 -0
  217. package/vendor/ars/academic-pipeline/references/process_summary_protocol.md +209 -0
  218. package/vendor/ars/academic-pipeline/references/progress_dashboard_template.md +38 -0
  219. package/vendor/ars/academic-pipeline/references/reinforcement_content.md +15 -0
  220. package/vendor/ars/academic-pipeline/references/reproducibility_audit.md +55 -0
  221. package/vendor/ars/academic-pipeline/references/score_trajectory_protocol.md +78 -0
  222. package/vendor/ars/academic-pipeline/references/team_collaboration_protocol.md +261 -0
  223. package/vendor/ars/academic-pipeline/references/two_stage_review_protocol.md +27 -0
  224. package/vendor/ars/academic-pipeline/templates/pipeline_status_template.md +146 -0
  225. package/vendor/ars/agents/report_compiler_agent.md +341 -0
  226. package/vendor/ars/agents/research_architect_agent.md +298 -0
  227. package/vendor/ars/agents/synthesis_agent.md +356 -0
  228. package/vendor/ars/commands/ars-3w.md +10 -0
  229. package/vendor/ars/commands/ars-abstract.md +10 -0
  230. package/vendor/ars/commands/ars-cache-invalidate.md +20 -0
  231. package/vendor/ars/commands/ars-citation-check.md +10 -0
  232. package/vendor/ars/commands/ars-disclosure.md +10 -0
  233. package/vendor/ars/commands/ars-format-convert.md +10 -0
  234. package/vendor/ars/commands/ars-full.md +9 -0
  235. package/vendor/ars/commands/ars-lit-review.md +12 -0
  236. package/vendor/ars/commands/ars-mark-read.md +18 -0
  237. package/vendor/ars/commands/ars-outline.md +10 -0
  238. package/vendor/ars/commands/ars-plan.md +10 -0
  239. package/vendor/ars/commands/ars-rebuttal-audit.md +12 -0
  240. package/vendor/ars/commands/ars-reviewer.md +9 -0
  241. package/vendor/ars/commands/ars-revision-coach.md +9 -0
  242. package/vendor/ars/commands/ars-revision.md +10 -0
  243. package/vendor/ars/commands/ars-unmark-read.md +16 -0
  244. package/vendor/ars/deep-research/SKILL.md +600 -0
  245. package/vendor/ars/deep-research/agents/bibliography_agent.md +473 -0
  246. package/vendor/ars/deep-research/agents/devils_advocate_agent.md +192 -0
  247. package/vendor/ars/deep-research/agents/editor_in_chief_agent.md +167 -0
  248. package/vendor/ars/deep-research/agents/ethics_review_agent.md +267 -0
  249. package/vendor/ars/deep-research/agents/meta_analysis_agent.md +325 -0
  250. package/vendor/ars/deep-research/agents/monitoring_agent.md +209 -0
  251. package/vendor/ars/deep-research/agents/report_compiler_agent.md +341 -0
  252. package/vendor/ars/deep-research/agents/research_architect_agent.md +298 -0
  253. package/vendor/ars/deep-research/agents/research_question_agent.md +216 -0
  254. package/vendor/ars/deep-research/agents/risk_of_bias_agent.md +231 -0
  255. package/vendor/ars/deep-research/agents/socratic_mentor_agent.md +764 -0
  256. package/vendor/ars/deep-research/agents/source_verification_agent.md +219 -0
  257. package/vendor/ars/deep-research/agents/synthesis_agent.md +356 -0
  258. package/vendor/ars/deep-research/agents/timeline_extraction_agent.md +99 -0
  259. package/vendor/ars/deep-research/examples/exploratory_research.md +157 -0
  260. package/vendor/ars/deep-research/examples/fact_check_mode.md +173 -0
  261. package/vendor/ars/deep-research/examples/handoff_to_paper.md +318 -0
  262. package/vendor/ars/deep-research/examples/idea_diversity_coverage_gap_advisory.md +64 -0
  263. package/vendor/ars/deep-research/examples/policy_analysis.md +161 -0
  264. package/vendor/ars/deep-research/examples/review_mode.md +253 -0
  265. package/vendor/ars/deep-research/examples/socratic_guided_research.md +331 -0
  266. package/vendor/ars/deep-research/examples/systematic_review.md +133 -0
  267. package/vendor/ars/deep-research/references/apa7_style_guide.md +162 -0
  268. package/vendor/ars/deep-research/references/argumentation_reasoning_framework.md +68 -0
  269. package/vendor/ars/deep-research/references/arxiv_api_protocol.md +76 -0
  270. package/vendor/ars/deep-research/references/changelog.md +22 -0
  271. package/vendor/ars/deep-research/references/chinese_literature_api_protocol.md +317 -0
  272. package/vendor/ars/deep-research/references/cross_agent_quality_definitions.md +14 -0
  273. package/vendor/ars/deep-research/references/crossref_api_protocol.md +84 -0
  274. package/vendor/ars/deep-research/references/equator_reporting_guidelines.md +482 -0
  275. package/vendor/ars/deep-research/references/ethics_checklist.md +282 -0
  276. package/vendor/ars/deep-research/references/failure_paths.md +355 -0
  277. package/vendor/ars/deep-research/references/interdisciplinary_bridges.md +292 -0
  278. package/vendor/ars/deep-research/references/irb_decision_tree.md +315 -0
  279. package/vendor/ars/deep-research/references/literature_monitoring_strategies.md +263 -0
  280. package/vendor/ars/deep-research/references/logical_fallacies.md +192 -0
  281. package/vendor/ars/deep-research/references/methodology_patterns.md +462 -0
  282. package/vendor/ars/deep-research/references/mode_selection_guide.md +331 -0
  283. package/vendor/ars/deep-research/references/openalex_api_protocol.md +82 -0
  284. package/vendor/ars/deep-research/references/preregistration_guide.md +324 -0
  285. package/vendor/ars/deep-research/references/semantic_scholar_api_protocol.md +107 -0
  286. package/vendor/ars/deep-research/references/socratic_mode_protocol.md +99 -0
  287. package/vendor/ars/deep-research/references/socratic_questioning_framework.md +232 -0
  288. package/vendor/ars/deep-research/references/source_quality_hierarchy.md +188 -0
  289. package/vendor/ars/deep-research/references/systematic_review_protocol.md +95 -0
  290. package/vendor/ars/deep-research/references/systematic_review_toolkit.md +353 -0
  291. package/vendor/ars/deep-research/templates/evidence_assessment_template.md +127 -0
  292. package/vendor/ars/deep-research/templates/literature_matrix_template.md +85 -0
  293. package/vendor/ars/deep-research/templates/preregistration_template.md +318 -0
  294. package/vendor/ars/deep-research/templates/prisma_protocol_template.md +248 -0
  295. package/vendor/ars/deep-research/templates/prisma_report_template.md +415 -0
  296. package/vendor/ars/deep-research/templates/research_brief_template.md +93 -0
  297. package/vendor/ars/package.json +24 -0
  298. package/vendor/ars/pi/README.md +161 -0
  299. package/vendor/ars/pi/package.json +26 -0
  300. package/vendor/ars/pi/wrapper.js +193 -0
  301. package/vendor/ars/pi/wrapper.test.mjs +201 -0
  302. package/vendor/ars/pyproject.toml +2 -0
  303. package/vendor/ars/requirements-pdf-content-classifier.txt +5 -0
  304. package/vendor/ars/scripts/_block_parser.py +396 -0
  305. package/vendor/ars/scripts/_ci_pytest_manifest.toml +661 -0
  306. package/vendor/ars/scripts/_claim_audit_constants.py +268 -0
  307. package/vendor/ars/scripts/_e4_evidence.py +110 -0
  308. package/vendor/ars/scripts/_eval_threshold_gate.py +73 -0
  309. package/vendor/ars/scripts/_markdown_lint_util.py +224 -0
  310. package/vendor/ars/scripts/_next_verified_at_ms.py +176 -0
  311. package/vendor/ars/scripts/_passport_yaml.py +53 -0
  312. package/vendor/ars/scripts/_skill_lint.py +254 -0
  313. package/vendor/ars/scripts/_text_similarity.py +141 -0
  314. package/vendor/ars/scripts/adapters/README.md +89 -0
  315. package/vendor/ars/scripts/adapters/_common.py +209 -0
  316. package/vendor/ars/scripts/adapters/examples/folder_scan/expected_passport.yaml +25 -0
  317. package/vendor/ars/scripts/adapters/examples/folder_scan/expected_rejection_log.yaml +18 -0
  318. package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/Chen2024_AIAssessment.pdf +0 -0
  319. package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/Wang_2023_formative_feedback.pdf +0 -0
  320. package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/paper1.pdf +0 -0
  321. 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
  322. package/vendor/ars/scripts/adapters/examples/obsidian/expected_passport.yaml +40 -0
  323. package/vendor/ars/scripts/adapters/examples/obsidian/expected_rejection_log.yaml +13 -0
  324. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/.gitkeep +0 -0
  325. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/.obsidian/app.json +1 -0
  326. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/_templates/tmpl.md +7 -0
  327. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/chen2024ai.md +13 -0
  328. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/invalid.md +1 -0
  329. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/wang2023formative.md +10 -0
  330. package/vendor/ars/scripts/adapters/examples/zotero/expected_passport.yaml +34 -0
  331. package/vendor/ars/scripts/adapters/examples/zotero/expected_rejection_log.yaml +34 -0
  332. package/vendor/ars/scripts/adapters/examples/zotero/input_fixture/.gitkeep +0 -0
  333. package/vendor/ars/scripts/adapters/examples/zotero/input_fixture/export.json +43 -0
  334. package/vendor/ars/scripts/adapters/folder_scan.py +214 -0
  335. package/vendor/ars/scripts/adapters/obsidian.py +336 -0
  336. package/vendor/ars/scripts/adapters/tests/.gitkeep +0 -0
  337. package/vendor/ars/scripts/adapters/tests/conftest.py +77 -0
  338. package/vendor/ars/scripts/adapters/tests/test_check_corpus_consumer_protocol.py +632 -0
  339. package/vendor/ars/scripts/adapters/tests/test_check_literature_corpus_schema.py +631 -0
  340. package/vendor/ars/scripts/adapters/tests/test_common.py +365 -0
  341. package/vendor/ars/scripts/adapters/tests/test_conftest.py +96 -0
  342. package/vendor/ars/scripts/adapters/tests/test_folder_scan.py +260 -0
  343. package/vendor/ars/scripts/adapters/tests/test_literature_corpus_entry_schema.py +745 -0
  344. package/vendor/ars/scripts/adapters/tests/test_obsidian.py +357 -0
  345. package/vendor/ars/scripts/adapters/tests/test_rejection_log_schema.py +271 -0
  346. package/vendor/ars/scripts/adapters/tests/test_sync_adapter_docs.py +88 -0
  347. package/vendor/ars/scripts/adapters/tests/test_zotero.py +454 -0
  348. package/vendor/ars/scripts/adapters/zotero.py +318 -0
  349. package/vendor/ars/scripts/adjudication_activity.py +1592 -0
  350. package/vendor/ars/scripts/announce-ars-loaded.sh +144 -0
  351. package/vendor/ars/scripts/ars_anchorize_draft.py +170 -0
  352. package/vendor/ars/scripts/ars_apply_revision_patch.py +912 -0
  353. package/vendor/ars/scripts/ars_cache_invalidate.py +40 -0
  354. package/vendor/ars/scripts/ars_mark_read.py +521 -0
  355. package/vendor/ars/scripts/ars_phase_scope_manifest.json +33 -0
  356. package/vendor/ars/scripts/ars_update_check.sh +215 -0
  357. package/vendor/ars/scripts/ars_write_scope_guard.py +506 -0
  358. package/vendor/ars/scripts/arxiv_client.py +222 -0
  359. package/vendor/ars/scripts/audit_snapshot.py +572 -0
  360. package/vendor/ars/scripts/bibliographic_integrity_signals.py +800 -0
  361. package/vendor/ars/scripts/bootstrap_timeline_yaml.py +146 -0
  362. package/vendor/ars/scripts/build_claim_standing_candidate_ledger.py +1238 -0
  363. package/vendor/ars/scripts/build_claim_standing_query_plan.py +643 -0
  364. package/vendor/ars/scripts/build_content_coverage_advisory.py +1205 -0
  365. package/vendor/ars/scripts/build_cross_document_consistency_advisory.py +2332 -0
  366. package/vendor/ars/scripts/build_review_pathway_rule_trace.py +830 -0
  367. package/vendor/ars/scripts/build_submission_packet_manifest.py +2310 -0
  368. package/vendor/ars/scripts/check_215_field_norm.py +173 -0
  369. package/vendor/ars/scripts/check_216_surface_form.py +250 -0
  370. package/vendor/ars/scripts/check_268_nested_commitment_ledger.py +180 -0
  371. package/vendor/ars/scripts/check_390_revision_patch_discipline.py +296 -0
  372. package/vendor/ars/scripts/check_392_citation_verification_intake.py +126 -0
  373. package/vendor/ars/scripts/check_394_submission_policy.py +178 -0
  374. package/vendor/ars/scripts/check_439_format_profile.py +307 -0
  375. package/vendor/ars/scripts/check_619_disclosure_closeout.py +237 -0
  376. package/vendor/ars/scripts/check_630_codex_subscription_transport.py +456 -0
  377. package/vendor/ars/scripts/check_669_review_pathway_rule_trace.py +662 -0
  378. package/vendor/ars/scripts/check_670_revision_roadmap_integration.py +518 -0
  379. package/vendor/ars/scripts/check_673_adjudication_activity.py +684 -0
  380. package/vendor/ars/scripts/check_684_review_criteria_binding.py +557 -0
  381. package/vendor/ars/scripts/check_agents_mirror_sync.py +115 -0
  382. package/vendor/ars/scripts/check_audit_artifact_consistency.py +2313 -0
  383. package/vendor/ars/scripts/check_benchmark_report.py +79 -0
  384. package/vendor/ars/scripts/check_bibliographic_integrity_signals.py +831 -0
  385. package/vendor/ars/scripts/check_calibration_tiers.py +235 -0
  386. package/vendor/ars/scripts/check_changelog_covers_merges.py +289 -0
  387. package/vendor/ars/scripts/check_ci_pytest_manifest.py +204 -0
  388. package/vendor/ars/scripts/check_claim_audit_consistency.py +1664 -0
  389. package/vendor/ars/scripts/check_claim_standing_candidate_ledger_integration.py +500 -0
  390. package/vendor/ars/scripts/check_claim_standing_freshness.py +253 -0
  391. package/vendor/ars/scripts/check_claim_standing_transmissions.py +449 -0
  392. package/vendor/ars/scripts/check_collaboration_depth_rubric.py +180 -0
  393. package/vendor/ars/scripts/check_command_frontmatter_name.py +116 -0
  394. package/vendor/ars/scripts/check_committee_correspondence.py +333 -0
  395. package/vendor/ars/scripts/check_compliance_report.py +108 -0
  396. package/vendor/ars/scripts/check_content_coverage_advisory_integration.py +796 -0
  397. package/vendor/ars/scripts/check_control_availability.py +172 -0
  398. package/vendor/ars/scripts/check_corpus_consumer_protocol.py +404 -0
  399. package/vendor/ars/scripts/check_cross_document_consistency_advisory_integration.py +1191 -0
  400. package/vendor/ars/scripts/check_cross_model_handoff_contract.py +234 -0
  401. package/vendor/ars/scripts/check_cross_model_verification_sync.py +261 -0
  402. package/vendor/ars/scripts/check_data_access_level.py +131 -0
  403. package/vendor/ars/scripts/check_data_flows.py +252 -0
  404. package/vendor/ars/scripts/check_decision_contract.py +464 -0
  405. package/vendor/ars/scripts/check_degradation_registry.py +326 -0
  406. package/vendor/ars/scripts/check_distribution_surface_claims.py +226 -0
  407. package/vendor/ars/scripts/check_domain_evidence_profile.py +538 -0
  408. package/vendor/ars/scripts/check_e4_promotion.py +195 -0
  409. package/vendor/ars/scripts/check_evals_gold_set.py +279 -0
  410. package/vendor/ars/scripts/check_evidence_row_integration.py +396 -0
  411. package/vendor/ars/scripts/check_experiment_provenance.py +117 -0
  412. package/vendor/ars/scripts/check_field_norm_severity.py +144 -0
  413. package/vendor/ars/scripts/check_firm_rules_sync.py +375 -0
  414. package/vendor/ars/scripts/check_heldout_measurement_report.py +1179 -0
  415. package/vendor/ars/scripts/check_human_subjects_output_contract.py +139 -0
  416. package/vendor/ars/scripts/check_human_subjects_reference_migration.py +844 -0
  417. package/vendor/ars/scripts/check_indirect_prompt_injection_no_call.py +328 -0
  418. package/vendor/ars/scripts/check_instruction_data_boundary.py +236 -0
  419. package/vendor/ars/scripts/check_judge_prompt_version.py +125 -0
  420. package/vendor/ars/scripts/check_literature_corpus_schema.py +402 -0
  421. package/vendor/ars/scripts/check_model_tiering.py +223 -0
  422. package/vendor/ars/scripts/check_panel_synthesis.py +1331 -0
  423. package/vendor/ars/scripts/check_passport_reset_contract.py +214 -0
  424. package/vendor/ars/scripts/check_pattern_eval_manifest.py +422 -0
  425. package/vendor/ars/scripts/check_persuasion_invariance_fixtures.py +637 -0
  426. package/vendor/ars/scripts/check_phase_conformance.py +2180 -0
  427. package/vendor/ars/scripts/check_pipeline_boundary_semantics.py +600 -0
  428. package/vendor/ars/scripts/check_pipeline_integrity.py +340 -0
  429. package/vendor/ars/scripts/check_policy_anchor_protocol.py +159 -0
  430. package/vendor/ars/scripts/check_policy_anchor_table.py +286 -0
  431. package/vendor/ars/scripts/check_preprint_venues_consistency.py +123 -0
  432. package/vendor/ars/scripts/check_prisma_trAIce_freshness.py +84 -0
  433. package/vendor/ars/scripts/check_promotion_bakeoff_preregistration.py +1303 -0
  434. package/vendor/ars/scripts/check_ranking_lift.py +323 -0
  435. package/vendor/ars/scripts/check_re_review_synthesis.py +2719 -0
  436. package/vendor/ars/scripts/check_receipt_enum_sync.py +203 -0
  437. package/vendor/ars/scripts/check_repro_lock.py +85 -0
  438. package/vendor/ars/scripts/check_review_pathway_output.py +276 -0
  439. package/vendor/ars/scripts/check_reviewer_data_fences.py +224 -0
  440. package/vendor/ars/scripts/check_reviewer_finding_contract.py +783 -0
  441. package/vendor/ars/scripts/check_reviewer_role_label.py +447 -0
  442. package/vendor/ars/scripts/check_reviewer_scoring_honesty.py +292 -0
  443. package/vendor/ars/scripts/check_reviewer_sprint_prompt_sync.py +409 -0
  444. package/vendor/ars/scripts/check_revision_claim_drift_suite_v2.py +1622 -0
  445. package/vendor/ars/scripts/check_revision_token_conservation.py +263 -0
  446. package/vendor/ars/scripts/check_risk_register.py +280 -0
  447. package/vendor/ars/scripts/check_role_scoped_contract.py +702 -0
  448. package/vendor/ars/scripts/check_rq_framing_patterns.py +184 -0
  449. package/vendor/ars/scripts/check_rubric_weight_consistency.py +32 -0
  450. package/vendor/ars/scripts/check_seeded_defect_fixtures.py +425 -0
  451. package/vendor/ars/scripts/check_setup_cross_model_parity.py +139 -0
  452. package/vendor/ars/scripts/check_spec_consistency.py +1249 -0
  453. package/vendor/ars/scripts/check_sprint_contract.py +371 -0
  454. package/vendor/ars/scripts/check_stage_capability_matrix.py +784 -0
  455. package/vendor/ars/scripts/check_submission_packet_manifest_integration.py +358 -0
  456. package/vendor/ars/scripts/check_surface_form_parity.py +445 -0
  457. package/vendor/ars/scripts/check_task_type.py +22 -0
  458. package/vendor/ars/scripts/check_tools_allowlist.py +538 -0
  459. package/vendor/ars/scripts/check_tortured_phrase_screening_integration.py +1974 -0
  460. package/vendor/ars/scripts/check_v3_10_134_write_scope.py +286 -0
  461. package/vendor/ars/scripts/check_v3_10_policy.py +656 -0
  462. package/vendor/ars/scripts/check_v3_6_6_ab_manifest.py +364 -0
  463. package/vendor/ars/scripts/check_v3_6_7_pattern_protection.py +1366 -0
  464. package/vendor/ars/scripts/check_v3_6_8_audit_scope_block.py +464 -0
  465. package/vendor/ars/scripts/check_v3_6_8_cite_provenance_pipeline.py +231 -0
  466. package/vendor/ars/scripts/check_v3_6_8_frontmatter_trust_schema.py +248 -0
  467. package/vendor/ars/scripts/check_v3_6_8_mark_read_commands.py +79 -0
  468. package/vendor/ars/scripts/check_v3_6_8_pattern_protection.py +941 -0
  469. package/vendor/ars/scripts/check_v3_7_3_three_layer_citation.py +318 -0
  470. package/vendor/ars/scripts/check_v3_8_annotation_literal_sync.py +228 -0
  471. package/vendor/ars/scripts/check_v3_9_0_triangulation.py +366 -0
  472. package/vendor/ars/scripts/check_v3_9_2_phase_boundary.py +270 -0
  473. package/vendor/ars/scripts/check_v3_9_4_temporal_verification.py +139 -0
  474. package/vendor/ars/scripts/check_venue_disclosure_policies.py +63 -0
  475. package/vendor/ars/scripts/check_version_consistency.py +826 -0
  476. package/vendor/ars/scripts/check_workflow_classification.py +223 -0
  477. package/vendor/ars/scripts/chinese_literature_client.py +1938 -0
  478. package/vendor/ars/scripts/citation_verification_summary.py +85 -0
  479. package/vendor/ars/scripts/claim_audit_calibration.py +517 -0
  480. package/vendor/ars/scripts/claim_audit_finalizer.py +456 -0
  481. package/vendor/ars/scripts/claim_audit_pipeline.py +1594 -0
  482. package/vendor/ars/scripts/claim_registry_coverage.py +493 -0
  483. package/vendor/ars/scripts/claim_standing_discovery.py +784 -0
  484. package/vendor/ars/scripts/claim_standing_stance_runner.py +758 -0
  485. package/vendor/ars/scripts/claim_standing_stance_scorer.py +239 -0
  486. package/vendor/ars/scripts/claim_strength_drift_disposition.py +666 -0
  487. package/vendor/ars/scripts/contamination_signals.py +689 -0
  488. package/vendor/ars/scripts/corpus_consumer_manifest.json +19 -0
  489. package/vendor/ars/scripts/cross_model_codex_transport.py +1374 -0
  490. package/vendor/ars/scripts/cross_model_codex_verify.sh +6 -0
  491. package/vendor/ars/scripts/cross_model_handoff.py +359 -0
  492. package/vendor/ars/scripts/cross_model_smoke_test.sh +183 -0
  493. package/vendor/ars/scripts/cross_model_smoke_test_codex.sh +35 -0
  494. package/vendor/ars/scripts/cross_model_verification/gemini_is_grounded.jq +45 -0
  495. package/vendor/ars/scripts/cross_model_verification/gemini_sources.jq +45 -0
  496. package/vendor/ars/scripts/cross_model_verification/normalize_compat_verdict.py +57 -0
  497. package/vendor/ars/scripts/cross_model_verification/openai_has_completed_web_search.jq +15 -0
  498. package/vendor/ars/scripts/cross_model_verification/openai_sources.jq +17 -0
  499. package/vendor/ars/scripts/cross_model_verification/openai_text.jq +11 -0
  500. package/vendor/ars/scripts/crossref_client.py +225 -0
  501. package/vendor/ars/scripts/dispatch_e4_panel.py +2731 -0
  502. package/vendor/ars/scripts/evidence_rows.py +2043 -0
  503. package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3.json +32 -0
  504. package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_input.json +27 -0
  505. package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_prime.json +32 -0
  506. package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_prime_input.json +27 -0
  507. package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_override.json +50 -0
  508. package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_override_action.json +14 -0
  509. package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_pass.json +44 -0
  510. package/vendor/ars/scripts/fixtures/adjudication_activity/explicit_user_request_log.json +26 -0
  511. package/vendor/ars/scripts/fixtures/adjudication_activity/mandatory_checkpoint_log.json +116 -0
  512. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/README.md +56 -0
  513. 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
  514. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
  515. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
  516. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.verdict.yaml +19 -0
  517. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a7_orphan_completion/2026-04-30T15-22-04Z-d8f3.jsonl +3 -0
  518. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.audit_artifact_entry.json +26 -0
  519. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
  520. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
  521. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.verdict.yaml +19 -0
  522. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.audit_artifact_entry.json +24 -0
  523. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
  524. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
  525. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.verdict.yaml +12 -0
  526. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/retraction.json +46 -0
  527. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/retraction_check_attestation.json +44 -0
  528. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase.json +44 -0
  529. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase_v1_2_abstract_missing.json +107 -0
  530. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase_v1_2_detected.json +124 -0
  531. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/expected_outcomes.json +77 -0
  532. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/manifest.yaml +37 -0
  533. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/001-valid-doi-test.json +20 -0
  534. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/002-valid-arxiv-test.json +19 -0
  535. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/003-fabricated-test.json +20 -0
  536. package/vendor/ars/scripts/fixtures/claim_audit_calibration/gold_set.json +344 -0
  537. package/vendor/ars/scripts/fixtures/claim_standing_candidate_ledger/query_plan.json +108 -0
  538. package/vendor/ars/scripts/fixtures/claim_standing_candidate_ledger/retrieval_input.json +179 -0
  539. package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/concern_tracker.json +129 -0
  540. package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/response_skeleton.md +19 -0
  541. package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/source_letter.txt +9 -0
  542. package/vendor/ars/scripts/fixtures/content_coverage_advisory/base_draft.json +48 -0
  543. package/vendor/ars/scripts/fixtures/content_coverage_advisory/base_inventory.json +38 -0
  544. package/vendor/ars/scripts/fixtures/content_coverage_advisory/packet/consent.txt +1 -0
  545. package/vendor/ars/scripts/fixtures/content_coverage_advisory/session_sources.json +3 -0
  546. package/vendor/ars/scripts/fixtures/cross_document_consistency/README.md +6 -0
  547. package/vendor/ars/scripts/fixtures/cross_document_consistency/accepted_draft.md +35 -0
  548. package/vendor/ars/scripts/fixtures/cross_document_consistency/cases.json +75 -0
  549. package/vendor/ars/scripts/fixtures/cross_document_consistency/preregistration.md +5 -0
  550. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/forbidden_event.jsonl +4 -0
  551. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/grounded_verified.jsonl +3 -0
  552. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/malformed.jsonl +3 -0
  553. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/missing_search.jsonl +2 -0
  554. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/multiple_finals.jsonl +4 -0
  555. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/not_found.jsonl +3 -0
  556. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/unbound_source.jsonl +3 -0
  557. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/wrong_search_shape.jsonl +3 -0
  558. package/vendor/ars/scripts/fixtures/evidence_rows/phase_e_inputs.json +114 -0
  559. package/vendor/ars/scripts/fixtures/evidence_rows/session_sources.json +5 -0
  560. package/vendor/ars/scripts/fixtures/human_subjects_authority/cross-border-us-tw-gdpr.json +71 -0
  561. package/vendor/ars/scripts/fixtures/human_subjects_authority/gdpr-member-state-unresolved.json +62 -0
  562. package/vendor/ars/scripts/fixtures/human_subjects_authority/missing-data-axis.json +45 -0
  563. package/vendor/ars/scripts/fixtures/human_subjects_authority/no-profile.json +33 -0
  564. package/vendor/ars/scripts/fixtures/human_subjects_authority/tw-gdpr-two-axis.json +74 -0
  565. package/vendor/ars/scripts/fixtures/human_subjects_authority/us-gdpr-two-axis.json +74 -0
  566. package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/lint-near-misses.json +72 -0
  567. package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/no-profile-request.json +18 -0
  568. package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/tw-candidates-request.json +63 -0
  569. package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/us-candidates-request.json +77 -0
  570. package/vendor/ars/scripts/fixtures/review_target_context/exact-declaration.json +24 -0
  571. package/vendor/ars/scripts/fixtures/review_target_context/field-general-declaration.json +24 -0
  572. package/vendor/ars/scripts/fixtures/review_target_context/msr-2027-technical-full-declaration.json +24 -0
  573. package/vendor/ars/scripts/fixtures/review_target_context/synthetic-registry.json +92 -0
  574. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/README.md +21 -0
  575. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_attested_only.json +55 -0
  576. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_machine_supported.json +49 -0
  577. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_not_isolated.json +56 -0
  578. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_unknown.json +49 -0
  579. package/vendor/ars/scripts/fixtures/submission_package/clean/paper.md +16 -0
  580. package/vendor/ars/scripts/fixtures/submission_package/clean/references.bib +13 -0
  581. package/vendor/ars/scripts/fixtures/submission_package/fallback_authoryear/paper.md +11 -0
  582. package/vendor/ars/scripts/fixtures/submission_package/fallback_authoryear/references.bib +13 -0
  583. package/vendor/ars/scripts/fixtures/submission_package/fallback_latex/paper.tex +9 -0
  584. package/vendor/ars/scripts/fixtures/submission_package/fallback_latex/references.bib +13 -0
  585. package/vendor/ars/scripts/fixtures/submission_package/marker_no_join/paper.md +7 -0
  586. package/vendor/ars/scripts/fixtures/submission_package/orphan_intext/paper.md +8 -0
  587. package/vendor/ars/scripts/fixtures/submission_package/orphan_intext/references.bib +6 -0
  588. package/vendor/ars/scripts/fixtures/submission_package/passports/corpus_only.yaml +9 -0
  589. package/vendor/ars/scripts/fixtures/submission_package/passports/summary_join.yaml +16 -0
  590. package/vendor/ars/scripts/fixtures/submission_package/profiles/full.yaml +15 -0
  591. package/vendor/ars/scripts/fixtures/submission_package/profiles/tight.yaml +15 -0
  592. package/vendor/ars/scripts/fixtures/submission_package/summary_join/paper.md +7 -0
  593. package/vendor/ars/scripts/fixtures/submission_package/uncited_reference/paper.md +9 -0
  594. package/vendor/ars/scripts/fixtures/submission_package/uncited_reference/references.bib +13 -0
  595. package/vendor/ars/scripts/fixtures/submission_package/venue_clean/paper.md +26 -0
  596. package/vendor/ars/scripts/fixtures/submission_package/venue_clean/references.bib +13 -0
  597. package/vendor/ars/scripts/fixtures/submission_package/venue_violations/paper.md +20 -0
  598. package/vendor/ars/scripts/fixtures/submission_package/venue_violations/references.bib +13 -0
  599. package/vendor/ars/scripts/fixtures/submission_packet_manifest/base_inventory.json +101 -0
  600. package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/consent-materials.txt +3 -0
  601. package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/training-certificate.txt +2 -0
  602. package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/tw-consent-materials.txt +3 -0
  603. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/corpus_input.yaml +30 -0
  604. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/own_draft.md +36 -0
  605. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/own_draft.tex +21 -0
  606. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/seed_expectations.json +218 -0
  607. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/snapshot.json +164 -0
  608. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/snapshot_manifest.json +29 -0
  609. package/vendor/ars/scripts/fixtures/transport_bodies/README.md +55 -0
  610. package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/empty_feed.xml +10 -0
  611. package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/error_5xx.html +7 -0
  612. package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/id_hit.xml +26 -0
  613. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/README.md +69 -0
  614. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/cnki_landing_page.html +13 -0
  615. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/error_5xx.html +2 -0
  616. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_ambiguous.json +16 -0
  617. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_hit.json +16 -0
  618. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_zero.json +14 -0
  619. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coverage_hit.json +15 -0
  620. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coverage_zero.json +13 -0
  621. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_hit.json +43 -0
  622. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_issn_mismatch.json +31 -0
  623. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_no_doi.json +31 -0
  624. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_unknown_ra.json +34 -0
  625. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_year_mismatch.json +31 -0
  626. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_absent.json +4 -0
  627. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_exists.json +16 -0
  628. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_internal_error.json +4 -0
  629. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/istic_csl_hit.json +25 -0
  630. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/istic_csl_other_title.json +18 -0
  631. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_cnki.json +6 -0
  632. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_crossref.json +6 -0
  633. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_istic.json +6 -0
  634. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_unknown_prefix.json +6 -0
  635. package/vendor/ars/scripts/fixtures/transport_bodies/crossref/doi_hit.json +24 -0
  636. package/vendor/ars/scripts/fixtures/transport_bodies/crossref/error_5xx.html +7 -0
  637. package/vendor/ars/scripts/fixtures/transport_bodies/crossref/title_search_miss.json +15 -0
  638. package/vendor/ars/scripts/fixtures/transport_bodies/openalex/doi_hit.json +29 -0
  639. package/vendor/ars/scripts/fixtures/transport_bodies/openalex/error_5xx.json +4 -0
  640. package/vendor/ars/scripts/fixtures/transport_bodies/openalex/title_search_miss.json +11 -0
  641. package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/doi_hit.json +14 -0
  642. package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/error_5xx.json +3 -0
  643. package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/title_search_miss.json +5 -0
  644. package/vendor/ars/scripts/human_read_attestation_resolver.py +467 -0
  645. package/vendor/ars/scripts/ideation_diversity_assignment_gate.py +430 -0
  646. package/vendor/ars/scripts/inquiry_branch_ledger.py +2540 -0
  647. package/vendor/ars/scripts/legacy/ars_apply_revision_patch_v1_0.py +715 -0
  648. package/vendor/ars/scripts/legacy/check_re_review_synthesis_v1_0.py +2215 -0
  649. package/vendor/ars/scripts/migrate_literature_corpus_to_v3_10.py +223 -0
  650. package/vendor/ars/scripts/migrate_literature_corpus_to_v3_7_3.py +277 -0
  651. package/vendor/ars/scripts/migrate_literature_corpus_to_v3_9_0.py +304 -0
  652. package/vendor/ars/scripts/model_tiering_manifest.json +46 -0
  653. package/vendor/ars/scripts/openalex_client.py +232 -0
  654. package/vendor/ars/scripts/parse_audit_verdict.py +802 -0
  655. package/vendor/ars/scripts/pdf_content_classifier_worker.py +176 -0
  656. package/vendor/ars/scripts/pdf_read_preflight.py +1455 -0
  657. package/vendor/ars/scripts/policy_anchor_disclosure_referee.py +354 -0
  658. package/vendor/ars/scripts/recompute_receipts.py +1414 -0
  659. package/vendor/ars/scripts/render_claim_standing_view.py +374 -0
  660. package/vendor/ars/scripts/render_eval_comment.py +130 -0
  661. package/vendor/ars/scripts/render_harness_retirement_issue.py +167 -0
  662. package/vendor/ars/scripts/repro_lock_validation.py +90 -0
  663. package/vendor/ars/scripts/research_workflow_profile.py +1079 -0
  664. package/vendor/ars/scripts/resolve_human_subjects_authority.py +1158 -0
  665. package/vendor/ars/scripts/resolve_review_target_context.py +683 -0
  666. package/vendor/ars/scripts/resume_e4_record.py +509 -0
  667. package/vendor/ars/scripts/retraction_status.py +484 -0
  668. package/vendor/ars/scripts/review_criteria_binding.py +889 -0
  669. package/vendor/ars/scripts/review_panel_provenance.py +744 -0
  670. package/vendor/ars/scripts/revision_roadmap.py +1967 -0
  671. package/vendor/ars/scripts/run_ci_pytest_manifest.py +116 -0
  672. package/vendor/ars/scripts/run_codex_audit.sh +1191 -0
  673. package/vendor/ars/scripts/run_evals.py +513 -0
  674. package/vendor/ars/scripts/run_ideation_diversity_no_call.py +3505 -0
  675. package/vendor/ars/scripts/run_indirect_prompt_injection_no_call.py +3330 -0
  676. package/vendor/ars/scripts/run_indirect_prompt_injection_probe.py +399 -0
  677. package/vendor/ars/scripts/run_review_criteria_constructive_value.py +1895 -0
  678. package/vendor/ars/scripts/run_role_topology_utility_dry_run.py +606 -0
  679. package/vendor/ars/scripts/score_review_criteria_constructive_value.py +617 -0
  680. package/vendor/ars/scripts/semantic_scholar_client.py +291 -0
  681. package/vendor/ars/scripts/slr_lineage.py +59 -0
  682. package/vendor/ars/scripts/sync_adapter_docs.py +118 -0
  683. package/vendor/ars/scripts/temporal_integrity_audit.py +840 -0
  684. package/vendor/ars/scripts/test_431_exact_or_bust.py +253 -0
  685. package/vendor/ars/scripts/test__eval_threshold_gate.py +125 -0
  686. package/vendor/ars/scripts/test__markdown_lint_util.py +129 -0
  687. package/vendor/ars/scripts/test__next_verified_at_ms.py +256 -0
  688. package/vendor/ars/scripts/test_adjacent_framing_probe_lint.py +171 -0
  689. package/vendor/ars/scripts/test_adjudication_activity.py +1515 -0
  690. package/vendor/ars/scripts/test_ars_anchorize_draft.py +178 -0
  691. package/vendor/ars/scripts/test_ars_apply_revision_patch.py +1335 -0
  692. package/vendor/ars/scripts/test_ars_cache_invalidate.py +51 -0
  693. package/vendor/ars/scripts/test_ars_mark_read.py +910 -0
  694. package/vendor/ars/scripts/test_ars_update_check.py +816 -0
  695. package/vendor/ars/scripts/test_ars_write_scope_guard.py +790 -0
  696. package/vendor/ars/scripts/test_arxiv_client.py +374 -0
  697. package/vendor/ars/scripts/test_audit_schemas.py +560 -0
  698. package/vendor/ars/scripts/test_audit_snapshot_render_section_0.py +105 -0
  699. package/vendor/ars/scripts/test_block_parser.py +259 -0
  700. package/vendor/ars/scripts/test_bootstrap_timeline_yaml.py +148 -0
  701. package/vendor/ars/scripts/test_build_claim_standing_candidate_ledger.py +1203 -0
  702. package/vendor/ars/scripts/test_build_claim_standing_query_plan.py +791 -0
  703. package/vendor/ars/scripts/test_build_submission_packet_manifest.py +2644 -0
  704. package/vendor/ars/scripts/test_check_215_field_norm.py +238 -0
  705. package/vendor/ars/scripts/test_check_216_surface_form.py +341 -0
  706. package/vendor/ars/scripts/test_check_268_nested_commitment_ledger.py +194 -0
  707. package/vendor/ars/scripts/test_check_390_revision_patch_discipline.py +279 -0
  708. package/vendor/ars/scripts/test_check_392_citation_verification_intake.py +144 -0
  709. package/vendor/ars/scripts/test_check_394_submission_policy.py +200 -0
  710. package/vendor/ars/scripts/test_check_439_format_profile.py +252 -0
  711. package/vendor/ars/scripts/test_check_619_disclosure_closeout.py +158 -0
  712. package/vendor/ars/scripts/test_check_630_codex_subscription_transport.py +245 -0
  713. package/vendor/ars/scripts/test_check_669_review_pathway_rule_trace.py +304 -0
  714. package/vendor/ars/scripts/test_check_670_revision_roadmap_integration.py +260 -0
  715. package/vendor/ars/scripts/test_check_673_adjudication_activity.py +367 -0
  716. package/vendor/ars/scripts/test_check_684_review_criteria_binding.py +274 -0
  717. package/vendor/ars/scripts/test_check_agents_mirror_sync.py +137 -0
  718. package/vendor/ars/scripts/test_check_audit_artifact_consistency.py +2133 -0
  719. package/vendor/ars/scripts/test_check_benchmark_report.py +117 -0
  720. package/vendor/ars/scripts/test_check_bibliographic_integrity_signals.py +609 -0
  721. package/vendor/ars/scripts/test_check_calibration_tiers.py +376 -0
  722. package/vendor/ars/scripts/test_check_changelog_covers_merges.py +509 -0
  723. package/vendor/ars/scripts/test_check_ci_pytest_manifest.py +425 -0
  724. package/vendor/ars/scripts/test_check_claim_standing_candidate_ledger_integration.py +256 -0
  725. package/vendor/ars/scripts/test_check_claim_standing_freshness.py +277 -0
  726. package/vendor/ars/scripts/test_check_collaboration_depth_rubric.py +239 -0
  727. package/vendor/ars/scripts/test_check_command_frontmatter_name.py +178 -0
  728. package/vendor/ars/scripts/test_check_committee_correspondence.py +298 -0
  729. package/vendor/ars/scripts/test_check_compliance_report.py +381 -0
  730. package/vendor/ars/scripts/test_check_content_coverage_advisory_integration.py +308 -0
  731. package/vendor/ars/scripts/test_check_control_availability.py +361 -0
  732. package/vendor/ars/scripts/test_check_cross_document_consistency_advisory_integration.py +698 -0
  733. package/vendor/ars/scripts/test_check_cross_model_handoff_contract.py +301 -0
  734. package/vendor/ars/scripts/test_check_cross_model_verification_sync.py +203 -0
  735. package/vendor/ars/scripts/test_check_data_access_level.py +227 -0
  736. package/vendor/ars/scripts/test_check_data_flows.py +382 -0
  737. package/vendor/ars/scripts/test_check_decision_contract.py +387 -0
  738. package/vendor/ars/scripts/test_check_degradation_registry.py +270 -0
  739. package/vendor/ars/scripts/test_check_distribution_surface_claims.py +225 -0
  740. package/vendor/ars/scripts/test_check_domain_evidence_profile.py +439 -0
  741. package/vendor/ars/scripts/test_check_e4_promotion.py +163 -0
  742. package/vendor/ars/scripts/test_check_evals_gold_set.py +312 -0
  743. package/vendor/ars/scripts/test_check_evidence_row_integration.py +187 -0
  744. package/vendor/ars/scripts/test_check_field_norm_severity.py +173 -0
  745. package/vendor/ars/scripts/test_check_firm_rules_sync.py +342 -0
  746. package/vendor/ars/scripts/test_check_heldout_measurement_report.py +1508 -0
  747. package/vendor/ars/scripts/test_check_human_subjects_output_contract.py +129 -0
  748. package/vendor/ars/scripts/test_check_human_subjects_reference_migration.py +756 -0
  749. package/vendor/ars/scripts/test_check_instruction_data_boundary.py +204 -0
  750. package/vendor/ars/scripts/test_check_judge_prompt_version.py +90 -0
  751. package/vendor/ars/scripts/test_check_model_tiering.py +236 -0
  752. package/vendor/ars/scripts/test_check_panel_synthesis.py +1658 -0
  753. package/vendor/ars/scripts/test_check_passport_reset_contract.py +249 -0
  754. package/vendor/ars/scripts/test_check_pattern_eval_manifest.py +381 -0
  755. package/vendor/ars/scripts/test_check_persuasion_invariance_fixtures.py +591 -0
  756. package/vendor/ars/scripts/test_check_phase_conformance.py +4366 -0
  757. package/vendor/ars/scripts/test_check_pipeline_boundary_semantics.py +931 -0
  758. package/vendor/ars/scripts/test_check_pipeline_integrity.py +243 -0
  759. package/vendor/ars/scripts/test_check_policy_anchor_protocol.py +236 -0
  760. package/vendor/ars/scripts/test_check_policy_anchor_table.py +295 -0
  761. package/vendor/ars/scripts/test_check_prisma_trAIce_freshness.py +68 -0
  762. package/vendor/ars/scripts/test_check_promotion_bakeoff_preregistration.py +799 -0
  763. package/vendor/ars/scripts/test_check_ranking_lift.py +377 -0
  764. package/vendor/ars/scripts/test_check_re_review_synthesis.py +3398 -0
  765. package/vendor/ars/scripts/test_check_receipt_enum_sync.py +176 -0
  766. package/vendor/ars/scripts/test_check_repro_lock.py +107 -0
  767. package/vendor/ars/scripts/test_check_reviewer_data_fences.py +334 -0
  768. package/vendor/ars/scripts/test_check_reviewer_finding_contract.py +1107 -0
  769. package/vendor/ars/scripts/test_check_reviewer_role_label.py +491 -0
  770. package/vendor/ars/scripts/test_check_reviewer_scoring_honesty.py +222 -0
  771. package/vendor/ars/scripts/test_check_reviewer_sprint_prompt_sync.py +373 -0
  772. package/vendor/ars/scripts/test_check_revision_claim_drift_suite_v2.py +1925 -0
  773. package/vendor/ars/scripts/test_check_revision_token_conservation.py +398 -0
  774. package/vendor/ars/scripts/test_check_risk_register.py +336 -0
  775. package/vendor/ars/scripts/test_check_role_scoped_contract.py +1104 -0
  776. package/vendor/ars/scripts/test_check_rq_framing_patterns.py +110 -0
  777. package/vendor/ars/scripts/test_check_rubric_weight_consistency.py +32 -0
  778. package/vendor/ars/scripts/test_check_seeded_defect_fixtures.py +392 -0
  779. package/vendor/ars/scripts/test_check_setup_cross_model_parity.py +121 -0
  780. package/vendor/ars/scripts/test_check_spec_consistency.py +1081 -0
  781. package/vendor/ars/scripts/test_check_sprint_contract.py +458 -0
  782. package/vendor/ars/scripts/test_check_stage_capability_matrix.py +698 -0
  783. package/vendor/ars/scripts/test_check_submission_packet_manifest_integration.py +177 -0
  784. package/vendor/ars/scripts/test_check_surface_form_parity.py +417 -0
  785. package/vendor/ars/scripts/test_check_task_type.py +116 -0
  786. package/vendor/ars/scripts/test_check_tools_allowlist.py +835 -0
  787. package/vendor/ars/scripts/test_check_tortured_phrase_screening_integration.py +1306 -0
  788. package/vendor/ars/scripts/test_check_v3_10_134_write_scope.py +251 -0
  789. package/vendor/ars/scripts/test_check_v3_10_policy.py +547 -0
  790. package/vendor/ars/scripts/test_check_v3_6_7_pattern_protection.py +960 -0
  791. package/vendor/ars/scripts/test_check_v3_6_8_audit_scope_block.py +1000 -0
  792. package/vendor/ars/scripts/test_check_v3_6_8_cite_provenance_pipeline.py +454 -0
  793. package/vendor/ars/scripts/test_check_v3_6_8_frontmatter_trust_schema.py +581 -0
  794. package/vendor/ars/scripts/test_check_v3_6_8_mark_read_commands.py +120 -0
  795. package/vendor/ars/scripts/test_check_v3_6_8_pattern_protection.py +1138 -0
  796. package/vendor/ars/scripts/test_check_v3_7_3_three_layer_citation.py +566 -0
  797. package/vendor/ars/scripts/test_check_v3_8_annotation_literal_sync.py +262 -0
  798. package/vendor/ars/scripts/test_check_v3_9_0_triangulation.py +321 -0
  799. package/vendor/ars/scripts/test_check_v3_9_2_phase_boundary.py +190 -0
  800. package/vendor/ars/scripts/test_check_v3_9_4_temporal_verification.py +471 -0
  801. package/vendor/ars/scripts/test_check_version_consistency.py +1462 -0
  802. package/vendor/ars/scripts/test_check_workflow_classification.py +225 -0
  803. package/vendor/ars/scripts/test_chinese_literature_client.py +1889 -0
  804. package/vendor/ars/scripts/test_citation_existence_policy.py +480 -0
  805. package/vendor/ars/scripts/test_citation_verification_summary.py +342 -0
  806. package/vendor/ars/scripts/test_claim_audit_calibration.py +882 -0
  807. package/vendor/ars/scripts/test_claim_audit_finalizer.py +979 -0
  808. package/vendor/ars/scripts/test_claim_audit_pipeline.py +2398 -0
  809. package/vendor/ars/scripts/test_claim_audit_schema.py +1778 -0
  810. package/vendor/ars/scripts/test_claim_intent_manifest.py +666 -0
  811. package/vendor/ars/scripts/test_claim_registry_coverage.py +246 -0
  812. package/vendor/ars/scripts/test_claim_standing_discovery.py +579 -0
  813. package/vendor/ars/scripts/test_claim_standing_pipeline_wiring.py +290 -0
  814. package/vendor/ars/scripts/test_claim_standing_stance_assets.py +399 -0
  815. package/vendor/ars/scripts/test_claim_standing_stance_contracts.py +383 -0
  816. package/vendor/ars/scripts/test_claim_standing_stance_runner.py +349 -0
  817. package/vendor/ars/scripts/test_claim_standing_transmissions.py +605 -0
  818. package/vendor/ars/scripts/test_claim_strength_drift_disposition.py +517 -0
  819. package/vendor/ars/scripts/test_claim_verification_coverage_contract.py +104 -0
  820. package/vendor/ars/scripts/test_contamination_signals.py +1086 -0
  821. package/vendor/ars/scripts/test_content_coverage_advisory.py +1767 -0
  822. package/vendor/ars/scripts/test_cross_document_consistency_advisory.py +1270 -0
  823. package/vendor/ars/scripts/test_cross_model_codex_transport.py +1152 -0
  824. package/vendor/ars/scripts/test_cross_model_handoff.py +571 -0
  825. package/vendor/ars/scripts/test_cross_model_verification_guards.py +792 -0
  826. package/vendor/ars/scripts/test_crossref_client.py +393 -0
  827. package/vendor/ars/scripts/test_dispatch_e4_panel.py +4235 -0
  828. package/vendor/ars/scripts/test_e2e_claim_audit.py +540 -0
  829. package/vendor/ars/scripts/test_eval_harness_workflow.py +140 -0
  830. package/vendor/ars/scripts/test_evals_citation_extraction.py +150 -0
  831. package/vendor/ars/scripts/test_evals_lift_report_schema.py +108 -0
  832. package/vendor/ars/scripts/test_evidence_rows.py +2593 -0
  833. package/vendor/ars/scripts/test_experiment_provenance.py +915 -0
  834. package/vendor/ars/scripts/test_human_read_attestation_resolver.py +477 -0
  835. package/vendor/ars/scripts/test_ideation_diversity_assignment_gate.py +615 -0
  836. package/vendor/ars/scripts/test_indirect_prompt_injection_behavior_probe.py +250 -0
  837. package/vendor/ars/scripts/test_inquiry_branch_ledger.py +2296 -0
  838. package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_10.py +248 -0
  839. package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_7_3.py +545 -0
  840. package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_9_0.py +497 -0
  841. package/vendor/ars/scripts/test_normalize_compat_verdict.py +149 -0
  842. package/vendor/ars/scripts/test_openalex_client.py +490 -0
  843. package/vendor/ars/scripts/test_passport_yaml.py +104 -0
  844. package/vendor/ars/scripts/test_pattern_eval_runtime.py +1295 -0
  845. package/vendor/ars/scripts/test_pdf_read_preflight.py +1943 -0
  846. package/vendor/ars/scripts/test_policy_anchor_disclosure.py +666 -0
  847. package/vendor/ars/scripts/test_reading_probe_lint.py +218 -0
  848. package/vendor/ars/scripts/test_recompute_receipts.py +778 -0
  849. package/vendor/ars/scripts/test_render_claim_standing_view.py +192 -0
  850. package/vendor/ars/scripts/test_render_eval_comment.py +162 -0
  851. package/vendor/ars/scripts/test_render_harness_retirement_issue.py +110 -0
  852. package/vendor/ars/scripts/test_repro_lock_validation_drift.py +100 -0
  853. package/vendor/ars/scripts/test_research_workflow_profile.py +734 -0
  854. package/vendor/ars/scripts/test_resolve_human_subjects_authority.py +1219 -0
  855. package/vendor/ars/scripts/test_resolve_review_target_context.py +703 -0
  856. package/vendor/ars/scripts/test_resume_e4_record.py +315 -0
  857. package/vendor/ars/scripts/test_retraction_status.py +456 -0
  858. package/vendor/ars/scripts/test_review_criteria_binding.py +629 -0
  859. package/vendor/ars/scripts/test_review_panel_provenance.py +565 -0
  860. package/vendor/ars/scripts/test_review_pathway_rule_trace.py +821 -0
  861. package/vendor/ars/scripts/test_revision_roadmap.py +1255 -0
  862. package/vendor/ars/scripts/test_run_ci_pytest_manifest.py +186 -0
  863. package/vendor/ars/scripts/test_run_codex_audit_e2e.py +369 -0
  864. package/vendor/ars/scripts/test_run_evals.py +430 -0
  865. package/vendor/ars/scripts/test_run_guard_launcher.py +500 -0
  866. package/vendor/ars/scripts/test_run_ideation_diversity_no_call.py +1833 -0
  867. package/vendor/ars/scripts/test_run_indirect_prompt_injection_no_call.py +1889 -0
  868. package/vendor/ars/scripts/test_run_review_criteria_constructive_value.py +586 -0
  869. package/vendor/ars/scripts/test_run_role_topology_utility_dry_run.py +428 -0
  870. package/vendor/ars/scripts/test_score_review_criteria_constructive_value.py +340 -0
  871. package/vendor/ars/scripts/test_semantic_scholar_client.py +554 -0
  872. package/vendor/ars/scripts/test_slr_lineage_emission.py +230 -0
  873. package/vendor/ars/scripts/test_socratic_rq_non_generation_contract.py +173 -0
  874. package/vendor/ars/scripts/test_temporal_integrity_audit.py +438 -0
  875. package/vendor/ars/scripts/test_text_similarity.py +95 -0
  876. package/vendor/ars/scripts/test_title_fuzzy_false_positive.py +111 -0
  877. package/vendor/ars/scripts/test_tortured_phrase_screening.py +2959 -0
  878. package/vendor/ars/scripts/test_transport_fixture_citation_gate.py +338 -0
  879. package/vendor/ars/scripts/test_uncited_assertion.py +558 -0
  880. package/vendor/ars/scripts/test_v3_6_7_phase_6_6.py +1279 -0
  881. package/vendor/ars/scripts/test_validate_compliance_fixtures.py +36 -0
  882. package/vendor/ars/scripts/test_validate_ideation_diversity_assets.py +230 -0
  883. package/vendor/ars/scripts/test_venue_disclosure_contract.py +755 -0
  884. package/vendor/ars/scripts/test_verification_cache.py +280 -0
  885. package/vendor/ars/scripts/test_verification_gate.py +461 -0
  886. package/vendor/ars/scripts/test_verify_passport_cli.py +123 -0
  887. package/vendor/ars/scripts/test_verify_submission_package.py +1407 -0
  888. package/vendor/ars/scripts/test_version_records_schema.py +211 -0
  889. package/vendor/ars/scripts/tortured_phrase_screening.py +3502 -0
  890. package/vendor/ars/scripts/uncited_assertion_detector.py +254 -0
  891. package/vendor/ars/scripts/v3_6_7_inversion_manifest.json +9 -0
  892. package/vendor/ars/scripts/v3_6_8_inversion_manifest.json +10 -0
  893. package/vendor/ars/scripts/validate_claim_standing_stance_assets.py +249 -0
  894. package/vendor/ars/scripts/validate_compliance_fixtures.py +56 -0
  895. package/vendor/ars/scripts/validate_ideation_diversity_assets.py +303 -0
  896. package/vendor/ars/scripts/venue_disclosure_contract_harness.py +837 -0
  897. package/vendor/ars/scripts/verification_cache.py +276 -0
  898. package/vendor/ars/scripts/verification_gate/__init__.py +345 -0
  899. package/vendor/ars/scripts/verify_passport.py +133 -0
  900. package/vendor/ars/scripts/verify_submission_package.py +1657 -0
  901. package/vendor/ars/shared/agents/compliance_agent.md +136 -0
  902. package/vendor/ars/shared/artifact_reproducibility_pattern.md +173 -0
  903. package/vendor/ars/shared/benchmark_report.schema.json +81 -0
  904. package/vendor/ars/shared/benchmark_report_pattern.md +180 -0
  905. package/vendor/ars/shared/bibliographic_integrity_signals.md +142 -0
  906. package/vendor/ars/shared/collaboration_depth_rubric.md +154 -0
  907. package/vendor/ars/shared/compliance_checkpoint_protocol.md +162 -0
  908. package/vendor/ars/shared/compliance_report.schema.json +187 -0
  909. package/vendor/ars/shared/contracts/README.md +938 -0
  910. package/vendor/ars/shared/contracts/activity/adjudication_activity_input.schema.json +555 -0
  911. package/vendor/ars/shared/contracts/activity/adjudication_activity_store.schema.json +560 -0
  912. package/vendor/ars/shared/contracts/audit/audit_jsonl.schema.json +128 -0
  913. package/vendor/ars/shared/contracts/audit/audit_sidecar.schema.json +169 -0
  914. package/vendor/ars/shared/contracts/audit/audit_verdict.schema.json +133 -0
  915. package/vendor/ars/shared/contracts/audit/cross_document_consistency_advisory.schema.json +480 -0
  916. package/vendor/ars/shared/contracts/audit/cross_document_consistency_advisory_draft.schema.json +564 -0
  917. package/vendor/ars/shared/contracts/audit/cross_document_source_manifest.schema.json +202 -0
  918. package/vendor/ars/shared/contracts/audit/tortured_phrase_advisory.schema.json +1362 -0
  919. package/vendor/ars/shared/contracts/audit/tortured_phrase_snapshot.schema.json +208 -0
  920. package/vendor/ars/shared/contracts/audit/tortured_phrase_snapshot_manifest.schema.json +335 -0
  921. package/vendor/ars/shared/contracts/capability/stage_capability_matrix.json +434 -0
  922. package/vendor/ars/shared/contracts/claim_standing/candidate_ledger.schema.json +705 -0
  923. package/vendor/ars/shared/contracts/claim_standing/query_plan.schema.json +389 -0
  924. package/vendor/ars/shared/contracts/claim_standing/query_plan_v1_1.schema.json +782 -0
  925. package/vendor/ars/shared/contracts/claim_standing/retrieval_input.schema.json +440 -0
  926. package/vendor/ars/shared/contracts/claim_standing/stance_record.schema.json +460 -0
  927. package/vendor/ars/shared/contracts/claim_standing/transmission_ledger.schema.json +282 -0
  928. package/vendor/ars/shared/contracts/cross_model/codex_citation_receipt.schema.json +157 -0
  929. package/vendor/ars/shared/contracts/cross_model/codex_citation_request.schema.json +23 -0
  930. package/vendor/ars/shared/contracts/cross_model/promotion_bakeoff_sealed_commitment.schema.json +55 -0
  931. package/vendor/ars/shared/contracts/cross_model/promotion_bakeoff_sealed_reveal.schema.json +50 -0
  932. package/vendor/ars/shared/contracts/degradation_registry.json +428 -0
  933. package/vendor/ars/shared/contracts/evaluator/full.json +126 -0
  934. package/vendor/ars/shared/contracts/evidence/claim_registry.schema.json +50 -0
  935. package/vendor/ars/shared/contracts/evidence/claim_registry_coverage_report.schema.json +79 -0
  936. package/vendor/ars/shared/contracts/evidence/evidence_row.schema.json +504 -0
  937. package/vendor/ars/shared/contracts/evidence/evidence_row_v1_1.schema.json +364 -0
  938. package/vendor/ars/shared/contracts/evidence/evidence_row_v1_2.schema.json +714 -0
  939. package/vendor/ars/shared/contracts/evidence/evidence_row_v1_3.schema.json +524 -0
  940. package/vendor/ars/shared/contracts/human_subjects/authority_profile_registry.schema.json +497 -0
  941. package/vendor/ars/shared/contracts/human_subjects/committee_correspondence.schema.json +253 -0
  942. package/vendor/ars/shared/contracts/human_subjects/content_coverage_advisory.schema.json +620 -0
  943. package/vendor/ars/shared/contracts/human_subjects/irb_context_record.schema.json +329 -0
  944. package/vendor/ars/shared/contracts/human_subjects/resolved_authority_context.schema.json +347 -0
  945. package/vendor/ars/shared/contracts/human_subjects/review_pathway_rule_trace.schema.json +265 -0
  946. package/vendor/ars/shared/contracts/human_subjects/review_pathway_trace_request.schema.json +127 -0
  947. package/vendor/ars/shared/contracts/human_subjects/submission_packet_inventory.schema.json +240 -0
  948. package/vendor/ars/shared/contracts/human_subjects/submission_packet_manifest.schema.json +999 -0
  949. package/vendor/ars/shared/contracts/passport/audit_artifact_entry.schema.json +266 -0
  950. package/vendor/ars/shared/contracts/passport/bibliographic_integrity_signal.schema.json +1676 -0
  951. package/vendor/ars/shared/contracts/passport/citation_provenance.schema.json +107 -0
  952. package/vendor/ars/shared/contracts/passport/citation_verification_summary.schema.json +164 -0
  953. package/vendor/ars/shared/contracts/passport/claim_audit_result.schema.json +124 -0
  954. package/vendor/ars/shared/contracts/passport/claim_drift.schema.json +58 -0
  955. package/vendor/ars/shared/contracts/passport/claim_intent_manifest.schema.json +107 -0
  956. package/vendor/ars/shared/contracts/passport/constraint_violation.schema.json +65 -0
  957. package/vendor/ars/shared/contracts/passport/experiment_alignment_result.schema.json +69 -0
  958. package/vendor/ars/shared/contracts/passport/experiment_provenance_entry.schema.json +169 -0
  959. package/vendor/ars/shared/contracts/passport/human_read_log.schema.json +86 -0
  960. package/vendor/ars/shared/contracts/passport/inquiry_ledger_ref.schema.json +22 -0
  961. package/vendor/ars/shared/contracts/passport/literature_corpus_entry.schema.json +650 -0
  962. package/vendor/ars/shared/contracts/passport/preregistration_artifact.schema.json +132 -0
  963. package/vendor/ars/shared/contracts/passport/rejection_log.schema.json +89 -0
  964. package/vendor/ars/shared/contracts/passport/reset_ledger_entry.schema.json +158 -0
  965. package/vendor/ars/shared/contracts/passport/temporal_audit_results.schema.json +208 -0
  966. package/vendor/ars/shared/contracts/passport/terminal_policies.schema.json +50 -0
  967. package/vendor/ars/shared/contracts/passport/timeline.schema.json +102 -0
  968. package/vendor/ars/shared/contracts/passport/uncited_assertion.schema.json +56 -0
  969. package/vendor/ars/shared/contracts/passport/uncited_audit_failure.schema.json +72 -0
  970. package/vendor/ars/shared/contracts/passport/user_attested_read_resolution.schema.json +87 -0
  971. package/vendor/ars/shared/contracts/passport/version_records.schema.json +138 -0
  972. package/vendor/ars/shared/contracts/patch/block_manifest.schema.json +45 -0
  973. package/vendor/ars/shared/contracts/patch/legacy/v1_0/revision_patch.schema.json +110 -0
  974. package/vendor/ars/shared/contracts/patch/revision_patch.schema.json +250 -0
  975. package/vendor/ars/shared/contracts/pdf/pdf_content_classifier_diagnostic.schema.json +39 -0
  976. package/vendor/ars/shared/contracts/pdf/pdf_content_classifier_worker.schema.json +88 -0
  977. package/vendor/ars/shared/contracts/pdf/pdf_read_preflight.schema.json +227 -0
  978. package/vendor/ars/shared/contracts/re_review/input_manifest.schema.json +161 -0
  979. package/vendor/ars/shared/contracts/re_review/legacy/v1_0/input_manifest.schema.json +134 -0
  980. package/vendor/ars/shared/contracts/re_review/legacy/v1_0/precommitment.schema.json +193 -0
  981. package/vendor/ars/shared/contracts/re_review/legacy/v1_0/traceability.schema.json +841 -0
  982. package/vendor/ars/shared/contracts/re_review/legacy/v1_0/verdict_record.schema.json +244 -0
  983. package/vendor/ars/shared/contracts/re_review/precommitment.schema.json +193 -0
  984. package/vendor/ars/shared/contracts/re_review/traceability.schema.json +916 -0
  985. package/vendor/ars/shared/contracts/re_review/verdict_record.schema.json +245 -0
  986. package/vendor/ars/shared/contracts/research_workflow/inquiry_branch_ledger.schema.json +449 -0
  987. package/vendor/ars/shared/contracts/research_workflow/research_workflow_profile.schema.json +222 -0
  988. package/vendor/ars/shared/contracts/research_workflow/research_workflow_profile_selection_receipt.schema.json +111 -0
  989. package/vendor/ars/shared/contracts/review_target/constructive_review_findings.schema.json +172 -0
  990. package/vendor/ars/shared/contracts/review_target/criteria_registry.schema.json +97 -0
  991. package/vendor/ars/shared/contracts/review_target/review_criteria_binding_manifest.schema.json +235 -0
  992. package/vendor/ars/shared/contracts/review_target/review_criteria_source_receipt.schema.json +158 -0
  993. package/vendor/ars/shared/contracts/review_target/review_target_context.schema.json +106 -0
  994. package/vendor/ars/shared/contracts/review_target/review_target_declaration.schema.json +118 -0
  995. package/vendor/ars/shared/contracts/reviewer/full.json +114 -0
  996. package/vendor/ars/shared/contracts/reviewer/methodology_focus.json +75 -0
  997. package/vendor/ars/shared/contracts/reviewer/review_panel_provenance.schema.json +263 -0
  998. package/vendor/ars/shared/contracts/reviewer/review_panel_provenance_carrier.schema.json +122 -0
  999. package/vendor/ars/shared/contracts/reviewer/review_panel_provenance_input.schema.json +120 -0
  1000. package/vendor/ars/shared/contracts/revision/author_adjudication.schema.json +251 -0
  1001. package/vendor/ars/shared/contracts/revision/author_adjudication_input.schema.json +25 -0
  1002. package/vendor/ars/shared/contracts/revision/claim_strength_drift_disposition.schema.json +97 -0
  1003. package/vendor/ars/shared/contracts/revision/claim_strength_drift_disposition_input.schema.json +80 -0
  1004. package/vendor/ars/shared/contracts/revision/claim_strength_drift_findings.schema.json +139 -0
  1005. package/vendor/ars/shared/contracts/revision/claim_surface_manifest.schema.json +99 -0
  1006. package/vendor/ars/shared/contracts/revision/integrity_correction_authorization.schema.json +140 -0
  1007. package/vendor/ars/shared/contracts/revision/integrity_correction_authorization_input.schema.json +28 -0
  1008. package/vendor/ars/shared/contracts/revision/integrity_correction_list.schema.json +48 -0
  1009. package/vendor/ars/shared/contracts/revision/integrity_pass_receipt.schema.json +16 -0
  1010. package/vendor/ars/shared/contracts/revision/revision_evidence_bundle.schema.json +134 -0
  1011. package/vendor/ars/shared/contracts/revision/revision_roadmap.schema.json +334 -0
  1012. package/vendor/ars/shared/contracts/submission/format_profile.example.yaml +33 -0
  1013. package/vendor/ars/shared/contracts/submission/format_profile.schema.json +102 -0
  1014. package/vendor/ars/shared/contracts/submission/submission_verification_report.schema.json +233 -0
  1015. package/vendor/ars/shared/contracts/submission/venue_profile.schema.json +114 -0
  1016. package/vendor/ars/shared/contracts/writer/full.json +87 -0
  1017. package/vendor/ars/shared/cross_model_verification.md +714 -0
  1018. package/vendor/ars/shared/evals_lift_report.schema.json +141 -0
  1019. package/vendor/ars/shared/ground_truth_isolation_pattern.md +275 -0
  1020. package/vendor/ars/shared/handoff_schemas.md +1209 -0
  1021. package/vendor/ars/shared/human_subjects_authority_registry.json +1278 -0
  1022. package/vendor/ars/shared/mode_spectrum.md +57 -0
  1023. package/vendor/ars/shared/model_tiering.md +83 -0
  1024. package/vendor/ars/shared/policy_data/nature_policy.md +56 -0
  1025. package/vendor/ars/shared/prisma_trAIce_protocol.md +157 -0
  1026. package/vendor/ars/shared/raise_framework.md +129 -0
  1027. package/vendor/ars/shared/references/authority_content_coverage_advisory_protocol.md +275 -0
  1028. package/vendor/ars/shared/references/claim_standing_candidate_ledger_protocol.md +66 -0
  1029. package/vendor/ars/shared/references/claim_strength_ladder.md +93 -0
  1030. package/vendor/ars/shared/references/cross_document_consistency_advisory_protocol.md +263 -0
  1031. package/vendor/ars/shared/references/evidence_row_protocol.md +260 -0
  1032. package/vendor/ars/shared/references/firm_rules.md +90 -0
  1033. package/vendor/ars/shared/references/human_subjects_authority_protocol.md +274 -0
  1034. package/vendor/ars/shared/references/intent_clarification_protocol.md +168 -0
  1035. package/vendor/ars/shared/references/irb_terminology_glossary.md +229 -0
  1036. package/vendor/ars/shared/references/protected_hedging_phrases.md +118 -0
  1037. package/vendor/ars/shared/references/psychometric_terminology_glossary.md +109 -0
  1038. package/vendor/ars/shared/references/review_criteria_consumer_protocol.md +238 -0
  1039. package/vendor/ars/shared/references/review_pathway_rule_trace_protocol.md +166 -0
  1040. package/vendor/ars/shared/references/submission_packet_manifest_protocol.md +292 -0
  1041. package/vendor/ars/shared/references/word_count_conventions.md +124 -0
  1042. package/vendor/ars/shared/research_workflow_profiles/field_general.json +1 -0
  1043. package/vendor/ars/shared/review_criteria_registry.json +207 -0
  1044. package/vendor/ars/shared/review_criteria_sources/msr-2027-technical-papers.2026-08-24.json +46 -0
  1045. package/vendor/ars/shared/review_criteria_sources/sigsoft-empirical-standards.2026-08-24.json +31 -0
  1046. package/vendor/ars/shared/sprint_contract.schema.json +482 -0
  1047. package/vendor/ars/shared/style_calibration_protocol.md +151 -0
  1048. package/vendor/ars/shared/templates/codex_audit_multifile_template.md +263 -0
  1049. package/vendor/ars/tools/release-discipline/.toolkit-version +1 -0
  1050. package/vendor/ars/tools/release-discipline/README.md +4 -0
  1051. package/vendor/ars/tools/release-discipline/scripts/_release_doc_alignment_schema.py +1011 -0
  1052. package/vendor/ars/tools/release-discipline/scripts/check_command_invariants.py +497 -0
  1053. package/vendor/ars/tools/release-discipline/scripts/check_release_doc_alignment.py +263 -0
  1054. package/vendor/ars/tools/release-discipline/scripts/sync-toolkit.sh +147 -0
  1055. package/vendor/windows/NOTICE.md +12 -0
  1056. package/vendor/windows/arm64/fd.exe +0 -0
  1057. package/vendor/windows/arm64/licenses/fd/LICENSE-APACHE +201 -0
  1058. package/vendor/windows/arm64/licenses/fd/LICENSE-MIT +21 -0
  1059. package/vendor/windows/arm64/licenses/ripgrep/COPYING +3 -0
  1060. package/vendor/windows/arm64/licenses/ripgrep/LICENSE-MIT +21 -0
  1061. package/vendor/windows/arm64/licenses/ripgrep/UNLICENSE +24 -0
  1062. package/vendor/windows/arm64/rg.exe +0 -0
  1063. package/vendor/windows/x64/fd.exe +0 -0
  1064. package/vendor/windows/x64/licenses/fd/LICENSE-APACHE +201 -0
  1065. package/vendor/windows/x64/licenses/fd/LICENSE-MIT +21 -0
  1066. package/vendor/windows/x64/licenses/ripgrep/COPYING +3 -0
  1067. package/vendor/windows/x64/licenses/ripgrep/LICENSE-MIT +21 -0
  1068. package/vendor/windows/x64/licenses/ripgrep/UNLICENSE +24 -0
  1069. package/vendor/windows/x64/rg.exe +0 -0
@@ -0,0 +1,1889 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for the Chinese-literature resolver client (#595).
3
+
4
+ Per `deep-research/references/chinese_literature_api_protocol.md`. Structure
5
+ mirrors `test_arxiv_client.py` (per-client unit suite) but uses the
6
+ transport-fixture discipline of `test_transport_fixture_citation_gate.py`: the
7
+ REAL `ChineseLiteratureClient` builds the URLs and parses the bodies, and a
8
+ URL-dispatch fake stands in for the socket. Every body is checked in under
9
+ `scripts/fixtures/transport_bodies/chinese_literature/` and is fully synthetic
10
+ (see that directory's README).
11
+
12
+ ZERO live network. The protocol doc's live examples are a record of a manual
13
+ verification; nothing here contacts doi.org, hdl.handle.net, or NCBI.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import http.client
18
+ import io
19
+ import sys
20
+ import traceback
21
+ import urllib.error
22
+ import urllib.parse
23
+ import urllib.request
24
+ from pathlib import Path
25
+ from unittest.mock import MagicMock, patch
26
+
27
+ import pytest
28
+
29
+
30
+ REPO_ROOT = Path(__file__).resolve().parent.parent
31
+ if str(REPO_ROOT / "scripts") not in sys.path:
32
+ sys.path.insert(0, str(REPO_ROOT / "scripts"))
33
+
34
+ FIXTURES = REPO_ROOT / "scripts" / "fixtures" / "transport_bodies" / "chinese_literature"
35
+
36
+ # Synthetic identifiers pinned to the fixture bodies (see the fixtures README):
37
+ # These suffixes are synthetic fixture namespaces. The 10.5555 prefix itself
38
+ # can contain real Crossref records, so tests make no claim about live minting.
39
+ ISTIC_DOI = "10.5555/ars.cn.istic.2026.0042"
40
+ CNKI_DOI = "10.5555/ars.cn.cnki.2026.0042"
41
+ FABRICATED_DOI = "10.5555/ars.cn.istic.9999.9999"
42
+
43
+ CN_TITLE = "合成引文核验闸的确定性评估方法研究"
44
+ CN_JOURNAL = "合成测试医学杂志"
45
+
46
+ # Injected through the client's documented extension point, so no test depends
47
+ # on the shipped seed table's contents.
48
+ TEST_JOURNAL_MAP = {
49
+ CN_JOURNAL: {
50
+ "issn": "0000-0000",
51
+ "nlm_ta": "Hecheng Ceshi Yi Xue Za Zhi",
52
+ "nlm_id": "999999901",
53
+ "display_name": CN_JOURNAL,
54
+ }
55
+ }
56
+
57
+
58
+ def _body(name: str) -> bytes:
59
+ return FIXTURES.joinpath(name).read_bytes()
60
+
61
+
62
+ @pytest.fixture(autouse=True)
63
+ def _no_real_sleep(monkeypatch):
64
+ """Pacing must never burn wall-clock in CI. Patched on the client module so
65
+ a `time.sleep` that leaks in from elsewhere still fails loudly."""
66
+ monkeypatch.setattr("chinese_literature_client.time.sleep", lambda seconds: None)
67
+
68
+
69
+ def _client(**kwargs):
70
+ from chinese_literature_client import ChineseLiteratureClient
71
+
72
+ kwargs.setdefault("ncbi_email", "ars-tests@example.invalid")
73
+ return ChineseLiteratureClient(**kwargs)
74
+
75
+
76
+ class FakeTransport:
77
+ """urlopen stand-in dispatching on the REAL URL the client built.
78
+
79
+ `routes` is an ordered list of `(predicate, action)`; predicates receive
80
+ `(urlsplit_result, parse_qs_dict)`. An unrouted URL is an immediate test
81
+ failure — the dispatch table doubles as an assertion that the client
82
+ contacted exactly the documented endpoints. AssertionError is not in the
83
+ client's degradation except-list, so it propagates instead of being
84
+ laundered into ChineseLiteratureUnavailable.
85
+ """
86
+
87
+ def __init__(self, routes):
88
+ self.routes = routes
89
+ self.requests: list[str] = []
90
+
91
+ def __call__(self, req, timeout=None):
92
+ url = req.full_url
93
+ self.requests.append(url)
94
+ parsed = urllib.parse.urlsplit(url)
95
+ query = (
96
+ urllib.parse.parse_qs(
97
+ parsed.query, keep_blank_values=True, strict_parsing=True)
98
+ if parsed.query else {}
99
+ )
100
+ for predicate, action in self.routes:
101
+ if predicate(parsed, query):
102
+ return action(url)
103
+ raise AssertionError(f"client contacted an unrouted URL: {url}")
104
+
105
+
106
+ def _ok(body: bytes):
107
+ def action(url):
108
+ resp = MagicMock()
109
+ resp.status = 200
110
+ resp.headers = {}
111
+ resp.geturl.return_value = url
112
+ resp.read.return_value = body
113
+ resp.__enter__ = MagicMock(return_value=resp)
114
+ resp.__exit__ = MagicMock(return_value=None)
115
+ return resp
116
+
117
+ return action
118
+
119
+
120
+ def _http_error(code: int, body: bytes = b""):
121
+ def action(url):
122
+ raise urllib.error.HTTPError(
123
+ url=url, code=code, msg="synthetic", hdrs={}, fp=io.BytesIO(body),
124
+ )
125
+
126
+ return action
127
+
128
+
129
+ # Route predicates, written against host+path so an accidental endpoint change
130
+ # in the client fails routing rather than silently following along.
131
+ def _is_ra(parsed, query):
132
+ return parsed.netloc == "doi.org" and parsed.path.startswith("/doiRA/")
133
+
134
+
135
+ def _is_doi_resolve(parsed, query):
136
+ return parsed.netloc == "doi.org" and not parsed.path.startswith("/doiRA/")
137
+
138
+
139
+ def _is_handle(parsed, query):
140
+ return parsed.netloc == "hdl.handle.net" and parsed.path.startswith("/api/handles/")
141
+
142
+
143
+ def _is_esearch(parsed, query):
144
+ return parsed.path.endswith("/esearch.fcgi")
145
+
146
+
147
+ def _is_esummary(parsed, query):
148
+ return parsed.path.endswith("/esummary.fcgi")
149
+
150
+
151
+ # ---------------------------------------------------------------- pure helpers
152
+
153
+
154
+ def test_normalize_folds_the_three_measured_chinese_variants():
155
+ """The shared #431 normalizer rejects three legitimate spellings of one
156
+ identical Chinese title (measured 2026-07-27: fullwidth 0.577/False, CJK
157
+ terminal punctuation 0.981/False, interior spaces 0.929/False). The
158
+ Chinese-aware normalizer must fold all three to one string."""
159
+ from chinese_literature_client import normalize_cn_title
160
+
161
+ canonical = normalize_cn_title("宫颈腺癌中ProEXC和PRMT5的表达及其临床意义")
162
+ assert normalize_cn_title("宫颈腺癌中ProEXC和PRMT5的表达及其临床意义") == canonical
163
+ assert normalize_cn_title("宫颈腺癌中ProEXC和PRMT5的表达及其临床意义。") == canonical
164
+ assert normalize_cn_title("宫颈腺癌中 ProEXC 和 PRMT5 的表达及其临床意义") == canonical
165
+ assert normalize_cn_title("《宫颈腺癌中ProEXC和PRMT5的表达及其临床意义》") == canonical
166
+
167
+
168
+ def test_fuzzy_similarity_alone_never_promotes_a_different_paper():
169
+ """Han-character overlap gives unrelated Chinese papers a high baseline
170
+ ratio (0.510 measured between two genuinely different papers), so the
171
+ shared fuzzy ratio is not sufficient — exact equality after Chinese-aware
172
+ normalization is the rule."""
173
+ from chinese_literature_client import _cn_titles_match
174
+
175
+ a = "宫颈腺癌中ProEXC和PRMT5的表达及其临床意义"
176
+ b = "宫颈癌及癌前病变组织hTERC基因表达及其临床意义"
177
+ assert _cn_titles_match(a, a) is True
178
+ assert _cn_titles_match(b, a) is False
179
+
180
+
181
+ def test_legitimate_variants_match_despite_a_sub_threshold_fuzzy_ratio():
182
+ """Regression for a live-smoke near-miss: the shared 0.70 ratio scores a
183
+ legitimate FULLWIDTH spelling of the identical title at 0.577, so ANDing it
184
+ in as an extra necessary condition would veto a correct match and file a
185
+ real paper at P0 next to the word 'fabricated'. On CJK the ratio separates
186
+ almost nothing (0.510 unrelated vs 0.577 identical), so it is excluded."""
187
+ from _text_similarity import _TITLE_SIMILARITY_THRESHOLD, _similarity
188
+ from chinese_literature_client import _cn_titles_match
189
+
190
+ canonical = "宫颈腺癌中ProEXC和PRMT5的表达及其临床意义"
191
+ fullwidth = "宫颈腺癌中ProEXC和PRMT5的表达及其临床意义。"
192
+
193
+ # The premise: the shared ratio really is below the floor for this pair.
194
+ assert _similarity(canonical, fullwidth) < _TITLE_SIMILARITY_THRESHOLD
195
+ # ...and the Chinese-aware rule still matches it.
196
+ assert _cn_titles_match(fullwidth, canonical) is True
197
+ assert _cn_titles_match(canonical, fullwidth) is True
198
+
199
+
200
+ def test_doi_keyed_exact_generic_title_can_match():
201
+ """The generic-title veto is for identifier-free title searches. A real,
202
+ DOI-keyed work may legitimately be titled Editorial."""
203
+ from chinese_literature_client import _cn_titles_match
204
+
205
+ assert _cn_titles_match("Editorial", "Editorial") is True
206
+
207
+
208
+ @pytest.mark.parametrize(
209
+ "left,right",
210
+ [
211
+ ("ER+ 乳腺癌患者的预后分析", "ER- 乳腺癌患者的预后分析"),
212
+ ("CD4+ T细胞计数与预后", "CD4− T细胞计数与预后"),
213
+ ("生存率提高 4.5% 的临床意义", "生存率提高 45% 的临床意义"),
214
+ ("剂量2²的研究", "剂量22的研究"),
215
+ ("Straße治疗研究", "Strasse治疗研究"),
216
+ ("治疗是否有效?", "治疗是否有效"),
217
+ ("PD L1蛋白表达", "PDL 1蛋白表达"),
218
+ ("P53蛋白表达", "p53蛋白表达"),
219
+ ],
220
+ )
221
+ def test_normalizer_preserves_scientifically_meaningful_distinctions(left, right):
222
+ from chinese_literature_client import normalize_cn_title
223
+
224
+ assert normalize_cn_title(left) != normalize_cn_title(right)
225
+
226
+
227
+ @pytest.mark.parametrize(
228
+ "variant",
229
+ [
230
+ CN_TITLE + ".",
231
+ CN_TITLE + ".",
232
+ "“" + CN_TITLE + "”",
233
+ "‘" + CN_TITLE + "’",
234
+ ],
235
+ )
236
+ def test_normalizer_folds_conventional_terminal_periods_and_outer_quotes(variant):
237
+ from chinese_literature_client import normalize_cn_title
238
+
239
+ assert normalize_cn_title(variant) == normalize_cn_title(CN_TITLE)
240
+
241
+
242
+ def test_applicability_gate_skips_non_chinese_citations():
243
+ """Mirrors _run_arxiv's skip semantics — an English citation must be
244
+ `skipped`, never `unmatched`, so English corpora are untouched."""
245
+ client = _client()
246
+ assert client.is_applicable({"title": "Attention Is All You Need"}) is False
247
+ assert client.is_applicable({"title": CN_TITLE}) is True
248
+ assert client.is_applicable({"container_title": CN_JOURNAL}) is True
249
+ assert client.is_applicable({"title": "A Study", "language": "zh"}) is True
250
+ # A Chinese RA on the DOI is sufficient even with a romanized title.
251
+ assert client.is_applicable({"title": "A Study"}, ra="ISTIC") is True
252
+
253
+
254
+ def test_resolve_skips_non_chinese_without_touching_the_network():
255
+ """An English citation must cost ZERO requests — including when it carries
256
+ a DOI. Establishing applicability from the registration agency would mean
257
+ one doi.org round-trip per English reference in every bibliography."""
258
+ client = _client()
259
+ with patch("chinese_literature_client._safe_urlopen", side_effect=AssertionError("no network")):
260
+ result = client.resolve({
261
+ "citation_key": "smith2020",
262
+ "title": "An English Paper",
263
+ "doi": "10.5555/english.2020.1",
264
+ })
265
+ assert result["status"] == "skipped"
266
+ assert result["queried_by"] is None
267
+ assert result["reason_code"] == "NOT_CHINESE_LITERATURE"
268
+ assert result["checklist_item"] is None
269
+
270
+
271
+ # ------------------------------------------------------- RA triage (3 branches)
272
+
273
+
274
+ @pytest.mark.parametrize(
275
+ "fixture,expected",
276
+ [
277
+ ("ra_istic.json", "ISTIC"),
278
+ ("ra_cnki.json", "CNKI"),
279
+ ("ra_crossref.json", "Crossref"),
280
+ ],
281
+ )
282
+ def test_ra_lookup_routes_all_three_agencies(fixture, expected):
283
+ transport = FakeTransport([(_is_ra, _ok(_body(fixture)))])
284
+ with patch("chinese_literature_client._safe_urlopen", transport):
285
+ assert _client().ra_for(ISTIC_DOI) == expected
286
+ assert transport.requests == ["https://doi.org/doiRA/10.5555"]
287
+
288
+
289
+ def test_ra_lookup_returns_none_for_an_unknown_prefix():
290
+ """The endpoint does not error on an unknown prefix: it answers 200 with a
291
+ `status` field in place of `RA` (verified 2026-07-27: `/doiRA/10.99999` ->
292
+ `[{"DOI": "10.99999", "status": "DOI does not exist"}]`). The missing RA
293
+ key must yield None, never a degradation or a fabricated agency name."""
294
+ transport = FakeTransport([(_is_ra, _ok(_body("ra_unknown_prefix.json")))])
295
+ with patch("chinese_literature_client._safe_urlopen", transport):
296
+ assert _client().ra_for("10.99999/ars.cn.unknown.2026.1") is None
297
+
298
+
299
+ @pytest.mark.parametrize(
300
+ "body",
301
+ [
302
+ b"{}",
303
+ b"[]",
304
+ b"null",
305
+ b'[{"DOI":"10.other","RA":"ISTIC"}]',
306
+ b'[{"DOI":"10.5555","RA":false}]',
307
+ b'[{"DOI":"10.5555"}]',
308
+ b'[{"DOI":"10.5555","RA":"ISTIC"},{"DOI":"10.5555","RA":"CNKI"}]',
309
+ ],
310
+ )
311
+ def test_ra_lookup_parseable_malformed_shape_is_typed_unavailable(body):
312
+ from chinese_literature_client import ChineseLiteratureUnavailable
313
+
314
+ transport = FakeTransport([(_is_ra, _ok(body))])
315
+ with patch("chinese_literature_client._safe_urlopen", transport):
316
+ with pytest.raises(ChineseLiteratureUnavailable):
317
+ _client().ra_for(ISTIC_DOI)
318
+
319
+
320
+ def test_ra_lookup_short_circuits_a_non_doi_without_a_request():
321
+ transport = FakeTransport([])
322
+ with patch("chinese_literature_client._safe_urlopen", transport):
323
+ assert _client().ra_for("not-a-doi") is None
324
+ assert transport.requests == []
325
+
326
+
327
+ def test_crossref_registered_chinese_doi_is_not_claimed():
328
+ """Division of labour: a Crossref-registered Chinese DOI belongs to the
329
+ existing crossref resolver. It must stop without discarding the supplied
330
+ DOI or attempting the DOI-less coordinate branch."""
331
+ transport = FakeTransport([
332
+ (_is_ra, _ok(_body("ra_crossref.json"))),
333
+ (_is_doi_resolve, _http_error(500)), # must never be reached
334
+ (_is_handle, _http_error(500)), # must never be reached
335
+ ])
336
+ with patch("chinese_literature_client._safe_urlopen", transport):
337
+ result = _client(journal_map=TEST_JOURNAL_MAP).resolve({
338
+ "citation_key": "chen2026",
339
+ "title": CN_TITLE,
340
+ "container_title": CN_JOURNAL,
341
+ "doi": "10.5555/crossref.zh.2026.1",
342
+ })
343
+ assert result["reason_code"] == "DOI_RA_OUT_OF_SCOPE"
344
+ assert result["status"] == "skipped"
345
+ assert result["checklist_item"]["priority"] == "P3"
346
+ assert len(transport.requests) == 1 # RA lookup only
347
+
348
+
349
+ def test_unknown_doi_ra_does_not_fall_through_to_coordinates():
350
+ transport = FakeTransport([
351
+ (_is_ra, _ok(_body("ra_unknown_prefix.json"))),
352
+ (_is_esearch, _http_error(500)), # must never be reached
353
+ ])
354
+ with patch("chinese_literature_client._safe_urlopen", transport):
355
+ result = _client(journal_map=TEST_JOURNAL_MAP).resolve(
356
+ _istic_entry(doi="10.99999/ars.cn.unknown.2026.1")
357
+ )
358
+ assert result["status"] == "skipped"
359
+ assert result["reason_code"] == "DOI_RA_UNRESOLVED"
360
+ assert result["checklist_item"]["priority"] == "P2"
361
+ assert len(transport.requests) == 1
362
+
363
+
364
+ # ------------------------------------------------------------ ISTIC DOI branch
365
+
366
+
367
+ def _istic_entry(**overrides):
368
+ entry = {
369
+ "citation_key": "hecheng2026",
370
+ "title": CN_TITLE,
371
+ "container_title": CN_JOURNAL,
372
+ "year": 2026,
373
+ "volume": "88",
374
+ "pages": "8801-8809",
375
+ "doi": ISTIC_DOI,
376
+ }
377
+ entry.update(overrides)
378
+ return entry
379
+
380
+
381
+ def test_istic_doi_hit_with_matching_chinese_title_is_matched_by_id():
382
+ transport = FakeTransport([
383
+ (_is_ra, _ok(_body("ra_istic.json"))),
384
+ (_is_doi_resolve, _ok(_body("istic_csl_hit.json"))),
385
+ ])
386
+ with patch("chinese_literature_client._safe_urlopen", transport):
387
+ result = _client().resolve(_istic_entry())
388
+
389
+ assert result["status"] == "matched"
390
+ assert result["queried_by"] == "id"
391
+ assert result["reason_code"] == "DOI_TITLE_VERIFIED"
392
+ assert result["evidence"]["title"] == CN_TITLE
393
+ assert result["evidence"]["container_title"] == CN_JOURNAL
394
+ assert result["evidence"]["year"] == 2026
395
+ assert result["checklist_item"] is None
396
+ # Exactly two requests: no Handle probe is needed once the title verifies.
397
+ assert len(transport.requests) == 2
398
+
399
+
400
+ def test_istic_doi_hit_matches_a_typeset_variant_of_the_cited_title():
401
+ """End-to-end companion to the unit regression: a citation whose title
402
+ differs from the record only by fullwidth forms, CJK punctuation and
403
+ interior spaces must MATCH, not land at P0. Typesetting differences between
404
+ a reference list and a metadata record are ordinary, not evidence."""
405
+ variant = "合成 引文核验闸 的确定性评估方法研究。"
406
+ transport = FakeTransport([
407
+ (_is_ra, _ok(_body("ra_istic.json"))),
408
+ (_is_doi_resolve, _ok(_body("istic_csl_hit.json"))),
409
+ ])
410
+ with patch("chinese_literature_client._safe_urlopen", transport):
411
+ result = _client().resolve(_istic_entry(title=variant))
412
+
413
+ assert result["status"] == "matched"
414
+ assert result["queried_by"] == "id"
415
+ assert result["checklist_item"] is None
416
+
417
+
418
+ def test_istic_doi_resolving_to_a_different_title_is_id_keyed_unmatched():
419
+ """Chimeric citation (real identifier, someone else's title). ID-keyed
420
+ unmatched is the C-V6(a) shape that licenses a `false`, and it is a P0 row."""
421
+ transport = FakeTransport([
422
+ (_is_ra, _ok(_body("ra_istic.json"))),
423
+ (_is_doi_resolve, _ok(_body("istic_csl_other_title.json"))),
424
+ (_is_handle, _ok(_body("handle_exists.json"))),
425
+ ])
426
+ with patch("chinese_literature_client._safe_urlopen", transport):
427
+ result = _client().resolve(_istic_entry())
428
+
429
+ assert result["status"] == "unmatched"
430
+ assert result["queried_by"] == "id"
431
+ assert result["reason_code"] == "DOI_TITLE_MISMATCH"
432
+ item = result["checklist_item"]
433
+ assert item["priority"] == "P0"
434
+ assert item["verdict_contribution"] == "false"
435
+ assert item["human_result"] is None
436
+
437
+
438
+ def test_istic_english_shadow_title_is_unverifiable_never_false():
439
+ """An English translation cannot refute the cited Chinese original.
440
+
441
+ PubMed and some DOI metadata surfaces expose English shadow titles for
442
+ Chinese articles. Without a translation oracle, a cross-script difference
443
+ is not a machine-verifiable chimeric citation.
444
+ """
445
+ transport = FakeTransport([
446
+ (_is_ra, _ok(_body("ra_istic.json"))),
447
+ (_is_doi_resolve, _ok(
448
+ b'{"title":"Deterministic Evaluation of a Synthetic Citation Gate"}'
449
+ )),
450
+ ])
451
+ with patch("chinese_literature_client._safe_urlopen", transport):
452
+ result = _client().resolve(_istic_entry())
453
+
454
+ assert result["status"] == "unmatched"
455
+ assert result["queried_by"] == "title"
456
+ assert result["reason_code"] == "DOI_EXISTS_TITLE_UNVERIFIABLE"
457
+ assert result["evidence"]["resolved_title"].startswith("Deterministic")
458
+ assert result["checklist_item"]["verdict_contribution"] == "unresolvable"
459
+ assert len(transport.requests) == 2
460
+
461
+
462
+ def test_two_different_latin_titles_are_unverifiable_not_mismatch():
463
+ """Matching script alone cannot make romanized titles comparable."""
464
+ from chinese_literature_client import DoiTitleState
465
+
466
+ transport = FakeTransport([
467
+ (_is_doi_resolve, _ok(b'{"title":"Romanized title alpha"}')),
468
+ ])
469
+ with patch("chinese_literature_client._safe_urlopen", transport):
470
+ outcome = _client().doi_lookup_with_title_check(
471
+ ISTIC_DOI, "Romanized title beta",
472
+ )
473
+
474
+ assert outcome.state is DoiTitleState.UNVERIFIABLE
475
+ assert outcome.record["title"] == "Romanized title alpha"
476
+
477
+
478
+ def test_lone_surrogate_doi_is_typed_unavailable_after_ra_lookup():
479
+ """A schema-valid JSON string still may not be UTF-8 encodable."""
480
+ from chinese_literature_client import ChineseLiteratureUnavailable
481
+
482
+ entry = _istic_entry(doi="10.5555/" + chr(0xD800))
483
+ transport = FakeTransport([
484
+ (_is_ra, _ok(_body("ra_istic.json"))),
485
+ ])
486
+ with patch("chinese_literature_client._safe_urlopen", transport):
487
+ with pytest.raises(ChineseLiteratureUnavailable, match="invalid Unicode"):
488
+ _client().resolve(entry)
489
+
490
+ assert len(transport.requests) == 1
491
+
492
+
493
+ def test_fabricated_doi_is_refuted_by_404_plus_handle_100():
494
+ """The fabrication canary: these prefixes carry no wildcard catch-all, so a
495
+ 404 + responseCode 100 is positive evidence the identifier does not exist."""
496
+ transport = FakeTransport([
497
+ (_is_ra, _ok(_body("ra_istic.json"))),
498
+ (_is_doi_resolve, _http_error(404)),
499
+ (_is_handle, _ok(_body("handle_absent.json"))),
500
+ ])
501
+ with patch("chinese_literature_client._safe_urlopen", transport):
502
+ result = _client().resolve(_istic_entry(doi=FABRICATED_DOI))
503
+
504
+ assert result["status"] == "unmatched"
505
+ assert result["queried_by"] == "id"
506
+ assert result["reason_code"] == "DOI_REFUTED"
507
+ item = result["checklist_item"]
508
+ assert item["priority"] == "P0"
509
+ assert item["verdict_contribution"] == "false"
510
+
511
+
512
+ def test_doi_404_is_a_negative_not_a_degradation():
513
+ """A 404 on the DOI path must be reported as data. Turning it into an
514
+ outage would throw away the resolver's strongest signal."""
515
+ from chinese_literature_client import (
516
+ ChineseLiteratureUnavailable,
517
+ DoiTitleState,
518
+ )
519
+
520
+ transport = FakeTransport([(_is_doi_resolve, _http_error(404))])
521
+ with patch("chinese_literature_client._safe_urlopen", transport):
522
+ client = _client()
523
+ try:
524
+ outcome = client.doi_lookup_with_title_check(FABRICATED_DOI, CN_TITLE)
525
+ assert outcome.state is DoiTitleState.NOT_FOUND
526
+ assert outcome.record is None
527
+ except ChineseLiteratureUnavailable: # pragma: no cover - failure path
528
+ pytest.fail("a DOI 404 must be a negative, not a degradation")
529
+
530
+
531
+ def test_doi_keyed_editorial_title_verifies_end_to_end():
532
+ from chinese_literature_client import DoiTitleState
533
+
534
+ transport = FakeTransport([
535
+ (_is_doi_resolve, _ok(b'{"title": "Editorial"}')),
536
+ ])
537
+ with patch("chinese_literature_client._safe_urlopen", transport):
538
+ outcome = _client().doi_lookup_with_title_check(ISTIC_DOI, "Editorial")
539
+ assert outcome.state is DoiTitleState.MATCH
540
+ assert outcome.record["title"] == "Editorial"
541
+
542
+
543
+ def test_doi_404_plus_existing_handle_is_title_unverifiable_never_false():
544
+ transport = FakeTransport([
545
+ (_is_ra, _ok(_body("ra_istic.json"))),
546
+ (_is_doi_resolve, _http_error(404)),
547
+ (_is_handle, _ok(_body("handle_exists.json"))),
548
+ ])
549
+ with patch("chinese_literature_client._safe_urlopen", transport):
550
+ result = _client().resolve(_istic_entry())
551
+
552
+ assert result["status"] == "unmatched"
553
+ assert result["queried_by"] == "title"
554
+ assert result["reason_code"] == "DOI_EXISTS_TITLE_UNVERIFIABLE"
555
+ assert result["checklist_item"]["verdict_contribution"] == "unresolvable"
556
+
557
+
558
+ def test_empty_expected_title_is_unverifiable_never_mismatch_or_false():
559
+ from chinese_literature_client import DoiTitleState
560
+
561
+ direct = FakeTransport([
562
+ (_is_doi_resolve, _ok(_body("istic_csl_hit.json"))),
563
+ ])
564
+ with patch("chinese_literature_client._safe_urlopen", direct):
565
+ outcome = _client().doi_lookup_with_title_check(ISTIC_DOI, "")
566
+ assert outcome.state is DoiTitleState.UNVERIFIABLE
567
+ assert outcome.record["title"] == CN_TITLE
568
+
569
+ transport = FakeTransport([
570
+ (_is_ra, _ok(_body("ra_istic.json"))),
571
+ (_is_doi_resolve, _ok(_body("istic_csl_hit.json"))),
572
+ ])
573
+ with patch("chinese_literature_client._safe_urlopen", transport):
574
+ result = _client().resolve(_istic_entry(title=""))
575
+ assert result["reason_code"] == "DOI_EXISTS_TITLE_UNVERIFIABLE"
576
+ assert result["queried_by"] == "title"
577
+ assert result["checklist_item"]["verdict_contribution"] == "unresolvable"
578
+
579
+
580
+ def test_normalized_empty_resolved_title_is_unverifiable_not_mismatch():
581
+ from chinese_literature_client import DoiTitleState
582
+
583
+ transport = FakeTransport([
584
+ (_is_doi_resolve, _ok('{"title": "。"}'.encode())),
585
+ ])
586
+ with patch("chinese_literature_client._safe_urlopen", transport):
587
+ outcome = _client().doi_lookup_with_title_check(ISTIC_DOI, CN_TITLE)
588
+ assert outcome.state is DoiTitleState.UNVERIFIABLE
589
+
590
+
591
+ # ------------------------------------------------------------- CNKI DOI branch
592
+
593
+
594
+ def test_cnki_doi_that_exists_is_never_matched():
595
+ """The CNKI RA serves no CSL-JSON and we refuse to parse its HTML, so
596
+ existence alone must degrade to title-keyed unmatched (-> unresolvable),
597
+ never `matched` — otherwise a real DOI with a fabricated title passes."""
598
+ transport = FakeTransport([
599
+ (_is_ra, _ok(_body("ra_cnki.json"))),
600
+ (_is_handle, _ok(_body("handle_exists.json"))),
601
+ (_is_doi_resolve, _http_error(500)), # must never be reached
602
+ ])
603
+ with patch("chinese_literature_client._safe_urlopen", transport):
604
+ result = _client().resolve(_istic_entry(doi=CNKI_DOI))
605
+
606
+ assert result["status"] == "unmatched"
607
+ assert result["queried_by"] == "title" # NOT "id" -> reduces to unresolvable
608
+ assert result["reason_code"] == "DOI_EXISTS_TITLE_UNVERIFIABLE"
609
+ item = result["checklist_item"]
610
+ assert item["priority"] == "P2"
611
+ assert item["verdict_contribution"] == "unresolvable"
612
+ assert item["verification_urls"] == ["https://doi.org/" + CNKI_DOI]
613
+ # Content negotiation is never attempted for a CNKI prefix.
614
+ assert len(transport.requests) == 2
615
+
616
+
617
+ def test_cnki_doi_that_does_not_exist_is_still_refuted():
618
+ transport = FakeTransport([
619
+ (_is_ra, _ok(_body("ra_cnki.json"))),
620
+ (_is_handle, _ok(_body("handle_absent.json"))),
621
+ ])
622
+ with patch("chinese_literature_client._safe_urlopen", transport):
623
+ result = _client().resolve(_istic_entry(doi=CNKI_DOI))
624
+
625
+ assert result["queried_by"] == "id"
626
+ assert result["reason_code"] == "DOI_REFUTED"
627
+
628
+
629
+ def test_non_csl_200_body_is_typed_unavailable():
630
+ """A 200 non-JSON body can be a proxy/CDN error page. It must fail closed,
631
+ not become routine human-check evidence."""
632
+ from chinese_literature_client import ChineseLiteratureUnavailable
633
+
634
+ transport = FakeTransport([
635
+ (_is_ra, _ok(_body("ra_istic.json"))),
636
+ (_is_doi_resolve, _ok(_body("cnki_landing_page.html"))),
637
+ ])
638
+ with patch("chinese_literature_client._safe_urlopen", transport):
639
+ with pytest.raises(ChineseLiteratureUnavailable):
640
+ _client().resolve(_istic_entry())
641
+
642
+
643
+ def test_parseable_csl_with_malformed_author_shape_is_typed_unavailable():
644
+ from chinese_literature_client import ChineseLiteratureUnavailable
645
+
646
+ malformed = ('{"title": "' + CN_TITLE + '", "author": 42}').encode()
647
+ transport = FakeTransport([(_is_doi_resolve, _ok(malformed))])
648
+ with patch("chinese_literature_client._safe_urlopen", transport):
649
+ with pytest.raises(ChineseLiteratureUnavailable):
650
+ _client().doi_lookup_with_title_check(ISTIC_DOI, CN_TITLE)
651
+
652
+
653
+ def test_parseable_csl_with_malformed_root_is_typed_unavailable():
654
+ from chinese_literature_client import ChineseLiteratureUnavailable
655
+
656
+ transport = FakeTransport([(_is_doi_resolve, _ok(b'[{"title": "x"}]'))])
657
+ with patch("chinese_literature_client._safe_urlopen", transport):
658
+ with pytest.raises(ChineseLiteratureUnavailable):
659
+ _client().doi_lookup_with_title_check(ISTIC_DOI, CN_TITLE)
660
+
661
+
662
+ @pytest.mark.parametrize("endpoint", ["ra", "csl"])
663
+ def test_deeply_nested_json_is_typed_unavailable(endpoint):
664
+ """A small but adversarial upstream body must not leak RecursionError."""
665
+ from chinese_literature_client import ChineseLiteratureUnavailable
666
+
667
+ body = b"[" * 2000 + b"]" * 2000
668
+ predicate = _is_ra if endpoint == "ra" else _is_doi_resolve
669
+ transport = FakeTransport([(predicate, _ok(body))])
670
+ with patch("chinese_literature_client._safe_urlopen", transport):
671
+ with pytest.raises(ChineseLiteratureUnavailable):
672
+ if endpoint == "ra":
673
+ _client().ra_for(ISTIC_DOI)
674
+ else:
675
+ _client().doi_lookup_with_title_check(ISTIC_DOI, CN_TITLE)
676
+
677
+
678
+ @pytest.mark.parametrize(
679
+ "body",
680
+ [
681
+ b'{"title": {"value": "x"}}',
682
+ b'{"title": [42]}',
683
+ b'{"title": []}',
684
+ b'{"title": "x", "author": [42]}',
685
+ b'{"title": "x", "author": false}',
686
+ b'{"title": "x", "author": [{"given": 42}]}',
687
+ b'{"title": "x", "issued": []}',
688
+ b'{"title": "x", "issued": {"date-parts": {}}}',
689
+ b'{"title": "x", "issued": {"date-parts": [[true]]}}',
690
+ b'{"title": "x", "issued": {"date-parts": [[-1]]}}',
691
+ '{"title": "x", "issued": {"date-parts": [["٢٠٢٦"]]}}'.encode(),
692
+ b'{"title": "x", "issued": {"date-parts": [["99999"]]}}',
693
+ b'{"title": "x", "volume": {"value": "1"}}',
694
+ b'{"title": "x", "DOI": ["10.5555/x"]}',
695
+ (b'{"title": "' + CN_TITLE.encode() + b'", "DOI": ""}'),
696
+ (b'{"title": "' + CN_TITLE.encode()
697
+ + b'", "DOI": "10.5555/a.different.record"}'),
698
+ ],
699
+ )
700
+ def test_parseable_csl_with_malformed_fields_is_typed_unavailable(body):
701
+ from chinese_literature_client import ChineseLiteratureUnavailable
702
+
703
+ transport = FakeTransport([(_is_doi_resolve, _ok(body))])
704
+ with patch("chinese_literature_client._safe_urlopen", transport):
705
+ with pytest.raises(ChineseLiteratureUnavailable):
706
+ _client().doi_lookup_with_title_check(ISTIC_DOI, CN_TITLE)
707
+
708
+
709
+ def test_handle_unknown_response_code_degrades():
710
+ """responseCode 2 (internal error) is an unknown state — degrade, do not
711
+ guess an existence answer."""
712
+ from chinese_literature_client import ChineseLiteratureUnavailable
713
+
714
+ transport = FakeTransport([(_is_handle, _ok(_body("handle_internal_error.json")))])
715
+ with patch("chinese_literature_client._safe_urlopen", transport):
716
+ with pytest.raises(ChineseLiteratureUnavailable):
717
+ _client().handle_exists(ISTIC_DOI)
718
+
719
+
720
+ @pytest.mark.parametrize(
721
+ "body",
722
+ [b'{"responseCode": true}', b'{"responseCode": 100.0}'],
723
+ )
724
+ def test_handle_bool_or_float_response_code_degrades(body):
725
+ from chinese_literature_client import ChineseLiteratureUnavailable
726
+
727
+ transport = FakeTransport([(_is_handle, _ok(body))])
728
+ with patch("chinese_literature_client._safe_urlopen", transport):
729
+ with pytest.raises(ChineseLiteratureUnavailable):
730
+ _client().handle_exists(ISTIC_DOI)
731
+
732
+
733
+ # --------------------------------------------------- PubMed coordinate branch
734
+
735
+
736
+ def _coordinate_entry(**overrides):
737
+ entry = {
738
+ "citation_key": "hecheng2026",
739
+ "title": CN_TITLE,
740
+ "container_title": CN_JOURNAL,
741
+ "year": 2026,
742
+ "volume": "88",
743
+ "pages": "8801-8809",
744
+ }
745
+ entry.update(overrides)
746
+ return entry
747
+
748
+
749
+ def _coordinate_client():
750
+ return _client(journal_map=TEST_JOURNAL_MAP)
751
+
752
+
753
+ def test_missing_coordinate_tuple_is_p3_without_a_pubmed_request():
754
+ with patch(
755
+ "chinese_literature_client._safe_urlopen",
756
+ side_effect=AssertionError("no PubMed request"),
757
+ ):
758
+ result = _coordinate_client().resolve(
759
+ _coordinate_entry(volume=None, pages=None, year=None)
760
+ )
761
+
762
+ assert result["status"] == "skipped"
763
+ assert result["queried_by"] is None
764
+ assert result["reason_code"] == "INSUFFICIENT_PUBMED_COORDINATES"
765
+ assert result["checklist_item"]["priority"] == "P3"
766
+ assert result["checklist_item"]["verdict_contribution"] == "unresolvable"
767
+
768
+
769
+ def test_lone_surrogate_eutils_parameter_is_typed_before_transport():
770
+ from chinese_literature_client import ChineseLiteratureUnavailable
771
+
772
+ with patch(
773
+ "chinese_literature_client._safe_urlopen",
774
+ side_effect=AssertionError("no request for unencodable EUtils term"),
775
+ ):
776
+ with pytest.raises(ChineseLiteratureUnavailable, match="invalid Unicode"):
777
+ _coordinate_client().journal_is_indexed("Journal " + chr(0xD800))
778
+
779
+
780
+ def test_volume_page_zero_hit_never_falls_back_to_author_year():
781
+ """A valid author/year candidate must not wash a cited wrong page into a
782
+ match after the stronger volume/page query returned zero."""
783
+ esearch_bodies = [
784
+ _body("esearch_coverage_hit.json"),
785
+ _body("esearch_coordinate_zero.json"),
786
+ ]
787
+ transport = FakeTransport([
788
+ (_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
789
+ ])
790
+ with patch("chinese_literature_client._safe_urlopen", transport):
791
+ result = _coordinate_client().resolve(
792
+ _coordinate_entry(pages="9999", first_author_pinyin="Hecheng Ceshi")
793
+ )
794
+
795
+ assert result["reason_code"] == "PUBMED_INDEXED_BUT_COORDINATE_MISS"
796
+ assert len(transport.requests) == 2 # coverage + volume/page, no fallback
797
+ assert result["checklist_item"]["attempts"][-1]["detail"] == "volume_page"
798
+
799
+
800
+ def test_pubmed_candidate_page_conflict_cannot_be_promoted():
801
+ esearch_bodies = [
802
+ _body("esearch_coverage_hit.json"),
803
+ _body("esearch_coordinate_hit.json"),
804
+ ]
805
+ transport = FakeTransport([
806
+ (_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
807
+ (_is_esummary, _ok(_body("esummary_hit.json"))),
808
+ ])
809
+ with patch("chinese_literature_client._safe_urlopen", transport):
810
+ result = _coordinate_client().resolve(_coordinate_entry(pages="9999"))
811
+
812
+ assert result["reason_code"] == "PUBMED_COORDINATE_CANDIDATE_UNVERIFIED"
813
+ assert result["status"] == "unmatched"
814
+ assert "first page" in result["checklist_item"]["human_action"]
815
+ assert len(transport.requests) == 3 # no DOI/RA lookup after conflict
816
+
817
+
818
+ def test_pubmed_candidate_volume_conflict_cannot_be_promoted():
819
+ """A unique hit with a different volume stops before DOI/title binding."""
820
+ esearch_bodies = [
821
+ _body("esearch_coverage_hit.json"),
822
+ _body("esearch_coordinate_hit.json"),
823
+ ]
824
+ transport = FakeTransport([
825
+ (_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
826
+ (_is_esummary, _ok(_body("esummary_hit.json"))),
827
+ ])
828
+ with patch("chinese_literature_client._safe_urlopen", transport):
829
+ result = _coordinate_client().resolve(_coordinate_entry(volume="99"))
830
+
831
+ assert result["reason_code"] == "PUBMED_COORDINATE_CANDIDATE_UNVERIFIED"
832
+ assert result["status"] == "unmatched"
833
+ assert "volume" in result["checklist_item"]["human_action"]
834
+ assert len(transport.requests) == 3 # no DOI/RA lookup after conflict
835
+
836
+
837
+ def test_pubmed_coordinate_candidate_is_matched_only_after_istic_title_binding():
838
+ """Coordinates nominate a candidate; its ISTIC DOI and Chinese CSL title
839
+ provide the evidence that upgrades it to a title-keyed match."""
840
+ esearch_bodies = [_body("esearch_coverage_hit.json"),
841
+ _body("esearch_coordinate_hit.json")]
842
+ transport = FakeTransport([
843
+ (_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
844
+ (_is_esummary, _ok(_body("esummary_hit.json"))),
845
+ (_is_ra, _ok(_body("ra_istic.json"))),
846
+ (_is_doi_resolve, _ok(_body("istic_csl_hit.json"))),
847
+ ])
848
+ with patch("chinese_literature_client._safe_urlopen", transport):
849
+ result = _coordinate_client().resolve(_coordinate_entry())
850
+
851
+ assert result["status"] == "matched"
852
+ assert result["queried_by"] == "title"
853
+ assert result["reason_code"] == "PUBMED_COORDINATE_VERIFIED"
854
+ assert result["evidence"]["pmid"] == "999999901"
855
+ assert result["evidence"]["doi"] == ISTIC_DOI
856
+ assert result["evidence"]["doi_metadata"]["title"] == CN_TITLE
857
+ # PubMed stores the ENGLISH bracketed shadow title for Chinese articles, so
858
+ # a Chinese title cross-check is structurally impossible on this branch —
859
+ # the shadow title is carried for the human instead.
860
+ assert result["evidence"]["english_title"].startswith("[")
861
+ assert result["checklist_item"] is None
862
+
863
+
864
+ def test_pubmed_coordinate_miss_is_p1_never_false():
865
+ """The single most important self-restraint in the design: coverage is
866
+ confirmed and the coordinates return nothing, yet the outcome stays
867
+ title-keyed (-> unresolvable). The strength goes into the P1 priority, not
868
+ into the verdict — the journal bridge is heuristic and PubMed indexes
869
+ Chinese journals selectively."""
870
+ esearch_bodies = [_body("esearch_coverage_hit.json"),
871
+ _body("esearch_coordinate_zero.json")]
872
+ transport = FakeTransport([
873
+ (_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
874
+ ])
875
+ with patch("chinese_literature_client._safe_urlopen", transport):
876
+ result = _coordinate_client().resolve(_coordinate_entry(pages="9999"))
877
+
878
+ assert result["status"] == "unmatched"
879
+ assert result["queried_by"] == "title"
880
+ assert result["reason_code"] == "PUBMED_INDEXED_BUT_COORDINATE_MISS"
881
+ item = result["checklist_item"]
882
+ assert item["priority"] == "P1"
883
+ assert item["verdict_contribution"] == "unresolvable"
884
+ # Wording discipline: fabrication vocabulary is P0-only.
885
+ assert "pending human check" in item["human_action"].lower()
886
+ assert "fabricat" not in item["human_action"].lower()
887
+
888
+
889
+ def test_pubmed_coordinate_ambiguity_never_picks_one():
890
+ esearch_bodies = [_body("esearch_coverage_hit.json"),
891
+ _body("esearch_coordinate_ambiguous.json")]
892
+ transport = FakeTransport([
893
+ (_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
894
+ ])
895
+ with patch("chinese_literature_client._safe_urlopen", transport):
896
+ result = _coordinate_client().resolve(_coordinate_entry())
897
+
898
+ assert result["reason_code"] == "PUBMED_COORDINATE_AMBIGUOUS"
899
+ assert result["queried_by"] == "title"
900
+ assert result["evidence"] is None
901
+ assert result["checklist_item"]["priority"] == "P1"
902
+
903
+
904
+ def test_pubmed_year_disagreement_demotes_the_hit():
905
+ esearch_bodies = [_body("esearch_coverage_hit.json"),
906
+ _body("esearch_coordinate_hit.json")]
907
+ transport = FakeTransport([
908
+ (_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
909
+ (_is_esummary, _ok(_body("esummary_year_mismatch.json"))),
910
+ ])
911
+ with patch("chinese_literature_client._safe_urlopen", transport):
912
+ result = _coordinate_client().resolve(_coordinate_entry())
913
+
914
+ assert result["status"] == "unmatched"
915
+ assert result["queried_by"] == "title"
916
+ assert result["reason_code"] == "PUBMED_COORDINATE_CANDIDATE_UNVERIFIED"
917
+
918
+
919
+ def test_pubmed_issn_disagreement_demotes_the_hit():
920
+ """The coordinate hit's corroboration is structural, so the ISSN the record
921
+ echoes must be the ISSN we bridged through — a disagreement means the
922
+ journal-name mapping, not the citation, is likely at fault."""
923
+ esearch_bodies = [_body("esearch_coverage_hit.json"),
924
+ _body("esearch_coordinate_hit.json")]
925
+ transport = FakeTransport([
926
+ (_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
927
+ (_is_esummary, _ok(_body("esummary_issn_mismatch.json"))),
928
+ ])
929
+ with patch("chinese_literature_client._safe_urlopen", transport):
930
+ result = _coordinate_client().resolve(_coordinate_entry())
931
+
932
+ assert result["status"] == "unmatched"
933
+ assert result["queried_by"] == "title"
934
+ assert result["reason_code"] == "PUBMED_COORDINATE_CANDIDATE_UNVERIFIED"
935
+ assert "ISSN" in result["checklist_item"]["human_action"]
936
+
937
+
938
+ def test_absent_year_does_not_block_doi_title_binding():
939
+ """Absence is not mismatch: exact Chinese title binding still verifies."""
940
+ esearch_bodies = [_body("esearch_coverage_hit.json"),
941
+ _body("esearch_coordinate_hit.json")]
942
+ transport = FakeTransport([
943
+ (_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
944
+ (_is_esummary, _ok(_body("esummary_hit.json"))),
945
+ (_is_ra, _ok(_body("ra_istic.json"))),
946
+ (_is_doi_resolve, _ok(_body("istic_csl_hit.json"))),
947
+ ])
948
+ with patch("chinese_literature_client._safe_urlopen", transport):
949
+ result = _coordinate_client().resolve(_coordinate_entry(year=None))
950
+
951
+ assert result["status"] == "matched"
952
+
953
+
954
+ def test_pubmed_coordinate_candidate_without_doi_is_never_matched():
955
+ esearch_bodies = [_body("esearch_coverage_hit.json"),
956
+ _body("esearch_coordinate_hit.json")]
957
+ transport = FakeTransport([
958
+ (_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
959
+ (_is_esummary, _ok(_body("esummary_no_doi.json"))),
960
+ ])
961
+ with patch("chinese_literature_client._safe_urlopen", transport):
962
+ result = _coordinate_client().resolve(_coordinate_entry())
963
+
964
+ assert result["status"] == "unmatched"
965
+ assert result["queried_by"] == "title"
966
+ assert result["reason_code"] == "PUBMED_COORDINATE_CANDIDATE_UNVERIFIED"
967
+ assert result["checklist_item"]["verdict_contribution"] == "unresolvable"
968
+
969
+
970
+ def test_pubmed_candidate_doi_title_mismatch_never_washes_fake_chinese_title():
971
+ esearch_bodies = [_body("esearch_coverage_hit.json"),
972
+ _body("esearch_coordinate_hit.json")]
973
+ transport = FakeTransport([
974
+ (_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
975
+ (_is_esummary, _ok(_body("esummary_hit.json"))),
976
+ (_is_ra, _ok(_body("ra_istic.json"))),
977
+ (_is_doi_resolve, _ok(_body("istic_csl_other_title.json"))),
978
+ ])
979
+ with patch("chinese_literature_client._safe_urlopen", transport):
980
+ result = _coordinate_client().resolve(_coordinate_entry())
981
+
982
+ assert result["status"] == "unmatched"
983
+ assert result["queried_by"] == "title"
984
+ assert result["reason_code"] == "PUBMED_COORDINATE_CANDIDATE_UNVERIFIED"
985
+ assert result["checklist_item"]["verdict_contribution"] == "unresolvable"
986
+
987
+
988
+ @pytest.mark.parametrize(
989
+ "esummary_fixture,ra_fixture",
990
+ [
991
+ ("esummary_hit.json", "ra_crossref.json"),
992
+ ("esummary_unknown_ra.json", "ra_unknown_prefix.json"),
993
+ ],
994
+ )
995
+ def test_pubmed_candidate_out_of_scope_or_unknown_ra_is_never_matched(
996
+ esummary_fixture, ra_fixture,
997
+ ):
998
+ esearch_bodies = [_body("esearch_coverage_hit.json"),
999
+ _body("esearch_coordinate_hit.json")]
1000
+ transport = FakeTransport([
1001
+ (_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
1002
+ (_is_esummary, _ok(_body(esummary_fixture))),
1003
+ (_is_ra, _ok(_body(ra_fixture))),
1004
+ ])
1005
+ with patch("chinese_literature_client._safe_urlopen", transport):
1006
+ result = _coordinate_client().resolve(_coordinate_entry())
1007
+
1008
+ assert result["status"] == "unmatched"
1009
+ assert result["queried_by"] == "title"
1010
+ assert result["reason_code"] == "PUBMED_COORDINATE_CANDIDATE_UNVERIFIED"
1011
+
1012
+
1013
+ def test_pubmed_candidate_cnki_handle_existence_is_never_matched():
1014
+ esearch_bodies = [_body("esearch_coverage_hit.json"),
1015
+ _body("esearch_coordinate_hit.json")]
1016
+ transport = FakeTransport([
1017
+ (_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
1018
+ (_is_esummary, _ok(_body("esummary_hit.json"))),
1019
+ (_is_ra, _ok(_body("ra_cnki.json"))),
1020
+ (_is_handle, _ok(_body("handle_exists.json"))),
1021
+ ])
1022
+ with patch("chinese_literature_client._safe_urlopen", transport):
1023
+ result = _coordinate_client().resolve(_coordinate_entry())
1024
+
1025
+ assert result["status"] == "unmatched"
1026
+ assert result["reason_code"] == "PUBMED_COORDINATE_CANDIDATE_UNVERIFIED"
1027
+ assert any(
1028
+ attempt["stage"] == "pubmed_candidate_handle"
1029
+ and attempt["outcome"] == "exists"
1030
+ for attempt in result["checklist_item"]["attempts"]
1031
+ )
1032
+
1033
+
1034
+ def test_journal_catalogued_but_not_indexed_is_skipped_not_unmatched():
1035
+ """'In the NLM Catalog' != 'indexed in PubMed' (中国全科医学 is catalogued
1036
+ with zero PubMed articles). A journal PubMed never indexed must produce
1037
+ `skipped` + a P3 row, because a coordinate miss against it says nothing."""
1038
+ transport = FakeTransport([
1039
+ (_is_esearch, _ok(_body("esearch_coverage_zero.json"))),
1040
+ ])
1041
+ with patch("chinese_literature_client._safe_urlopen", transport):
1042
+ result = _coordinate_client().resolve(_coordinate_entry())
1043
+
1044
+ assert result["status"] == "skipped"
1045
+ assert result["queried_by"] is None
1046
+ assert result["reason_code"] == "JOURNAL_NOT_INDEXED"
1047
+ assert result["checklist_item"]["priority"] == "P3"
1048
+ # Coverage confirmation only — the coordinate query is never issued.
1049
+ assert len(transport.requests) == 1
1050
+
1051
+
1052
+ def test_unmapped_journal_is_skipped_with_a_p3_row():
1053
+ """A gap in OUR bridge table is never evidence about the citation."""
1054
+ transport = FakeTransport([])
1055
+ with patch("chinese_literature_client._safe_urlopen", transport):
1056
+ result = _coordinate_client().resolve(
1057
+ _coordinate_entry(container_title="某本尚未映射的中文期刊"))
1058
+
1059
+ assert result["status"] == "skipped"
1060
+ assert result["reason_code"] == "NO_ISSN_MAPPING"
1061
+ assert result["checklist_item"]["priority"] == "P3"
1062
+ assert transport.requests == []
1063
+
1064
+
1065
+ def test_seed_journal_map_rows_are_lookup_reachable():
1066
+ """The shipped seed rows (all verified live 2026-07-27) must be reachable
1067
+ through the same normalization the resolver applies, including 《》 wrapping."""
1068
+ client = _client()
1069
+ row = client.journal_bridge("中华医学杂志")
1070
+ assert row is not None and row["nlm_ta"] == "Zhonghua Yi Xue Za Zhi"
1071
+ assert client.journal_bridge("《中华医学杂志》") == row
1072
+ assert client.journal_bridge("中华内科杂志")["issn"] == "0578-1426"
1073
+
1074
+
1075
+ def test_caller_journal_map_extends_the_seed():
1076
+ """Documented user extension point: caller rows merge over the seed."""
1077
+ client = _client(journal_map=TEST_JOURNAL_MAP)
1078
+ assert client.journal_bridge(CN_JOURNAL)["nlm_ta"] == "Hecheng Ceshi Yi Xue Za Zhi"
1079
+ assert client.journal_bridge("中华医学杂志") is not None # seed still present
1080
+
1081
+
1082
+ def test_ncbi_api_key_is_passed_through_but_does_not_relax_pacing():
1083
+ from chinese_literature_client import _EUTILS_MIN_INTERVAL
1084
+
1085
+ transport = FakeTransport([(_is_esearch, _ok(_body("esearch_coverage_hit.json")))])
1086
+ with patch("chinese_literature_client._safe_urlopen", transport):
1087
+ client = _client(ncbi_api_key="test-ncbi-key")
1088
+ client.journal_is_indexed("Hecheng Ceshi Yi Xue Za Zhi")
1089
+
1090
+ assert "api_key=test-ncbi-key" in transport.requests[0]
1091
+ assert "tool=academic-research-skills" in transport.requests[0]
1092
+ assert "email=ars-tests%40example.invalid" in transport.requests[0]
1093
+ assert _EUTILS_MIN_INTERVAL == 0.34 # the keyless NCBI floor, unconditionally
1094
+
1095
+
1096
+ def test_ncbi_email_is_required_before_any_eutils_request():
1097
+ from chinese_literature_client import (
1098
+ ChineseLiteratureClient,
1099
+ ChineseLiteratureUnavailable,
1100
+ )
1101
+
1102
+ with patch("chinese_literature_client._safe_urlopen", side_effect=AssertionError("no request")):
1103
+ with pytest.raises(ChineseLiteratureUnavailable, match="ncbi_email"):
1104
+ ChineseLiteratureClient().journal_is_indexed("Hecheng Ceshi Yi Xue Za Zhi")
1105
+
1106
+
1107
+ def test_pubmed_author_fallback_uses_first_author_tag():
1108
+ bodies = [_body("esearch_coordinate_zero.json")]
1109
+ transport = FakeTransport([
1110
+ (_is_esearch, lambda url: _ok(bodies.pop(0))(url)),
1111
+ ])
1112
+ with patch("chinese_literature_client._safe_urlopen", transport):
1113
+ _client().pubmed_coordinate_lookup(
1114
+ nlm_ta="Hecheng Ceshi Yi Xue Za Zhi",
1115
+ first_author="Hecheng Ceshi",
1116
+ year=2026,
1117
+ )
1118
+ term = urllib.parse.parse_qs(
1119
+ urllib.parse.urlsplit(transport.requests[0]).query
1120
+ )["term"][0]
1121
+ assert '"Hecheng Ceshi"[1au]' in term
1122
+ assert "[au]" not in term
1123
+
1124
+
1125
+ @pytest.mark.parametrize(
1126
+ "kwargs",
1127
+ [
1128
+ {"volume": "88[vi] OR 1", "pages": "8801"},
1129
+ {"volume": "88", "pages": "8801[pg] OR 1"},
1130
+ {"volume": "1:9999", "pages": "8801"},
1131
+ {"first_author": 'Chen" OR cancer[ti]', "year": 2026},
1132
+ ],
1133
+ )
1134
+ def test_untrusted_pubmed_fields_cannot_inject_entrez_grammar(kwargs):
1135
+ from chinese_literature_client import ChineseLiteratureUnavailable
1136
+
1137
+ with patch("chinese_literature_client._safe_urlopen", side_effect=AssertionError("no request")):
1138
+ with pytest.raises(ChineseLiteratureUnavailable):
1139
+ _client().pubmed_coordinate_lookup(
1140
+ nlm_ta="Hecheng Ceshi Yi Xue Za Zhi", **kwargs
1141
+ )
1142
+
1143
+
1144
+ # -------------------------------------------------- URL hygiene / credentials
1145
+
1146
+
1147
+ def test_malicious_doi_is_encoded_and_never_crashes():
1148
+ """A DOI carrying CRLF / query-injection characters must be percent-encoded
1149
+ before it reaches the transport (sibling quote discipline) — no raw control
1150
+ character or `?`/`#` may survive into the request line, and the client must
1151
+ never escape its own degradation contract over a hostile identifier."""
1152
+ from chinese_literature_client import ChineseLiteratureUnavailable
1153
+
1154
+ evil = "10.5555/evil\r\nHost: attacker?x=1#frag"
1155
+ transport = FakeTransport([
1156
+ (_is_handle, _ok(_body("handle_absent.json"))),
1157
+ ])
1158
+ with patch("chinese_literature_client._safe_urlopen", transport):
1159
+ assert _client().handle_exists(evil) is False
1160
+
1161
+ url = transport.requests[0]
1162
+ assert "\r" not in url and "\n" not in url
1163
+ assert "?" not in url and "#" not in url # no query/fragment injection
1164
+ assert "%0D%0A" in url # CRLF arrived encoded, not stripped
1165
+
1166
+ # Defense in depth: even if a malformed request slips past encoding and
1167
+ # urlopen rejects it (http.client.InvalidURL), the client degrades cleanly.
1168
+ def reject(req, timeout=None):
1169
+ raise http.client.InvalidURL("URL can't contain control characters")
1170
+
1171
+ with patch("chinese_literature_client._safe_urlopen", reject):
1172
+ with pytest.raises(ChineseLiteratureUnavailable):
1173
+ _client().handle_exists(evil)
1174
+
1175
+
1176
+ def test_api_key_never_lands_in_exception_text():
1177
+ """#495 red line (mirrors crossref/openalex): refusal/degradation messages
1178
+ strip the query string so a configured NCBI api_key can never reach logs or
1179
+ raised-exception text — checked across the 5xx, network-error and
1180
+ unparseable-body paths."""
1181
+ from chinese_literature_client import ChineseLiteratureUnavailable
1182
+
1183
+ key = "test-ncbi-key"
1184
+
1185
+ scenarios = [
1186
+ FakeTransport([(_is_esearch, _http_error(503))]),
1187
+ FakeTransport([(_is_esearch, _ok(_body("error_5xx.html")))]),
1188
+ ]
1189
+ for transport in scenarios:
1190
+ with patch("chinese_literature_client._safe_urlopen", transport):
1191
+ with pytest.raises(ChineseLiteratureUnavailable) as excinfo:
1192
+ _client(ncbi_api_key=key).journal_is_indexed("Hecheng Ceshi")
1193
+ assert key not in str(excinfo.value)
1194
+ assert "api_key" not in str(excinfo.value)
1195
+ assert "?" not in str(excinfo.value) # whole query stripped, not just the key
1196
+
1197
+ def net_error(req, timeout=None):
1198
+ raise urllib.error.URLError("connection refused")
1199
+
1200
+ with patch("chinese_literature_client._safe_urlopen", net_error):
1201
+ with pytest.raises(ChineseLiteratureUnavailable) as excinfo:
1202
+ _client(ncbi_api_key=key).journal_is_indexed("Hecheng Ceshi")
1203
+ assert key not in str(excinfo.value)
1204
+ assert "?" not in str(excinfo.value)
1205
+
1206
+
1207
+ @pytest.mark.parametrize("failure_kind", ["http", "url"])
1208
+ def test_reflected_request_url_is_absent_from_full_exception_traceback(failure_kind):
1209
+ """Removing a secret from the wrapper message is insufficient when the
1210
+ original exception remains as an explicit cause: logging the traceback
1211
+ would print that cause and its full query string."""
1212
+ from chinese_literature_client import ChineseLiteratureUnavailable
1213
+
1214
+ key = "SECRET123"
1215
+
1216
+ def reflected(req, timeout=None):
1217
+ if failure_kind == "http":
1218
+ raise urllib.error.HTTPError(
1219
+ url=req.full_url,
1220
+ code=503,
1221
+ msg=f"upstream reflected {req.full_url}",
1222
+ hdrs={},
1223
+ fp=io.BytesIO(),
1224
+ )
1225
+ raise urllib.error.URLError(f"upstream reflected {req.full_url}")
1226
+
1227
+ with patch("chinese_literature_client._safe_urlopen", reflected):
1228
+ with pytest.raises(ChineseLiteratureUnavailable) as excinfo:
1229
+ _client(ncbi_api_key=key).journal_is_indexed("Hecheng Ceshi")
1230
+
1231
+ rendered = "".join(traceback.format_exception(
1232
+ excinfo.type, excinfo.value, excinfo.tb,
1233
+ ))
1234
+ assert key not in rendered
1235
+ assert f"api_key={key}" not in rendered
1236
+
1237
+
1238
+ @pytest.mark.parametrize("error_url", ["", "http://evil.invalid/stolen"])
1239
+ def test_rejected_http_error_url_does_not_retain_reflected_secret(error_url):
1240
+ from chinese_literature_client import ChineseLiteratureUnavailable
1241
+
1242
+ key = "SECRET123"
1243
+
1244
+ def reflected(req, timeout=None):
1245
+ raise urllib.error.HTTPError(
1246
+ url=error_url,
1247
+ code=503,
1248
+ msg=f"upstream reflected {req.full_url}",
1249
+ hdrs={},
1250
+ fp=io.BytesIO(),
1251
+ )
1252
+
1253
+ with patch("chinese_literature_client._safe_urlopen", reflected):
1254
+ with pytest.raises(ChineseLiteratureUnavailable) as excinfo:
1255
+ _client(ncbi_api_key=key).journal_is_indexed("Hecheng Ceshi")
1256
+
1257
+ rendered = "".join(traceback.format_exception(
1258
+ excinfo.type, excinfo.value, excinfo.tb,
1259
+ ))
1260
+ assert key not in rendered
1261
+
1262
+
1263
+ def test_malformed_port_does_not_retain_secret_as_a_cause():
1264
+ from chinese_literature_client import ChineseLiteratureUnavailable, _require_api_url
1265
+
1266
+ key = "SECRET123"
1267
+ with pytest.raises(ChineseLiteratureUnavailable) as excinfo:
1268
+ _require_api_url(f"https://doi.org:{key}/path?api_key={key}")
1269
+
1270
+ rendered = "".join(traceback.format_exception(
1271
+ excinfo.type, excinfo.value, excinfo.tb,
1272
+ ))
1273
+ assert key not in rendered
1274
+
1275
+
1276
+ def test_host_allowlist_guard_refuses_and_redacts():
1277
+ """Multi-host `_require_api_url` (sibling defense-in-depth): a non-HTTPS or
1278
+ non-allowlisted host is refused before any request, and the refusal text is
1279
+ query-stripped."""
1280
+ from chinese_literature_client import (
1281
+ ChineseLiteratureUnavailable,
1282
+ _require_api_url,
1283
+ )
1284
+
1285
+ _require_api_url("https://doi.org/doiRA/10.5555") # allowlisted: no raise
1286
+ _require_api_url("https://eutils.ncbi.nlm.nih.gov/x") # allowlisted: no raise
1287
+ for bad in (
1288
+ "https://www.cnki.net/search?q=x", # zero-scraping red line
1289
+ "http://doi.org/doiRA/10.5555", # https only
1290
+ "https://evil.invalid/a?api_key=test-ncbi-key",
1291
+ "https://127.0.0.1/doiRA/10.5555", # no bare IPs
1292
+ "https://user@doi.org/doiRA/10.5555", # no userinfo
1293
+ "https://doi.org:443/doiRA/10.5555", # exact origin, no ports
1294
+ ):
1295
+ with pytest.raises(ChineseLiteratureUnavailable) as excinfo:
1296
+ _require_api_url(bad)
1297
+ assert "api_key" not in str(excinfo.value)
1298
+ assert "?" not in str(excinfo.value)
1299
+
1300
+
1301
+ def test_redirect_handler_revalidates_every_destination_before_following():
1302
+ from chinese_literature_client import (
1303
+ ChineseLiteratureUnavailable,
1304
+ _SafeRedirectHandler,
1305
+ )
1306
+
1307
+ handler = _SafeRedirectHandler()
1308
+ request = urllib.request.Request("https://doi.org/10.5555/example")
1309
+ for target in (
1310
+ "http://doi.org/10.5555/example", # downgrade
1311
+ "https://127.0.0.1/10.5555/example", # bare IP
1312
+ "https://user@doi.org/10.5555/example", # userinfo
1313
+ "https://doi.org:443/10.5555/example", # explicit port
1314
+ "https://doi.org.evil.invalid/example", # suffix confusion
1315
+ "https://evil.invalid/example", # external host
1316
+ ):
1317
+ with pytest.raises(ChineseLiteratureUnavailable):
1318
+ handler.redirect_request(request, None, 302, "Found", {}, target)
1319
+
1320
+ redirected = handler.redirect_request(
1321
+ request, None, 302, "Found", {},
1322
+ "https://hdl.handle.net/api/handles/10.5555/example",
1323
+ )
1324
+ assert redirected.full_url.startswith("https://hdl.handle.net/")
1325
+
1326
+
1327
+ @pytest.mark.parametrize("code", [301, 302, 303, 307, 308])
1328
+ def test_redirect_response_body_is_never_read_before_following(code):
1329
+ from chinese_literature_client import _SafeRedirectHandler
1330
+
1331
+ handler = _SafeRedirectHandler()
1332
+ handler.parent = MagicMock()
1333
+ request = urllib.request.Request("https://doi.org/10.5555/example")
1334
+ request.timeout = 30
1335
+ response = MagicMock()
1336
+ redirected_response = object()
1337
+ handler.parent.open.return_value = redirected_response
1338
+
1339
+ result = getattr(handler, f"http_error_{code}")(
1340
+ request,
1341
+ response,
1342
+ code,
1343
+ "Found",
1344
+ {"location": "/10.5555/next"},
1345
+ )
1346
+
1347
+ assert result is redirected_response
1348
+ response.read.assert_not_called()
1349
+ response.close.assert_called_once_with()
1350
+ followed = handler.parent.open.call_args.args[0]
1351
+ assert followed.full_url == "https://doi.org/10.5555/next"
1352
+
1353
+
1354
+ @pytest.mark.parametrize(
1355
+ "location",
1356
+ ["http://doi.org/downgrade", "https://evil.invalid/escape", 42],
1357
+ )
1358
+ def test_rejected_redirect_is_closed_without_reading(location):
1359
+ from chinese_literature_client import (
1360
+ ChineseLiteratureUnavailable,
1361
+ _SafeRedirectHandler,
1362
+ )
1363
+
1364
+ handler = _SafeRedirectHandler()
1365
+ request = urllib.request.Request("https://doi.org/10.5555/example")
1366
+ request.timeout = 30
1367
+ response = MagicMock()
1368
+
1369
+ with pytest.raises(ChineseLiteratureUnavailable):
1370
+ handler.http_error_302(
1371
+ request, response, 302, "Found", {"location": location},
1372
+ )
1373
+
1374
+ response.read.assert_not_called()
1375
+ response.close.assert_called_once_with()
1376
+
1377
+
1378
+ def test_redirect_without_location_is_closed_without_reading():
1379
+ from chinese_literature_client import (
1380
+ ChineseLiteratureUnavailable,
1381
+ _SafeRedirectHandler,
1382
+ )
1383
+
1384
+ handler = _SafeRedirectHandler()
1385
+ request = urllib.request.Request("https://doi.org/10.5555/example")
1386
+ request.timeout = 30
1387
+ response = MagicMock()
1388
+
1389
+ with pytest.raises(ChineseLiteratureUnavailable, match="missing a Location"):
1390
+ handler.http_error_302(request, response, 302, "Found", {})
1391
+
1392
+ response.read.assert_not_called()
1393
+ response.close.assert_called_once_with()
1394
+
1395
+
1396
+ def test_istic_plaintext_redirect_stops_resolve_before_any_verdict():
1397
+ """End-to-end regression for the maintainer's live ISTIC observation.
1398
+
1399
+ RA triage may identify ISTIC, but the subsequent DOI request currently has
1400
+ the shape ``HTTPS doi.org -> HTTP bare IP``. Exercise the real resolver and
1401
+ redirect handler together: the unsafe hop must be closed unread, propagate
1402
+ as typed degradation, and never return a MATCH/MISMATCH-shaped result.
1403
+ """
1404
+ from chinese_literature_client import (
1405
+ ChineseLiteratureUnavailable,
1406
+ _SafeRedirectHandler,
1407
+ )
1408
+
1409
+ requests: list[str] = []
1410
+ redirect_response = MagicMock()
1411
+
1412
+ def transport(req, timeout=None):
1413
+ url = req.full_url
1414
+ requests.append(url)
1415
+ parsed = urllib.parse.urlsplit(url)
1416
+ query = (
1417
+ urllib.parse.parse_qs(
1418
+ parsed.query, keep_blank_values=True, strict_parsing=True,
1419
+ )
1420
+ if parsed.query else {}
1421
+ )
1422
+ if _is_ra(parsed, query):
1423
+ return _ok(_body("ra_istic.json"))(url)
1424
+ if _is_doi_resolve(parsed, query):
1425
+ handler = _SafeRedirectHandler()
1426
+ return handler.http_error_302(
1427
+ req,
1428
+ redirect_response,
1429
+ 302,
1430
+ "Found",
1431
+ {"location": "http://122.115.55.36:8000/doi/synthetic"},
1432
+ )
1433
+ raise AssertionError(f"client contacted an unrouted URL: {url}")
1434
+
1435
+ with patch("chinese_literature_client._safe_urlopen", transport):
1436
+ with pytest.raises(ChineseLiteratureUnavailable):
1437
+ _client().resolve(_istic_entry())
1438
+
1439
+ assert len(requests) == 2 # RA triage, then the refused DOI request
1440
+ redirect_response.read.assert_not_called()
1441
+ redirect_response.close.assert_called_once_with()
1442
+
1443
+
1444
+ def test_final_response_url_is_revalidated_even_after_transport_returns():
1445
+ from chinese_literature_client import ChineseLiteratureUnavailable
1446
+
1447
+ response = MagicMock()
1448
+ response.status = 200
1449
+ response.headers = {}
1450
+ response.geturl.return_value = "http://127.0.0.1/stolen"
1451
+ response.read.return_value = _body("ra_istic.json")
1452
+ response.__enter__ = MagicMock(return_value=response)
1453
+ response.__exit__ = MagicMock(return_value=None)
1454
+
1455
+ with patch("chinese_literature_client._safe_urlopen", return_value=response):
1456
+ with pytest.raises(ChineseLiteratureUnavailable):
1457
+ _client().ra_for(ISTIC_DOI)
1458
+ response.read.assert_not_called()
1459
+
1460
+
1461
+ def test_http_error_final_url_is_revalidated_before_404_becomes_data():
1462
+ from chinese_literature_client import ChineseLiteratureUnavailable
1463
+
1464
+ def escaped_404(req, timeout=None):
1465
+ raise urllib.error.HTTPError(
1466
+ url="http://127.0.0.1/not-found",
1467
+ code=404,
1468
+ msg="synthetic",
1469
+ hdrs={},
1470
+ fp=io.BytesIO(b""),
1471
+ )
1472
+
1473
+ with patch("chinese_literature_client._safe_urlopen", escaped_404):
1474
+ with pytest.raises(ChineseLiteratureUnavailable):
1475
+ _client().doi_lookup_with_title_check(FABRICATED_DOI, CN_TITLE)
1476
+
1477
+
1478
+ @pytest.mark.parametrize("code", [404, 503])
1479
+ def test_http_error_response_is_closed_without_reading(code):
1480
+ from chinese_literature_client import ChineseLiteratureUnavailable
1481
+
1482
+ error_body = MagicMock()
1483
+
1484
+ def fail(req, timeout=None):
1485
+ raise urllib.error.HTTPError(
1486
+ url=req.full_url,
1487
+ code=code,
1488
+ msg="synthetic",
1489
+ hdrs={},
1490
+ fp=error_body,
1491
+ )
1492
+
1493
+ with patch("chinese_literature_client._safe_urlopen", fail):
1494
+ if code == 404:
1495
+ assert _client().handle_exists(FABRICATED_DOI) is False
1496
+ else:
1497
+ with pytest.raises(ChineseLiteratureUnavailable, match="HTTP 503"):
1498
+ _client().handle_exists(FABRICATED_DOI)
1499
+
1500
+ error_body.read.assert_not_called()
1501
+ error_body.close.assert_called_once_with()
1502
+
1503
+
1504
+ @pytest.mark.parametrize("status", [None, 0, 503, "200"])
1505
+ def test_returned_invalid_or_non_success_status_is_rejected_before_read(status):
1506
+ from chinese_literature_client import ChineseLiteratureUnavailable
1507
+
1508
+ response = MagicMock()
1509
+ response.status = status
1510
+ response.headers = {}
1511
+ response.geturl.return_value = "https://doi.org/doiRA/10.5555"
1512
+ response.__enter__ = MagicMock(return_value=response)
1513
+ response.__exit__ = MagicMock(return_value=None)
1514
+
1515
+ with patch("chinese_literature_client._safe_urlopen", return_value=response):
1516
+ with pytest.raises(ChineseLiteratureUnavailable):
1517
+ _client().ra_for(ISTIC_DOI)
1518
+ response.read.assert_not_called()
1519
+
1520
+
1521
+ def test_returned_404_is_data_only_on_an_allow_404_path():
1522
+ response = MagicMock()
1523
+ response.status = 404
1524
+ response.headers = {}
1525
+ response.geturl.return_value = "https://doi.org/doiRA/10.5555"
1526
+ response.__enter__ = MagicMock(return_value=response)
1527
+ response.__exit__ = MagicMock(return_value=None)
1528
+
1529
+ with patch("chinese_literature_client._safe_urlopen", return_value=response):
1530
+ assert _client().ra_for(ISTIC_DOI) is None
1531
+ response.read.assert_not_called()
1532
+
1533
+
1534
+ def test_content_length_precheck_rejects_oversized_body_without_reading():
1535
+ from chinese_literature_client import ChineseLiteratureUnavailable, MAX_BODY_BYTES
1536
+
1537
+ response = MagicMock()
1538
+ response.status = 200
1539
+ response.headers = {"Content-Length": str(MAX_BODY_BYTES + 1)}
1540
+ response.geturl.return_value = "https://doi.org/doiRA/10.5555"
1541
+ response.__enter__ = MagicMock(return_value=response)
1542
+ response.__exit__ = MagicMock(return_value=None)
1543
+
1544
+ with patch("chinese_literature_client._safe_urlopen", return_value=response):
1545
+ with pytest.raises(ChineseLiteratureUnavailable, match="exceeds"):
1546
+ _client().ra_for(ISTIC_DOI)
1547
+ response.read.assert_not_called()
1548
+
1549
+
1550
+ @pytest.mark.parametrize(
1551
+ "value",
1552
+ ["+1", "-0", "1_0", "١", " 1 x ", "\u00a01\u00a0", "9" * 5000],
1553
+ )
1554
+ def test_malformed_content_length_is_rejected_before_read(value):
1555
+ from chinese_literature_client import ChineseLiteratureUnavailable
1556
+
1557
+ response = MagicMock()
1558
+ response.status = 200
1559
+ response.headers = {"Content-Length": value}
1560
+ response.geturl.return_value = "https://doi.org/doiRA/10.5555"
1561
+ response.__enter__ = MagicMock(return_value=response)
1562
+ response.__exit__ = MagicMock(return_value=None)
1563
+
1564
+ with patch("chinese_literature_client._safe_urlopen", return_value=response):
1565
+ with pytest.raises(ChineseLiteratureUnavailable, match="Content-Length"):
1566
+ _client().ra_for(ISTIC_DOI)
1567
+ response.read.assert_not_called()
1568
+
1569
+
1570
+ def test_body_read_is_bounded_and_rejects_stream_over_limit():
1571
+ from chinese_literature_client import ChineseLiteratureUnavailable, MAX_BODY_BYTES
1572
+
1573
+ response = MagicMock()
1574
+ response.status = 200
1575
+ response.headers = {}
1576
+ response.geturl.return_value = "https://doi.org/doiRA/10.5555"
1577
+ response.read.return_value = b"x" * (MAX_BODY_BYTES + 1)
1578
+ response.__enter__ = MagicMock(return_value=response)
1579
+ response.__exit__ = MagicMock(return_value=None)
1580
+
1581
+ with patch("chinese_literature_client._safe_urlopen", return_value=response):
1582
+ with pytest.raises(ChineseLiteratureUnavailable, match="exceeds"):
1583
+ _client().ra_for(ISTIC_DOI)
1584
+ response.read.assert_called_once_with(MAX_BODY_BYTES + 1)
1585
+
1586
+
1587
+ # ------------------------------------------------------- degradation contract
1588
+
1589
+
1590
+ def test_5xx_does_not_retry():
1591
+ """Per protocol: 5xx -> fail fast, exactly one request."""
1592
+ from chinese_literature_client import ChineseLiteratureUnavailable
1593
+
1594
+ transport = FakeTransport([(_is_ra, _http_error(503, _body("error_5xx.html")))])
1595
+ with patch("chinese_literature_client._safe_urlopen", transport):
1596
+ with pytest.raises(ChineseLiteratureUnavailable):
1597
+ _client().ra_for(ISTIC_DOI)
1598
+
1599
+ assert len(transport.requests) == 1
1600
+
1601
+
1602
+ def test_429_backs_off_then_raises_after_the_shared_retry_budget(monkeypatch):
1603
+ from chinese_literature_client import ChineseLiteratureUnavailable
1604
+ from _text_similarity import _MAX_RETRIES
1605
+
1606
+ sleeps: list[float] = []
1607
+ monkeypatch.setattr("chinese_literature_client.time.sleep", sleeps.append)
1608
+
1609
+ transport = FakeTransport([(_is_ra, _http_error(429))])
1610
+ with patch("chinese_literature_client._safe_urlopen", transport):
1611
+ with pytest.raises(ChineseLiteratureUnavailable):
1612
+ _client().ra_for(ISTIC_DOI)
1613
+
1614
+ assert len(transport.requests) == _MAX_RETRIES + 1
1615
+ # Growing backoff, floored at 2s so a retry cannot re-violate the limit the
1616
+ # 429 is enforcing (arxiv_client.py's rule).
1617
+ assert sleeps == [2.0, 4.0, 6.0]
1618
+ assert all(delay >= 2.0 for delay in sleeps)
1619
+
1620
+
1621
+ def test_network_error_raises_unavailable():
1622
+ from chinese_literature_client import ChineseLiteratureUnavailable
1623
+
1624
+ def boom(req, timeout=None):
1625
+ raise urllib.error.URLError("connection refused")
1626
+
1627
+ with patch("chinese_literature_client._safe_urlopen", boom):
1628
+ with pytest.raises(ChineseLiteratureUnavailable):
1629
+ _client().ra_for(ISTIC_DOI)
1630
+
1631
+
1632
+ @pytest.mark.parametrize("failure_point", ["open", "close"])
1633
+ def test_raw_oserror_from_transport_or_response_close_is_typed(failure_point):
1634
+ from chinese_literature_client import ChineseLiteratureUnavailable
1635
+
1636
+ if failure_point == "open":
1637
+ transport = MagicMock(side_effect=OSError("socket failure"))
1638
+ else:
1639
+ response = MagicMock()
1640
+ response.status = 200
1641
+ response.headers = {}
1642
+ response.geturl.return_value = "https://doi.org/doiRA/10.5555"
1643
+ response.read.return_value = _body("ra_istic.json")
1644
+ response.__enter__ = MagicMock(return_value=response)
1645
+ response.__exit__ = MagicMock(side_effect=OSError("close failure"))
1646
+ transport = MagicMock(return_value=response)
1647
+
1648
+ with patch("chinese_literature_client._safe_urlopen", transport):
1649
+ with pytest.raises(ChineseLiteratureUnavailable, match="network error"):
1650
+ _client().ra_for(ISTIC_DOI)
1651
+
1652
+
1653
+ def test_truncated_body_raises_unavailable():
1654
+ """IncompleteRead inherits HTTPException, not OSError — a truncated body
1655
+ must degrade, never become a miss."""
1656
+ from chinese_literature_client import ChineseLiteratureUnavailable
1657
+
1658
+ resp = MagicMock()
1659
+ resp.status = 200
1660
+ resp.headers = {}
1661
+ resp.geturl.return_value = "https://doi.org/doiRA/10.5555"
1662
+ resp.read.side_effect = http.client.IncompleteRead(partial=b"{", expected=200)
1663
+ resp.__enter__ = MagicMock(return_value=resp)
1664
+ resp.__exit__ = MagicMock(return_value=None)
1665
+
1666
+ with patch("chinese_literature_client._safe_urlopen", return_value=resp):
1667
+ with pytest.raises(ChineseLiteratureUnavailable, match="read failed"):
1668
+ _client().ra_for(ISTIC_DOI)
1669
+
1670
+
1671
+ def test_unparseable_200_body_raises_unavailable():
1672
+ """#331: an HTML error page served with 200 by a proxy/CDN must degrade, not
1673
+ be recorded as an empty result."""
1674
+ from chinese_literature_client import ChineseLiteratureUnavailable
1675
+
1676
+ transport = FakeTransport([(_is_esearch, _ok(_body("error_5xx.html")))])
1677
+ with patch("chinese_literature_client._safe_urlopen", transport):
1678
+ with pytest.raises(ChineseLiteratureUnavailable):
1679
+ _client().journal_is_indexed("Hecheng Ceshi Yi Xue Za Zhi")
1680
+
1681
+
1682
+ @pytest.mark.parametrize(
1683
+ "body",
1684
+ [
1685
+ b'{"unexpected": true}',
1686
+ b'{"esearchresult":{"idlist":[null]}}',
1687
+ b'{"esearchresult":{"idlist":[42]}}',
1688
+ b'{"esearchresult":{"idlist":[""]}}',
1689
+ b'{"esearchresult":{"idlist":["0"]}}',
1690
+ b'{"esearchresult":{"count":false,"idlist":[]}}',
1691
+ b'{"esearchresult":{"count":"0","idlist":["999999901"]}}',
1692
+ b'{"esearchresult":{"count":"1","idlist":[]}}',
1693
+ (b'{"esearchresult":{"count":"' + b"9" * 5000 + b'","idlist":[]}}'),
1694
+ b'{"esearchresult":{"count":"2","idlist":["999999901","999999901"]}}',
1695
+ ],
1696
+ )
1697
+ def test_unexpected_esearch_shape_raises_unavailable(body):
1698
+ from chinese_literature_client import ChineseLiteratureUnavailable
1699
+
1700
+ transport = FakeTransport([(_is_esearch, _ok(body))])
1701
+ with patch("chinese_literature_client._safe_urlopen", transport):
1702
+ with pytest.raises(ChineseLiteratureUnavailable):
1703
+ _client().journal_is_indexed("Hecheng Ceshi Yi Xue Za Zhi")
1704
+
1705
+
1706
+ @pytest.mark.parametrize(
1707
+ "body",
1708
+ [
1709
+ b'{"result": {"999999901": 42}}',
1710
+ b'{"result": {"999999901": {"articleids": {}}}}',
1711
+ b'{"result": {"999999901": {"articleids": [], "issn": {}}}}',
1712
+ b'{"result": {"999999901": {"articleids": [], "title": false}}}',
1713
+ b'{"result": {"999999901": {"articleids": [], "pubdate": false}}}',
1714
+ b'{"result": {"999999901": {"articleids": [], "pubdate": "garbage"}}}',
1715
+ '{"result": {"999999901": {"articleids": [], "pubdate": "٢٠٢٦"}}}'.encode(),
1716
+ b'{"result": {"999999901": {"articleids": [], "source": []}}}',
1717
+ b'{"result": {"999999901": {"articleids": [], "lang": false}}}',
1718
+ b'{"result": {"999999901": {"articleids": [{"idtype": false, "value": "x"}]}}}',
1719
+ b'{"result": {"999999901": {"articleids": [{"idtype": "doi", "value": false}]}}}',
1720
+ b'{"result": {"999999901": {"articleids": [{"idtype": "doi", "value": "10.a/x"}, {"idtype": "doi", "value": "10.b/y"}]}}}',
1721
+ b'{"result": {"uids": ["888888888"], "999999901": {"articleids": []}}}',
1722
+ b'{"result": {"uids": [null], "999999901": {"articleids": []}}}',
1723
+ b'{"result": {"uids": ["999999901"], "999999901": {"uid": "888888888", "articleids": []}}}',
1724
+ b'{"result": {"uids": ["999999901"], "999999901": {"uid": 999999901, "articleids": []}}}',
1725
+ ],
1726
+ )
1727
+ def test_unexpected_esummary_shape_raises_unavailable(body):
1728
+ from chinese_literature_client import ChineseLiteratureUnavailable
1729
+
1730
+ transport = FakeTransport([(_is_esummary, _ok(body))])
1731
+ with patch("chinese_literature_client._safe_urlopen", transport):
1732
+ with pytest.raises(ChineseLiteratureUnavailable):
1733
+ _client()._esummary("999999901")
1734
+
1735
+
1736
+ def test_resolve_propagates_unavailable_rather_than_returning_a_verdict():
1737
+ """fail-closed: `resolve` has no `unreachable` return value by design. An
1738
+ outage must reach the caller as an exception so it can never be silently
1739
+ rendered as a lookup result."""
1740
+ from chinese_literature_client import ChineseLiteratureUnavailable
1741
+
1742
+ transport = FakeTransport([(_is_ra, _http_error(503))])
1743
+ with patch("chinese_literature_client._safe_urlopen", transport):
1744
+ with pytest.raises(ChineseLiteratureUnavailable):
1745
+ _client().resolve(_istic_entry())
1746
+
1747
+
1748
+ # ------------------------------------------------------------------ throttling
1749
+
1750
+
1751
+ def test_throttle_uses_monotonic_clock(monkeypatch):
1752
+ """time.monotonic for elapsed measurement (NTP-safe), never time.time
1753
+ (#128 §6). Aligns with arxiv/crossref/openalex/S2."""
1754
+ monotonic_calls: list[int] = []
1755
+ time_calls: list[int] = []
1756
+ monkeypatch.setattr("chinese_literature_client.time.monotonic",
1757
+ lambda: (monotonic_calls.append(1), 100.0)[1])
1758
+ monkeypatch.setattr("chinese_literature_client.time.time",
1759
+ lambda: (time_calls.append(1), 100.0)[1])
1760
+
1761
+ client = _client()
1762
+ client._throttle("_last_doi_at", 0.2) # no prior request -> short-circuit
1763
+ client._last_doi_at = 99.9
1764
+ client._throttle("_last_doi_at", 0.2)
1765
+
1766
+ assert monotonic_calls, "throttle must read time.monotonic"
1767
+ assert time_calls == [], "throttle must NOT read time.time (NTP-unsafe)"
1768
+
1769
+
1770
+ def test_doi_and_ncbi_have_independent_throttle_anchors(monkeypatch):
1771
+ """Sharing one anchor would either over-throttle doi.org or under-throttle
1772
+ NCBI, whose published expectations differ."""
1773
+ sleeps: list[float] = []
1774
+ monkeypatch.setattr("chinese_literature_client.time.sleep", sleeps.append)
1775
+ clock = [1000.0]
1776
+ monkeypatch.setattr("chinese_literature_client.time.monotonic", lambda: clock[0])
1777
+
1778
+ esearch_bodies = [_body("esearch_coverage_hit.json"),
1779
+ _body("esearch_coverage_hit.json")]
1780
+ transport = FakeTransport([
1781
+ (_is_ra, _ok(_body("ra_istic.json"))),
1782
+ (_is_esearch, lambda url: _ok(esearch_bodies.pop(0))(url)),
1783
+ ])
1784
+ with patch("chinese_literature_client._safe_urlopen", transport):
1785
+ client = _client()
1786
+ client.ra_for(ISTIC_DOI)
1787
+ # A DOI request must not make the (independent) NCBI anchor wait.
1788
+ client.journal_is_indexed("Hecheng Ceshi Yi Xue Za Zhi")
1789
+ assert sleeps == []
1790
+ # A second NCBI request on a frozen clock does pace at the NCBI floor.
1791
+ client.journal_is_indexed("Hecheng Ceshi Yi Xue Za Zhi")
1792
+ assert sleeps == [pytest.approx(0.34)]
1793
+
1794
+
1795
+ # ------------------------------------------------------------ checklist shape
1796
+
1797
+
1798
+ def test_every_applicable_non_matched_scenario_emits_exactly_one_checklist_row():
1799
+ """The checklist is a first-class deliverable, not a log side effect: it is
1800
+ what turns 'nobody ever actually checked this reference' from an invisible
1801
+ default into an item somebody has to sign off on."""
1802
+ from chinese_literature_client import REASON_CODES
1803
+
1804
+ scenarios = [
1805
+ ([(_is_ra, _ok(_body("ra_istic.json"))),
1806
+ (_is_doi_resolve, _http_error(404)),
1807
+ (_is_handle, _ok(_body("handle_absent.json")))], _istic_entry(), {}),
1808
+ ([(_is_ra, _ok(_body("ra_cnki.json"))),
1809
+ (_is_handle, _ok(_body("handle_exists.json")))],
1810
+ _istic_entry(doi=CNKI_DOI), {}),
1811
+ ([(_is_esearch, _ok(_body("esearch_coverage_zero.json")))],
1812
+ _coordinate_entry(), {"journal_map": TEST_JOURNAL_MAP}),
1813
+ ]
1814
+ for routes, entry, kwargs in scenarios:
1815
+ transport = FakeTransport(routes)
1816
+ with patch("chinese_literature_client._safe_urlopen", transport):
1817
+ result = _client(**kwargs).resolve(entry)
1818
+ item = result["checklist_item"]
1819
+ assert item is not None
1820
+ assert item["reason_code"] in REASON_CODES
1821
+ assert item["priority"] in {"P0", "P1", "P2", "P3"}
1822
+ assert item["citation_key"] == entry["citation_key"]
1823
+ assert item["human_result"] is None # the tool never fills the verdict
1824
+ assert item["attempts"], "a row must record which sources were tried"
1825
+
1826
+
1827
+ def test_priority_is_workload_ordering_not_a_suspicion_score():
1828
+ """Fabrication vocabulary is permitted only at P0. Mislabeling a real paper
1829
+ by a real author costs far more than missing one bad citation."""
1830
+ from chinese_literature_client import _PRIORITY_BY_REASON
1831
+
1832
+ assert _PRIORITY_BY_REASON["DOI_REFUTED"] == "P0"
1833
+ assert _PRIORITY_BY_REASON["DOI_TITLE_MISMATCH"] == "P0"
1834
+ assert _PRIORITY_BY_REASON["DOI_EXISTS_TITLE_UNVERIFIABLE"] == "P2"
1835
+ assert _PRIORITY_BY_REASON["NO_ISSN_MAPPING"] == "P3"
1836
+ non_p0 = {code for code, priority in _PRIORITY_BY_REASON.items()
1837
+ if priority != "P0"}
1838
+ assert "DOI_REFUTED" not in non_p0
1839
+
1840
+
1841
+ def test_unknown_reason_code_is_rejected():
1842
+ """The reason-code set is closed; adding a member is a protocol-doc change."""
1843
+ from chinese_literature_client import _checklist_item
1844
+
1845
+ with pytest.raises(ValueError):
1846
+ _checklist_item(
1847
+ reason_code="MADE_UP_CODE",
1848
+ verdict_contribution="unresolvable",
1849
+ entry={"citation_key": "x"},
1850
+ attempts=[],
1851
+ human_action="",
1852
+ )
1853
+
1854
+
1855
+ def test_no_forbidden_upstream_host_appears_in_client_code():
1856
+ """Zero-scraping red line, pinned executably: CNKI / Wanfang / VIP / chndoi
1857
+ hosts must never become upstreams here. Guards against a well-intentioned
1858
+ later edit 'just adding a title lookup'.
1859
+
1860
+ Scanned over the AST's non-docstring string constants rather than raw text:
1861
+ prose EXPLAINING why we refuse to scrape chndoi.org is exactly the comment
1862
+ a future editor most needs to read, so a raw-text scan would pressure
1863
+ someone to delete the warning in order to pass the test."""
1864
+ import ast
1865
+
1866
+ source = (REPO_ROOT / "scripts" / "chinese_literature_client.py").read_text(
1867
+ encoding="utf-8")
1868
+ tree = ast.parse(source)
1869
+ docstrings = set()
1870
+ for node in ast.walk(tree):
1871
+ if isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef,
1872
+ ast.AsyncFunctionDef)):
1873
+ body = getattr(node, "body", None)
1874
+ if (body and isinstance(body[0], ast.Expr)
1875
+ and isinstance(body[0].value, ast.Constant)
1876
+ and isinstance(body[0].value.value, str)):
1877
+ docstrings.add(id(body[0].value))
1878
+
1879
+ literals = [
1880
+ node.value for node in ast.walk(tree)
1881
+ if isinstance(node, ast.Constant) and isinstance(node.value, str)
1882
+ and id(node) not in docstrings
1883
+ ]
1884
+ # Sanity: the scan must actually see the real endpoint constants, otherwise
1885
+ # it would pass vacuously.
1886
+ assert any("hdl.handle.net" in literal for literal in literals)
1887
+ for host in ("cnki.net", "wanfangdata", "cqvip.com", "chndoi.org"):
1888
+ for literal in literals:
1889
+ assert host not in literal, f"forbidden upstream host {host!r} in client"