psyclaw 0.27.22 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1048) hide show
  1. package/README.md +11 -5
  2. package/dist/apps/panel/index.html +2 -2
  3. package/dist/src/adapters/pi/extension.js +225 -92
  4. package/dist/src/adapters/pi/extension.js.map +1 -1
  5. package/dist/src/agents/import.js +44 -32
  6. package/dist/src/agents/import.js.map +1 -1
  7. package/dist/src/analysis/hooks.d.ts +1 -0
  8. package/dist/src/analysis/hooks.js +20 -1
  9. package/dist/src/analysis/hooks.js.map +1 -1
  10. package/dist/src/ars/bridge.d.ts +20 -0
  11. package/dist/src/ars/bridge.js +132 -0
  12. package/dist/src/ars/bridge.js.map +1 -0
  13. package/dist/src/ars/contracts.d.ts +55 -0
  14. package/dist/src/ars/contracts.js +2 -0
  15. package/dist/src/ars/contracts.js.map +1 -0
  16. package/dist/src/ars/panel-plan.d.ts +4 -0
  17. package/dist/src/ars/panel-plan.js +11 -0
  18. package/dist/src/ars/panel-plan.js.map +1 -0
  19. package/dist/src/ars/pi-panel-executor.d.ts +30 -0
  20. package/dist/src/ars/pi-panel-executor.js +140 -0
  21. package/dist/src/ars/pi-panel-executor.js.map +1 -0
  22. package/dist/src/ars/profile.d.ts +36 -0
  23. package/dist/src/ars/profile.js +119 -0
  24. package/dist/src/ars/profile.js.map +1 -0
  25. package/dist/src/ars/re-review.d.ts +22 -0
  26. package/dist/src/ars/re-review.js +172 -0
  27. package/dist/src/ars/re-review.js.map +1 -0
  28. package/dist/src/branding.d.ts +2 -4
  29. package/dist/src/branding.js +3 -5
  30. package/dist/src/branding.js.map +1 -1
  31. package/dist/src/bundled-tools.d.ts +3 -0
  32. package/dist/src/bundled-tools.js +21 -0
  33. package/dist/src/bundled-tools.js.map +1 -0
  34. package/dist/src/chat.js +6 -5
  35. package/dist/src/chat.js.map +1 -1
  36. package/dist/src/creation/contracts.d.ts +38 -0
  37. package/dist/src/creation/contracts.js +2 -0
  38. package/dist/src/creation/contracts.js.map +1 -0
  39. package/dist/src/creation/service.d.ts +7 -0
  40. package/dist/src/creation/service.js +196 -0
  41. package/dist/src/creation/service.js.map +1 -0
  42. package/dist/src/index.d.ts +10 -0
  43. package/dist/src/index.js +10 -0
  44. package/dist/src/index.js.map +1 -1
  45. package/dist/src/install/installer.js +32 -10
  46. package/dist/src/install/installer.js.map +1 -1
  47. package/dist/src/orchestration/personas.d.ts +15 -0
  48. package/dist/src/orchestration/personas.js +51 -0
  49. package/dist/src/orchestration/personas.js.map +1 -0
  50. package/dist/src/orchestration/pi-executor.d.ts +1 -0
  51. package/dist/src/orchestration/pi-executor.js +1 -1
  52. package/dist/src/orchestration/pi-executor.js.map +1 -1
  53. package/dist/src/panel/extension.js +29 -8
  54. package/dist/src/panel/extension.js.map +1 -1
  55. package/dist/src/panel/server.js +14 -5
  56. package/dist/src/panel/server.js.map +1 -1
  57. package/dist/src/project/paths.d.ts +3 -0
  58. package/dist/src/project/paths.js +7 -0
  59. package/dist/src/project/paths.js.map +1 -1
  60. package/dist/src/rules/user-rules.d.ts +8 -0
  61. package/dist/src/rules/user-rules.js +36 -0
  62. package/dist/src/rules/user-rules.js.map +1 -0
  63. package/dist/src/skills/contracts.d.ts +4 -4
  64. package/dist/src/skills/recommended.js +1 -1
  65. package/dist/src/skills/registry.js +49 -28
  66. package/dist/src/skills/registry.js.map +1 -1
  67. package/dist/src/style/cli-ui.js +0 -1
  68. package/dist/src/style/cli-ui.js.map +1 -1
  69. package/package.json +14 -3
  70. package/scripts/rebrand-pi.mjs +6 -0
  71. package/skills/recommended/catalog.json +2 -11
  72. package/vendor/ars/.claude/CLAUDE.md +371 -0
  73. package/vendor/ars/.command-invariants.toml +24 -0
  74. package/vendor/ars/CITATION.cff +35 -0
  75. package/vendor/ars/LICENSE +417 -0
  76. package/vendor/ars/MODE_REGISTRY.md +76 -0
  77. package/vendor/ars/NOTICE.md +26 -0
  78. package/vendor/ars/POSITIONING.md +99 -0
  79. package/vendor/ars/PSYCLAW_SOURCE.json +10 -0
  80. package/vendor/ars/README.md +751 -0
  81. package/vendor/ars/SECURITY.md +52 -0
  82. package/vendor/ars/THIRD_PARTY.md +70 -0
  83. package/vendor/ars/academic-paper/SKILL.md +542 -0
  84. package/vendor/ars/academic-paper/agents/abstract_bilingual_agent.md +171 -0
  85. package/vendor/ars/academic-paper/agents/argument_builder_agent.md +276 -0
  86. package/vendor/ars/academic-paper/agents/citation_compliance_agent.md +422 -0
  87. package/vendor/ars/academic-paper/agents/draft_writer_agent.md +656 -0
  88. package/vendor/ars/academic-paper/agents/formatter_agent.md +999 -0
  89. package/vendor/ars/academic-paper/agents/intake_agent.md +393 -0
  90. package/vendor/ars/academic-paper/agents/literature_strategist_agent.md +626 -0
  91. package/vendor/ars/academic-paper/agents/peer_reviewer_agent.md +516 -0
  92. package/vendor/ars/academic-paper/agents/revision_coach_agent.md +334 -0
  93. package/vendor/ars/academic-paper/agents/socratic_mentor_agent.md +527 -0
  94. package/vendor/ars/academic-paper/agents/structure_architect_agent.md +401 -0
  95. package/vendor/ars/academic-paper/agents/visualization_agent.md +441 -0
  96. package/vendor/ars/academic-paper/examples/chinese_paper_example.md +278 -0
  97. package/vendor/ars/academic-paper/examples/clinical_citation_verification_checklist.md +95 -0
  98. package/vendor/ars/academic-paper/examples/clinical_epistemic_status_example.md +100 -0
  99. package/vendor/ars/academic-paper/examples/commitment_ledger_example.md +147 -0
  100. package/vendor/ars/academic-paper/examples/imrad_hei_example.md +234 -0
  101. package/vendor/ars/academic-paper/examples/literature_review_example.md +260 -0
  102. package/vendor/ars/academic-paper/examples/plan_mode_guided_writing.md +600 -0
  103. package/vendor/ars/academic-paper/examples/revision_mode_example.md +344 -0
  104. package/vendor/ars/academic-paper/examples/revision_recovery_example.md +506 -0
  105. package/vendor/ars/academic-paper/examples/version_family_reconciliation_example.md +89 -0
  106. package/vendor/ars/academic-paper/references/abstract_writing_guide.md +169 -0
  107. package/vendor/ars/academic-paper/references/academic_writing_style.md +188 -0
  108. package/vendor/ars/academic-paper/references/anti_leakage_protocol.md +83 -0
  109. package/vendor/ars/academic-paper/references/apa7_chinese_citation_guide.md +364 -0
  110. package/vendor/ars/academic-paper/references/apa7_extended_guide.md +198 -0
  111. package/vendor/ars/academic-paper/references/changelog.md +11 -0
  112. package/vendor/ars/academic-paper/references/citation_format_switcher.md +228 -0
  113. package/vendor/ars/academic-paper/references/committee_correspondence_protocol.md +158 -0
  114. package/vendor/ars/academic-paper/references/credit_authorship_guide.md +308 -0
  115. package/vendor/ars/academic-paper/references/disclosure_mode_protocol.md +478 -0
  116. package/vendor/ars/academic-paper/references/domain_evidence_profiles.md +38 -0
  117. package/vendor/ars/academic-paper/references/failure_paths.md +349 -0
  118. package/vendor/ars/academic-paper/references/funding_statement_guide.md +319 -0
  119. package/vendor/ars/academic-paper/references/hei_domain_glossary.md +169 -0
  120. package/vendor/ars/academic-paper/references/intro_title_rhetoric_guide.md +114 -0
  121. package/vendor/ars/academic-paper/references/journal_submission_guide.md +249 -0
  122. package/vendor/ars/academic-paper/references/latex_template_reference.md +378 -0
  123. package/vendor/ars/academic-paper/references/mode_selection_guide.md +378 -0
  124. package/vendor/ars/academic-paper/references/paper_structure_patterns.md +330 -0
  125. package/vendor/ars/academic-paper/references/plan_mode_protocol.md +112 -0
  126. package/vendor/ars/academic-paper/references/policy_anchor_disclosure_protocol.md +200 -0
  127. package/vendor/ars/academic-paper/references/policy_anchor_table.md +157 -0
  128. package/vendor/ars/academic-paper/references/revision_patch_protocol.md +173 -0
  129. package/vendor/ars/academic-paper/references/statistical_visualization_standards.md +750 -0
  130. package/vendor/ars/academic-paper/references/venue_disclosure_policies.md +259 -0
  131. package/vendor/ars/academic-paper/references/vlm_figure_verification.md +126 -0
  132. package/vendor/ars/academic-paper/references/workflow_phase_details.md +135 -0
  133. package/vendor/ars/academic-paper/references/writing_judgment_framework.md +59 -0
  134. package/vendor/ars/academic-paper/references/writing_quality_check.md +173 -0
  135. package/vendor/ars/academic-paper/templates/bilingual_abstract_template.md +78 -0
  136. package/vendor/ars/academic-paper/templates/case_study_template.md +129 -0
  137. package/vendor/ars/academic-paper/templates/conference_paper_template.md +108 -0
  138. package/vendor/ars/academic-paper/templates/credit_statement_template.md +132 -0
  139. package/vendor/ars/academic-paper/templates/funding_statement_template.md +290 -0
  140. package/vendor/ars/academic-paper/templates/imrad_template.md +183 -0
  141. package/vendor/ars/academic-paper/templates/latex_article_template.tex +199 -0
  142. package/vendor/ars/academic-paper/templates/literature_review_template.md +135 -0
  143. package/vendor/ars/academic-paper/templates/policy_brief_template.md +139 -0
  144. package/vendor/ars/academic-paper/templates/revision_tracking_template.md +199 -0
  145. package/vendor/ars/academic-paper/templates/theoretical_paper_template.md +119 -0
  146. package/vendor/ars/academic-paper-reviewer/SKILL.md +491 -0
  147. package/vendor/ars/academic-paper-reviewer/agents/devils_advocate_reviewer_agent.md +443 -0
  148. package/vendor/ars/academic-paper-reviewer/agents/domain_reviewer_agent.md +412 -0
  149. package/vendor/ars/academic-paper-reviewer/agents/editorial_synthesizer_agent.md +478 -0
  150. package/vendor/ars/academic-paper-reviewer/agents/eic_agent.md +339 -0
  151. package/vendor/ars/academic-paper-reviewer/agents/field_analyst_agent.md +221 -0
  152. package/vendor/ars/academic-paper-reviewer/agents/methodology_reviewer_agent.md +449 -0
  153. package/vendor/ars/academic-paper-reviewer/agents/perspective_reviewer_agent.md +427 -0
  154. package/vendor/ars/academic-paper-reviewer/examples/hei_paper_review_example.md +391 -0
  155. package/vendor/ars/academic-paper-reviewer/examples/interdisciplinary_review_example.md +299 -0
  156. package/vendor/ars/academic-paper-reviewer/examples/subclaim_decomposition_example.md +80 -0
  157. package/vendor/ars/academic-paper-reviewer/references/calibration_mode_protocol.md +256 -0
  158. package/vendor/ars/academic-paper-reviewer/references/changelog.md +10 -0
  159. package/vendor/ars/academic-paper-reviewer/references/editorial_decision_standards.md +236 -0
  160. package/vendor/ars/academic-paper-reviewer/references/guided_mode_protocol.md +34 -0
  161. package/vendor/ars/academic-paper-reviewer/references/integration_guide.md +15 -0
  162. package/vendor/ars/academic-paper-reviewer/references/quality_rubrics.md +84 -0
  163. package/vendor/ars/academic-paper-reviewer/references/re_review_mode_protocol.md +340 -0
  164. package/vendor/ars/academic-paper-reviewer/references/review_criteria_framework.md +98 -0
  165. package/vendor/ars/academic-paper-reviewer/references/review_panel_provenance_protocol.md +197 -0
  166. package/vendor/ars/academic-paper-reviewer/references/review_quality_thinking.md +58 -0
  167. package/vendor/ars/academic-paper-reviewer/references/reviewer_sprint_prompt_source.md +324 -0
  168. package/vendor/ars/academic-paper-reviewer/references/sprint_contract_protocol.md +296 -0
  169. package/vendor/ars/academic-paper-reviewer/references/statistical_reporting_standards.md +505 -0
  170. package/vendor/ars/academic-paper-reviewer/references/top_journals_by_field.md +206 -0
  171. package/vendor/ars/academic-paper-reviewer/templates/editorial_decision_template.md +235 -0
  172. package/vendor/ars/academic-paper-reviewer/templates/peer_review_report_template.md +305 -0
  173. package/vendor/ars/academic-paper-reviewer/templates/revision_response_template.md +248 -0
  174. package/vendor/ars/academic-pipeline/SKILL.md +736 -0
  175. package/vendor/ars/academic-pipeline/agents/claim_ref_alignment_audit_agent.md +382 -0
  176. package/vendor/ars/academic-pipeline/agents/collaboration_depth_agent.md +164 -0
  177. package/vendor/ars/academic-pipeline/agents/integrity_verification_agent.md +870 -0
  178. package/vendor/ars/academic-pipeline/agents/pipeline_orchestrator_agent.md +1379 -0
  179. package/vendor/ars/academic-pipeline/agents/state_tracker_agent.md +622 -0
  180. package/vendor/ars/academic-pipeline/examples/full_pipeline_example.md +482 -0
  181. package/vendor/ars/academic-pipeline/examples/integrity_failure_recovery.md +389 -0
  182. package/vendor/ars/academic-pipeline/examples/mid_entry_example.md +414 -0
  183. package/vendor/ars/academic-pipeline/references/adapters/.gitkeep +0 -0
  184. package/vendor/ars/academic-pipeline/references/adapters/overview.md +153 -0
  185. package/vendor/ars/academic-pipeline/references/ai_research_failure_modes.md +185 -0
  186. package/vendor/ars/academic-pipeline/references/changelog.md +14 -0
  187. package/vendor/ars/academic-pipeline/references/claim_audit_calibration_protocol.md +175 -0
  188. package/vendor/ars/academic-pipeline/references/claim_verification_protocol.md +282 -0
  189. package/vendor/ars/academic-pipeline/references/external_review_protocol.md +131 -0
  190. package/vendor/ars/academic-pipeline/references/integrity_review_protocol.md +110 -0
  191. package/vendor/ars/academic-pipeline/references/literature_corpus_consumers.md +193 -0
  192. package/vendor/ars/academic-pipeline/references/mode_advisor.md +135 -0
  193. package/vendor/ars/academic-pipeline/references/passport_as_reset_boundary.md +132 -0
  194. package/vendor/ars/academic-pipeline/references/pipeline_state_machine.md +405 -0
  195. package/vendor/ars/academic-pipeline/references/plagiarism_detection_protocol.md +239 -0
  196. package/vendor/ars/academic-pipeline/references/process_summary_protocol.md +209 -0
  197. package/vendor/ars/academic-pipeline/references/progress_dashboard_template.md +38 -0
  198. package/vendor/ars/academic-pipeline/references/reinforcement_content.md +15 -0
  199. package/vendor/ars/academic-pipeline/references/reproducibility_audit.md +55 -0
  200. package/vendor/ars/academic-pipeline/references/score_trajectory_protocol.md +78 -0
  201. package/vendor/ars/academic-pipeline/references/team_collaboration_protocol.md +261 -0
  202. package/vendor/ars/academic-pipeline/references/two_stage_review_protocol.md +27 -0
  203. package/vendor/ars/academic-pipeline/templates/pipeline_status_template.md +146 -0
  204. package/vendor/ars/agents/report_compiler_agent.md +341 -0
  205. package/vendor/ars/agents/research_architect_agent.md +298 -0
  206. package/vendor/ars/agents/synthesis_agent.md +356 -0
  207. package/vendor/ars/commands/ars-3w.md +10 -0
  208. package/vendor/ars/commands/ars-abstract.md +10 -0
  209. package/vendor/ars/commands/ars-cache-invalidate.md +20 -0
  210. package/vendor/ars/commands/ars-citation-check.md +10 -0
  211. package/vendor/ars/commands/ars-disclosure.md +10 -0
  212. package/vendor/ars/commands/ars-format-convert.md +10 -0
  213. package/vendor/ars/commands/ars-full.md +9 -0
  214. package/vendor/ars/commands/ars-lit-review.md +12 -0
  215. package/vendor/ars/commands/ars-mark-read.md +18 -0
  216. package/vendor/ars/commands/ars-outline.md +10 -0
  217. package/vendor/ars/commands/ars-plan.md +10 -0
  218. package/vendor/ars/commands/ars-rebuttal-audit.md +12 -0
  219. package/vendor/ars/commands/ars-reviewer.md +9 -0
  220. package/vendor/ars/commands/ars-revision-coach.md +9 -0
  221. package/vendor/ars/commands/ars-revision.md +10 -0
  222. package/vendor/ars/commands/ars-unmark-read.md +16 -0
  223. package/vendor/ars/deep-research/SKILL.md +600 -0
  224. package/vendor/ars/deep-research/agents/bibliography_agent.md +473 -0
  225. package/vendor/ars/deep-research/agents/devils_advocate_agent.md +192 -0
  226. package/vendor/ars/deep-research/agents/editor_in_chief_agent.md +167 -0
  227. package/vendor/ars/deep-research/agents/ethics_review_agent.md +267 -0
  228. package/vendor/ars/deep-research/agents/meta_analysis_agent.md +325 -0
  229. package/vendor/ars/deep-research/agents/monitoring_agent.md +209 -0
  230. package/vendor/ars/deep-research/agents/report_compiler_agent.md +341 -0
  231. package/vendor/ars/deep-research/agents/research_architect_agent.md +298 -0
  232. package/vendor/ars/deep-research/agents/research_question_agent.md +216 -0
  233. package/vendor/ars/deep-research/agents/risk_of_bias_agent.md +231 -0
  234. package/vendor/ars/deep-research/agents/socratic_mentor_agent.md +764 -0
  235. package/vendor/ars/deep-research/agents/source_verification_agent.md +219 -0
  236. package/vendor/ars/deep-research/agents/synthesis_agent.md +356 -0
  237. package/vendor/ars/deep-research/agents/timeline_extraction_agent.md +99 -0
  238. package/vendor/ars/deep-research/examples/exploratory_research.md +157 -0
  239. package/vendor/ars/deep-research/examples/fact_check_mode.md +173 -0
  240. package/vendor/ars/deep-research/examples/handoff_to_paper.md +318 -0
  241. package/vendor/ars/deep-research/examples/idea_diversity_coverage_gap_advisory.md +64 -0
  242. package/vendor/ars/deep-research/examples/policy_analysis.md +161 -0
  243. package/vendor/ars/deep-research/examples/review_mode.md +253 -0
  244. package/vendor/ars/deep-research/examples/socratic_guided_research.md +331 -0
  245. package/vendor/ars/deep-research/examples/systematic_review.md +133 -0
  246. package/vendor/ars/deep-research/references/apa7_style_guide.md +162 -0
  247. package/vendor/ars/deep-research/references/argumentation_reasoning_framework.md +68 -0
  248. package/vendor/ars/deep-research/references/arxiv_api_protocol.md +76 -0
  249. package/vendor/ars/deep-research/references/changelog.md +22 -0
  250. package/vendor/ars/deep-research/references/chinese_literature_api_protocol.md +317 -0
  251. package/vendor/ars/deep-research/references/cross_agent_quality_definitions.md +14 -0
  252. package/vendor/ars/deep-research/references/crossref_api_protocol.md +84 -0
  253. package/vendor/ars/deep-research/references/equator_reporting_guidelines.md +482 -0
  254. package/vendor/ars/deep-research/references/ethics_checklist.md +282 -0
  255. package/vendor/ars/deep-research/references/failure_paths.md +355 -0
  256. package/vendor/ars/deep-research/references/interdisciplinary_bridges.md +292 -0
  257. package/vendor/ars/deep-research/references/irb_decision_tree.md +315 -0
  258. package/vendor/ars/deep-research/references/literature_monitoring_strategies.md +263 -0
  259. package/vendor/ars/deep-research/references/logical_fallacies.md +192 -0
  260. package/vendor/ars/deep-research/references/methodology_patterns.md +462 -0
  261. package/vendor/ars/deep-research/references/mode_selection_guide.md +331 -0
  262. package/vendor/ars/deep-research/references/openalex_api_protocol.md +82 -0
  263. package/vendor/ars/deep-research/references/preregistration_guide.md +324 -0
  264. package/vendor/ars/deep-research/references/semantic_scholar_api_protocol.md +107 -0
  265. package/vendor/ars/deep-research/references/socratic_mode_protocol.md +99 -0
  266. package/vendor/ars/deep-research/references/socratic_questioning_framework.md +232 -0
  267. package/vendor/ars/deep-research/references/source_quality_hierarchy.md +188 -0
  268. package/vendor/ars/deep-research/references/systematic_review_protocol.md +95 -0
  269. package/vendor/ars/deep-research/references/systematic_review_toolkit.md +353 -0
  270. package/vendor/ars/deep-research/templates/evidence_assessment_template.md +127 -0
  271. package/vendor/ars/deep-research/templates/literature_matrix_template.md +85 -0
  272. package/vendor/ars/deep-research/templates/preregistration_template.md +318 -0
  273. package/vendor/ars/deep-research/templates/prisma_protocol_template.md +248 -0
  274. package/vendor/ars/deep-research/templates/prisma_report_template.md +415 -0
  275. package/vendor/ars/deep-research/templates/research_brief_template.md +93 -0
  276. package/vendor/ars/package.json +24 -0
  277. package/vendor/ars/pi/README.md +161 -0
  278. package/vendor/ars/pi/package.json +26 -0
  279. package/vendor/ars/pi/wrapper.js +193 -0
  280. package/vendor/ars/pi/wrapper.test.mjs +201 -0
  281. package/vendor/ars/pyproject.toml +2 -0
  282. package/vendor/ars/requirements-pdf-content-classifier.txt +5 -0
  283. package/vendor/ars/scripts/_block_parser.py +396 -0
  284. package/vendor/ars/scripts/_ci_pytest_manifest.toml +661 -0
  285. package/vendor/ars/scripts/_claim_audit_constants.py +268 -0
  286. package/vendor/ars/scripts/_e4_evidence.py +110 -0
  287. package/vendor/ars/scripts/_eval_threshold_gate.py +73 -0
  288. package/vendor/ars/scripts/_markdown_lint_util.py +224 -0
  289. package/vendor/ars/scripts/_next_verified_at_ms.py +176 -0
  290. package/vendor/ars/scripts/_passport_yaml.py +53 -0
  291. package/vendor/ars/scripts/_skill_lint.py +254 -0
  292. package/vendor/ars/scripts/_text_similarity.py +141 -0
  293. package/vendor/ars/scripts/adapters/README.md +89 -0
  294. package/vendor/ars/scripts/adapters/_common.py +209 -0
  295. package/vendor/ars/scripts/adapters/examples/folder_scan/expected_passport.yaml +25 -0
  296. package/vendor/ars/scripts/adapters/examples/folder_scan/expected_rejection_log.yaml +18 -0
  297. package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/Chen2024_AIAssessment.pdf +0 -0
  298. package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/Wang_2023_formative_feedback.pdf +0 -0
  299. package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/paper1.pdf +0 -0
  300. package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture//344/270/255/346/226/207/346/252/224/345/220/215_2024.pdf +0 -0
  301. package/vendor/ars/scripts/adapters/examples/obsidian/expected_passport.yaml +40 -0
  302. package/vendor/ars/scripts/adapters/examples/obsidian/expected_rejection_log.yaml +13 -0
  303. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/.gitkeep +0 -0
  304. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/.obsidian/app.json +1 -0
  305. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/_templates/tmpl.md +7 -0
  306. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/chen2024ai.md +13 -0
  307. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/invalid.md +1 -0
  308. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/wang2023formative.md +10 -0
  309. package/vendor/ars/scripts/adapters/examples/zotero/expected_passport.yaml +34 -0
  310. package/vendor/ars/scripts/adapters/examples/zotero/expected_rejection_log.yaml +34 -0
  311. package/vendor/ars/scripts/adapters/examples/zotero/input_fixture/.gitkeep +0 -0
  312. package/vendor/ars/scripts/adapters/examples/zotero/input_fixture/export.json +43 -0
  313. package/vendor/ars/scripts/adapters/folder_scan.py +214 -0
  314. package/vendor/ars/scripts/adapters/obsidian.py +336 -0
  315. package/vendor/ars/scripts/adapters/tests/.gitkeep +0 -0
  316. package/vendor/ars/scripts/adapters/tests/conftest.py +77 -0
  317. package/vendor/ars/scripts/adapters/tests/test_check_corpus_consumer_protocol.py +632 -0
  318. package/vendor/ars/scripts/adapters/tests/test_check_literature_corpus_schema.py +631 -0
  319. package/vendor/ars/scripts/adapters/tests/test_common.py +365 -0
  320. package/vendor/ars/scripts/adapters/tests/test_conftest.py +96 -0
  321. package/vendor/ars/scripts/adapters/tests/test_folder_scan.py +260 -0
  322. package/vendor/ars/scripts/adapters/tests/test_literature_corpus_entry_schema.py +745 -0
  323. package/vendor/ars/scripts/adapters/tests/test_obsidian.py +357 -0
  324. package/vendor/ars/scripts/adapters/tests/test_rejection_log_schema.py +271 -0
  325. package/vendor/ars/scripts/adapters/tests/test_sync_adapter_docs.py +88 -0
  326. package/vendor/ars/scripts/adapters/tests/test_zotero.py +454 -0
  327. package/vendor/ars/scripts/adapters/zotero.py +318 -0
  328. package/vendor/ars/scripts/adjudication_activity.py +1592 -0
  329. package/vendor/ars/scripts/announce-ars-loaded.sh +144 -0
  330. package/vendor/ars/scripts/ars_anchorize_draft.py +170 -0
  331. package/vendor/ars/scripts/ars_apply_revision_patch.py +912 -0
  332. package/vendor/ars/scripts/ars_cache_invalidate.py +40 -0
  333. package/vendor/ars/scripts/ars_mark_read.py +521 -0
  334. package/vendor/ars/scripts/ars_phase_scope_manifest.json +33 -0
  335. package/vendor/ars/scripts/ars_update_check.sh +215 -0
  336. package/vendor/ars/scripts/ars_write_scope_guard.py +506 -0
  337. package/vendor/ars/scripts/arxiv_client.py +222 -0
  338. package/vendor/ars/scripts/audit_snapshot.py +572 -0
  339. package/vendor/ars/scripts/bibliographic_integrity_signals.py +800 -0
  340. package/vendor/ars/scripts/bootstrap_timeline_yaml.py +146 -0
  341. package/vendor/ars/scripts/build_claim_standing_candidate_ledger.py +1238 -0
  342. package/vendor/ars/scripts/build_claim_standing_query_plan.py +643 -0
  343. package/vendor/ars/scripts/build_content_coverage_advisory.py +1205 -0
  344. package/vendor/ars/scripts/build_cross_document_consistency_advisory.py +2332 -0
  345. package/vendor/ars/scripts/build_review_pathway_rule_trace.py +830 -0
  346. package/vendor/ars/scripts/build_submission_packet_manifest.py +2310 -0
  347. package/vendor/ars/scripts/check_215_field_norm.py +173 -0
  348. package/vendor/ars/scripts/check_216_surface_form.py +250 -0
  349. package/vendor/ars/scripts/check_268_nested_commitment_ledger.py +180 -0
  350. package/vendor/ars/scripts/check_390_revision_patch_discipline.py +296 -0
  351. package/vendor/ars/scripts/check_392_citation_verification_intake.py +126 -0
  352. package/vendor/ars/scripts/check_394_submission_policy.py +178 -0
  353. package/vendor/ars/scripts/check_439_format_profile.py +307 -0
  354. package/vendor/ars/scripts/check_619_disclosure_closeout.py +237 -0
  355. package/vendor/ars/scripts/check_630_codex_subscription_transport.py +456 -0
  356. package/vendor/ars/scripts/check_669_review_pathway_rule_trace.py +662 -0
  357. package/vendor/ars/scripts/check_670_revision_roadmap_integration.py +518 -0
  358. package/vendor/ars/scripts/check_673_adjudication_activity.py +684 -0
  359. package/vendor/ars/scripts/check_684_review_criteria_binding.py +557 -0
  360. package/vendor/ars/scripts/check_agents_mirror_sync.py +115 -0
  361. package/vendor/ars/scripts/check_audit_artifact_consistency.py +2313 -0
  362. package/vendor/ars/scripts/check_benchmark_report.py +79 -0
  363. package/vendor/ars/scripts/check_bibliographic_integrity_signals.py +831 -0
  364. package/vendor/ars/scripts/check_calibration_tiers.py +235 -0
  365. package/vendor/ars/scripts/check_changelog_covers_merges.py +289 -0
  366. package/vendor/ars/scripts/check_ci_pytest_manifest.py +204 -0
  367. package/vendor/ars/scripts/check_claim_audit_consistency.py +1664 -0
  368. package/vendor/ars/scripts/check_claim_standing_candidate_ledger_integration.py +500 -0
  369. package/vendor/ars/scripts/check_claim_standing_freshness.py +253 -0
  370. package/vendor/ars/scripts/check_claim_standing_transmissions.py +449 -0
  371. package/vendor/ars/scripts/check_collaboration_depth_rubric.py +180 -0
  372. package/vendor/ars/scripts/check_command_frontmatter_name.py +116 -0
  373. package/vendor/ars/scripts/check_committee_correspondence.py +333 -0
  374. package/vendor/ars/scripts/check_compliance_report.py +108 -0
  375. package/vendor/ars/scripts/check_content_coverage_advisory_integration.py +796 -0
  376. package/vendor/ars/scripts/check_control_availability.py +172 -0
  377. package/vendor/ars/scripts/check_corpus_consumer_protocol.py +404 -0
  378. package/vendor/ars/scripts/check_cross_document_consistency_advisory_integration.py +1191 -0
  379. package/vendor/ars/scripts/check_cross_model_handoff_contract.py +234 -0
  380. package/vendor/ars/scripts/check_cross_model_verification_sync.py +261 -0
  381. package/vendor/ars/scripts/check_data_access_level.py +131 -0
  382. package/vendor/ars/scripts/check_data_flows.py +252 -0
  383. package/vendor/ars/scripts/check_decision_contract.py +464 -0
  384. package/vendor/ars/scripts/check_degradation_registry.py +326 -0
  385. package/vendor/ars/scripts/check_distribution_surface_claims.py +226 -0
  386. package/vendor/ars/scripts/check_domain_evidence_profile.py +538 -0
  387. package/vendor/ars/scripts/check_e4_promotion.py +195 -0
  388. package/vendor/ars/scripts/check_evals_gold_set.py +279 -0
  389. package/vendor/ars/scripts/check_evidence_row_integration.py +396 -0
  390. package/vendor/ars/scripts/check_experiment_provenance.py +117 -0
  391. package/vendor/ars/scripts/check_field_norm_severity.py +144 -0
  392. package/vendor/ars/scripts/check_firm_rules_sync.py +375 -0
  393. package/vendor/ars/scripts/check_heldout_measurement_report.py +1179 -0
  394. package/vendor/ars/scripts/check_human_subjects_output_contract.py +139 -0
  395. package/vendor/ars/scripts/check_human_subjects_reference_migration.py +844 -0
  396. package/vendor/ars/scripts/check_indirect_prompt_injection_no_call.py +328 -0
  397. package/vendor/ars/scripts/check_instruction_data_boundary.py +236 -0
  398. package/vendor/ars/scripts/check_judge_prompt_version.py +125 -0
  399. package/vendor/ars/scripts/check_literature_corpus_schema.py +402 -0
  400. package/vendor/ars/scripts/check_model_tiering.py +223 -0
  401. package/vendor/ars/scripts/check_panel_synthesis.py +1331 -0
  402. package/vendor/ars/scripts/check_passport_reset_contract.py +214 -0
  403. package/vendor/ars/scripts/check_pattern_eval_manifest.py +422 -0
  404. package/vendor/ars/scripts/check_persuasion_invariance_fixtures.py +637 -0
  405. package/vendor/ars/scripts/check_phase_conformance.py +2180 -0
  406. package/vendor/ars/scripts/check_pipeline_boundary_semantics.py +600 -0
  407. package/vendor/ars/scripts/check_pipeline_integrity.py +340 -0
  408. package/vendor/ars/scripts/check_policy_anchor_protocol.py +159 -0
  409. package/vendor/ars/scripts/check_policy_anchor_table.py +286 -0
  410. package/vendor/ars/scripts/check_preprint_venues_consistency.py +123 -0
  411. package/vendor/ars/scripts/check_prisma_trAIce_freshness.py +84 -0
  412. package/vendor/ars/scripts/check_promotion_bakeoff_preregistration.py +1303 -0
  413. package/vendor/ars/scripts/check_ranking_lift.py +323 -0
  414. package/vendor/ars/scripts/check_re_review_synthesis.py +2719 -0
  415. package/vendor/ars/scripts/check_receipt_enum_sync.py +203 -0
  416. package/vendor/ars/scripts/check_repro_lock.py +85 -0
  417. package/vendor/ars/scripts/check_review_pathway_output.py +276 -0
  418. package/vendor/ars/scripts/check_reviewer_data_fences.py +224 -0
  419. package/vendor/ars/scripts/check_reviewer_finding_contract.py +783 -0
  420. package/vendor/ars/scripts/check_reviewer_role_label.py +447 -0
  421. package/vendor/ars/scripts/check_reviewer_scoring_honesty.py +292 -0
  422. package/vendor/ars/scripts/check_reviewer_sprint_prompt_sync.py +409 -0
  423. package/vendor/ars/scripts/check_revision_claim_drift_suite_v2.py +1622 -0
  424. package/vendor/ars/scripts/check_revision_token_conservation.py +263 -0
  425. package/vendor/ars/scripts/check_risk_register.py +280 -0
  426. package/vendor/ars/scripts/check_role_scoped_contract.py +702 -0
  427. package/vendor/ars/scripts/check_rq_framing_patterns.py +184 -0
  428. package/vendor/ars/scripts/check_rubric_weight_consistency.py +32 -0
  429. package/vendor/ars/scripts/check_seeded_defect_fixtures.py +425 -0
  430. package/vendor/ars/scripts/check_setup_cross_model_parity.py +139 -0
  431. package/vendor/ars/scripts/check_spec_consistency.py +1249 -0
  432. package/vendor/ars/scripts/check_sprint_contract.py +371 -0
  433. package/vendor/ars/scripts/check_stage_capability_matrix.py +784 -0
  434. package/vendor/ars/scripts/check_submission_packet_manifest_integration.py +358 -0
  435. package/vendor/ars/scripts/check_surface_form_parity.py +445 -0
  436. package/vendor/ars/scripts/check_task_type.py +22 -0
  437. package/vendor/ars/scripts/check_tools_allowlist.py +538 -0
  438. package/vendor/ars/scripts/check_tortured_phrase_screening_integration.py +1974 -0
  439. package/vendor/ars/scripts/check_v3_10_134_write_scope.py +286 -0
  440. package/vendor/ars/scripts/check_v3_10_policy.py +656 -0
  441. package/vendor/ars/scripts/check_v3_6_6_ab_manifest.py +364 -0
  442. package/vendor/ars/scripts/check_v3_6_7_pattern_protection.py +1366 -0
  443. package/vendor/ars/scripts/check_v3_6_8_audit_scope_block.py +464 -0
  444. package/vendor/ars/scripts/check_v3_6_8_cite_provenance_pipeline.py +231 -0
  445. package/vendor/ars/scripts/check_v3_6_8_frontmatter_trust_schema.py +248 -0
  446. package/vendor/ars/scripts/check_v3_6_8_mark_read_commands.py +79 -0
  447. package/vendor/ars/scripts/check_v3_6_8_pattern_protection.py +941 -0
  448. package/vendor/ars/scripts/check_v3_7_3_three_layer_citation.py +318 -0
  449. package/vendor/ars/scripts/check_v3_8_annotation_literal_sync.py +228 -0
  450. package/vendor/ars/scripts/check_v3_9_0_triangulation.py +366 -0
  451. package/vendor/ars/scripts/check_v3_9_2_phase_boundary.py +270 -0
  452. package/vendor/ars/scripts/check_v3_9_4_temporal_verification.py +139 -0
  453. package/vendor/ars/scripts/check_venue_disclosure_policies.py +63 -0
  454. package/vendor/ars/scripts/check_version_consistency.py +826 -0
  455. package/vendor/ars/scripts/check_workflow_classification.py +223 -0
  456. package/vendor/ars/scripts/chinese_literature_client.py +1938 -0
  457. package/vendor/ars/scripts/citation_verification_summary.py +85 -0
  458. package/vendor/ars/scripts/claim_audit_calibration.py +517 -0
  459. package/vendor/ars/scripts/claim_audit_finalizer.py +456 -0
  460. package/vendor/ars/scripts/claim_audit_pipeline.py +1594 -0
  461. package/vendor/ars/scripts/claim_registry_coverage.py +493 -0
  462. package/vendor/ars/scripts/claim_standing_discovery.py +784 -0
  463. package/vendor/ars/scripts/claim_standing_stance_runner.py +758 -0
  464. package/vendor/ars/scripts/claim_standing_stance_scorer.py +239 -0
  465. package/vendor/ars/scripts/claim_strength_drift_disposition.py +666 -0
  466. package/vendor/ars/scripts/contamination_signals.py +689 -0
  467. package/vendor/ars/scripts/corpus_consumer_manifest.json +19 -0
  468. package/vendor/ars/scripts/cross_model_codex_transport.py +1374 -0
  469. package/vendor/ars/scripts/cross_model_codex_verify.sh +6 -0
  470. package/vendor/ars/scripts/cross_model_handoff.py +359 -0
  471. package/vendor/ars/scripts/cross_model_smoke_test.sh +183 -0
  472. package/vendor/ars/scripts/cross_model_smoke_test_codex.sh +35 -0
  473. package/vendor/ars/scripts/cross_model_verification/gemini_is_grounded.jq +45 -0
  474. package/vendor/ars/scripts/cross_model_verification/gemini_sources.jq +45 -0
  475. package/vendor/ars/scripts/cross_model_verification/normalize_compat_verdict.py +57 -0
  476. package/vendor/ars/scripts/cross_model_verification/openai_has_completed_web_search.jq +15 -0
  477. package/vendor/ars/scripts/cross_model_verification/openai_sources.jq +17 -0
  478. package/vendor/ars/scripts/cross_model_verification/openai_text.jq +11 -0
  479. package/vendor/ars/scripts/crossref_client.py +225 -0
  480. package/vendor/ars/scripts/dispatch_e4_panel.py +2731 -0
  481. package/vendor/ars/scripts/evidence_rows.py +2043 -0
  482. package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3.json +32 -0
  483. package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_input.json +27 -0
  484. package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_prime.json +32 -0
  485. package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_prime_input.json +27 -0
  486. package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_override.json +50 -0
  487. package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_override_action.json +14 -0
  488. package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_pass.json +44 -0
  489. package/vendor/ars/scripts/fixtures/adjudication_activity/explicit_user_request_log.json +26 -0
  490. package/vendor/ars/scripts/fixtures/adjudication_activity/mandatory_checkpoint_log.json +116 -0
  491. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/README.md +56 -0
  492. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.audit_artifact_entry.json +24 -0
  493. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
  494. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
  495. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.verdict.yaml +19 -0
  496. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a7_orphan_completion/2026-04-30T15-22-04Z-d8f3.jsonl +3 -0
  497. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.audit_artifact_entry.json +26 -0
  498. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
  499. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
  500. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.verdict.yaml +19 -0
  501. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.audit_artifact_entry.json +24 -0
  502. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
  503. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
  504. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.verdict.yaml +12 -0
  505. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/retraction.json +46 -0
  506. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/retraction_check_attestation.json +44 -0
  507. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase.json +44 -0
  508. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase_v1_2_abstract_missing.json +107 -0
  509. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase_v1_2_detected.json +124 -0
  510. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/expected_outcomes.json +77 -0
  511. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/manifest.yaml +37 -0
  512. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/001-valid-doi-test.json +20 -0
  513. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/002-valid-arxiv-test.json +19 -0
  514. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/003-fabricated-test.json +20 -0
  515. package/vendor/ars/scripts/fixtures/claim_audit_calibration/gold_set.json +344 -0
  516. package/vendor/ars/scripts/fixtures/claim_standing_candidate_ledger/query_plan.json +108 -0
  517. package/vendor/ars/scripts/fixtures/claim_standing_candidate_ledger/retrieval_input.json +179 -0
  518. package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/concern_tracker.json +129 -0
  519. package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/response_skeleton.md +19 -0
  520. package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/source_letter.txt +9 -0
  521. package/vendor/ars/scripts/fixtures/content_coverage_advisory/base_draft.json +48 -0
  522. package/vendor/ars/scripts/fixtures/content_coverage_advisory/base_inventory.json +38 -0
  523. package/vendor/ars/scripts/fixtures/content_coverage_advisory/packet/consent.txt +1 -0
  524. package/vendor/ars/scripts/fixtures/content_coverage_advisory/session_sources.json +3 -0
  525. package/vendor/ars/scripts/fixtures/cross_document_consistency/README.md +6 -0
  526. package/vendor/ars/scripts/fixtures/cross_document_consistency/accepted_draft.md +35 -0
  527. package/vendor/ars/scripts/fixtures/cross_document_consistency/cases.json +75 -0
  528. package/vendor/ars/scripts/fixtures/cross_document_consistency/preregistration.md +5 -0
  529. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/forbidden_event.jsonl +4 -0
  530. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/grounded_verified.jsonl +3 -0
  531. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/malformed.jsonl +3 -0
  532. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/missing_search.jsonl +2 -0
  533. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/multiple_finals.jsonl +4 -0
  534. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/not_found.jsonl +3 -0
  535. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/unbound_source.jsonl +3 -0
  536. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/wrong_search_shape.jsonl +3 -0
  537. package/vendor/ars/scripts/fixtures/evidence_rows/phase_e_inputs.json +114 -0
  538. package/vendor/ars/scripts/fixtures/evidence_rows/session_sources.json +5 -0
  539. package/vendor/ars/scripts/fixtures/human_subjects_authority/cross-border-us-tw-gdpr.json +71 -0
  540. package/vendor/ars/scripts/fixtures/human_subjects_authority/gdpr-member-state-unresolved.json +62 -0
  541. package/vendor/ars/scripts/fixtures/human_subjects_authority/missing-data-axis.json +45 -0
  542. package/vendor/ars/scripts/fixtures/human_subjects_authority/no-profile.json +33 -0
  543. package/vendor/ars/scripts/fixtures/human_subjects_authority/tw-gdpr-two-axis.json +74 -0
  544. package/vendor/ars/scripts/fixtures/human_subjects_authority/us-gdpr-two-axis.json +74 -0
  545. package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/lint-near-misses.json +72 -0
  546. package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/no-profile-request.json +18 -0
  547. package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/tw-candidates-request.json +63 -0
  548. package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/us-candidates-request.json +77 -0
  549. package/vendor/ars/scripts/fixtures/review_target_context/exact-declaration.json +24 -0
  550. package/vendor/ars/scripts/fixtures/review_target_context/field-general-declaration.json +24 -0
  551. package/vendor/ars/scripts/fixtures/review_target_context/msr-2027-technical-full-declaration.json +24 -0
  552. package/vendor/ars/scripts/fixtures/review_target_context/synthetic-registry.json +92 -0
  553. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/README.md +21 -0
  554. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_attested_only.json +55 -0
  555. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_machine_supported.json +49 -0
  556. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_not_isolated.json +56 -0
  557. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_unknown.json +49 -0
  558. package/vendor/ars/scripts/fixtures/submission_package/clean/paper.md +16 -0
  559. package/vendor/ars/scripts/fixtures/submission_package/clean/references.bib +13 -0
  560. package/vendor/ars/scripts/fixtures/submission_package/fallback_authoryear/paper.md +11 -0
  561. package/vendor/ars/scripts/fixtures/submission_package/fallback_authoryear/references.bib +13 -0
  562. package/vendor/ars/scripts/fixtures/submission_package/fallback_latex/paper.tex +9 -0
  563. package/vendor/ars/scripts/fixtures/submission_package/fallback_latex/references.bib +13 -0
  564. package/vendor/ars/scripts/fixtures/submission_package/marker_no_join/paper.md +7 -0
  565. package/vendor/ars/scripts/fixtures/submission_package/orphan_intext/paper.md +8 -0
  566. package/vendor/ars/scripts/fixtures/submission_package/orphan_intext/references.bib +6 -0
  567. package/vendor/ars/scripts/fixtures/submission_package/passports/corpus_only.yaml +9 -0
  568. package/vendor/ars/scripts/fixtures/submission_package/passports/summary_join.yaml +16 -0
  569. package/vendor/ars/scripts/fixtures/submission_package/profiles/full.yaml +15 -0
  570. package/vendor/ars/scripts/fixtures/submission_package/profiles/tight.yaml +15 -0
  571. package/vendor/ars/scripts/fixtures/submission_package/summary_join/paper.md +7 -0
  572. package/vendor/ars/scripts/fixtures/submission_package/uncited_reference/paper.md +9 -0
  573. package/vendor/ars/scripts/fixtures/submission_package/uncited_reference/references.bib +13 -0
  574. package/vendor/ars/scripts/fixtures/submission_package/venue_clean/paper.md +26 -0
  575. package/vendor/ars/scripts/fixtures/submission_package/venue_clean/references.bib +13 -0
  576. package/vendor/ars/scripts/fixtures/submission_package/venue_violations/paper.md +20 -0
  577. package/vendor/ars/scripts/fixtures/submission_package/venue_violations/references.bib +13 -0
  578. package/vendor/ars/scripts/fixtures/submission_packet_manifest/base_inventory.json +101 -0
  579. package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/consent-materials.txt +3 -0
  580. package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/training-certificate.txt +2 -0
  581. package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/tw-consent-materials.txt +3 -0
  582. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/corpus_input.yaml +30 -0
  583. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/own_draft.md +36 -0
  584. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/own_draft.tex +21 -0
  585. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/seed_expectations.json +218 -0
  586. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/snapshot.json +164 -0
  587. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/snapshot_manifest.json +29 -0
  588. package/vendor/ars/scripts/fixtures/transport_bodies/README.md +55 -0
  589. package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/empty_feed.xml +10 -0
  590. package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/error_5xx.html +7 -0
  591. package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/id_hit.xml +26 -0
  592. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/README.md +69 -0
  593. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/cnki_landing_page.html +13 -0
  594. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/error_5xx.html +2 -0
  595. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_ambiguous.json +16 -0
  596. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_hit.json +16 -0
  597. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_zero.json +14 -0
  598. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coverage_hit.json +15 -0
  599. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coverage_zero.json +13 -0
  600. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_hit.json +43 -0
  601. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_issn_mismatch.json +31 -0
  602. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_no_doi.json +31 -0
  603. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_unknown_ra.json +34 -0
  604. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_year_mismatch.json +31 -0
  605. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_absent.json +4 -0
  606. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_exists.json +16 -0
  607. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_internal_error.json +4 -0
  608. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/istic_csl_hit.json +25 -0
  609. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/istic_csl_other_title.json +18 -0
  610. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_cnki.json +6 -0
  611. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_crossref.json +6 -0
  612. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_istic.json +6 -0
  613. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_unknown_prefix.json +6 -0
  614. package/vendor/ars/scripts/fixtures/transport_bodies/crossref/doi_hit.json +24 -0
  615. package/vendor/ars/scripts/fixtures/transport_bodies/crossref/error_5xx.html +7 -0
  616. package/vendor/ars/scripts/fixtures/transport_bodies/crossref/title_search_miss.json +15 -0
  617. package/vendor/ars/scripts/fixtures/transport_bodies/openalex/doi_hit.json +29 -0
  618. package/vendor/ars/scripts/fixtures/transport_bodies/openalex/error_5xx.json +4 -0
  619. package/vendor/ars/scripts/fixtures/transport_bodies/openalex/title_search_miss.json +11 -0
  620. package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/doi_hit.json +14 -0
  621. package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/error_5xx.json +3 -0
  622. package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/title_search_miss.json +5 -0
  623. package/vendor/ars/scripts/human_read_attestation_resolver.py +467 -0
  624. package/vendor/ars/scripts/ideation_diversity_assignment_gate.py +430 -0
  625. package/vendor/ars/scripts/inquiry_branch_ledger.py +2540 -0
  626. package/vendor/ars/scripts/legacy/ars_apply_revision_patch_v1_0.py +715 -0
  627. package/vendor/ars/scripts/legacy/check_re_review_synthesis_v1_0.py +2215 -0
  628. package/vendor/ars/scripts/migrate_literature_corpus_to_v3_10.py +223 -0
  629. package/vendor/ars/scripts/migrate_literature_corpus_to_v3_7_3.py +277 -0
  630. package/vendor/ars/scripts/migrate_literature_corpus_to_v3_9_0.py +304 -0
  631. package/vendor/ars/scripts/model_tiering_manifest.json +46 -0
  632. package/vendor/ars/scripts/openalex_client.py +232 -0
  633. package/vendor/ars/scripts/parse_audit_verdict.py +802 -0
  634. package/vendor/ars/scripts/pdf_content_classifier_worker.py +176 -0
  635. package/vendor/ars/scripts/pdf_read_preflight.py +1455 -0
  636. package/vendor/ars/scripts/policy_anchor_disclosure_referee.py +354 -0
  637. package/vendor/ars/scripts/recompute_receipts.py +1414 -0
  638. package/vendor/ars/scripts/render_claim_standing_view.py +374 -0
  639. package/vendor/ars/scripts/render_eval_comment.py +130 -0
  640. package/vendor/ars/scripts/render_harness_retirement_issue.py +167 -0
  641. package/vendor/ars/scripts/repro_lock_validation.py +90 -0
  642. package/vendor/ars/scripts/research_workflow_profile.py +1079 -0
  643. package/vendor/ars/scripts/resolve_human_subjects_authority.py +1158 -0
  644. package/vendor/ars/scripts/resolve_review_target_context.py +683 -0
  645. package/vendor/ars/scripts/resume_e4_record.py +509 -0
  646. package/vendor/ars/scripts/retraction_status.py +484 -0
  647. package/vendor/ars/scripts/review_criteria_binding.py +889 -0
  648. package/vendor/ars/scripts/review_panel_provenance.py +744 -0
  649. package/vendor/ars/scripts/revision_roadmap.py +1967 -0
  650. package/vendor/ars/scripts/run_ci_pytest_manifest.py +116 -0
  651. package/vendor/ars/scripts/run_codex_audit.sh +1191 -0
  652. package/vendor/ars/scripts/run_evals.py +513 -0
  653. package/vendor/ars/scripts/run_ideation_diversity_no_call.py +3505 -0
  654. package/vendor/ars/scripts/run_indirect_prompt_injection_no_call.py +3330 -0
  655. package/vendor/ars/scripts/run_indirect_prompt_injection_probe.py +399 -0
  656. package/vendor/ars/scripts/run_review_criteria_constructive_value.py +1895 -0
  657. package/vendor/ars/scripts/run_role_topology_utility_dry_run.py +606 -0
  658. package/vendor/ars/scripts/score_review_criteria_constructive_value.py +617 -0
  659. package/vendor/ars/scripts/semantic_scholar_client.py +291 -0
  660. package/vendor/ars/scripts/slr_lineage.py +59 -0
  661. package/vendor/ars/scripts/sync_adapter_docs.py +118 -0
  662. package/vendor/ars/scripts/temporal_integrity_audit.py +840 -0
  663. package/vendor/ars/scripts/test_431_exact_or_bust.py +253 -0
  664. package/vendor/ars/scripts/test__eval_threshold_gate.py +125 -0
  665. package/vendor/ars/scripts/test__markdown_lint_util.py +129 -0
  666. package/vendor/ars/scripts/test__next_verified_at_ms.py +256 -0
  667. package/vendor/ars/scripts/test_adjacent_framing_probe_lint.py +171 -0
  668. package/vendor/ars/scripts/test_adjudication_activity.py +1515 -0
  669. package/vendor/ars/scripts/test_ars_anchorize_draft.py +178 -0
  670. package/vendor/ars/scripts/test_ars_apply_revision_patch.py +1335 -0
  671. package/vendor/ars/scripts/test_ars_cache_invalidate.py +51 -0
  672. package/vendor/ars/scripts/test_ars_mark_read.py +910 -0
  673. package/vendor/ars/scripts/test_ars_update_check.py +816 -0
  674. package/vendor/ars/scripts/test_ars_write_scope_guard.py +790 -0
  675. package/vendor/ars/scripts/test_arxiv_client.py +374 -0
  676. package/vendor/ars/scripts/test_audit_schemas.py +560 -0
  677. package/vendor/ars/scripts/test_audit_snapshot_render_section_0.py +105 -0
  678. package/vendor/ars/scripts/test_block_parser.py +259 -0
  679. package/vendor/ars/scripts/test_bootstrap_timeline_yaml.py +148 -0
  680. package/vendor/ars/scripts/test_build_claim_standing_candidate_ledger.py +1203 -0
  681. package/vendor/ars/scripts/test_build_claim_standing_query_plan.py +791 -0
  682. package/vendor/ars/scripts/test_build_submission_packet_manifest.py +2644 -0
  683. package/vendor/ars/scripts/test_check_215_field_norm.py +238 -0
  684. package/vendor/ars/scripts/test_check_216_surface_form.py +341 -0
  685. package/vendor/ars/scripts/test_check_268_nested_commitment_ledger.py +194 -0
  686. package/vendor/ars/scripts/test_check_390_revision_patch_discipline.py +279 -0
  687. package/vendor/ars/scripts/test_check_392_citation_verification_intake.py +144 -0
  688. package/vendor/ars/scripts/test_check_394_submission_policy.py +200 -0
  689. package/vendor/ars/scripts/test_check_439_format_profile.py +252 -0
  690. package/vendor/ars/scripts/test_check_619_disclosure_closeout.py +158 -0
  691. package/vendor/ars/scripts/test_check_630_codex_subscription_transport.py +245 -0
  692. package/vendor/ars/scripts/test_check_669_review_pathway_rule_trace.py +304 -0
  693. package/vendor/ars/scripts/test_check_670_revision_roadmap_integration.py +260 -0
  694. package/vendor/ars/scripts/test_check_673_adjudication_activity.py +367 -0
  695. package/vendor/ars/scripts/test_check_684_review_criteria_binding.py +274 -0
  696. package/vendor/ars/scripts/test_check_agents_mirror_sync.py +137 -0
  697. package/vendor/ars/scripts/test_check_audit_artifact_consistency.py +2133 -0
  698. package/vendor/ars/scripts/test_check_benchmark_report.py +117 -0
  699. package/vendor/ars/scripts/test_check_bibliographic_integrity_signals.py +609 -0
  700. package/vendor/ars/scripts/test_check_calibration_tiers.py +376 -0
  701. package/vendor/ars/scripts/test_check_changelog_covers_merges.py +509 -0
  702. package/vendor/ars/scripts/test_check_ci_pytest_manifest.py +425 -0
  703. package/vendor/ars/scripts/test_check_claim_standing_candidate_ledger_integration.py +256 -0
  704. package/vendor/ars/scripts/test_check_claim_standing_freshness.py +277 -0
  705. package/vendor/ars/scripts/test_check_collaboration_depth_rubric.py +239 -0
  706. package/vendor/ars/scripts/test_check_command_frontmatter_name.py +178 -0
  707. package/vendor/ars/scripts/test_check_committee_correspondence.py +298 -0
  708. package/vendor/ars/scripts/test_check_compliance_report.py +381 -0
  709. package/vendor/ars/scripts/test_check_content_coverage_advisory_integration.py +308 -0
  710. package/vendor/ars/scripts/test_check_control_availability.py +361 -0
  711. package/vendor/ars/scripts/test_check_cross_document_consistency_advisory_integration.py +698 -0
  712. package/vendor/ars/scripts/test_check_cross_model_handoff_contract.py +301 -0
  713. package/vendor/ars/scripts/test_check_cross_model_verification_sync.py +203 -0
  714. package/vendor/ars/scripts/test_check_data_access_level.py +227 -0
  715. package/vendor/ars/scripts/test_check_data_flows.py +382 -0
  716. package/vendor/ars/scripts/test_check_decision_contract.py +387 -0
  717. package/vendor/ars/scripts/test_check_degradation_registry.py +270 -0
  718. package/vendor/ars/scripts/test_check_distribution_surface_claims.py +225 -0
  719. package/vendor/ars/scripts/test_check_domain_evidence_profile.py +439 -0
  720. package/vendor/ars/scripts/test_check_e4_promotion.py +163 -0
  721. package/vendor/ars/scripts/test_check_evals_gold_set.py +312 -0
  722. package/vendor/ars/scripts/test_check_evidence_row_integration.py +187 -0
  723. package/vendor/ars/scripts/test_check_field_norm_severity.py +173 -0
  724. package/vendor/ars/scripts/test_check_firm_rules_sync.py +342 -0
  725. package/vendor/ars/scripts/test_check_heldout_measurement_report.py +1508 -0
  726. package/vendor/ars/scripts/test_check_human_subjects_output_contract.py +129 -0
  727. package/vendor/ars/scripts/test_check_human_subjects_reference_migration.py +756 -0
  728. package/vendor/ars/scripts/test_check_instruction_data_boundary.py +204 -0
  729. package/vendor/ars/scripts/test_check_judge_prompt_version.py +90 -0
  730. package/vendor/ars/scripts/test_check_model_tiering.py +236 -0
  731. package/vendor/ars/scripts/test_check_panel_synthesis.py +1658 -0
  732. package/vendor/ars/scripts/test_check_passport_reset_contract.py +249 -0
  733. package/vendor/ars/scripts/test_check_pattern_eval_manifest.py +381 -0
  734. package/vendor/ars/scripts/test_check_persuasion_invariance_fixtures.py +591 -0
  735. package/vendor/ars/scripts/test_check_phase_conformance.py +4366 -0
  736. package/vendor/ars/scripts/test_check_pipeline_boundary_semantics.py +931 -0
  737. package/vendor/ars/scripts/test_check_pipeline_integrity.py +243 -0
  738. package/vendor/ars/scripts/test_check_policy_anchor_protocol.py +236 -0
  739. package/vendor/ars/scripts/test_check_policy_anchor_table.py +295 -0
  740. package/vendor/ars/scripts/test_check_prisma_trAIce_freshness.py +68 -0
  741. package/vendor/ars/scripts/test_check_promotion_bakeoff_preregistration.py +799 -0
  742. package/vendor/ars/scripts/test_check_ranking_lift.py +377 -0
  743. package/vendor/ars/scripts/test_check_re_review_synthesis.py +3398 -0
  744. package/vendor/ars/scripts/test_check_receipt_enum_sync.py +176 -0
  745. package/vendor/ars/scripts/test_check_repro_lock.py +107 -0
  746. package/vendor/ars/scripts/test_check_reviewer_data_fences.py +334 -0
  747. package/vendor/ars/scripts/test_check_reviewer_finding_contract.py +1107 -0
  748. package/vendor/ars/scripts/test_check_reviewer_role_label.py +491 -0
  749. package/vendor/ars/scripts/test_check_reviewer_scoring_honesty.py +222 -0
  750. package/vendor/ars/scripts/test_check_reviewer_sprint_prompt_sync.py +373 -0
  751. package/vendor/ars/scripts/test_check_revision_claim_drift_suite_v2.py +1925 -0
  752. package/vendor/ars/scripts/test_check_revision_token_conservation.py +398 -0
  753. package/vendor/ars/scripts/test_check_risk_register.py +336 -0
  754. package/vendor/ars/scripts/test_check_role_scoped_contract.py +1104 -0
  755. package/vendor/ars/scripts/test_check_rq_framing_patterns.py +110 -0
  756. package/vendor/ars/scripts/test_check_rubric_weight_consistency.py +32 -0
  757. package/vendor/ars/scripts/test_check_seeded_defect_fixtures.py +392 -0
  758. package/vendor/ars/scripts/test_check_setup_cross_model_parity.py +121 -0
  759. package/vendor/ars/scripts/test_check_spec_consistency.py +1081 -0
  760. package/vendor/ars/scripts/test_check_sprint_contract.py +458 -0
  761. package/vendor/ars/scripts/test_check_stage_capability_matrix.py +698 -0
  762. package/vendor/ars/scripts/test_check_submission_packet_manifest_integration.py +177 -0
  763. package/vendor/ars/scripts/test_check_surface_form_parity.py +417 -0
  764. package/vendor/ars/scripts/test_check_task_type.py +116 -0
  765. package/vendor/ars/scripts/test_check_tools_allowlist.py +835 -0
  766. package/vendor/ars/scripts/test_check_tortured_phrase_screening_integration.py +1306 -0
  767. package/vendor/ars/scripts/test_check_v3_10_134_write_scope.py +251 -0
  768. package/vendor/ars/scripts/test_check_v3_10_policy.py +547 -0
  769. package/vendor/ars/scripts/test_check_v3_6_7_pattern_protection.py +960 -0
  770. package/vendor/ars/scripts/test_check_v3_6_8_audit_scope_block.py +1000 -0
  771. package/vendor/ars/scripts/test_check_v3_6_8_cite_provenance_pipeline.py +454 -0
  772. package/vendor/ars/scripts/test_check_v3_6_8_frontmatter_trust_schema.py +581 -0
  773. package/vendor/ars/scripts/test_check_v3_6_8_mark_read_commands.py +120 -0
  774. package/vendor/ars/scripts/test_check_v3_6_8_pattern_protection.py +1138 -0
  775. package/vendor/ars/scripts/test_check_v3_7_3_three_layer_citation.py +566 -0
  776. package/vendor/ars/scripts/test_check_v3_8_annotation_literal_sync.py +262 -0
  777. package/vendor/ars/scripts/test_check_v3_9_0_triangulation.py +321 -0
  778. package/vendor/ars/scripts/test_check_v3_9_2_phase_boundary.py +190 -0
  779. package/vendor/ars/scripts/test_check_v3_9_4_temporal_verification.py +471 -0
  780. package/vendor/ars/scripts/test_check_version_consistency.py +1462 -0
  781. package/vendor/ars/scripts/test_check_workflow_classification.py +225 -0
  782. package/vendor/ars/scripts/test_chinese_literature_client.py +1889 -0
  783. package/vendor/ars/scripts/test_citation_existence_policy.py +480 -0
  784. package/vendor/ars/scripts/test_citation_verification_summary.py +342 -0
  785. package/vendor/ars/scripts/test_claim_audit_calibration.py +882 -0
  786. package/vendor/ars/scripts/test_claim_audit_finalizer.py +979 -0
  787. package/vendor/ars/scripts/test_claim_audit_pipeline.py +2398 -0
  788. package/vendor/ars/scripts/test_claim_audit_schema.py +1778 -0
  789. package/vendor/ars/scripts/test_claim_intent_manifest.py +666 -0
  790. package/vendor/ars/scripts/test_claim_registry_coverage.py +246 -0
  791. package/vendor/ars/scripts/test_claim_standing_discovery.py +579 -0
  792. package/vendor/ars/scripts/test_claim_standing_pipeline_wiring.py +290 -0
  793. package/vendor/ars/scripts/test_claim_standing_stance_assets.py +399 -0
  794. package/vendor/ars/scripts/test_claim_standing_stance_contracts.py +383 -0
  795. package/vendor/ars/scripts/test_claim_standing_stance_runner.py +349 -0
  796. package/vendor/ars/scripts/test_claim_standing_transmissions.py +605 -0
  797. package/vendor/ars/scripts/test_claim_strength_drift_disposition.py +517 -0
  798. package/vendor/ars/scripts/test_claim_verification_coverage_contract.py +104 -0
  799. package/vendor/ars/scripts/test_contamination_signals.py +1086 -0
  800. package/vendor/ars/scripts/test_content_coverage_advisory.py +1767 -0
  801. package/vendor/ars/scripts/test_cross_document_consistency_advisory.py +1270 -0
  802. package/vendor/ars/scripts/test_cross_model_codex_transport.py +1152 -0
  803. package/vendor/ars/scripts/test_cross_model_handoff.py +571 -0
  804. package/vendor/ars/scripts/test_cross_model_verification_guards.py +792 -0
  805. package/vendor/ars/scripts/test_crossref_client.py +393 -0
  806. package/vendor/ars/scripts/test_dispatch_e4_panel.py +4235 -0
  807. package/vendor/ars/scripts/test_e2e_claim_audit.py +540 -0
  808. package/vendor/ars/scripts/test_eval_harness_workflow.py +140 -0
  809. package/vendor/ars/scripts/test_evals_citation_extraction.py +150 -0
  810. package/vendor/ars/scripts/test_evals_lift_report_schema.py +108 -0
  811. package/vendor/ars/scripts/test_evidence_rows.py +2593 -0
  812. package/vendor/ars/scripts/test_experiment_provenance.py +915 -0
  813. package/vendor/ars/scripts/test_human_read_attestation_resolver.py +477 -0
  814. package/vendor/ars/scripts/test_ideation_diversity_assignment_gate.py +615 -0
  815. package/vendor/ars/scripts/test_indirect_prompt_injection_behavior_probe.py +250 -0
  816. package/vendor/ars/scripts/test_inquiry_branch_ledger.py +2296 -0
  817. package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_10.py +248 -0
  818. package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_7_3.py +545 -0
  819. package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_9_0.py +497 -0
  820. package/vendor/ars/scripts/test_normalize_compat_verdict.py +149 -0
  821. package/vendor/ars/scripts/test_openalex_client.py +490 -0
  822. package/vendor/ars/scripts/test_passport_yaml.py +104 -0
  823. package/vendor/ars/scripts/test_pattern_eval_runtime.py +1295 -0
  824. package/vendor/ars/scripts/test_pdf_read_preflight.py +1943 -0
  825. package/vendor/ars/scripts/test_policy_anchor_disclosure.py +666 -0
  826. package/vendor/ars/scripts/test_reading_probe_lint.py +218 -0
  827. package/vendor/ars/scripts/test_recompute_receipts.py +778 -0
  828. package/vendor/ars/scripts/test_render_claim_standing_view.py +192 -0
  829. package/vendor/ars/scripts/test_render_eval_comment.py +162 -0
  830. package/vendor/ars/scripts/test_render_harness_retirement_issue.py +110 -0
  831. package/vendor/ars/scripts/test_repro_lock_validation_drift.py +100 -0
  832. package/vendor/ars/scripts/test_research_workflow_profile.py +734 -0
  833. package/vendor/ars/scripts/test_resolve_human_subjects_authority.py +1219 -0
  834. package/vendor/ars/scripts/test_resolve_review_target_context.py +703 -0
  835. package/vendor/ars/scripts/test_resume_e4_record.py +315 -0
  836. package/vendor/ars/scripts/test_retraction_status.py +456 -0
  837. package/vendor/ars/scripts/test_review_criteria_binding.py +629 -0
  838. package/vendor/ars/scripts/test_review_panel_provenance.py +565 -0
  839. package/vendor/ars/scripts/test_review_pathway_rule_trace.py +821 -0
  840. package/vendor/ars/scripts/test_revision_roadmap.py +1255 -0
  841. package/vendor/ars/scripts/test_run_ci_pytest_manifest.py +186 -0
  842. package/vendor/ars/scripts/test_run_codex_audit_e2e.py +369 -0
  843. package/vendor/ars/scripts/test_run_evals.py +430 -0
  844. package/vendor/ars/scripts/test_run_guard_launcher.py +500 -0
  845. package/vendor/ars/scripts/test_run_ideation_diversity_no_call.py +1833 -0
  846. package/vendor/ars/scripts/test_run_indirect_prompt_injection_no_call.py +1889 -0
  847. package/vendor/ars/scripts/test_run_review_criteria_constructive_value.py +586 -0
  848. package/vendor/ars/scripts/test_run_role_topology_utility_dry_run.py +428 -0
  849. package/vendor/ars/scripts/test_score_review_criteria_constructive_value.py +340 -0
  850. package/vendor/ars/scripts/test_semantic_scholar_client.py +554 -0
  851. package/vendor/ars/scripts/test_slr_lineage_emission.py +230 -0
  852. package/vendor/ars/scripts/test_socratic_rq_non_generation_contract.py +173 -0
  853. package/vendor/ars/scripts/test_temporal_integrity_audit.py +438 -0
  854. package/vendor/ars/scripts/test_text_similarity.py +95 -0
  855. package/vendor/ars/scripts/test_title_fuzzy_false_positive.py +111 -0
  856. package/vendor/ars/scripts/test_tortured_phrase_screening.py +2959 -0
  857. package/vendor/ars/scripts/test_transport_fixture_citation_gate.py +338 -0
  858. package/vendor/ars/scripts/test_uncited_assertion.py +558 -0
  859. package/vendor/ars/scripts/test_v3_6_7_phase_6_6.py +1279 -0
  860. package/vendor/ars/scripts/test_validate_compliance_fixtures.py +36 -0
  861. package/vendor/ars/scripts/test_validate_ideation_diversity_assets.py +230 -0
  862. package/vendor/ars/scripts/test_venue_disclosure_contract.py +755 -0
  863. package/vendor/ars/scripts/test_verification_cache.py +280 -0
  864. package/vendor/ars/scripts/test_verification_gate.py +461 -0
  865. package/vendor/ars/scripts/test_verify_passport_cli.py +123 -0
  866. package/vendor/ars/scripts/test_verify_submission_package.py +1407 -0
  867. package/vendor/ars/scripts/test_version_records_schema.py +211 -0
  868. package/vendor/ars/scripts/tortured_phrase_screening.py +3502 -0
  869. package/vendor/ars/scripts/uncited_assertion_detector.py +254 -0
  870. package/vendor/ars/scripts/v3_6_7_inversion_manifest.json +9 -0
  871. package/vendor/ars/scripts/v3_6_8_inversion_manifest.json +10 -0
  872. package/vendor/ars/scripts/validate_claim_standing_stance_assets.py +249 -0
  873. package/vendor/ars/scripts/validate_compliance_fixtures.py +56 -0
  874. package/vendor/ars/scripts/validate_ideation_diversity_assets.py +303 -0
  875. package/vendor/ars/scripts/venue_disclosure_contract_harness.py +837 -0
  876. package/vendor/ars/scripts/verification_cache.py +276 -0
  877. package/vendor/ars/scripts/verification_gate/__init__.py +345 -0
  878. package/vendor/ars/scripts/verify_passport.py +133 -0
  879. package/vendor/ars/scripts/verify_submission_package.py +1657 -0
  880. package/vendor/ars/shared/agents/compliance_agent.md +136 -0
  881. package/vendor/ars/shared/artifact_reproducibility_pattern.md +173 -0
  882. package/vendor/ars/shared/benchmark_report.schema.json +81 -0
  883. package/vendor/ars/shared/benchmark_report_pattern.md +180 -0
  884. package/vendor/ars/shared/bibliographic_integrity_signals.md +142 -0
  885. package/vendor/ars/shared/collaboration_depth_rubric.md +154 -0
  886. package/vendor/ars/shared/compliance_checkpoint_protocol.md +162 -0
  887. package/vendor/ars/shared/compliance_report.schema.json +187 -0
  888. package/vendor/ars/shared/contracts/README.md +938 -0
  889. package/vendor/ars/shared/contracts/activity/adjudication_activity_input.schema.json +555 -0
  890. package/vendor/ars/shared/contracts/activity/adjudication_activity_store.schema.json +560 -0
  891. package/vendor/ars/shared/contracts/audit/audit_jsonl.schema.json +128 -0
  892. package/vendor/ars/shared/contracts/audit/audit_sidecar.schema.json +169 -0
  893. package/vendor/ars/shared/contracts/audit/audit_verdict.schema.json +133 -0
  894. package/vendor/ars/shared/contracts/audit/cross_document_consistency_advisory.schema.json +480 -0
  895. package/vendor/ars/shared/contracts/audit/cross_document_consistency_advisory_draft.schema.json +564 -0
  896. package/vendor/ars/shared/contracts/audit/cross_document_source_manifest.schema.json +202 -0
  897. package/vendor/ars/shared/contracts/audit/tortured_phrase_advisory.schema.json +1362 -0
  898. package/vendor/ars/shared/contracts/audit/tortured_phrase_snapshot.schema.json +208 -0
  899. package/vendor/ars/shared/contracts/audit/tortured_phrase_snapshot_manifest.schema.json +335 -0
  900. package/vendor/ars/shared/contracts/capability/stage_capability_matrix.json +434 -0
  901. package/vendor/ars/shared/contracts/claim_standing/candidate_ledger.schema.json +705 -0
  902. package/vendor/ars/shared/contracts/claim_standing/query_plan.schema.json +389 -0
  903. package/vendor/ars/shared/contracts/claim_standing/query_plan_v1_1.schema.json +782 -0
  904. package/vendor/ars/shared/contracts/claim_standing/retrieval_input.schema.json +440 -0
  905. package/vendor/ars/shared/contracts/claim_standing/stance_record.schema.json +460 -0
  906. package/vendor/ars/shared/contracts/claim_standing/transmission_ledger.schema.json +282 -0
  907. package/vendor/ars/shared/contracts/cross_model/codex_citation_receipt.schema.json +157 -0
  908. package/vendor/ars/shared/contracts/cross_model/codex_citation_request.schema.json +23 -0
  909. package/vendor/ars/shared/contracts/cross_model/promotion_bakeoff_sealed_commitment.schema.json +55 -0
  910. package/vendor/ars/shared/contracts/cross_model/promotion_bakeoff_sealed_reveal.schema.json +50 -0
  911. package/vendor/ars/shared/contracts/degradation_registry.json +428 -0
  912. package/vendor/ars/shared/contracts/evaluator/full.json +126 -0
  913. package/vendor/ars/shared/contracts/evidence/claim_registry.schema.json +50 -0
  914. package/vendor/ars/shared/contracts/evidence/claim_registry_coverage_report.schema.json +79 -0
  915. package/vendor/ars/shared/contracts/evidence/evidence_row.schema.json +504 -0
  916. package/vendor/ars/shared/contracts/evidence/evidence_row_v1_1.schema.json +364 -0
  917. package/vendor/ars/shared/contracts/evidence/evidence_row_v1_2.schema.json +714 -0
  918. package/vendor/ars/shared/contracts/evidence/evidence_row_v1_3.schema.json +524 -0
  919. package/vendor/ars/shared/contracts/human_subjects/authority_profile_registry.schema.json +497 -0
  920. package/vendor/ars/shared/contracts/human_subjects/committee_correspondence.schema.json +253 -0
  921. package/vendor/ars/shared/contracts/human_subjects/content_coverage_advisory.schema.json +620 -0
  922. package/vendor/ars/shared/contracts/human_subjects/irb_context_record.schema.json +329 -0
  923. package/vendor/ars/shared/contracts/human_subjects/resolved_authority_context.schema.json +347 -0
  924. package/vendor/ars/shared/contracts/human_subjects/review_pathway_rule_trace.schema.json +265 -0
  925. package/vendor/ars/shared/contracts/human_subjects/review_pathway_trace_request.schema.json +127 -0
  926. package/vendor/ars/shared/contracts/human_subjects/submission_packet_inventory.schema.json +240 -0
  927. package/vendor/ars/shared/contracts/human_subjects/submission_packet_manifest.schema.json +999 -0
  928. package/vendor/ars/shared/contracts/passport/audit_artifact_entry.schema.json +266 -0
  929. package/vendor/ars/shared/contracts/passport/bibliographic_integrity_signal.schema.json +1676 -0
  930. package/vendor/ars/shared/contracts/passport/citation_provenance.schema.json +107 -0
  931. package/vendor/ars/shared/contracts/passport/citation_verification_summary.schema.json +164 -0
  932. package/vendor/ars/shared/contracts/passport/claim_audit_result.schema.json +124 -0
  933. package/vendor/ars/shared/contracts/passport/claim_drift.schema.json +58 -0
  934. package/vendor/ars/shared/contracts/passport/claim_intent_manifest.schema.json +107 -0
  935. package/vendor/ars/shared/contracts/passport/constraint_violation.schema.json +65 -0
  936. package/vendor/ars/shared/contracts/passport/experiment_alignment_result.schema.json +69 -0
  937. package/vendor/ars/shared/contracts/passport/experiment_provenance_entry.schema.json +169 -0
  938. package/vendor/ars/shared/contracts/passport/human_read_log.schema.json +86 -0
  939. package/vendor/ars/shared/contracts/passport/inquiry_ledger_ref.schema.json +22 -0
  940. package/vendor/ars/shared/contracts/passport/literature_corpus_entry.schema.json +650 -0
  941. package/vendor/ars/shared/contracts/passport/preregistration_artifact.schema.json +132 -0
  942. package/vendor/ars/shared/contracts/passport/rejection_log.schema.json +89 -0
  943. package/vendor/ars/shared/contracts/passport/reset_ledger_entry.schema.json +158 -0
  944. package/vendor/ars/shared/contracts/passport/temporal_audit_results.schema.json +208 -0
  945. package/vendor/ars/shared/contracts/passport/terminal_policies.schema.json +50 -0
  946. package/vendor/ars/shared/contracts/passport/timeline.schema.json +102 -0
  947. package/vendor/ars/shared/contracts/passport/uncited_assertion.schema.json +56 -0
  948. package/vendor/ars/shared/contracts/passport/uncited_audit_failure.schema.json +72 -0
  949. package/vendor/ars/shared/contracts/passport/user_attested_read_resolution.schema.json +87 -0
  950. package/vendor/ars/shared/contracts/passport/version_records.schema.json +138 -0
  951. package/vendor/ars/shared/contracts/patch/block_manifest.schema.json +45 -0
  952. package/vendor/ars/shared/contracts/patch/legacy/v1_0/revision_patch.schema.json +110 -0
  953. package/vendor/ars/shared/contracts/patch/revision_patch.schema.json +250 -0
  954. package/vendor/ars/shared/contracts/pdf/pdf_content_classifier_diagnostic.schema.json +39 -0
  955. package/vendor/ars/shared/contracts/pdf/pdf_content_classifier_worker.schema.json +88 -0
  956. package/vendor/ars/shared/contracts/pdf/pdf_read_preflight.schema.json +227 -0
  957. package/vendor/ars/shared/contracts/re_review/input_manifest.schema.json +161 -0
  958. package/vendor/ars/shared/contracts/re_review/legacy/v1_0/input_manifest.schema.json +134 -0
  959. package/vendor/ars/shared/contracts/re_review/legacy/v1_0/precommitment.schema.json +193 -0
  960. package/vendor/ars/shared/contracts/re_review/legacy/v1_0/traceability.schema.json +841 -0
  961. package/vendor/ars/shared/contracts/re_review/legacy/v1_0/verdict_record.schema.json +244 -0
  962. package/vendor/ars/shared/contracts/re_review/precommitment.schema.json +193 -0
  963. package/vendor/ars/shared/contracts/re_review/traceability.schema.json +916 -0
  964. package/vendor/ars/shared/contracts/re_review/verdict_record.schema.json +245 -0
  965. package/vendor/ars/shared/contracts/research_workflow/inquiry_branch_ledger.schema.json +449 -0
  966. package/vendor/ars/shared/contracts/research_workflow/research_workflow_profile.schema.json +222 -0
  967. package/vendor/ars/shared/contracts/research_workflow/research_workflow_profile_selection_receipt.schema.json +111 -0
  968. package/vendor/ars/shared/contracts/review_target/constructive_review_findings.schema.json +172 -0
  969. package/vendor/ars/shared/contracts/review_target/criteria_registry.schema.json +97 -0
  970. package/vendor/ars/shared/contracts/review_target/review_criteria_binding_manifest.schema.json +235 -0
  971. package/vendor/ars/shared/contracts/review_target/review_criteria_source_receipt.schema.json +158 -0
  972. package/vendor/ars/shared/contracts/review_target/review_target_context.schema.json +106 -0
  973. package/vendor/ars/shared/contracts/review_target/review_target_declaration.schema.json +118 -0
  974. package/vendor/ars/shared/contracts/reviewer/full.json +114 -0
  975. package/vendor/ars/shared/contracts/reviewer/methodology_focus.json +75 -0
  976. package/vendor/ars/shared/contracts/reviewer/review_panel_provenance.schema.json +263 -0
  977. package/vendor/ars/shared/contracts/reviewer/review_panel_provenance_carrier.schema.json +122 -0
  978. package/vendor/ars/shared/contracts/reviewer/review_panel_provenance_input.schema.json +120 -0
  979. package/vendor/ars/shared/contracts/revision/author_adjudication.schema.json +251 -0
  980. package/vendor/ars/shared/contracts/revision/author_adjudication_input.schema.json +25 -0
  981. package/vendor/ars/shared/contracts/revision/claim_strength_drift_disposition.schema.json +97 -0
  982. package/vendor/ars/shared/contracts/revision/claim_strength_drift_disposition_input.schema.json +80 -0
  983. package/vendor/ars/shared/contracts/revision/claim_strength_drift_findings.schema.json +139 -0
  984. package/vendor/ars/shared/contracts/revision/claim_surface_manifest.schema.json +99 -0
  985. package/vendor/ars/shared/contracts/revision/integrity_correction_authorization.schema.json +140 -0
  986. package/vendor/ars/shared/contracts/revision/integrity_correction_authorization_input.schema.json +28 -0
  987. package/vendor/ars/shared/contracts/revision/integrity_correction_list.schema.json +48 -0
  988. package/vendor/ars/shared/contracts/revision/integrity_pass_receipt.schema.json +16 -0
  989. package/vendor/ars/shared/contracts/revision/revision_evidence_bundle.schema.json +134 -0
  990. package/vendor/ars/shared/contracts/revision/revision_roadmap.schema.json +334 -0
  991. package/vendor/ars/shared/contracts/submission/format_profile.example.yaml +33 -0
  992. package/vendor/ars/shared/contracts/submission/format_profile.schema.json +102 -0
  993. package/vendor/ars/shared/contracts/submission/submission_verification_report.schema.json +233 -0
  994. package/vendor/ars/shared/contracts/submission/venue_profile.schema.json +114 -0
  995. package/vendor/ars/shared/contracts/writer/full.json +87 -0
  996. package/vendor/ars/shared/cross_model_verification.md +714 -0
  997. package/vendor/ars/shared/evals_lift_report.schema.json +141 -0
  998. package/vendor/ars/shared/ground_truth_isolation_pattern.md +275 -0
  999. package/vendor/ars/shared/handoff_schemas.md +1209 -0
  1000. package/vendor/ars/shared/human_subjects_authority_registry.json +1278 -0
  1001. package/vendor/ars/shared/mode_spectrum.md +57 -0
  1002. package/vendor/ars/shared/model_tiering.md +83 -0
  1003. package/vendor/ars/shared/policy_data/nature_policy.md +56 -0
  1004. package/vendor/ars/shared/prisma_trAIce_protocol.md +157 -0
  1005. package/vendor/ars/shared/raise_framework.md +129 -0
  1006. package/vendor/ars/shared/references/authority_content_coverage_advisory_protocol.md +275 -0
  1007. package/vendor/ars/shared/references/claim_standing_candidate_ledger_protocol.md +66 -0
  1008. package/vendor/ars/shared/references/claim_strength_ladder.md +93 -0
  1009. package/vendor/ars/shared/references/cross_document_consistency_advisory_protocol.md +263 -0
  1010. package/vendor/ars/shared/references/evidence_row_protocol.md +260 -0
  1011. package/vendor/ars/shared/references/firm_rules.md +90 -0
  1012. package/vendor/ars/shared/references/human_subjects_authority_protocol.md +274 -0
  1013. package/vendor/ars/shared/references/intent_clarification_protocol.md +168 -0
  1014. package/vendor/ars/shared/references/irb_terminology_glossary.md +229 -0
  1015. package/vendor/ars/shared/references/protected_hedging_phrases.md +118 -0
  1016. package/vendor/ars/shared/references/psychometric_terminology_glossary.md +109 -0
  1017. package/vendor/ars/shared/references/review_criteria_consumer_protocol.md +238 -0
  1018. package/vendor/ars/shared/references/review_pathway_rule_trace_protocol.md +166 -0
  1019. package/vendor/ars/shared/references/submission_packet_manifest_protocol.md +292 -0
  1020. package/vendor/ars/shared/references/word_count_conventions.md +124 -0
  1021. package/vendor/ars/shared/research_workflow_profiles/field_general.json +1 -0
  1022. package/vendor/ars/shared/review_criteria_registry.json +207 -0
  1023. package/vendor/ars/shared/review_criteria_sources/msr-2027-technical-papers.2026-08-24.json +46 -0
  1024. package/vendor/ars/shared/review_criteria_sources/sigsoft-empirical-standards.2026-08-24.json +31 -0
  1025. package/vendor/ars/shared/sprint_contract.schema.json +482 -0
  1026. package/vendor/ars/shared/style_calibration_protocol.md +151 -0
  1027. package/vendor/ars/shared/templates/codex_audit_multifile_template.md +263 -0
  1028. package/vendor/ars/tools/release-discipline/.toolkit-version +1 -0
  1029. package/vendor/ars/tools/release-discipline/README.md +4 -0
  1030. package/vendor/ars/tools/release-discipline/scripts/_release_doc_alignment_schema.py +1011 -0
  1031. package/vendor/ars/tools/release-discipline/scripts/check_command_invariants.py +497 -0
  1032. package/vendor/ars/tools/release-discipline/scripts/check_release_doc_alignment.py +263 -0
  1033. package/vendor/ars/tools/release-discipline/scripts/sync-toolkit.sh +147 -0
  1034. package/vendor/windows/NOTICE.md +12 -0
  1035. package/vendor/windows/arm64/fd.exe +0 -0
  1036. package/vendor/windows/arm64/licenses/fd/LICENSE-APACHE +201 -0
  1037. package/vendor/windows/arm64/licenses/fd/LICENSE-MIT +21 -0
  1038. package/vendor/windows/arm64/licenses/ripgrep/COPYING +3 -0
  1039. package/vendor/windows/arm64/licenses/ripgrep/LICENSE-MIT +21 -0
  1040. package/vendor/windows/arm64/licenses/ripgrep/UNLICENSE +24 -0
  1041. package/vendor/windows/arm64/rg.exe +0 -0
  1042. package/vendor/windows/x64/fd.exe +0 -0
  1043. package/vendor/windows/x64/licenses/fd/LICENSE-APACHE +201 -0
  1044. package/vendor/windows/x64/licenses/fd/LICENSE-MIT +21 -0
  1045. package/vendor/windows/x64/licenses/ripgrep/COPYING +3 -0
  1046. package/vendor/windows/x64/licenses/ripgrep/LICENSE-MIT +21 -0
  1047. package/vendor/windows/x64/licenses/ripgrep/UNLICENSE +24 -0
  1048. package/vendor/windows/x64/rg.exe +0 -0
