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,2959 @@
1
+ """Hermetic conformance and mutation tests for issue #660."""
2
+ from __future__ import annotations
3
+
4
+ import copy
5
+ import hashlib
6
+ import json
7
+ import subprocess
8
+ import sys
9
+ import unicodedata
10
+ from collections import Counter
11
+ from pathlib import Path
12
+
13
+ import pytest
14
+ import yaml
15
+ from jsonschema import Draft202012Validator
16
+
17
+
18
+ SCRIPTS = Path(__file__).resolve().parent
19
+ REPO_ROOT = SCRIPTS.parent
20
+ FIXTURES = SCRIPTS / "fixtures/tortured_phrase_screening"
21
+ sys.path.insert(0, str(SCRIPTS))
22
+
23
+ import tortured_phrase_screening as screening # noqa: E402
24
+
25
+
26
+ CHECKED_AT = "2026-08-10T01:00:00Z"
27
+ RECORDED_AT = "2026-08-10T01:00:01Z"
28
+
29
+
30
+ def _bundle() -> screening.SnapshotBundle:
31
+ return screening.load_snapshot(
32
+ FIXTURES / "snapshot.json", FIXTURES / "snapshot_manifest.json"
33
+ )
34
+
35
+
36
+ def _state() -> screening.SnapshotState:
37
+ bundle = _bundle()
38
+ return screening.SnapshotState(
39
+ status="loaded",
40
+ reason_code="CHECK_COMPLETED",
41
+ bundle=bundle,
42
+ snapshot_sha256=bundle.snapshot_sha256,
43
+ manifest_sha256=bundle.manifest_sha256,
44
+ detail=None,
45
+ )
46
+
47
+
48
+ def _draft(name: str) -> str:
49
+ return (FIXTURES / name).read_text(encoding="utf-8")
50
+
51
+
52
+ def _report(name: str = "own_draft.md") -> dict:
53
+ return screening.build_own_draft_report(
54
+ _draft(name),
55
+ artifact_id=name,
56
+ artifact_format="latex" if name.endswith(".tex") else "markdown",
57
+ state=_state(),
58
+ checked_at=CHECKED_AT,
59
+ recorded_at=RECORDED_AT,
60
+ )
61
+
62
+
63
+ def _passport() -> dict:
64
+ return yaml.safe_load((FIXTURES / "corpus_input.yaml").read_text(encoding="utf-8"))
65
+
66
+
67
+ def _enriched(state: screening.SnapshotState | None = None) -> dict:
68
+ return screening.enrich_passport(
69
+ _passport(),
70
+ state=state or _state(),
71
+ checked_at=CHECKED_AT,
72
+ recorded_at=RECORDED_AT,
73
+ )
74
+
75
+
76
+ def _seed_expectations() -> dict:
77
+ value, _raw = screening._strict_json_path(
78
+ FIXTURES / "seed_expectations.json",
79
+ label="synthetic seed expectations",
80
+ maximum=512 * 1024,
81
+ )
82
+ if not isinstance(value, dict):
83
+ raise AssertionError("seed expectations root must be an object")
84
+ return value
85
+
86
+
87
+ def _expect_exact_keys(
88
+ value: object,
89
+ *,
90
+ required: set[str],
91
+ optional: set[str] | None = None,
92
+ label: str,
93
+ ) -> dict:
94
+ if not isinstance(value, dict):
95
+ raise AssertionError(f"{label} must be an object")
96
+ optional = optional or set()
97
+ keys = set(value)
98
+ if keys != required | (keys & optional):
99
+ missing = required - keys
100
+ unknown = keys - required - optional
101
+ raise AssertionError(
102
+ f"{label} keys drifted; missing={sorted(missing)!r}, "
103
+ f"unknown={sorted(unknown)!r}"
104
+ )
105
+ return value
106
+
107
+
108
+ def _compiled_rules() -> dict[str, screening.CompiledRule]:
109
+ return {rule.rule_id: rule for rule in _bundle().rules}
110
+
111
+
112
+ def _actual_suppressions(
113
+ text: str,
114
+ segments: list[screening.Segment],
115
+ rules: dict[str, screening.CompiledRule],
116
+ ) -> Counter[tuple[str, str]]:
117
+ suppressed: Counter[tuple[str, str]] = Counter()
118
+ for segment in segments:
119
+ tokens = screening.tokenize(text[segment.start : segment.end])
120
+ for rule in rules.values():
121
+ included = set(screening.evaluate_expression(rule.expression, tokens))
122
+ retained = set(screening.evaluate_rule(rule, tokens))
123
+ suppressed[(rule.rule_id, segment.kind)] += len(included - retained)
124
+ return +suppressed
125
+
126
+
127
+ def _has_token_boundary_negative(
128
+ rule: screening.CompiledRule,
129
+ text: str,
130
+ segments: list[screening.Segment],
131
+ ) -> bool:
132
+ expression = rule.expression
133
+ if expression.get("op") != "literal":
134
+ return False
135
+ literal_tokens = screening.tokenize(expression["value"])
136
+ if len(literal_tokens) != 1:
137
+ return False
138
+ literal = literal_tokens[0].normalized
139
+ return any(
140
+ literal in token.normalized and literal != token.normalized
141
+ for segment in segments
142
+ for token in screening.tokenize(text[segment.start : segment.end])
143
+ )
144
+
145
+
146
+ def _has_same_segment_exclusion(
147
+ rule: screening.CompiledRule,
148
+ text: str,
149
+ segments: list[screening.Segment],
150
+ ) -> bool:
151
+ if not rule.exclude_if:
152
+ return False
153
+ for segment in segments:
154
+ tokens = screening.tokenize(text[segment.start : segment.end])
155
+ included = set(screening.evaluate_expression(rule.expression, tokens))
156
+ if included and included - set(screening.evaluate_rule(rule, tokens)):
157
+ return True
158
+ return False
159
+
160
+
161
+ def _non_match_reason_holds(
162
+ reason: str,
163
+ rule: screening.CompiledRule,
164
+ text: str,
165
+ segments: list[screening.Segment],
166
+ ) -> bool:
167
+ if reason == "token_boundary":
168
+ return _has_token_boundary_negative(rule, text, segments)
169
+ if reason == "exclude_if_in_same_segment":
170
+ return _has_same_segment_exclusion(rule, text, segments)
171
+ raise AssertionError(f"unsupported synthetic non-match reason {reason!r}")
172
+
173
+
174
+ def _normalization_case_holds(
175
+ case: str,
176
+ match: dict,
177
+ rule: screening.CompiledRule,
178
+ ) -> bool:
179
+ expression = rule.expression
180
+ if expression.get("op") != "literal":
181
+ return False
182
+ raw = match["matched_text"]
183
+ actual_tokens = tuple(token.normalized for token in screening.tokenize(raw))
184
+ literal_tokens = tuple(
185
+ token.normalized for token in screening.tokenize(expression["value"])
186
+ )
187
+ if actual_tokens != literal_tokens:
188
+ return False
189
+ if case == "nfkc_casefold":
190
+ folded = unicodedata.normalize("NFKC", raw).casefold()
191
+ return folded == expression["value"].casefold() and raw.casefold() != folded
192
+ if case == "same_line_dash_separator":
193
+ return "\n" not in raw and any(char in screening._DASHES for char in raw)
194
+ if case == "soft_hyphen_join":
195
+ return "\u00ad" in raw and "\n" not in raw
196
+ if case == "line_break_hyphen_join":
197
+ return any(
198
+ char in screening._DASHES and index + 1 < len(raw) and raw[index + 1] == "\n"
199
+ for index, char in enumerate(raw)
200
+ )
201
+ raise AssertionError(f"unsupported normalization case {case!r}")
202
+
203
+
204
+ def _alternative_matches_record(
205
+ alternative: dict,
206
+ match: dict,
207
+ *,
208
+ text: str,
209
+ segments: dict[str, screening.Segment],
210
+ ) -> bool:
211
+ segment = segments[match["segment_id"]]
212
+ tokens = screening.tokenize(text[segment.start : segment.end])
213
+ span = match["source_span"]
214
+ return any(
215
+ segment.start + witness.codepoint_start == span["codepoint_start"]
216
+ and segment.start + witness.codepoint_end == span["codepoint_end"]
217
+ for witness in screening.evaluate_expression(alternative, tokens)
218
+ )
219
+
220
+
221
+ def _spans_overlap(left: dict, right: dict) -> bool:
222
+ if left["segment_id"] != right["segment_id"]:
223
+ return False
224
+ left_span = left["source_span"]
225
+ right_span = right["source_span"]
226
+ return max(left_span["codepoint_start"], right_span["codepoint_start"]) < min(
227
+ left_span["codepoint_end"], right_span["codepoint_end"]
228
+ )
229
+
230
+
231
+ def _replay_draft_expectation(
232
+ expectation: dict,
233
+ rules: dict[str, screening.CompiledRule],
234
+ ) -> set[str]:
235
+ expectation = _expect_exact_keys(
236
+ expectation,
237
+ required={
238
+ "path",
239
+ "expected_counts",
240
+ "expected_matches",
241
+ "expected_suppressions",
242
+ "expected_non_matches",
243
+ },
244
+ label="draft expectation",
245
+ )
246
+ path = expectation["path"]
247
+ if path not in {"own_draft.md", "own_draft.tex"}:
248
+ raise AssertionError(f"unsupported draft expectation path {path!r}")
249
+ text = _draft(path)
250
+ artifact_format = "latex" if path.endswith(".tex") else "markdown"
251
+ segments = screening.segment_document(text, artifact_format)
252
+ segments_by_id = {segment.segment_id: segment for segment in segments}
253
+ report = _report(path)
254
+ if report["evaluation_status"] != "UNMEASURED":
255
+ raise AssertionError(f"{path}: synthetic results must remain UNMEASURED")
256
+
257
+ expected_counts = _expect_exact_keys(
258
+ expectation["expected_counts"],
259
+ required={
260
+ "matched_rule_count",
261
+ "rule_match_count",
262
+ "unique_instance_count",
263
+ },
264
+ label=f"{path}.expected_counts",
265
+ )
266
+ for key, expected in expected_counts.items():
267
+ if type(expected) is not int or expected < 0:
268
+ raise AssertionError(f"{path}.expected_counts.{key} must be nonnegative int")
269
+ if report["counts"][key] != expected:
270
+ raise AssertionError(
271
+ f"{path}.{key}: expected {expected}, got {report['counts'][key]}"
272
+ )
273
+
274
+ expected_match_counts: Counter[tuple[str, str]] = Counter()
275
+ seen_match_keys: set[tuple[str, str]] = set()
276
+ match_groups: dict[str, list[dict]] = {}
277
+ group_by_match_id: dict[str, str | None] = {}
278
+ normalization_cases: set[str] = set()
279
+ for index, raw_expected in enumerate(expectation["expected_matches"]):
280
+ expected = _expect_exact_keys(
281
+ raw_expected,
282
+ required={"rule_id", "count", "context"},
283
+ optional={
284
+ "matched_alternative_index",
285
+ "overlap_group",
286
+ "normalization_case",
287
+ },
288
+ label=f"{path}.expected_matches[{index}]",
289
+ )
290
+ rule_id = expected["rule_id"]
291
+ context = expected["context"]
292
+ count = expected["count"]
293
+ if rule_id not in rules:
294
+ raise AssertionError(f"{path}: unknown expected rule {rule_id!r}")
295
+ if context not in screening.CONTEXTS:
296
+ raise AssertionError(f"{path}: unknown expected context {context!r}")
297
+ if type(count) is not int or count < 1:
298
+ raise AssertionError(f"{path}: expected match count must be positive int")
299
+ match_key = (rule_id, context)
300
+ if match_key in seen_match_keys:
301
+ raise AssertionError(f"{path}: duplicate expected match key {match_key!r}")
302
+ seen_match_keys.add(match_key)
303
+ expected_match_counts[match_key] = count
304
+ actual_matches = [
305
+ match
306
+ for match in report["matches"]
307
+ if (match["pattern_id"], match["context"]) == match_key
308
+ ]
309
+
310
+ expression = rules[rule_id].expression
311
+ alternative_index = expected.get("matched_alternative_index")
312
+ if expression.get("op") == "any":
313
+ if type(alternative_index) is not int:
314
+ raise AssertionError(f"{path}: any rule {rule_id!r} needs an alternative index")
315
+ alternatives = expression["alternatives"]
316
+ if not 0 <= alternative_index < len(alternatives):
317
+ raise AssertionError(f"{path}: alternative index out of range")
318
+ for match in actual_matches:
319
+ matching_indexes = {
320
+ child_index
321
+ for child_index, alternative in enumerate(alternatives)
322
+ if _alternative_matches_record(
323
+ alternative,
324
+ match,
325
+ text=text,
326
+ segments=segments_by_id,
327
+ )
328
+ }
329
+ if matching_indexes != {alternative_index}:
330
+ raise AssertionError(
331
+ f"{path}: any alternative mismatch for {rule_id!r}: "
332
+ f"{sorted(matching_indexes)!r}"
333
+ )
334
+ elif alternative_index is not None:
335
+ raise AssertionError(f"{path}: non-any rule carries alternative index")
336
+
337
+ overlap_group = expected.get("overlap_group")
338
+ if overlap_group is not None and (
339
+ not isinstance(overlap_group, str) or not overlap_group
340
+ ):
341
+ raise AssertionError(f"{path}: overlap_group must be a nonempty string")
342
+ for match in actual_matches:
343
+ group_by_match_id[match["match_id"]] = overlap_group
344
+ if overlap_group is not None:
345
+ match_groups.setdefault(overlap_group, []).append(match)
346
+
347
+ normalization_case = expected.get("normalization_case")
348
+ if normalization_case is not None:
349
+ if not isinstance(normalization_case, str):
350
+ raise AssertionError(f"{path}: normalization_case must be a string")
351
+ normalization_cases.add(normalization_case)
352
+ if not actual_matches or not all(
353
+ _normalization_case_holds(normalization_case, match, rules[rule_id])
354
+ for match in actual_matches
355
+ ):
356
+ raise AssertionError(
357
+ f"{path}: normalization case {normalization_case!r} did not replay"
358
+ )
359
+
360
+ actual_match_counts = Counter(
361
+ (match["pattern_id"], match["context"]) for match in report["matches"]
362
+ )
363
+ if actual_match_counts != expected_match_counts:
364
+ raise AssertionError(
365
+ f"{path}: expected rule/context counts do not match runtime output"
366
+ )
367
+
368
+ for group, matches in match_groups.items():
369
+ if len(matches) < 2 or screening._unique_instance_count(matches) != 1:
370
+ raise AssertionError(f"{path}: overlap group {group!r} is not one component")
371
+ for left_index, left in enumerate(report["matches"]):
372
+ for right in report["matches"][left_index + 1 :]:
373
+ if _spans_overlap(left, right) and (
374
+ group_by_match_id.get(left["match_id"]) is None
375
+ or group_by_match_id.get(left["match_id"])
376
+ != group_by_match_id.get(right["match_id"])
377
+ ):
378
+ raise AssertionError(f"{path}: overlapping matches lack one shared group")
379
+
380
+ expected_suppressions: Counter[tuple[str, str]] = Counter()
381
+ for index, raw_expected in enumerate(expectation["expected_suppressions"]):
382
+ expected = _expect_exact_keys(
383
+ raw_expected,
384
+ required={"rule_id", "count", "context", "reason"},
385
+ label=f"{path}.expected_suppressions[{index}]",
386
+ )
387
+ rule_id = expected["rule_id"]
388
+ count = expected["count"]
389
+ context = expected["context"]
390
+ if rule_id not in rules or not rules[rule_id].exclude_if:
391
+ raise AssertionError(f"{path}: suppression names a non-excluding rule")
392
+ if context not in screening.CONTEXTS:
393
+ raise AssertionError(f"{path}: suppression context is unknown")
394
+ if type(count) is not int or count < 1:
395
+ raise AssertionError(f"{path}: suppression count must be positive int")
396
+ if expected["reason"] != "exclude_if_within_token_window":
397
+ raise AssertionError(f"{path}: unsupported suppression reason")
398
+ key = (rule_id, context)
399
+ if key in expected_suppressions:
400
+ raise AssertionError(f"{path}: duplicate expected suppression {key!r}")
401
+ expected_suppressions[key] = count
402
+ if _actual_suppressions(text, segments, rules) != expected_suppressions:
403
+ raise AssertionError(f"{path}: suppression oracle does not replay")
404
+
405
+ non_match_reasons: set[str] = set()
406
+ seen_non_matches: set[str] = set()
407
+ for index, raw_expected in enumerate(expectation["expected_non_matches"]):
408
+ expected = _expect_exact_keys(
409
+ raw_expected,
410
+ required={"rule_id", "reason"},
411
+ label=f"{path}.expected_non_matches[{index}]",
412
+ )
413
+ rule_id = expected["rule_id"]
414
+ reason = expected["reason"]
415
+ if rule_id not in rules:
416
+ raise AssertionError(f"{path}: non-match names unknown rule {rule_id!r}")
417
+ if rule_id in seen_non_matches:
418
+ raise AssertionError(f"{path}: duplicate non-match rule {rule_id!r}")
419
+ seen_non_matches.add(rule_id)
420
+ if any(match["pattern_id"] == rule_id for match in report["matches"]):
421
+ raise AssertionError(f"{path}: expected non-match {rule_id!r} matched")
422
+ if not _non_match_reason_holds(reason, rules[rule_id], text, segments):
423
+ raise AssertionError(f"{path}: non-match reason {reason!r} did not replay")
424
+ non_match_reasons.add(reason)
425
+ return normalization_cases | {f"nonmatch:{reason}" for reason in non_match_reasons}
426
+
427
+
428
+ def _corpus_surface_text(entry: dict, surface: str) -> str | None:
429
+ return entry["title"] if surface == "title" else entry.get("abstract")
430
+
431
+
432
+ def _replay_corpus_expectation(
433
+ expectation: dict,
434
+ rules: dict[str, screening.CompiledRule],
435
+ ) -> set[str]:
436
+ expectation = _expect_exact_keys(
437
+ expectation,
438
+ required={"path", "records"},
439
+ label="corpus expectation",
440
+ )
441
+ if expectation["path"] != "corpus_input.yaml":
442
+ raise AssertionError("corpus expectation path drifted")
443
+ source_entries = {
444
+ entry["citation_key"]: entry for entry in _passport()["literature_corpus"]
445
+ }
446
+ output_entries = {
447
+ entry["citation_key"]: entry for entry in _enriched()["literature_corpus"]
448
+ }
449
+ records = expectation["records"]
450
+ if not isinstance(records, list):
451
+ raise AssertionError("corpus records must be an array")
452
+ citation_keys = [
453
+ record.get("citation_key") if isinstance(record, dict) else None
454
+ for record in records
455
+ ]
456
+ if (
457
+ any(not isinstance(key, str) or not key for key in citation_keys)
458
+ or len(citation_keys) != len(set(citation_keys))
459
+ or set(citation_keys) != set(source_entries)
460
+ or set(citation_keys) != set(output_entries)
461
+ ):
462
+ raise AssertionError("corpus expectation citation inventory drifted")
463
+
464
+ replayed_reasons: set[str] = set()
465
+ surface_contexts = {"title": "cited_title", "abstract": "cited_abstract"}
466
+ for record_index, raw_record in enumerate(records):
467
+ record = _expect_exact_keys(
468
+ raw_record,
469
+ required={"citation_key", "surfaces"},
470
+ label=f"corpus.records[{record_index}]",
471
+ )
472
+ citation_key = record["citation_key"]
473
+ surfaces = _expect_exact_keys(
474
+ record["surfaces"],
475
+ required={"title", "abstract"},
476
+ label=f"corpus.records[{citation_key}].surfaces",
477
+ )
478
+ source_entry = source_entries[citation_key]
479
+ output_entry = output_entries[citation_key]
480
+ signals = [
481
+ signal
482
+ for signal in output_entry["bibliographic_integrity_signals"]
483
+ if signal.get("schema_version") == screening.SIGNAL_VERSION
484
+ and signal.get("signal_type") == "tortured_phrase_match"
485
+ ]
486
+ if len(signals) != 2:
487
+ raise AssertionError(
488
+ f"{citation_key}: expected exactly the title and abstract signals"
489
+ )
490
+ for surface, context in surface_contexts.items():
491
+ expected = _expect_exact_keys(
492
+ surfaces[surface],
493
+ required={"check_status", "reason", "expected_rule_ids"},
494
+ optional={"non_match_reasons"},
495
+ label=f"corpus.records[{citation_key}].{surface}",
496
+ )
497
+ matching_signals = [
498
+ signal
499
+ for signal in signals
500
+ if signal["tortured_phrase_context"]["surface"] == context
501
+ ]
502
+ if len(matching_signals) != 1:
503
+ raise AssertionError(
504
+ f"{citation_key}.{surface}: expected exactly one current signal"
505
+ )
506
+ signal = matching_signals[0]
507
+ signal_context = signal["tortured_phrase_context"]
508
+ if signal["check_status"] != expected["check_status"]:
509
+ raise AssertionError(f"{citation_key}.{surface}: status drifted")
510
+ if signal_context["reason_code"] != expected["reason"]:
511
+ raise AssertionError(f"{citation_key}.{surface}: reason drifted")
512
+
513
+ expected_rule_ids = expected["expected_rule_ids"]
514
+ if (
515
+ not isinstance(expected_rule_ids, list)
516
+ or any(not isinstance(rule_id, str) for rule_id in expected_rule_ids)
517
+ or len(expected_rule_ids) != len(set(expected_rule_ids))
518
+ or any(rule_id not in rules for rule_id in expected_rule_ids)
519
+ ):
520
+ raise AssertionError(
521
+ f"{citation_key}.{surface}: expected_rule_ids are invalid"
522
+ )
523
+ actual_rule_ids = {
524
+ match["pattern_id"] for match in signal_context["matches"]
525
+ }
526
+ if actual_rule_ids != set(expected_rule_ids):
527
+ raise AssertionError(f"{citation_key}.{surface}: rule ids drifted")
528
+ expected_finding = (
529
+ "unresolved"
530
+ if expected["check_status"] != "checked"
531
+ else "detected" if expected_rule_ids else "not_detected"
532
+ )
533
+ if signal["finding"] != expected_finding:
534
+ raise AssertionError(f"{citation_key}.{surface}: finding drifted")
535
+
536
+ raw_non_matches = expected.get("non_match_reasons", [])
537
+ if expected["check_status"] == "checked" and not expected_rule_ids:
538
+ if not raw_non_matches:
539
+ raise AssertionError(
540
+ f"{citation_key}.{surface}: zero result lacks a rule-bound reason"
541
+ )
542
+ text = _corpus_surface_text(source_entry, surface)
543
+ seen_non_match_rules: set[str] = set()
544
+ for reason_index, raw_reason in enumerate(raw_non_matches):
545
+ reason_record = _expect_exact_keys(
546
+ raw_reason,
547
+ required={"rule_id", "reason"},
548
+ label=(
549
+ f"corpus.records[{citation_key}].{surface}."
550
+ f"non_match_reasons[{reason_index}]"
551
+ ),
552
+ )
553
+ rule_id = reason_record["rule_id"]
554
+ reason = reason_record["reason"]
555
+ if rule_id not in rules or rule_id in seen_non_match_rules:
556
+ raise AssertionError(
557
+ f"{citation_key}.{surface}: invalid non-match rule {rule_id!r}"
558
+ )
559
+ seen_non_match_rules.add(rule_id)
560
+ if rule_id in actual_rule_ids or text is None:
561
+ raise AssertionError(
562
+ f"{citation_key}.{surface}: non-match rule is not absent"
563
+ )
564
+ one_segment = [screening.Segment("SEG-000001", context, 0, len(text))]
565
+ if not _non_match_reason_holds(
566
+ reason,
567
+ rules[rule_id],
568
+ text,
569
+ one_segment,
570
+ ):
571
+ raise AssertionError(
572
+ f"{citation_key}.{surface}: non-match reason did not replay"
573
+ )
574
+ replayed_reasons.add(reason)
575
+ return replayed_reasons
576
+
577
+
578
+ def _replay_seed_expectations(expectations: dict) -> None:
579
+ expectations = _expect_exact_keys(
580
+ expectations,
581
+ required={
582
+ "schema_version",
583
+ "snapshot_id",
584
+ "snapshot_sha256",
585
+ "label_scope",
586
+ "empirical_accuracy_claimed",
587
+ "contextual_false_positive_labels_provided",
588
+ "contextual_false_negative_labels_provided",
589
+ "inputs",
590
+ },
591
+ label="seed expectations",
592
+ )
593
+ if expectations["schema_version"] != "tortured-phrase-seed-expectations/1.0":
594
+ raise AssertionError("seed expectations schema_version drifted")
595
+ bundle = _bundle()
596
+ if (
597
+ expectations["snapshot_id"] != bundle.snapshot["snapshot_id"]
598
+ or expectations["snapshot_id"] != bundle.manifest["snapshot_id"]
599
+ or expectations["snapshot_sha256"] != bundle.snapshot_sha256
600
+ ):
601
+ raise AssertionError("seed expectations snapshot identity drifted")
602
+ if expectations["label_scope"] != "mechanical_matcher_conformance_only":
603
+ raise AssertionError("seed expectations label scope drifted")
604
+ for flag in (
605
+ "empirical_accuracy_claimed",
606
+ "contextual_false_positive_labels_provided",
607
+ "contextual_false_negative_labels_provided",
608
+ ):
609
+ if expectations[flag] is not False:
610
+ raise AssertionError(f"seed expectations {flag} must remain false")
611
+
612
+ inputs = expectations["inputs"]
613
+ if not isinstance(inputs, list):
614
+ raise AssertionError("seed expectations inputs must be an array")
615
+ paths = [item.get("path") if isinstance(item, dict) else None for item in inputs]
616
+ required_paths = {"own_draft.md", "own_draft.tex", "corpus_input.yaml"}
617
+ if (
618
+ any(not isinstance(path, str) for path in paths)
619
+ or len(paths) != len(set(paths))
620
+ or set(paths) != required_paths
621
+ ):
622
+ raise AssertionError("seed expectations input inventory drifted")
623
+
624
+ rules = _compiled_rules()
625
+ draft_coverage: set[str] = set()
626
+ corpus_non_match_reasons: set[str] = set()
627
+ for expectation in inputs:
628
+ if expectation["path"] == "corpus_input.yaml":
629
+ corpus_non_match_reasons |= _replay_corpus_expectation(expectation, rules)
630
+ else:
631
+ draft_coverage |= _replay_draft_expectation(expectation, rules)
632
+ required_normalization = {
633
+ "nfkc_casefold",
634
+ "same_line_dash_separator",
635
+ "soft_hyphen_join",
636
+ "line_break_hyphen_join",
637
+ }
638
+ if not required_normalization.issubset(draft_coverage):
639
+ raise AssertionError("seed expectations normalization coverage drifted")
640
+ if "nonmatch:token_boundary" not in draft_coverage:
641
+ raise AssertionError("seed expectations draft token-boundary negative is missing")
642
+ if corpus_non_match_reasons != {
643
+ "token_boundary",
644
+ "exclude_if_in_same_segment",
645
+ }:
646
+ raise AssertionError("seed expectations corpus non-match coverage drifted")
647
+
648
+
649
+ def _strict_dump(path: Path, value: dict) -> None:
650
+ path.write_text(
651
+ json.dumps(value, ensure_ascii=False, indent=2, allow_nan=False) + "\n",
652
+ encoding="utf-8",
653
+ )
654
+
655
+
656
+ def _snapshot_pair(tmp_path: Path) -> tuple[Path, Path, dict, dict]:
657
+ snapshot = json.loads((FIXTURES / "snapshot.json").read_text(encoding="utf-8"))
658
+ manifest = json.loads(
659
+ (FIXTURES / "snapshot_manifest.json").read_text(encoding="utf-8")
660
+ )
661
+ snapshot_path = tmp_path / "snapshot.json"
662
+ manifest_path = tmp_path / "manifest.json"
663
+ _strict_dump(snapshot_path, snapshot)
664
+ manifest["snapshot_sha256"] = hashlib.sha256(snapshot_path.read_bytes()).hexdigest()
665
+ _strict_dump(manifest_path, manifest)
666
+ return snapshot_path, manifest_path, snapshot, manifest
667
+
668
+
669
+ def _rewrite_pair(
670
+ snapshot_path: Path,
671
+ manifest_path: Path,
672
+ snapshot: dict,
673
+ manifest: dict,
674
+ ) -> None:
675
+ _strict_dump(snapshot_path, snapshot)
676
+ manifest["snapshot_sha256"] = hashlib.sha256(snapshot_path.read_bytes()).hexdigest()
677
+ manifest["rule_count"] = len(snapshot.get("rules", []))
678
+ _strict_dump(manifest_path, manifest)
679
+
680
+
681
+ def test_snapshot_and_manifest_exact_hash_bind() -> None:
682
+ bundle = _bundle()
683
+ assert bundle.snapshot_sha256 == "962879909bfdd338047dc4569ff42188c4200fcd56a0a459b0e0f9169f2446c4"
684
+ assert len(bundle.rules) == 17
685
+ assert bundle.manifest["unsupported_rule_count"] == 0
686
+ assert bundle.manifest["rights"]["basis"] == "synthetic_fixture"
687
+
688
+
689
+ def test_seed_expectations_are_fully_replayed_as_the_mechanical_oracle() -> None:
690
+ _replay_seed_expectations(_seed_expectations())
691
+
692
+
693
+ def _expectation_input(expectations: dict, path: str) -> dict:
694
+ return next(item for item in expectations["inputs"] if item["path"] == path)
695
+
696
+
697
+ def _corpus_surface(expectations: dict, citation_key: str, surface: str) -> dict:
698
+ corpus = _expectation_input(expectations, "corpus_input.yaml")
699
+ record = next(
700
+ item for item in corpus["records"] if item["citation_key"] == citation_key
701
+ )
702
+ return record["surfaces"][surface]
703
+
704
+
705
+ def _mutate_seed_expectations(expectations: dict, mutation: str) -> None:
706
+ markdown = _expectation_input(expectations, "own_draft.md")
707
+ if mutation == "wrong_count":
708
+ markdown["expected_counts"]["rule_match_count"] += 1
709
+ elif mutation == "wrong_rule":
710
+ markdown["expected_matches"][0]["rule_id"] = "syn_corpus_abstract"
711
+ elif mutation == "wrong_context":
712
+ markdown["expected_matches"][0]["context"] = "quote"
713
+ elif mutation == "wrong_alternative":
714
+ expected = next(
715
+ item
716
+ for item in markdown["expected_matches"]
717
+ if "matched_alternative_index" in item
718
+ )
719
+ expected["matched_alternative_index"] += 1
720
+ elif mutation == "wrong_overlap":
721
+ expected = next(
722
+ item for item in markdown["expected_matches"] if "overlap_group" in item
723
+ )
724
+ expected["overlap_group"] += "-mutated"
725
+ elif mutation == "wrong_normalization":
726
+ expected = next(
727
+ item
728
+ for item in markdown["expected_matches"]
729
+ if item.get("normalization_case") == "nfkc_casefold"
730
+ )
731
+ expected["normalization_case"] = "same_line_dash_separator"
732
+ elif mutation == "wrong_suppression":
733
+ markdown["expected_suppressions"][0]["count"] += 1
734
+ elif mutation == "wrong_nonmatch":
735
+ markdown["expected_non_matches"][0]["reason"] = "exclude_if_in_same_segment"
736
+ elif mutation == "wrong_corpus_status":
737
+ _corpus_surface(expectations, "fixture_complete_2026", "title")[
738
+ "check_status"
739
+ ] = "not_checked"
740
+ elif mutation == "wrong_corpus_reason":
741
+ _corpus_surface(expectations, "fixture_missing_2026", "abstract")[
742
+ "reason"
743
+ ] = "ABSTRACT_EMPTY"
744
+ elif mutation == "wrong_corpus_rule_id":
745
+ _corpus_surface(expectations, "fixture_complete_2026", "title")[
746
+ "expected_rule_ids"
747
+ ] = ["syn_corpus_abstract"]
748
+ elif mutation == "wrong_corpus_nonmatch":
749
+ _corpus_surface(expectations, "fixture_negative_2026", "title")[
750
+ "non_match_reasons"
751
+ ][0]["rule_id"] = "syn_literal_author"
752
+ elif mutation == "wrong_corpus_nonmatch_reason":
753
+ _corpus_surface(expectations, "fixture_negative_2026", "title")[
754
+ "non_match_reasons"
755
+ ][0]["reason"] = "exclude_if_in_same_segment"
756
+ elif mutation == "wrong_snapshot_identity":
757
+ expectations["snapshot_sha256"] = "0" * 64
758
+ elif mutation == "wrong_claim_flag":
759
+ expectations["empirical_accuracy_claimed"] = True
760
+ elif mutation == "unknown_root_key":
761
+ expectations["unexpected"] = True
762
+ elif mutation == "missing_root_key":
763
+ expectations.pop("schema_version")
764
+ elif mutation == "unknown_input":
765
+ expectations["inputs"].append({"path": "unknown.txt"})
766
+ elif mutation == "missing_input":
767
+ expectations["inputs"].pop()
768
+ elif mutation == "duplicate_input":
769
+ expectations["inputs"].append(copy.deepcopy(markdown))
770
+ else: # pragma: no cover - test parameter list is closed below
771
+ raise AssertionError(f"unknown test mutation {mutation!r}")
772
+
773
+
774
+ @pytest.mark.parametrize(
775
+ "mutation",
776
+ [
777
+ "wrong_count",
778
+ "wrong_rule",
779
+ "wrong_context",
780
+ "wrong_alternative",
781
+ "wrong_overlap",
782
+ "wrong_normalization",
783
+ "wrong_suppression",
784
+ "wrong_nonmatch",
785
+ "wrong_corpus_status",
786
+ "wrong_corpus_reason",
787
+ "wrong_corpus_rule_id",
788
+ "wrong_corpus_nonmatch",
789
+ "wrong_corpus_nonmatch_reason",
790
+ "wrong_snapshot_identity",
791
+ "wrong_claim_flag",
792
+ "unknown_root_key",
793
+ "missing_root_key",
794
+ "unknown_input",
795
+ "missing_input",
796
+ "duplicate_input",
797
+ ],
798
+ )
799
+ def test_seed_expectation_mutations_fail_closed(mutation: str) -> None:
800
+ expectations = copy.deepcopy(_seed_expectations())
801
+ _mutate_seed_expectations(expectations, mutation)
802
+ with pytest.raises(AssertionError):
803
+ _replay_seed_expectations(expectations)
804
+
805
+
806
+ def test_context_detection_is_never_suppressed() -> None:
807
+ report = _report()
808
+ assert report["check_status"] == "checked"
809
+ assert report["finding"] == "detected"
810
+ assert report["counts"]["unknown_segments"] == 0
811
+ by_rule: dict[str, list[dict]] = {}
812
+ for match in report["matches"]:
813
+ by_rule.setdefault(match["pattern_id"], []).append(match)
814
+ assert by_rule["syn_context_quote"][0]["context"] == "quote"
815
+ assert {item["context"] for item in by_rule["syn_context_code"]} == {
816
+ "code_or_verbatim"
817
+ }
818
+ assert by_rule["syn_context_reference"][0]["context"] == "reference_entry"
819
+ assert all(
820
+ item["disposition"] == "preserve_verbatim_review_context"
821
+ for rule_id in ("syn_context_quote", "syn_context_code", "syn_context_reference")
822
+ for item in by_rule[rule_id]
823
+ )
824
+
825
+
826
+ def test_exclude_if_is_segment_scoped() -> None:
827
+ matches = [
828
+ item for item in _report()["matches"] if item["pattern_id"] == "syn_exclude_segment"
829
+ ]
830
+ assert len(matches) == 1
831
+ assert matches[0]["context"] == "quote"
832
+
833
+
834
+ @pytest.mark.parametrize(
835
+ ("text", "artifact_format"),
836
+ [
837
+ ("moonlit pickle\n\nrecipe glossary", "markdown"),
838
+ (
839
+ "# References\nmoonlit pickle\nrecipe glossary\n",
840
+ "markdown",
841
+ ),
842
+ (
843
+ "\\begin{thebibliography}{9}\n"
844
+ "\\bibitem{a} moonlit pickle\n"
845
+ "\\bibitem{b} recipe glossary\n"
846
+ "\\end{thebibliography}\n",
847
+ "latex",
848
+ ),
849
+ ],
850
+ )
851
+ def test_exclude_if_never_crosses_paragraph_or_reference_entry(
852
+ text: str, artifact_format: str
853
+ ) -> None:
854
+ rule = _compiled_rules()["syn_exclude_segment"]
855
+ segments = screening.segment_document(text, artifact_format)
856
+ retained = [
857
+ witness
858
+ for segment in segments
859
+ for witness in screening.evaluate_rule(
860
+ rule, screening.tokenize(text[segment.start : segment.end])
861
+ )
862
+ ]
863
+ assert len(retained) == 1
864
+
865
+
866
+ def test_overlaps_preserve_rule_hits_and_cluster_unique_instances() -> None:
867
+ matches = [
868
+ item
869
+ for item in _report()["matches"]
870
+ if item["pattern_id"] in {"syn_overlap_long", "syn_overlap_short"}
871
+ ]
872
+ assert len(matches) == 2
873
+ assert screening._unique_instance_count(matches) == 1
874
+
875
+
876
+ @pytest.mark.parametrize(
877
+ "rule_id",
878
+ ["syn_nfkc_casefold", "syn_dash_same_line", "syn_soft_hyphen", "syn_hyphen_line_break"],
879
+ )
880
+ def test_frozen_normalization_cases_match(rule_id: str) -> None:
881
+ assert any(item["pattern_id"] == rule_id for item in _report()["matches"])
882
+
883
+
884
+ @pytest.mark.parametrize(
885
+ ("raw", "expected", "expected_start"),
886
+ [
887
+ ("a-\nb", ["ab"], 0),
888
+ ("silver- \nleaf", ["silver", "leaf"], 0),
889
+ ("silver—\nleaf", ["silver", "leaf"], 0),
890
+ ("1-\na", ["1", "a"], 0),
891
+ ("a-\n2", ["a", "2"], 0),
892
+ ("a1-\nb", ["a1", "b"], 0),
893
+ ("a\u0301-\nb", ["á", "b"], 0),
894
+ ("\u0301alpha", ["alpha"], 1),
895
+ ],
896
+ )
897
+ def test_only_exact_ascii_immediate_line_hyphen_joins_and_marks_cannot_start(
898
+ raw: str, expected: list[str], expected_start: int
899
+ ) -> None:
900
+ tokens = screening.tokenize(raw)
901
+ assert [token.normalized for token in tokens] == expected
902
+ assert tokens[0].start == expected_start
903
+
904
+
905
+ def test_token_boundary_prevents_substring_match() -> None:
906
+ assert all(item["pattern_id"] != "syn_boundary_orb" for item in _report()["matches"])
907
+
908
+
909
+ def test_repeated_literal_hits_fail_closed_at_the_witness_cap() -> None:
910
+ tokens = screening.tokenize(" ".join(["orb"] * (screening.MAX_NODE_WITNESSES + 1)))
911
+ with pytest.raises(screening.MatchLimitError, match="literal produced more"):
912
+ screening.evaluate_expression({"op": "literal", "value": "orb"}, tokens)
913
+
914
+
915
+ def test_witness_reducer_deduplicates_only_exact_spans() -> None:
916
+ inner = screening.Witness(2, 3, 2, 3)
917
+ same = screening.Witness(2, 3, 2, 3)
918
+ outer = screening.Witness(1, 4, 1, 4)
919
+ separate = screening.Witness(5, 6, 5, 6)
920
+ assert screening._minimal_witnesses([outer, inner, same, separate]) == [
921
+ outer,
922
+ inner,
923
+ separate,
924
+ ]
925
+
926
+
927
+ def test_any_and_all_preserve_distinct_nested_witnesses() -> None:
928
+ tokens = screening.tokenize("amber reed reed")
929
+ any_values = screening.evaluate_expression(
930
+ {
931
+ "op": "any",
932
+ "alternatives": [
933
+ {"op": "literal", "value": "amber"},
934
+ {"op": "literal", "value": "amber reed"},
935
+ ],
936
+ },
937
+ tokens,
938
+ )
939
+ assert [(item.token_start, item.token_end) for item in any_values] == [
940
+ (0, 1),
941
+ (0, 2),
942
+ ]
943
+ all_values = screening.evaluate_expression(
944
+ {
945
+ "op": "all",
946
+ "terms": [
947
+ {"op": "literal", "value": "amber"},
948
+ {"op": "literal", "value": "reed"},
949
+ ],
950
+ "max_span_tokens": 3,
951
+ },
952
+ tokens,
953
+ )
954
+ assert [(item.token_start, item.token_end) for item in all_values] == [
955
+ (0, 2),
956
+ (0, 3),
957
+ ]
958
+
959
+
960
+ def test_full_scan_counts_nested_same_rule_witnesses_as_one_overlap_instance() -> None:
961
+ expression = {
962
+ "op": "any",
963
+ "alternatives": [
964
+ {"op": "literal", "value": "amber"},
965
+ {"op": "literal", "value": "amber reed"},
966
+ ],
967
+ }
968
+ base = _bundle()
969
+ rule = screening.CompiledRule(
970
+ rule_id="nested_union",
971
+ expression=expression,
972
+ exclude_if=(),
973
+ rule_sha256=screening._sha256_text(screening._canonical_json(expression)),
974
+ semantic_key=screening._canonical_json(expression),
975
+ )
976
+ bundle = screening.SnapshotBundle(
977
+ snapshot=base.snapshot,
978
+ manifest=base.manifest,
979
+ snapshot_sha256=base.snapshot_sha256,
980
+ manifest_sha256=base.manifest_sha256,
981
+ rules=(rule,),
982
+ unicode_data_version=base.unicode_data_version,
983
+ )
984
+ text = "amber reed"
985
+ matches, counts = screening.scan_segments(
986
+ text,
987
+ [screening.Segment("SEG-000001", "author_prose", 0, len(text))],
988
+ bundle,
989
+ artifact_sha256=hashlib.sha256(text.encode()).hexdigest(),
990
+ surface="own_draft",
991
+ )
992
+ assert len(matches) == 2
993
+ assert counts["rule_match_count"] == 2
994
+ assert counts["matched_rule_count"] == 1
995
+ assert counts["unique_instance_count"] == 1
996
+
997
+
998
+ def test_utf8_and_codepoint_spans_replay_multibyte_text() -> None:
999
+ report = screening.build_own_draft_report(
1000
+ "前文 luminous turnip 後文",
1001
+ artifact_id="unicode.md",
1002
+ artifact_format="markdown",
1003
+ state=_state(),
1004
+ checked_at=CHECKED_AT,
1005
+ recorded_at=RECORDED_AT,
1006
+ )
1007
+ match = next(item for item in report["matches"] if item["pattern_id"] == "syn_literal_author")
1008
+ span = match["source_span"]
1009
+ text = "前文 luminous turnip 後文"
1010
+ assert text[span["codepoint_start"] : span["codepoint_end"]] == match["matched_text"]
1011
+ raw = text.encode("utf-8")
1012
+ assert raw[span["utf8_start"] : span["utf8_end"]].decode() == match["matched_text"]
1013
+
1014
+
1015
+ @pytest.mark.parametrize("name", ["own_draft.md", "own_draft.tex"])
1016
+ def test_segment_partition_covers_source_exactly(name: str) -> None:
1017
+ text = _draft(name)
1018
+ segments = screening.segment_document(text, "latex" if name.endswith(".tex") else "markdown")
1019
+ assert segments[0].start == 0
1020
+ assert segments[-1].end == len(text)
1021
+ assert all(left.end == right.start for left, right in zip(segments, segments[1:]))
1022
+
1023
+
1024
+ def test_reference_heading_inside_fence_does_not_reclassify_following_prose() -> None:
1025
+ text = "```text\n# References\n```\nFollowing luminous turnip."
1026
+ segments = screening.segment_document(text, "markdown")
1027
+ following = next(item for item in segments if item.start <= text.index("Following") < item.end)
1028
+ assert following.kind == "author_prose"
1029
+
1030
+
1031
+ def test_reference_heading_inside_html_comment_does_not_reclassify_prose() -> None:
1032
+ text = "<!--\n# References\n-->\nordinary luminous turnip\n"
1033
+ segments = screening.segment_document(text, "markdown")
1034
+ following = next(
1035
+ item for item in segments if item.start <= text.index("ordinary") < item.end
1036
+ )
1037
+ assert following.kind == "author_prose"
1038
+ report = screening.build_own_draft_report(
1039
+ text,
1040
+ artifact_id="comment-heading.md",
1041
+ artifact_format="markdown",
1042
+ state=_state(),
1043
+ checked_at=CHECKED_AT,
1044
+ recorded_at=RECORDED_AT,
1045
+ )
1046
+ prose_match = next(
1047
+ item for item in report["matches"] if item["matched_text"] == "luminous turnip"
1048
+ )
1049
+ assert prose_match["context"] == "author_prose"
1050
+
1051
+
1052
+ @pytest.mark.parametrize("marker", ["```python", "~~~text"])
1053
+ def test_fence_opener_inside_html_comment_cannot_leak_unknown_context(
1054
+ marker: str,
1055
+ ) -> None:
1056
+ text = f"<!--\n{marker}\n-->\nordinary luminous turnip\n"
1057
+ segments = screening.segment_document(text, "markdown")
1058
+ following = next(
1059
+ item for item in segments if item.start <= text.index("ordinary") < item.end
1060
+ )
1061
+ assert following.kind == "author_prose"
1062
+ assert all(item.kind != "unknown" for item in segments)
1063
+
1064
+
1065
+ def test_latex_environment_opener_inside_comment_cannot_leak_context() -> None:
1066
+ text = (
1067
+ "% \\begin{thebibliography}{9}\n"
1068
+ "ordinary luminous turnip\n"
1069
+ "\\end{thebibliography}\n"
1070
+ )
1071
+ segments = screening.segment_document(text, "latex")
1072
+ following = next(
1073
+ item for item in segments if item.start <= text.index("ordinary") < item.end
1074
+ )
1075
+ assert following.kind == "author_prose"
1076
+ report = screening.build_own_draft_report(
1077
+ text,
1078
+ artifact_id="comment-environment.tex",
1079
+ artifact_format="latex",
1080
+ state=_state(),
1081
+ checked_at=CHECKED_AT,
1082
+ recorded_at=RECORDED_AT,
1083
+ )
1084
+ prose_match = next(
1085
+ item for item in report["matches"] if item["matched_text"] == "luminous turnip"
1086
+ )
1087
+ assert prose_match["context"] == "author_prose"
1088
+
1089
+
1090
+ @pytest.mark.parametrize(
1091
+ ("environment", "expected_kind"),
1092
+ [
1093
+ ("thebibliography", "reference_entry"),
1094
+ ("quote", "quote"),
1095
+ ("verbatim", "code_or_verbatim"),
1096
+ ],
1097
+ )
1098
+ def test_latex_environment_escape_parity(
1099
+ environment: str, expected_kind: str
1100
+ ) -> None:
1101
+ even = (
1102
+ "\\" * 2
1103
+ + f"begin{{{environment}}}\nordinary\n"
1104
+ + "\\" * 2
1105
+ + f"end{{{environment}}}\n"
1106
+ )
1107
+ assert all(
1108
+ item.kind == "author_prose"
1109
+ for item in screening.segment_document(even, "latex")
1110
+ )
1111
+ odd = (
1112
+ "\\" * 3
1113
+ + f"begin{{{environment}}}\nordinary\n"
1114
+ + "\\" * 3
1115
+ + f"end{{{environment}}}\n"
1116
+ )
1117
+ assert any(
1118
+ item.kind == expected_kind
1119
+ for item in screening.segment_document(odd, "latex")
1120
+ )
1121
+
1122
+
1123
+ def test_latex_verb_escape_parity_and_opaque_opener_precedence() -> None:
1124
+ even = r"\\verb|ordinary luminous turnip|"
1125
+ assert all(
1126
+ item.kind == "author_prose"
1127
+ for item in screening.segment_document(even, "latex")
1128
+ )
1129
+ active = r"\verb|\begin{verbatim}| ordinary luminous turnip"
1130
+ segments = screening.segment_document(active, "latex")
1131
+ following = next(
1132
+ item for item in segments if item.start <= active.index("ordinary") < item.end
1133
+ )
1134
+ assert following.kind == "author_prose"
1135
+ assert all(item.kind != "unknown" for item in segments)
1136
+
1137
+
1138
+ def test_source_order_opaque_lexer_does_not_let_inner_opener_consume_later_close() -> None:
1139
+ text = (
1140
+ r"\verb|\begin{verbatim}| "
1141
+ r"\begin{verbatim} recipe glossary \end{verbatim} moonlit pickle"
1142
+ )
1143
+ segments = screening.segment_document(text, "latex")
1144
+ real_environment = text.index(r"\begin{verbatim}", len(r"\verb|"))
1145
+ later_prose = text.index("moonlit pickle")
1146
+ assert next(
1147
+ item for item in segments if item.start <= real_environment < item.end
1148
+ ).kind == "code_or_verbatim"
1149
+ assert next(
1150
+ item for item in segments if item.start <= later_prose < item.end
1151
+ ).kind == "author_prose"
1152
+ report = screening.build_own_draft_report(
1153
+ text,
1154
+ artifact_id="source-order.tex",
1155
+ artifact_format="latex",
1156
+ state=_state(),
1157
+ checked_at=CHECKED_AT,
1158
+ recorded_at=RECORDED_AT,
1159
+ )
1160
+ assert any(
1161
+ item["pattern_id"] == "syn_exclude_segment"
1162
+ and item["matched_text"] == "moonlit pickle"
1163
+ for item in report["matches"]
1164
+ )
1165
+
1166
+
1167
+ def test_source_order_markdown_comment_does_not_consume_later_inline_code() -> None:
1168
+ text = "<!-- ` --> `luminous turnip`"
1169
+ segments = screening.segment_document(text, "markdown")
1170
+ later = text.index("`luminous")
1171
+ assert next(item for item in segments if item.start <= later < item.end).kind == (
1172
+ "code_or_verbatim"
1173
+ )
1174
+ assert all(item.kind != "unknown" for item in segments)
1175
+
1176
+
1177
+ def test_blockquote_marker_inside_comment_cannot_capture_later_prose() -> None:
1178
+ text = '<!--\n> --> "recipe glossary" moonlit pickle\n'
1179
+ segments = screening.segment_document(text, "markdown")
1180
+ target = text.index("moonlit pickle")
1181
+ assert next(item for item in segments if item.start <= target < item.end).kind == (
1182
+ "author_prose"
1183
+ )
1184
+ report = screening.build_own_draft_report(
1185
+ text,
1186
+ artifact_id="comment-blockquote.md",
1187
+ artifact_format="markdown",
1188
+ state=_state(),
1189
+ checked_at=CHECKED_AT,
1190
+ recorded_at=RECORDED_AT,
1191
+ )
1192
+ assert any(
1193
+ item["pattern_id"] == "syn_exclude_segment"
1194
+ and item["matched_text"] == "moonlit pickle"
1195
+ for item in report["matches"]
1196
+ )
1197
+
1198
+
1199
+ def test_context_openers_inside_opaque_bytes_do_not_consume_later_context() -> None:
1200
+ text = '`"` "recipe glossary" moonlit pickle '
1201
+ text += '`[` [Visible title](https://doi.org/10.1/example)'
1202
+ segments = screening.segment_document(text, "markdown")
1203
+ glossary = text.index('"recipe glossary"')
1204
+ title = text.index("Visible title")
1205
+ assert next(item for item in segments if item.start <= glossary < item.end).kind == (
1206
+ "quote"
1207
+ )
1208
+ assert next(item for item in segments if item.start <= title < item.end).kind == (
1209
+ "cited_title"
1210
+ )
1211
+ report = screening.build_own_draft_report(
1212
+ text,
1213
+ artifact_id="opaque-context.md",
1214
+ artifact_format="markdown",
1215
+ state=_state(),
1216
+ checked_at=CHECKED_AT,
1217
+ recorded_at=RECORDED_AT,
1218
+ )
1219
+ assert any(
1220
+ item["pattern_id"] == "syn_exclude_segment"
1221
+ and item["matched_text"] == "moonlit pickle"
1222
+ for item in report["matches"]
1223
+ )
1224
+
1225
+
1226
+ @pytest.mark.parametrize(
1227
+ "text",
1228
+ [
1229
+ r"\verbose ordinary luminous turnip",
1230
+ r"\verbatim ordinary luminous turnip",
1231
+ ],
1232
+ )
1233
+ def test_latex_verb_control_word_prefixes_remain_prose(text: str) -> None:
1234
+ assert all(
1235
+ item.kind == "author_prose"
1236
+ for item in screening.segment_document(text, "latex")
1237
+ )
1238
+
1239
+
1240
+ def test_latex_starred_verb_and_nested_quote_are_recognized() -> None:
1241
+ starred = r"\verb*|ordinary luminous turnip|"
1242
+ assert any(
1243
+ item.kind == "code_or_verbatim"
1244
+ and starred[item.start : item.end] == starred
1245
+ for item in screening.segment_document(starred, "latex")
1246
+ )
1247
+ nested = (
1248
+ r"\begin{quote} outer \begin{quote} inner \end{quote} "
1249
+ r"tail \end{quote}"
1250
+ )
1251
+ tail = nested.index("tail")
1252
+ assert next(
1253
+ item
1254
+ for item in screening.segment_document(nested, "latex")
1255
+ if item.start <= tail < item.end
1256
+ ).kind == "quote"
1257
+
1258
+
1259
+ @pytest.mark.parametrize("text", [r"\verb ", "\\verb\n", r"\verb* "])
1260
+ def test_malformed_bare_latex_verb_is_unknown(text: str) -> None:
1261
+ assert any(
1262
+ item.kind == "unknown"
1263
+ for item in screening.segment_document(text, "latex")
1264
+ )
1265
+
1266
+
1267
+ def test_starred_latex_verb_cannot_backtrack_into_unstarred_delimiter() -> None:
1268
+ text = r"\verb* foo * ordinary luminous turnip"
1269
+ segments = screening.segment_document(text, "latex")
1270
+ assert len(segments) == 1
1271
+ assert segments[0].kind == "unknown"
1272
+
1273
+
1274
+ def test_latex_accent_macro_quotes_remain_author_prose() -> None:
1275
+ text = r'\"o ordinary luminous turnip \"u'
1276
+ assert all(
1277
+ item.kind == "author_prose"
1278
+ for item in screening.segment_document(text, "latex")
1279
+ )
1280
+
1281
+
1282
+ @pytest.mark.parametrize(
1283
+ "target",
1284
+ [
1285
+ "HTTPS://DOI.ORG/10.1/example",
1286
+ "HTTP://DX.DOI.ORG/10.1/example",
1287
+ "DOI:10.1/example",
1288
+ ],
1289
+ )
1290
+ def test_doi_title_prefix_is_ascii_case_insensitive(target: str) -> None:
1291
+ text = f"[Cited Turnip]({target})"
1292
+ title = text.index("Cited Turnip")
1293
+ assert next(
1294
+ item
1295
+ for item in screening.segment_document(text, "markdown")
1296
+ if item.start <= title < item.end
1297
+ ).kind == "cited_title"
1298
+
1299
+
1300
+ def test_opaque_opener_cap_precedes_unclosed_verb_pairing(
1301
+ monkeypatch: pytest.MonkeyPatch,
1302
+ ) -> None:
1303
+ monkeypatch.setattr(screening, "MAX_PARSE_INTERVALS", 3)
1304
+ with pytest.raises(screening.MatchLimitError, match="opaque parse candidates"):
1305
+ screening.segment_document(r"\verb!" * 4, "latex")
1306
+
1307
+
1308
+ def test_unclosed_context_scanners_advance_monotonically() -> None:
1309
+ text = "[" * 20_000 + "\n" + "“" * 10_000
1310
+ segments = screening.segment_document(text, "markdown")
1311
+ assert len(segments) == 1
1312
+ assert segments[0].kind == "author_prose"
1313
+
1314
+
1315
+ def test_escaped_latex_environment_closer_does_not_close_early() -> None:
1316
+ text = (
1317
+ "\\begin{thebibliography}{9}\n"
1318
+ "\\\\end{thebibliography}\n"
1319
+ "ordinary luminous turnip\n"
1320
+ "\\end{thebibliography}\n"
1321
+ )
1322
+ following = next(
1323
+ item
1324
+ for item in screening.segment_document(text, "latex")
1325
+ if item.start <= text.index("ordinary") < item.end
1326
+ )
1327
+ assert following.kind == "reference_entry"
1328
+
1329
+
1330
+ @pytest.mark.parametrize(
1331
+ "text",
1332
+ [
1333
+ r"\(\begin{quote}\) ordinary luminous turnip",
1334
+ r"$\begin{thebibliography}$ ordinary luminous turnip",
1335
+ r"\[\begin{verbatim}\] ordinary luminous turnip",
1336
+ ],
1337
+ )
1338
+ def test_latex_environment_openers_inside_math_are_opaque(text: str) -> None:
1339
+ segments = screening.segment_document(text, "latex")
1340
+ following = next(
1341
+ item for item in segments if item.start <= text.index("ordinary") < item.end
1342
+ )
1343
+ assert following.kind == "author_prose"
1344
+ assert all(item.kind != "unknown" for item in segments)
1345
+
1346
+
1347
+ @pytest.mark.parametrize("artifact_format", ["markdown", "latex"])
1348
+ def test_odd_backslash_escaped_dollar_is_literal_prose(
1349
+ artifact_format: str,
1350
+ ) -> None:
1351
+ text = r"Cost is \$5."
1352
+ segments = screening.segment_document(text, artifact_format)
1353
+ assert all(segment.kind == "author_prose" for segment in segments)
1354
+
1355
+
1356
+ def test_markdown_backtick_escape_parity_is_respected() -> None:
1357
+ escaped = r"Literal \` tick."
1358
+ assert all(
1359
+ segment.kind == "author_prose"
1360
+ for segment in screening.segment_document(escaped, "markdown")
1361
+ )
1362
+ active = r"Literal \\`code` tick."
1363
+ assert any(
1364
+ segment.kind == "code_or_verbatim"
1365
+ for segment in screening.segment_document(active, "markdown")
1366
+ )
1367
+
1368
+
1369
+ def test_latex_percent_escape_parity_is_respected() -> None:
1370
+ escaped = r"Literal \% text."
1371
+ assert all(
1372
+ segment.kind == "author_prose"
1373
+ for segment in screening.segment_document(escaped, "latex")
1374
+ )
1375
+ active = r"Literal \\% comment"
1376
+ assert any(
1377
+ segment.kind == "code_or_verbatim"
1378
+ for segment in screening.segment_document(active, "latex")
1379
+ )
1380
+
1381
+
1382
+ def test_unclosed_construct_is_unknown_and_degrades_not_clean() -> None:
1383
+ report = screening.build_own_draft_report(
1384
+ "Ordinary prose `luminous turnip",
1385
+ artifact_id="broken.md",
1386
+ artifact_format="markdown",
1387
+ state=_state(),
1388
+ checked_at=CHECKED_AT,
1389
+ recorded_at=RECORDED_AT,
1390
+ )
1391
+ assert report["check_status"] == "degraded"
1392
+ assert report["finding"] == "unresolved"
1393
+ assert report["reason_code"] == "DOCUMENT_PARSE_DEGRADED"
1394
+ assert report["counts"]["unknown_segments"] > 0
1395
+ assert report["matches"]
1396
+ assert report["matches"][0]["disposition"] == "review_unknown_no_automatic_rewrite"
1397
+
1398
+
1399
+ def test_segment_resource_cap_degrades_without_partial_matches(
1400
+ monkeypatch: pytest.MonkeyPatch,
1401
+ ) -> None:
1402
+ monkeypatch.setattr(screening, "MAX_SEGMENTS", 1)
1403
+ report = screening.build_own_draft_report(
1404
+ "prose `code` prose",
1405
+ artifact_id="many-segments.md",
1406
+ artifact_format="markdown",
1407
+ state=_state(),
1408
+ checked_at=CHECKED_AT,
1409
+ recorded_at=RECORDED_AT,
1410
+ )
1411
+ assert report["check_status"] == "degraded"
1412
+ assert report["reason_code"] == "MATCH_RESOURCE_LIMIT"
1413
+ assert report["matches"] == []
1414
+ assert report["counts"]["rules_evaluated"] == 0
1415
+
1416
+
1417
+ @pytest.mark.parametrize(
1418
+ "text",
1419
+ [
1420
+ "a\n\nb\n\nc\n\n",
1421
+ "# References\none\ntwo\nthree\n",
1422
+ ],
1423
+ )
1424
+ def test_partition_boundaries_fail_while_collecting(
1425
+ monkeypatch: pytest.MonkeyPatch, text: str
1426
+ ) -> None:
1427
+ monkeypatch.setattr(screening, "MAX_SEGMENTS", 2)
1428
+ with pytest.raises(screening.MatchLimitError, match="document partition exceeds 2"):
1429
+ screening.segment_document(text, "markdown")
1430
+
1431
+
1432
+ def test_over_token_input_fails_before_utf8_endpoint_projection(
1433
+ monkeypatch: pytest.MonkeyPatch,
1434
+ ) -> None:
1435
+ bundle = _bundle()
1436
+ monkeypatch.setattr(screening, "MAX_TOKENS", 1)
1437
+
1438
+ def unexpected_offsets(_text, _indices):
1439
+ raise AssertionError("UTF-8 endpoint projection must follow token admission")
1440
+
1441
+ monkeypatch.setattr(screening, "_selected_byte_offsets", unexpected_offsets)
1442
+ with pytest.raises(screening.MatchLimitError, match="token count exceeds 1"):
1443
+ screening.scan_segments(
1444
+ "alpha beta",
1445
+ [screening.Segment("SEG-000001", "author_prose", 0, 10)],
1446
+ bundle,
1447
+ artifact_sha256=hashlib.sha256(b"alpha beta").hexdigest(),
1448
+ surface="own_draft",
1449
+ )
1450
+
1451
+
1452
+ def test_raw_token_expansion_is_bounded_before_normalization(
1453
+ monkeypatch: pytest.MonkeyPatch,
1454
+ ) -> None:
1455
+ monkeypatch.setattr(screening, "MAX_RAW_TOKEN_CODEPOINTS", 2)
1456
+
1457
+ def unexpected_normalization(_value: str):
1458
+ raise AssertionError("overlong raw token must fail before NFKC expansion")
1459
+
1460
+ monkeypatch.setattr(screening, "_normalized_token_parts", unexpected_normalization)
1461
+ with pytest.raises(screening.MatchLimitError, match="raw token exceeds 2"):
1462
+ screening.tokenize("\ufdfa" * 3)
1463
+
1464
+
1465
+ def test_raw_token_cap_accepts_n_and_rejects_n_plus_one(
1466
+ monkeypatch: pytest.MonkeyPatch,
1467
+ ) -> None:
1468
+ monkeypatch.setattr(screening, "MAX_RAW_TOKEN_CODEPOINTS", 2)
1469
+ assert [item.normalized for item in screening.tokenize("ab")] == ["ab"]
1470
+ with pytest.raises(screening.MatchLimitError, match="raw token exceeds 2"):
1471
+ screening.tokenize("abc")
1472
+
1473
+
1474
+ def test_parser_work_budget_accepts_n_and_rejects_n_plus_one(
1475
+ monkeypatch: pytest.MonkeyPatch,
1476
+ ) -> None:
1477
+ monkeypatch.setattr(screening, "MAX_PARSE_WORK_UNITS", 2)
1478
+ budget = screening.ParseWorkBudget()
1479
+ budget.spend()
1480
+ budget.spend()
1481
+ with pytest.raises(screening.MatchLimitError, match="parser work exceeds 2"):
1482
+ budget.spend()
1483
+
1484
+
1485
+ def test_escaped_and_doi_closer_candidates_spend_parser_budget(
1486
+ monkeypatch: pytest.MonkeyPatch,
1487
+ ) -> None:
1488
+ monkeypatch.setattr(screening, "MAX_PARSE_WORK_UNITS", 2)
1489
+ accepted = screening.ParseWorkBudget()
1490
+ assert screening._find_unescaped(r"\$$", "$", 0, budget=accepted) == 2
1491
+ exhausted = screening.ParseWorkBudget()
1492
+ with pytest.raises(screening.MatchLimitError, match="parser work exceeds 2"):
1493
+ screening._find_unescaped(r"\$\$$", "$", 0, budget=exhausted)
1494
+ doi_budget = screening.ParseWorkBudget()
1495
+ with pytest.raises(screening.MatchLimitError, match="parser work exceeds 2"):
1496
+ screening._doi_title_intervals(
1497
+ "[title](doi:10.1/example)",
1498
+ excluded=(),
1499
+ budget=doi_budget,
1500
+ )
1501
+
1502
+
1503
+ @pytest.mark.parametrize(
1504
+ ("text", "artifact_format"),
1505
+ [
1506
+ ("<!--\n> one\n> two\n> three\n-->", "markdown"),
1507
+ (r"\end{quote}\end{quote}\end{quote}\end{quote}", "latex"),
1508
+ ],
1509
+ )
1510
+ def test_excluded_and_unmatched_context_candidates_spend_parser_budget(
1511
+ monkeypatch: pytest.MonkeyPatch,
1512
+ text: str,
1513
+ artifact_format: str,
1514
+ ) -> None:
1515
+ monkeypatch.setattr(screening, "MAX_PARSE_WORK_UNITS", 3)
1516
+ with pytest.raises(screening.MatchLimitError, match="parser work exceeds 3"):
1517
+ screening.segment_document(text, artifact_format)
1518
+
1519
+
1520
+ def test_selected_utf8_offsets_are_exact_and_endpoint_bounded() -> None:
1521
+ text = "A台灣B"
1522
+ assert screening._selected_byte_offsets(text, {0, 1, 3, 4}) == {
1523
+ 0: 0,
1524
+ 1: 1,
1525
+ 3: 7,
1526
+ 4: 8,
1527
+ }
1528
+
1529
+
1530
+ def test_delimiter_parser_enforces_interval_cap_before_return(
1531
+ monkeypatch: pytest.MonkeyPatch,
1532
+ ) -> None:
1533
+ monkeypatch.setattr(screening, "MAX_PARSE_INTERVALS", 2)
1534
+ with pytest.raises(screening.MatchLimitError, match="parse interval count exceeds 2"):
1535
+ screening._paired_delimiter_intervals(
1536
+ "$one$ $two$ $three$", "$", "$", "code_or_verbatim"
1537
+ )
1538
+
1539
+
1540
+ def test_rule_by_segment_work_is_globally_bounded(
1541
+ monkeypatch: pytest.MonkeyPatch,
1542
+ ) -> None:
1543
+ monkeypatch.setattr(screening, "MAX_RULE_SEGMENT_EVALUATIONS", 1)
1544
+ with pytest.raises(
1545
+ screening.MatchLimitError, match="rule-by-segment evaluations exceed 1"
1546
+ ):
1547
+ screening.scan_segments(
1548
+ "luminous turnip",
1549
+ [screening.Segment("SEG-000001", "author_prose", 0, 16)],
1550
+ _bundle(),
1551
+ artifact_sha256=hashlib.sha256(b"luminous turnip").hexdigest(),
1552
+ surface="own_draft",
1553
+ )
1554
+
1555
+
1556
+ def test_literal_and_composition_work_budget_is_shared_across_corpus(
1557
+ monkeypatch: pytest.MonkeyPatch,
1558
+ ) -> None:
1559
+ monkeypatch.setattr(screening, "MAX_MATCH_WORK_UNITS", 300)
1560
+ output = _enriched()
1561
+ first, second = output["literature_corpus"][:2]
1562
+ assert all(
1563
+ signal["check_status"] == "checked"
1564
+ for signal in first["bibliographic_integrity_signals"]
1565
+ )
1566
+ second_title = next(
1567
+ signal
1568
+ for signal in second["bibliographic_integrity_signals"]
1569
+ if signal["tortured_phrase_context"]["surface"] == "cited_title"
1570
+ )
1571
+ assert second_title["check_status"] == "degraded"
1572
+ assert (
1573
+ second_title["tortured_phrase_context"]["reason_code"]
1574
+ == "MATCH_RESOURCE_LIMIT"
1575
+ )
1576
+ assert second_title["tortured_phrase_context"]["matches"] == []
1577
+
1578
+
1579
+ def test_report_digest_and_full_replay() -> None:
1580
+ report = _report()
1581
+ screening.validate_own_draft_report(report, _draft("own_draft.md"), state=_state())
1582
+ report["counts"]["rule_match_count"] += 1
1583
+ with pytest.raises(screening.ScreeningError, match="report_sha256"):
1584
+ screening.validate_own_draft_report(report, _draft("own_draft.md"), state=_state())
1585
+
1586
+
1587
+ def test_rehashed_report_still_fails_source_replay() -> None:
1588
+ report = _report()
1589
+ report["matches"][0]["matched_text"] = "forged"
1590
+ report["matches"][0]["matched_text_sha256"] = screening._sha256_text("forged")
1591
+ report["report_sha256"] = screening._report_digest(report)
1592
+ with pytest.raises(screening.ScreeningError, match="replay"):
1593
+ screening.validate_own_draft_report(report, _draft("own_draft.md"), state=_state())
1594
+
1595
+
1596
+ def test_oversized_full_report_collapses_to_a_self_readable_degraded_artifact(
1597
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
1598
+ ) -> None:
1599
+ text = " ".join(["luminous turnip"] * 100)
1600
+ monkeypatch.setattr(screening, "MAX_ADVISORY_BYTES", 5000)
1601
+ report = screening.build_own_draft_report(
1602
+ text,
1603
+ artifact_id="bounded.md",
1604
+ artifact_format="markdown",
1605
+ state=_state(),
1606
+ checked_at=CHECKED_AT,
1607
+ recorded_at=RECORDED_AT,
1608
+ )
1609
+ assert report["check_status"] == "degraded"
1610
+ assert report["reason_code"] == "MATCH_RESOURCE_LIMIT"
1611
+ assert report["matches"] == []
1612
+ assert len(screening._pretty_json_bytes(report)) <= screening.MAX_ADVISORY_BYTES
1613
+ path = tmp_path / "report.json"
1614
+ screening._atomic_write_json(
1615
+ path, report, maximum=screening.MAX_ADVISORY_BYTES
1616
+ )
1617
+ assert screening._load_report(path) == report
1618
+ screening.validate_own_draft_report(report, text, state=_state())
1619
+
1620
+
1621
+ def test_bounded_reader_rejects_before_consuming_beyond_limit(tmp_path: Path) -> None:
1622
+ path = tmp_path / "oversized.bin"
1623
+ path.write_bytes(b"x" * 9)
1624
+ with pytest.raises(screening.MatchLimitError, match="exceeds 8 bytes"):
1625
+ screening._read_bounded_bytes(path, maximum=8)
1626
+
1627
+
1628
+ def test_no_snapshot_is_explicit_not_checked() -> None:
1629
+ state = screening.snapshot_state(None, None)
1630
+ report = screening.build_own_draft_report(
1631
+ "luminous turnip",
1632
+ artifact_id="draft.md",
1633
+ artifact_format="markdown",
1634
+ state=state,
1635
+ checked_at=CHECKED_AT,
1636
+ recorded_at=RECORDED_AT,
1637
+ )
1638
+ assert report["check_status"] == "not_checked"
1639
+ assert report["finding"] == "unresolved"
1640
+ assert report["reason_code"] == "SNAPSHOT_NOT_PROVIDED"
1641
+ assert report["matches"] == []
1642
+
1643
+
1644
+ @pytest.mark.parametrize(
1645
+ "state",
1646
+ [
1647
+ screening.snapshot_state(None, None),
1648
+ screening.snapshot_state(FIXTURES / "snapshot.json", None),
1649
+ ],
1650
+ )
1651
+ def test_empty_draft_preserves_missing_or_degraded_snapshot_authority(
1652
+ state: screening.SnapshotState,
1653
+ ) -> None:
1654
+ report = screening.build_own_draft_report(
1655
+ "",
1656
+ artifact_id="empty.md",
1657
+ artifact_format="markdown",
1658
+ state=state,
1659
+ checked_at=CHECKED_AT,
1660
+ recorded_at=RECORDED_AT,
1661
+ )
1662
+ assert report["check_status"] == (
1663
+ "not_checked" if state.status == "not_checked" else "degraded"
1664
+ )
1665
+ assert report["reason_code"] == state.reason_code
1666
+ assert report["finding"] == "unresolved"
1667
+ screening.validate_own_draft_report(report, "", state=state)
1668
+
1669
+
1670
+ def test_empty_draft_is_degraded_never_a_zero_match() -> None:
1671
+ report = screening.build_own_draft_report(
1672
+ " \t\n",
1673
+ artifact_id="empty.md",
1674
+ artifact_format="markdown",
1675
+ state=_state(),
1676
+ checked_at=CHECKED_AT,
1677
+ recorded_at=RECORDED_AT,
1678
+ )
1679
+ assert report["check_status"] == "degraded"
1680
+ assert report["finding"] == "unresolved"
1681
+ assert report["reason_code"] == "DOCUMENT_EMPTY"
1682
+ assert report["matches"] == []
1683
+ assert report["counts"]["rules_evaluated"] == 0
1684
+
1685
+
1686
+ def test_snapshot_byte_tamper_fails_hash_before_content_access() -> None:
1687
+ manifest = FIXTURES / "snapshot_manifest.json"
1688
+ state = screening.snapshot_state(FIXTURES / "own_draft.md", manifest)
1689
+ assert state.status == "degraded"
1690
+ assert state.reason_code == "SNAPSHOT_HASH_MISMATCH"
1691
+
1692
+
1693
+ @pytest.mark.parametrize(
1694
+ ("raw", "reason"),
1695
+ [
1696
+ (b"\xef\xbb\xbf{}", "SNAPSHOT_BYTES_INVALID"),
1697
+ (b"\xff", "SNAPSHOT_BYTES_INVALID"),
1698
+ (b'{"schema_version":"a","schema_version":"b"}', "SNAPSHOT_BYTES_INVALID"),
1699
+ (b'{"x":NaN}', "SNAPSHOT_BYTES_INVALID"),
1700
+ ],
1701
+ )
1702
+ def test_strict_snapshot_loader_rejects_unsafe_bytes(
1703
+ tmp_path: Path, raw: bytes, reason: str
1704
+ ) -> None:
1705
+ snapshot_path = tmp_path / "bad.json"
1706
+ snapshot_path.write_bytes(raw)
1707
+ manifest = json.loads(
1708
+ (FIXTURES / "snapshot_manifest.json").read_text(encoding="utf-8")
1709
+ )
1710
+ manifest["snapshot_sha256"] = hashlib.sha256(raw).hexdigest()
1711
+ manifest_path = tmp_path / "manifest.json"
1712
+ _strict_dump(manifest_path, manifest)
1713
+ state = screening.snapshot_state(snapshot_path, manifest_path)
1714
+ assert state.status == "degraded"
1715
+ assert state.reason_code == reason
1716
+
1717
+
1718
+ def test_draft_reader_rejects_isolated_carriage_return(tmp_path: Path) -> None:
1719
+ draft = tmp_path / "draft.md"
1720
+ draft.write_bytes(b"alpha\rbeta")
1721
+ with pytest.raises(screening.ScreeningError, match="isolated carriage return"):
1722
+ screening._read_strict_text(
1723
+ draft, maximum=screening.MAX_DOCUMENT_BYTES, label="draft"
1724
+ )
1725
+
1726
+
1727
+ def test_direct_builder_allows_crlf_but_rejects_isolated_carriage_return() -> None:
1728
+ screening.build_own_draft_report(
1729
+ "alpha\r\nbeta",
1730
+ artifact_id="draft.md",
1731
+ artifact_format="markdown",
1732
+ state=_state(),
1733
+ checked_at=CHECKED_AT,
1734
+ recorded_at=RECORDED_AT,
1735
+ )
1736
+ with pytest.raises(screening.ScreeningError, match="isolated carriage return"):
1737
+ screening.build_own_draft_report(
1738
+ "alpha\rbeta",
1739
+ artifact_id="draft.md",
1740
+ artifact_format="markdown",
1741
+ state=_state(),
1742
+ checked_at=CHECKED_AT,
1743
+ recorded_at=RECORDED_AT,
1744
+ )
1745
+
1746
+
1747
+ def test_manifest_hash_mismatch_never_checks_remaining_rules(tmp_path: Path) -> None:
1748
+ snapshot_path, manifest_path, snapshot, manifest = _snapshot_pair(tmp_path)
1749
+ snapshot["rules"][0]["expression"]["value"] = "changed phrase"
1750
+ _strict_dump(snapshot_path, snapshot)
1751
+ state = screening.snapshot_state(snapshot_path, manifest_path)
1752
+ assert state.reason_code == "SNAPSHOT_HASH_MISMATCH"
1753
+
1754
+
1755
+ def test_manifest_snapshot_identity_must_match_hash_bound_snapshot(
1756
+ tmp_path: Path,
1757
+ ) -> None:
1758
+ snapshot_path, manifest_path, _snapshot, manifest = _snapshot_pair(tmp_path)
1759
+ manifest["snapshot_id"] = "forged-snapshot-id"
1760
+ _strict_dump(manifest_path, manifest)
1761
+ state = screening.snapshot_state(snapshot_path, manifest_path)
1762
+ assert state.status == "degraded"
1763
+ assert state.reason_code == "SNAPSHOT_HASH_MISMATCH"
1764
+
1765
+
1766
+ @pytest.mark.parametrize(
1767
+ "mutation",
1768
+ [
1769
+ "synthetic_reference",
1770
+ "user_without_declaration",
1771
+ "permission_without_reference",
1772
+ "unresolved_but_permitted",
1773
+ "null_source_locator",
1774
+ ],
1775
+ )
1776
+ def test_manifest_rights_and_source_projection_fail_closed(
1777
+ tmp_path: Path, mutation: str
1778
+ ) -> None:
1779
+ snapshot_path, manifest_path, _snapshot, manifest = _snapshot_pair(tmp_path)
1780
+ if mutation == "synthetic_reference":
1781
+ manifest["rights"]["reference"] = "unexpected reference"
1782
+ elif mutation == "user_without_declaration":
1783
+ manifest["supply_mode"] = "user_supplied"
1784
+ manifest["rights"] = {
1785
+ "basis": "user_declared_authorized",
1786
+ "redistribution_status": "permitted",
1787
+ "reference": None,
1788
+ "user_declaration": None,
1789
+ }
1790
+ elif mutation == "permission_without_reference":
1791
+ manifest["supply_mode"] = "user_supplied"
1792
+ manifest["rights"] = {
1793
+ "basis": "written_permission",
1794
+ "redistribution_status": "permitted",
1795
+ "reference": None,
1796
+ "user_declaration": None,
1797
+ }
1798
+ elif mutation == "unresolved_but_permitted":
1799
+ manifest["supply_mode"] = "user_supplied"
1800
+ manifest["rights"] = {
1801
+ "basis": "unresolved",
1802
+ "redistribution_status": "permitted",
1803
+ "reference": None,
1804
+ "user_declaration": None,
1805
+ }
1806
+ else:
1807
+ manifest["source"]["locator"] = None
1808
+ _strict_dump(manifest_path, manifest)
1809
+ state = screening.snapshot_state(snapshot_path, manifest_path)
1810
+ assert state.status == "degraded"
1811
+ assert state.reason_code == "SNAPSHOT_MANIFEST_INVALID"
1812
+
1813
+
1814
+ def test_advisory_schema_mirrors_manifest_rights_conditionals() -> None:
1815
+ report = _report()
1816
+ validator = Draft202012Validator(
1817
+ screening._load_schema(screening.ADVISORY_SCHEMA_PATH),
1818
+ format_checker=Draft202012Validator.FORMAT_CHECKER,
1819
+ )
1820
+ rights = report["input_binding"]["snapshot"]["rights"]
1821
+ rights["basis"] = "user_declared_authorized"
1822
+ rights["user_declaration"] = None
1823
+ assert list(validator.iter_errors(report))
1824
+
1825
+
1826
+ def test_explicit_but_unreadable_snapshot_is_degraded_not_absent(
1827
+ tmp_path: Path,
1828
+ ) -> None:
1829
+ missing = tmp_path / "missing-snapshot.json"
1830
+ state = screening.snapshot_state(missing, FIXTURES / "snapshot_manifest.json")
1831
+ assert state.status == "degraded"
1832
+ assert state.reason_code == "SNAPSHOT_BYTES_INVALID"
1833
+ signal = screening.build_cited_signal(
1834
+ _passport()["literature_corpus"][0],
1835
+ surface="cited_title",
1836
+ state=state,
1837
+ checked_at=CHECKED_AT,
1838
+ recorded_at=RECORDED_AT,
1839
+ )
1840
+ assert signal["check_status"] == "degraded"
1841
+ assert signal["tortured_phrase_context"]["reason_code"] == "SNAPSHOT_BYTES_INVALID"
1842
+
1843
+
1844
+ @pytest.mark.parametrize(
1845
+ ("mutation", "expected_reason"),
1846
+ [
1847
+ (lambda snap, man: snap.update({"grammar_profile": "unknown/9"}), "SNAPSHOT_PROFILE_UNSUPPORTED"),
1848
+ (lambda snap, man: snap.update({"rules": []}), "SNAPSHOT_MANIFEST_INVALID"),
1849
+ (
1850
+ lambda snap, man: snap["rules"][0].update(
1851
+ {"expression": {"op": "regex", "value": ".*"}}
1852
+ ),
1853
+ "SNAPSHOT_RULES_UNSUPPORTED",
1854
+ ),
1855
+ (
1856
+ lambda snap, man: snap["rules"].append(copy.deepcopy(snap["rules"][0])),
1857
+ "SNAPSHOT_RULES_UNSUPPORTED",
1858
+ ),
1859
+ (
1860
+ lambda snap, man: man.update({"unsupported_rule_count": 1}),
1861
+ "SNAPSHOT_RULES_UNSUPPORTED",
1862
+ ),
1863
+ ],
1864
+ )
1865
+ def test_invalid_or_partial_grammar_is_never_clean(
1866
+ tmp_path: Path, mutation, expected_reason: str
1867
+ ) -> None:
1868
+ snapshot_path, manifest_path, snapshot, manifest = _snapshot_pair(tmp_path)
1869
+ mutation(snapshot, manifest)
1870
+ _rewrite_pair(snapshot_path, manifest_path, snapshot, manifest)
1871
+ state = screening.snapshot_state(snapshot_path, manifest_path)
1872
+ assert state.status == "degraded"
1873
+ assert state.reason_code == expected_reason
1874
+
1875
+
1876
+ @pytest.mark.parametrize(
1877
+ "injected_value",
1878
+ ["grammar_profile", "normalizer_profile", "schema_version"],
1879
+ )
1880
+ def test_invalid_rule_text_cannot_inject_profile_failure_reason(
1881
+ tmp_path: Path, injected_value: str
1882
+ ) -> None:
1883
+ snapshot_path, manifest_path, snapshot, manifest = _snapshot_pair(tmp_path)
1884
+ snapshot["rules"][0]["expression"] = {
1885
+ "op": "regex",
1886
+ "value": injected_value,
1887
+ }
1888
+ _rewrite_pair(snapshot_path, manifest_path, snapshot, manifest)
1889
+ state = screening.snapshot_state(snapshot_path, manifest_path)
1890
+ assert state.status == "degraded"
1891
+ assert state.reason_code == "SNAPSHOT_RULES_UNSUPPORTED"
1892
+
1893
+
1894
+ def test_semantically_duplicate_rule_is_rejected(tmp_path: Path) -> None:
1895
+ snapshot_path, manifest_path, snapshot, manifest = _snapshot_pair(tmp_path)
1896
+ duplicate = copy.deepcopy(snapshot["rules"][0])
1897
+ duplicate["rule_id"] = "different_id_same_semantics"
1898
+ snapshot["rules"].append(duplicate)
1899
+ _rewrite_pair(snapshot_path, manifest_path, snapshot, manifest)
1900
+ state = screening.snapshot_state(snapshot_path, manifest_path)
1901
+ assert state.reason_code == "SNAPSHOT_RULES_UNSUPPORTED"
1902
+
1903
+
1904
+ def test_near_gap_is_intervening_token_count_and_ordered() -> None:
1905
+ rule = next(item for item in _bundle().rules if item.rule_id == "syn_near_ordered")
1906
+ assert screening.evaluate_rule(rule, screening.tokenize("nebula x cabbage"))
1907
+ assert not screening.evaluate_rule(rule, screening.tokenize("nebula x y cabbage"))
1908
+ assert not screening.evaluate_rule(rule, screening.tokenize("cabbage x nebula"))
1909
+
1910
+
1911
+ def test_near_never_crosses_context_segments() -> None:
1912
+ report = screening.build_own_draft_report(
1913
+ "nebula `x` cabbage",
1914
+ artifact_id="segments.md",
1915
+ artifact_format="markdown",
1916
+ state=_state(),
1917
+ checked_at=CHECKED_AT,
1918
+ recorded_at=RECORDED_AT,
1919
+ )
1920
+ assert all(item["pattern_id"] != "syn_near_ordered" for item in report["matches"])
1921
+
1922
+
1923
+ def test_explicit_timestamps_only_no_ambient_clock_source() -> None:
1924
+ source = (SCRIPTS / "tortured_phrase_screening.py").read_text(encoding="utf-8")
1925
+ assert ".now(" not in source
1926
+ with pytest.raises(screening.ScreeningError, match="explicit RFC 3339"):
1927
+ screening.build_own_draft_report(
1928
+ "luminous turnip",
1929
+ artifact_id="draft.md",
1930
+ artifact_format="markdown",
1931
+ state=_state(),
1932
+ checked_at="today",
1933
+ recorded_at=RECORDED_AT,
1934
+ )
1935
+
1936
+
1937
+ def test_recorded_at_cannot_precede_checked_at() -> None:
1938
+ with pytest.raises(screening.ScreeningError, match="must not precede"):
1939
+ screening.build_own_draft_report(
1940
+ "luminous turnip",
1941
+ artifact_id="draft.md",
1942
+ artifact_format="markdown",
1943
+ state=_state(),
1944
+ checked_at="2026-08-10T01:00:01Z",
1945
+ recorded_at="2026-08-10T01:00:00Z",
1946
+ )
1947
+ entry = _passport()["literature_corpus"][0]
1948
+ with pytest.raises(screening.ScreeningError, match="must not precede"):
1949
+ screening.build_cited_signal(
1950
+ entry,
1951
+ surface="cited_title",
1952
+ state=_state(),
1953
+ checked_at="2026-08-10T01:00:01Z",
1954
+ recorded_at="2026-08-10T01:00:00Z",
1955
+ )
1956
+
1957
+
1958
+ def test_submicrosecond_timestamp_order_cannot_be_truncated() -> None:
1959
+ checked_at = "2026-08-10T01:00:00.0000009Z"
1960
+ recorded_at = "2026-08-10T01:00:00.0000001Z"
1961
+ with pytest.raises(screening.ScreeningError, match="explicit RFC 3339"):
1962
+ screening.build_own_draft_report(
1963
+ "luminous turnip",
1964
+ artifact_id="draft.md",
1965
+ artifact_format="markdown",
1966
+ state=_state(),
1967
+ checked_at=checked_at,
1968
+ recorded_at=recorded_at,
1969
+ )
1970
+ with pytest.raises(screening.ScreeningError, match="explicit RFC 3339"):
1971
+ screening.build_cited_signal(
1972
+ _passport()["literature_corpus"][0],
1973
+ surface="cited_title",
1974
+ state=_state(),
1975
+ checked_at=checked_at,
1976
+ recorded_at=recorded_at,
1977
+ )
1978
+
1979
+
1980
+ def test_renderer_is_one_page_bounded_and_has_claim_ceiling() -> None:
1981
+ report = _report()
1982
+ rendered = screening.render_own_draft_report(
1983
+ report, _draft("own_draft.md"), state=_state()
1984
+ )
1985
+ assert screening.SUMMARY_LABEL in rendered
1986
+ assert "does not establish papermill, AI, or author origin" in rendered
1987
+ assert "not a clean-text certificate" in rendered
1988
+ assert "one fixed page capped at 25" in rendered
1989
+ assert "--all" not in rendered
1990
+
1991
+
1992
+ def test_renderer_reports_omitted_count_and_machine_replay_key() -> None:
1993
+ text = " ".join(["luminous turnip"] * 30)
1994
+ report = screening.build_own_draft_report(
1995
+ text,
1996
+ artifact_id="many-matches.md",
1997
+ artifact_format="markdown",
1998
+ state=_state(),
1999
+ checked_at=CHECKED_AT,
2000
+ recorded_at=RECORDED_AT,
2001
+ )
2002
+ assert len(report["matches"]) > screening.MAX_RENDER_PAGE_SIZE
2003
+ rendered = screening.render_own_draft_report(report, text, state=_state())
2004
+ omitted = len(report["matches"]) - screening.MAX_RENDER_PAGE_SIZE
2005
+ assert f"omitted {omitted}" in rendered
2006
+ assert "Complete machine JSON replay key" in rendered
2007
+ assert "artifact_id=many-matches.md" in rendered
2008
+ assert report["report_sha256"] in rendered
2009
+
2010
+
2011
+ def test_renderer_uses_outcome_specific_zero_and_unresolved_wording() -> None:
2012
+ zero = screening.build_own_draft_report(
2013
+ "ordinary unmatched prose",
2014
+ artifact_id="zero.md",
2015
+ artifact_format="markdown",
2016
+ state=_state(),
2017
+ checked_at=CHECKED_AT,
2018
+ recorded_at=RECORDED_AT,
2019
+ )
2020
+ rendered_zero = screening.render_own_draft_report(
2021
+ zero, "ordinary unmatched prose", state=_state()
2022
+ )
2023
+ assert screening.SUMMARY_LABEL not in rendered_zero
2024
+ assert "No phrase-list match observed on the checked surface" in rendered_zero
2025
+ assert "not a clean-text certificate" in rendered_zero
2026
+
2027
+ unresolved_state = screening.snapshot_state(None, None)
2028
+ unresolved = screening.build_own_draft_report(
2029
+ "ordinary unmatched prose",
2030
+ artifact_id="unresolved.md",
2031
+ artifact_format="markdown",
2032
+ state=unresolved_state,
2033
+ checked_at=CHECKED_AT,
2034
+ recorded_at=RECORDED_AT,
2035
+ )
2036
+ rendered_unresolved = screening.render_own_draft_report(
2037
+ unresolved, "ordinary unmatched prose", state=unresolved_state
2038
+ )
2039
+ assert screening.SUMMARY_LABEL not in rendered_unresolved
2040
+ assert "screening is unresolved" in rendered_unresolved
2041
+
2042
+
2043
+ def test_renderer_escapes_markdown_and_html_injection() -> None:
2044
+ assert "<img" not in screening._markdown_cell("<img src=x>")
2045
+ assert "\\[click\\]" in screening._markdown_cell("[click](https://example.invalid)")
2046
+
2047
+
2048
+ def test_corpus_rows_are_per_surface_and_missing_abstract_is_explicit() -> None:
2049
+ output = _enriched()
2050
+ by_key = {item["citation_key"]: item for item in output["literature_corpus"]}
2051
+ complete = by_key["fixture_complete_2026"]["bibliographic_integrity_signals"]
2052
+ assert {item["tortured_phrase_context"]["surface"] for item in complete} == {
2053
+ "cited_title",
2054
+ "cited_abstract",
2055
+ }
2056
+ missing = by_key["fixture_missing_2026"]["bibliographic_integrity_signals"]
2057
+ abstract = next(
2058
+ item for item in missing if item["tortured_phrase_context"]["surface"] == "cited_abstract"
2059
+ )
2060
+ title = next(
2061
+ item for item in missing if item["tortured_phrase_context"]["surface"] == "cited_title"
2062
+ )
2063
+ assert title["finding"] == "detected"
2064
+ assert abstract["check_status"] == "not_checked"
2065
+ assert abstract["finding"] == "unresolved"
2066
+ assert abstract["tortured_phrase_context"]["reason_code"] == "ABSTRACT_MISSING"
2067
+ assert abstract["tortured_phrase_context"]["counts"]["rules_evaluated"] == 0
2068
+
2069
+
2070
+ def test_whitespace_abstract_is_explicitly_empty_not_checked() -> None:
2071
+ document = _passport()
2072
+ document["literature_corpus"][0]["abstract"] = " \t\n"
2073
+ output = screening.enrich_passport(
2074
+ document,
2075
+ state=_state(),
2076
+ checked_at=CHECKED_AT,
2077
+ recorded_at=RECORDED_AT,
2078
+ )
2079
+ abstract = next(
2080
+ item
2081
+ for item in output["literature_corpus"][0]["bibliographic_integrity_signals"]
2082
+ if item["tortured_phrase_context"]["surface"] == "cited_abstract"
2083
+ )
2084
+ assert abstract["check_status"] == "not_checked"
2085
+ assert abstract["finding"] == "unresolved"
2086
+ assert abstract["tortured_phrase_context"]["reason_code"] == "ABSTRACT_EMPTY"
2087
+ assert abstract["tortured_phrase_context"]["surface_binding"] == {
2088
+ "content_sha256": None,
2089
+ "content_utf8_bytes": None,
2090
+ }
2091
+ assert abstract["tortured_phrase_context"]["counts"]["rules_evaluated"] == 0
2092
+
2093
+
2094
+ def test_legitimate_negative_only_means_no_configured_match() -> None:
2095
+ output = _enriched()
2096
+ entry = next(
2097
+ item for item in output["literature_corpus"] if item["citation_key"] == "fixture_negative_2026"
2098
+ )
2099
+ assert all(
2100
+ signal["finding"] == "not_detected"
2101
+ for signal in entry["bibliographic_integrity_signals"]
2102
+ )
2103
+ assert all(
2104
+ signal["tortured_phrase_context"]["boundary"]["absence_is_clean_certificate"]
2105
+ is False
2106
+ for signal in entry["bibliographic_integrity_signals"]
2107
+ )
2108
+
2109
+
2110
+ def test_manual_entries_are_not_exempt_and_pointer_is_not_dereferenced() -> None:
2111
+ output = _enriched()
2112
+ entry = output["literature_corpus"][0]
2113
+ assert entry["obtained_via"] == "manual"
2114
+ assert entry["source_pointer"].startswith("fixture://")
2115
+ assert len(entry["bibliographic_integrity_signals"]) == 2
2116
+
2117
+
2118
+ def test_enrichment_is_semantically_idempotent_and_supersedes_current_row() -> None:
2119
+ first = _enriched()
2120
+ second = screening.enrich_passport(
2121
+ first,
2122
+ state=_state(),
2123
+ checked_at=CHECKED_AT,
2124
+ recorded_at=RECORDED_AT,
2125
+ )
2126
+ assert second == first
2127
+ first["literature_corpus"][0]["title"] += " changed"
2128
+ third = screening.enrich_passport(
2129
+ first,
2130
+ state=_state(),
2131
+ checked_at=CHECKED_AT,
2132
+ recorded_at=RECORDED_AT,
2133
+ )
2134
+ signals = third["literature_corpus"][0]["bibliographic_integrity_signals"]
2135
+ assert len([item for item in signals if item["schema_version"] == screening.SIGNAL_VERSION]) == 2
2136
+
2137
+
2138
+ def test_enrichment_rejects_foreign_current_row_before_supersession() -> None:
2139
+ document = _enriched()
2140
+ row = document["literature_corpus"][0]["bibliographic_integrity_signals"][0]
2141
+ row["subject"]["source_pointer"] = "fixture://foreign-source"
2142
+ with pytest.raises(screening.ScreeningError, match="source_pointer does not belong"):
2143
+ screening.enrich_passport(
2144
+ document,
2145
+ state=_state(),
2146
+ checked_at=CHECKED_AT,
2147
+ recorded_at=RECORDED_AT,
2148
+ )
2149
+
2150
+
2151
+ def test_legacy_and_unrelated_signals_are_preserved() -> None:
2152
+ document = _passport()
2153
+ legacy = json.loads(
2154
+ (SCRIPTS / "fixtures/bibliographic_integrity_signals/tortured_phrase.json").read_text(
2155
+ encoding="utf-8"
2156
+ )
2157
+ )
2158
+ legacy["subject"]["citation_key"] = document["literature_corpus"][0]["citation_key"]
2159
+ legacy["subject"]["source_pointer"] = document["literature_corpus"][0]["source_pointer"]
2160
+ document["literature_corpus"][0]["bibliographic_integrity_signals"] = [legacy]
2161
+ output = screening.enrich_passport(
2162
+ document,
2163
+ state=_state(),
2164
+ checked_at=CHECKED_AT,
2165
+ recorded_at=RECORDED_AT,
2166
+ )
2167
+ signals = output["literature_corpus"][0]["bibliographic_integrity_signals"]
2168
+ assert signals[0] == legacy
2169
+ assert len(signals) == 3
2170
+
2171
+
2172
+ def test_multiple_current_rows_are_rejected() -> None:
2173
+ document = _enriched()
2174
+ entry = document["literature_corpus"][0]
2175
+ duplicate = copy.deepcopy(entry["bibliographic_integrity_signals"][0])
2176
+ context = duplicate["tortured_phrase_context"]
2177
+ context["snapshot"]["snapshot_sha256"] = "0" * 64
2178
+ context["snapshot"]["manifest_sha256"] = "1" * 64
2179
+ duplicate["provenance"]["source_sha256"] = "0" * 64
2180
+ duplicate["signal_id"] = screening._signal_id(
2181
+ entry["citation_key"],
2182
+ context["surface"],
2183
+ context["snapshot"]["snapshot_sha256"],
2184
+ context["surface_binding"]["content_sha256"],
2185
+ )
2186
+ for match in context["matches"]:
2187
+ span = match["source_span"]
2188
+ match["match_id"] = "tpm-" + screening._sha256_text(
2189
+ screening._canonical_json(
2190
+ {
2191
+ "artifact_sha256": context["surface_binding"]["content_sha256"],
2192
+ "snapshot_sha256": context["snapshot"]["snapshot_sha256"],
2193
+ "surface": context["surface"],
2194
+ "segment_id": match["segment_id"],
2195
+ "context": match["context"],
2196
+ "rule_id": match["pattern_id"],
2197
+ "codepoint_start": span["codepoint_start"],
2198
+ "codepoint_end": span["codepoint_end"],
2199
+ }
2200
+ )
2201
+ )[:24]
2202
+ entry["bibliographic_integrity_signals"].append(duplicate)
2203
+ with pytest.raises(screening.ScreeningError, match="multiple current"):
2204
+ screening.enrich_passport(
2205
+ document,
2206
+ state=_state(),
2207
+ checked_at=CHECKED_AT,
2208
+ recorded_at=RECORDED_AT,
2209
+ )
2210
+
2211
+
2212
+ def test_internally_invalid_current_row_is_not_silently_superseded() -> None:
2213
+ document = _enriched()
2214
+ row = document["literature_corpus"][0]["bibliographic_integrity_signals"][0]
2215
+ row["signal_id"] = row["signal_id"][:-1] + (
2216
+ "0" if row["signal_id"][-1] != "0" else "1"
2217
+ )
2218
+ with pytest.raises(screening.ScreeningError, match="internally inconsistent"):
2219
+ screening.enrich_passport(
2220
+ document,
2221
+ state=_state(),
2222
+ checked_at=CHECKED_AT,
2223
+ recorded_at=RECORDED_AT,
2224
+ )
2225
+
2226
+
2227
+ def test_generated_id_collision_with_preserved_legacy_row_is_rejected() -> None:
2228
+ document = _passport()
2229
+ generated = screening.build_cited_signal(
2230
+ document["literature_corpus"][0],
2231
+ surface="cited_title",
2232
+ state=_state(),
2233
+ checked_at=CHECKED_AT,
2234
+ recorded_at=RECORDED_AT,
2235
+ )
2236
+ legacy = json.loads(
2237
+ (SCRIPTS / "fixtures/bibliographic_integrity_signals/tortured_phrase.json").read_text(
2238
+ encoding="utf-8"
2239
+ )
2240
+ )
2241
+ legacy["signal_id"] = generated["signal_id"]
2242
+ legacy["subject"]["citation_key"] = document["literature_corpus"][0]["citation_key"]
2243
+ legacy["subject"]["source_pointer"] = document["literature_corpus"][0]["source_pointer"]
2244
+ document["literature_corpus"][0]["bibliographic_integrity_signals"] = [legacy]
2245
+ with pytest.raises(screening.ScreeningError, match="collides with preserved"):
2246
+ screening.enrich_passport(
2247
+ document,
2248
+ state=_state(),
2249
+ checked_at=CHECKED_AT,
2250
+ recorded_at=RECORDED_AT,
2251
+ )
2252
+
2253
+
2254
+ @pytest.mark.parametrize(
2255
+ "mutation",
2256
+ [
2257
+ "title",
2258
+ "source_pointer",
2259
+ "signal_id",
2260
+ "span",
2261
+ "count",
2262
+ "match_id",
2263
+ "evidence",
2264
+ ],
2265
+ )
2266
+ def test_cited_binding_mutations_fail(mutation: str) -> None:
2267
+ output = _enriched()
2268
+ entry = output["literature_corpus"][0]
2269
+ target_surface = "cited_abstract" if mutation == "span" else "cited_title"
2270
+ signal = next(
2271
+ item
2272
+ for item in entry["bibliographic_integrity_signals"]
2273
+ if item["tortured_phrase_context"]["surface"] == target_surface
2274
+ )
2275
+ if mutation == "title":
2276
+ entry["title"] += " mutation"
2277
+ elif mutation == "source_pointer":
2278
+ entry["source_pointer"] += "/mutation"
2279
+ elif mutation == "signal_id":
2280
+ signal["signal_id"] = signal["signal_id"][:-1] + "0"
2281
+ elif mutation == "span":
2282
+ signal["tortured_phrase_context"]["matches"][0]["source_span"]["utf8_end"] -= 1
2283
+ elif mutation == "count":
2284
+ signal["tortured_phrase_context"]["counts"]["rule_match_count"] += 1
2285
+ elif mutation == "match_id":
2286
+ signal["tortured_phrase_context"]["matches"][0]["match_id"] = "tpm-" + "0" * 24
2287
+ else:
2288
+ signal["evidence"][0]["observed_value"] = "forged"
2289
+ with pytest.raises(screening.ScreeningError):
2290
+ screening.validate_cited_signal_binding(signal, entry)
2291
+
2292
+
2293
+ @pytest.mark.parametrize(
2294
+ "mutation",
2295
+ [
2296
+ "missing_abstract_as_checked",
2297
+ "checked_with_zero_rules",
2298
+ "checked_with_unknown_segment",
2299
+ "match_context_crosses_surface",
2300
+ "outer_not_checked_with_loaded_snapshot",
2301
+ "not_checked_snapshot_keeps_hashes",
2302
+ "match_limit_with_not_checked_snapshot",
2303
+ ],
2304
+ )
2305
+ def test_cited_state_and_surface_laundering_mutations_fail(mutation: str) -> None:
2306
+ output = _enriched()
2307
+ if mutation == "missing_abstract_as_checked":
2308
+ entry = next(
2309
+ item
2310
+ for item in output["literature_corpus"]
2311
+ if item["citation_key"] == "fixture_missing_2026"
2312
+ )
2313
+ signal = next(
2314
+ item
2315
+ for item in entry["bibliographic_integrity_signals"]
2316
+ if item["tortured_phrase_context"]["surface"] == "cited_abstract"
2317
+ )
2318
+ signal["check_status"] = "checked"
2319
+ signal["finding"] = "not_detected"
2320
+ signal["provenance"]["checked_at"] = CHECKED_AT
2321
+ signal["evidence"][0]["evidence_type"] = "list_record"
2322
+ signal["evidence"][0]["observed_value"] = 0
2323
+ signal["tortured_phrase_context"]["reason_code"] = "CHECK_COMPLETED"
2324
+ else:
2325
+ entry = output["literature_corpus"][0]
2326
+ signal = next(
2327
+ item
2328
+ for item in entry["bibliographic_integrity_signals"]
2329
+ if item["tortured_phrase_context"]["surface"] == "cited_title"
2330
+ )
2331
+ context = signal["tortured_phrase_context"]
2332
+ if mutation == "checked_with_zero_rules":
2333
+ context["counts"]["rules_evaluated"] = 0
2334
+ elif mutation == "checked_with_unknown_segment":
2335
+ context["counts"]["unknown_segments"] = 1
2336
+ elif mutation == "match_context_crosses_surface":
2337
+ match = context["matches"][0]
2338
+ match["context"] = "cited_abstract"
2339
+ payload = {
2340
+ "artifact_sha256": context["surface_binding"]["content_sha256"],
2341
+ "snapshot_sha256": context["snapshot"]["snapshot_sha256"],
2342
+ "surface": context["surface"],
2343
+ "segment_id": match["segment_id"],
2344
+ "context": match["context"],
2345
+ "rule_id": match["pattern_id"],
2346
+ "codepoint_start": match["source_span"]["codepoint_start"],
2347
+ "codepoint_end": match["source_span"]["codepoint_end"],
2348
+ }
2349
+ match["match_id"] = "tpm-" + screening._sha256_text(
2350
+ screening._canonical_json(payload)
2351
+ )[:24]
2352
+ context["counts"]["matches_by_context"]["cited_title"] -= 1
2353
+ context["counts"]["matches_by_context"]["cited_abstract"] += 1
2354
+ elif mutation in {
2355
+ "outer_not_checked_with_loaded_snapshot",
2356
+ "not_checked_snapshot_keeps_hashes",
2357
+ }:
2358
+ signal["check_status"] = "not_checked"
2359
+ signal["finding"] = "unresolved"
2360
+ signal["provenance"]["checked_at"] = None
2361
+ signal["evidence"][0]["evidence_type"] = "degradation_record"
2362
+ signal["evidence"][0]["observed_value"] = "SNAPSHOT_NOT_PROVIDED"
2363
+ context["reason_code"] = "SNAPSHOT_NOT_PROVIDED"
2364
+ context["counts"] = screening._empty_counts()
2365
+ context["matches"] = []
2366
+ if mutation == "not_checked_snapshot_keeps_hashes":
2367
+ context["snapshot"] = screening._snapshot_binding(
2368
+ screening.snapshot_state(None, None)
2369
+ )
2370
+ context["snapshot"]["snapshot_sha256"] = "0" * 64
2371
+ context["snapshot"]["manifest_sha256"] = "1" * 64
2372
+ signal["signal_id"] = screening._signal_id(
2373
+ entry["citation_key"],
2374
+ context["surface"],
2375
+ context["snapshot"]["snapshot_sha256"],
2376
+ context["surface_binding"]["content_sha256"],
2377
+ )
2378
+ signal["provenance"]["source_name"] = (
2379
+ "tortured-phrase snapshot unavailable"
2380
+ )
2381
+ signal["provenance"]["source_version"] = None
2382
+ signal["provenance"]["source_sha256"] = "0" * 64
2383
+ else:
2384
+ signal["check_status"] = "degraded"
2385
+ signal["finding"] = "unresolved"
2386
+ signal["evidence"][0]["evidence_type"] = "degradation_record"
2387
+ signal["evidence"][0]["observed_value"] = "MATCH_RESOURCE_LIMIT"
2388
+ context["reason_code"] = "MATCH_RESOURCE_LIMIT"
2389
+ context["counts"] = screening._empty_counts()
2390
+ context["matches"] = []
2391
+ context["snapshot"] = screening._snapshot_binding(
2392
+ screening.snapshot_state(None, None)
2393
+ )
2394
+ signal["provenance"]["source_name"] = (
2395
+ "tortured-phrase snapshot unavailable"
2396
+ )
2397
+ signal["provenance"]["source_version"] = None
2398
+ signal["provenance"]["source_sha256"] = None
2399
+ with pytest.raises(screening.ScreeningError):
2400
+ screening.validate_cited_signal_binding(signal, entry)
2401
+
2402
+
2403
+ @pytest.mark.parametrize(
2404
+ "rights",
2405
+ [
2406
+ {
2407
+ "basis": "user_declared_authorized",
2408
+ "redistribution_status": "permitted",
2409
+ "reference": None,
2410
+ "user_declaration": None,
2411
+ },
2412
+ {
2413
+ "basis": "written_permission",
2414
+ "redistribution_status": "permitted",
2415
+ "reference": None,
2416
+ "user_declaration": None,
2417
+ },
2418
+ {
2419
+ "basis": "unresolved",
2420
+ "redistribution_status": "permitted",
2421
+ "reference": None,
2422
+ "user_declaration": None,
2423
+ },
2424
+ ],
2425
+ )
2426
+ def test_cited_carrier_rejects_impossible_rights_projection(rights: dict) -> None:
2427
+ output = _enriched()
2428
+ entry = output["literature_corpus"][0]
2429
+ signal = entry["bibliographic_integrity_signals"][0]
2430
+ signal["tortured_phrase_context"]["snapshot"]["rights"] = rights
2431
+ with pytest.raises(screening.ScreeningError):
2432
+ screening.validate_cited_signal_binding(signal, entry)
2433
+
2434
+
2435
+ def test_cited_carrier_rejects_missing_loaded_source_locator() -> None:
2436
+ output = _enriched()
2437
+ entry = output["literature_corpus"][0]
2438
+ signal = entry["bibliographic_integrity_signals"][0]
2439
+ signal["tortured_phrase_context"]["snapshot"]["source"]["locator"] = None
2440
+ with pytest.raises(screening.ScreeningError):
2441
+ screening.validate_cited_signal_binding(signal, entry)
2442
+
2443
+
2444
+ def test_tortured_phrase_cannot_be_deterministic_or_terminal() -> None:
2445
+ schema = screening._load_schema(screening.SIGNAL_SCHEMA_PATH)
2446
+ validator = Draft202012Validator(
2447
+ schema, format_checker=Draft202012Validator.FORMAT_CHECKER
2448
+ )
2449
+ signal = _enriched()["literature_corpus"][0]["bibliographic_integrity_signals"][0]
2450
+ assert not list(validator.iter_errors(signal))
2451
+ signal["epistemic_class"] = "deterministic_fact"
2452
+ signal["epistemic_label"] = "RESOLVER-OR-LIST-OBSERVATION"
2453
+ assert list(validator.iter_errors(signal))
2454
+ signal = _enriched()["literature_corpus"][0]["bibliographic_integrity_signals"][0]
2455
+ signal["terminal_policy"] = {
2456
+ "eligible": True,
2457
+ "owner": "citation_finalizer",
2458
+ "policy_key": "tortured_phrase",
2459
+ "current_effect": "policy_gated",
2460
+ }
2461
+ assert list(validator.iter_errors(signal))
2462
+
2463
+
2464
+ def test_report_never_copies_raw_pattern_bodies() -> None:
2465
+ report_bytes = json.dumps(_report(), ensure_ascii=False)
2466
+ snapshot = json.loads((FIXTURES / "snapshot.json").read_text(encoding="utf-8"))
2467
+ for rule in snapshot["rules"]:
2468
+ expression = json.dumps(rule["expression"], ensure_ascii=False, sort_keys=True)
2469
+ assert expression not in report_bytes
2470
+
2471
+
2472
+ def test_cli_rejects_in_place_passport_output() -> None:
2473
+ result = subprocess.run(
2474
+ [
2475
+ sys.executable,
2476
+ str(SCRIPTS / "tortured_phrase_screening.py"),
2477
+ "enrich-passport",
2478
+ "--input",
2479
+ str(FIXTURES / "corpus_input.yaml"),
2480
+ "--output",
2481
+ str(FIXTURES / "corpus_input.yaml"),
2482
+ "--snapshot",
2483
+ str(FIXTURES / "snapshot.json"),
2484
+ "--snapshot-manifest",
2485
+ str(FIXTURES / "snapshot_manifest.json"),
2486
+ "--checked-at",
2487
+ CHECKED_AT,
2488
+ "--recorded-at",
2489
+ RECORDED_AT,
2490
+ ],
2491
+ text=True,
2492
+ capture_output=True,
2493
+ check=False,
2494
+ )
2495
+ assert result.returncode == 1
2496
+ assert "refuses in-place" in result.stderr
2497
+
2498
+
2499
+ @pytest.mark.parametrize("alias", ["snapshot", "manifest", "snapshot_hardlink"])
2500
+ def test_scan_output_cannot_alias_any_named_input(
2501
+ tmp_path: Path, alias: str
2502
+ ) -> None:
2503
+ draft = tmp_path / "draft.md"
2504
+ snapshot = tmp_path / "snapshot.json"
2505
+ manifest = tmp_path / "manifest.json"
2506
+ draft.write_bytes((FIXTURES / "own_draft.md").read_bytes())
2507
+ snapshot.write_bytes((FIXTURES / "snapshot.json").read_bytes())
2508
+ manifest.write_bytes((FIXTURES / "snapshot_manifest.json").read_bytes())
2509
+ if alias == "snapshot":
2510
+ output = snapshot
2511
+ elif alias == "manifest":
2512
+ output = manifest
2513
+ else:
2514
+ output = tmp_path / "snapshot-hardlink.json"
2515
+ output.hardlink_to(snapshot)
2516
+ snapshot_before = snapshot.read_bytes()
2517
+ manifest_before = manifest.read_bytes()
2518
+ result = subprocess.run(
2519
+ [
2520
+ sys.executable,
2521
+ str(SCRIPTS / "tortured_phrase_screening.py"),
2522
+ "scan-draft",
2523
+ "--input",
2524
+ str(draft),
2525
+ "--artifact-id",
2526
+ "draft.md",
2527
+ "--format",
2528
+ "markdown",
2529
+ "--snapshot",
2530
+ str(snapshot),
2531
+ "--snapshot-manifest",
2532
+ str(manifest),
2533
+ "--checked-at",
2534
+ CHECKED_AT,
2535
+ "--recorded-at",
2536
+ RECORDED_AT,
2537
+ "--output",
2538
+ str(output),
2539
+ ],
2540
+ text=True,
2541
+ capture_output=True,
2542
+ check=False,
2543
+ )
2544
+ assert result.returncode == 1
2545
+ assert "named-input alias" in result.stderr
2546
+ assert snapshot.read_bytes() == snapshot_before
2547
+ assert manifest.read_bytes() == manifest_before
2548
+
2549
+
2550
+ def test_corpus_output_match_cap_rejects_before_write(
2551
+ monkeypatch: pytest.MonkeyPatch,
2552
+ ) -> None:
2553
+ monkeypatch.setattr(screening, "MAX_CORPUS_OUTPUT_MATCHES", 0)
2554
+ with pytest.raises(screening.MatchLimitError, match="corpus output match count"):
2555
+ _enriched()
2556
+
2557
+
2558
+ @pytest.mark.parametrize(
2559
+ ("limit_name", "document", "message"),
2560
+ [
2561
+ (
2562
+ "MAX_CORPUS_ENTRIES",
2563
+ {"literature_corpus": [{}, {}]},
2564
+ "more than 1 entries",
2565
+ ),
2566
+ (
2567
+ "MAX_CORPUS_EXISTING_SIGNALS",
2568
+ {"literature_corpus": [{"bibliographic_integrity_signals": [{}]}]},
2569
+ "exceed 0 rows",
2570
+ ),
2571
+ ],
2572
+ )
2573
+ def test_corpus_cardinality_caps_fail_before_copy_or_row_construction(
2574
+ monkeypatch: pytest.MonkeyPatch,
2575
+ limit_name: str,
2576
+ document: dict,
2577
+ message: str,
2578
+ ) -> None:
2579
+ monkeypatch.setattr(screening, limit_name, 1 if limit_name.endswith("ENTRIES") else 0)
2580
+
2581
+ def unexpected_copy(_value):
2582
+ raise AssertionError("copy must not run after an over-limit input")
2583
+
2584
+ monkeypatch.setattr(screening.copy, "deepcopy", unexpected_copy)
2585
+ with pytest.raises(screening.MatchLimitError, match=message):
2586
+ screening.enrich_passport(
2587
+ document,
2588
+ state=screening.snapshot_state(None, None),
2589
+ checked_at=CHECKED_AT,
2590
+ recorded_at=RECORDED_AT,
2591
+ )
2592
+
2593
+
2594
+ def test_deep_json_passport_fails_closed_without_traceback_or_output_mutation(
2595
+ tmp_path: Path,
2596
+ ) -> None:
2597
+ passport = tmp_path / "deep-passport.json"
2598
+ passport.write_text(
2599
+ '{"literature_corpus":[],"extension":'
2600
+ + "[" * 1000
2601
+ + "0"
2602
+ + "]" * 1000
2603
+ + "}\n",
2604
+ encoding="utf-8",
2605
+ )
2606
+ output = tmp_path / "output.json"
2607
+ output.write_bytes(b"sentinel\n")
2608
+ result = subprocess.run(
2609
+ [
2610
+ sys.executable,
2611
+ str(SCRIPTS / "tortured_phrase_screening.py"),
2612
+ "enrich-passport",
2613
+ "--input",
2614
+ str(passport),
2615
+ "--output",
2616
+ str(output),
2617
+ "--checked-at",
2618
+ CHECKED_AT,
2619
+ "--recorded-at",
2620
+ RECORDED_AT,
2621
+ ],
2622
+ text=True,
2623
+ capture_output=True,
2624
+ check=False,
2625
+ )
2626
+ assert result.returncode == 1
2627
+ assert "Traceback" not in result.stderr
2628
+ assert "strict JSON" in result.stderr or "structure exceeds" in result.stderr
2629
+ assert output.read_bytes() == b"sentinel\n"
2630
+
2631
+
2632
+ def test_deep_advisory_report_fails_closed_without_traceback(tmp_path: Path) -> None:
2633
+ report = tmp_path / "deep-report.json"
2634
+ report.write_text(
2635
+ '{"extension":' + "[" * 1000 + "0" + "]" * 1000 + "}\n",
2636
+ encoding="utf-8",
2637
+ )
2638
+ result = subprocess.run(
2639
+ [
2640
+ sys.executable,
2641
+ str(SCRIPTS / "tortured_phrase_screening.py"),
2642
+ "validate-draft",
2643
+ "--input",
2644
+ str(FIXTURES / "own_draft.md"),
2645
+ "--artifact-id",
2646
+ "own_draft.md",
2647
+ "--format",
2648
+ "markdown",
2649
+ "--snapshot",
2650
+ str(FIXTURES / "snapshot.json"),
2651
+ "--snapshot-manifest",
2652
+ str(FIXTURES / "snapshot_manifest.json"),
2653
+ "--report",
2654
+ str(report),
2655
+ ],
2656
+ text=True,
2657
+ capture_output=True,
2658
+ check=False,
2659
+ )
2660
+ assert result.returncode == 1
2661
+ assert "Traceback" not in result.stderr
2662
+ assert "strict JSON" in result.stderr or "structure exceeds" in result.stderr
2663
+
2664
+
2665
+ @pytest.mark.parametrize("cwd", [REPO_ROOT, SCRIPTS])
2666
+ def test_runtime_imports_in_package_and_direct_script_modes(cwd: Path) -> None:
2667
+ module = "scripts.tortured_phrase_screening" if cwd == REPO_ROOT else "tortured_phrase_screening"
2668
+ result = subprocess.run(
2669
+ [sys.executable, "-c", f"import {module}"],
2670
+ cwd=cwd,
2671
+ text=True,
2672
+ capture_output=True,
2673
+ check=False,
2674
+ )
2675
+ assert result.returncode == 0, result.stderr
2676
+
2677
+
2678
+ def test_package_import_cannot_bind_shadow_bibliographic_module(
2679
+ tmp_path: Path,
2680
+ ) -> None:
2681
+ (tmp_path / "bibliographic_integrity_signals.py").write_text(
2682
+ "raise RuntimeError('shadow module imported')\n", encoding="utf-8"
2683
+ )
2684
+ code = (
2685
+ "import sys; "
2686
+ f"sys.path.insert(0, {str(tmp_path)!r}); "
2687
+ "import scripts.tortured_phrase_screening as module; "
2688
+ "print(module._validate_existing_phrase_projection.__module__); "
2689
+ "print(module._validate_existing_phrase_projection.__code__.co_filename)"
2690
+ )
2691
+ result = subprocess.run(
2692
+ [sys.executable, "-c", code],
2693
+ cwd=REPO_ROOT,
2694
+ text=True,
2695
+ capture_output=True,
2696
+ check=False,
2697
+ )
2698
+ assert result.returncode == 0, result.stderr
2699
+ lines = result.stdout.splitlines()
2700
+ assert lines[0] == "scripts.bibliographic_integrity_signals"
2701
+ assert Path(lines[1]).resolve() == (
2702
+ SCRIPTS / "bibliographic_integrity_signals.py"
2703
+ ).resolve()
2704
+
2705
+
2706
+ def test_yaml_aliases_fail_closed_without_output(tmp_path: Path) -> None:
2707
+ passport = tmp_path / "aliased.yaml"
2708
+ passport.write_text(
2709
+ "shared: &rows []\nliterature_corpus: *rows\n",
2710
+ encoding="utf-8",
2711
+ )
2712
+ output = tmp_path / "output.yaml"
2713
+ result = subprocess.run(
2714
+ [
2715
+ sys.executable,
2716
+ str(SCRIPTS / "tortured_phrase_screening.py"),
2717
+ "enrich-passport",
2718
+ "--input",
2719
+ str(passport),
2720
+ "--output",
2721
+ str(output),
2722
+ "--snapshot",
2723
+ str(FIXTURES / "snapshot.json"),
2724
+ "--snapshot-manifest",
2725
+ str(FIXTURES / "snapshot_manifest.json"),
2726
+ "--checked-at",
2727
+ CHECKED_AT,
2728
+ "--recorded-at",
2729
+ RECORDED_AT,
2730
+ ],
2731
+ text=True,
2732
+ capture_output=True,
2733
+ check=False,
2734
+ )
2735
+ assert result.returncode == 1
2736
+ assert "YAML aliases are forbidden" in result.stderr
2737
+ assert not output.exists()
2738
+
2739
+
2740
+ def test_cli_has_no_all_or_page_traversal() -> None:
2741
+ result = subprocess.run(
2742
+ [
2743
+ sys.executable,
2744
+ str(SCRIPTS / "tortured_phrase_screening.py"),
2745
+ "render-draft",
2746
+ "--help",
2747
+ ],
2748
+ text=True,
2749
+ capture_output=True,
2750
+ check=False,
2751
+ )
2752
+ assert result.returncode == 0
2753
+ assert "--all" not in result.stdout
2754
+ assert "--page" not in result.stdout
2755
+
2756
+
2757
+ def test_invalid_snapshot_cli_writes_degraded_artifact_and_returns_one(tmp_path: Path) -> None:
2758
+ bad = tmp_path / "bad.json"
2759
+ bad.write_text("{}\n", encoding="utf-8")
2760
+ output = tmp_path / "report.json"
2761
+ result = subprocess.run(
2762
+ [
2763
+ sys.executable,
2764
+ str(SCRIPTS / "tortured_phrase_screening.py"),
2765
+ "scan-draft",
2766
+ "--input",
2767
+ str(FIXTURES / "own_draft.md"),
2768
+ "--artifact-id",
2769
+ "own_draft.md",
2770
+ "--format",
2771
+ "markdown",
2772
+ "--snapshot",
2773
+ str(bad),
2774
+ "--snapshot-manifest",
2775
+ str(FIXTURES / "snapshot_manifest.json"),
2776
+ "--checked-at",
2777
+ CHECKED_AT,
2778
+ "--recorded-at",
2779
+ RECORDED_AT,
2780
+ "--output",
2781
+ str(output),
2782
+ ],
2783
+ text=True,
2784
+ capture_output=True,
2785
+ check=False,
2786
+ )
2787
+ assert result.returncode == 1
2788
+ report = json.loads(output.read_text(encoding="utf-8"))
2789
+ assert report["check_status"] == "degraded"
2790
+ assert report["finding"] == "unresolved"
2791
+
2792
+
2793
+ @pytest.mark.parametrize(
2794
+ ("text", "reason"),
2795
+ [
2796
+ ("", "DOCUMENT_EMPTY"),
2797
+ ("`unclosed luminous turnip", "DOCUMENT_PARSE_DEGRADED"),
2798
+ ],
2799
+ )
2800
+ def test_loaded_snapshot_scan_returns_one_for_degraded_artifact(
2801
+ tmp_path: Path, text: str, reason: str
2802
+ ) -> None:
2803
+ draft = tmp_path / "draft.md"
2804
+ draft.write_text(text, encoding="utf-8")
2805
+ output = tmp_path / "report.json"
2806
+ result = subprocess.run(
2807
+ [
2808
+ sys.executable,
2809
+ str(SCRIPTS / "tortured_phrase_screening.py"),
2810
+ "scan-draft",
2811
+ "--input",
2812
+ str(draft),
2813
+ "--artifact-id",
2814
+ "draft.md",
2815
+ "--format",
2816
+ "markdown",
2817
+ "--snapshot",
2818
+ str(FIXTURES / "snapshot.json"),
2819
+ "--snapshot-manifest",
2820
+ str(FIXTURES / "snapshot_manifest.json"),
2821
+ "--checked-at",
2822
+ CHECKED_AT,
2823
+ "--recorded-at",
2824
+ RECORDED_AT,
2825
+ "--output",
2826
+ str(output),
2827
+ ],
2828
+ text=True,
2829
+ capture_output=True,
2830
+ check=False,
2831
+ )
2832
+ assert result.returncode == 1
2833
+ report = json.loads(output.read_text(encoding="utf-8"))
2834
+ assert report["check_status"] == "degraded"
2835
+ assert report["reason_code"] == reason
2836
+
2837
+
2838
+ def test_loaded_snapshot_resource_degradation_returns_one_after_write(
2839
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
2840
+ ) -> None:
2841
+ draft = tmp_path / "draft.md"
2842
+ draft.write_text("alpha\n\nbeta", encoding="utf-8")
2843
+ output = tmp_path / "report.json"
2844
+ monkeypatch.setattr(screening, "MAX_SEGMENTS", 1)
2845
+ result = screening.main(
2846
+ [
2847
+ "scan-draft",
2848
+ "--input",
2849
+ str(draft),
2850
+ "--artifact-id",
2851
+ "draft.md",
2852
+ "--format",
2853
+ "markdown",
2854
+ "--snapshot",
2855
+ str(FIXTURES / "snapshot.json"),
2856
+ "--snapshot-manifest",
2857
+ str(FIXTURES / "snapshot_manifest.json"),
2858
+ "--checked-at",
2859
+ CHECKED_AT,
2860
+ "--recorded-at",
2861
+ RECORDED_AT,
2862
+ "--output",
2863
+ str(output),
2864
+ ]
2865
+ )
2866
+ assert result == 1
2867
+ report = json.loads(output.read_text(encoding="utf-8"))
2868
+ assert report["check_status"] == "degraded"
2869
+ assert report["reason_code"] == "MATCH_RESOURCE_LIMIT"
2870
+
2871
+
2872
+ def test_loaded_snapshot_corpus_degradation_returns_one_after_write(
2873
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
2874
+ ) -> None:
2875
+ passport = tmp_path / "corpus.yaml"
2876
+ passport.write_bytes((FIXTURES / "corpus_input.yaml").read_bytes())
2877
+ output = tmp_path / "output.yaml"
2878
+ monkeypatch.setattr(screening, "MAX_MATCH_WORK_UNITS", 0)
2879
+ result = screening.main(
2880
+ [
2881
+ "enrich-passport",
2882
+ "--input",
2883
+ str(passport),
2884
+ "--output",
2885
+ str(output),
2886
+ "--snapshot",
2887
+ str(FIXTURES / "snapshot.json"),
2888
+ "--snapshot-manifest",
2889
+ str(FIXTURES / "snapshot_manifest.json"),
2890
+ "--checked-at",
2891
+ CHECKED_AT,
2892
+ "--recorded-at",
2893
+ RECORDED_AT,
2894
+ ]
2895
+ )
2896
+ assert result == 1
2897
+ enriched, _kind = screening._load_passport(output)
2898
+ assert any(
2899
+ signal.get("check_status") == "degraded"
2900
+ for entry in enriched["literature_corpus"]
2901
+ for signal in entry["bibliographic_integrity_signals"]
2902
+ if signal.get("schema_version") == screening.SIGNAL_VERSION
2903
+ )
2904
+
2905
+
2906
+ def test_explicit_not_checked_scan_remains_successful(tmp_path: Path) -> None:
2907
+ output = tmp_path / "report.json"
2908
+ result = screening.main(
2909
+ [
2910
+ "scan-draft",
2911
+ "--input",
2912
+ str(FIXTURES / "own_draft.md"),
2913
+ "--artifact-id",
2914
+ "own_draft.md",
2915
+ "--format",
2916
+ "markdown",
2917
+ "--checked-at",
2918
+ CHECKED_AT,
2919
+ "--recorded-at",
2920
+ RECORDED_AT,
2921
+ "--output",
2922
+ str(output),
2923
+ ]
2924
+ )
2925
+ assert result == 0
2926
+ assert json.loads(output.read_text(encoding="utf-8"))["check_status"] == (
2927
+ "not_checked"
2928
+ )
2929
+
2930
+
2931
+ def test_failed_scan_does_not_overwrite_input_or_partial_output(tmp_path: Path) -> None:
2932
+ draft = tmp_path / "draft.md"
2933
+ draft.write_text("luminous turnip", encoding="utf-8")
2934
+ before = draft.read_bytes()
2935
+ result = subprocess.run(
2936
+ [
2937
+ sys.executable,
2938
+ str(SCRIPTS / "tortured_phrase_screening.py"),
2939
+ "scan-draft",
2940
+ "--input",
2941
+ str(draft),
2942
+ "--artifact-id",
2943
+ "draft.md",
2944
+ "--format",
2945
+ "markdown",
2946
+ "--checked-at",
2947
+ "bad-time",
2948
+ "--recorded-at",
2949
+ RECORDED_AT,
2950
+ "--output",
2951
+ str(tmp_path / "report.json"),
2952
+ ],
2953
+ text=True,
2954
+ capture_output=True,
2955
+ check=False,
2956
+ )
2957
+ assert result.returncode == 1
2958
+ assert draft.read_bytes() == before
2959
+ assert not (tmp_path / "report.json").exists()