@@ -0,0 +1,4366 @@
1
+ """Tests for the six Schema 13.2 phase-conformance check families."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import re
6
+ import time
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ from scripts import check_panel_synthesis as panel
12
+ from scripts import check_phase_conformance as phase
13
+
14
+ REPO = Path(__file__).resolve().parents[1]
15
+ FULL_PATH = REPO / "shared/contracts/reviewer/full.json"
16
+ FULL = json.loads(FULL_PATH.read_text(encoding="utf-8"))
17
+
18
+
19
+ def phase1_text(role: str, overrides=None) -> str:
20
+ overrides = overrides or {}
21
+ # One paragraph per contract dimension: §4 reads
22
+ # `paraphrase_minimum_dimensions` ("all" in the full contract), and
23
+ # the real dispatch outputs carry exactly this shape, so a fixture
24
+ # with fewer paragraphs would stop being a valid Phase 1 at all.
25
+ lines = ["## Contract Paraphrase", ""]
26
+ for dim in FULL["acceptance_dimensions"]:
27
+ lines += [f"{dim['id']} concerns {dim['name']} as the contract "
28
+ "defines it.", ""]
29
+ lines += ["## Scoring Plan", ""]
30
+ for dim in FULL["acceptance_dimensions"]:
31
+ if role not in dim["eligible_roles"]:
32
+ continue
33
+ did = dim["id"]
34
+ fields = {
35
+ "dimension_id": did,
36
+ "what_to_look_for": f"observable evidence relevant to {did}",
37
+ "what_triggers_block":
38
+ f"block evidence pattern for {did} requiring major repair",
39
+ "what_triggers_warn":
40
+ f"warn evidence pattern for {did} requiring clarification",
41
+ }
42
+ if dim["priority"] == "mandatory":
43
+ fields["what_triggers_fatal"] = (
44
+ f"fatal evidence pattern for {did} invalidating the core"
45
+ )
46
+ fields.update(overrides.get(did, {}))
47
+ lines += [f"### {did}: {dim['name']}"]
48
+ lines += [f"{key}: {value}" for key, value in fields.items()
49
+ if value is not None]
50
+ lines.append("")
51
+ lines.append("[CONTRACT-ACKNOWLEDGED]")
52
+ return "\n".join(lines)
53
+
54
+
55
+ def phase2_text(
56
+ role: str, overrides=None, body="", dissent=(), receipts=None
57
+ ) -> str:
58
+ """Build a Phase 2 card; a methodology card carries its #610 receipt
59
+ section (default: the no-recomputable-statistics attestation), because a
60
+ methodology card without one stopped being conformant when the receipt
61
+ gate landed. Pass a list of section lines via ``receipts`` to override.
62
+ """
63
+ overrides = overrides or {}
64
+ lines = [f"contract_role: {role}", ""]
65
+ if dissent:
66
+ lines += ["## Scoring Plan Dissent", ""]
67
+ for did in dissent:
68
+ lines += [f"dimension_id: {did}", "rationale: plan was inadequate"]
69
+ lines.append("")
70
+ lines += ["## Dimension Scores", ""]
71
+ for dim in FULL["acceptance_dimensions"]:
72
+ did = dim["id"]
73
+ lines.append(f"### {did}: {dim['name']}")
74
+ if role not in dim["eligible_roles"]:
75
+ lines.append("score: not_assessed")
76
+ else:
77
+ value = overrides.get(did, "pass")
78
+ if value == "warn":
79
+ lines += [
80
+ "score: warn",
81
+ f'trigger: "warn evidence pattern for {did}"',
82
+ ]
83
+ elif value == "block":
84
+ lines += [
85
+ "score: block", "block_class: repairable",
86
+ f'trigger: "block evidence pattern for {did}"',
87
+ ]
88
+ elif value == "fatal":
89
+ lines += [
90
+ "score: block", "block_class: fatal",
91
+ f'trigger: "fatal evidence pattern for {did}"',
92
+ ]
93
+ elif value == "abstain":
94
+ lines += [
95
+ "score: not_assessed",
96
+ "abstain_reason: materially inapplicable",
97
+ ]
98
+ else:
99
+ lines.append("score: pass")
100
+ lines.append("")
101
+ lines += ["## Review Body", "", body]
102
+ if role == "methodology":
103
+ if receipts is None:
104
+ receipts = [
105
+ "no_recomputable_statistics: the fixture manuscript reports "
106
+ "no statistic covered by a bounded procedure",
107
+ ]
108
+ lines += ["", "## Arithmetic Receipts", "", *receipts]
109
+ return "\n".join(lines)
110
+
111
+
112
+ def parse_plan(role="methodology", overrides=None):
113
+ return phase.parse_phase1(
114
+ "p1.md", phase1_text(role, overrides), FULL, role
115
+ )
116
+
117
+
118
+ def parse_report(role="methodology", overrides=None, body="", dissent=()):
119
+ text = phase2_text(role, overrides, body, dissent)
120
+ return panel.parse_report("p2.md", text, FULL), text
121
+
122
+
123
+ def test_required_cli_flags_cannot_be_omitted():
124
+ with pytest.raises(SystemExit) as exc:
125
+ phase._parse_args(["--contract", str(FULL_PATH)])
126
+ assert exc.value.code == 2
127
+
128
+
129
+ @pytest.mark.parametrize("missing", ["--role", "--manuscript", "--metadata"])
130
+ def test_each_required_context_flag_fails_closed(tmp_path, missing):
131
+ args = write_cli_files(tmp_path, "methodology") + [
132
+ "--role", "methodology"
133
+ ]
134
+ index = args.index(missing)
135
+ del args[index:index + 2]
136
+ with pytest.raises(SystemExit) as exc:
137
+ phase._parse_args(args)
138
+ assert exc.value.code == 2
139
+
140
+
141
+ def test_invalid_dispatch_role_is_contract_error(tmp_path):
142
+ paths = write_cli_files(tmp_path, "methodology")
143
+ assert phase.main(paths + ["--role", "writer"]) == 2
144
+
145
+
146
+ def test_role_swap_is_conformance_failure(tmp_path):
147
+ paths = write_cli_files(tmp_path, "methodology")
148
+ assert phase.main(paths + ["--role", "eic"]) == 3
149
+
150
+
151
+ def test_missing_fatal_trigger_fails():
152
+ with pytest.raises(
153
+ phase.ConformanceError,
154
+ match=r"what_triggers_fatal: line for dimension D1, found 0",
155
+ ):
156
+ parse_plan("methodology", {"D1": {"what_triggers_fatal": None}})
157
+
158
+
159
+ def test_duplicate_scoring_plan_diagnostic_names_dimension():
160
+ text = phase1_text("methodology").replace(
161
+ "\n[CONTRACT-ACKNOWLEDGED]",
162
+ "\n### D3: argumentative_coherence\n[CONTRACT-ACKNOWLEDGED]",
163
+ )
164
+ with pytest.raises(
165
+ phase.ConformanceError,
166
+ match=r"duplicate scoring-plan subsection: D3: argumentative_coherence",
167
+ ):
168
+ phase.parse_phase1("p1.md", text, FULL, "methodology")
169
+
170
+
171
+ @pytest.mark.parametrize(
172
+ "overrides",
173
+ [
174
+ {"what_triggers_warn": "same", "what_triggers_block": "same"},
175
+ {"what_triggers_fatal": "same", "what_triggers_block": "same"},
176
+ {"what_triggers_fatal": "same", "what_triggers_warn": "same"},
177
+ ],
178
+ )
179
+ def test_all_three_trigger_collision_pairs_fail(overrides):
180
+ with pytest.raises(phase.ConformanceError, match="TRIGGER-COLLISION"):
181
+ parse_plan("methodology", {"D1": overrides})
182
+
183
+
184
+ def test_pairwise_distinct_triggers_pass():
185
+ assert set(parse_plan().commitments) == {"D1", "D3"}
186
+
187
+
188
+ def test_short_trigger_is_advisory_not_failure():
189
+ plan = parse_plan(
190
+ "methodology",
191
+ {"D1": {"what_triggers_warn": "short warning trigger"}},
192
+ )
193
+ assert any(
194
+ "D1 what_triggers_warn has fewer than 8 words" in warning
195
+ for warning in plan.warnings
196
+ )
197
+
198
+
199
+ @pytest.mark.parametrize(
200
+ "mutation",
201
+ [
202
+ lambda text: text.replace(
203
+ "what_triggers_fatal:", "- what_triggers_fatal:", 1
204
+ ),
205
+ lambda text: text.replace(
206
+ "what_triggers_fatal:", "what_triggers_fatal —", 1
207
+ ),
208
+ ],
209
+ )
210
+ def test_phase1_noncanonical_line_forms_fail(mutation):
211
+ text = mutation(phase1_text("methodology"))
212
+ with pytest.raises(phase.ConformanceError, match="PHASE1-GRAMMAR"):
213
+ phase.parse_phase1("p1.md", text, FULL, "methodology")
214
+
215
+
216
+ def test_canonical_phase1_line_form_passes():
217
+ parse_plan()
218
+
219
+
220
+ def test_malformed_fence_closer_keeps_phase1_plan_hidden():
221
+ text = "```text\n```not-a-close\n" + phase1_text("eic") + "\n```\n"
222
+ with pytest.raises(phase.ConformanceError, match="Scoring Plan required"):
223
+ phase.parse_phase1("p1.md", text, FULL, "eic")
224
+
225
+
226
+ @pytest.mark.parametrize("separator", ("\x85", "\u2028", "\u2029"))
227
+ def test_unicode_separator_keeps_phase1_plan_fenced(separator):
228
+ text = "```text\n```" + separator + phase1_text("eic") + "\n```\n"
229
+ with pytest.raises(phase.ConformanceError, match="Scoring Plan required"):
230
+ phase.parse_phase1("hidden-p1.md", text, FULL, "eic")
231
+
232
+
233
+ def test_manuscript_12_word_shingle_leaks():
234
+ manuscript = (
235
+ "alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu "
236
+ "nu xi"
237
+ )
238
+ leaked = phase1_text("methodology") + (
239
+ "\nalpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu"
240
+ )
241
+ with pytest.raises(phase.ConformanceError, match="MANUSCRIPT-LEAK"):
242
+ phase.check_manuscript_leakage(
243
+ leaked,
244
+ manuscript,
245
+ {"title": "Synthetic", "field": "testing", "word_count": 14},
246
+ FULL,
247
+ )
248
+
249
+
250
+ def test_metadata_title_shingle_is_exempt():
251
+ title = "alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu"
252
+ phase.check_manuscript_leakage(
253
+ phase1_text("methodology") + "\n" + title,
254
+ title + "\nBody words begin here.",
255
+ {"title": title, "field": "testing", "word_count": 4},
256
+ FULL,
257
+ )
258
+
259
+
260
+ @pytest.mark.parametrize(
261
+ "metadata",
262
+ [
263
+ {
264
+ "title": "Synthetic",
265
+ "field": "testing",
266
+ "word_count": 12,
267
+ "unexpected_body": (
268
+ "alpha beta gamma delta epsilon zeta eta theta iota "
269
+ "kappa lambda mu"
270
+ ),
271
+ },
272
+ {"title": "Synthetic", "field": "testing"},
273
+ {"title": "Synthetic", "field": "testing", "word_count": True},
274
+ ],
275
+ )
276
+ def test_metadata_envelope_shape_fails_closed(metadata):
277
+ with pytest.raises(phase.panel.ContractError, match="METADATA-INVALID"):
278
+ phase.validate_metadata_envelope(metadata)
279
+
280
+
281
+ def test_trigger_text_absent_from_phase1_fails():
282
+ report, _ = parse_report("methodology", {"D1": "warn"})
283
+ report.scores["D1"] = panel.DimensionScore(
284
+ "warn", trigger="a completely new threshold"
285
+ )
286
+ with pytest.raises(phase.ConformanceError, match="TRIGGER-DRIFT"):
287
+ phase.check_trigger_binding(
288
+ report, parse_plan(),
289
+ {dim["id"]: dim for dim in FULL["acceptance_dimensions"]}, set()
290
+ )
291
+
292
+
293
+ def test_trigger_binding_rechecks_required_trigger_defence_in_depth():
294
+ report, _ = parse_report("methodology", {"D1": "warn"})
295
+ report.scores["D1"] = panel.DimensionScore("warn")
296
+ with pytest.raises(phase.ConformanceError, match="TRIGGER-GRAMMAR"):
297
+ phase.check_trigger_binding(
298
+ report,
299
+ parse_plan(),
300
+ {dim["id"]: dim for dim in FULL["acceptance_dimensions"]},
301
+ set(),
302
+ )
303
+
304
+
305
+ @pytest.mark.parametrize(
306
+ "role,did,value",
307
+ (
308
+ ("methodology", "D1", panel.DimensionScore(
309
+ "pass", trigger="surplus post hoc trigger"
310
+ )),
311
+ ("eic", "D1", panel.DimensionScore(
312
+ "not_assessed", trigger="surplus structural trigger"
313
+ )),
314
+ ),
315
+ )
316
+ def test_trigger_binding_rechecks_surplus_trigger_defence_in_depth(
317
+ role, did, value
318
+ ):
319
+ report, _ = parse_report(role)
320
+ report.scores[did] = value
321
+ with pytest.raises(phase.ConformanceError, match="TRIGGER-GRAMMAR"):
322
+ phase.check_trigger_binding(
323
+ report,
324
+ parse_plan(role),
325
+ {dim["id"]: dim for dim in FULL["acceptance_dimensions"]},
326
+ set(),
327
+ )
328
+
329
+
330
+ def test_fatal_block_cannot_bind_warn_trigger():
331
+ report, _ = parse_report("methodology", {"D1": "fatal"})
332
+ report.scores["D1"] = panel.DimensionScore(
333
+ "block", "fatal", "warn evidence pattern for D1"
334
+ )
335
+ with pytest.raises(phase.ConformanceError, match="TRIGGER-DRIFT"):
336
+ phase.check_trigger_binding(
337
+ report, parse_plan(),
338
+ {dim["id"]: dim for dim in FULL["acceptance_dimensions"]}, set()
339
+ )
340
+
341
+
342
+ @pytest.mark.parametrize(
343
+ "score,shared_fields",
344
+ [
345
+ ("fatal", ("what_triggers_block", "what_triggers_fatal")),
346
+ ("block", ("what_triggers_block", "what_triggers_warn")),
347
+ ("warn", ("what_triggers_warn", "what_triggers_fatal")),
348
+ ],
349
+ )
350
+ def test_trigger_substring_must_bind_one_kind_only(score, shared_fields):
351
+ shared = "shared evidence pattern appears"
352
+ overrides = {
353
+ "what_triggers_block":
354
+ "repairable evidence pattern requires bounded revision",
355
+ "what_triggers_warn":
356
+ "warning evidence pattern requires clarification only",
357
+ "what_triggers_fatal":
358
+ "fatal evidence pattern proves the core cannot recover",
359
+ }
360
+ for field in shared_fields:
361
+ overrides[field] = f"{shared} and then diverges for {field}"
362
+ plan = parse_plan("methodology", {"D1": overrides})
363
+ report, _ = parse_report("methodology", {"D1": score})
364
+ report.scores["D1"] = panel.DimensionScore(
365
+ "warn" if score == "warn" else "block",
366
+ "fatal" if score == "fatal" else (
367
+ "repairable" if score == "block" else None
368
+ ),
369
+ shared,
370
+ )
371
+ with pytest.raises(phase.ConformanceError, match="TRIGGER-AMBIGUOUS"):
372
+ phase.check_trigger_binding(
373
+ report, plan,
374
+ {dim["id"]: dim for dim in FULL["acceptance_dimensions"]},
375
+ set(),
376
+ )
377
+
378
+
379
+ def test_dissent_cannot_mint_fatality():
380
+ report, _ = parse_report(
381
+ "methodology", {"D1": "fatal"}, dissent=("D1",)
382
+ )
383
+ with pytest.raises(phase.ConformanceError, match="DISSENT-FATALITY"):
384
+ phase.check_trigger_binding(
385
+ report, parse_plan(),
386
+ {dim["id"]: dim for dim in FULL["acceptance_dimensions"]}, {"D1"}
387
+ )
388
+
389
+
390
+ def test_dissent_requires_rationale():
391
+ _, text = parse_report(
392
+ "methodology", {"D1": "block"}, dissent=("D1",)
393
+ )
394
+ text = text.replace("rationale: plan was inadequate\n", "")
395
+ with pytest.raises(phase.ConformanceError, match="requires one rationale"):
396
+ phase.parse_dissent_dimensions(text)
397
+
398
+
399
+ def test_late_dissent_fails_closed_at_cli_boundary(tmp_path):
400
+ args = write_cli_files(tmp_path, "methodology")
401
+ phase2_path = Path(args[args.index("--phase2") + 1])
402
+ text = phase2_text(
403
+ "methodology", {"D1": "block"}, dissent=("D1",)
404
+ )
405
+ dissent_block = (
406
+ "## Scoring Plan Dissent\n\n"
407
+ "dimension_id: D1\n"
408
+ "rationale: plan was inadequate\n\n"
409
+ )
410
+ text = text.replace(dissent_block, "", 1).replace(
411
+ "## Review Body",
412
+ f"{dissent_block}## Review Body",
413
+ 1,
414
+ ).replace(
415
+ 'trigger: "block evidence pattern for D1"',
416
+ 'trigger: "novel post hoc trigger"',
417
+ 1,
418
+ )
419
+ phase2_path.write_text(text, encoding="utf-8")
420
+ assert phase.main(args + ["--role", "methodology"]) == \
421
+ phase.EXIT_CONFORMANCE
422
+
423
+
424
+ def test_dissent_must_name_a_dimension_committed_by_the_seat():
425
+ report, _ = parse_report("methodology", dissent=("D2",))
426
+ with pytest.raises(
427
+ phase.ConformanceError, match="not committed by this seat"
428
+ ):
429
+ phase.check_trigger_binding(
430
+ report, parse_plan(),
431
+ {dim["id"]: dim for dim in FULL["acceptance_dimensions"]},
432
+ {"D2"},
433
+ )
434
+
435
+
436
+ def test_dissent_repairable_block_passes_binding_exemption():
437
+ report, _ = parse_report(
438
+ "methodology", {"D1": "block"}, dissent=("D1",)
439
+ )
440
+ phase.check_trigger_binding(
441
+ report, parse_plan(),
442
+ {dim["id"]: dim for dim in FULL["acceptance_dimensions"]}, {"D1"}
443
+ )
444
+
445
+
446
+ def test_two_dissent_dimensions_fail():
447
+ report, _ = parse_report(
448
+ "methodology", dissent=("D1", "D3")
449
+ )
450
+ with pytest.raises(phase.ConformanceError, match="multi_dissent"):
451
+ phase.check_trigger_binding(
452
+ report, parse_plan(),
453
+ {dim["id"]: dim for dim in FULL["acceptance_dimensions"]},
454
+ {"D1", "D3"},
455
+ )
456
+
457
+
458
+ def phase2_with_dissent_section(
459
+ body_lines, *, late=False, role="methodology", overrides=None
460
+ ) -> str:
461
+ """Splice a custom ## Scoring Plan Dissent section into a phase 2 card."""
462
+ text = phase2_text(role, overrides)
463
+ section = "\n".join(
464
+ ["## Scoring Plan Dissent", "", *body_lines, ""]
465
+ ) + "\n"
466
+ anchor = "## Review Body" if late else "## Dimension Scores"
467
+ return text.replace(anchor, f"{section}{anchor}", 1)
468
+
469
+
470
+ def test_empty_dissent_section_is_read_as_no_dissent():
471
+ text = phase2_with_dissent_section([])
472
+ assert phase.parse_dissent_dimensions(text).dimensions == set()
473
+
474
+
475
+ def test_empty_dissent_section_records_an_auditable_diagnostic():
476
+ text = phase2_with_dissent_section([])
477
+ diagnostics = phase.parse_dissent_dimensions(text).diagnostics
478
+ assert len(diagnostics) == 1
479
+ assert "[DISSENT-EMPTY-SECTION:" in diagnostics[0]
480
+
481
+
482
+ def test_observed_placeholder_retraction_prose_no_longer_aborts():
483
+ # Verbatim shape of the 2026-07-27 perspective Phase 2 abort (#609).
484
+ text = phase2_with_dissent_section([
485
+ "*(omitted — Phase 1 plan holds)*",
486
+ "",
487
+ "Wait — that placeholder is not permitted. Removing it.",
488
+ ])
489
+ assert phase.parse_dissent_dimensions(text).dimensions == set()
490
+
491
+
492
+ @pytest.mark.parametrize(
493
+ "placeholder", ["none", "omitted", "not applicable", "N/A"]
494
+ )
495
+ def test_placeholder_words_are_not_a_dissent(placeholder):
496
+ text = phase2_with_dissent_section([placeholder])
497
+ parsed = phase.parse_dissent_dimensions(text)
498
+ assert parsed.dimensions == set()
499
+ assert len(parsed.diagnostics) == 1
500
+
501
+
502
+ def test_empty_dissent_section_after_dimension_scores_is_tolerated():
503
+ text = phase2_with_dissent_section([], late=True)
504
+ parsed = phase.parse_dissent_dimensions(text)
505
+ assert parsed.dimensions == set()
506
+ assert len(parsed.diagnostics) == 1
507
+
508
+
509
+ def test_canonical_dissent_still_parses_without_a_diagnostic():
510
+ _, text = parse_report("methodology", {"D1": "block"}, dissent=("D1",))
511
+ parsed = phase.parse_dissent_dimensions(text)
512
+ assert parsed.dimensions == {"D1"}
513
+ assert parsed.diagnostics == []
514
+
515
+
516
+ def test_claimed_dissent_without_dimension_id_still_aborts():
517
+ text = phase2_with_dissent_section(["rationale: plan was inadequate"])
518
+ with pytest.raises(
519
+ phase.ConformanceError, match="must name dimension_id"
520
+ ):
521
+ phase.parse_dissent_dimensions(text)
522
+
523
+
524
+ def test_duplicate_dissent_dimension_ids_still_abort():
525
+ text = phase2_with_dissent_section([
526
+ "dimension_id: D1",
527
+ "rationale: plan was inadequate",
528
+ "dimension_id: D1",
529
+ "rationale: plan was inadequate again",
530
+ ])
531
+ with pytest.raises(
532
+ phase.ConformanceError, match="duplicate dimension_id"
533
+ ):
534
+ phase.parse_dissent_dimensions(text)
535
+
536
+
537
+ def test_claimed_dissent_after_dimension_scores_still_aborts():
538
+ text = phase2_with_dissent_section(
539
+ ["dimension_id: D1", "rationale: plan was inadequate"], late=True
540
+ )
541
+ with pytest.raises(
542
+ phase.ConformanceError, match="must precede"
543
+ ):
544
+ phase.parse_dissent_dimensions(text)
545
+
546
+
547
+ def test_duplicate_empty_dissent_headings_still_abort():
548
+ text = phase2_with_dissent_section([])
549
+ text = text.replace(
550
+ "## Scoring Plan Dissent",
551
+ "## Scoring Plan Dissent\n\n## Scoring Plan Dissent",
552
+ 1,
553
+ )
554
+ with pytest.raises(
555
+ phase.ConformanceError, match="duplicate ## Scoring Plan Dissent"
556
+ ):
557
+ phase.parse_dissent_dimensions(text)
558
+
559
+
560
+ def test_absent_dissent_section_emits_no_diagnostic():
561
+ parsed = phase.parse_dissent_dimensions(phase2_text("methodology"))
562
+ assert parsed.dimensions == set()
563
+ assert parsed.diagnostics == []
564
+
565
+
566
+ def test_empty_section_diagnostic_reports_the_non_blank_line_count():
567
+ text = phase2_with_dissent_section(["prose one", "", "prose two"])
568
+ diagnostic = phase.parse_dissent_dimensions(text).diagnostics[0]
569
+ assert "2 non-blank line(s)" in diagnostic
570
+
571
+
572
+ @pytest.mark.parametrize("field_line", [
573
+ "- dimension_id: D1",
574
+ "* dimension_id: D1",
575
+ "+ dimension_id: D1",
576
+ "> dimension_id: D1",
577
+ "1. dimension_id: D1",
578
+ "**dimension_id**: D1",
579
+ "`dimension_id`: D1",
580
+ " dimension_id: D1",
581
+ "dimension_id:D1",
582
+ "- [ ] dimension_id: D1",
583
+ "- [x] dimension_id: D1",
584
+ "* [X] rationale: the phase 1 plan was inadequate",
585
+ "| dimension_id: D1 |",
586
+ "[dimension_id]: D1",
587
+ "_dimension_id_: D1",
588
+ "dimension id: D1",
589
+ "- rationale: the phase 1 plan was inadequate",
590
+ "**rationale**: the phase 1 plan was inadequate",
591
+ "| rationale: the phase 1 plan was inadequate |",
592
+ "[dimension_id](#dissent): D1",
593
+ "[rationale][note]: the phase 1 plan was inadequate",
594
+ "<b>dimension_id</b>: D1",
595
+ "[dimension_id](https://example.com/a:b): D1",
596
+ "[rationale](https://example.com): the phase 1 plan was inadequate",
597
+ "> | dimension_id: D1 |",
598
+ "\tdimension_id: D1",
599
+ ])
600
+ def test_non_canonical_dissent_field_shape_aborts(field_line):
601
+ text = phase2_with_dissent_section([field_line])
602
+ with pytest.raises(
603
+ phase.ConformanceError,
604
+ match="DISSENT-HIDDEN|DISSENT-RAW-HTML|canonical unbulleted",
605
+ ):
606
+ phase.parse_dissent_dimensions(text)
607
+
608
+
609
+ @pytest.mark.parametrize("hidden", [
610
+ ["```", "dimension_id: D1", "rationale: plan was inadequate", "```"],
611
+ ["<!-- dimension_id: D1 -->", "<!-- rationale: plan was inadequate -->"],
612
+ ["## dimension_id: D1", "## rationale: plan was inadequate"],
613
+ ])
614
+ def test_a_dissent_hidden_from_the_sanitizers_still_aborts(hidden):
615
+ """Fences, comments and headings must not launder a dissent field."""
616
+ with pytest.raises(
617
+ phase.ConformanceError, match="DISSENT-HIDDEN|canonical unbulleted"
618
+ ):
619
+ phase.parse_dissent_dimensions(phase2_with_dissent_section(hidden))
620
+
621
+
622
+ @pytest.mark.parametrize("wrapper", [
623
+ ("## dimension_id: D3", "## rationale: second plan was inadequate"),
624
+ ("<!-- dimension_id: D3 -->", "<!-- rationale: second -->"),
625
+ ("```", "dimension_id: D3", "rationale: second", "```"),
626
+ ])
627
+ def test_a_canonical_dissent_cannot_hide_a_second_laundered_one(wrapper):
628
+ text = phase2_with_dissent_section([
629
+ "dimension_id: D1",
630
+ "rationale: plan was inadequate",
631
+ *wrapper,
632
+ ])
633
+ with pytest.raises(
634
+ phase.ConformanceError, match="DISSENT-HIDDEN|canonical unbulleted"
635
+ ):
636
+ phase.parse_dissent_dimensions(text)
637
+
638
+
639
+ @pytest.mark.parametrize("body", [
640
+ ["```", "(omitted — the Phase 1 plan holds)", "```"],
641
+ ["<!-- no dissent; the Phase 1 plan holds -->"],
642
+ ])
643
+ def test_a_wrapper_carrying_no_field_stays_tolerated(body):
644
+ parsed = phase.parse_dissent_dimensions(phase2_with_dissent_section(body))
645
+ assert parsed.dimensions == set()
646
+ assert len(parsed.diagnostics) == 1
647
+
648
+
649
+ def test_a_field_shaped_heading_outside_the_span_is_not_a_dissent():
650
+ """parse_report permits extra H2 sections; they are not dissent fields."""
651
+ text = phase2_with_dissent_section(["*(omitted)*"]).replace(
652
+ "## Review Body", "## Rationale: Additional notes\n\nprose\n\n"
653
+ "## Review Body", 1
654
+ )
655
+ parsed = phase.parse_dissent_dimensions(text)
656
+ assert parsed.dimensions == set()
657
+ assert len(parsed.diagnostics) == 1
658
+
659
+
660
+ def test_a_fenced_heading_does_not_end_the_scanned_span():
661
+ text = phase2_with_dissent_section([
662
+ "dimension_id: D1",
663
+ "rationale: plan was inadequate",
664
+ "```",
665
+ "## Notes",
666
+ "dimension_id: D3",
667
+ "rationale: second plan was inadequate",
668
+ "```",
669
+ ])
670
+ with pytest.raises(
671
+ phase.ConformanceError, match="DISSENT-HIDDEN|canonical unbulleted"
672
+ ):
673
+ phase.parse_dissent_dimensions(text)
674
+
675
+
676
+ @pytest.mark.parametrize("sample", [
677
+ "a\n```\nb\n```\nc",
678
+ "a\n~~~\nb\n~~~\nc",
679
+ "a\n````\nb\n```\nc\n````\nd",
680
+ "a\n```py`\nb\n",
681
+ "a\n ```\nb\n```\nc",
682
+ "a\n```\nb",
683
+ "x\r\ny\n```\nz\n```\n",
684
+ ])
685
+ def test_local_fence_state_agrees_with_the_shared_stripper(sample):
686
+ """Anti-drift: the mirrored bookkeeping must track panel.strip_fences."""
687
+ unfenced = [
688
+ line for line, fenced in phase._lines_with_fence_state(sample)
689
+ if not fenced
690
+ ]
691
+ assert unfenced == panel.strip_fences(sample)
692
+
693
+
694
+ @pytest.mark.parametrize("fence", ["```", "~~~", "````"])
695
+ def test_a_fenced_structural_heading_does_not_end_the_scanned_span(fence):
696
+ """A real section title inside a fence is inert, not a boundary."""
697
+ text = phase2_with_dissent_section([
698
+ "dimension_id: D1",
699
+ "rationale: plan was inadequate",
700
+ fence,
701
+ "## Dimension Scores",
702
+ "dimension_id: D3",
703
+ "rationale: second plan was inadequate",
704
+ fence,
705
+ ])
706
+ with pytest.raises(
707
+ phase.ConformanceError, match="DISSENT-HIDDEN|canonical unbulleted"
708
+ ):
709
+ phase.parse_dissent_dimensions(text)
710
+
711
+
712
+ def test_a_backtick_fence_with_a_backtick_info_string_is_not_a_fence():
713
+ """CommonMark: it opens no fence, so its lines stay ordinary content.
714
+
715
+ Treating it as one would hide the canonical dissent below it, which the
716
+ raw-span scan would then report as a laundered field.
717
+ """
718
+ text = phase2_with_dissent_section([
719
+ "```py`",
720
+ "dimension_id: D1",
721
+ "rationale: plan was inadequate",
722
+ ])
723
+ assert phase.parse_dissent_dimensions(text).dimensions == {"D1"}
724
+
725
+
726
+ def test_a_commented_heading_ends_the_span_and_credits_nothing():
727
+ """Documents an ACCEPTED miss, not a desired behaviour.
728
+
729
+ `split_sections` reads a commented heading as a real boundary, so the
730
+ fields below it land in that other section. Teaching the raw walk to
731
+ disagree cost four false aborts across review rounds and closed only this
732
+ miss, which grants the seat nothing: no dissent is credited, so no
733
+ trigger-binding exemption follows. Change deliberately, never incidentally.
734
+ """
735
+ text = phase2_with_dissent_section([
736
+ "<!--",
737
+ "## Notes",
738
+ "dimension_id: D1",
739
+ "rationale: plan was inadequate",
740
+ "-->",
741
+ ])
742
+ assert phase.parse_dissent_dimensions(text).dimensions == set()
743
+
744
+
745
+ def test_a_comment_marker_inside_a_fence_does_not_leak_the_span():
746
+ """Comment delimiters are inert inside a fence, as they are to Markdown.
747
+
748
+ Treating one as real would swallow every later heading, run the span into
749
+ ## Review Body, and abort a valid card on prose there.
750
+ """
751
+ text = phase2_with_dissent_section([
752
+ "```", "<!-- an unmatched marker in an example", "```",
753
+ ]).replace(
754
+ "## Review Body\n\n",
755
+ "## Review Body\n\nrationale: the seat explains itself here\n\n",
756
+ 1,
757
+ )
758
+ parsed = phase.parse_dissent_dimensions(text)
759
+ assert parsed.dimensions == set()
760
+ assert len(parsed.diagnostics) == 1
761
+
762
+
763
+ def test_an_inline_code_comment_marker_does_not_leak_the_span():
764
+ """CommonMark reads it as code; only a line-initial opener hides a head."""
765
+ text = phase2_with_dissent_section([
766
+ "The literal `<!--` token is discussed here without a closer.",
767
+ ]).replace(
768
+ "## Review Body\n\n",
769
+ "## Review Body\n\nrationale: the seat explains itself here\n\n",
770
+ 1,
771
+ )
772
+ parsed = phase.parse_dissent_dimensions(text)
773
+ assert parsed.dimensions == set()
774
+ assert len(parsed.diagnostics) == 1
775
+
776
+
777
+ def test_a_duplicate_field_hidden_in_a_fence_is_counted_not_matched():
778
+ """Identical strings do not launder: occurrences are compared."""
779
+ text = phase2_with_dissent_section([
780
+ "dimension_id: D1",
781
+ "rationale: plan was inadequate",
782
+ "```",
783
+ "dimension_id: D1",
784
+ "rationale: plan was inadequate",
785
+ "```",
786
+ ])
787
+ with pytest.raises(
788
+ phase.ConformanceError, match="DISSENT-HIDDEN|canonical unbulleted"
789
+ ):
790
+ phase.parse_dissent_dimensions(text)
791
+
792
+
793
+ def test_a_commented_out_dissent_is_not_credited_as_one():
794
+ """Pre-existing hole: strip_fences leaves comments, so canonical fields
795
+ inside `<!-- ... -->` were parsed as a real dissent and granted the
796
+ trigger-binding exemption, letting a drifting trigger through."""
797
+ text = phase2_with_dissent_section([
798
+ "<!--", "dimension_id: D1", "rationale: plan was inadequate", "-->",
799
+ ])
800
+ with pytest.raises(
801
+ phase.ConformanceError, match="DISSENT-HIDDEN|canonical unbulleted"
802
+ ):
803
+ phase.parse_dissent_dimensions(text)
804
+
805
+
806
+ def test_a_commented_out_dissent_cannot_excuse_a_drifting_trigger(tmp_path):
807
+ args = write_cli_files(tmp_path, "methodology")
808
+ phase2_path = Path(args[args.index("--phase2") + 1])
809
+ text = phase2_with_dissent_section(
810
+ ["<!--", "dimension_id: D1", "rationale: plan was inadequate", "-->"],
811
+ overrides={"D1": "block"},
812
+ ).replace(
813
+ 'trigger: "block evidence pattern for D1"',
814
+ 'trigger: "novel post hoc trigger never committed in phase 1"',
815
+ 1,
816
+ )
817
+ phase2_path.write_text(text, encoding="utf-8")
818
+ assert phase.main(args + ["--role", "methodology"]) == \
819
+ phase.EXIT_CONFORMANCE
820
+
821
+
822
+ @pytest.mark.parametrize("opener_line", [
823
+ "<!-- an aside --> <!--",
824
+ "<!-- an aside --><!--",
825
+ ])
826
+ def test_a_comment_reopened_on_its_opening_line_still_hides(opener_line):
827
+ """Comment state is resolved by delimiter ORDER, not by presence.
828
+
829
+ A line holding a closer AND a later opener leaves the comment open, so
830
+ reading it as closed credited the fields below as a real dissent and
831
+ handed back the trigger-binding exemption this scan exists to withhold.
832
+ """
833
+ text = phase2_with_dissent_section([
834
+ opener_line,
835
+ "dimension_id: D1",
836
+ "rationale: plan was inadequate",
837
+ "-->",
838
+ ])
839
+ with pytest.raises(
840
+ phase.ConformanceError, match="DISSENT-HIDDEN|canonical unbulleted"
841
+ ):
842
+ phase.parse_dissent_dimensions(text)
843
+
844
+
845
+ def test_a_comment_reopened_on_its_closing_line_still_hides():
846
+ """Same order rule while already inside one: the closer does not win
847
+ merely by appearing on a line that reopens after it."""
848
+ text = phase2_with_dissent_section([
849
+ "<!--",
850
+ "an aside",
851
+ "--> visible again <!--",
852
+ "dimension_id: D1",
853
+ "rationale: plan was inadequate",
854
+ "-->",
855
+ ])
856
+ with pytest.raises(
857
+ phase.ConformanceError, match="DISSENT-HIDDEN|canonical unbulleted"
858
+ ):
859
+ phase.parse_dissent_dimensions(text)
860
+
861
+
862
+ @pytest.mark.parametrize("opener", [
863
+ "- <!--", "* <!--", "+ <!--", "1. <!--", "1) <!--",
864
+ "> <!--", "> - <!--", " - <!--", "- <!--",
865
+ "- <!--", "> <!--", "> <!--",
866
+ "> \t<!--", " > \t<!--",
867
+ ])
868
+ def test_a_container_prefixed_opener_still_hides(opener):
869
+ """A block opener behind a list or quote marker is still a block opener.
870
+
871
+ Rendered through CommonMark, `- <!--` puts the fields that follow inside
872
+ raw HTML a reader never sees, and the bullet form needs no closer at all.
873
+ Crediting them handed the trigger-binding exemption to a dissent invisible
874
+ on the page, which is the hole the raw-span scan exists to close.
875
+ """
876
+ text = phase2_with_dissent_section([
877
+ opener,
878
+ "dimension_id: D1",
879
+ "rationale: plan was inadequate",
880
+ ])
881
+ with pytest.raises(
882
+ phase.ConformanceError, match="DISSENT-HIDDEN|canonical unbulleted"
883
+ ):
884
+ phase.parse_dissent_dimensions(text)
885
+
886
+
887
+ @pytest.mark.parametrize("indented", [
888
+ " - <!-- a draft this seat withdrew",
889
+ " > <!--",
890
+ " 1. <!--",
891
+ "\t- <!--",
892
+ "- <!--",
893
+ "> <!--",
894
+ "> - \t<!--",
895
+ " - \t<!--",
896
+ " 1. \t<!--",
897
+ ])
898
+ def test_an_indented_container_marker_aborts(indented):
899
+ """#613: a bare `<!--` inside the dissent span is out-of-grammar
900
+ wherever it appears (the delivered prompts require inline code for
901
+ any mention), so this shape now aborts loudly instead of the
902
+ pre-#613 credit this test used to pin. Whether the renderer would
903
+ have shown the fields is decided by the output grammar now, not by
904
+ block-structure modelling in the parser.
905
+ """
906
+ text = phase2_with_dissent_section([
907
+ indented,
908
+ "dimension_id: D1",
909
+ "rationale: plan was inadequate",
910
+ ])
911
+ with pytest.raises(phase.ConformanceError, match="DISSENT-HIDDEN"):
912
+ phase.parse_dissent_dimensions(text)
913
+
914
+
915
+ @pytest.mark.parametrize("marker", ["2.", "9)", "10.", " 2."])
916
+ def test_an_ordered_marker_mid_paragraph_aborts(marker):
917
+ """#613: a bare `<!--` inside the dissent span is out-of-grammar
918
+ wherever it appears (the delivered prompts require inline code for
919
+ any mention), so this shape now aborts loudly instead of the
920
+ pre-#613 credit this test used to pin. Whether the renderer would
921
+ have shown the fields is decided by the output grammar now, not by
922
+ block-structure modelling in the parser.
923
+ """
924
+ text = phase2_with_dissent_section([
925
+ "Reviewed the plan and stand by it.",
926
+ f"{marker} <!--",
927
+ "dimension_id: D1",
928
+ "rationale: plan was inadequate",
929
+ ])
930
+ with pytest.raises(phase.ConformanceError, match="DISSENT-HIDDEN"):
931
+ phase.parse_dissent_dimensions(text)
932
+
933
+
934
+ @pytest.mark.parametrize("marker", ["1.", "1)", "2.", "10.", "-", ">"])
935
+ def test_a_container_marker_at_a_block_start_still_opens(marker):
936
+ """After a blank line every marker opens a list or quote, so the comment
937
+ inside it hides the fields below and the card is refused.
938
+
939
+ A block start reached some other way (a thematic break, a heading, a
940
+ setext underline) is covered separately; one reached from an open
941
+ paragraph is not a block start at all for a non-1 ordered marker.
942
+ """
943
+ text = phase2_with_dissent_section([
944
+ f"{marker} <!--",
945
+ "dimension_id: D1",
946
+ "rationale: plan was inadequate",
947
+ ])
948
+ with pytest.raises(
949
+ phase.ConformanceError, match="DISSENT-HIDDEN|canonical unbulleted"
950
+ ):
951
+ phase.parse_dissent_dimensions(text)
952
+
953
+
954
+ @pytest.mark.parametrize("closer,marker", [
955
+ ("---", "2."), ("***", "9)"), ("___", "10."),
956
+ ("### an aside", "2."), ("#### deeper", "2."), ("#", "2."),
957
+ ("-", "2."),
958
+ ])
959
+ def test_a_paragraph_closing_line_restores_every_marker(closer, marker):
960
+ """A thematic break or an ATX heading ends the paragraph above it.
961
+
962
+ The line after one is at a genuine block start, where an ordered marker of
963
+ any start number opens a list and so a raw-HTML comment. Reading it with
964
+ the paragraph-restricted pattern credited fields no reader can see, a
965
+ bypass neither `main` nor the pre-paragraph-fix commit had.
966
+ """
967
+ text = phase2_with_dissent_section([
968
+ "Reviewed the plan and stand by it.",
969
+ closer,
970
+ f"{marker} <!--",
971
+ "dimension_id: D1",
972
+ "rationale: plan was inadequate",
973
+ "<!-- -->",
974
+ ])
975
+ with pytest.raises(
976
+ phase.ConformanceError, match="DISSENT-HIDDEN|canonical unbulleted"
977
+ ):
978
+ phase.parse_dissent_dimensions(text)
979
+
980
+
981
+ def test_a_deeply_nested_quote_line_resolves_without_backtracking():
982
+ """The opener test runs on every unfenced line, so it must stay linear.
983
+
984
+ An earlier spelling let a single space be claimed by either of two
985
+ adjacent optional-space groups, giving two ways to match each marker and
986
+ doubling the work per level: 20 markers already cost 0.17s and 30 would
987
+ have stalled the checker instead of returning a verdict.
988
+ """
989
+ line = "> " * 24 + "prose that never opens a comment"
990
+ started = time.perf_counter()
991
+ assert phase._opens_comment(line, paragraph_open=False) is False
992
+ assert time.perf_counter() - started < 0.5
993
+
994
+
995
+ @pytest.mark.parametrize("comment_block", [
996
+ ["<!-- an aside -->"],
997
+ ["<!--", "an aside", "-->"],
998
+ ["<!-- an aside", "-->"],
999
+ ])
1000
+ def test_a_comment_block_is_not_a_paragraph(comment_block):
1001
+ """Raw HTML is not a paragraph, so the line after a comment block is at a
1002
+ block start where any ordered marker opens a list.
1003
+
1004
+ Recording the comment's own lines as an open paragraph read the next
1005
+ `2. <!--` with the restricted pattern and credited the fields it hides.
1006
+ """
1007
+ text = phase2_with_dissent_section([
1008
+ *comment_block,
1009
+ "2. <!--",
1010
+ "dimension_id: D1",
1011
+ "rationale: plan was inadequate",
1012
+ ])
1013
+ with pytest.raises(
1014
+ phase.ConformanceError, match="DISSENT-HIDDEN|canonical unbulleted"
1015
+ ):
1016
+ phase.parse_dissent_dimensions(text)
1017
+
1018
+
1019
+ @pytest.mark.parametrize("nested", ["- 2. <!--", "> 2. <!--", "> - 2. <!--"])
1020
+ def test_a_nested_marker_after_a_paragraph_aborts(nested):
1021
+ """#613: a bare `<!--` inside the dissent span is out-of-grammar
1022
+ wherever it appears (the delivered prompts require inline code for
1023
+ any mention), so this shape now aborts loudly instead of the
1024
+ pre-#613 credit this test used to pin. Whether the renderer would
1025
+ have shown the fields is decided by the output grammar now, not by
1026
+ block-structure modelling in the parser.
1027
+ """
1028
+ text = phase2_with_dissent_section([
1029
+ "Standing by the plan.",
1030
+ nested,
1031
+ "dimension_id: D1",
1032
+ "rationale: plan was inadequate",
1033
+ ])
1034
+ with pytest.raises(phase.ConformanceError, match="DISSENT-HIDDEN"):
1035
+ phase.parse_dissent_dimensions(text)
1036
+
1037
+
1038
+ @pytest.mark.parametrize("orphan", ["==", "--", "=", "===="])
1039
+ def test_a_marker_after_an_orphan_setext_line_aborts(orphan):
1040
+ """#613: a bare `<!--` inside the dissent span is out-of-grammar
1041
+ wherever it appears (the delivered prompts require inline code for
1042
+ any mention), so this shape now aborts loudly instead of the
1043
+ pre-#613 credit this test used to pin. Whether the renderer would
1044
+ have shown the fields is decided by the output grammar now, not by
1045
+ block-structure modelling in the parser.
1046
+ """
1047
+ text = phase2_with_dissent_section([
1048
+ orphan,
1049
+ "2. <!--",
1050
+ "dimension_id: D1",
1051
+ "rationale: plan was inadequate",
1052
+ ])
1053
+ with pytest.raises(phase.ConformanceError, match="DISSENT-HIDDEN"):
1054
+ phase.parse_dissent_dimensions(text)
1055
+
1056
+
1057
+ @pytest.mark.parametrize("marker", ["-", "*", "+"])
1058
+ def test_a_lone_bullet_at_a_block_start_is_an_empty_list_item(marker):
1059
+ """With no paragraph above it, a bullet alone is an empty list item, so the
1060
+ next line is at a block start where any ordered marker opens a comment."""
1061
+ text = phase2_with_dissent_section([
1062
+ marker,
1063
+ "2. <!--",
1064
+ "dimension_id: D1",
1065
+ "rationale: plan was inadequate",
1066
+ "<!-- -->",
1067
+ ])
1068
+ with pytest.raises(
1069
+ phase.ConformanceError, match="DISSENT-HIDDEN|canonical unbulleted"
1070
+ ):
1071
+ phase.parse_dissent_dimensions(text)
1072
+
1073
+
1074
+ @pytest.mark.parametrize("marker", ["*", "+", "2.", "10)"])
1075
+ def test_a_marker_after_a_lone_list_marker_aborts(marker):
1076
+ """#613: a bare `<!--` inside the dissent span is out-of-grammar
1077
+ wherever it appears (the delivered prompts require inline code for
1078
+ any mention), so this shape now aborts loudly instead of the
1079
+ pre-#613 credit this test used to pin. Whether the renderer would
1080
+ have shown the fields is decided by the output grammar now, not by
1081
+ block-structure modelling in the parser.
1082
+ """
1083
+ text = phase2_with_dissent_section([
1084
+ "Standing by the plan.",
1085
+ marker,
1086
+ "2. <!--",
1087
+ "dimension_id: D1",
1088
+ "rationale: plan was inadequate",
1089
+ ])
1090
+ with pytest.raises(phase.ConformanceError, match="DISSENT-HIDDEN"):
1091
+ phase.parse_dissent_dimensions(text)
1092
+
1093
+
1094
+ def test_a_setext_underline_closes_the_paragraph_above_it():
1095
+ text = phase2_with_dissent_section([
1096
+ "An underlined heading",
1097
+ "===",
1098
+ "2. <!--",
1099
+ "dimension_id: D1",
1100
+ "rationale: plan was inadequate",
1101
+ "<!-- -->",
1102
+ ])
1103
+ with pytest.raises(
1104
+ phase.ConformanceError, match="DISSENT-HIDDEN|canonical unbulleted"
1105
+ ):
1106
+ phase.parse_dissent_dimensions(text)
1107
+
1108
+
1109
+ @pytest.mark.parametrize("whitespace", [" ", " ", " ", "\x0c"])
1110
+ def test_a_marker_after_exotic_whitespace_aborts(whitespace):
1111
+ """#613: a bare `<!--` inside the dissent span is out-of-grammar
1112
+ wherever it appears (the delivered prompts require inline code for
1113
+ any mention), so this shape now aborts loudly instead of the
1114
+ pre-#613 credit this test used to pin. Whether the renderer would
1115
+ have shown the fields is decided by the output grammar now, not by
1116
+ block-structure modelling in the parser.
1117
+ """
1118
+ text = phase2_with_dissent_section([
1119
+ "Reviewed the plan and stand by it.",
1120
+ whitespace,
1121
+ "2. <!--",
1122
+ "dimension_id: D1",
1123
+ "rationale: plan was inadequate",
1124
+ ])
1125
+ with pytest.raises(phase.ConformanceError, match="DISSENT-HIDDEN"):
1126
+ phase.parse_dissent_dimensions(text)
1127
+
1128
+
1129
+ @pytest.mark.parametrize("marker", ["1.", "1)", "-", "*", ">"])
1130
+ def test_a_paragraph_interrupting_marker_still_opens(marker):
1131
+ """A bullet, a blockquote, and an ordered list starting at 1 DO interrupt
1132
+ a paragraph, so those keep hiding the fields below them."""
1133
+ text = phase2_with_dissent_section([
1134
+ "Reviewed the plan and stand by it.",
1135
+ f"{marker} <!--",
1136
+ "dimension_id: D1",
1137
+ "rationale: plan was inadequate",
1138
+ ])
1139
+ with pytest.raises(
1140
+ phase.ConformanceError, match="DISSENT-HIDDEN|canonical unbulleted"
1141
+ ):
1142
+ phase.parse_dissent_dimensions(text)
1143
+
1144
+
1145
+ def test_a_balanced_container_prefixed_comment_hides_nothing_after():
1146
+ """Same order rule behind a marker: a closed aside leaves the dissent."""
1147
+ text = phase2_with_dissent_section([
1148
+ "- <!-- drafted and withdrawn -->",
1149
+ "dimension_id: D1",
1150
+ "rationale: plan was inadequate",
1151
+ ])
1152
+ assert phase.parse_dissent_dimensions(text).dimensions == {"D1"}
1153
+
1154
+
1155
+ @pytest.mark.parametrize("container,opener", [
1156
+ ("- an earlier note", "2. <!--"),
1157
+ ("- an earlier note", " <!--"),
1158
+ ("- an earlier note", "\t<!--"),
1159
+ ("- an earlier note", "- 2. <!--"),
1160
+ ("> an earlier note", "> 2. <!--"),
1161
+ ("> an earlier note", "> <!--"),
1162
+ ])
1163
+ def test_an_opener_inside_an_open_container_aborts(
1164
+ container, opener
1165
+ ):
1166
+ """#613: a bare `<!--` inside the dissent span is out-of-grammar
1167
+ wherever it appears (the delivered prompts require inline code for
1168
+ any mention), so this shape now aborts loudly instead of the
1169
+ pre-#613 credit this test used to pin. Whether the renderer would
1170
+ have shown the fields is decided by the output grammar now, not by
1171
+ block-structure modelling in the parser.
1172
+ """
1173
+ text = phase2_with_dissent_section([
1174
+ container,
1175
+ opener,
1176
+ "dimension_id: D1",
1177
+ "rationale: plan was inadequate",
1178
+ "-->",
1179
+ ])
1180
+ with pytest.raises(phase.ConformanceError, match="DISSENT-HIDDEN"):
1181
+ phase.parse_dissent_dimensions(text)
1182
+
1183
+
1184
+ def test_an_angle_bracket_field_label_is_read_as_prose():
1185
+ """A `<dimension_id>: D1` placeholder is stripped as a markup span, so it
1186
+ reads as prose and the section is tolerated instead of aborting.
1187
+
1188
+ Deliberate, and on the safe side of the asymmetry: nothing is credited, so
1189
+ no exemption follows, and leaving a template placeholder in is the very
1190
+ mistake #609 exists to stop from killing a panel.
1191
+ """
1192
+ text = phase2_with_dissent_section(["<dimension_id>: D1"])
1193
+ parsed = phase.parse_dissent_dimensions(text)
1194
+ assert parsed.dimensions == set()
1195
+ assert len(parsed.diagnostics) == 1
1196
+
1197
+
1198
+ def test_an_indented_opener_continuing_a_paragraph_aborts():
1199
+ """#613: a bare `<!--` inside the dissent span is out-of-grammar
1200
+ wherever it appears (the delivered prompts require inline code for
1201
+ any mention), so this shape now aborts loudly instead of the
1202
+ pre-#613 credit this test used to pin. Whether the renderer would
1203
+ have shown the fields is decided by the output grammar now, not by
1204
+ block-structure modelling in the parser.
1205
+ """
1206
+ text = phase2_with_dissent_section([
1207
+ "Reviewed the plan and stand by it.",
1208
+ " <!--",
1209
+ "dimension_id: D1",
1210
+ "rationale: plan was inadequate",
1211
+ "-->",
1212
+ ])
1213
+ with pytest.raises(phase.ConformanceError, match="DISSENT-HIDDEN"):
1214
+ phase.parse_dissent_dimensions(text)
1215
+
1216
+
1217
+ @pytest.mark.parametrize("line,entering,expected", [
1218
+ ("<!--", False, True),
1219
+ ("- <!--", False, True),
1220
+ ("> <!--", False, True),
1221
+ ("1. <!--", False, True),
1222
+ ("- <!-- a -->", False, False),
1223
+ ("- text <!--", False, False),
1224
+ (" <!--", False, False),
1225
+ (" - <!--", False, False),
1226
+ (" > <!--", False, False),
1227
+ ("- <!--", False, False),
1228
+ ("> <!--", False, False),
1229
+ (" - <!--", False, True),
1230
+ ("> <!--", False, True),
1231
+ ("- `<!--`", False, False),
1232
+ ("<!-->", False, False),
1233
+ ("<!--->", False, False),
1234
+ ("<!-- -->", False, False),
1235
+ ("<!--x-->", False, False),
1236
+ ("<!-- a --> <!--", False, True),
1237
+ ("<!-- a --><!--", False, True),
1238
+ ("<!-- a --> <!-- b -->", False, False),
1239
+ (" <!--", False, True),
1240
+ (" <!--", False, False),
1241
+ ("prose <!--", False, False),
1242
+ ("plain prose", False, False),
1243
+ ("still inside", True, True),
1244
+ ("--> out", True, False),
1245
+ ("--> out <!--", True, True),
1246
+ ("-->", True, False),
1247
+ ])
1248
+ def test_comment_state_after_resolves_by_order(line, entering, expected):
1249
+ """The state machine itself, pinned line by line.
1250
+
1251
+ Both directions matter: a state left open credits fields the reader never
1252
+ saw, and a state closed too eagerly aborts a card whose fields are in the
1253
+ clear. Neither is visible from the parse-level tests alone.
1254
+ """
1255
+ assert phase._comment_state_after(line, commented=entering) is expected
1256
+
1257
+
1258
+ @pytest.mark.parametrize("empty_comment", ["<!-->", "<!--->", "<!-- -->"])
1259
+ def test_a_commonmark_empty_comment_hides_nothing_after(empty_comment):
1260
+ """CommonMark closes `<!-->` and `<!--->` on their own dashes.
1261
+
1262
+ Ordering the delimiter scan must not read those as unterminated: doing so
1263
+ struck the fields below and aborted a card that presence-testing for a
1264
+ closer had passed, which is the failure this tolerance removes.
1265
+ """
1266
+ text = phase2_with_dissent_section([
1267
+ empty_comment,
1268
+ "dimension_id: D1",
1269
+ "rationale: plan was inadequate",
1270
+ ])
1271
+ assert phase.parse_dissent_dimensions(text).dimensions == {"D1"}
1272
+
1273
+
1274
+ def test_a_comment_closed_and_reopened_then_closed_hides_nothing_after():
1275
+ """The order rule cuts both ways: a line that ends outside a comment
1276
+ leaves the fields below visible, so a balanced aside cannot abort a
1277
+ valid card."""
1278
+ text = phase2_with_dissent_section([
1279
+ "<!-- drafted --> <!-- reviewed -->",
1280
+ "dimension_id: D1",
1281
+ "rationale: plan was inadequate",
1282
+ ])
1283
+ assert phase.parse_dissent_dimensions(text).dimensions == {"D1"}
1284
+
1285
+
1286
+ def test_a_comment_opened_after_prose_on_its_line_aborts():
1287
+ """#613: a bare `<!--` inside the dissent span is out-of-grammar
1288
+ wherever it appears (the delivered prompts require inline code for
1289
+ any mention), so this shape now aborts loudly instead of the
1290
+ pre-#613 credit this test used to pin. Whether the renderer would
1291
+ have shown the fields is decided by the output grammar now, not by
1292
+ block-structure modelling in the parser.
1293
+ """
1294
+ text = phase2_with_dissent_section([
1295
+ "an aside <!--",
1296
+ "dimension_id: D1",
1297
+ "rationale: plan was inadequate",
1298
+ "-->",
1299
+ ])
1300
+ with pytest.raises(phase.ConformanceError, match="DISSENT-HIDDEN"):
1301
+ phase.parse_dissent_dimensions(text)
1302
+
1303
+
1304
+ def test_an_unbackticked_marker_in_a_rationale_now_aborts_loudly():
1305
+ """#613 flips the #612 pin: the delivered output grammar makes a bare
1306
+ marker out-of-grammar prose (mentions go in inline code), so the
1307
+ mid-line opener is read as real — it swallows the second dissent, and
1308
+ the card aborts as unparsed occurrences instead of silently keeping
1309
+ D2's credit while CommonMark hides it."""
1310
+ text = phase2_with_dissent_section([
1311
+ "dimension_id: D1",
1312
+ "rationale: the card left an unclosed <!-- marker in its own output",
1313
+ "dimension_id: D2",
1314
+ "rationale: the second plan was inadequate too",
1315
+ ])
1316
+ with pytest.raises(phase.ConformanceError, match="DISSENT-HIDDEN"):
1317
+ phase.parse_dissent_dimensions(text)
1318
+
1319
+
1320
+ def test_a_canonical_rationale_may_mention_comment_syntax():
1321
+ """The sanctioned spelling: comment tokens in INLINE CODE are prose
1322
+ under the #613 output grammar — code spans are blanked before the
1323
+ inline-opener scan, so the mention neither opens a comment nor aborts."""
1324
+ text = phase2_with_dissent_section([
1325
+ "dimension_id: D1",
1326
+ "rationale: the seat wrote `<!--` and `-->` in its explanation",
1327
+ ])
1328
+ assert phase.parse_dissent_dimensions(text).dimensions == {"D1"}
1329
+
1330
+
1331
+ def test_an_inline_comment_that_closes_restores_the_parse():
1332
+ """A mid-line comment that opens AND closes leaves the following
1333
+ canonical fields rendered, so they parse normally — the inline state is
1334
+ delimiter-order-resolved, not presence-tested."""
1335
+ text = phase2_with_dissent_section([
1336
+ "Reviewed the plan. <!-- aside --> Standing by the dissent:",
1337
+ "dimension_id: D1",
1338
+ "rationale: plan was inadequate",
1339
+ ])
1340
+ assert phase.parse_dissent_dimensions(text).dimensions == {"D1"}
1341
+
1342
+
1343
+ def test_a_comment_opened_before_the_heading_credits_no_dissent():
1344
+ """The bypass: the fields parse into the dissent body, so they must not
1345
+ be credited merely because the opener sits above the heading."""
1346
+ text = phase2_text("methodology").replace(
1347
+ "## Dimension Scores",
1348
+ "<!--\n\n## Scoring Plan Dissent\n\ndimension_id: D1\n"
1349
+ "rationale: plan was inadequate\n\n-->\n\n## Dimension Scores",
1350
+ 1,
1351
+ )
1352
+ with pytest.raises(
1353
+ phase.ConformanceError, match="DISSENT-HIDDEN|canonical unbulleted"
1354
+ ):
1355
+ phase.parse_dissent_dimensions(text)
1356
+
1357
+
1358
+ def test_an_indented_comment_marker_aborts():
1359
+ """#613: a bare `<!--` inside the dissent span is out-of-grammar
1360
+ wherever it appears (the delivered prompts require inline code for
1361
+ any mention), so this shape now aborts loudly instead of the
1362
+ pre-#613 credit this test used to pin. Whether the renderer would
1363
+ have shown the fields is decided by the output grammar now, not by
1364
+ block-structure modelling in the parser.
1365
+ """
1366
+ text = phase2_with_dissent_section([
1367
+ " <!-- an indented example with no closer",
1368
+ "dimension_id: D1",
1369
+ "rationale: plan was inadequate",
1370
+ ])
1371
+ with pytest.raises(phase.ConformanceError, match="DISSENT-HIDDEN"):
1372
+ phase.parse_dissent_dimensions(text)
1373
+
1374
+
1375
+ def test_a_nested_paren_link_destination_is_a_declared_limit():
1376
+ """Documents an ACCEPTED miss, not a desired behaviour.
1377
+
1378
+ Link destinations are unwrapped one nesting level deep. A deeper one
1379
+ leaves destination letters on the label, so the field reads as prose and
1380
+ is absorbed with the advisory diagnostic. Chasing it further is declined
1381
+ on the stated asymmetry: a miss costs one flagged record, while every
1382
+ broadening of the rule risks the false aborts this tolerance removes.
1383
+ Change this test deliberately, never incidentally.
1384
+ """
1385
+ for line in (
1386
+ "[dimension_id](https://e/x_(y_(z))w): D1",
1387
+ "dimension_id&#58; D1",
1388
+ ):
1389
+ text = phase2_with_dissent_section([line])
1390
+ assert phase.parse_dissent_dimensions(text).dimensions == set()
1391
+
1392
+
1393
+ def test_a_quoted_attribute_raw_html_field_now_aborts():
1394
+ """#682 closes the raw-HTML half of the old declared limit.
1395
+
1396
+ The field-shape helper still need not parse quoted ``>`` attributes: the
1397
+ span guard rejects the tag itself before an empty-section advisory could
1398
+ grant any exemption.
1399
+ """
1400
+ text = phase2_with_dissent_section([
1401
+ '<span title="x>y">dimension_id</span>: D1',
1402
+ ])
1403
+ with pytest.raises(phase.ConformanceError, match="DISSENT-RAW-HTML"):
1404
+ phase.parse_dissent_dimensions(text)
1405
+
1406
+
1407
+ def test_one_nesting_level_in_a_link_destination_still_aborts():
1408
+ text = phase2_with_dissent_section([
1409
+ "[dimension_id](https://e/x_(y)z): D1",
1410
+ ])
1411
+ with pytest.raises(
1412
+ phase.ConformanceError, match="DISSENT-HIDDEN|canonical unbulleted"
1413
+ ):
1414
+ phase.parse_dissent_dimensions(text)
1415
+
1416
+
1417
+ def test_the_diagnostic_counts_fenced_placeholder_prose():
1418
+ text = phase2_with_dissent_section([
1419
+ "```", "(omitted — the Phase 1 plan holds)", "```",
1420
+ ])
1421
+ diagnostic = phase.parse_dissent_dimensions(text).diagnostics[0]
1422
+ assert "0 non-blank line(s)" not in diagnostic
1423
+
1424
+
1425
+ def test_a_fenced_block_elsewhere_leaves_the_dissent_section_alone():
1426
+ text = phase2_with_dissent_section(["*(omitted)*"]).replace(
1427
+ "## Review Body", "## Review Body\n\n```\ndimension_id: D1\n```", 1
1428
+ )
1429
+ parsed = phase.parse_dissent_dimensions(text)
1430
+ assert parsed.dimensions == set()
1431
+ assert len(parsed.diagnostics) == 1
1432
+
1433
+
1434
+ def test_bulleted_multi_dissent_cannot_bypass_the_cardinality_gate():
1435
+ text = phase2_with_dissent_section([
1436
+ "- dimension_id: D1",
1437
+ "- rationale: first plan was inadequate",
1438
+ "- dimension_id: D3",
1439
+ "- rationale: second plan was inadequate",
1440
+ ])
1441
+ with pytest.raises(
1442
+ phase.ConformanceError, match="DISSENT-HIDDEN|canonical unbulleted"
1443
+ ):
1444
+ phase.parse_dissent_dimensions(text)
1445
+
1446
+
1447
+ @pytest.mark.parametrize("wrapper", [
1448
+ ("- dimension_id: D3", "- rationale: second plan was inadequate"),
1449
+ ("| dimension_id: D3 |", "| rationale: second plan was inadequate |"),
1450
+ ("- [ ] dimension_id: D3", "- [ ] rationale: second plan was inadequate"),
1451
+ ("[dimension_id](#d): D3", "[rationale](#d): second plan was inadequate"),
1452
+ ("[dimension_id](https://e.com): D3",
1453
+ "[rationale](https://e.com): second"),
1454
+ ])
1455
+ def test_a_canonical_dissent_cannot_hide_a_second_decorated_one(wrapper):
1456
+ text = phase2_with_dissent_section([
1457
+ "dimension_id: D1",
1458
+ "rationale: plan was inadequate",
1459
+ *wrapper,
1460
+ ])
1461
+ with pytest.raises(
1462
+ phase.ConformanceError, match="DISSENT-HIDDEN|canonical unbulleted"
1463
+ ):
1464
+ phase.parse_dissent_dimensions(text)
1465
+
1466
+
1467
+ @pytest.mark.parametrize("prose", [
1468
+ "No dissent, so there is no dimension_id: line under this heading.",
1469
+ "I dissent from D1: the plan held after all, so nothing is claimed.",
1470
+ "Note: the Phase 1 plan holds.",
1471
+ "Dissent: none.",
1472
+ "無異議,dimension_id 已省略:Phase 1 計畫維持不變",
1473
+ "異議なし:dimension_id は省略しました。",
1474
+ "이견 없음: dimension_id 줄은 생략했습니다.",
1475
+ ])
1476
+ def test_prose_carrying_a_colon_stays_tolerated(prose):
1477
+ parsed = phase.parse_dissent_dimensions(
1478
+ phase2_with_dissent_section([prose])
1479
+ )
1480
+ assert parsed.dimensions == set()
1481
+ assert len(parsed.diagnostics) == 1
1482
+
1483
+
1484
+ def test_tolerated_empty_dissent_section_still_binds_every_trigger(tmp_path):
1485
+ args = write_cli_files(tmp_path, "methodology")
1486
+ phase2_path = Path(args[args.index("--phase2") + 1])
1487
+ text = phase2_with_dissent_section(
1488
+ ["*(omitted)*"], overrides={"D1": "block"}
1489
+ ).replace(
1490
+ 'trigger: "block evidence pattern for D1"',
1491
+ 'trigger: "novel post hoc trigger never committed in phase 1"',
1492
+ 1,
1493
+ )
1494
+ phase2_path.write_text(text, encoding="utf-8")
1495
+ assert phase.main(args + ["--role", "methodology"]) == \
1496
+ phase.EXIT_CONFORMANCE
1497
+
1498
+
1499
+ def test_cli_passes_and_prints_diagnostic_for_empty_dissent_section(
1500
+ tmp_path, capsys
1501
+ ):
1502
+ args = write_cli_files(tmp_path, "methodology")
1503
+ phase2_path = Path(args[args.index("--phase2") + 1])
1504
+ phase2_path.write_text(
1505
+ phase2_with_dissent_section([]), encoding="utf-8"
1506
+ )
1507
+ assert phase.main(args + ["--role", "methodology"]) == phase.EXIT_PASS
1508
+ assert "[DISSENT-EMPTY-SECTION:" in capsys.readouterr().out
1509
+
1510
+
1511
+ def test_fatal_trigger_for_nonmandatory_dimension_fails():
1512
+ text = phase1_text("eic").replace(
1513
+ "what_triggers_warn: warn evidence pattern for D5 requiring clarification",
1514
+ "what_triggers_warn: warn evidence pattern for D5 requiring clarification\n"
1515
+ "what_triggers_fatal: forbidden fatal trigger",
1516
+ )
1517
+ with pytest.raises(phase.ConformanceError, match="forbidden"):
1518
+ phase.parse_phase1("p1.md", text, FULL, "eic")
1519
+
1520
+
1521
+ def test_nonmandatory_block_class_fails_phase_checker(tmp_path):
1522
+ args = write_cli_files(tmp_path, "perspective")
1523
+ phase2_path = Path(args[args.index("--phase2") + 1])
1524
+ text = phase2_path.read_text(encoding="utf-8").replace(
1525
+ "### D4: cross_disciplinary_relevance\nscore: pass",
1526
+ "### D4: cross_disciplinary_relevance\n"
1527
+ "score: block\n"
1528
+ "block_class: repairable\n"
1529
+ 'trigger: "block trigger"',
1530
+ )
1531
+ phase2_path.write_text(text, encoding="utf-8")
1532
+ assert phase.main(args + ["--role", "perspective"]) == 3
1533
+
1534
+
1535
+ @pytest.mark.parametrize(
1536
+ "body",
1537
+ [
1538
+ "### W1: no anchor\n**Severity**: Critical\n**Problem**: no anchor",
1539
+ "### W1: long quote\n**Severity**: Major\n**Evidence Anchor**: text: "
1540
+ "\"one two three four five six seven eight nine ten eleven twelve "
1541
+ "thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty "
1542
+ "twentyone twentytwo twentythree twentyfour twentyfive twentysix\"",
1543
+ "### W1: empty absence\n**Severity**: Critical\n"
1544
+ "**Evidence Anchor**: absence:",
1545
+ "### W1: incomplete absence\n**Severity**: Critical\n"
1546
+ "**Evidence Anchor**: absence: x",
1547
+ "### W1: missing expected item\n**Severity**: Critical\n"
1548
+ "**Evidence Anchor**: absence: Methods — expected ; checked appendix",
1549
+ "### W1: missing checked surfaces\n**Severity**: Critical\n"
1550
+ "**Evidence Anchor**: absence: Methods — expected ethics statement; checked",
1551
+ "### W1: missing literal separator space\n**Severity**: Critical\n"
1552
+ "**Evidence Anchor**: absence: Methods — expected ethics statement;checked appendix",
1553
+ "### W1: doubled separator space\n**Severity**: Critical\n"
1554
+ "**Evidence Anchor**: absence: Methods — expected ethics statement; checked appendix",
1555
+ "### W1: repeated separators\n**Severity**: Critical\n"
1556
+ "**Evidence Anchor**: absence: Methods — expected ; checked appendix "
1557
+ "— expected ethics statement; checked supplement",
1558
+ "### W1: reversed separators\n**Severity**: Critical\n"
1559
+ "**Evidence Anchor**: absence: Methods; checked appendix — expected ethics statement",
1560
+ ],
1561
+ )
1562
+ def test_critical_major_anchor_failures(body):
1563
+ report, _ = parse_report("eic", body=body)
1564
+ with pytest.raises(phase.ConformanceError):
1565
+ phase.check_scoring_seat_anchors(report)
1566
+
1567
+
1568
+ @pytest.mark.parametrize(
1569
+ "body",
1570
+ [
1571
+ '### W1: quoted defect\n**Severity**: Critical\n'
1572
+ '**Evidence Anchor**: text: "short exact quote" p. 2',
1573
+ "### W1: missing surfaces\n**Severity**: Major\n"
1574
+ "**Evidence Anchor**: absence: Methods — expected an ethics statement; "
1575
+ "checked Methods, appendix, and supplement",
1576
+ ],
1577
+ )
1578
+ def test_compliant_critical_major_anchors_pass(body):
1579
+ report, _ = parse_report("eic", body=body)
1580
+ phase.check_scoring_seat_anchors(report)
1581
+
1582
+
1583
+ def test_two_findings_cannot_share_one_anchor():
1584
+ body = (
1585
+ "### W1: first\n"
1586
+ "**Severity**: Critical\n"
1587
+ '**Evidence Anchor**: text: "first quote" p. 1\n'
1588
+ "**Severity**: Major\n"
1589
+ "### W2: second\n"
1590
+ "**Severity**: Major"
1591
+ )
1592
+ report, _ = parse_report("eic", body=body)
1593
+ with pytest.raises(phase.ConformanceError):
1594
+ phase.check_scoring_seat_anchors(report)
1595
+
1596
+
1597
+ def test_two_independently_anchored_findings_pass():
1598
+ body = (
1599
+ "### W1: first\n"
1600
+ "**Severity**: Critical\n"
1601
+ '**Evidence Anchor**: text: "first quote" p. 1\n'
1602
+ "### W2: second\n"
1603
+ "**Severity**: Major\n"
1604
+ "**Evidence Anchor**: absence: Methods — expected an ethics statement; "
1605
+ "checked Methods and appendix"
1606
+ )
1607
+ report, _ = parse_report("eic", body=body)
1608
+ phase.check_scoring_seat_anchors(report)
1609
+
1610
+
1611
+ def test_multiple_minor_severities_in_one_finding_fail():
1612
+ body = """### W1: bundled minor findings
1613
+ **Severity**: Minor
1614
+ first
1615
+ **Severity**: Minor
1616
+ second"""
1617
+ report, _ = parse_report("eic", body=body)
1618
+ with pytest.raises(phase.ConformanceError, match="FINDING-GRAMMAR"):
1619
+ phase.check_scoring_seat_anchors(report)
1620
+
1621
+
1622
+ def test_same_line_duplicate_severity_declarations_fail():
1623
+ """Unchanged by #637: a mid-line second declaration is a declaration
1624
+ (`_SEVERITY_DECL_RE`) whose value cannot parse mid-prose, so the
1625
+ declared-but-unparseable guard still aborts. Only cross-line
1626
+ supersession takes last-wins."""
1627
+ body = (
1628
+ "### W1: hidden critical\n"
1629
+ "**Severity**: Minor and **Severity**: Critical"
1630
+ )
1631
+ report, _ = parse_report("eic", body=body)
1632
+ with pytest.raises(phase.ConformanceError, match="FINDING-GRAMMAR"):
1633
+ phase.check_scoring_seat_anchors(report)
1634
+
1635
+
1636
+ def test_same_line_pipe_separated_severity_pair_fails():
1637
+ """`_SEVERITY_RE` also parses after a table-cell pipe, so both halves
1638
+ of `Minor | Critical` on ONE line are parseable — but two parseable
1639
+ declarations on one line are not a reading-order self-correction and
1640
+ must not enter the supersession path (which could otherwise waive the
1641
+ anchor requirement via `Critical | Minor`)."""
1642
+ for pair in ("Minor | **Severity**: Critical",
1643
+ "Critical | **Severity**: Minor"):
1644
+ body = (
1645
+ "### W1: table smuggle\n"
1646
+ f"**Severity**: {pair}"
1647
+ )
1648
+ report, _ = parse_report("eic", body=body)
1649
+ with pytest.raises(
1650
+ phase.ConformanceError, match="exactly one parseable Severity"
1651
+ ):
1652
+ phase.check_scoring_seat_anchors(report)
1653
+
1654
+
1655
+ def test_cross_line_severity_supersession_takes_last(capsys):
1656
+ """#637 ms01_quant baseline r1: the domain seat declared Major, then
1657
+ self-corrected to Critical with explicit supersession prose. The card
1658
+ passes with the last value operative and the trail in the gate log."""
1659
+ body = (
1660
+ "### W1: construct mismatch\n"
1661
+ "**Severity**: Major\n"
1662
+ "Correction: recording this as Critical; the Severity line below "
1663
+ "supersedes the line above.\n"
1664
+ "**Severity**: Critical\n"
1665
+ '**Evidence Anchor**: text: "quote" p. 1'
1666
+ )
1667
+ report, _ = parse_report("eic", body=body)
1668
+ phase.check_scoring_seat_anchors(report)
1669
+ assert (
1670
+ "[SEVERITY-SUPERSEDED: p2.md: W1: construct mismatch: "
1671
+ "Major -> Critical]"
1672
+ ) in capsys.readouterr().out
1673
+
1674
+
1675
+ def test_unparseable_severity_declaration_still_fails():
1676
+ body = (
1677
+ "### W1: bad value\n"
1678
+ "**Severity**: High"
1679
+ )
1680
+ report, _ = parse_report("eic", body=body)
1681
+ with pytest.raises(
1682
+ phase.ConformanceError, match="exactly one parseable Severity"
1683
+ ):
1684
+ phase.check_scoring_seat_anchors(report)
1685
+
1686
+
1687
+ def test_revisited_severity_value_still_fails():
1688
+ """A chain that revisits a value (Minor -> Major -> Minor) is not a
1689
+ supersession — a non-escalating chain keeps the anti-bundling abort."""
1690
+ body = (
1691
+ "### W1: bundled pair\n"
1692
+ "**Severity**: Minor\n"
1693
+ "first\n"
1694
+ "**Severity**: Major\n"
1695
+ "second\n"
1696
+ "**Severity**: Minor"
1697
+ )
1698
+ report, _ = parse_report("eic", body=body)
1699
+ with pytest.raises(
1700
+ phase.ConformanceError, match="strictly escalating"
1701
+ ):
1702
+ phase.check_scoring_seat_anchors(report)
1703
+
1704
+
1705
+ def test_deescalating_severity_pair_still_fails():
1706
+ """Critical -> Minor must abort: last-wins de-escalation would waive
1707
+ the Critical Evidence-Anchor hard gate with one appended line. Only
1708
+ the observed self-correction direction (escalation) is tolerated."""
1709
+ body = (
1710
+ "### W1: fabricated denominators\n"
1711
+ "**Severity**: Critical\n"
1712
+ "The paper invents denominators.\n"
1713
+ "**Severity**: Minor"
1714
+ )
1715
+ report, _ = parse_report("eic", body=body)
1716
+ with pytest.raises(
1717
+ phase.ConformanceError, match="strictly escalating"
1718
+ ):
1719
+ phase.check_scoring_seat_anchors(report)
1720
+
1721
+
1722
+ def test_distinct_severity_bundle_still_fails():
1723
+ """Three findings bundled under one W heading with distinct descending
1724
+ severities are not a supersession chain and keep the loud abort."""
1725
+ body = (
1726
+ "### W1: bundled triple\n"
1727
+ "**Severity**: Critical\n"
1728
+ "first\n"
1729
+ "**Severity**: Major\n"
1730
+ "second\n"
1731
+ "**Severity**: Minor"
1732
+ )
1733
+ report, _ = parse_report("eic", body=body)
1734
+ with pytest.raises(
1735
+ phase.ConformanceError, match="strictly escalating"
1736
+ ):
1737
+ phase.check_scoring_seat_anchors(report)
1738
+
1739
+
1740
+ def test_escalating_supersession_operative_value_needs_anchor():
1741
+ """Minor -> Major with NO anchor must abort at ANCHOR-MISSING: the
1742
+ LAST value (Major) is operative. Pins last-wins — if the first value
1743
+ (Minor) were operative the card would take the no-anchor branch and
1744
+ pass."""
1745
+ body = (
1746
+ "### W1: upgraded finding\n"
1747
+ "**Severity**: Minor\n"
1748
+ "on reflection this forecloses the design claim\n"
1749
+ "**Severity**: Major"
1750
+ )
1751
+ report, _ = parse_report("eic", body=body)
1752
+ with pytest.raises(phase.ConformanceError, match="ANCHOR-MISSING"):
1753
+ phase.check_scoring_seat_anchors(report)
1754
+
1755
+
1756
+ def test_noncanonical_heading_and_severity_label_fail_together():
1757
+ body = (
1758
+ "### Weakness 1: fabricated denominators\n"
1759
+ "**Severity:** Critical"
1760
+ )
1761
+ report, _ = parse_report("eic", body=body)
1762
+ with pytest.raises(phase.ConformanceError, match="FINDING-GRAMMAR"):
1763
+ phase.check_scoring_seat_anchors(report)
1764
+
1765
+
1766
+ def test_same_line_duplicate_anchor_declarations_fail():
1767
+ body = (
1768
+ "### W1: duplicate anchors\n"
1769
+ "**Severity**: Critical\n"
1770
+ '**Evidence Anchor**: text: "first" and '
1771
+ '**Evidence Anchor**: text: "second"'
1772
+ )
1773
+ report, _ = parse_report("eic", body=body)
1774
+ with pytest.raises(phase.ConformanceError, match="ANCHOR-MISSING"):
1775
+ phase.check_scoring_seat_anchors(report)
1776
+
1777
+
1778
+ def test_same_line_duplicate_minor_anchor_declarations_fail():
1779
+ body = (
1780
+ "### W1: duplicate optional anchors\n"
1781
+ "**Severity**: Minor\n"
1782
+ '**Evidence Anchor**: text: "first" and '
1783
+ '**Evidence Anchor**: text: "second"'
1784
+ )
1785
+ report, _ = parse_report("eic", body=body)
1786
+ with pytest.raises(phase.ConformanceError, match="FINDING-GRAMMAR"):
1787
+ phase.check_scoring_seat_anchors(report)
1788
+
1789
+
1790
+ def test_malformed_minor_anchor_declaration_fails():
1791
+ body = (
1792
+ "### W1: malformed optional anchor\n"
1793
+ "**Severity**: Minor\n"
1794
+ '**Evidence Anchor:** text: "quote"'
1795
+ )
1796
+ report, _ = parse_report("eic", body=body)
1797
+ with pytest.raises(phase.ConformanceError, match="FINDING-GRAMMAR"):
1798
+ phase.check_scoring_seat_anchors(report)
1799
+
1800
+
1801
+ def test_indented_bullet_fields_still_enforce_anchor_gate():
1802
+ body = """### W1: indented finding
1803
+ - **Severity**: Critical
1804
+ - **Confidence**: 5 — core expertise"""
1805
+ report, _ = parse_report("eic", body=body)
1806
+ with pytest.raises(phase.ConformanceError, match="ANCHOR-MISSING"):
1807
+ phase.check_scoring_seat_anchors(report)
1808
+
1809
+
1810
+ @pytest.mark.parametrize(
1811
+ "anchor",
1812
+ (
1813
+ '`text: §5 "short exact quote"`',
1814
+ '[`text: §5 "short exact quote"`]',
1815
+ '[text: §5 "short exact quote"]',
1816
+ "text: §5 “short exact quote”",
1817
+ "equation: Eq. [3]",
1818
+ "[equation: Eq. [3]]",
1819
+ 'text: §5 "short exact quote" per `df`',
1820
+ '`text: §5 "short exact quote" per `df``',
1821
+ "text: §2 “the term “quality culture” is undefined”",
1822
+ 'text: §2 "he said “quality culture” often"',
1823
+ ),
1824
+ )
1825
+ def test_whole_value_wrapped_template_anchor_is_normalised_and_accepted(anchor):
1826
+ body = (
1827
+ "### W1: template-shaped finding\n"
1828
+ "**Severity**: Critical\n"
1829
+ f"**Evidence Anchor**: {anchor}"
1830
+ )
1831
+ report, _ = parse_report("eic", body=body)
1832
+ phase.check_scoring_seat_anchors(report)
1833
+
1834
+
1835
+ @pytest.mark.parametrize(
1836
+ "anchor",
1837
+ (
1838
+ '[`text`: §5 "short exact quote"]',
1839
+ '`text` — §5 "short exact quote"',
1840
+ ),
1841
+ )
1842
+ def test_type_only_wrapping_anchor_is_rejected(anchor):
1843
+ body = (
1844
+ "### W1: malformed type wrapping\n"
1845
+ "**Severity**: Critical\n"
1846
+ f"**Evidence Anchor**: {anchor}"
1847
+ )
1848
+ report, _ = parse_report("eic", body=body)
1849
+ with pytest.raises(phase.ConformanceError, match="ANCHOR-INVALID"):
1850
+ phase.check_scoring_seat_anchors(report)
1851
+
1852
+
1853
+ @pytest.mark.parametrize(
1854
+ "anchor",
1855
+ (
1856
+ '`text: §5 "short exact quote"',
1857
+ 'text: §5 "short exact quote"`',
1858
+ '[text: §5 "short exact quote"',
1859
+ 'text: §5 "short exact quote"]',
1860
+ 'text: §5 "short exact quote"`]',
1861
+ '[text: §5 "short exact quote"] trailing]',
1862
+ '[ text: §5 "short exact quote" ]',
1863
+ '[[text: §5 "short exact quote"]]',
1864
+ '``text: §5 "short exact quote"``',
1865
+ '` text: §5 "short exact quote" `',
1866
+ '[`text: §5 "short exact quote"]',
1867
+ '[text: §5 "short exact quote"`]',
1868
+ "equation: Eq. ]3[",
1869
+ ),
1870
+ )
1871
+ def test_unpaired_or_repeated_whole_value_wrappers_are_rejected(anchor):
1872
+ body = (
1873
+ "### W1: malformed whole-value wrapper\n"
1874
+ "**Severity**: Critical\n"
1875
+ f"**Evidence Anchor**: {anchor}"
1876
+ )
1877
+ report, _ = parse_report("eic", body=body)
1878
+ with pytest.raises(phase.ConformanceError, match="ANCHOR-INVALID"):
1879
+ phase.check_scoring_seat_anchors(report)
1880
+
1881
+
1882
+ @pytest.mark.parametrize(
1883
+ "anchor",
1884
+ (
1885
+ 'text: §5 "short exact quote”',
1886
+ 'text: §5 “short exact quote"',
1887
+ 'text: §5 "outer “inner”',
1888
+ 'text: §5 “outer "inner”"',
1889
+ ),
1890
+ )
1891
+ def test_hybrid_double_quote_pairs_are_rejected(anchor):
1892
+ body = (
1893
+ "### W1: mismatched quote pair\n"
1894
+ "**Severity**: Critical\n"
1895
+ f"**Evidence Anchor**: {anchor}"
1896
+ )
1897
+ report, _ = parse_report("eic", body=body)
1898
+ with pytest.raises(phase.ConformanceError, match="ANCHOR-INVALID"):
1899
+ phase.check_scoring_seat_anchors(report)
1900
+
1901
+
1902
+ def test_combined_template_fields_are_parsed():
1903
+ body = (
1904
+ "### W1: combined fields\n"
1905
+ " - **Severity**: Major | **Evidence Anchor**: "
1906
+ "`absence: Methods — expected an ethics statement; "
1907
+ "checked Methods and appendix` | "
1908
+ "**Confidence**: 4 — core expertise"
1909
+ )
1910
+ report, _ = parse_report("eic", body=body)
1911
+ phase.check_scoring_seat_anchors(report)
1912
+
1913
+
1914
+ def test_h4_finding_under_generic_h3_fails():
1915
+ body = (
1916
+ "### Commentary\n"
1917
+ "#### W1: hidden finding\n"
1918
+ "**Severity**: Critical\n"
1919
+ '**Evidence Anchor**: text: "quote" p. 1'
1920
+ )
1921
+ report, _ = parse_report("eic", body=body)
1922
+ with pytest.raises(phase.ConformanceError, match="own ### W<n>"):
1923
+ phase.check_scoring_seat_anchors(report)
1924
+
1925
+
1926
+ def test_severity_outside_review_body_fails():
1927
+ report, text = parse_report("eic")
1928
+ report.text = text + (
1929
+ "\n## Appendix\n### W1: misplaced\n"
1930
+ "**Severity**: Critical\n"
1931
+ '**Evidence Anchor**: text: "quote" p. 1'
1932
+ )
1933
+ with pytest.raises(phase.ConformanceError, match="outside"):
1934
+ phase.check_scoring_seat_anchors(report)
1935
+
1936
+
1937
+ def test_flat_severity_without_finding_heading_fails():
1938
+ report, _ = parse_report(
1939
+ "eic",
1940
+ body=(
1941
+ "**Severity**: Critical\n"
1942
+ '**Evidence Anchor**: text: "quote" p. 1'
1943
+ ),
1944
+ )
1945
+ with pytest.raises(phase.ConformanceError, match="own ### finding"):
1946
+ phase.check_scoring_seat_anchors(report)
1947
+
1948
+
1949
+ @pytest.mark.parametrize("label", ("severity", "sEvErItY"))
1950
+ def test_case_variant_severity_without_finding_heading_fails(label):
1951
+ report, _ = parse_report(
1952
+ "eic",
1953
+ body=f"Commentary line with **{label}**: Critical",
1954
+ )
1955
+ with pytest.raises(phase.ConformanceError, match="own ### finding"):
1956
+ phase.check_scoring_seat_anchors(report)
1957
+
1958
+
1959
+ @pytest.mark.parametrize("label", ("evidence anchor", "eViDeNcE aNcHoR"))
1960
+ def test_case_variant_optional_anchor_declaration_fails(label):
1961
+ report, _ = parse_report(
1962
+ "eic",
1963
+ body=(
1964
+ "### W1: malformed optional anchor\n"
1965
+ "**Severity**: Minor\n"
1966
+ f'**{label}**: text: "quote"'
1967
+ ),
1968
+ )
1969
+ with pytest.raises(phase.ConformanceError, match="FINDING-GRAMMAR"):
1970
+ phase.check_scoring_seat_anchors(report)
1971
+
1972
+
1973
+ def test_missing_review_body_fails_anchor_family():
1974
+ report, _ = parse_report("eic")
1975
+ report.text = report.text.replace("## Review Body", "## Commentary")
1976
+ with pytest.raises(phase.ConformanceError, match="REVIEW-BODY-MISSING"):
1977
+ phase.check_scoring_seat_anchors(report)
1978
+
1979
+
1980
+ def da_text(ids=("C1",), anchors=None, major_rows=()):
1981
+ anchors = anchors or {finding_id: 'text: "quote" p. 1' for finding_id in ids}
1982
+ rows = "\n".join(
1983
+ f"| {finding_id} | Issue | {anchors.get(finding_id, '')} |"
1984
+ for finding_id in ids
1985
+ )
1986
+ return phase2_text(
1987
+ "da",
1988
+ body=(
1989
+ "#### CRITICAL\n"
1990
+ "| # | Issue | Evidence Anchor |\n"
1991
+ "|---|-------|-----------------|\n" + rows + "\n\n"
1992
+ "#### MAJOR\n"
1993
+ "| # | Issue | Evidence Anchor |\n"
1994
+ "|---|-------|-----------------|\n" + "\n".join(major_rows)
1995
+ ),
1996
+ )
1997
+
1998
+
1999
+ def test_da_empty_critical_anchor_fails():
2000
+ report = panel.parse_report("da.md", da_text(anchors={"C1": ""}), FULL)
2001
+ with pytest.raises(phase.ConformanceError, match="ANCHOR-MISSING"):
2002
+ phase.check_da_anchors(report)
2003
+
2004
+
2005
+ def test_da_ids_must_be_dense():
2006
+ report = panel.parse_report("da.md", da_text(ids=("C2",)), FULL)
2007
+ with pytest.raises(phase.ConformanceError, match="dense"):
2008
+ phase.check_da_anchors(report)
2009
+
2010
+
2011
+ def test_da_conforming_table_passes():
2012
+ report = panel.parse_report("da.md", da_text(ids=("C1", "C2")), FULL)
2013
+ phase.check_da_anchors(report)
2014
+
2015
+
2016
+ @pytest.mark.parametrize(
2017
+ "old,new,fragment",
2018
+ [
2019
+ (
2020
+ "| # | Issue | Evidence Anchor |",
2021
+ "| # | # | Evidence Anchor |",
2022
+ "exactly one #",
2023
+ ),
2024
+ (
2025
+ "| # | Issue | Evidence Anchor |",
2026
+ "| # | Evidence Anchor | Evidence Anchor |",
2027
+ "exactly one #",
2028
+ ),
2029
+ (
2030
+ "| # | Issue | Evidence Anchor |",
2031
+ "| ID | Issue | Anchor |",
2032
+ "missing table header",
2033
+ ),
2034
+ ("| C2 | Issue |", "| C1 | Issue |", "duplicate CRITICAL ID"),
2035
+ ("| C2 | Issue |", "| X2 | Issue |", "invalid CRITICAL ID"),
2036
+ ],
2037
+ )
2038
+ def test_da_header_and_critical_id_gates_fail_phase_checker(
2039
+ old, new, fragment
2040
+ ):
2041
+ report = panel.parse_report(
2042
+ "da.md", da_text(ids=("C1", "C2")).replace(old, new, 1), FULL
2043
+ )
2044
+ with pytest.raises(
2045
+ (panel.ReportError, phase.panel.ReportError, phase.ConformanceError),
2046
+ match=fragment,
2047
+ ):
2048
+ phase.check_da_anchors(report)
2049
+
2050
+
2051
+ def test_da_shadow_table_fails_phase_checker():
2052
+ canonical = (
2053
+ '| # | Issue | Evidence Anchor |\n'
2054
+ '|---|-------|-----------------|\n'
2055
+ '| C1 | Issue | text: "quote" p. 1 |'
2056
+ )
2057
+ shadowed = (
2058
+ '| ID | Issue | Anchor |\n'
2059
+ '|---|-------|--------|\n'
2060
+ '| C1 | Issue | text: "quoted evidence" p. 1 |\n\n'
2061
+ '| # | Issue | Evidence Anchor |\n'
2062
+ '|---|-------|-----------------|'
2063
+ )
2064
+ report = panel.parse_report(
2065
+ "da.md", da_text(ids=("C1",)).replace(canonical, shadowed, 1), FULL
2066
+ )
2067
+ with pytest.raises(phase.ConformanceError, match="first nonblank line"):
2068
+ phase.check_da_anchors(report)
2069
+
2070
+
2071
+ def test_da_standalone_critical_fails_phase_checker():
2072
+ text = da_text().replace(
2073
+ "#### CRITICAL",
2074
+ "### Further adversarial challenge\n"
2075
+ "- **Severity**: Critical | **Confidence**: 5 (statistics)\n\n"
2076
+ "#### CRITICAL",
2077
+ 1,
2078
+ )
2079
+ report = panel.parse_report("da.md", text, FULL)
2080
+ with pytest.raises(phase.ConformanceError, match="standalone Severity"):
2081
+ phase.check_da_anchors(report)
2082
+
2083
+
2084
+ @pytest.mark.parametrize("label", ("severity", "sEvErItY"))
2085
+ def test_da_case_variant_standalone_critical_fails_phase_checker(label):
2086
+ text = da_text().replace(
2087
+ "#### CRITICAL",
2088
+ "### Further adversarial challenge\n"
2089
+ f"This is **{label}**: Critical and no revision cures it.\n\n"
2090
+ "#### CRITICAL",
2091
+ 1,
2092
+ )
2093
+ report = panel.parse_report("da.md", text, FULL)
2094
+ with pytest.raises(phase.ConformanceError, match="standalone Severity"):
2095
+ phase.check_da_anchors(report)
2096
+
2097
+
2098
+ def test_da_extra_issue_table_band_fails_phase_checker():
2099
+ text = da_text().replace(
2100
+ "#### MAJOR",
2101
+ "#### ADDITIONAL CRITICAL FINDINGS\n"
2102
+ "| # | Issue | Evidence Anchor |\n"
2103
+ "|---|-------|-----------------|\n"
2104
+ '| C1 | impossible df | text: "n=41" p. 4 |\n\n'
2105
+ "#### MAJOR",
2106
+ 1,
2107
+ )
2108
+ report = panel.parse_report("da.md", text, FULL)
2109
+ with pytest.raises(
2110
+ phase.ConformanceError, match="unexpected issue-table band"
2111
+ ):
2112
+ phase.check_da_anchors(report)
2113
+
2114
+
2115
+ @pytest.mark.parametrize(
2116
+ ("lead_in", "header"),
2117
+ (
2118
+ ("The following issues invalidate the claim:\n\n",
2119
+ "| # | Issue | Evidence Anchor |"),
2120
+ ("", "| # | Issue | evidence anchor |"),
2121
+ ("", "# | Issue | Evidence Anchor"),
2122
+ ),
2123
+ )
2124
+ def test_da_disguised_extra_issue_table_band_fails_phase_checker(
2125
+ lead_in, header
2126
+ ):
2127
+ text = da_text().replace(
2128
+ "#### MAJOR",
2129
+ "#### ADDITIONAL CRITICAL FINDINGS\n"
2130
+ f"{lead_in}{header}\n"
2131
+ "|---|-------|-----------------|\n"
2132
+ '| C9 | impossible df | text: "n=41" p. 4 |\n\n'
2133
+ "#### MAJOR",
2134
+ 1,
2135
+ )
2136
+ report = panel.parse_report("da.md", text, FULL)
2137
+ with pytest.raises(
2138
+ phase.ConformanceError, match="unexpected issue-table band"
2139
+ ):
2140
+ phase.check_da_anchors(report)
2141
+
2142
+
2143
+ @pytest.mark.parametrize("placement", ("preamble", "extra_h2"))
2144
+ def test_da_issue_table_outside_canonical_bands_fails_phase(placement):
2145
+ table = (
2146
+ "| # | Issue | Evidence Anchor |\n"
2147
+ "|---|-------|-----------------|\n"
2148
+ '| C9 | impossible df | text: "n=41" p. 4 |\n'
2149
+ )
2150
+ text = da_text()
2151
+ if placement == "preamble":
2152
+ text = text.replace("#### CRITICAL", table + "\n#### CRITICAL", 1)
2153
+ else:
2154
+ text += "\n## Appendix\n" + table
2155
+ report = panel.parse_report("da.md", text, FULL)
2156
+ with pytest.raises(phase.ConformanceError, match="unexpected issue-table"):
2157
+ phase.check_da_anchors(report)
2158
+
2159
+
2160
+ def test_da_internal_header_whitespace_fails_phase_checker():
2161
+ text = da_text().replace(
2162
+ "#### MAJOR",
2163
+ "#### ADDITIONAL CRITICAL FINDINGS\n"
2164
+ "# | Issue | Evidence Anchor\n"
2165
+ "---|-------|-----------------\n"
2166
+ 'C9 | impossible df | text: "n=41" p. 4\n\n'
2167
+ "#### MAJOR",
2168
+ 1,
2169
+ )
2170
+ report = panel.parse_report("da.md", text, FULL)
2171
+ with pytest.raises(phase.ConformanceError, match="unexpected issue-table"):
2172
+ phase.check_da_anchors(report)
2173
+
2174
+
2175
+ @pytest.mark.parametrize(
2176
+ "header",
2177
+ (
2178
+ "| ID | Issue | Evidence Anchor |",
2179
+ "| # | Issue | Evidence |",
2180
+ "| **#** | Issue | **Evidence Anchor** |",
2181
+ "| `#` | Issue | `Evidence Anchor` |",
2182
+ ),
2183
+ )
2184
+ def test_da_partial_or_formatted_issue_header_fails_phase(header):
2185
+ text = da_text().replace(
2186
+ "#### MAJOR",
2187
+ "#### ADDITIONAL CRITICAL FINDINGS\n"
2188
+ f"{header}\n"
2189
+ "|---|-------|-----------------|\n"
2190
+ '| C9 | impossible df | text: "n=41" p. 4 |\n\n'
2191
+ "#### MAJOR",
2192
+ 1,
2193
+ )
2194
+ report = panel.parse_report("da.md", text, FULL)
2195
+ with pytest.raises(phase.ConformanceError, match="unexpected issue-table"):
2196
+ phase.check_da_anchors(report)
2197
+
2198
+
2199
+ @pytest.mark.parametrize(
2200
+ "header",
2201
+ (
2202
+ "| ID | Issue | [Evidence Anchor][anchor] |",
2203
+ r"| \# | Issue | Evidence |",
2204
+ '| ID | Issue | <span title="x>y">Evidence Anchor</span> |',
2205
+ ),
2206
+ )
2207
+ def test_da_commonmark_visible_issue_header_fails_phase(header):
2208
+ text = da_text().replace(
2209
+ "#### MAJOR",
2210
+ "#### ADDITIONAL CRITICAL FINDINGS\n"
2211
+ f"{header}\n"
2212
+ "|---|-------|-----------------|\n"
2213
+ '| C9 | impossible df | text: "n=41" p. 4 |\n\n'
2214
+ "#### MAJOR",
2215
+ 1,
2216
+ )
2217
+ report = panel.parse_report("da.md", text, FULL)
2218
+ with pytest.raises(phase.ConformanceError, match="unexpected issue-table"):
2219
+ phase.check_da_anchors(report)
2220
+
2221
+
2222
+ def test_da_balanced_link_destination_header_fails_phase():
2223
+ header = (
2224
+ r"| [\#](<https://x.test/a(b)>) | Issue | "
2225
+ r"[Evidence Anchor](<https://x.test/a(b)>) |"
2226
+ )
2227
+ text = da_text().replace(
2228
+ "#### MAJOR",
2229
+ "#### ADDITIONAL CRITICAL FINDINGS\n"
2230
+ f"{header}\n"
2231
+ "|---|-------|-----------------|\n"
2232
+ '| C9 | impossible df | text: "n=41" p. 4 |\n\n'
2233
+ "#### MAJOR",
2234
+ 1,
2235
+ )
2236
+ report = panel.parse_report("da.md", text, FULL)
2237
+ with pytest.raises(phase.ConformanceError, match="unexpected issue-table"):
2238
+ phase.check_da_anchors(report)
2239
+
2240
+
2241
+ def test_da_typed_anchor_payload_alone_fails_phase():
2242
+ header = (
2243
+ r"| [\#](<https://x.test/a(b)>) | Issue | "
2244
+ r"[Evidence Anchor](<https://x.test/a(b)>) |"
2245
+ )
2246
+ text = da_text().replace(
2247
+ "#### MAJOR",
2248
+ "#### ADDITIONAL CRITICAL FINDINGS\n"
2249
+ f"{header}\n"
2250
+ "|---|-------|-----------------|\n"
2251
+ '| 1 | impossible df | `text: "n=41" p. 4` |\n\n'
2252
+ "#### MAJOR",
2253
+ 1,
2254
+ )
2255
+ report = panel.parse_report("da.md", text, FULL)
2256
+ with pytest.raises(phase.ConformanceError, match="unexpected issue-table"):
2257
+ phase.check_da_anchors(report)
2258
+
2259
+
2260
+ def test_da_escaped_pipe_cell_evasion_fails_phase():
2261
+ block = (
2262
+ "#### ADDITIONAL CRITICAL FINDINGS\n"
2263
+ r"| [\#<!--\|-->](https://x.test) | Issue | "
2264
+ r"[Evidence<!--\|--> Anchor](https://x.test) |" "\n"
2265
+ "|---|---|---|\n"
2266
+ r"| [C<!--\|-->9](https://x.test) | impossible df | "
2267
+ r'[text<!--\|-->: "n=41" p. 4](https://x.test) |' "\n\n"
2268
+ )
2269
+ text = da_text().replace("#### MAJOR", block + "#### MAJOR", 1)
2270
+ report = panel.parse_report("da.md", text, FULL)
2271
+ with pytest.raises(phase.ConformanceError, match="HTML comments are forbidden"):
2272
+ phase.check_da_anchors(report)
2273
+
2274
+
2275
+ @pytest.mark.parametrize(
2276
+ "invisible", ("\u0600", "\u200b", "\u034f", "\ufe0e", "\u3164", "\ufff0")
2277
+ )
2278
+ def test_da_invisible_issue_payload_fails_phase(invisible):
2279
+ block = (
2280
+ "#### ADDITIONAL CRITICAL FINDINGS\n"
2281
+ f"| #{invisible} | Issue | Evidence{invisible} Anchor |\n"
2282
+ "|---|---|---|\n"
2283
+ f'| C{invisible}9 | impossible df | text{invisible}: "n=41" p. 4 |\n\n'
2284
+ )
2285
+ text = da_text().replace("#### MAJOR", block + "#### MAJOR", 1)
2286
+ report = panel.parse_report("da.md", text, FULL)
2287
+ with pytest.raises(phase.ConformanceError, match="unexpected issue-table"):
2288
+ phase.check_da_anchors(report)
2289
+
2290
+
2291
+ def test_da_fullwidth_issue_payload_fails_phase():
2292
+ block = (
2293
+ "#### ADDITIONAL CRITICAL FINDINGS\n"
2294
+ "| # | Issue | Evidence Anchor |\n"
2295
+ "|---|---|---|\n"
2296
+ '| C9 | impossible df | text: "n=41" p. 4 |\n\n'
2297
+ )
2298
+ text = da_text().replace("#### MAJOR", block + "#### MAJOR", 1)
2299
+ report = panel.parse_report("da.md", text, FULL)
2300
+ with pytest.raises(phase.ConformanceError, match="unexpected issue-table"):
2301
+ phase.check_da_anchors(report)
2302
+
2303
+
2304
+ def test_da_raw_html_issue_table_fails_phase():
2305
+ block = (
2306
+ "#### ADDITIONAL CRITICAL FINDINGS\n"
2307
+ "<table><tr><th>ID</th><th>Evidence</th></tr>"
2308
+ "<tr><td>C9</td><td>text: n=41</td></tr></table>\n\n"
2309
+ )
2310
+ text = da_text().replace("#### MAJOR", block + "#### MAJOR", 1)
2311
+ report = panel.parse_report("da.md", text, FULL)
2312
+ with pytest.raises(phase.ConformanceError, match="raw HTML issue-table"):
2313
+ phase.check_da_anchors(report)
2314
+
2315
+
2316
+ def test_da_nested_html_issue_table_in_canonical_row_fails_phase():
2317
+ nested = (
2318
+ "real issue <table><tr><th>#</th><th>Evidence Anchor</th></tr>"
2319
+ '<tr><td>C9</td><td>text: "impossible df" p. 4</td></tr></table>'
2320
+ )
2321
+ text = da_text().replace("Issue | text:", nested + " | text:", 1)
2322
+ report = panel.parse_report("da.md", text, FULL)
2323
+ with pytest.raises(phase.ConformanceError, match="raw HTML issue-table"):
2324
+ phase.check_da_anchors(report)
2325
+
2326
+
2327
+ def test_da_bare_html_row_fails_phase():
2328
+ block = (
2329
+ "#### ADDITIONAL CRITICAL FINDINGS\n"
2330
+ "<tr><td>C9</td><td>text: n=41</td></tr>\n\n"
2331
+ )
2332
+ text = da_text().replace("#### MAJOR", block + "#### MAJOR", 1)
2333
+ report = panel.parse_report("da.md", text, FULL)
2334
+ with pytest.raises(phase.ConformanceError, match="raw HTML issue-table"):
2335
+ phase.check_da_anchors(report)
2336
+
2337
+
2338
+ @pytest.mark.parametrize(
2339
+ "row,fragment",
2340
+ [
2341
+ ("| M1 | Issue | |", "ANCHOR-MISSING"),
2342
+ ("| M1 | Issue | see page 3 |", "ANCHOR-INVALID"),
2343
+ ('| | Issue | text: "quote" |', "empty MAJOR # cell"),
2344
+ ],
2345
+ )
2346
+ def test_da_major_row_gates_fail_phase_checker(row, fragment):
2347
+ report = panel.parse_report("da.md", da_text(major_rows=(row,)), FULL)
2348
+ with pytest.raises(
2349
+ (panel.ReportError, phase.panel.ReportError, phase.ConformanceError),
2350
+ match=fragment,
2351
+ ):
2352
+ phase.check_da_anchors(report)
2353
+
2354
+
2355
+ def test_da_valid_major_row_passes_phase_checker():
2356
+ report = panel.parse_report(
2357
+ "da.md",
2358
+ da_text(major_rows=('| M1 | Issue | text: "short quote" |',)),
2359
+ FULL,
2360
+ )
2361
+ phase.check_da_anchors(report)
2362
+
2363
+
2364
+ @pytest.mark.parametrize(
2365
+ "old,new",
2366
+ [
2367
+ ("|---|-------|-----------------|", ""),
2368
+ ("|---|-------|-----------------|", "|--|-------|-----------------|"),
2369
+ ],
2370
+ )
2371
+ def test_da_separator_drift_fails_phase_checker(old, new):
2372
+ report = panel.parse_report(
2373
+ "da.md", da_text().replace(old, new, 1), FULL
2374
+ )
2375
+ with pytest.raises(
2376
+ (panel.ReportError, phase.panel.ReportError, phase.ConformanceError),
2377
+ match="separator",
2378
+ ):
2379
+ phase.check_da_anchors(report)
2380
+
2381
+
2382
+ def test_da_row_without_outer_pipes_fails_phase_checker():
2383
+ report = panel.parse_report(
2384
+ "da.md",
2385
+ da_text().replace(
2386
+ '| C1 | Issue | text: "quote" p. 1 |',
2387
+ 'C1 | Issue | text: "quote" p. 1',
2388
+ 1,
2389
+ ),
2390
+ FULL,
2391
+ )
2392
+ with pytest.raises(
2393
+ (panel.ReportError, phase.panel.ReportError, phase.ConformanceError),
2394
+ match="outer-pipe-delimited",
2395
+ ):
2396
+ phase.check_da_anchors(report)
2397
+
2398
+
2399
+ def test_da_pre_table_prose_passes_phase_checker():
2400
+ report = panel.parse_report(
2401
+ "da.md",
2402
+ da_text(
2403
+ major_rows=('| M1 | Issue | text: "short quote" |',)
2404
+ ).replace(
2405
+ "#### CRITICAL",
2406
+ "Ordinary adversarial commentary precedes the terminal tables.\n\n"
2407
+ "#### CRITICAL",
2408
+ 1,
2409
+ ),
2410
+ FULL,
2411
+ )
2412
+ phase.check_da_anchors(report)
2413
+
2414
+
2415
+ def test_da_bare_comment_closer_passes_phase_checker():
2416
+ text = da_text(ids=("C1",)).replace(
2417
+ "#### CRITICAL",
2418
+ "The reported N moves 41 --> 38 without explanation.\n\n"
2419
+ "#### CRITICAL",
2420
+ 1,
2421
+ ).replace(
2422
+ 'text: "quote" p. 1',
2423
+ 'text: "N moves 41 --> 38" p. 1',
2424
+ 1,
2425
+ )
2426
+ report = panel.parse_report("da.md", text, FULL)
2427
+ phase.check_da_anchors(report)
2428
+
2429
+
2430
+ def test_da_post_critical_table_prose_fails_phase_checker():
2431
+ text = da_text(ids=()).replace(
2432
+ "\n\n#### MAJOR",
2433
+ "\n\n*None. Ordinary adversarial commentary.*\n\n#### MAJOR",
2434
+ 1,
2435
+ )
2436
+ report = panel.parse_report("da.md", text, FULL)
2437
+ with pytest.raises(
2438
+ (panel.ReportError, phase.panel.ReportError, phase.ConformanceError),
2439
+ match="issue tables are terminal",
2440
+ ):
2441
+ phase.check_da_anchors(report)
2442
+
2443
+
2444
+ _DA_TERMINAL_LATE_SURFACES = (
2445
+ '| C2 | Late issue | text: "late quoted evidence" p. 2 |',
2446
+ "| X9 | Bogus issue | |",
2447
+ (
2448
+ "| # | Issue | Evidence Anchor |\n"
2449
+ "|---|-------|-----------------|\n"
2450
+ "| C9 | Shadow issue | |"
2451
+ ),
2452
+ 'C2 | Late issue | text: "late quoted evidence" p. 2',
2453
+ '— | Late issue | text: "late quoted evidence"',
2454
+ "# | Issue | Evidence Anchor",
2455
+ "C2\nLate issue without pipes\nfigure: Figure 2",
2456
+ "- C2\n- Late critical issue\n- figure: Figure 2",
2457
+ "> Ordinary post-table commentary",
2458
+ "##### Additional commentary",
2459
+ "### Closing note\nOrdinary late prose",
2460
+ )
2461
+
2462
+
2463
+ @pytest.mark.parametrize(
2464
+ "late_surface",
2465
+ _DA_TERMINAL_LATE_SURFACES,
2466
+ )
2467
+ def test_da_post_boundary_table_surfaces_fail_phase_checker(
2468
+ late_surface,
2469
+ ):
2470
+ text = da_text(ids=("C1",)).replace(
2471
+ "\n\n#### MAJOR",
2472
+ f"\n\n{late_surface}\n\n#### MAJOR",
2473
+ 1,
2474
+ )
2475
+ report = panel.parse_report("da.md", text, FULL)
2476
+ with pytest.raises(
2477
+ (panel.ReportError, phase.panel.ReportError, phase.ConformanceError),
2478
+ match="issue tables are terminal",
2479
+ ):
2480
+ phase.check_da_anchors(report)
2481
+
2482
+
2483
+ @pytest.mark.parametrize("late_surface", _DA_TERMINAL_LATE_SURFACES)
2484
+ def test_da_post_major_table_surfaces_fail_phase_checker(
2485
+ late_surface,
2486
+ ):
2487
+ report = panel.parse_report(
2488
+ "da.md", da_text(ids=("C1",)) + f"\n\n{late_surface}", FULL
2489
+ )
2490
+ with pytest.raises(
2491
+ (panel.ReportError, phase.panel.ReportError, phase.ConformanceError),
2492
+ match="issue tables are terminal",
2493
+ ):
2494
+ phase.check_da_anchors(report)
2495
+
2496
+
2497
+ def test_da_fenced_payload_after_major_fails_phase_checker():
2498
+ text = da_text(ids=("C1",)) + (
2499
+ "\n\n```markdown\n"
2500
+ '| C2 | Hidden critical issue | text: "hidden evidence" p. 2 |\n'
2501
+ "```"
2502
+ )
2503
+ report = panel.parse_report("da.md", text, FULL)
2504
+ with pytest.raises(
2505
+ (panel.ReportError, phase.panel.ReportError, phase.ConformanceError),
2506
+ match="issue tables are terminal",
2507
+ ):
2508
+ phase.check_da_anchors(report)
2509
+
2510
+
2511
+ def test_da_html_comment_inside_fence_fails_phase_checker():
2512
+ text = da_text(ids=()) + (
2513
+ "\n\n```\n<!-- hidden adjudication payload -->\n```"
2514
+ )
2515
+ report = panel.parse_report("da.md", text, FULL)
2516
+ with pytest.raises(
2517
+ (panel.ReportError, phase.panel.ReportError, phase.ConformanceError),
2518
+ match="HTML comments are forbidden",
2519
+ ):
2520
+ phase.check_da_anchors(report)
2521
+
2522
+
2523
+ def test_da_html_commented_tables_fail_phase_checker():
2524
+ text = da_text(ids=()).replace(
2525
+ "#### CRITICAL", "<!--\n#### CRITICAL", 1
2526
+ )
2527
+ text += "\n-->"
2528
+ report = panel.parse_report("da.md", text, FULL)
2529
+ with pytest.raises(
2530
+ (panel.ReportError, phase.panel.ReportError, phase.ConformanceError),
2531
+ match="HTML comments are forbidden",
2532
+ ):
2533
+ phase.check_da_anchors(report)
2534
+
2535
+
2536
+ @pytest.mark.parametrize(
2537
+ "old,new,fragment",
2538
+ [
2539
+ ("#### CRITICAL", "#### Critical", "DA-CRITICAL-PARSE"),
2540
+ ("#### MAJOR", "#### Major", "DA-MAJOR-PARSE"),
2541
+ ("#### MAJOR", "#### MAJOR\n\n#### MAJOR", "DA-MAJOR-PARSE"),
2542
+ ],
2543
+ )
2544
+ def test_da_required_sections_fail_closed(old, new, fragment):
2545
+ report = panel.parse_report(
2546
+ "da.md", da_text().replace(old, new, 1), FULL
2547
+ )
2548
+ with pytest.raises(
2549
+ (phase.ConformanceError, phase.panel.ReportError), match=re.escape(fragment)
2550
+ ):
2551
+ phase.check_da_anchors(report)
2552
+
2553
+
2554
+ def write_cli_files(tmp_path: Path, role: str) -> list[str]:
2555
+ phase1 = tmp_path / "p1.md"
2556
+ phase2 = tmp_path / "p2.md"
2557
+ manuscript = tmp_path / "m.md"
2558
+ metadata = tmp_path / "meta.json"
2559
+ phase1.write_text(phase1_text(role), encoding="utf-8")
2560
+ phase2.write_text(phase2_text(role), encoding="utf-8")
2561
+ manuscript.write_text("short synthetic manuscript", encoding="utf-8")
2562
+ metadata.write_text(json.dumps({
2563
+ "title": "Synthetic", "field": "testing", "word_count": 3
2564
+ }), encoding="utf-8")
2565
+ return [
2566
+ "--contract", str(FULL_PATH),
2567
+ "--phase1", str(phase1),
2568
+ "--phase2", str(phase2),
2569
+ "--manuscript", str(manuscript),
2570
+ "--metadata", str(metadata),
2571
+ ]
2572
+
2573
+
2574
+ def test_full_cli_pass(tmp_path):
2575
+ assert phase.main(
2576
+ write_cli_files(tmp_path, "methodology") + ["--role", "methodology"]
2577
+ ) == 0
2578
+
2579
+
2580
+ def phase1_only_args(tmp_path: Path, role: str) -> list[str]:
2581
+ """CLI arguments for the gate that runs before Phase 2 exists."""
2582
+ args = write_cli_files(tmp_path, role)
2583
+ del args[args.index("--phase2"):args.index("--phase2") + 2]
2584
+ return args + ["--phase1-only", "--role", role]
2585
+
2586
+
2587
+ def test_phase1_only_cli_passes_before_phase2_exists(tmp_path, capsys):
2588
+ """The dispatch harness needs a verdict on Phase 1 alone.
2589
+
2590
+ A retry decision is taken while Phase 2 has not been requested yet, so
2591
+ requiring `--phase2` forced the harness to invent one or to reimplement
2592
+ the gate, and a reimplemented gate is not the checker's own output.
2593
+ """
2594
+ assert phase.main(phase1_only_args(tmp_path, "methodology")) == \
2595
+ phase.EXIT_PASS
2596
+ assert "PHASE1-CONFORMANCE: PASS" in capsys.readouterr().out
2597
+
2598
+
2599
+ def test_phase1_only_cli_rejects_a_malformed_plan(tmp_path, capsys):
2600
+ args = phase1_only_args(tmp_path, "methodology")
2601
+ phase1_path = Path(args[args.index("--phase1") + 1])
2602
+ phase1_path.write_text(
2603
+ phase1_text("methodology").replace(
2604
+ "what_triggers_fatal: fatal evidence pattern for D3", "", 1
2605
+ ),
2606
+ encoding="utf-8",
2607
+ )
2608
+ assert phase.main(args) == phase.EXIT_CONFORMANCE
2609
+ assert "[PHASE1-GRAMMAR:" in capsys.readouterr().out
2610
+
2611
+
2612
+ def test_phase1_only_cli_still_checks_manuscript_blindness(tmp_path, capsys):
2613
+ args = phase1_only_args(tmp_path, "methodology")
2614
+ phase1_path = Path(args[args.index("--phase1") + 1])
2615
+ manuscript = Path(args[args.index("--manuscript") + 1])
2616
+ leak = " ".join(f"leaked{index}" for index in range(14))
2617
+ manuscript.write_text(leak, encoding="utf-8")
2618
+ phase1_path.write_text(
2619
+ phase1_text("methodology") + "\n" + leak + "\n", encoding="utf-8"
2620
+ )
2621
+ assert phase.main(args) == phase.EXIT_CONFORMANCE
2622
+ assert "LEAK" in capsys.readouterr().out
2623
+
2624
+
2625
+ def test_phase1_only_and_phase2_are_mutually_exclusive(tmp_path):
2626
+ args = write_cli_files(tmp_path, "methodology")
2627
+ with pytest.raises(SystemExit):
2628
+ phase.main(args + ["--role", "methodology", "--phase1-only"])
2629
+
2630
+
2631
+ def test_one_of_phase1_only_or_phase2_is_required(tmp_path):
2632
+ args = write_cli_files(tmp_path, "methodology")
2633
+ del args[args.index("--phase2"):args.index("--phase2") + 2]
2634
+ with pytest.raises(SystemExit):
2635
+ phase.main(args + ["--role", "methodology"])
2636
+
2637
+
2638
+ def test_multi_dissent_emits_its_machine_token_on_the_violation_line(
2639
+ tmp_path, capsys
2640
+ ):
2641
+ """Pinned as an interface, not prose.
2642
+
2643
+ The dispatch harness routes §5's one permitted Phase 2 retry on this exact
2644
+ token, matched inside this exact line prefix. A reword must fail here
2645
+ rather than silently turn a permitted retry into an aborted fleet.
2646
+ """
2647
+ args = write_cli_files(tmp_path, "methodology")
2648
+ Path(args[args.index("--phase2") + 1]).write_text(
2649
+ phase2_with_dissent_section([
2650
+ "dimension_id: D1",
2651
+ "rationale: the plan understated the risk here",
2652
+ "dimension_id: D3",
2653
+ "rationale: the plan understated a second risk here",
2654
+ ]),
2655
+ encoding="utf-8",
2656
+ )
2657
+ assert phase.main(args + ["--role", "methodology"]) == \
2658
+ phase.EXIT_CONFORMANCE
2659
+ violations = [
2660
+ line for line in capsys.readouterr().out.splitlines()
2661
+ if line.lstrip().startswith("[PROTOCOL-VIOLATION:")
2662
+ ]
2663
+ assert len(violations) == 1, violations
2664
+ assert "multi_dissent=true" in violations[0]
2665
+
2666
+
2667
+ def test_phase1_requires_the_paraphrase_section():
2668
+ """§4 names three requirements; a valid plan alone must not pass."""
2669
+ text = re.sub(r"## Contract Paraphrase.*?(?=## Scoring Plan)", "",
2670
+ phase1_text("methodology"), flags=re.S)
2671
+ with pytest.raises(phase.ConformanceError,
2672
+ match="Contract Paraphrase"):
2673
+ phase.parse_phase1("p1.md", text, FULL, "methodology")
2674
+
2675
+
2676
+ def test_phase1_requires_the_terminal_acknowledgement():
2677
+ text = phase1_text("methodology").replace(
2678
+ "[CONTRACT-ACKNOWLEDGED]", "").rstrip() + "\n"
2679
+ with pytest.raises(phase.ConformanceError,
2680
+ match="CONTRACT-ACKNOWLEDGED"):
2681
+ phase.parse_phase1("p1.md", text, FULL, "methodology")
2682
+
2683
+
2684
+ def test_phase1_requires_the_paraphrase_paragraph_floor():
2685
+ """A bare heading over one line is not the paraphrase the contract's
2686
+ `paraphrase_minimum_dimensions` names; the count is the
2687
+ machine-checkable lower bound (real dispatch outputs carry exactly
2688
+ one paragraph per dimension).
2689
+ """
2690
+ text = re.sub(
2691
+ r"## Contract Paraphrase.*?(?=## Scoring Plan)",
2692
+ "## Contract Paraphrase\n\nAll dimensions understood.\n\n",
2693
+ phase1_text("methodology"), flags=re.S)
2694
+ with pytest.raises(phase.ConformanceError, match="fewer than"):
2695
+ phase.parse_phase1("p1.md", text, FULL, "methodology")
2696
+
2697
+
2698
+ def test_phase1_requires_the_exact_h2_sequence():
2699
+ """§4: exactly `## Contract Paraphrase` then `## Scoring Plan`, in
2700
+ that order and nothing else at H2 -- presence alone let a reordered
2701
+ or extra-sectioned precommitment pass. All four real dispatch
2702
+ outputs carry exactly this sequence.
2703
+ """
2704
+ good = phase1_text("methodology")
2705
+ reordered = good.replace("## Contract Paraphrase", "## ZZZ", 1)
2706
+ reordered = reordered.replace("## Scoring Plan",
2707
+ "## Contract Paraphrase", 1)
2708
+ reordered = reordered.replace("## ZZZ", "## Scoring Plan", 1)
2709
+ with pytest.raises(phase.ConformanceError, match="H2 sections"):
2710
+ phase.parse_phase1("p1.md", reordered, FULL, "methodology")
2711
+ extra = good.replace("## Scoring Plan",
2712
+ "## Reviewer Notes\n\nan extra section\n\n"
2713
+ "## Scoring Plan", 1)
2714
+ with pytest.raises(phase.ConformanceError, match="H2 sections"):
2715
+ phase.parse_phase1("p1.md", extra, FULL, "methodology")
2716
+
2717
+
2718
+ def test_phase1_heading_only_paraphrase_fails_the_paragraph_floor():
2719
+ """codex r41: six bare `### Dn` headings separated by blank lines
2720
+ counted as six paragraphs, so a precommitment with no paraphrase
2721
+ prose at all satisfied the floor. A heading is a heading, not a
2722
+ paragraph, and it separates paragraphs rather than joining them.
2723
+ """
2724
+ headings = "\n\n".join(
2725
+ f"### {dim['id']}" for dim in FULL["acceptance_dimensions"])
2726
+ text = re.sub(
2727
+ r"## Contract Paraphrase.*?(?=## Scoring Plan)",
2728
+ f"## Contract Paraphrase\n\n{headings}\n\n",
2729
+ phase1_text("methodology"), flags=re.S)
2730
+ with pytest.raises(phase.ConformanceError, match="fewer than"):
2731
+ phase.parse_phase1("p1.md", text, FULL, "methodology")
2732
+
2733
+
2734
+ def test_phase1_headings_do_not_join_two_prose_paragraphs():
2735
+ """A heading between two prose lines separates them; treating it as
2736
+ transparent would count `prose / ### H / prose` as one paragraph
2737
+ and undercount, while counting it as prose overcounts.
2738
+ """
2739
+ dims = FULL["acceptance_dimensions"]
2740
+ body = "\n\n".join(
2741
+ f"### {dim['id']}\n{dim['id']} concerns {dim['name']} as the "
2742
+ "contract defines it." for dim in dims)
2743
+ text = re.sub(
2744
+ r"## Contract Paraphrase.*?(?=## Scoring Plan)",
2745
+ f"## Contract Paraphrase\n\n{body}\n\n",
2746
+ phase1_text("methodology"), flags=re.S)
2747
+ parsed = phase.parse_phase1("p1.md", text, FULL, "methodology")
2748
+ assert parsed is not None
2749
+
2750
+
2751
+ def test_phase1_zero_content_blocks_do_not_satisfy_the_floor():
2752
+ """codex r42: six `---` thematic breaks (or six single-line HTML
2753
+ comments) counted as six paragraphs. Zero-content lines separate;
2754
+ they never count.
2755
+ """
2756
+ for filler in ("---", "- - -", "***", "<!-- noted -->"):
2757
+ blocks = "\n\n".join([filler] * len(FULL["acceptance_dimensions"]))
2758
+ text = re.sub(
2759
+ r"## Contract Paraphrase.*?(?=## Scoring Plan)",
2760
+ f"## Contract Paraphrase\n\n{blocks}\n\n",
2761
+ phase1_text("methodology"), flags=re.S)
2762
+ with pytest.raises(phase.ConformanceError, match="fewer than"):
2763
+ phase.parse_phase1("p1.md", text, FULL, "methodology")
2764
+
2765
+
2766
+ def test_phase1_a_bulleted_paraphrase_still_counts_as_content():
2767
+ """The zero-content list is CLOSED: a bulleted six-point paraphrase
2768
+ is real content, and refusing it would abort a panel over
2769
+ formatting -- the false-abort channel #609 exists to remove. A
2770
+ dash-led bullet must not be confused with a `---` thematic break.
2771
+ """
2772
+ dims = FULL["acceptance_dimensions"]
2773
+ bullets = "\n\n".join(
2774
+ f"- {dim['id']} concerns {dim['name']} as the contract defines "
2775
+ "it." for dim in dims)
2776
+ text = re.sub(
2777
+ r"## Contract Paraphrase.*?(?=## Scoring Plan)",
2778
+ f"## Contract Paraphrase\n\n{bullets}\n\n",
2779
+ phase1_text("methodology"), flags=re.S)
2780
+ parsed = phase.parse_phase1("p1.md", text, FULL, "methodology")
2781
+ assert parsed is not None
2782
+
2783
+
2784
+ def test_phase1_multiline_html_comments_do_not_satisfy_the_floor():
2785
+ """codex r43: only same-line comments matched the separator, so six
2786
+ multi-line `<!-- ... -->` blocks counted as six paragraphs -- an
2787
+ entirely hidden §4 with no rendered paraphrase at all.
2788
+ """
2789
+ block = "<!--\nhidden not-a-paraphrase\n-->"
2790
+ blocks = "\n\n".join([block] * len(FULL["acceptance_dimensions"]))
2791
+ text = re.sub(
2792
+ r"## Contract Paraphrase.*?(?=## Scoring Plan)",
2793
+ f"## Contract Paraphrase\n\n{blocks}\n\n",
2794
+ phase1_text("methodology"), flags=re.S)
2795
+ with pytest.raises(phase.ConformanceError, match="fewer than"):
2796
+ phase.parse_phase1("p1.md", text, FULL, "methodology")
2797
+
2798
+
2799
+ def test_phase1_prose_mentioning_a_comment_opener_still_counts():
2800
+ """The comment-state entry is conservative: a paragraph that merely
2801
+ mentions `<!--` mid-line (or contains a closed comment) must not be
2802
+ swallowed as hidden.
2803
+ """
2804
+ dims = FULL["acceptance_dimensions"]
2805
+ body = "\n\n".join(
2806
+ f"{dim['id']} concerns {dim['name']}; authors sometimes hide "
2807
+ "text with <!-- markers --> in drafts." for dim in dims)
2808
+ text = re.sub(
2809
+ r"## Contract Paraphrase.*?(?=## Scoring Plan)",
2810
+ f"## Contract Paraphrase\n\n{body}\n\n",
2811
+ phase1_text("methodology"), flags=re.S)
2812
+ parsed = phase.parse_phase1("p1.md", text, FULL, "methodology")
2813
+ assert parsed is not None
2814
+
2815
+
2816
+ def test_phase1_lone_list_markers_do_not_satisfy_the_floor():
2817
+ """codex r46: six lone `-` (or `*`, `+`) list markers are empty
2818
+ Markdown constructs, each counted as a paragraph. A bare marker
2819
+ joins the closed separator list; a marker WITH text still counts.
2820
+ """
2821
+ for marker in ("-", "*", "+"):
2822
+ blocks = "\n\n".join([marker] * len(FULL["acceptance_dimensions"]))
2823
+ text = re.sub(
2824
+ r"## Contract Paraphrase.*?(?=## Scoring Plan)",
2825
+ f"## Contract Paraphrase\n\n{blocks}\n\n",
2826
+ phase1_text("methodology"), flags=re.S)
2827
+ with pytest.raises(phase.ConformanceError, match="fewer than"):
2828
+ phase.parse_phase1("p1.md", text, FULL, "methodology")
2829
+
2830
+
2831
+ # --- #610 methodology arithmetic-receipt gate ----------------------------
2832
+
2833
+
2834
+ W1_BACKREF_BODY = (
2835
+ "### W1: reported mean is unreachable\n"
2836
+ "**Severity**: Critical\n"
2837
+ "**Evidence Anchor**: table: Table 2, M=3.847 with N=87\n"
2838
+ "**Confidence**: 5 — quantitative methods reviewer\n"
2839
+ "**Arithmetic Receipt**: AR1\n"
2840
+ )
2841
+
2842
+
2843
+ def grim_receipt(**overrides) -> list[str]:
2844
+ fields = {
2845
+ "procedure_id": "grim",
2846
+ "evidence_anchor": "table: Table 2, M=3.847 with N=87",
2847
+ "reported_inputs": "single 1-5 integer item, N=87, M=3.847, "
2848
+ "three-decimal precision",
2849
+ "assumptions": "unweighted single-item mean as stated in §3.2",
2850
+ "derivation": "integer sums 334 and 335 bracket 87 * 3.847",
2851
+ "derived_value_or_range": "334/87 = 3.8390...; 335/87 = 3.8505...",
2852
+ "comparison_rule": "an achievable sum must round to 3.847 at three "
2853
+ "decimals",
2854
+ "rounding_interval": "[3.8465, 3.8475)",
2855
+ "nearest_achievable": "334/87 = 3.8390...; 335/87 = 3.8505...",
2856
+ "status": "mismatch",
2857
+ "finding_ref": "W1",
2858
+ }
2859
+ fields.update(overrides)
2860
+ return [f"{key}: {value}" for key, value in fields.items()
2861
+ if value is not None]
2862
+
2863
+
2864
+ def p_receipt(**overrides) -> list[str]:
2865
+ fields = {
2866
+ "procedure_id": "p_from_test_statistic",
2867
+ "evidence_anchor": 'text: §4.1 "t(140) = 1.31, p = .008"',
2868
+ "reported_inputs": "independent t, t=1.31, df=140, p=.008, no tail "
2869
+ "stated",
2870
+ "assumptions": "central t distribution; no paper-licensed tail",
2871
+ "tail_convention": "unstated",
2872
+ "derivation": "p from t=1.31 at df=140 under both tail readings",
2873
+ "derived_value_or_range": "two-tailed p ≈ .192; one-tailed p ≈ .096",
2874
+ "comparison_rule": "reported .008 must match either tail value at "
2875
+ "reported precision",
2876
+ "status": "mismatch",
2877
+ "finding_ref": "W1",
2878
+ }
2879
+ fields.update(overrides)
2880
+ return [f"{key}: {value}" for key, value in fields.items()
2881
+ if value is not None]
2882
+
2883
+
2884
+ def n_from_df_receipt(**overrides) -> list[str]:
2885
+ fields = {
2886
+ "procedure_id": "n_from_df",
2887
+ "evidence_anchor": 'text: §4.2 "t(156)" against the stated maximum '
2888
+ 'analytic sample',
2889
+ "reported_inputs": "independent-groups t with df=156; stated maximum "
2890
+ "analytic N=142",
2891
+ "assumptions": "equal-variance independent t as the paper names",
2892
+ "df_identity": "df = N1 + N2 - 2",
2893
+ "derivation": "df=156 requires total N=158 under the named identity",
2894
+ "derived_value_or_range": "required N = 158",
2895
+ "comparison_rule": "required N must not exceed the stated analytic "
2896
+ "ceiling of 142",
2897
+ "status": "mismatch",
2898
+ "finding_ref": "W1",
2899
+ }
2900
+ fields.update(overrides)
2901
+ return [f"{key}: {value}" for key, value in fields.items()
2902
+ if value is not None]
2903
+
2904
+
2905
+ def receipt_section(*receipts: list[str]) -> list[str]:
2906
+ lines: list[str] = []
2907
+ for index, fields in enumerate(receipts, start=1):
2908
+ lines += [f"### AR{index}", *fields, ""]
2909
+ return lines
2910
+
2911
+
2912
+ def methodology_report(receipts=None, body=W1_BACKREF_BODY):
2913
+ text = phase2_text("methodology", body=body, receipts=receipts)
2914
+ return panel.parse_report("p2.md", text, FULL)
2915
+
2916
+
2917
+ def check_receipts(receipts=None, body=W1_BACKREF_BODY):
2918
+ phase.check_methodology_receipts(methodology_report(receipts, body))
2919
+
2920
+
2921
+ def test_receipt_attestation_card_passes():
2922
+ check_receipts(receipts=None, body="prose review body without findings")
2923
+
2924
+
2925
+ def test_valid_grim_mismatch_receipt_passes():
2926
+ check_receipts(receipt_section(grim_receipt()))
2927
+
2928
+
2929
+ def test_valid_p_unstated_both_tails_receipt_passes():
2930
+ check_receipts(receipt_section(p_receipt()))
2931
+
2932
+
2933
+ def test_valid_n_from_df_receipt_passes():
2934
+ check_receipts(receipt_section(n_from_df_receipt()))
2935
+
2936
+
2937
+ def test_valid_consistent_receipt_needs_no_finding():
2938
+ check_receipts(
2939
+ receipt_section(
2940
+ grim_receipt(status="consistent", finding_ref=None)
2941
+ ),
2942
+ body="prose only",
2943
+ )
2944
+
2945
+
2946
+ def test_valid_not_computable_receipt_passes():
2947
+ check_receipts(
2948
+ receipt_section(p_receipt(
2949
+ status="not_computable",
2950
+ not_computable_reason="tail_ambiguous",
2951
+ finding_ref=None,
2952
+ derived_value_or_range="not derived; tail choice flips the "
2953
+ "verdict",
2954
+ )),
2955
+ body="prose only",
2956
+ )
2957
+
2958
+
2959
+ def test_receipt_lines_tolerate_bold_and_list_decoration():
2960
+ decorated = [
2961
+ line if ":" not in line else "- **" + line.replace(": ", "**: ", 1)
2962
+ for line in grim_receipt()
2963
+ ]
2964
+ check_receipts(receipt_section(decorated))
2965
+
2966
+
2967
+ def test_missing_receipt_section_fails():
2968
+ text = phase2_text("methodology", body=W1_BACKREF_BODY)
2969
+ text = text[: text.index("\n## Arithmetic Receipts")]
2970
+ report = panel.parse_report("p2.md", text, FULL)
2971
+ with pytest.raises(phase.ConformanceError, match=r"RECEIPT-MISSING"):
2972
+ phase.check_methodology_receipts(report)
2973
+
2974
+
2975
+ def test_duplicate_receipt_section_fails():
2976
+ text = phase2_text("methodology", body="prose") + \
2977
+ "\n\n## Arithmetic Receipts\n\nno_recomputable_statistics: twice\n"
2978
+ report = panel.parse_report("p2.md", text, FULL)
2979
+ with pytest.raises(phase.ConformanceError, match="duplicate"):
2980
+ phase.check_methodology_receipts(report)
2981
+
2982
+
2983
+ def test_receipt_section_before_review_body_fails():
2984
+ text = phase2_text("methodology", body="prose", receipts=())
2985
+ text = text[: text.index("\n## Arithmetic Receipts")]
2986
+ text = text.replace(
2987
+ "## Review Body",
2988
+ "## Arithmetic Receipts\n\nno_recomputable_statistics: early\n\n"
2989
+ "## Review Body",
2990
+ 1,
2991
+ )
2992
+ report = panel.parse_report("p2.md", text, FULL)
2993
+ with pytest.raises(
2994
+ phase.ConformanceError, match="must be the final section"
2995
+ ):
2996
+ phase.check_methodology_receipts(report)
2997
+
2998
+
2999
+ def test_empty_receipt_section_without_attestation_fails():
3000
+ with pytest.raises(phase.ConformanceError, match="exactly one"):
3001
+ check_receipts(receipts=(), body="prose")
3002
+
3003
+
3004
+ def test_attestation_alongside_receipts_fails():
3005
+ lines = receipt_section(grim_receipt())
3006
+ lines.append("no_recomputable_statistics: but also receipts")
3007
+ with pytest.raises(phase.ConformanceError, match="forbidden when"):
3008
+ check_receipts(lines)
3009
+
3010
+
3011
+ def test_non_dense_receipt_ids_fail():
3012
+ lines = ["### AR2", *grim_receipt(), ""]
3013
+ with pytest.raises(phase.ConformanceError, match="dense"):
3014
+ check_receipts(lines)
3015
+
3016
+
3017
+ def test_invalid_receipt_heading_fails():
3018
+ lines = ["### AR0", *grim_receipt(), ""]
3019
+ with pytest.raises(phase.ConformanceError, match="invalid receipt"):
3020
+ check_receipts(lines)
3021
+
3022
+
3023
+ def test_duplicate_receipt_heading_fails():
3024
+ lines = [
3025
+ "### AR1", *grim_receipt(), "",
3026
+ "### AR1", *grim_receipt(), "",
3027
+ ]
3028
+ with pytest.raises(phase.ConformanceError, match="duplicate receipt"):
3029
+ check_receipts(lines)
3030
+
3031
+
3032
+ def test_unknown_procedure_id_fails():
3033
+ with pytest.raises(
3034
+ phase.ConformanceError, match="not a bounded procedure"
3035
+ ):
3036
+ check_receipts(
3037
+ receipt_section(grim_receipt(procedure_id="effect_size_check"))
3038
+ )
3039
+
3040
+
3041
+ @pytest.mark.parametrize("key", [
3042
+ "procedure_id", "evidence_anchor", "reported_inputs", "assumptions",
3043
+ "derivation", "derived_value_or_range", "comparison_rule", "status",
3044
+ ])
3045
+ def test_each_missing_canonical_receipt_field_fails(key):
3046
+ with pytest.raises(phase.ConformanceError, match="RECEIPT-GRAMMAR"):
3047
+ check_receipts(receipt_section(grim_receipt(**{key: None})))
3048
+
3049
+
3050
+ def test_duplicated_receipt_field_fails():
3051
+ lines = receipt_section(grim_receipt())
3052
+ lines.insert(2, "status: consistent")
3053
+ with pytest.raises(phase.ConformanceError, match="found 2"):
3054
+ check_receipts(lines)
3055
+
3056
+
3057
+ def test_unknown_status_fails():
3058
+ with pytest.raises(phase.ConformanceError, match="closed status enum"):
3059
+ check_receipts(
3060
+ receipt_section(grim_receipt(status="plausible"))
3061
+ )
3062
+
3063
+
3064
+ def test_not_computable_without_reason_fails():
3065
+ with pytest.raises(phase.ConformanceError, match="not_computable_reason"):
3066
+ check_receipts(
3067
+ receipt_section(grim_receipt(
3068
+ status="not_computable", finding_ref=None
3069
+ )),
3070
+ body="prose",
3071
+ )
3072
+
3073
+
3074
+ def test_reason_on_verdict_status_fails():
3075
+ with pytest.raises(phase.ConformanceError, match="forbidden unless"):
3076
+ check_receipts(
3077
+ receipt_section(grim_receipt(
3078
+ not_computable_reason="rounding_rule_ambiguous"
3079
+ ))
3080
+ )
3081
+
3082
+
3083
+ def test_unknown_not_computable_reason_fails():
3084
+ with pytest.raises(phase.ConformanceError, match="closed v1 enum"):
3085
+ check_receipts(
3086
+ receipt_section(grim_receipt(
3087
+ status="not_computable",
3088
+ not_computable_reason="model_was_unsure",
3089
+ finding_ref=None,
3090
+ )),
3091
+ body="prose",
3092
+ )
3093
+
3094
+
3095
+ def test_p_receipt_without_tail_convention_fails():
3096
+ with pytest.raises(phase.ConformanceError, match="tail_convention"):
3097
+ check_receipts(receipt_section(p_receipt(tail_convention=None)))
3098
+
3099
+
3100
+ def test_tail_convention_on_grim_is_forbidden():
3101
+ with pytest.raises(phase.ConformanceError, match="forbidden for"):
3102
+ check_receipts(
3103
+ receipt_section(grim_receipt(tail_convention="two-tailed"))
3104
+ )
3105
+
3106
+
3107
+ def test_unknown_tail_convention_fails():
3108
+ with pytest.raises(phase.ConformanceError, match="closed enum"):
3109
+ check_receipts(
3110
+ receipt_section(p_receipt(tail_convention="lower-tail"))
3111
+ )
3112
+
3113
+
3114
+ @pytest.mark.parametrize("derived", [
3115
+ "two-tailed p ≈ .192 only",
3116
+ "one-tailed p ≈ .096 only",
3117
+ "p ≈ .192 under the default reading",
3118
+ ])
3119
+ def test_unstated_tail_verdict_must_show_both_tails(derived):
3120
+ with pytest.raises(phase.ConformanceError, match="RECEIPT-TAILS"):
3121
+ check_receipts(
3122
+ receipt_section(p_receipt(derived_value_or_range=derived))
3123
+ )
3124
+
3125
+
3126
+ def test_stated_tail_needs_no_both_tail_display():
3127
+ check_receipts(
3128
+ receipt_section(p_receipt(
3129
+ tail_convention="two-tailed",
3130
+ reported_inputs="paired t, t=1.31, df=140, p=.008, two-tailed "
3131
+ "stated in §4.1",
3132
+ derived_value_or_range="two-tailed p ≈ .192",
3133
+ ))
3134
+ )
3135
+
3136
+
3137
+ @pytest.mark.parametrize("key", ["rounding_interval", "nearest_achievable"])
3138
+ def test_grim_verdict_without_reachability_fields_fails(key):
3139
+ with pytest.raises(phase.ConformanceError, match=key):
3140
+ check_receipts(receipt_section(grim_receipt(**{key: None})))
3141
+
3142
+
3143
+ def test_grim_not_computable_may_omit_reachability_fields():
3144
+ check_receipts(
3145
+ receipt_section(grim_receipt(
3146
+ status="not_computable",
3147
+ not_computable_reason="scale_granularity_unknown",
3148
+ rounding_interval=None,
3149
+ nearest_achievable=None,
3150
+ finding_ref=None,
3151
+ )),
3152
+ body="prose",
3153
+ )
3154
+
3155
+
3156
+ def test_rounding_interval_on_p_procedure_is_forbidden():
3157
+ with pytest.raises(phase.ConformanceError, match="forbidden for"):
3158
+ check_receipts(
3159
+ receipt_section(p_receipt(rounding_interval="[.0075, .0085)"))
3160
+ )
3161
+
3162
+
3163
+ def test_n_from_df_verdict_without_identity_fails():
3164
+ with pytest.raises(phase.ConformanceError, match="df_identity"):
3165
+ check_receipts(receipt_section(n_from_df_receipt(df_identity=None)))
3166
+
3167
+
3168
+ def test_df_identity_on_grim_is_forbidden():
3169
+ with pytest.raises(phase.ConformanceError, match="forbidden for"):
3170
+ check_receipts(
3171
+ receipt_section(grim_receipt(df_identity="df = N - 1"))
3172
+ )
3173
+
3174
+
3175
+ def test_invalid_receipt_anchor_fails():
3176
+ with pytest.raises(phase.ConformanceError):
3177
+ check_receipts(
3178
+ receipt_section(grim_receipt(evidence_anchor="somewhere in §4"))
3179
+ )
3180
+
3181
+
3182
+ def test_mismatch_without_finding_ref_fails():
3183
+ with pytest.raises(phase.ConformanceError, match="finding_ref"):
3184
+ check_receipts(receipt_section(grim_receipt(finding_ref=None)))
3185
+
3186
+
3187
+ def test_finding_ref_on_consistent_receipt_fails():
3188
+ with pytest.raises(phase.ConformanceError, match="RECEIPT-LINKAGE"):
3189
+ check_receipts(
3190
+ receipt_section(grim_receipt(status="consistent"))
3191
+ )
3192
+
3193
+
3194
+ def test_malformed_finding_ref_fails():
3195
+ with pytest.raises(phase.ConformanceError, match="must name one W<n>"):
3196
+ check_receipts(receipt_section(grim_receipt(finding_ref="C1")))
3197
+
3198
+
3199
+ def test_finding_ref_to_absent_weakness_fails():
3200
+ with pytest.raises(phase.ConformanceError, match="no matching"):
3201
+ check_receipts(receipt_section(grim_receipt(finding_ref="W9")))
3202
+
3203
+
3204
+ def test_two_receipts_sharing_a_finding_ref_fail():
3205
+ with pytest.raises(phase.ConformanceError, match="share a finding_ref"):
3206
+ check_receipts(
3207
+ receipt_section(grim_receipt(), n_from_df_receipt())
3208
+ )
3209
+
3210
+
3211
+ def test_mismatch_weakness_without_backref_fails():
3212
+ body = W1_BACKREF_BODY.replace("**Arithmetic Receipt**: AR1\n", "")
3213
+ with pytest.raises(phase.ConformanceError, match="back-reference"):
3214
+ check_receipts(receipt_section(grim_receipt()), body=body)
3215
+
3216
+
3217
+ def test_backref_naming_wrong_receipt_fails():
3218
+ body = W1_BACKREF_BODY.replace(
3219
+ "**Arithmetic Receipt**: AR1", "**Arithmetic Receipt**: AR2"
3220
+ )
3221
+ with pytest.raises(phase.ConformanceError, match="RECEIPT-LINKAGE"):
3222
+ check_receipts(receipt_section(grim_receipt()), body=body)
3223
+
3224
+
3225
+ def test_stale_backref_without_mismatch_receipt_fails():
3226
+ with pytest.raises(
3227
+ phase.ConformanceError, match="does not correspond"
3228
+ ):
3229
+ check_receipts(
3230
+ receipt_section(
3231
+ grim_receipt(status="consistent", finding_ref=None)
3232
+ ),
3233
+ body=W1_BACKREF_BODY,
3234
+ )
3235
+
3236
+
3237
+ def test_fenced_receipt_content_is_read_not_dropped():
3238
+ # #610 round-1 fix 3, false-abort direction (#637 family): a model that
3239
+ # fences its receipt block still wrote the receipts, so the section is
3240
+ # read fence-transparently and a well-formed fenced block passes.
3241
+ lines = ["```", *receipt_section(grim_receipt()), "```"]
3242
+ check_receipts(lines)
3243
+
3244
+
3245
+ def test_fenced_receipts_beside_unfenced_attestation_abort():
3246
+ # #610 round-1 fix 3, hiding direction: a fenced AR block cannot vanish
3247
+ # and launder the section into an attestation-only card.
3248
+ lines = [
3249
+ "no_recomputable_statistics: nothing recomputable here",
3250
+ "",
3251
+ "```",
3252
+ *receipt_section(grim_receipt()),
3253
+ "```",
3254
+ ]
3255
+ with pytest.raises(phase.ConformanceError, match="forbidden when"):
3256
+ check_receipts(lines)
3257
+
3258
+
3259
+ def test_fenced_malformed_receipt_still_fails_its_field_counts():
3260
+ lines = ["```", *receipt_section(grim_receipt(status=None)), "```"]
3261
+ with pytest.raises(phase.ConformanceError, match="exactly one"):
3262
+ check_receipts(lines)
3263
+
3264
+
3265
+ def test_fenced_h2_inside_receipt_section_does_not_delimit():
3266
+ lines = receipt_section(grim_receipt()) + [
3267
+ "```",
3268
+ "## Not A Real Section",
3269
+ "```",
3270
+ ]
3271
+ check_receipts(lines)
3272
+
3273
+
3274
+ def test_receipt_section_must_be_the_final_h2():
3275
+ # #610 round-1 fix 6: the prompt says "final section"; the checker
3276
+ # enforces exactly that, not merely "after Review Body".
3277
+ text = phase2_text(
3278
+ "methodology",
3279
+ body=W1_BACKREF_BODY,
3280
+ receipts=receipt_section(grim_receipt()),
3281
+ ) + "\n\n## Trailing Notes\n\nnothing\n"
3282
+ report = panel.parse_report("p2.md", text, FULL)
3283
+ with pytest.raises(
3284
+ phase.ConformanceError, match="must be the final section"
3285
+ ):
3286
+ phase.check_methodology_receipts(report)
3287
+
3288
+
3289
+ @pytest.mark.parametrize("derived", [
3290
+ "two-tailed; one-tailed p ≈ .096",
3291
+ "two-tailed p ≈ .192; one-tailed",
3292
+ "two-tailed and one-tailed; both p ≈ .192",
3293
+ ])
3294
+ def test_unstated_tail_label_without_value_in_segment_fails(derived):
3295
+ # #610 round-1 fix 1: a bare label is not a shown value — each label
3296
+ # needs a digit inside its own `;`-delimited segment.
3297
+ with pytest.raises(phase.ConformanceError, match="RECEIPT-TAILS"):
3298
+ check_receipts(
3299
+ receipt_section(p_receipt(derived_value_or_range=derived))
3300
+ )
3301
+
3302
+
3303
+ @pytest.mark.parametrize("derived", [
3304
+ "two–tailed p ≈ .192; one–tailed p ≈ .096",
3305
+ "two‐tailed p = 0.192; one tailed p = 0.096",
3306
+ "TWO-TAILED p ≈ .192; One-Tailed p ≈ .096",
3307
+ ])
3308
+ def test_unstated_tail_hyphen_and_case_variants_pass(derived):
3309
+ check_receipts(
3310
+ receipt_section(p_receipt(derived_value_or_range=derived))
3311
+ )
3312
+
3313
+
3314
+ def test_attestation_pass_prints_declaration_only_advisory(capsys):
3315
+ # #610 round-1 fix 2: the attestation is declaration-only; the pass is
3316
+ # annotated so the run record cannot read it as machine-verified
3317
+ # applicability.
3318
+ check_receipts(receipts=None, body="prose review body without findings")
3319
+ out = capsys.readouterr().out
3320
+ assert "[RECEIPT-ATTESTATION: declaration-only" in out
3321
+
3322
+
3323
+ def test_receipt_pass_prints_no_attestation_advisory(capsys):
3324
+ check_receipts(receipt_section(grim_receipt()))
3325
+ assert "[RECEIPT-ATTESTATION" not in capsys.readouterr().out
3326
+
3327
+
3328
+ @pytest.mark.parametrize("decorated", [
3329
+ "**status:** mismatch",
3330
+ "| status: mismatch |",
3331
+ "1. status: mismatch",
3332
+ "> status: mismatch",
3333
+ " status: mismatch",
3334
+ "`status`: mismatch",
3335
+ "sTaTus: mismatch",
3336
+ "status: mismatch",
3337
+ ])
3338
+ def test_decorated_status_line_aborts_loudly(decorated):
3339
+ # #610 round-1 fix 4: an unenumerated decoration of a machine field is a
3340
+ # loud declaration, never a silently starved required-field count.
3341
+ lines = receipt_section(grim_receipt(status=None))
3342
+ lines.insert(lines.index("finding_ref: W1"), decorated)
3343
+ with pytest.raises(
3344
+ phase.ConformanceError, match="decorated or non-canonical"
3345
+ ):
3346
+ check_receipts(lines)
3347
+
3348
+
3349
+ def test_decorated_forbidden_field_cannot_pass_silently():
3350
+ # A decorated spelling of a field the procedure forbids used to sail
3351
+ # through the forbidden-field guard unseen; it now aborts.
3352
+ lines = receipt_section(grim_receipt())
3353
+ lines.insert(
3354
+ lines.index("finding_ref: W1"), "**tail_convention:** two-tailed"
3355
+ )
3356
+ with pytest.raises(
3357
+ phase.ConformanceError, match="decorated or non-canonical"
3358
+ ):
3359
+ check_receipts(lines)
3360
+
3361
+
3362
+ def test_prose_naming_a_field_mid_sentence_stays_tolerated():
3363
+ lines = receipt_section(grim_receipt())
3364
+ lines.insert(
3365
+ lines.index("finding_ref: W1"),
3366
+ "The reported status: a value the paper never reconciles.",
3367
+ )
3368
+ check_receipts(lines)
3369
+
3370
+
3371
+ @pytest.mark.parametrize("backref", [
3372
+ "**Arithmetic Receipt**: AR1, AR2",
3373
+ "**Arithmetic Receipt**: AR1 which shows the mismatch",
3374
+ "**Arithmetic Receipt:** AR1",
3375
+ "**Arithmetic Receipt**: see AR1",
3376
+ ])
3377
+ def test_non_exact_backref_value_aborts(backref):
3378
+ # #610 round-1 fix 5: the back-reference value is exactly AR<n> to end
3379
+ # of line or next pipe; list values and trailing prose abort.
3380
+ body = W1_BACKREF_BODY.replace("**Arithmetic Receipt**: AR1", backref)
3381
+ with pytest.raises(phase.ConformanceError, match="RECEIPT-LINKAGE"):
3382
+ check_receipts(receipt_section(grim_receipt()), body=body)
3383
+
3384
+
3385
+ def test_fenced_backref_declaration_aborts():
3386
+ body = W1_BACKREF_BODY.replace(
3387
+ "**Arithmetic Receipt**: AR1\n",
3388
+ "```\n**Arithmetic Receipt**: AR1\n```\n",
3389
+ )
3390
+ with pytest.raises(phase.ConformanceError, match="RECEIPT-LINKAGE"):
3391
+ check_receipts(receipt_section(grim_receipt()), body=body)
3392
+
3393
+
3394
+ def test_prose_mentioning_the_receipts_section_stays_tolerated():
3395
+ body = W1_BACKREF_BODY + (
3396
+ "\nSee the Arithmetic Receipts section: AR1 documents the "
3397
+ "reachability check.\n"
3398
+ )
3399
+ check_receipts(receipt_section(grim_receipt()), body=body)
3400
+
3401
+
3402
+ def grimmer_receipt(**overrides) -> list[str]:
3403
+ # Spec §5.3 prospective MS01 v0.2 worked case: N=10, M=3.00, SD=0.10.
3404
+ fields = {
3405
+ "procedure_id": "grimmer",
3406
+ "evidence_anchor": "table: Table 3, SD=0.10 with N=10, M=3.00",
3407
+ "reported_inputs": "single 1-5 integer item, N=10, M=3.00, SD=0.10, "
3408
+ "sample SD, two-decimal precision",
3409
+ "assumptions": "sample SD as stated in §3.4; integer responses on "
3410
+ "the published 1-5 scale",
3411
+ "derivation": "sum fixed at 30; all-3s give SD 0; any deviation "
3412
+ "pair gives squared-deviation sum >= 2, so minimum "
3413
+ "nonzero sample SD is sqrt(2/9)",
3414
+ "derived_value_or_range": "attainable sample SDs: 0 or >= 0.4714...",
3415
+ "comparison_rule": "an attainable SD must round to 0.10 at two "
3416
+ "decimals",
3417
+ "rounding_interval": "[0.095, 0.105)",
3418
+ "nearest_achievable": "0 and sqrt(2/9) = 0.4714... straddle the "
3419
+ "reported 0.10",
3420
+ "status": "mismatch",
3421
+ "finding_ref": "W1",
3422
+ }
3423
+ fields.update(overrides)
3424
+ return [f"{key}: {value}" for key, value in fields.items()
3425
+ if value is not None]
3426
+
3427
+
3428
+ def test_valid_grimmer_mismatch_receipt_passes():
3429
+ check_receipts(receipt_section(grimmer_receipt()))
3430
+
3431
+
3432
+ @pytest.mark.parametrize("key", [
3433
+ "rounding_interval", "nearest_achievable", "derivation",
3434
+ ])
3435
+ def test_grimmer_verdict_missing_field_fails(key):
3436
+ with pytest.raises(phase.ConformanceError, match=key):
3437
+ check_receipts(receipt_section(grimmer_receipt(**{key: None})))
3438
+
3439
+
3440
+ # --- #610 round-2 pins: comment visibility, preamble, cells, tails ---------
3441
+
3442
+
3443
+ def test_comment_hidden_receipt_block_aborts():
3444
+ # Round-2 P1 (both tracks): a receipt the rendered card does not show
3445
+ # cannot satisfy an auditability gate.
3446
+ lines = ["<!--", *receipt_section(grim_receipt()), "-->"]
3447
+ with pytest.raises(phase.ConformanceError, match="HTML comment"):
3448
+ check_receipts(lines)
3449
+
3450
+
3451
+ def test_comment_hidden_attestation_aborts():
3452
+ lines = ["<!--", "no_recomputable_statistics: nothing", "-->"]
3453
+ with pytest.raises(phase.ConformanceError, match="HTML comment"):
3454
+ check_receipts(lines, body="prose")
3455
+
3456
+
3457
+ def test_unclosed_comment_cannot_swallow_the_receipts():
3458
+ lines = ["<!--", *receipt_section(grim_receipt())]
3459
+ with pytest.raises(phase.ConformanceError, match="HTML comment"):
3460
+ check_receipts(lines)
3461
+
3462
+
3463
+ def test_visible_receipts_with_hidden_duplicate_field_abort():
3464
+ lines = receipt_section(grim_receipt()) + [
3465
+ "<!--", "status: consistent", "-->",
3466
+ ]
3467
+ with pytest.raises(phase.ConformanceError, match="HTML comment"):
3468
+ check_receipts(lines)
3469
+
3470
+
3471
+ def test_comment_markup_in_receipt_section_aborts():
3472
+ # Round-3 adjudication supersedes the round-2 commented-prose
3473
+ # tolerance: the receipt section is a comment-free zone, because a
3474
+ # paragraph-inline `prose <!--` opener the block visibility model
3475
+ # cannot read would otherwise launder the receipts below it.
3476
+ lines = receipt_section(grim_receipt()) + [
3477
+ "<!--", "internal note, no machine fields", "-->",
3478
+ ]
3479
+ with pytest.raises(phase.ConformanceError, match="HTML comment markup"):
3480
+ check_receipts(lines)
3481
+
3482
+
3483
+ def test_inline_comment_opener_in_receipt_section_aborts():
3484
+ lines = [
3485
+ "visible explanation <!--",
3486
+ *receipt_section(grim_receipt()),
3487
+ "-->",
3488
+ ]
3489
+ with pytest.raises(phase.ConformanceError, match="HTML comment markup"):
3490
+ check_receipts(lines)
3491
+
3492
+
3493
+ def test_fenced_comment_markup_in_receipts_is_literal_text():
3494
+ lines = receipt_section(grim_receipt()) + [
3495
+ "```", "<!-- rendered literally inside the fence -->", "```",
3496
+ ]
3497
+ check_receipts(lines)
3498
+
3499
+
3500
+ def test_inline_comment_span_hidden_backref_aborts():
3501
+ # Round-3 P1 (codex track): `prose <!--` inside a paragraph hides the
3502
+ # following lines until `-->` without the block model seeing it.
3503
+ body = W1_BACKREF_BODY.replace(
3504
+ "**Arithmetic Receipt**: AR1\n",
3505
+ "prose lead-in <!--\n**Arithmetic Receipt**: AR1\n-->\n",
3506
+ )
3507
+ with pytest.raises(phase.ConformanceError, match="paragraph-inline"):
3508
+ check_receipts(receipt_section(grim_receipt()), body=body)
3509
+
3510
+
3511
+ def test_inline_comment_span_dies_at_a_blank_line():
3512
+ # A blank line ends the paragraph and with it the raw-HTML span, so a
3513
+ # backref in the NEXT paragraph is live and the card conforms.
3514
+ body = W1_BACKREF_BODY.replace(
3515
+ "**Arithmetic Receipt**: AR1\n",
3516
+ "prose mentioning <!-- an unclosed marker\n\n"
3517
+ "**Arithmetic Receipt**: AR1\n",
3518
+ )
3519
+ check_receipts(receipt_section(grim_receipt()), body=body)
3520
+
3521
+
3522
+ @pytest.mark.parametrize("entity_line", [
3523
+ "tail_convention&#xFF1A; two-tailed",
3524
+ "status&#65306; mismatch",
3525
+ ])
3526
+ def test_fullwidth_colon_entity_field_spelling_aborts(entity_line):
3527
+ # Round-3 P1 (codex track): unescape must run BEFORE the NFKC fold so
3528
+ # a fullwidth-colon entity decodes and then folds to `:`.
3529
+ lines = receipt_section(grim_receipt())
3530
+ lines.insert(lines.index("finding_ref: W1"), entity_line)
3531
+ with pytest.raises(
3532
+ phase.ConformanceError, match="decorated or non-canonical"
3533
+ ):
3534
+ check_receipts(lines)
3535
+
3536
+
3537
+ def test_fullwidth_colon_entity_backref_aborts():
3538
+ body = W1_BACKREF_BODY.replace(
3539
+ "**Arithmetic Receipt**: AR1",
3540
+ "**Arithmetic Receipt**: AR1\n**Arithmetic Receipt**&#xFF1A; AR2",
3541
+ )
3542
+ with pytest.raises(phase.ConformanceError, match="RECEIPT-LINKAGE"):
3543
+ check_receipts(receipt_section(grim_receipt()), body=body)
3544
+
3545
+
3546
+ def test_pipe_inside_link_destination_is_not_a_cell( # codex round-3 P2
3547
+ ):
3548
+ lines = receipt_section(grim_receipt())
3549
+ lines.insert(
3550
+ lines.index("finding_ref: W1"),
3551
+ "Quoted source: [record](https://example.org/a|status:pending)",
3552
+ )
3553
+ check_receipts(lines)
3554
+
3555
+
3556
+ def test_escaped_pipe_and_code_span_pipes_are_not_cells():
3557
+ lines = receipt_section(grim_receipt())
3558
+ lines.insert(
3559
+ lines.index("finding_ref: W1"),
3560
+ "See the raw marker \\| and the cited `a|status:pending` token.",
3561
+ )
3562
+ check_receipts(lines)
3563
+
3564
+
3565
+ def test_comment_hidden_backref_aborts():
3566
+ body = W1_BACKREF_BODY.replace(
3567
+ "**Arithmetic Receipt**: AR1\n",
3568
+ "<!--\n**Arithmetic Receipt**: AR1\n-->\n",
3569
+ )
3570
+ with pytest.raises(phase.ConformanceError, match="commented-out"):
3571
+ check_receipts(receipt_section(grim_receipt()), body=body)
3572
+
3573
+
3574
+ def test_indented_code_backref_is_not_credited():
3575
+ # Round-3 (security track): a 4-column-indented back-reference renders
3576
+ # as literal indented code, not a field line, and must not earn the
3577
+ # linkage credit its fenced twin is denied.
3578
+ body = W1_BACKREF_BODY.replace(
3579
+ "**Arithmetic Receipt**: AR1\n",
3580
+ "\n **Arithmetic Receipt**: AR1\n",
3581
+ )
3582
+ with pytest.raises(phase.ConformanceError, match="indented-code"):
3583
+ check_receipts(receipt_section(grim_receipt()), body=body)
3584
+
3585
+
3586
+ def test_four_space_paragraph_continuation_backref_still_counts():
3587
+ # A 4-space-indented line CONTINUING an open paragraph is prose to
3588
+ # CommonMark, not code; the backref on it renders with its bold applied
3589
+ # and stays a live declaration.
3590
+ body = W1_BACKREF_BODY.replace(
3591
+ "**Arithmetic Receipt**: AR1\n",
3592
+ "prose lead-in line\n **Arithmetic Receipt**: AR1\n",
3593
+ )
3594
+ check_receipts(receipt_section(grim_receipt()), body=body)
3595
+
3596
+
3597
+ @pytest.mark.parametrize("bad", [
3598
+ "procedure_id: cohen_d_check",
3599
+ "finding_ref: W9",
3600
+ "not_computable_reason: bogus_reason",
3601
+ "status: not_applicable",
3602
+ ])
3603
+ def test_machine_line_in_section_preamble_aborts(bad):
3604
+ # Round-2 P1 (security track): the preamble is not a parking lot — a
3605
+ # canonical machine line the enum/linkage gates never inspect aborts.
3606
+ lines = [bad, "", *receipt_section(grim_receipt())]
3607
+ with pytest.raises(
3608
+ phase.ConformanceError, match="outside every ### AR<n>"
3609
+ ):
3610
+ check_receipts(lines)
3611
+
3612
+
3613
+ def test_attestation_card_with_stray_machine_line_aborts():
3614
+ lines = [
3615
+ "no_recomputable_statistics: nothing recomputable",
3616
+ "status: consistent",
3617
+ ]
3618
+ with pytest.raises(
3619
+ phase.ConformanceError, match="outside every ### AR<n>"
3620
+ ):
3621
+ check_receipts(lines, body="prose")
3622
+
3623
+
3624
+ def test_forbidden_field_in_a_later_table_cell_aborts():
3625
+ # Round-2 P1 (codex track): the head-of-line shape test alone let a
3626
+ # decorated forbidden field hide in a later GFM cell.
3627
+ lines = receipt_section(grim_receipt())
3628
+ lines.insert(
3629
+ lines.index("finding_ref: W1"),
3630
+ "| note | **tail_convention:** two-tailed |",
3631
+ )
3632
+ with pytest.raises(
3633
+ phase.ConformanceError, match="decorated or non-canonical"
3634
+ ):
3635
+ check_receipts(lines)
3636
+
3637
+
3638
+ def test_entity_colon_field_spelling_aborts():
3639
+ lines = receipt_section(grim_receipt())
3640
+ lines.insert(
3641
+ lines.index("finding_ref: W1"), "tail_convention&#58; two-tailed"
3642
+ )
3643
+ with pytest.raises(
3644
+ phase.ConformanceError, match="decorated or non-canonical"
3645
+ ):
3646
+ check_receipts(lines)
3647
+
3648
+
3649
+ def test_cjk_prose_in_a_table_cell_stays_tolerated():
3650
+ lines = receipt_section(grim_receipt())
3651
+ lines.insert(lines.index("finding_ref: W1"), "| 註記 | 狀態說明:如上 |")
3652
+ check_receipts(lines)
3653
+
3654
+
3655
+ @pytest.mark.parametrize("half_bold", ["**status: mismatch", "status**: mismatch"])
3656
+ def test_half_bold_field_is_not_canonical(half_bold):
3657
+ lines = receipt_section(grim_receipt(status=None))
3658
+ lines.insert(lines.index("finding_ref: W1"), half_bold)
3659
+ with pytest.raises(
3660
+ phase.ConformanceError, match="decorated or non-canonical"
3661
+ ):
3662
+ check_receipts(lines)
3663
+
3664
+
3665
+ def test_canonical_plus_malformed_backref_on_one_line_aborts():
3666
+ # Round-2 P1 (codex track): a canonical match must not launder a second
3667
+ # malformed declaration on the same line.
3668
+ body = W1_BACKREF_BODY.replace(
3669
+ "**Arithmetic Receipt**: AR1",
3670
+ "**Arithmetic Receipt**: AR1 | **Arithmetic Receipt:** see AR2",
3671
+ )
3672
+ with pytest.raises(phase.ConformanceError, match="RECEIPT-LINKAGE"):
3673
+ check_receipts(receipt_section(grim_receipt()), body=body)
3674
+
3675
+
3676
+ def test_indented_trailing_h2_still_violates_terminal_rule():
3677
+ # Round-2 (security track): a 1-3-space-indented `##` renders as a
3678
+ # heading even though the section grammar ignores it.
3679
+ text = phase2_text(
3680
+ "methodology",
3681
+ body=W1_BACKREF_BODY,
3682
+ receipts=receipt_section(grim_receipt()),
3683
+ ) + "\n\n ## Trailing Section\n\ntext\n"
3684
+ report = panel.parse_report("p2.md", text, FULL)
3685
+ with pytest.raises(
3686
+ phase.ConformanceError, match="must be the final section"
3687
+ ):
3688
+ phase.check_methodology_receipts(report)
3689
+
3690
+
3691
+ def test_three_space_indented_fence_is_read_dedented():
3692
+ # Round-2 (codex track): CommonMark strips the opener's indent from the
3693
+ # displayed content of an indented fence; the gate reads display form.
3694
+ lines = [" ```"] + [
3695
+ " " + line for line in receipt_section(grim_receipt())
3696
+ ] + [" ```"]
3697
+ check_receipts(lines)
3698
+
3699
+
3700
+ def test_indented_fence_hiding_direction_still_aborts():
3701
+ lines = [
3702
+ "no_recomputable_statistics: nothing",
3703
+ "",
3704
+ " ```",
3705
+ *(" " + line for line in receipt_section(grim_receipt())),
3706
+ " ```",
3707
+ ]
3708
+ with pytest.raises(phase.ConformanceError, match="forbidden when"):
3709
+ check_receipts(lines)
3710
+
3711
+
3712
+ def test_tail_value_before_label_in_segment_passes():
3713
+ check_receipts(receipt_section(p_receipt(
3714
+ derived_value_or_range="p = .192 (two-tailed); p = .096 (one-tailed)",
3715
+ )))
3716
+
3717
+
3718
+ def test_embedded_word_tail_labels_do_not_satisfy_the_rule():
3719
+ with pytest.raises(phase.ConformanceError, match="RECEIPT-TAILS"):
3720
+ check_receipts(receipt_section(p_receipt(
3721
+ derived_value_or_range="notwo-tailed note 2; someone-tailed 1",
3722
+ )))
3723
+
3724
+
3725
+ def test_fused_tail_label_with_value_still_counts():
3726
+ # A `twotailed` typo carries its value; rejecting it would be a false
3727
+ # abort on an unretryable phase (declared boundary).
3728
+ check_receipts(receipt_section(p_receipt(
3729
+ derived_value_or_range="twotailed p=.192; onetailed p=.096",
3730
+ )))
3731
+
3732
+
3733
+ def test_field_name_leading_prose_abort_is_a_declared_boundary():
3734
+ # Security-track P2, adjudicated as documented cost: a prose line whose
3735
+ # head spells a field name is indistinguishable from a decorated
3736
+ # machine line, and the fragment now warns the seat not to write one.
3737
+ lines = receipt_section(grim_receipt()) + [
3738
+ "Assumptions: none beyond what the paper licenses.",
3739
+ ]
3740
+ with pytest.raises(
3741
+ phase.ConformanceError, match="decorated or non-canonical"
3742
+ ):
3743
+ check_receipts(lines)
3744
+
3745
+
3746
+ # --- #610 round-4 pins: display-form parity, spans, code semantics ---------
3747
+
3748
+
3749
+ def test_code_span_backref_cannot_outrank_the_rendered_one():
3750
+ # Round-4 P1 (security track): canonical parsing and the declaration
3751
+ # count run on the same display form, so a code-span declaration can
3752
+ # never be credited over the rendered field beside it.
3753
+ body = W1_BACKREF_BODY.replace(
3754
+ "**Arithmetic Receipt**: AR1",
3755
+ "`| **Arithmetic Receipt**: AR1 |` **Arithmetic Receipt**: AR7",
3756
+ )
3757
+ with pytest.raises(phase.ConformanceError, match="exactly one"):
3758
+ check_receipts(receipt_section(grim_receipt()), body=body)
3759
+
3760
+
3761
+ def test_inert_code_span_beside_a_canonical_backref_is_harmless():
3762
+ body = W1_BACKREF_BODY.replace(
3763
+ "**Arithmetic Receipt**: AR1",
3764
+ "`inert` **Arithmetic Receipt**: AR1",
3765
+ )
3766
+ check_receipts(receipt_section(grim_receipt()), body=body)
3767
+
3768
+
3769
+ def test_backref_after_a_closing_comment_on_the_same_line_is_live():
3770
+ # Round-4 (security track): the span ends at `-->`; the rendered
3771
+ # remainder of the line is parsed, not dropped.
3772
+ body = W1_BACKREF_BODY.replace(
3773
+ "**Arithmetic Receipt**: AR1\n",
3774
+ "open <!-- note\nclosed --> **Arithmetic Receipt**: AR1\n",
3775
+ )
3776
+ check_receipts(receipt_section(grim_receipt()), body=body)
3777
+
3778
+
3779
+ def test_spurious_backref_after_a_closing_comment_still_aborts():
3780
+ body = W1_BACKREF_BODY.replace(
3781
+ "**Arithmetic Receipt**: AR1\n",
3782
+ "**Arithmetic Receipt**: AR1\n"
3783
+ "open <!-- x\nclosed --> **Arithmetic Receipt**: AR2\n",
3784
+ )
3785
+ with pytest.raises(phase.ConformanceError, match="exactly one"):
3786
+ check_receipts(receipt_section(grim_receipt()), body=body)
3787
+
3788
+
3789
+ def test_backref_behind_an_opener_on_its_own_line_is_not_credited():
3790
+ # Round-4 P1 (codex track), tightened in round 5: content after `<!--`
3791
+ # on the opener line is inside the span; since round 5 the hidden
3792
+ # declaration itself aborts at the hiding site rather than surfacing
3793
+ # later as a missing linkage.
3794
+ body = W1_BACKREF_BODY.replace(
3795
+ "**Arithmetic Receipt**: AR1\n",
3796
+ "prose <!-- | **Arithmetic Receipt**: AR1\n-->\n",
3797
+ )
3798
+ with pytest.raises(phase.ConformanceError, match="paragraph-inline"):
3799
+ check_receipts(receipt_section(grim_receipt()), body=body)
3800
+
3801
+
3802
+ def test_unequal_code_ticks_do_not_hide_a_later_cell_field():
3803
+ # Round-4 P1 (codex track): a 1-backtick opener with a 2-backtick
3804
+ # closer is NOT a code span to the renderer; the pipe still makes a
3805
+ # cell and the forbidden field in it still aborts.
3806
+ lines = receipt_section(grim_receipt())
3807
+ lines.insert(
3808
+ lines.index("finding_ref: W1"),
3809
+ "| `note | **tail_convention:** two-tailed`` |",
3810
+ )
3811
+ with pytest.raises(
3812
+ phase.ConformanceError, match="decorated or non-canonical"
3813
+ ):
3814
+ check_receipts(lines)
3815
+
3816
+
3817
+ def test_equal_code_ticks_with_pipes_stay_tolerated():
3818
+ lines = receipt_section(grim_receipt())
3819
+ lines.insert(
3820
+ lines.index("finding_ref: W1"),
3821
+ "raw `|status: pending` token cited here.",
3822
+ )
3823
+ check_receipts(lines)
3824
+
3825
+
3826
+ def test_second_line_of_an_indented_code_block_is_still_code():
3827
+ # Round-4 P2 (codex track): an indented-code line never opens a
3828
+ # paragraph, so the next code line is not a paragraph continuation.
3829
+ body = W1_BACKREF_BODY.replace(
3830
+ "**Arithmetic Receipt**: AR1\n",
3831
+ "\n note\n **Arithmetic Receipt**: AR1\n",
3832
+ )
3833
+ with pytest.raises(phase.ConformanceError, match="indented-code"):
3834
+ check_receipts(receipt_section(grim_receipt()), body=body)
3835
+
3836
+
3837
+ def test_literal_comment_marker_in_a_code_span_opens_nothing():
3838
+ # Round-4 P2 (codex track): code spans outrank raw HTML, so a quoted
3839
+ # `<!--` cannot open an inline span and abort the next backref.
3840
+ body = W1_BACKREF_BODY.replace(
3841
+ "**Arithmetic Receipt**: AR1\n",
3842
+ "prose `<!--` literal\n**Arithmetic Receipt**: AR1\n",
3843
+ )
3844
+ check_receipts(receipt_section(grim_receipt()), body=body)
3845
+
3846
+
3847
+ def test_hidden_declaration_beside_a_visible_one_still_aborts():
3848
+ # Round-5 P2 (codex track): a declaration inside the hidden span
3849
+ # aborts even when the same line also carries a visible, credited one.
3850
+ body = W1_BACKREF_BODY.replace(
3851
+ "**Arithmetic Receipt**: AR1\n",
3852
+ "**Arithmetic Receipt**: AR1 <!-- | **Arithmetic Receipt**: AR2\n"
3853
+ "-->\n",
3854
+ )
3855
+ with pytest.raises(phase.ConformanceError, match="paragraph-inline"):
3856
+ check_receipts(receipt_section(grim_receipt()), body=body)
3857
+
3858
+
3859
+ def test_prose_span_cannot_shield_a_declaration_in_the_next_span():
3860
+ # Round-6 P2 (codex track): hidden spans are checked one by one, so a
3861
+ # harmless first span cannot prefix-shield a declaration in the next.
3862
+ body = W1_BACKREF_BODY.replace(
3863
+ "**Arithmetic Receipt**: AR1\n",
3864
+ "**Arithmetic Receipt**: AR1 <!-- reviewer note --> "
3865
+ "<!-- **Arithmetic Receipt**: AR2 -->\n",
3866
+ )
3867
+ with pytest.raises(phase.ConformanceError, match="paragraph-inline"):
3868
+ check_receipts(receipt_section(grim_receipt()), body=body)
3869
+
3870
+
3871
+ def test_fragments_across_spans_cannot_synthesize_a_declaration():
3872
+ body = W1_BACKREF_BODY.replace(
3873
+ "**Arithmetic Receipt**: AR1\n",
3874
+ "**Arithmetic Receipt**: AR1 <!-- Arithmetic --> "
3875
+ "<!-- Receipt: typo -->\n",
3876
+ )
3877
+ check_receipts(receipt_section(grim_receipt()), body=body)
3878
+
3879
+
3880
+ def test_hidden_prose_beside_a_visible_declaration_is_harmless():
3881
+ body = W1_BACKREF_BODY.replace(
3882
+ "**Arithmetic Receipt**: AR1\n",
3883
+ "**Arithmetic Receipt**: AR1 <!-- reviewer note\n-->\n",
3884
+ )
3885
+ check_receipts(receipt_section(grim_receipt()), body=body)
3886
+
3887
+
3888
+ def test_any_atx_heading_ends_an_inline_comment_span():
3889
+ body = W1_BACKREF_BODY.replace(
3890
+ "**Arithmetic Receipt**: AR1\n",
3891
+ "prose <!-- open\n#### Detail\n**Arithmetic Receipt**: AR1\n",
3892
+ )
3893
+ check_receipts(receipt_section(grim_receipt()), body=body)
3894
+
3895
+
3896
+ def test_grimmer_grim_inconsistent_mean_is_not_computable():
3897
+ check_receipts(
3898
+ receipt_section(grimmer_receipt(
3899
+ status="not_computable",
3900
+ not_computable_reason="mean_grim_inconsistent",
3901
+ rounding_interval=None,
3902
+ nearest_achievable=None,
3903
+ finding_ref=None,
3904
+ )),
3905
+ body="prose",
3906
+ )
3907
+
3908
+
3909
+ @pytest.mark.parametrize("role", ["eic", "domain", "perspective", "da"])
3910
+ def test_receipt_section_is_forbidden_on_other_seats(role):
3911
+ text = phase2_text(role) + \
3912
+ "\n\n## Arithmetic Receipts\n\nno_recomputable_statistics: rogue\n"
3913
+ report = panel.parse_report("p2.md", text, FULL)
3914
+ with pytest.raises(
3915
+ phase.ConformanceError, match="RECEIPT-SECTION-FORBIDDEN"
3916
+ ):
3917
+ phase.check_receipt_section_forbidden(report)
3918
+
3919
+
3920
+ def test_full_cli_pass_with_real_receipts(tmp_path):
3921
+ args = write_cli_files(tmp_path, "methodology")
3922
+ phase2_path = Path(args[args.index("--phase2") + 1])
3923
+ phase2_path.write_text(
3924
+ phase2_text(
3925
+ "methodology",
3926
+ body=W1_BACKREF_BODY,
3927
+ receipts=receipt_section(grim_receipt()),
3928
+ ),
3929
+ encoding="utf-8",
3930
+ )
3931
+ assert phase.main(args + ["--role", "methodology"]) == phase.EXIT_PASS
3932
+
3933
+
3934
+ def test_full_cli_rejects_missing_receipt_section(tmp_path, capsys):
3935
+ args = write_cli_files(tmp_path, "methodology")
3936
+ phase2_path = Path(args[args.index("--phase2") + 1])
3937
+ text = phase2_text("methodology", body="prose")
3938
+ phase2_path.write_text(
3939
+ text[: text.index("\n## Arithmetic Receipts")], encoding="utf-8"
3940
+ )
3941
+ assert phase.main(args + ["--role", "methodology"]) == \
3942
+ phase.EXIT_CONFORMANCE
3943
+ assert "[RECEIPT-MISSING:" in capsys.readouterr().out
3944
+
3945
+
3946
+ # --- #610 step-5 extraction gate + injected-receipt identity gate ---------
3947
+
3948
+
3949
+ from scripts import recompute_receipts as recompute # noqa: E402
3950
+
3951
+
3952
+ GRIM_EXTRACTION = (
3953
+ "## Recompute Extraction\n"
3954
+ "\n"
3955
+ "### RR1\n"
3956
+ "procedure_id: grim\n"
3957
+ "evidence_anchor: table: Table 2, M=3.847 with N=87\n"
3958
+ "reported_inputs: single 1-5 integer item, N=87, M=3.847\n"
3959
+ "assumptions: unweighted single-item mean as stated in §3.2\n"
3960
+ "n: 87\n"
3961
+ "reported_mean: 3.847\n"
3962
+ "scale_min: 1\n"
3963
+ "scale_max: 5\n"
3964
+ "rounding_rule: unstated\n"
3965
+ )
3966
+ ATTESTATION_EXTRACTION = (
3967
+ "## Recompute Extraction\n"
3968
+ "\n"
3969
+ "no_recomputable_statistics: the manuscript reports no statistic a "
3970
+ "bounded procedure covers\n"
3971
+ )
3972
+
3973
+
3974
+ def extraction_args(tmp_path: Path, extraction_text: str,
3975
+ role: str = "methodology") -> list[str]:
3976
+ args = write_cli_files(tmp_path, role)
3977
+ del args[args.index("--phase2"):args.index("--phase2") + 2]
3978
+ extraction = tmp_path / "extraction.md"
3979
+ extraction.write_text(extraction_text, encoding="utf-8")
3980
+ return args + ["--extraction", str(extraction), "--role", role]
3981
+
3982
+
3983
+ def test_extraction_stage_passes_on_rr_grammar(tmp_path, capsys):
3984
+ assert phase.main(extraction_args(tmp_path, GRIM_EXTRACTION)) == \
3985
+ phase.EXIT_PASS
3986
+ assert "EXTRACTION-CONFORMANCE: PASS" in capsys.readouterr().out
3987
+
3988
+
3989
+ def test_extraction_stage_passes_on_attestation_with_advisory(tmp_path,
3990
+ capsys):
3991
+ assert phase.main(extraction_args(tmp_path, ATTESTATION_EXTRACTION)) == \
3992
+ phase.EXIT_PASS
3993
+ out = capsys.readouterr().out
3994
+ assert "EXTRACTION-CONFORMANCE: PASS" in out
3995
+ assert "[RECEIPT-ATTESTATION:" in out
3996
+
3997
+
3998
+ def test_extraction_stage_is_methodology_only(tmp_path, capsys):
3999
+ assert phase.main(
4000
+ extraction_args(tmp_path, GRIM_EXTRACTION, role="eic")
4001
+ ) == phase.EXIT_CONTRACT
4002
+ assert "[ROLE-BINDING:" in capsys.readouterr().out
4003
+
4004
+
4005
+ def test_extraction_preamble_prose_is_nonconforming(tmp_path, capsys):
4006
+ assert phase.main(extraction_args(
4007
+ tmp_path, "I will now extract the statistics.\n\n" + GRIM_EXTRACTION
4008
+ )) == phase.EXIT_CONFORMANCE
4009
+ assert "[EXTRACTION-GRAMMAR:" in capsys.readouterr().out
4010
+
4011
+
4012
+ def test_extraction_extra_section_is_nonconforming(tmp_path, capsys):
4013
+ assert phase.main(extraction_args(
4014
+ tmp_path, GRIM_EXTRACTION + "\n## Notes\n\nsome prose\n"
4015
+ )) == phase.EXIT_CONFORMANCE
4016
+ assert "[EXTRACTION-GRAMMAR:" in capsys.readouterr().out
4017
+
4018
+
4019
+ def test_extraction_missing_section_is_nonconforming(tmp_path, capsys):
4020
+ assert phase.main(extraction_args(
4021
+ tmp_path, "## Wrong Heading\n\nn: 87\n"
4022
+ )) == phase.EXIT_CONFORMANCE
4023
+ assert "[EXTRACTION-GRAMMAR:" in capsys.readouterr().out
4024
+
4025
+
4026
+ def test_extraction_unknown_field_is_nonconforming(tmp_path, capsys):
4027
+ assert phase.main(extraction_args(
4028
+ tmp_path, GRIM_EXTRACTION + "surprise_field: value\n"
4029
+ )) == phase.EXIT_CONFORMANCE
4030
+ assert "unknown field" in capsys.readouterr().out
4031
+
4032
+
4033
+ def test_extraction_prose_line_inside_section_is_nonconforming(tmp_path,
4034
+ capsys):
4035
+ assert phase.main(extraction_args(
4036
+ tmp_path, GRIM_EXTRACTION + "This mean looks impossible to me.\n"
4037
+ )) == phase.EXIT_CONFORMANCE
4038
+ assert "not a machine line" in capsys.readouterr().out
4039
+
4040
+
4041
+ def test_extraction_invalid_anchor_is_nonconforming(tmp_path, capsys):
4042
+ bad = GRIM_EXTRACTION.replace(
4043
+ "evidence_anchor: table: Table 2, M=3.847 with N=87",
4044
+ "evidence_anchor: somewhere in the paper",
4045
+ )
4046
+ assert phase.main(extraction_args(tmp_path, bad)) == \
4047
+ phase.EXIT_CONFORMANCE
4048
+ assert "ANCHOR" in capsys.readouterr().out
4049
+
4050
+
4051
+ def test_extraction_and_phase2_stages_are_mutually_exclusive(tmp_path):
4052
+ args = write_cli_files(tmp_path, "methodology")
4053
+ extraction = tmp_path / "extraction.md"
4054
+ extraction.write_text(GRIM_EXTRACTION, encoding="utf-8")
4055
+ with pytest.raises(SystemExit) as exc:
4056
+ phase._parse_args(
4057
+ args + ["--extraction", str(extraction), "--role", "methodology"]
4058
+ )
4059
+ assert exc.value.code == 2
4060
+
4061
+
4062
+ def test_injected_receipts_flag_requires_phase2(tmp_path):
4063
+ args = phase1_only_args(tmp_path, "methodology")
4064
+ with pytest.raises(SystemExit) as exc:
4065
+ phase._parse_args(args + ["--injected-receipts", "x.md"])
4066
+ assert exc.value.code == 2
4067
+
4068
+
4069
+ def injected_receipts_text() -> str:
4070
+ return recompute.compute_receipts(
4071
+ recompute.parse_extraction(GRIM_EXTRACTION)
4072
+ )
4073
+
4074
+
4075
+ def injected_cli(tmp_path: Path, card_receipt_lines: list[str],
4076
+ injected_text: str, body: str = W1_BACKREF_BODY,
4077
+ role: str = "methodology") -> list[str]:
4078
+ args = write_cli_files(tmp_path, role)
4079
+ (tmp_path / "p2.md").write_text(
4080
+ phase2_text(role, body=body, receipts=card_receipt_lines),
4081
+ encoding="utf-8",
4082
+ )
4083
+ injected = tmp_path / "injected.md"
4084
+ injected.write_text(injected_text, encoding="utf-8")
4085
+ return args + ["--role", role, "--injected-receipts", str(injected)]
4086
+
4087
+
4088
+ def faithful_card_lines(injected_text: str) -> list[str]:
4089
+ # The seat's only permitted addition: the finding_ref linkage line on
4090
+ # the (single, mismatch) receipt.
4091
+ lines = [line for line in injected_text.splitlines()[1:] if line]
4092
+ return lines + ["finding_ref: W1"]
4093
+
4094
+
4095
+ def test_injected_identity_passes_on_verbatim_copy(tmp_path):
4096
+ injected = injected_receipts_text()
4097
+ assert phase.main(injected_cli(
4098
+ tmp_path, faithful_card_lines(injected), injected
4099
+ )) == phase.EXIT_PASS
4100
+
4101
+
4102
+ def test_injected_identity_rejects_an_altered_line(tmp_path, capsys):
4103
+ injected = injected_receipts_text()
4104
+ lines = [
4105
+ line.replace("status: mismatch", "status: consistent")
4106
+ if line == "status: mismatch" else line
4107
+ for line in faithful_card_lines(injected)
4108
+ if line != "finding_ref: W1"
4109
+ ]
4110
+ assert phase.main(injected_cli(
4111
+ tmp_path, lines, injected, body="clean methodology review prose"
4112
+ )) == phase.EXIT_CONFORMANCE
4113
+ assert "[RECEIPT-IDENTITY:" in capsys.readouterr().out
4114
+
4115
+
4116
+ def test_injected_identity_rejects_a_paraphrased_line(tmp_path, capsys):
4117
+ # A reworded derivation still satisfies the receipt GRAMMAR (the line
4118
+ # exists and parses), so only the identity gate can catch it — the
4119
+ # incremental value this gate exists for.
4120
+ injected = injected_receipts_text()
4121
+ lines = [
4122
+ "derivation: I re-derived this my own way" if
4123
+ line.startswith("derivation: ") else line
4124
+ for line in faithful_card_lines(injected)
4125
+ ]
4126
+ assert phase.main(injected_cli(tmp_path, lines, injected)) == \
4127
+ phase.EXIT_CONFORMANCE
4128
+ assert "[RECEIPT-IDENTITY:" in capsys.readouterr().out
4129
+
4130
+
4131
+ def test_injected_identity_rejects_an_extra_receipt(tmp_path, capsys):
4132
+ injected = injected_receipts_text()
4133
+ extra = [
4134
+ "### AR2",
4135
+ "procedure_id: n_from_df",
4136
+ "evidence_anchor: table: Table 3, t(156) with N at most 142",
4137
+ "reported_inputs: df=156, stated analytic N at most 142",
4138
+ "assumptions: independent-groups t as stated",
4139
+ "derivation: df=N1+N2-2 gives N = 158",
4140
+ "derived_value_or_range: implied N = 158",
4141
+ "comparison_rule: implied N must not exceed 142",
4142
+ "status: consistent",
4143
+ "df_identity: df=N1+N2-2",
4144
+ ]
4145
+ assert phase.main(injected_cli(
4146
+ tmp_path, faithful_card_lines(injected) + extra, injected
4147
+ )) == phase.EXIT_CONFORMANCE
4148
+ assert "[RECEIPT-IDENTITY:" in capsys.readouterr().out
4149
+
4150
+
4151
+ def test_injected_identity_survives_added_blank_lines(tmp_path):
4152
+ injected = injected_receipts_text()
4153
+ spaced = []
4154
+ for line in faithful_card_lines(injected):
4155
+ spaced += [line, ""]
4156
+ assert phase.main(injected_cli(tmp_path, spaced, injected)) == \
4157
+ phase.EXIT_PASS
4158
+
4159
+
4160
+ def test_injected_receipts_are_methodology_only(tmp_path, capsys):
4161
+ injected = injected_receipts_text()
4162
+ args = write_cli_files(tmp_path, "eic")
4163
+ injected_path = tmp_path / "injected.md"
4164
+ injected_path.write_text(injected, encoding="utf-8")
4165
+ assert phase.main(
4166
+ args + ["--role", "eic", "--injected-receipts", str(injected_path)]
4167
+ ) == phase.EXIT_CONTRACT
4168
+ assert "[ROLE-BINDING:" in capsys.readouterr().out
4169
+
4170
+
4171
+ def test_injected_file_without_heading_is_a_contract_error(tmp_path, capsys):
4172
+ injected = injected_receipts_text()
4173
+ assert phase.main(injected_cli(
4174
+ tmp_path, faithful_card_lines(injected),
4175
+ "### AR1\nprocedure_id: grim\n",
4176
+ )) == phase.EXIT_CONTRACT
4177
+ assert "[INJECTED-RECEIPTS-INVALID:" in capsys.readouterr().out
4178
+
4179
+
4180
+ def test_injected_identity_pass_emits_its_witness_marker(tmp_path, capsys):
4181
+ injected = injected_receipts_text()
4182
+ assert phase.main(injected_cli(
4183
+ tmp_path, faithful_card_lines(injected), injected
4184
+ )) == phase.EXIT_PASS
4185
+ assert "RECEIPT-IDENTITY: PASS" in capsys.readouterr().out
4186
+
4187
+
4188
+ def test_injected_identity_rejects_a_decorated_finding_ref(tmp_path, capsys):
4189
+ # codex round 1, P2-4: the receipt GRAMMAR tolerates a decorated
4190
+ # finding_ref, but under injection only the plain spelling is the
4191
+ # permitted addition.
4192
+ injected = injected_receipts_text()
4193
+ lines = [
4194
+ "- **finding_ref**: W1" if line == "finding_ref: W1" else line
4195
+ for line in faithful_card_lines(injected)
4196
+ ]
4197
+ assert phase.main(injected_cli(tmp_path, lines, injected)) == \
4198
+ phase.EXIT_CONFORMANCE
4199
+ assert "[RECEIPT-IDENTITY:" in capsys.readouterr().out
4200
+
4201
+
4202
+ def test_an_escaped_backtick_span_cannot_hide_a_dissent( # #613 sec P1a
4203
+ ):
4204
+ r"""CommonMark: `\`` is a literal backtick and opens no code span, so
4205
+ the marker between two escaped backticks is a live comment opener —
4206
+ blanking it credited a dissent the rendered page hides."""
4207
+ text = phase2_with_dissent_section([
4208
+ "Note: \\` <!-- \\` end.",
4209
+ "dimension_id: D1",
4210
+ "rationale: plan understated the sampling frame. -->",
4211
+ ])
4212
+ with pytest.raises(phase.ConformanceError, match="DISSENT-HIDDEN"):
4213
+ phase.parse_dissent_dimensions(text)
4214
+
4215
+
4216
+ def test_a_cross_line_code_span_cannot_hide_a_dissent(): # #613 sec P1b
4217
+ """A trailing unpaired backtick run pairs into the NEXT line for the
4218
+ renderer, pulling the marker out of code; once a paragraph's runs stop
4219
+ pairing locally, blanking is off and the marker opens."""
4220
+ text = phase2_with_dissent_section([
4221
+ "Note on markup: `",
4222
+ "` <!-- `",
4223
+ "dimension_id: D1",
4224
+ "rationale: plan was inadequate -->",
4225
+ ])
4226
+ with pytest.raises(phase.ConformanceError,
4227
+ match="DISSENT-HIDDEN|DISSENT-GRAMMAR"):
4228
+ phase.parse_dissent_dimensions(text)
4229
+
4230
+
4231
+ def test_balanced_inline_code_mention_still_parses_after_the_fix():
4232
+ """The sanctioned spelling survives both new guards: escaped-backtick
4233
+ blanking and paragraph run-parity poisoning leave a balanced same-line
4234
+ span as prose."""
4235
+ text = phase2_with_dissent_section([
4236
+ "dimension_id: D1",
4237
+ "rationale: the seat wrote `<!--` and `-->` in inline code",
4238
+ ])
4239
+ assert phase.parse_dissent_dimensions(text).dimensions == {"D1"}
4240
+
4241
+
4242
+ def test_an_empty_comment_closer_overlap_does_not_false_abort():
4243
+ """codex #650 round 1 (P2): `<!-->` and `<!--->` CLOSE in CommonMark —
4244
+ the closer reuses the opener's dashes — so the rendered fields below
4245
+ them must keep parsing."""
4246
+ for empty in ("<!-->", "<!--->"):
4247
+ text = phase2_with_dissent_section([
4248
+ f"note {empty}",
4249
+ "dimension_id: D1",
4250
+ "rationale: plan was inadequate",
4251
+ ])
4252
+ assert phase.parse_dissent_dimensions(text).dimensions == {"D1"}
4253
+
4254
+
4255
+ def test_a_mid_line_reopen_after_a_close_hides_again():
4256
+ """codex #650 round 1 (P3): a genuine close-and-REOPEN on one mid-line
4257
+ — the second opener hides the fields below it."""
4258
+ text = phase2_with_dissent_section([
4259
+ "prose <!-- first --> more <!--",
4260
+ "dimension_id: D1",
4261
+ "rationale: plan was inadequate",
4262
+ "-->",
4263
+ ])
4264
+ with pytest.raises(phase.ConformanceError, match="DISSENT-HIDDEN"):
4265
+ phase.parse_dissent_dimensions(text)
4266
+
4267
+
4268
+ def test_a_mid_line_double_close_leaves_fields_parsed():
4269
+ text = phase2_with_dissent_section([
4270
+ "prose <!-- a --> and <!-- b --> clear:",
4271
+ "dimension_id: D1",
4272
+ "rationale: plan was inadequate",
4273
+ ])
4274
+ assert phase.parse_dissent_dimensions(text).dimensions == {"D1"}
4275
+
4276
+
4277
+ @pytest.mark.parametrize("raw_html", [
4278
+ "<script>",
4279
+ "</script>",
4280
+ "<style media=\"screen\">",
4281
+ "<template>",
4282
+ "<div hidden>",
4283
+ "<span style=\"display:none\">",
4284
+ "<input type=\"hidden\" />",
4285
+ "<details>",
4286
+ "<svg aria-hidden=\"true\">",
4287
+ "<!DOCTYPE html>",
4288
+ "<![CDATA[",
4289
+ "<?xml version=\"1.0\"?>",
4290
+ "<script",
4291
+ "<span>dimension_id</span>: D1",
4292
+ ])
4293
+ def test_non_comment_raw_html_in_dissent_aborts(raw_html):
4294
+ text = phase2_with_dissent_section([
4295
+ raw_html,
4296
+ "dimension_id: D1",
4297
+ "rationale: plan was inadequate",
4298
+ ])
4299
+ with pytest.raises(phase.ConformanceError, match="DISSENT-RAW-HTML"):
4300
+ phase.parse_dissent_dimensions(text)
4301
+
4302
+
4303
+ @pytest.mark.parametrize("container", ["- ", "* ", "1. ", "> ", "> - "])
4304
+ def test_container_prefixed_raw_html_in_dissent_aborts(container):
4305
+ text = phase2_with_dissent_section([
4306
+ f"{container}<template hidden>",
4307
+ "dimension_id: D1",
4308
+ "rationale: plan was inadequate",
4309
+ f"{container}</template>",
4310
+ ])
4311
+ with pytest.raises(phase.ConformanceError, match="DISSENT-RAW-HTML"):
4312
+ phase.parse_dissent_dimensions(text)
4313
+
4314
+
4315
+ @pytest.mark.parametrize("code", [
4316
+ "`<script>`",
4317
+ "``<template data-tick=`x`>``",
4318
+ "```<span hidden>```",
4319
+ ])
4320
+ def test_inline_code_raw_html_mention_in_dissent_is_permitted(code):
4321
+ text = phase2_with_dissent_section([
4322
+ "dimension_id: D1",
4323
+ f"rationale: the seat mentioned {code} as literal syntax",
4324
+ ])
4325
+ assert phase.parse_dissent_dimensions(text).dimensions == {"D1"}
4326
+
4327
+
4328
+ def test_fenced_raw_html_example_in_dissent_keeps_existing_semantics():
4329
+ text = phase2_with_dissent_section([
4330
+ "```html",
4331
+ "<script>",
4332
+ "const example = true;",
4333
+ "</script>",
4334
+ "```",
4335
+ "dimension_id: D1",
4336
+ "rationale: plan was inadequate",
4337
+ ])
4338
+ assert phase.parse_dissent_dimensions(text).dimensions == {"D1"}
4339
+
4340
+
4341
+ def test_raw_html_outside_dissent_span_is_not_scanned():
4342
+ text = phase2_with_dissent_section([
4343
+ "dimension_id: D1",
4344
+ "rationale: plan was inadequate",
4345
+ ]).replace(
4346
+ "## Review Body",
4347
+ "## Review Body\n\n<script hidden>outside the dissent span</script>",
4348
+ 1,
4349
+ )
4350
+ assert phase.parse_dissent_dimensions(text).dimensions == {"D1"}
4351
+
4352
+
4353
+ @pytest.mark.parametrize("prose", [
4354
+ "rationale: compare x < y before accepting the plan",
4355
+ "rationale: see <https://example.test> for the public protocol",
4356
+ "rationale: contact <reviewer@example.test> for the archived note",
4357
+ ])
4358
+ def test_non_html_angle_bracket_prose_in_dissent_is_permitted(prose):
4359
+ text = phase2_with_dissent_section(["dimension_id: D1", prose])
4360
+ assert phase.parse_dissent_dimensions(text).dimensions == {"D1"}
4361
+
4362
+
4363
+ def test_raw_html_without_fields_aborts_instead_of_empty_advisory():
4364
+ text = phase2_with_dissent_section(["<template>withdrawn draft</template>"])
4365
+ with pytest.raises(phase.ConformanceError, match="DISSENT-RAW-HTML"):
4366
+ phase.parse_dissent_dimensions(text)