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,2313 @@
1
+ #!/usr/bin/env python3
2
+ """check_audit_artifact_consistency.py — ARS v3.6.7 Step 6 Phase 6.3 lint.
3
+
4
+ Implements every row of spec §3.7 invariant families A-F (cross-artifact
5
+ contract for the audit-artifact lifecycle). Reads a proposal/persisted entry
6
+ plus its companion JSONL / sidecar / verdict files (and optionally the
7
+ passport for D1/D3/E2/E6 ledger-level checks) and aggregates findings.
8
+
9
+ CLI modes:
10
+ --mode proposal — proposal-state entry validation (oneOf.proposal)
11
+ --mode persisted — persisted-state entry validation (oneOf.persisted)
12
+ --mode jsonl-stream — A7 stream-shape pairing only (orchestrator §5.2 L2-5)
13
+ --example-validation-harness — F4: walk docs/design/*.md, validate spec example
14
+ payloads against the audit/* schemas
15
+
16
+ Exit codes:
17
+ 0 no findings
18
+ 1 lint findings (printed to stdout, one per line)
19
+ 2 internal error (exception, missing file the rule cannot ignore)
20
+ 64 EX_USAGE — bad CLI arguments
21
+
22
+ Lifecycle ownership notes (E8/E9 — discipline rules with no lint surface):
23
+ E8 — proposal entry file is the LAST file the wrapper writes (after JSONL +
24
+ sidecar + verdict are all on disk). This is wrapper-side atomicity
25
+ discipline; orchestrator has no lint signal beyond "proposal entry
26
+ exists" (a partial wrapper write is invisible from the entry alone).
27
+ E9 — wrapper is NOT invoked from an in-LLM Bash tool call from the same
28
+ session producing the deliverable (deployment discipline; spec §4.7).
29
+ A same-session in-LLM call is undetectable from artifact evidence;
30
+ defense lives at deployment-level (CI / SubagentStop / out-of-band
31
+ human terminal).
32
+
33
+ Reuse:
34
+ - tests/test_helpers.load_json_schema / build_schema_validator (FORMAT_CHECKER)
35
+ - scripts/_next_verified_at_ms.next_verified_at_ms (D3 monotonic helper)
36
+ - audit_snapshot is the Phase 6.1 reference for argparse subcommand pattern.
37
+
38
+ The spec §3.7 table is the source of truth; this script is the executable
39
+ mirror. Each rule function is annotated with its rule id and one-line summary
40
+ so reviewers can grep both directions.
41
+ """
42
+ from __future__ import annotations
43
+
44
+ import argparse
45
+ import hashlib
46
+ import json
47
+ import os
48
+ import re
49
+ import subprocess
50
+ import sys
51
+ from dataclasses import dataclass, field
52
+ from pathlib import Path
53
+ from typing import Any, Iterable
54
+
55
+ try:
56
+ import yaml
57
+ except ImportError as e: # pragma: no cover
58
+ print(f"Missing dependency: {e}. Install with: pip install pyyaml", file=sys.stderr)
59
+ sys.exit(2)
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Constants — mirror the schema regexes so we don't depend on a JSON Schema
63
+ # load just to get a pattern. Keep regex pair in sync with shared/contracts/.
64
+ # ---------------------------------------------------------------------------
65
+
66
+ RUN_ID_RE = re.compile(
67
+ r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}-[0-9]{2}-[0-9]{2}Z-[0-9a-f]{4}$"
68
+ )
69
+ RFC3339_MS_RE = re.compile(
70
+ r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$"
71
+ )
72
+ THREAD_UUID_RE = re.compile(
73
+ r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
74
+ )
75
+ SHA256_RE = re.compile(r"^[a-f0-9]{64}$")
76
+
77
+ REPO_ROOT = Path(__file__).resolve().parent.parent
78
+ PASSPORT_SCHEMAS = REPO_ROOT / "shared/contracts/passport"
79
+ AUDIT_SCHEMAS = REPO_ROOT / "shared/contracts/audit"
80
+
81
+ ENTRY_SCHEMA_PATH = PASSPORT_SCHEMAS / "audit_artifact_entry.schema.json"
82
+ JSONL_SCHEMA_PATH = AUDIT_SCHEMAS / "audit_jsonl.schema.json"
83
+ SIDECAR_SCHEMA_PATH = AUDIT_SCHEMAS / "audit_sidecar.schema.json"
84
+ VERDICT_SCHEMA_PATH = AUDIT_SCHEMAS / "audit_verdict.schema.json"
85
+
86
+ # audit template path is a const in the sidecar schema — used by B3 to know
87
+ # which file is the "template" role in the bundle manifest computation.
88
+ AUDIT_TEMPLATE_PATH = "shared/templates/codex_audit_multifile_template.md"
89
+
90
+
91
+ def _load_parse_audit_verdict():
92
+ """Load parse_audit_verdict module by file path via importlib.
93
+
94
+ Codex round 5 P2 closure: a bare `from parse_audit_verdict import …`
95
+ only resolves when scripts/ is on sys.path (typical for the CLI
96
+ invocation `python scripts/check_audit_artifact_consistency.py …`).
97
+ A package-style invocation `python -m
98
+ scripts.check_audit_artifact_consistency` or import-from-package
99
+ usage leaves scripts/ off sys.path, the import fails, and the
100
+ fallback silently disabled the L2-3/L2-4 stream-shape gate. Loading
101
+ by absolute file path via importlib makes the gate work in every
102
+ invocation context. The module is co-located by repo convention; if
103
+ it ever moves, this helper raises rather than degrading the gate.
104
+ """
105
+ import importlib.util as _ilu
106
+
107
+ here = Path(__file__).resolve().parent
108
+ target = here / "parse_audit_verdict.py"
109
+ spec = _ilu.spec_from_file_location("parse_audit_verdict", target)
110
+ if spec is None or spec.loader is None:
111
+ raise RuntimeError(
112
+ f"could not load parse_audit_verdict from {target}; "
113
+ "Phase 6.1 dependency missing — Phase 6.3 gate cannot run "
114
+ "without the stream-shape validator"
115
+ )
116
+ module = _ilu.module_from_spec(spec)
117
+ spec.loader.exec_module(module)
118
+ return module
119
+
120
+
121
+ def _load_stream_shape_validator():
122
+ """Backwards-compatible accessor returning (validate_stream_shape, ParseError)."""
123
+ module = _load_parse_audit_verdict()
124
+ return module.validate_stream_shape, module.ParseError
125
+
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # Data structures
129
+ # ---------------------------------------------------------------------------
130
+
131
+ @dataclass
132
+ class LintError:
133
+ """One finding from a rule check.
134
+
135
+ rule_id — stable handle from spec §3.7 (e.g., "A1", "B7", "F2").
136
+ message — human-readable finding; should name the offending value.
137
+ location — file path or logical location (e.g., "<entry>", "<jsonl>:row=12").
138
+ severity — "error" (default; counts toward exit code 1) or "info"
139
+ (printed to stderr but does not fail the run; used by the
140
+ F4 harness for `...` placeholder rows that are intentional).
141
+ """
142
+
143
+ rule_id: str
144
+ message: str
145
+ location: str = "<unknown>"
146
+ severity: str = "error"
147
+
148
+ def render(self) -> str:
149
+ prefix = self.rule_id if self.severity == "error" else f"{self.rule_id} info"
150
+ return f"[{prefix}] {self.location}: {self.message}"
151
+
152
+
153
+ @dataclass
154
+ class LintContext:
155
+ """Bundle of artifacts a check might want to read.
156
+
157
+ Members are Optional because some modes don't need every artifact (e.g.,
158
+ jsonl-stream mode only sees the JSONL). Each rule guards against the
159
+ missing pieces it needs and skips silently if irrelevant; that lets the
160
+ aggregator run every rule it can without short-circuiting.
161
+ """
162
+
163
+ mode: str # "proposal" | "persisted" | "jsonl-stream" | "harness"
164
+ entry: dict[str, Any] | None = None
165
+ entry_path: Path | None = None
166
+ sidecar: dict[str, Any] | None = None
167
+ sidecar_path: Path | None = None
168
+ verdict: dict[str, Any] | None = None
169
+ verdict_path: Path | None = None
170
+ jsonl_events: list[dict[str, Any]] | None = None
171
+ jsonl_path: Path | None = None
172
+ output_dir: Path | None = None
173
+ passport_audit_artifacts: list[dict[str, Any]] | None = None
174
+ passport_path: Path | None = None
175
+ repo_root: Path = field(default_factory=lambda: REPO_ROOT)
176
+
177
+
178
+ # ---------------------------------------------------------------------------
179
+ # Helpers
180
+ # ---------------------------------------------------------------------------
181
+
182
+
183
+ def _sha256_file(path: Path) -> str | None:
184
+ """SHA-256 hex of a file (None if unreadable)."""
185
+ try:
186
+ h = hashlib.sha256()
187
+ with path.open("rb") as f:
188
+ for chunk in iter(lambda: f.read(65536), b""):
189
+ h.update(chunk)
190
+ return h.hexdigest()
191
+ except OSError:
192
+ return None
193
+
194
+
195
+ def _safe_get(d: Any, *path: str) -> Any:
196
+ """Dotted-path get on a dict tree; returns None if any segment missing."""
197
+ for key in path:
198
+ if not isinstance(d, dict):
199
+ return None
200
+ d = d.get(key)
201
+ if d is None:
202
+ return None
203
+ return d
204
+
205
+
206
+ def _load_yaml_or_json(path: Path) -> Any:
207
+ """Parse YAML/JSON keeping RFC 3339 timestamps as strings.
208
+
209
+ PyYAML's safe_load auto-casts unquoted ISO-8601 datetimes to datetime
210
+ objects (which fails our string-typed schema). We use a custom loader
211
+ that disables the timestamp constructor so timestamps stay strings,
212
+ while still parsing ints/floats/bools as native types — BaseLoader's
213
+ str-only output broke C1 mirror checks comparing verdict_file (loaded
214
+ as YAML, all-string) against entry (loaded as JSON, typed).
215
+ """
216
+ text = path.read_text(encoding="utf-8")
217
+ if path.suffix.lower() == ".json":
218
+ return json.loads(text)
219
+ return yaml.load(text, Loader=_StrTimestampSafeLoader)
220
+
221
+
222
+ class _StrTimestampSafeLoader(yaml.SafeLoader):
223
+ """SafeLoader variant that keeps timestamps as strings.
224
+
225
+ Otherwise PyYAML auto-converts `2026-04-30T15:22:04.123Z` into a
226
+ datetime object, which breaks string-typed schema validation and the
227
+ C1 mirror equality check (entry side reads as str via JSON).
228
+ """
229
+
230
+
231
+ def _yaml_str_timestamp_constructor(loader: yaml.Loader, node: yaml.Node) -> str:
232
+ return loader.construct_scalar(node)
233
+
234
+
235
+ _StrTimestampSafeLoader.add_constructor(
236
+ "tag:yaml.org,2002:timestamp", _yaml_str_timestamp_constructor
237
+ )
238
+
239
+
240
+ def _load_jsonl(path: Path) -> list[dict[str, Any]]:
241
+ out: list[dict[str, Any]] = []
242
+ with path.open(encoding="utf-8") as fh:
243
+ for lineno, line in enumerate(fh, start=1):
244
+ line = line.strip()
245
+ if not line:
246
+ continue
247
+ try:
248
+ row = json.loads(line)
249
+ except json.JSONDecodeError as e:
250
+ raise ValueError(f"jsonl row {lineno}: {e}") from e
251
+ if not isinstance(row, dict):
252
+ raise ValueError(f"jsonl row {lineno}: not a JSON object")
253
+ out.append(row)
254
+ return out
255
+
256
+
257
+ def _bare_run_id_from_basename(filename: str, ext: str) -> str | None:
258
+ """Strip a known extension and return the bare <run_id>, or None on mismatch.
259
+
260
+ We avoid Path.stem / .name parsing tricks to keep the rule legible: a
261
+ sidecar named `2026-04-30T15-22-04Z-d8f3.meta.json` returns
262
+ `2026-04-30T15-22-04Z-d8f3` only when ext='.meta.json'.
263
+ """
264
+ if not filename.endswith(ext):
265
+ return None
266
+ return filename[: -len(ext)]
267
+
268
+
269
+ # ---------------------------------------------------------------------------
270
+ # Family A — Per-artifact cross-field rules
271
+ # ---------------------------------------------------------------------------
272
+
273
+
274
+ def check_a1(entry: dict[str, Any] | None, verdict: dict[str, Any] | None,
275
+ location: str = "<entry>") -> list[LintError]:
276
+ """A1 — verdict.status agrees with finding_counts and failure_reason.
277
+
278
+ Applies to BOTH the entry's verdict block and the verdict file. We
279
+ check whichever is provided. Spec rules:
280
+ PASS: p1==0 AND p2==0 AND p3==0 AND failure_reason absent
281
+ MINOR: p1==0 AND p2==0 AND p3<=3 AND failure_reason absent
282
+ MATERIAL: (p1>0 OR p2>0 OR p3>3) AND failure_reason absent
283
+ AUDIT_FAILED: p1==0 AND p2==0 AND p3==0 AND failure_reason set non-empty
284
+ """
285
+ findings: list[LintError] = []
286
+ for label, doc in (("entry.verdict", _safe_get(entry, "verdict")), ("verdict_file", verdict)):
287
+ if doc is None:
288
+ continue
289
+ # verdict file uses verdict_status; entry.verdict uses status
290
+ status = doc.get("status") if "status" in doc else doc.get("verdict_status")
291
+ counts = doc.get("finding_counts") or {}
292
+ p1 = counts.get("p1")
293
+ p2 = counts.get("p2")
294
+ p3 = counts.get("p3")
295
+ if status is None or not all(isinstance(x, int) for x in (p1, p2, p3)):
296
+ continue # schema-level rejection; not A1's surface
297
+ failure_reason = doc.get("failure_reason")
298
+ has_failure = isinstance(failure_reason, str) and len(failure_reason) > 0
299
+
300
+ loc = f"{location}:{label}"
301
+ if status == "PASS":
302
+ if not (p1 == 0 and p2 == 0 and p3 == 0):
303
+ findings.append(LintError("A1",
304
+ f"PASS requires p1==p2==p3==0 (got p1={p1},p2={p2},p3={p3})", loc))
305
+ if has_failure:
306
+ findings.append(LintError("A1",
307
+ "PASS forbids failure_reason", loc))
308
+ elif status == "MINOR":
309
+ # Codex round 10 P2 closure: spec §3.2 cross-field rule for MINOR
310
+ # is "p1==0 AND p2==0 AND p3<=3" — but PASS already covers
311
+ # p3==0 case, and the wrapper / parse_audit_verdict.py classifies
312
+ # zero findings as PASS not MINOR. A MINOR verdict with
313
+ # p1=p2=p3=0 is malformed (status disagrees with the parser's
314
+ # classification) and would let an inconsistent verdict file
315
+ # send the orchestrator down the MINOR escalation path with
316
+ # no findings to show. Require p3>=1 for MINOR — the lower
317
+ # bound that distinguishes MINOR from PASS.
318
+ if not (p1 == 0 and p2 == 0 and 1 <= p3 <= 3):
319
+ findings.append(LintError("A1",
320
+ f"MINOR requires p1==0 AND p2==0 AND 1<=p3<=3 (got p1={p1},p2={p2},p3={p3}); "
321
+ f"zero-count MINOR is malformed — wrapper classifies zero findings as PASS",
322
+ loc))
323
+ if has_failure:
324
+ findings.append(LintError("A1",
325
+ "MINOR forbids failure_reason", loc))
326
+ elif status == "MATERIAL":
327
+ if not (p1 > 0 or p2 > 0 or p3 > 3):
328
+ findings.append(LintError("A1",
329
+ f"MATERIAL requires p1>0 OR p2>0 OR p3>3 (got p1={p1},p2={p2},p3={p3})", loc))
330
+ if has_failure:
331
+ findings.append(LintError("A1",
332
+ "MATERIAL forbids failure_reason", loc))
333
+ elif status == "AUDIT_FAILED":
334
+ if not (p1 == 0 and p2 == 0 and p3 == 0):
335
+ findings.append(LintError("A1",
336
+ f"AUDIT_FAILED requires p1==p2==p3==0 (got p1={p1},p2={p2},p3={p3})", loc))
337
+ if not has_failure:
338
+ findings.append(LintError("A1",
339
+ "AUDIT_FAILED requires failure_reason (non-empty)", loc))
340
+ return findings
341
+
342
+
343
+ def check_a2(entry: dict[str, Any] | None, verdict: dict[str, Any] | None,
344
+ location: str = "<entry>") -> list[LintError]:
345
+ """A2 — failure_reason required iff status == AUDIT_FAILED.
346
+
347
+ Mostly redundant with A1 when both sides provide a status, but A2
348
+ surfaces the rule independently so partial documents still get flagged.
349
+ """
350
+ findings: list[LintError] = []
351
+ for label, doc in (("entry.verdict", _safe_get(entry, "verdict")), ("verdict_file", verdict)):
352
+ if doc is None:
353
+ continue
354
+ status = doc.get("status") if "status" in doc else doc.get("verdict_status")
355
+ if status is None:
356
+ continue
357
+ failure_reason = doc.get("failure_reason")
358
+ has_failure = isinstance(failure_reason, str) and len(failure_reason) > 0
359
+ loc = f"{location}:{label}"
360
+ if status == "AUDIT_FAILED" and not has_failure:
361
+ findings.append(LintError("A2",
362
+ "AUDIT_FAILED requires non-empty failure_reason", loc))
363
+ if status != "AUDIT_FAILED" and has_failure:
364
+ findings.append(LintError("A2",
365
+ f"failure_reason forbidden when status={status!r}", loc))
366
+ return findings
367
+
368
+
369
+ def check_a3(entry: dict[str, Any] | None, verdict: dict[str, Any] | None,
370
+ location: str = "<entry>") -> list[LintError]:
371
+ """A3 — round <= target_rounds (entry-side and verdict-file)."""
372
+ findings: list[LintError] = []
373
+ for label, doc in (("entry.verdict", _safe_get(entry, "verdict")), ("verdict_file", verdict)):
374
+ if doc is None:
375
+ continue
376
+ rnd = doc.get("round")
377
+ target = doc.get("target_rounds")
378
+ if not isinstance(rnd, int) or not isinstance(target, int):
379
+ continue
380
+ if rnd > target:
381
+ findings.append(LintError("A3",
382
+ f"round={rnd} > target_rounds={target}", f"{location}:{label}"))
383
+ return findings
384
+
385
+
386
+ def check_a4(entry: dict[str, Any] | None, mode: str,
387
+ location: str = "<entry>") -> list[LintError]:
388
+ """A4 — acknowledgement allowed only on persisted MATERIAL entries.
389
+
390
+ Proposal arm forbids acknowledgement entirely (per JSON Schema sketch).
391
+ Persisted PASS / MINOR with acknowledgement → reject.
392
+ """
393
+ if entry is None:
394
+ return []
395
+ if "acknowledgement" not in entry:
396
+ return []
397
+ findings: list[LintError] = []
398
+ if mode == "proposal":
399
+ findings.append(LintError("A4",
400
+ "proposal arm forbids acknowledgement", location))
401
+ return findings
402
+ if mode != "persisted":
403
+ return []
404
+ status = _safe_get(entry, "verdict", "status")
405
+ if status != "MATERIAL":
406
+ findings.append(LintError("A4",
407
+ f"acknowledgement requires verdict.status==MATERIAL (got {status!r})", location))
408
+ return findings
409
+
410
+
411
+ def check_a5(verdict: dict[str, Any] | None,
412
+ location: str = "<verdict>") -> list[LintError]:
413
+ """A5 — verdict file's finding_counts.pN == count(findings[severity == PN]).
414
+
415
+ Verdict-file rule only (entry's verdict block is a counts-only mirror per C1).
416
+ """
417
+ if verdict is None:
418
+ return []
419
+ counts = verdict.get("finding_counts") or {}
420
+ findings_list = verdict.get("findings")
421
+ if not isinstance(findings_list, list):
422
+ return []
423
+ out: list[LintError] = []
424
+ actual = {"P1": 0, "P2": 0, "P3": 0}
425
+ for f in findings_list:
426
+ sev = f.get("severity") if isinstance(f, dict) else None
427
+ if sev in actual:
428
+ actual[sev] += 1
429
+ for n in (1, 2, 3):
430
+ declared = counts.get(f"p{n}")
431
+ seen = actual[f"P{n}"]
432
+ if isinstance(declared, int) and declared != seen:
433
+ out.append(LintError("A5",
434
+ f"finding_counts.p{n}={declared} disagrees with findings[severity==P{n}].count={seen}",
435
+ location))
436
+ return out
437
+
438
+
439
+ def check_a6(verdict: dict[str, Any] | None,
440
+ location: str = "<verdict>") -> list[LintError]:
441
+ """A6 — when verdict_status==AUDIT_FAILED, findings == []."""
442
+ if verdict is None:
443
+ return []
444
+ status = verdict.get("verdict_status")
445
+ findings_list = verdict.get("findings")
446
+ if status == "AUDIT_FAILED" and isinstance(findings_list, list) and len(findings_list) > 0:
447
+ return [LintError("A6",
448
+ f"AUDIT_FAILED requires findings==[] (got {len(findings_list)} entries)",
449
+ location)]
450
+ return []
451
+
452
+
453
+ def check_a7(events: list[dict[str, Any]] | None,
454
+ location: str = "<jsonl>") -> list[LintError]:
455
+ """A7 — JSONL stream tool-event pairing.
456
+
457
+ For every NON-agent_message item.started, exactly one matching
458
+ item.completed with same item.id, completed-row > started-row, each id
459
+ at most once on each side. agent_message item.completed events are
460
+ EXEMPT from prior-item.started requirement (they don't have one).
461
+ Orphan completions (non-agent_message item.completed without prior
462
+ item.started) are rejected.
463
+ """
464
+ if events is None:
465
+ return []
466
+ findings: list[LintError] = []
467
+ started: dict[str, int] = {} # item.id -> row
468
+ started_dups: list[tuple[str, int]] = []
469
+ completed: dict[str, int] = {} # item.id -> row (non-agent_message only)
470
+ completed_dups: list[tuple[str, int]] = []
471
+
472
+ for idx, ev in enumerate(events):
473
+ ev_type = ev.get("type")
474
+ if ev_type == "item.started":
475
+ item = ev.get("item") or {}
476
+ iid = item.get("id")
477
+ itype = item.get("type")
478
+ if not isinstance(iid, str):
479
+ continue
480
+ # agent_message starts shouldn't happen but be conservative
481
+ if itype == "agent_message":
482
+ continue
483
+ if iid in started:
484
+ started_dups.append((iid, idx))
485
+ else:
486
+ started[iid] = idx
487
+ elif ev_type == "item.completed":
488
+ item = ev.get("item") or {}
489
+ iid = item.get("id")
490
+ itype = item.get("type")
491
+ if not isinstance(iid, str):
492
+ continue
493
+ if itype == "agent_message":
494
+ continue # exempt
495
+ if iid in completed:
496
+ completed_dups.append((iid, idx))
497
+ else:
498
+ completed[iid] = idx
499
+
500
+ # Duplicates
501
+ for iid, row in started_dups:
502
+ findings.append(LintError("A7",
503
+ f"duplicate item.started for item.id={iid!r} at row {row}", location))
504
+ for iid, row in completed_dups:
505
+ findings.append(LintError("A7",
506
+ f"duplicate item.completed for item.id={iid!r} at row {row}", location))
507
+
508
+ # Orphan completions (no prior started)
509
+ for iid, row in completed.items():
510
+ if iid not in started:
511
+ findings.append(LintError("A7",
512
+ f"orphan item.completed for item.id={iid!r} at row {row} (no prior item.started)",
513
+ location))
514
+
515
+ # Unmatched starts (no completion)
516
+ for iid, row in started.items():
517
+ if iid not in completed:
518
+ findings.append(LintError("A7",
519
+ f"unmatched item.started for item.id={iid!r} at row {row} (no item.completed)",
520
+ location))
521
+ else:
522
+ # ordering — completed-row must exceed started-row
523
+ crow = completed[iid]
524
+ srow = started[iid]
525
+ if crow <= srow:
526
+ findings.append(LintError("A7",
527
+ f"item.completed (row {crow}) precedes or equals item.started (row {srow}) for id={iid!r}",
528
+ location))
529
+ return findings
530
+
531
+
532
+ # ---------------------------------------------------------------------------
533
+ # Family B — Cross-file rules (Layer 3)
534
+ # ---------------------------------------------------------------------------
535
+
536
+
537
+ def check_b1(sidecar: dict[str, Any] | None, events: list[dict[str, Any]] | None,
538
+ verdict: dict[str, Any] | None, location: str = "<sidecar/jsonl>") -> list[LintError]:
539
+ """B1 — sidecar.stream.jsonl_thread_id matches JSONL's thread.started.thread_id.
540
+
541
+ SUSPENDED when companion verdict.verdict_status == AUDIT_FAILED (per
542
+ §3.4 conditional — empty string permitted there).
543
+ """
544
+ if sidecar is None or events is None:
545
+ return []
546
+ side_tid = _safe_get(sidecar, "stream", "jsonl_thread_id")
547
+ # AUDIT_FAILED suspension
548
+ v_status = (_safe_get(verdict, "verdict_status")
549
+ if verdict is not None else None)
550
+ if v_status == "AUDIT_FAILED":
551
+ # rule suspended; empty string allowed
552
+ return []
553
+ # find first thread.started event
554
+ jsonl_tid = None
555
+ for ev in events:
556
+ if ev.get("type") == "thread.started":
557
+ jsonl_tid = ev.get("thread_id")
558
+ break
559
+ if side_tid is None or jsonl_tid is None:
560
+ return [LintError("B1",
561
+ f"missing thread_id (sidecar={side_tid!r}, jsonl={jsonl_tid!r})",
562
+ location)]
563
+ if side_tid != jsonl_tid:
564
+ return [LintError("B1",
565
+ f"sidecar.stream.jsonl_thread_id={side_tid!r} != JSONL.thread.started.thread_id={jsonl_tid!r}",
566
+ location)]
567
+ return []
568
+
569
+
570
+ def check_b2(entry: dict[str, Any] | None, sidecar: dict[str, Any] | None,
571
+ repo_root: Path, location: str = "<entry/sidecar>") -> list[LintError]:
572
+ """B2 — entry.deliverable_sha == sidecar's matching primary sha
573
+ == current SHA-256 of deliverable file on disk."""
574
+ if entry is None or sidecar is None:
575
+ return []
576
+ findings: list[LintError] = []
577
+ entry_sha = entry.get("deliverable_sha")
578
+ deliv_path = entry.get("deliverable_path")
579
+ primaries = _safe_get(sidecar, "prompt", "bundle", "primary_deliverables") or []
580
+ side_sha = None
581
+ for p in primaries:
582
+ if isinstance(p, dict) and p.get("path") == deliv_path:
583
+ side_sha = p.get("sha")
584
+ break
585
+ if side_sha is None:
586
+ return [LintError("B2",
587
+ f"sidecar.prompt.bundle.primary_deliverables has no entry for {deliv_path!r}",
588
+ location)]
589
+ if entry_sha != side_sha:
590
+ findings.append(LintError("B2",
591
+ f"entry.deliverable_sha={entry_sha!r} != sidecar primary[{deliv_path!r}].sha={side_sha!r}",
592
+ location))
593
+ # Verify against on-disk file
594
+ if isinstance(deliv_path, str):
595
+ disk_path = repo_root / deliv_path
596
+ if disk_path.exists():
597
+ disk_sha = _sha256_file(disk_path)
598
+ if disk_sha is not None and disk_sha != entry_sha:
599
+ findings.append(LintError("B2",
600
+ f"current_file_SHA256({deliv_path!r})={disk_sha!r} != entry.deliverable_sha={entry_sha!r}",
601
+ location))
602
+ # Missing file is not a B2 failure if entry/sidecar agree — caller
603
+ # may be running against synthetic fixtures. Spec allows lint to
604
+ # detect mutation; absence of file is a separate concern.
605
+ return findings
606
+
607
+
608
+ def compute_bundle_manifest(primary: list[dict[str, Any]],
609
+ supporting: list[dict[str, Any]],
610
+ template_path: str,
611
+ template_sha: str) -> tuple[str, str]:
612
+ """Compute (manifest_text, manifest_sha) per spec §3.6.
613
+
614
+ Manifest format: '<role>:<path>:<sha>' lines, sorted lex by (role, path),
615
+ LF separator, trailing LF, then SHA-256 of UTF-8 bytes.
616
+ """
617
+ lines: list[tuple[str, str, str]] = []
618
+ for p in primary:
619
+ if isinstance(p, dict) and "path" in p and "sha" in p:
620
+ lines.append(("primary", str(p["path"]), str(p["sha"])))
621
+ for p in supporting:
622
+ if isinstance(p, dict) and "path" in p and "sha" in p:
623
+ lines.append(("supporting", str(p["path"]), str(p["sha"])))
624
+ lines.append(("template", template_path, template_sha))
625
+ lines.sort(key=lambda t: (t[0], t[1]))
626
+ text = "".join(f"{role}:{path}:{sha}\n" for role, path, sha in lines)
627
+ sha = hashlib.sha256(text.encode("utf-8")).hexdigest()
628
+ return text, sha
629
+
630
+
631
+ def check_b3(sidecar: dict[str, Any] | None, repo_root: Path,
632
+ location: str = "<sidecar>") -> list[LintError]:
633
+ """B3 — sidecar.prompt.bundle.bundle_manifest_sha == recomputed_sha.
634
+
635
+ Recomputes over the CURRENT on-disk SHA-256 of every primary +
636
+ supporting + template file (per §3.6 manifest format).
637
+
638
+ NOTE: this rule needs the actual files on disk. When fixtures use fake
639
+ paths, we recompute using the SHAs the sidecar declared (so the rule
640
+ still catches the case where the manifest_sha doesn't agree with the
641
+ declared per-file SHAs even before live-disk verification). When the
642
+ files DO exist on disk, we additionally verify that the live SHAs
643
+ agree, raising the more specific finding if they don't.
644
+ """
645
+ if sidecar is None:
646
+ return []
647
+ bundle = _safe_get(sidecar, "prompt", "bundle")
648
+ if not isinstance(bundle, dict):
649
+ return []
650
+ declared_sha = bundle.get("bundle_manifest_sha")
651
+ primary = bundle.get("primary_deliverables") or []
652
+ supporting = bundle.get("supporting_context") or []
653
+ template_path = _safe_get(sidecar, "prompt", "audit_template_path") or AUDIT_TEMPLATE_PATH
654
+ template_sha = _safe_get(sidecar, "prompt", "audit_template_sha")
655
+ if template_sha is None:
656
+ return [LintError("B3",
657
+ "sidecar.prompt.audit_template_sha missing", location)]
658
+
659
+ # Step 1 — recompute against the SHAs the sidecar itself declares.
660
+ _, sha_from_declared = compute_bundle_manifest(primary, supporting, template_path, template_sha)
661
+ findings: list[LintError] = []
662
+ if declared_sha != sha_from_declared:
663
+ findings.append(LintError("B3",
664
+ f"declared bundle_manifest_sha={declared_sha!r} disagrees with manifest of declared file SHAs (computed={sha_from_declared!r})",
665
+ location))
666
+
667
+ # Step 2 — verify against live disk SHAs. Codex round 2 P2 closure:
668
+ # a missing bundle file must NOT fall back to the sidecar-declared SHA
669
+ # (that would let a deleted/moved file silently pass B3 because the
670
+ # recomputed manifest would still match). Treat missing as
671
+ # stale/unverifiable: emit B3 finding and skip live-manifest comparison
672
+ # (no live manifest exists when files are gone). When repo_root has no
673
+ # .git/ marker (synthetic fixtures in tmp_path), skip the live check
674
+ # entirely — same convention as B4.
675
+ if not (repo_root / ".git").exists():
676
+ return findings # Step 1 already ran; Step 2 needs a real repo
677
+
678
+ live_primary: list[dict[str, Any]] = []
679
+ live_supporting: list[dict[str, Any]] = []
680
+ missing_bundle_files: list[str] = []
681
+ for role, items, sink in (("primary", primary, live_primary), ("supporting", supporting, live_supporting)):
682
+ for p in items:
683
+ if not isinstance(p, dict):
684
+ continue
685
+ path = p.get("path")
686
+ if not isinstance(path, str):
687
+ continue
688
+ disk_path = repo_root / path
689
+ if disk_path.exists():
690
+ disk_sha = _sha256_file(disk_path)
691
+ sink.append({"path": path, "sha": disk_sha or p.get("sha")})
692
+ else:
693
+ missing_bundle_files.append(f"{role}:{path}")
694
+ template_disk = repo_root / template_path
695
+ if template_disk.exists():
696
+ live_template_sha = _sha256_file(template_disk) or template_sha
697
+ else:
698
+ missing_bundle_files.append(f"template:{template_path}")
699
+ live_template_sha = template_sha
700
+
701
+ if missing_bundle_files:
702
+ # File-level mutation evidence (deletion / move) — flag explicitly so
703
+ # the auditor sees which file is gone, not just a manifest SHA mismatch.
704
+ findings.append(LintError("B3",
705
+ f"bundle file(s) missing on disk; cannot verify live manifest: "
706
+ f"{missing_bundle_files} — treat as stale/unverifiable",
707
+ location))
708
+ return findings
709
+
710
+ # All files present — recompute live manifest and compare.
711
+ _, sha_live = compute_bundle_manifest(live_primary, live_supporting, template_path, live_template_sha)
712
+ if declared_sha != sha_live:
713
+ findings.append(LintError("B3",
714
+ f"declared bundle_manifest_sha={declared_sha!r} disagrees with live bundle (computed={sha_live!r}) — bundle file changed since audit",
715
+ location))
716
+ return findings
717
+
718
+
719
+ def check_b4(sidecar: dict[str, Any] | None, repo_root: Path,
720
+ location: str = "<sidecar>") -> list[LintError]:
721
+ """B4 — sidecar.runner.git_sha resolves to a real commit."""
722
+ if sidecar is None:
723
+ return []
724
+ git_sha = _safe_get(sidecar, "runner", "git_sha")
725
+ if not isinstance(git_sha, str):
726
+ return []
727
+ # Skip live-git check when not in a real git repo (allows synthetic fixtures
728
+ # with valid-looking but fictitious SHAs to pass B4 without false positive).
729
+ git_dir = repo_root / ".git"
730
+ if not git_dir.exists():
731
+ return []
732
+ try:
733
+ result = subprocess.run(
734
+ ["git", "-C", str(repo_root), "cat-file", "-e", f"{git_sha}^{{commit}}"],
735
+ capture_output=True, text=True, timeout=10,
736
+ )
737
+ if result.returncode != 0:
738
+ return [LintError("B4",
739
+ f"runner.git_sha={git_sha!r} does not resolve to a real commit",
740
+ location)]
741
+ except (subprocess.SubprocessError, OSError) as e:
742
+ # Internal — don't fail the whole lint over a git error
743
+ return [LintError("B4",
744
+ f"git resolve failed for {git_sha!r}: {e}", location)]
745
+ return []
746
+
747
+
748
+ def check_b5(sidecar: dict[str, Any] | None,
749
+ location: str = "<sidecar>") -> list[LintError]:
750
+ """B5 — ended_at - started_at == duration_seconds (±1s)."""
751
+ if sidecar is None:
752
+ return []
753
+ timing = sidecar.get("timing") or {}
754
+ started = timing.get("started_at")
755
+ ended = timing.get("ended_at")
756
+ duration_raw = timing.get("duration_seconds")
757
+ if not (isinstance(started, str) and isinstance(ended, str)):
758
+ return []
759
+ try:
760
+ duration = float(duration_raw)
761
+ except (TypeError, ValueError):
762
+ return [LintError("B5",
763
+ f"duration_seconds not numeric: {duration_raw!r}", location)]
764
+ # Parse RFC3339 ms strings
765
+ if not (RFC3339_MS_RE.match(started) and RFC3339_MS_RE.match(ended)):
766
+ # Schema-level rejection territory; B5 still flags arithmetic mismatch
767
+ # but only if we can parse. Skip non-RFC3339-ms here.
768
+ return []
769
+ from datetime import datetime, timezone
770
+ fmt = "%Y-%m-%dT%H:%M:%S.%fZ"
771
+ try:
772
+ dt_s = datetime.strptime(started, fmt).replace(tzinfo=timezone.utc)
773
+ dt_e = datetime.strptime(ended, fmt).replace(tzinfo=timezone.utc)
774
+ except ValueError:
775
+ return []
776
+ delta = (dt_e - dt_s).total_seconds()
777
+ if abs(delta - duration) > 1.0:
778
+ return [LintError("B5",
779
+ f"timing arithmetic: ended_at - started_at = {delta:.3f}s, duration_seconds = {duration:.3f}s (±1s tolerance exceeded)",
780
+ location)]
781
+ return []
782
+
783
+
784
+ def check_b6(sidecar: dict[str, Any] | None, verdict: dict[str, Any] | None,
785
+ location: str = "<sidecar/verdict>") -> list[LintError]:
786
+ """B6 — sidecar.process.exit_code == 0 for non-AUDIT_FAILED entries.
787
+
788
+ Non-zero exit_code allowed only when verdict_status == AUDIT_FAILED.
789
+ """
790
+ if sidecar is None:
791
+ return []
792
+ exit_code = _safe_get(sidecar, "process", "exit_code")
793
+ if not isinstance(exit_code, int):
794
+ return []
795
+ v_status = _safe_get(verdict, "verdict_status") if verdict else None
796
+ if exit_code != 0 and v_status != "AUDIT_FAILED":
797
+ return [LintError("B6",
798
+ f"process.exit_code={exit_code} non-zero with verdict_status={v_status!r} (only AUDIT_FAILED permits non-zero exit)",
799
+ location)]
800
+ if exit_code == 0 and v_status == "AUDIT_FAILED":
801
+ # Spec doesn't strictly forbid; AUDIT_FAILED can come from JSONL parse
802
+ # error even when codex exited 0. Don't flag.
803
+ pass
804
+ return []
805
+
806
+
807
+ def check_b7(entry: dict[str, Any] | None, sidecar: dict[str, Any] | None,
808
+ entry_path: Path | None, sidecar_path: Path | None,
809
+ jsonl_path: Path | None, verdict_path: Path | None,
810
+ mode: str, location: str = "<artifacts>",
811
+ repo_root: Path | None = None,
812
+ verdict: dict[str, Any] | None = None) -> list[LintError]:
813
+ """B7 — entry.run_id == sidecar.run_id == verdict.run_id == bare basename of every co-located file.
814
+
815
+ Proposal mode: 4 files (jsonl + meta.json + verdict.yaml + entry.json).
816
+ Persisted mode: 3 files (entry consumed; not file-checked here).
817
+
818
+ repo_root enables the round-8 P2 closure: cross-check that entry's
819
+ recorded artifact_paths actually resolve to (and equal) the CLI-loaded
820
+ files. Without this, an entry could record artifact_paths pointing at
821
+ a missing or wrong directory while the CLI loads valid evidence from
822
+ --output-dir; the orchestrator later follows the recorded paths.
823
+
824
+ `verdict` enables the round-15 P1 closure: cross-check the verdict
825
+ file's internal `run_id` field against entry+sidecar+basename. C1
826
+ mirrors only the nested verdict block (status/round/counts/...) and
827
+ skips the verdict-file-level run_id; without this check, swapping a
828
+ valid verdict.yaml from a different run into the canonical filename
829
+ slot would pass B7 (basenames match), C1 (counts mirror entry), and
830
+ the rest of the gate as long as PASS/MINOR/MATERIAL counts agree.
831
+ """
832
+ if entry is None or sidecar is None:
833
+ return []
834
+ findings: list[LintError] = []
835
+ entry_run_id = entry.get("run_id")
836
+ side_run_id = sidecar.get("run_id")
837
+ if entry_run_id != side_run_id:
838
+ findings.append(LintError("B7",
839
+ f"entry.run_id={entry_run_id!r} != sidecar.run_id={side_run_id!r}",
840
+ location))
841
+ # Codex round 15 P1 closure: also verify verdict.run_id matches.
842
+ if verdict is not None:
843
+ v_run_id = verdict.get("run_id")
844
+ if v_run_id is not None and v_run_id != side_run_id:
845
+ findings.append(LintError("B7",
846
+ f"verdict.run_id={v_run_id!r} != sidecar.run_id={side_run_id!r} "
847
+ f"(swap-one-verdict-file forgery seam — verdict file was renamed "
848
+ f"into <run_id>.verdict.yaml but internal run_id belongs to a "
849
+ f"different run)",
850
+ location))
851
+
852
+ # File basename checks (only when we have actual paths)
853
+ canonical = side_run_id if isinstance(side_run_id, str) else entry_run_id
854
+ if not isinstance(canonical, str):
855
+ return findings
856
+
857
+ files_to_check: list[tuple[Path, str, str]] = []
858
+ if sidecar_path is not None:
859
+ files_to_check.append((sidecar_path, ".meta.json", "sidecar"))
860
+ if jsonl_path is not None:
861
+ files_to_check.append((jsonl_path, ".jsonl", "jsonl"))
862
+ if verdict_path is not None:
863
+ files_to_check.append((verdict_path, ".verdict.yaml", "verdict"))
864
+ if mode == "proposal" and entry_path is not None:
865
+ files_to_check.append((entry_path, ".audit_artifact_entry.json", "entry"))
866
+
867
+ for path, ext, role in files_to_check:
868
+ bare = _bare_run_id_from_basename(path.name, ext)
869
+ if bare is None:
870
+ findings.append(LintError("B7",
871
+ f"{role} file {path.name!r} does not end with {ext!r}",
872
+ location))
873
+ elif bare != canonical:
874
+ findings.append(LintError("B7",
875
+ f"{role} file basename stem={bare!r} != canonical run_id={canonical!r}",
876
+ location))
877
+
878
+ # Codex round 4 P2 closure: entry["artifact_paths"] is the contract the
879
+ # orchestrator follows post-merge. A hand-edited entry that points its
880
+ # artifact_paths at a different run's files (jsonl/sidecar/verdict)
881
+ # would forge the swap-one-artifact-file attack seam (§3.7 B7). The
882
+ # B7 cross-file check above only inspects the paths supplied to the
883
+ # CLI; an explicit basename validation of the recorded artifact_paths
884
+ # closes the gap regardless of how the lint was invoked.
885
+ artifact_paths = entry.get("artifact_paths") if isinstance(entry, dict) else None
886
+ cli_loaded_paths = {
887
+ "jsonl": jsonl_path,
888
+ "sidecar": sidecar_path,
889
+ "verdict": verdict_path,
890
+ }
891
+ if isinstance(artifact_paths, dict):
892
+ for key, ext in (("jsonl", ".jsonl"),
893
+ ("sidecar", ".meta.json"),
894
+ ("verdict", ".verdict.yaml")):
895
+ recorded = artifact_paths.get(key)
896
+ if not isinstance(recorded, str) or not recorded:
897
+ continue # schema validation already covers missing/non-str
898
+ recorded_basename = recorded.rsplit("/", 1)[-1]
899
+ bare = _bare_run_id_from_basename(recorded_basename, ext)
900
+ if bare is None:
901
+ findings.append(LintError("B7",
902
+ f"entry.artifact_paths.{key}={recorded!r} basename does not end with {ext!r}",
903
+ location))
904
+ continue
905
+ if bare != canonical:
906
+ findings.append(LintError("B7",
907
+ f"entry.artifact_paths.{key}={recorded!r} basename stem={bare!r} "
908
+ f"!= canonical run_id={canonical!r} (artifact-paths forgery seam)",
909
+ location))
910
+ continue
911
+ # Codex round 8 P2 closure: basename match alone leaves the path
912
+ # forgery seam open — entry can record paths pointing at a
913
+ # missing or wrong directory while CLI loads valid files from
914
+ # elsewhere. Resolve the recorded path against repo_root and
915
+ # confirm (a) the file actually exists at that location, and
916
+ # (b) it matches the CLI-loaded artifact for that role.
917
+ if repo_root is not None:
918
+ resolved = (repo_root / recorded).resolve()
919
+ if not resolved.exists():
920
+ findings.append(LintError("B7",
921
+ f"entry.artifact_paths.{key}={recorded!r} resolves to "
922
+ f"{resolved} which does not exist on disk — recorded "
923
+ f"path is the contract orchestrator follows post-merge",
924
+ location))
925
+ continue
926
+ cli_path = cli_loaded_paths.get(key)
927
+ if cli_path is not None and cli_path.exists():
928
+ cli_resolved = cli_path.resolve()
929
+ if resolved != cli_resolved and not resolved.samefile(cli_resolved):
930
+ findings.append(LintError("B7",
931
+ f"entry.artifact_paths.{key}={recorded!r} resolves to "
932
+ f"{resolved} but CLI loaded {cli_resolved} — declared "
933
+ f"path is the evidence the orchestrator will verify; "
934
+ f"loading from a different location masks divergence",
935
+ location))
936
+ return findings
937
+
938
+
939
+ def check_b8(entry: dict[str, Any] | None, mode: str,
940
+ location: str = "<entry>") -> list[LintError]:
941
+ """B8 — persisted ack: verdict.verified_at == acknowledgement.acknowledged_at."""
942
+ if entry is None or mode != "persisted":
943
+ return []
944
+ ack = entry.get("acknowledgement")
945
+ if not isinstance(ack, dict):
946
+ return []
947
+ verified_at = _safe_get(entry, "verdict", "verified_at")
948
+ ack_at = ack.get("acknowledged_at")
949
+ if verified_at != ack_at:
950
+ return [LintError("B8",
951
+ f"verified_at={verified_at!r} != acknowledgement.acknowledged_at={ack_at!r} (must be same instant by D3 construction)",
952
+ location)]
953
+ return []
954
+
955
+
956
+ def check_b9(entry: dict[str, Any] | None, sidecar: dict[str, Any] | None,
957
+ location: str = "<entry/sidecar>") -> list[LintError]:
958
+ """B9 — when entry.bundle_id present, equals sidecar.prompt.bundle.bundle_id."""
959
+ if entry is None or sidecar is None:
960
+ return []
961
+ if "bundle_id" not in entry:
962
+ return []
963
+ entry_bid = entry.get("bundle_id")
964
+ side_bid = _safe_get(sidecar, "prompt", "bundle", "bundle_id")
965
+ if entry_bid != side_bid:
966
+ return [LintError("B9",
967
+ f"entry.bundle_id={entry_bid!r} != sidecar.prompt.bundle.bundle_id={side_bid!r}",
968
+ location)]
969
+ return []
970
+
971
+
972
+ def check_b10(entry: dict[str, Any] | None, verdict: dict[str, Any] | None,
973
+ mode: str, location: str = "<entry/verdict>") -> list[LintError]:
974
+ """B10 — persisted ack acknowledgement.finding_ids: non-empty, all exist
975
+ in verdict.findings[].id, full coverage (set equality)."""
976
+ if entry is None or verdict is None or mode != "persisted":
977
+ return []
978
+ ack = entry.get("acknowledgement")
979
+ if not isinstance(ack, dict):
980
+ return []
981
+ declared = ack.get("finding_ids")
982
+ if not isinstance(declared, list):
983
+ return [LintError("B10",
984
+ f"acknowledgement.finding_ids must be array (got {type(declared).__name__})",
985
+ location)]
986
+ if len(declared) == 0:
987
+ return [LintError("B10",
988
+ "acknowledgement.finding_ids must be non-empty", location)]
989
+ findings_list = verdict.get("findings") or []
990
+ real_ids = {f.get("id") for f in findings_list if isinstance(f, dict)}
991
+ declared_set = set(declared)
992
+ out: list[LintError] = []
993
+ missing_in_real = declared_set - real_ids
994
+ if missing_in_real:
995
+ out.append(LintError("B10",
996
+ f"acknowledgement.finding_ids contains unknown ids {sorted(missing_in_real)} (not in verdict.findings[].id)",
997
+ location))
998
+ missing_in_declared = real_ids - declared_set
999
+ if missing_in_declared:
1000
+ out.append(LintError("B10",
1001
+ f"acknowledgement.finding_ids missing full coverage: {sorted(missing_in_declared)} not acknowledged",
1002
+ location))
1003
+ return out
1004
+
1005
+
1006
+ # ---------------------------------------------------------------------------
1007
+ # Family C — Mirror rules
1008
+ # ---------------------------------------------------------------------------
1009
+
1010
+
1011
+ def check_c1(entry: dict[str, Any] | None, verdict: dict[str, Any] | None,
1012
+ location: str = "<entry/verdict>") -> list[LintError]:
1013
+ """C1 — entry.verdict.{status, round, target_rounds, finding_counts, failure_reason}
1014
+ mirrors verdict file's matching fields (verdict file wins)."""
1015
+ if entry is None or verdict is None:
1016
+ return []
1017
+ ev = entry.get("verdict") or {}
1018
+ findings: list[LintError] = []
1019
+
1020
+ pairs = [
1021
+ ("status", "verdict_status"),
1022
+ ("round", "round"),
1023
+ ("target_rounds", "target_rounds"),
1024
+ ("finding_counts", "finding_counts"),
1025
+ ("failure_reason", "failure_reason"),
1026
+ ]
1027
+ for entry_key, vfile_key in pairs:
1028
+ ev_val = ev.get(entry_key)
1029
+ vf_val = verdict.get(vfile_key)
1030
+ # failure_reason is conditional — present-or-absent on each side; we
1031
+ # compare presence consistency too.
1032
+ if entry_key == "failure_reason":
1033
+ if (ev_val is None) != (vf_val is None):
1034
+ findings.append(LintError("C1",
1035
+ f"failure_reason presence drift: entry={ev_val!r}, verdict_file={vf_val!r}",
1036
+ location))
1037
+ elif ev_val is not None and ev_val != vf_val:
1038
+ findings.append(LintError("C1",
1039
+ f"failure_reason drift: entry={ev_val!r}, verdict_file={vf_val!r}",
1040
+ location))
1041
+ continue
1042
+ if ev_val != vf_val:
1043
+ findings.append(LintError("C1",
1044
+ f"{entry_key} drift: entry={ev_val!r}, verdict_file={vf_val!r} (verdict file wins)",
1045
+ location))
1046
+ return findings
1047
+
1048
+
1049
+ def check_c2(entry: dict[str, Any] | None, sidecar: dict[str, Any] | None,
1050
+ location: str = "<entry/sidecar>") -> list[LintError]:
1051
+ """C2 — entry.bundle_manifest_sha == sidecar.prompt.bundle.bundle_manifest_sha."""
1052
+ if entry is None or sidecar is None:
1053
+ return []
1054
+ e_sha = entry.get("bundle_manifest_sha")
1055
+ s_sha = _safe_get(sidecar, "prompt", "bundle", "bundle_manifest_sha")
1056
+ if e_sha != s_sha:
1057
+ return [LintError("C2",
1058
+ f"entry.bundle_manifest_sha={e_sha!r} != sidecar.bundle_manifest_sha={s_sha!r}",
1059
+ location)]
1060
+ return []
1061
+
1062
+
1063
+ def check_c3(new_entry: dict[str, Any] | None, prior_entry: dict[str, Any] | None,
1064
+ location: str = "<entry/prior>") -> list[LintError]:
1065
+ """C3 — Acknowledgement append entry copies prior entry's
1066
+ (stage, agent, deliverable_path, deliverable_sha, run_id,
1067
+ bundle_manifest_sha, artifact_paths) AND inner verdict shape
1068
+ (status, round, target_rounds, finding_counts, failure_reason)
1069
+ byte-for-byte. verified_at and verified_by are FRESHLY set, NOT copied.
1070
+ """
1071
+ if new_entry is None or prior_entry is None:
1072
+ return []
1073
+ findings: list[LintError] = []
1074
+ copied_fields = ("stage", "agent", "deliverable_path", "deliverable_sha",
1075
+ "run_id", "bundle_manifest_sha", "artifact_paths")
1076
+ for f in copied_fields:
1077
+ if new_entry.get(f) != prior_entry.get(f):
1078
+ findings.append(LintError("C3",
1079
+ f"ack entry must copy {f} byte-for-byte: prior={prior_entry.get(f)!r}, new={new_entry.get(f)!r}",
1080
+ location))
1081
+ # Inner verdict shape
1082
+ new_v = new_entry.get("verdict") or {}
1083
+ prior_v = prior_entry.get("verdict") or {}
1084
+ inner_fields = ("status", "round", "target_rounds", "finding_counts", "failure_reason")
1085
+ for f in inner_fields:
1086
+ if new_v.get(f) != prior_v.get(f):
1087
+ findings.append(LintError("C3",
1088
+ f"ack entry must copy verdict.{f} byte-for-byte: prior={prior_v.get(f)!r}, new={new_v.get(f)!r}",
1089
+ location))
1090
+ # verified_at must be FRESHLY SET (different from prior).
1091
+ if (new_v.get("verified_at") is not None and
1092
+ new_v.get("verified_at") == prior_v.get("verified_at")):
1093
+ findings.append(LintError("C3",
1094
+ f"ack entry verified_at={new_v.get('verified_at')!r} must be freshly set (>= prior + 1ms via D3); equals prior",
1095
+ location))
1096
+ return findings
1097
+
1098
+
1099
+ def check_c4(entry: dict[str, Any] | None, sidecar: dict[str, Any] | None,
1100
+ location: str = "<entry/sidecar>") -> list[LintError]:
1101
+ """C4 — entry.deliverable_sha == sidecar primary_deliverables[].sha
1102
+ for matching primary deliverable (entry-side half of B2)."""
1103
+ # We reuse the same check as B2 minus the disk verification; emit C4 not B2.
1104
+ if entry is None or sidecar is None:
1105
+ return []
1106
+ findings: list[LintError] = []
1107
+ entry_sha = entry.get("deliverable_sha")
1108
+ deliv_path = entry.get("deliverable_path")
1109
+ primaries = _safe_get(sidecar, "prompt", "bundle", "primary_deliverables") or []
1110
+ side_sha = None
1111
+ for p in primaries:
1112
+ if isinstance(p, dict) and p.get("path") == deliv_path:
1113
+ side_sha = p.get("sha")
1114
+ break
1115
+ if side_sha is None:
1116
+ findings.append(LintError("C4",
1117
+ f"sidecar.prompt.bundle.primary_deliverables has no entry for {deliv_path!r}",
1118
+ location))
1119
+ elif entry_sha != side_sha:
1120
+ findings.append(LintError("C4",
1121
+ f"entry.deliverable_sha={entry_sha!r} != sidecar primary[{deliv_path!r}].sha={side_sha!r}",
1122
+ location))
1123
+ return findings
1124
+
1125
+
1126
+ # ---------------------------------------------------------------------------
1127
+ # Family D — Ordering rules
1128
+ # ---------------------------------------------------------------------------
1129
+
1130
+
1131
+ def check_d1(passport_audit_artifacts: list[dict[str, Any]] | None,
1132
+ location: str = "<passport>") -> list[LintError]:
1133
+ """D1 — Latest persisted entry by max(verdict.verified_at) per (stage, agent,
1134
+ deliverable_sha) tuple. Lint flags duplicate verified_at within group as
1135
+ ordering ambiguity (D3 should make this impossible).
1136
+
1137
+ We report duplicates; selection logic itself isn't a runtime check.
1138
+ """
1139
+ if not passport_audit_artifacts:
1140
+ return []
1141
+ findings: list[LintError] = []
1142
+ seen: dict[tuple, list[str]] = {}
1143
+ for entry in passport_audit_artifacts:
1144
+ if not isinstance(entry, dict):
1145
+ continue
1146
+ key = (entry.get("stage"), entry.get("agent"),
1147
+ entry.get("deliverable_sha"), entry.get("run_id"))
1148
+ va = _safe_get(entry, "verdict", "verified_at")
1149
+ if va is None:
1150
+ continue
1151
+ seen.setdefault(key, []).append(va)
1152
+ for key, vas in seen.items():
1153
+ if len(vas) != len(set(vas)):
1154
+ findings.append(LintError("D1",
1155
+ f"duplicate verified_at in (stage,agent,sha,run_id)={key}: {vas} — D3 monotonic helper should prevent this",
1156
+ location))
1157
+ return findings
1158
+
1159
+
1160
+ def check_d2(proposals: list[dict[str, Any]] | None,
1161
+ location: str = "<output-dir>") -> list[LintError]:
1162
+ """D2 — Path B proposal selection determinism check.
1163
+
1164
+ Lint surface: detect proposals sharing identical sidecar.timing.started_at
1165
+ where run_id lex-max would be the only tie-breaker (legitimate but worth
1166
+ surfacing as ambiguity warning).
1167
+ """
1168
+ if not proposals:
1169
+ return []
1170
+ findings: list[LintError] = []
1171
+ by_started: dict[str, list[str]] = {}
1172
+ for p in proposals:
1173
+ st = _safe_get(p, "sidecar", "timing", "started_at")
1174
+ rid = _safe_get(p, "entry", "run_id")
1175
+ if not isinstance(st, str) or not isinstance(rid, str):
1176
+ continue
1177
+ by_started.setdefault(st, []).append(rid)
1178
+ for st, rids in by_started.items():
1179
+ if len(rids) > 1:
1180
+ findings.append(LintError("D2",
1181
+ f"proposals share started_at={st!r}: {sorted(rids)} — falling back to run_id lex-max tie-breaker",
1182
+ location))
1183
+ return findings
1184
+
1185
+
1186
+ def check_d3(passport_audit_artifacts: list[dict[str, Any]] | None,
1187
+ location: str = "<passport>") -> list[LintError]:
1188
+ """D3 — every persisted verified_at strictly greater than every prior.
1189
+
1190
+ Validates monotonicity over the ledger as ordered. A correctly-running
1191
+ orchestrator (using _next_verified_at_ms) emits monotonic verified_at;
1192
+ if the ledger is not in append order we instead check lex-max property:
1193
+ no entry's verified_at is < a later-indexed entry's verified_at.
1194
+ Strictly: each entry's verified_at must be > all earlier entries'.
1195
+ """
1196
+ if not passport_audit_artifacts:
1197
+ return []
1198
+ findings: list[LintError] = []
1199
+ seen: list[str] = []
1200
+ for idx, entry in enumerate(passport_audit_artifacts):
1201
+ if not isinstance(entry, dict):
1202
+ continue
1203
+ va = _safe_get(entry, "verdict", "verified_at")
1204
+ if not isinstance(va, str):
1205
+ continue
1206
+ for prior_idx, prior_va in enumerate(seen):
1207
+ if va <= prior_va:
1208
+ findings.append(LintError("D3",
1209
+ f"audit_artifact[{idx}].verified_at={va!r} not > audit_artifact[{prior_idx}].verified_at={prior_va!r}",
1210
+ location))
1211
+ break
1212
+ seen.append(va)
1213
+ return findings
1214
+
1215
+
1216
+ def check_d4(persisted_round: int | None, proposal_round: int | None,
1217
+ location: str = "<entry>") -> list[LintError]:
1218
+ """D4 — higher-round unmerged proposals supersede lower-round persisted entries.
1219
+
1220
+ Lint surface: when an unmerged proposal exists alongside a persisted
1221
+ entry for the same (stage, agent, deliverable_sha) tuple AND the
1222
+ proposal's round is greater than the persisted round, surface the
1223
+ supersession requirement (spec §3.7 family D row D4 + §5.6 A1.5).
1224
+ The orchestrator must preempt Path A and run Path B with
1225
+ supersession_required=true; persisted-mode lint flagging this lets
1226
+ the caller see the supersession before the orchestrator processes
1227
+ the artifacts.
1228
+ """
1229
+ if persisted_round is None or proposal_round is None:
1230
+ return []
1231
+ if proposal_round > persisted_round:
1232
+ return [LintError("D4",
1233
+ f"unmerged proposal round={proposal_round} supersedes persisted round={persisted_round} "
1234
+ f"— orchestrator must preempt Path A and run Path B with supersession_required=true",
1235
+ location)]
1236
+ return []
1237
+
1238
+
1239
+ # ---------------------------------------------------------------------------
1240
+ # Family E — Lifecycle ownership
1241
+ # ---------------------------------------------------------------------------
1242
+
1243
+
1244
+ def check_e1_e2_e6(passport_audit_artifacts: list[dict[str, Any]] | None,
1245
+ location: str = "<passport>") -> list[LintError]:
1246
+ """E1/E2/E6 — post-hoc passport-shape checks.
1247
+
1248
+ Detect non-orchestrator-emitted entries by looking for entries that lack
1249
+ the orchestrator-set fields (verified_at / verified_by). Also detect
1250
+ AUDIT_FAILED entries in the passport (E5/E2 — never persisted).
1251
+ """
1252
+ if not passport_audit_artifacts:
1253
+ return []
1254
+ findings: list[LintError] = []
1255
+ for idx, entry in enumerate(passport_audit_artifacts):
1256
+ if not isinstance(entry, dict):
1257
+ findings.append(LintError("E1/E2/E6",
1258
+ f"audit_artifact[{idx}] is not an object (type={type(entry).__name__})",
1259
+ location))
1260
+ continue
1261
+ verdict = entry.get("verdict") or {}
1262
+ if "verified_at" not in verdict:
1263
+ findings.append(LintError("E1/E2/E6",
1264
+ f"audit_artifact[{idx}].verdict missing verified_at — non-orchestrator writer suspected",
1265
+ location))
1266
+ if "verified_by" not in verdict:
1267
+ findings.append(LintError("E1/E2/E6",
1268
+ f"audit_artifact[{idx}].verdict missing verified_by — non-orchestrator writer suspected",
1269
+ location))
1270
+ elif verdict.get("verified_by") != "pipeline_orchestrator_agent":
1271
+ findings.append(LintError("E1/E2/E6",
1272
+ f"audit_artifact[{idx}].verdict.verified_by={verdict.get('verified_by')!r} not 'pipeline_orchestrator_agent'",
1273
+ location))
1274
+ # Codex round 3 P2 closure: §3.2 lifecycle-conditional table
1275
+ # excludes AUDIT_FAILED from the persisted arm. A hand-edited
1276
+ # passport entry with verified_at + verified_by + AUDIT_FAILED
1277
+ # status would otherwise pass E1/E2/E6's verified_at + verified_by
1278
+ # checks above (E5 lives at schema level for CLI modes via the
1279
+ # persisted oneOf arm; passport scan needs the same enforcement
1280
+ # because AUDIT_FAILED entries are forbidden in `audit_artifact[]`).
1281
+ if verdict.get("status") == "AUDIT_FAILED":
1282
+ findings.append(LintError("E5",
1283
+ f"audit_artifact[{idx}].verdict.status='AUDIT_FAILED' is forbidden in "
1284
+ f"passport audit_artifact[] (§3.2: AUDIT_FAILED is proposal-arm only; "
1285
+ f"persisted ledger never carries failed-audit entries)",
1286
+ location))
1287
+ return findings
1288
+
1289
+
1290
+ def check_e3_e4(entry: dict[str, Any] | None, mode: str,
1291
+ location: str = "<entry>") -> list[LintError]:
1292
+ """E3/E4 — wrapper-emitted proposals carrying verified_at/verified_by are rejected.
1293
+
1294
+ Defense-in-depth on top of schema oneOf.proposal arm.
1295
+ """
1296
+ if entry is None or mode != "proposal":
1297
+ return []
1298
+ findings: list[LintError] = []
1299
+ verdict = entry.get("verdict") or {}
1300
+ if "verified_at" in verdict:
1301
+ findings.append(LintError("E3/E4",
1302
+ "proposal entry carries verdict.verified_at (Pattern C3 attack surface)",
1303
+ location))
1304
+ if "verified_by" in verdict:
1305
+ findings.append(LintError("E3/E4",
1306
+ "proposal entry carries verdict.verified_by (Pattern C3 attack surface)",
1307
+ location))
1308
+ return findings
1309
+
1310
+
1311
+ def check_e5(entry: dict[str, Any] | None, mode: str,
1312
+ location: str = "<entry>") -> list[LintError]:
1313
+ """E5 — --mode persisted rejects AUDIT_FAILED status."""
1314
+ if entry is None or mode != "persisted":
1315
+ return []
1316
+ status = _safe_get(entry, "verdict", "status")
1317
+ if status == "AUDIT_FAILED":
1318
+ return [LintError("E5",
1319
+ "persisted entry MUST NOT carry status=AUDIT_FAILED (proposal-arm only)",
1320
+ location)]
1321
+ return []
1322
+
1323
+
1324
+ def check_e7(entry: dict[str, Any] | None, location: str = "<entry>") -> list[LintError]:
1325
+ """E7 — ack entry's verdict.status remains MATERIAL (not synthetic
1326
+ 'MATERIAL_ACKNOWLEDGED' or similar)."""
1327
+ if entry is None:
1328
+ return []
1329
+ if "acknowledgement" not in entry:
1330
+ return []
1331
+ status = _safe_get(entry, "verdict", "status")
1332
+ if status != "MATERIAL":
1333
+ return [LintError("E7",
1334
+ f"ack entry must keep verdict.status='MATERIAL'; got {status!r} (no synthetic MATERIAL_ACKNOWLEDGED)",
1335
+ location)]
1336
+ return []
1337
+
1338
+
1339
+ # ---------------------------------------------------------------------------
1340
+ # Family F — Naming conventions
1341
+ # ---------------------------------------------------------------------------
1342
+
1343
+
1344
+ def check_f1(run_id: Any, location: str = "<entry>") -> list[LintError]:
1345
+ """F1 — run_id format <ISO-8601-Z>-<4-hex>."""
1346
+ if not isinstance(run_id, str):
1347
+ return [LintError("F1",
1348
+ f"run_id missing or not string (got {type(run_id).__name__})",
1349
+ location)]
1350
+ if not RUN_ID_RE.match(run_id):
1351
+ return [LintError("F1",
1352
+ f"run_id={run_id!r} does not match {RUN_ID_RE.pattern}",
1353
+ location)]
1354
+ return []
1355
+
1356
+
1357
+ def check_f2(jsonl_path: Path | None, sidecar_path: Path | None,
1358
+ verdict_path: Path | None, entry_path: Path | None,
1359
+ mode: str, location: str = "<artifacts>") -> list[LintError]:
1360
+ """F2 — 4 (proposal) or 3 (persisted) artifact basenames use bare run_id stem with
1361
+ extensions .jsonl / .meta.json / .verdict.yaml / .audit_artifact_entry.json.
1362
+ No stage/agent/deliverable prefix.
1363
+ """
1364
+ findings: list[LintError] = []
1365
+ expectations: list[tuple[Path | None, str, str]] = [
1366
+ (jsonl_path, ".jsonl", "jsonl"),
1367
+ (sidecar_path, ".meta.json", "sidecar"),
1368
+ (verdict_path, ".verdict.yaml", "verdict"),
1369
+ ]
1370
+ if mode == "proposal":
1371
+ expectations.append((entry_path, ".audit_artifact_entry.json", "entry"))
1372
+
1373
+ for path, ext, role in expectations:
1374
+ if path is None:
1375
+ continue
1376
+ if not path.name.endswith(ext):
1377
+ findings.append(LintError("F2",
1378
+ f"{role} file {path.name!r} does not end with {ext!r}",
1379
+ location))
1380
+ continue
1381
+ bare = path.name[: -len(ext)]
1382
+ if not RUN_ID_RE.match(bare):
1383
+ findings.append(LintError("F2",
1384
+ f"{role} file basename stem={bare!r} not bare run_id (no stage/agent prefix allowed)",
1385
+ location))
1386
+ return findings
1387
+
1388
+
1389
+ def check_f3(sidecar: dict[str, Any] | None, sidecar_path: Path | None,
1390
+ location: str = "<sidecar>") -> list[LintError]:
1391
+ """F3 — sidecar.run_id == file basename (with .meta.json stripped)."""
1392
+ if sidecar is None or sidecar_path is None:
1393
+ return []
1394
+ side_run_id = sidecar.get("run_id")
1395
+ bare = _bare_run_id_from_basename(sidecar_path.name, ".meta.json")
1396
+ if bare is None:
1397
+ return [LintError("F3",
1398
+ f"sidecar file {sidecar_path.name!r} does not end with .meta.json",
1399
+ location)]
1400
+ if side_run_id != bare:
1401
+ return [LintError("F3",
1402
+ f"sidecar.run_id={side_run_id!r} != file basename stem={bare!r}",
1403
+ location)]
1404
+ return []
1405
+
1406
+
1407
+ # ---------------------------------------------------------------------------
1408
+ # Aggregation per mode
1409
+ # ---------------------------------------------------------------------------
1410
+
1411
+
1412
+ def run_checks(ctx: LintContext) -> list[LintError]:
1413
+ """Run every applicable rule for the given mode."""
1414
+ findings: list[LintError] = []
1415
+ mode = ctx.mode
1416
+
1417
+ if mode == "jsonl-stream":
1418
+ findings.extend(check_a7(ctx.jsonl_events, location=str(ctx.jsonl_path or "<jsonl>")))
1419
+ return findings
1420
+
1421
+ # Proposal / persisted modes — full sweep
1422
+ entry_loc = str(ctx.entry_path or "<entry>")
1423
+ verdict_loc = str(ctx.verdict_path or "<verdict>")
1424
+ sidecar_loc = str(ctx.sidecar_path or "<sidecar>")
1425
+
1426
+ # Family A
1427
+ findings.extend(check_a1(ctx.entry, ctx.verdict, location=entry_loc))
1428
+ findings.extend(check_a2(ctx.entry, ctx.verdict, location=entry_loc))
1429
+ findings.extend(check_a3(ctx.entry, ctx.verdict, location=entry_loc))
1430
+ findings.extend(check_a4(ctx.entry, mode, location=entry_loc))
1431
+ findings.extend(check_a5(ctx.verdict, location=verdict_loc))
1432
+ findings.extend(check_a6(ctx.verdict, location=verdict_loc))
1433
+ if ctx.jsonl_events is not None:
1434
+ # Codex round 6 P2 closure: A7 tool-event pairing is suspended for
1435
+ # AUDIT_FAILED bundles. A failed audit's stream is legitimately
1436
+ # truncated — codex may be killed after an item.started but before
1437
+ # the matching item.completed — so unmatched starts are expected
1438
+ # evidence, not pairing violations. §3.4 suspends Layer 3 cross-
1439
+ # file rules for AUDIT_FAILED; the symmetric Layer 2 suspension
1440
+ # for stream-shape (round 3 closure) applies to A7 too.
1441
+ v_status_for_a7 = (ctx.verdict or {}).get("verdict_status")
1442
+ if v_status_for_a7 != "AUDIT_FAILED":
1443
+ findings.extend(check_a7(ctx.jsonl_events, location=str(ctx.jsonl_path or "<jsonl>")))
1444
+ # Codex round 3 P1 closure: A7 alone does not reject a JSONL that
1445
+ # ends after `thread.started + turn.started` (no item.started, so
1446
+ # no pairing violation). Phase 6.1's parse_audit_verdict.validate_
1447
+ # stream_shape covers L2-3/L2-4 stream-shape gates: exactly one
1448
+ # thread.started, second event is turn.started, exactly one
1449
+ # terminal turn.completed strictly after the last agent_message,
1450
+ # turn.completed.usage all integers >= 0 with input_tokens > 0,
1451
+ # canonical UUID thread_id, no error events. Reuse it here so a
1452
+ # truncated stream is rejected before the rest of the gate runs.
1453
+ # AUDIT_FAILED bundles legitimately have malformed streams (codex
1454
+ # was killed mid-run); §3.4 already suspends Layer 3 cross-file
1455
+ # rules for them, and we apply the same suspension here.
1456
+ v_status = (ctx.verdict or {}).get("verdict_status")
1457
+ if v_status != "AUDIT_FAILED":
1458
+ # Codex round 5 P2 closure: load parse_audit_verdict by file path
1459
+ # via importlib so the stream-shape gate runs regardless of how
1460
+ # this checker is invoked (CLI on sys.path, `python -m
1461
+ # scripts.check_audit_artifact_consistency`, or imported as
1462
+ # `scripts.check_audit_artifact_consistency` from elsewhere).
1463
+ # Silent ImportError degrade was hiding the gate from package
1464
+ # callers and letting truncated streams pass.
1465
+ module = _load_parse_audit_verdict()
1466
+ ParseError = module.ParseError
1467
+ try:
1468
+ module.validate_stream_shape(ctx.jsonl_events)
1469
+ except ParseError as e:
1470
+ findings.append(LintError(
1471
+ "L2-3/L2-4",
1472
+ f"jsonl stream-shape rejected: {e} — non-AUDIT_FAILED "
1473
+ f"verdict requires a complete stream (thread.started → "
1474
+ f"turn.started → … → turn.completed with valid usage)",
1475
+ str(ctx.jsonl_path or "<jsonl>"),
1476
+ ))
1477
+ else:
1478
+ # Codex round 8 P1 closure: stream-shape alone validates
1479
+ # ordering / usage / canonical UUID but does NOT verify the
1480
+ # last agent_message contains a parseable Section 6 verdict.
1481
+ # A bundle whose JSONL ends with arbitrary text or no
1482
+ # verdict at all could pass stream-shape and exit 0 as long
1483
+ # as the separate verdict.yaml mirrored the entry. L2-4 in
1484
+ # spec §5.2 is `parse_audit_verdict.py --probe` — extract
1485
+ # the last agent_message and parse_section6 on its text.
1486
+ # cmd_probe in parse_audit_verdict already chains all three
1487
+ # checks; we replicate that chain here without invoking
1488
+ # the module's CLI (avoids subprocess overhead in lint
1489
+ # path) and capture each layer as its own LintError.
1490
+ try:
1491
+ verdict_text = module.extract_verdict_text(ctx.jsonl_events)
1492
+ except ParseError as e:
1493
+ findings.append(LintError(
1494
+ "L2-4",
1495
+ f"verdict text extraction failed: {e} — non-AUDIT_FAILED "
1496
+ f"bundle requires a parseable agent_message in the JSONL",
1497
+ str(ctx.jsonl_path or "<jsonl>"),
1498
+ ))
1499
+ else:
1500
+ try:
1501
+ # Probe-style: pass current_round=None to accept any
1502
+ # parseable summary; cross-field count validation is
1503
+ # already covered by A5 against the verdict.yaml.
1504
+ module.parse_section6(verdict_text, current_round=None)
1505
+ except ParseError as e:
1506
+ findings.append(LintError(
1507
+ "L2-4",
1508
+ f"verdict text Section 6 parse failed: {e} — "
1509
+ f"agent_message did not contain a parseable "
1510
+ f"audit-template Section 6 verdict block",
1511
+ str(ctx.jsonl_path or "<jsonl>"),
1512
+ ))
1513
+
1514
+ # Codex round 9 P2 closure: §3.4 + §5.6 Path B5 + §3.7 family B note say
1515
+ # "Layer 3 verification is suspended for AUDIT_FAILED" — the orchestrator
1516
+ # short-circuits to BLOCK with failure_reason without running L3-2..L3-8
1517
+ # gates. A failed audit caused by bundle mutation legitimately has a
1518
+ # different live deliverable SHA from the recorded audit-time SHA;
1519
+ # treating that as a B2/B3 violation would reject the failure-signaling
1520
+ # artifact instead of letting the orchestrator surface failure_reason.
1521
+ # Same suspension extends to B4 git_sha, B5 timing arithmetic, B7
1522
+ # cross-file basename / path resolution: all are Layer 3 cross-file
1523
+ # verification rules that don't apply when the audit boundary itself
1524
+ # failed. B6 is suspended internally by its own AUDIT_FAILED branch
1525
+ # (spec §3.4 rule 6 + F-027); B1 has its own conditional handling.
1526
+ is_audit_failed = (ctx.verdict or {}).get("verdict_status") == "AUDIT_FAILED"
1527
+
1528
+ # Family B
1529
+ findings.extend(check_b1(ctx.sidecar, ctx.jsonl_events, ctx.verdict, location=sidecar_loc))
1530
+ if not is_audit_failed:
1531
+ findings.extend(check_b2(ctx.entry, ctx.sidecar, ctx.repo_root, location=entry_loc))
1532
+ findings.extend(check_b3(ctx.sidecar, ctx.repo_root, location=sidecar_loc))
1533
+ findings.extend(check_b4(ctx.sidecar, ctx.repo_root, location=sidecar_loc))
1534
+ findings.extend(check_b5(ctx.sidecar, location=sidecar_loc))
1535
+ findings.extend(check_b6(ctx.sidecar, ctx.verdict, location=sidecar_loc))
1536
+ if not is_audit_failed:
1537
+ findings.extend(check_b7(ctx.entry, ctx.sidecar, ctx.entry_path, ctx.sidecar_path,
1538
+ ctx.jsonl_path, ctx.verdict_path, mode, location=entry_loc,
1539
+ repo_root=ctx.repo_root, verdict=ctx.verdict))
1540
+ findings.extend(check_b8(ctx.entry, mode, location=entry_loc))
1541
+ findings.extend(check_b9(ctx.entry, ctx.sidecar, location=entry_loc))
1542
+ findings.extend(check_b10(ctx.entry, ctx.verdict, mode, location=entry_loc))
1543
+
1544
+ # Family C
1545
+ findings.extend(check_c1(ctx.entry, ctx.verdict, location=entry_loc))
1546
+ findings.extend(check_c2(ctx.entry, ctx.sidecar, location=entry_loc))
1547
+ # C3 needs prior_entry (passport scan) — skipped in single-entry mode unless
1548
+ # the caller passes a passport. For ack entries we'd compare against the latest
1549
+ # MATERIAL non-ack entry of the same (stage, agent, deliverable_sha).
1550
+ if mode == "persisted" and ctx.passport_audit_artifacts is not None and ctx.entry is not None:
1551
+ if "acknowledgement" in ctx.entry:
1552
+ prior = _find_latest_material_entry_for_ack(
1553
+ ctx.passport_audit_artifacts, ctx.entry)
1554
+ if prior is None:
1555
+ # Codex round 5 P1 closure: an ack entry without a prior
1556
+ # MATERIAL entry of the same (stage, agent, deliverable_sha,
1557
+ # run_id) tuple violates C3's copy contract by construction
1558
+ # — there is nothing to copy from. A standalone ack or one
1559
+ # whose copied fields were altered to break the tuple match
1560
+ # would silently pass C3 if `prior is None` skipped the
1561
+ # check. Surface as C3 finding.
1562
+ findings.append(LintError(
1563
+ "C3",
1564
+ f"acknowledgement entry has no prior MATERIAL entry to copy "
1565
+ f"from: no passport entry matches "
1566
+ f"(stage={ctx.entry.get('stage')!r}, "
1567
+ f"agent={ctx.entry.get('agent')!r}, "
1568
+ f"deliverable_sha={ctx.entry.get('deliverable_sha')!r}, "
1569
+ f"run_id={ctx.entry.get('run_id')!r}) — §5.4 ack mechanism "
1570
+ f"requires the prior MATERIAL entry to exist before append",
1571
+ entry_loc,
1572
+ ))
1573
+ else:
1574
+ findings.extend(check_c3(ctx.entry, prior, location=entry_loc))
1575
+ findings.extend(check_c4(ctx.entry, ctx.sidecar, location=entry_loc))
1576
+
1577
+ # Family D
1578
+ if ctx.passport_audit_artifacts is not None:
1579
+ findings.extend(check_d1(ctx.passport_audit_artifacts, location=str(ctx.passport_path or "<passport>")))
1580
+ findings.extend(check_d3(ctx.passport_audit_artifacts, location=str(ctx.passport_path or "<passport>")))
1581
+ findings.extend(check_e1_e2_e6(ctx.passport_audit_artifacts, location=str(ctx.passport_path or "<passport>")))
1582
+
1583
+ # Codex round 13 P2 closure: D2/D4 supersession is only meaningful in
1584
+ # persisted mode (proposal mode is itself the unmerged proposal). When
1585
+ # --mode persisted runs with --output-dir present, scan the dir for
1586
+ # OTHER unmerged proposal entries matching the same (stage, agent,
1587
+ # deliverable_sha) tuple, then run:
1588
+ # - D2: detect ambiguous proposal selection (proposals sharing
1589
+ # started_at where run_id lex-max would be the only tie-breaker)
1590
+ # - D4: detect supersession requirement — a higher-round unmerged
1591
+ # proposal preempts the persisted entry's Path A selection
1592
+ if mode == "persisted" and ctx.output_dir is not None and ctx.output_dir.is_dir() and ctx.entry is not None:
1593
+ proposals = _scan_unmerged_proposals(ctx.output_dir, ctx.entry,
1594
+ exclude_path=ctx.entry_path)
1595
+ if proposals:
1596
+ findings.extend(check_d2(proposals, location=str(ctx.output_dir)))
1597
+ persisted_round = _safe_get(ctx.entry, "verdict", "round")
1598
+ for p in proposals:
1599
+ proposal_round = _safe_get(p, "entry", "verdict", "round")
1600
+ findings.extend(check_d4(
1601
+ persisted_round if isinstance(persisted_round, int) else None,
1602
+ proposal_round if isinstance(proposal_round, int) else None,
1603
+ location=str(p.get("entry_path") or "<proposal-entry>"),
1604
+ ))
1605
+
1606
+ # Family E
1607
+ findings.extend(check_e3_e4(ctx.entry, mode, location=entry_loc))
1608
+ findings.extend(check_e5(ctx.entry, mode, location=entry_loc))
1609
+ findings.extend(check_e7(ctx.entry, location=entry_loc))
1610
+
1611
+ # Family F
1612
+ if ctx.entry is not None:
1613
+ findings.extend(check_f1(ctx.entry.get("run_id"), location=entry_loc))
1614
+ findings.extend(check_f2(ctx.jsonl_path, ctx.sidecar_path, ctx.verdict_path,
1615
+ ctx.entry_path, mode, location=entry_loc))
1616
+ findings.extend(check_f3(ctx.sidecar, ctx.sidecar_path, location=sidecar_loc))
1617
+
1618
+ return findings
1619
+
1620
+
1621
+ def _find_latest_material_entry_for_ack(
1622
+ artifacts: list[dict[str, Any]], ack_entry: dict[str, Any]
1623
+ ) -> dict[str, Any] | None:
1624
+ """Find the prior MATERIAL entry (no acknowledgement) matching the ack
1625
+ entry's (stage, agent, deliverable_sha, run_id) tuple, latest by verified_at."""
1626
+ key = (ack_entry.get("stage"), ack_entry.get("agent"),
1627
+ ack_entry.get("deliverable_sha"), ack_entry.get("run_id"))
1628
+ candidates = []
1629
+ for e in artifacts:
1630
+ if not isinstance(e, dict):
1631
+ continue
1632
+ if "acknowledgement" in e:
1633
+ continue
1634
+ if (e.get("stage"), e.get("agent"), e.get("deliverable_sha"),
1635
+ e.get("run_id")) != key:
1636
+ continue
1637
+ if _safe_get(e, "verdict", "status") != "MATERIAL":
1638
+ continue
1639
+ candidates.append(e)
1640
+ if not candidates:
1641
+ return None
1642
+ return max(candidates, key=lambda e: _safe_get(e, "verdict", "verified_at") or "")
1643
+
1644
+
1645
+ def _scan_unmerged_proposals(
1646
+ output_dir: Path, persisted_entry: dict[str, Any],
1647
+ exclude_path: Path | None = None,
1648
+ ) -> list[dict[str, Any]]:
1649
+ """Scan output_dir for unmerged proposal entries matching the persisted
1650
+ entry's (stage, agent, deliverable_sha) tuple.
1651
+
1652
+ `exclude_path` is the persisted entry's own path (when present in
1653
+ output_dir, e.g. fixture smoke tests where the persisted entry hasn't
1654
+ been moved to consumed/ yet) — exclude it so we don't compare an
1655
+ entry against itself.
1656
+
1657
+ Returns a list of dicts shaped {entry: <parsed entry>, sidecar: <parsed
1658
+ sidecar>, entry_path: <Path>} so D2/D4 callers can read both sides
1659
+ (entry has verdict.round; sidecar has timing.started_at).
1660
+ Errors loading any single proposal are silently skipped — D2/D4 are
1661
+ advisory checks; SCHEMA findings on those files would already fire
1662
+ via validate_against_schema if they were the target of --mode
1663
+ proposal in a separate invocation.
1664
+ """
1665
+ key = (persisted_entry.get("stage"), persisted_entry.get("agent"),
1666
+ persisted_entry.get("deliverable_sha"))
1667
+ proposals: list[dict[str, Any]] = []
1668
+ try:
1669
+ candidates = sorted(output_dir.glob("*.audit_artifact_entry.json"))
1670
+ except OSError:
1671
+ return proposals
1672
+ exclude_resolved = exclude_path.resolve() if exclude_path is not None else None
1673
+ for entry_path in candidates:
1674
+ if exclude_resolved is not None:
1675
+ try:
1676
+ if entry_path.resolve() == exclude_resolved:
1677
+ continue
1678
+ except OSError:
1679
+ pass
1680
+ try:
1681
+ entry_data = json.loads(entry_path.read_text(encoding="utf-8"))
1682
+ except (OSError, json.JSONDecodeError):
1683
+ continue
1684
+ if not isinstance(entry_data, dict):
1685
+ continue
1686
+ # Proposal arm: no verified_at / verified_by
1687
+ verdict = entry_data.get("verdict") or {}
1688
+ if "verified_at" in verdict or "verified_by" in verdict:
1689
+ continue
1690
+ if (entry_data.get("stage"), entry_data.get("agent"),
1691
+ entry_data.get("deliverable_sha")) != key:
1692
+ continue
1693
+ # Try to load companion sidecar (for D2 started_at)
1694
+ sidecar_path = entry_path.parent / entry_path.name.replace(
1695
+ ".audit_artifact_entry.json", ".meta.json")
1696
+ sidecar_data: dict[str, Any] = {}
1697
+ if sidecar_path.exists():
1698
+ try:
1699
+ sidecar_data = _load_yaml_or_json(sidecar_path)
1700
+ if not isinstance(sidecar_data, dict):
1701
+ sidecar_data = {}
1702
+ except Exception:
1703
+ sidecar_data = {}
1704
+ proposals.append({
1705
+ "entry": entry_data,
1706
+ "sidecar": sidecar_data,
1707
+ "entry_path": entry_path,
1708
+ })
1709
+ return proposals
1710
+
1711
+
1712
+ # ---------------------------------------------------------------------------
1713
+ # Schema validation (defense in depth — re-run schemas at lint time)
1714
+ # ---------------------------------------------------------------------------
1715
+
1716
+
1717
+ def validate_against_schema(doc: Any, schema_path: Path) -> list[LintError]:
1718
+ """Validate a document against a JSON Schema; return findings (one per error)."""
1719
+ try:
1720
+ from jsonschema import Draft202012Validator
1721
+ with schema_path.open("r", encoding="utf-8") as f:
1722
+ schema = json.load(f)
1723
+ validator = Draft202012Validator(
1724
+ schema, format_checker=Draft202012Validator.FORMAT_CHECKER
1725
+ )
1726
+ out: list[LintError] = []
1727
+ for err in validator.iter_errors(doc):
1728
+ out.append(LintError("SCHEMA",
1729
+ f"{err.message} (path={list(err.absolute_path)})",
1730
+ str(schema_path.name)))
1731
+ return out
1732
+ except Exception as e: # pragma: no cover
1733
+ return [LintError("SCHEMA", f"validator error: {e}", str(schema_path.name))]
1734
+
1735
+
1736
+ # ---------------------------------------------------------------------------
1737
+ # §3.7 F4 example-validation harness
1738
+ # ---------------------------------------------------------------------------
1739
+
1740
+
1741
+ def run_example_harness(repo_root: Path) -> list[LintError]:
1742
+ """F4 — walk docs/design/*.md, find code-fenced example payloads, validate
1743
+ against the appropriate schema. Any spec drift emerges as findings.
1744
+
1745
+ We look for fenced blocks marked ```yaml / ```json / ```jsonl. For each
1746
+ block, we apply heuristics: if it contains 'verdict_status:' it's a verdict
1747
+ file; 'audit_artifact:' is a Schema 9 list of entries; 'codex_cli_version:'
1748
+ is a sidecar; first event 'thread.started' is a JSONL stream.
1749
+ """
1750
+ findings: list[LintError] = []
1751
+ design_dir = repo_root / "docs/design"
1752
+ if not design_dir.exists():
1753
+ return [LintError("F4", f"docs/design not found at {design_dir}", str(design_dir))]
1754
+
1755
+ fence_re = re.compile(r"^```(\w+)?\s*$")
1756
+ for md_path in sorted(design_dir.glob("*.md")):
1757
+ try:
1758
+ text = md_path.read_text(encoding="utf-8")
1759
+ except OSError as e:
1760
+ findings.append(LintError("F4", f"cannot read: {e}", str(md_path)))
1761
+ continue
1762
+ lines = text.splitlines()
1763
+ in_fence = False
1764
+ fence_lang = None
1765
+ fence_start = 0
1766
+ buffer: list[str] = []
1767
+ for lineno, line in enumerate(lines, start=1):
1768
+ m = fence_re.match(line)
1769
+ if m:
1770
+ if not in_fence:
1771
+ in_fence = True
1772
+ fence_lang = (m.group(1) or "").lower()
1773
+ fence_start = lineno
1774
+ buffer = []
1775
+ else:
1776
+ block = "\n".join(buffer)
1777
+ findings.extend(_classify_and_validate_block(
1778
+ block, fence_lang, md_path, fence_start))
1779
+ in_fence = False
1780
+ fence_lang = None
1781
+ buffer = []
1782
+ elif in_fence:
1783
+ buffer.append(line)
1784
+ return findings
1785
+
1786
+
1787
+ def _classify_and_validate_block(
1788
+ block: str, lang: str | None, md_path: Path, fence_start: int
1789
+ ) -> list[LintError]:
1790
+ """Heuristically classify a fenced block and validate against the right schema."""
1791
+ out: list[LintError] = []
1792
+ location = f"{md_path}:{fence_start}"
1793
+ stripped = block.strip()
1794
+ if not stripped:
1795
+ return []
1796
+
1797
+ # JSONL detection — multiple lines each starting with `{"type":`
1798
+ looks_like_jsonl = (
1799
+ ("\n" in stripped) and all(
1800
+ s.strip().startswith("{") for s in stripped.splitlines() if s.strip()
1801
+ )
1802
+ )
1803
+ # heuristic guard: at least one line looks like a JSON object with a "type" field
1804
+ if looks_like_jsonl and ('"type":"thread.started"' in stripped or
1805
+ '"type":"turn.started"' in stripped or
1806
+ '"type":"item.completed"' in stripped):
1807
+ # Validate each line against jsonl schema
1808
+ try:
1809
+ with JSONL_SCHEMA_PATH.open("r", encoding="utf-8") as fh:
1810
+ jsonl_schema = json.load(fh)
1811
+ from jsonschema import Draft202012Validator
1812
+ validator = Draft202012Validator(
1813
+ jsonl_schema, format_checker=Draft202012Validator.FORMAT_CHECKER
1814
+ )
1815
+ for ln_idx, line in enumerate(stripped.splitlines(), start=fence_start + 1):
1816
+ line = line.strip()
1817
+ if not line:
1818
+ continue
1819
+ # Skip ellipsis-only schematic rows from the spec, e.g.
1820
+ # `..."input_tokens":...,...`
1821
+ if "..." in line:
1822
+ # Mark as "schematic, not validated" if it contains literal '...'
1823
+ out.append(LintError("F4",
1824
+ f"schematic JSONL line contains '...' placeholder (skipped from validation)",
1825
+ f"{md_path}:{ln_idx}",
1826
+ severity="info"))
1827
+ continue
1828
+ try:
1829
+ row = json.loads(line)
1830
+ except json.JSONDecodeError as e:
1831
+ out.append(LintError("F4",
1832
+ f"invalid JSON in fenced JSONL: {e}",
1833
+ f"{md_path}:{ln_idx}"))
1834
+ continue
1835
+ for err in validator.iter_errors(row):
1836
+ out.append(LintError("F4",
1837
+ f"jsonl row drift from schema: {err.message}",
1838
+ f"{md_path}:{ln_idx}"))
1839
+ except Exception as e: # pragma: no cover
1840
+ out.append(LintError("F4", f"jsonl harness error: {e}", location))
1841
+ return out
1842
+
1843
+ if lang == "yaml":
1844
+ # Cheap pre-filter: only parse blocks that LOOK like audit artifacts.
1845
+ # The harness's job is to surface drift in §3.1/§3.3/§3.4/§3.5
1846
+ # examples, not lint every YAML fence in every design doc.
1847
+ audit_signature_keys = (
1848
+ "audit_artifact:", "verdict_status:", "codex_cli_version:",
1849
+ "primary_deliverables:", "audit_template_path:",
1850
+ )
1851
+ if not any(sig in stripped for sig in audit_signature_keys):
1852
+ return out
1853
+ # parse as YAML — use timestamp-as-string loader so we don't double-flag
1854
+ # `2026-04-30T15:22:04.123Z` as a YAML datetime object (schema needs str).
1855
+ try:
1856
+ doc = yaml.load(stripped, Loader=_StrTimestampSafeLoader)
1857
+ except yaml.YAMLError as e:
1858
+ out.append(LintError("F4", f"yaml parse error: {e}", location))
1859
+ return out
1860
+ if not isinstance(doc, dict):
1861
+ return out
1862
+ # Classify
1863
+ if "verdict_status" in doc:
1864
+ # verdict file
1865
+ out.extend([_relabel(f, "F4") for f in
1866
+ validate_against_schema(doc, VERDICT_SCHEMA_PATH)])
1867
+ out[-len(out) or 0:] = [_with_location(f, location) for f in out[-len(out) or 0:]]
1868
+ elif "codex_cli_version" in doc:
1869
+ # sidecar — but note this section may have hyphen drift in timestamps
1870
+ out.extend([_with_location(_relabel(f, "F4"), location) for f in
1871
+ validate_against_schema(doc, SIDECAR_SCHEMA_PATH)])
1872
+ elif "audit_artifact" in doc:
1873
+ # Schema 9 list of persisted entries
1874
+ entries = doc.get("audit_artifact") or []
1875
+ if isinstance(entries, list):
1876
+ for idx, e in enumerate(entries):
1877
+ if isinstance(e, dict):
1878
+ loc = f"{location}:audit_artifact[{idx}]"
1879
+ out.extend([_with_location(_relabel(f, "F4"), loc) for f in
1880
+ validate_against_schema(e, ENTRY_SCHEMA_PATH)])
1881
+ elif {"stage", "agent", "deliverable_path", "run_id"} <= set(doc.keys()):
1882
+ # bare entry block
1883
+ out.extend([_with_location(_relabel(f, "F4"), location) for f in
1884
+ validate_against_schema(doc, ENTRY_SCHEMA_PATH)])
1885
+ return out
1886
+
1887
+ if lang == "json":
1888
+ try:
1889
+ doc = json.loads(stripped)
1890
+ except json.JSONDecodeError:
1891
+ return out
1892
+ if not isinstance(doc, dict):
1893
+ return out
1894
+ # Same classification
1895
+ if "verdict_status" in doc:
1896
+ out.extend([_with_location(_relabel(f, "F4"), location) for f in
1897
+ validate_against_schema(doc, VERDICT_SCHEMA_PATH)])
1898
+ elif "codex_cli_version" in doc:
1899
+ out.extend([_with_location(_relabel(f, "F4"), location) for f in
1900
+ validate_against_schema(doc, SIDECAR_SCHEMA_PATH)])
1901
+ elif {"stage", "agent", "deliverable_path", "run_id"} <= set(doc.keys()):
1902
+ out.extend([_with_location(_relabel(f, "F4"), location) for f in
1903
+ validate_against_schema(doc, ENTRY_SCHEMA_PATH)])
1904
+ return out
1905
+
1906
+ return out
1907
+
1908
+
1909
+ def _relabel(f: LintError, rule_id: str) -> LintError:
1910
+ return LintError(rule_id, f.message, f.location)
1911
+
1912
+
1913
+ def _with_location(f: LintError, location: str) -> LintError:
1914
+ return LintError(f.rule_id, f.message, location)
1915
+
1916
+
1917
+ # ---------------------------------------------------------------------------
1918
+ # CLI
1919
+ # ---------------------------------------------------------------------------
1920
+
1921
+
1922
+ class _ExUsageParser(argparse.ArgumentParser):
1923
+ """ArgumentParser subclass enforcing the EX_USAGE=64 contract.
1924
+
1925
+ Codex round 4 P3 closure: stock argparse calls sys.exit(2) on any
1926
+ parser-level failure (invalid --mode value, unknown option, missing
1927
+ required arg). The CLI contract documented at the top of this module
1928
+ promises 64 EX_USAGE for every bad-CLI-arg case. Override .error() so
1929
+ the exit code matches the contract.
1930
+ """
1931
+
1932
+ def error(self, message: str) -> None: # type: ignore[override]
1933
+ self.print_usage(sys.stderr)
1934
+ print(f"{self.prog}: error: {message}", file=sys.stderr)
1935
+ sys.exit(64)
1936
+
1937
+
1938
+ def _build_parser() -> argparse.ArgumentParser:
1939
+ p = _ExUsageParser(
1940
+ description="Lint audit-artifact contract per ARS v3.6.7 §3.7 invariants.")
1941
+ p.add_argument("--mode", choices=("proposal", "persisted", "jsonl-stream"))
1942
+ p.add_argument("--example-validation-harness", action="store_true",
1943
+ help="Walk docs/design/*.md and validate fenced example payloads.")
1944
+ p.add_argument("--entry", type=Path,
1945
+ help="Path to entry JSON (proposal or persisted).")
1946
+ p.add_argument("--sidecar", type=Path,
1947
+ help="Path to sidecar .meta.json. Auto-discovered via run_id when --output-dir given.")
1948
+ p.add_argument("--verdict", type=Path,
1949
+ help="Path to verdict .verdict.yaml. Auto-discovered.")
1950
+ p.add_argument("--jsonl", type=Path,
1951
+ help="Path to JSONL file. Auto-discovered, or explicitly required by jsonl-stream mode.")
1952
+ p.add_argument("--output-dir", type=Path,
1953
+ help="Directory containing the four artifact files (used for auto-discovery).")
1954
+ p.add_argument("--passport-path", type=Path,
1955
+ help="Passport YAML/JSON. Enables D1/D3/E2/E6 ledger checks.")
1956
+ p.add_argument("--run-id", type=str,
1957
+ help="Used with --output-dir for auto-discovery.")
1958
+ p.add_argument("--repo-root", type=Path, default=REPO_ROOT,
1959
+ help="Repo root for B2/B3/B4 disk verification.")
1960
+ return p
1961
+
1962
+
1963
+ def _autodiscover(output_dir: Path | None, run_id: str | None,
1964
+ jsonl: Path | None, sidecar: Path | None,
1965
+ verdict: Path | None, entry: Path | None,
1966
+ mode: str) -> tuple[Path | None, Path | None, Path | None, Path | None]:
1967
+ """Fill missing artifact paths from --output-dir + --run-id."""
1968
+ if output_dir is None:
1969
+ return jsonl, sidecar, verdict, entry
1970
+ if run_id is None and entry is not None and entry.exists():
1971
+ # peek run_id from entry
1972
+ try:
1973
+ data = json.loads(entry.read_text(encoding="utf-8"))
1974
+ run_id = data.get("run_id")
1975
+ except Exception:
1976
+ pass
1977
+ if run_id is None:
1978
+ return jsonl, sidecar, verdict, entry
1979
+ if jsonl is None:
1980
+ jsonl = output_dir / f"{run_id}.jsonl"
1981
+ if sidecar is None:
1982
+ sidecar = output_dir / f"{run_id}.meta.json"
1983
+ if verdict is None:
1984
+ verdict = output_dir / f"{run_id}.verdict.yaml"
1985
+ if entry is None:
1986
+ # Persisted mode entry files are conventionally moved to consumed/
1987
+ # at §4.9 step 9, but the CLI must still be able to read an entry
1988
+ # the caller has on disk. Default to the bare-run_id name; if not
1989
+ # found, fall back to consumed/<run_id>.audit_artifact_entry.json.
1990
+ candidate = output_dir / f"{run_id}.audit_artifact_entry.json"
1991
+ consumed = output_dir / "consumed" / f"{run_id}.audit_artifact_entry.json"
1992
+ if candidate.exists():
1993
+ entry = candidate
1994
+ elif consumed.exists():
1995
+ entry = consumed
1996
+ else:
1997
+ entry = candidate # report the missing path in the loader error
1998
+ return jsonl, sidecar, verdict, entry
1999
+
2000
+
2001
+ def main(argv: list[str] | None = None) -> int:
2002
+ parser = _build_parser()
2003
+ args = parser.parse_args(argv)
2004
+
2005
+ # Harness mode
2006
+ if args.example_validation_harness:
2007
+ repo_root = args.repo_root or REPO_ROOT
2008
+ findings = run_example_harness(repo_root)
2009
+ for f in findings:
2010
+ print(f.render())
2011
+ # Only error-severity findings drive a non-zero exit. Info findings
2012
+ # (e.g., schematic '...' placeholders the harness intentionally skips)
2013
+ # are surfaced but don't fail the run.
2014
+ return 1 if any(f.severity == "error" for f in findings) else 0
2015
+
2016
+ if args.mode is None:
2017
+ # Phase 6.1 EX_USAGE convention is exit 64. argparse's parser.error()
2018
+ # calls sys.exit(2), which would silently downgrade our usage errors;
2019
+ # print + return 64 keeps the exit-code contract aligned with
2020
+ # scripts/audit_snapshot.py and scripts/run_codex_audit.sh.
2021
+ print("ERROR: either --mode or --example-validation-harness is required",
2022
+ file=sys.stderr)
2023
+ return 64
2024
+
2025
+ # jsonl-stream mode — orchestrator §5.2 L2-5 invocation
2026
+ if args.mode == "jsonl-stream":
2027
+ if args.jsonl is None:
2028
+ print("ERROR: --mode jsonl-stream requires --jsonl", file=sys.stderr)
2029
+ return 64
2030
+ if not args.jsonl.exists():
2031
+ print(f"ERROR: jsonl not found: {args.jsonl}", file=sys.stderr)
2032
+ return 2
2033
+ try:
2034
+ events = _load_jsonl(args.jsonl)
2035
+ except ValueError as e:
2036
+ print(f"ERROR: {e}", file=sys.stderr)
2037
+ return 2
2038
+ ctx = LintContext(mode="jsonl-stream", jsonl_events=events,
2039
+ jsonl_path=args.jsonl, repo_root=args.repo_root or REPO_ROOT)
2040
+ findings = run_checks(ctx)
2041
+ # Per spec §5.2 L2-5 the orchestrator wants the offending item.id and
2042
+ # reason on stderr when pairing fails. We mirror to stdout too so the
2043
+ # CLI output is grep-friendly in interactive use.
2044
+ for f in findings:
2045
+ print(f.render())
2046
+ print(f.render(), file=sys.stderr)
2047
+ return 1 if any(f.severity == "error" for f in findings) else 0
2048
+
2049
+ # proposal / persisted modes
2050
+ jsonl, sidecar, verdict, entry = _autodiscover(
2051
+ args.output_dir, args.run_id, args.jsonl, args.sidecar,
2052
+ args.verdict, args.entry, args.mode,
2053
+ )
2054
+
2055
+ if entry is None:
2056
+ print(
2057
+ f"ERROR: --mode {args.mode} requires --entry (or --output-dir + --run-id)",
2058
+ file=sys.stderr,
2059
+ )
2060
+ return 64
2061
+
2062
+ try:
2063
+ entry_data = json.loads(entry.read_text(encoding="utf-8"))
2064
+ except (OSError, json.JSONDecodeError) as e:
2065
+ print(f"ERROR: cannot load entry {entry}: {e}", file=sys.stderr)
2066
+ return 2
2067
+
2068
+ # Codex round 5 traceback closure: a non-object entry payload (caller
2069
+ # passed `[]` / a scalar / null) would crash check_e3_e4 with
2070
+ # AttributeError on `entry.get(...)`. Surface as a SCHEMA finding
2071
+ # instead — schema validation below would also catch it, but bailing
2072
+ # early avoids the cross-field rules running on a non-dict and
2073
+ # producing tracebacks before the schema finding is rendered.
2074
+ if not isinstance(entry_data, dict):
2075
+ print(
2076
+ f"ERROR: entry {entry} is not a JSON object "
2077
+ f"(got {type(entry_data).__name__}); audit_artifact_entry must be "
2078
+ "an object per audit_artifact_entry.schema.json",
2079
+ file=sys.stderr,
2080
+ )
2081
+ return 2
2082
+
2083
+ # Schema validation on the entry first (defense in depth — the oneOf
2084
+ # proposal/persisted arms enforce verified_at presence + AUDIT_FAILED
2085
+ # exclusion at this layer, before any cross-field rule fires).
2086
+ schema_findings: list[LintError] = list(
2087
+ validate_against_schema(entry_data, ENTRY_SCHEMA_PATH)
2088
+ )
2089
+
2090
+ # Codex round 2 P1 closure: --mode persisted MUST enforce the persisted
2091
+ # oneOf arm specifically. JSON Schema oneOf accepts EITHER arm by
2092
+ # construction, so a proposal-shaped entry (no verified_at, no
2093
+ # verified_by, possibly AUDIT_FAILED status) would silently pass
2094
+ # `--mode persisted`. The mode flag is the lifecycle assertion the
2095
+ # caller is making about the entry; the lint must enforce it.
2096
+ # E3/E4 are the spec rules for proposal-mode rejection of orchestrator
2097
+ # fields; the symmetric rule for persisted mode lives here.
2098
+ verdict_block = entry_data.get("verdict") if isinstance(entry_data, dict) else None
2099
+ if isinstance(verdict_block, dict):
2100
+ if args.mode == "persisted":
2101
+ # persisted arm requires verified_at + verified_by, forbids AUDIT_FAILED
2102
+ if "verified_at" not in verdict_block:
2103
+ schema_findings.append(LintError(
2104
+ "E3", "--mode persisted: entry.verdict.verified_at is required "
2105
+ "(entry has proposal-arm shape; orchestrator-side fields missing)",
2106
+ str(entry)))
2107
+ if "verified_by" not in verdict_block:
2108
+ schema_findings.append(LintError(
2109
+ "E3", "--mode persisted: entry.verdict.verified_by is required "
2110
+ "(entry has proposal-arm shape; orchestrator-side fields missing)",
2111
+ str(entry)))
2112
+ if verdict_block.get("status") == "AUDIT_FAILED":
2113
+ schema_findings.append(LintError(
2114
+ "E5", "--mode persisted: entry.verdict.status='AUDIT_FAILED' is "
2115
+ "forbidden (AUDIT_FAILED entries are proposal-arm only per "
2116
+ "§3.2 lifecycle-conditional table)",
2117
+ str(entry)))
2118
+ elif args.mode == "proposal":
2119
+ # proposal arm forbids verified_at + verified_by + acknowledgement
2120
+ if "verified_at" in verdict_block:
2121
+ schema_findings.append(LintError(
2122
+ "E4", "--mode proposal: entry.verdict.verified_at must be absent "
2123
+ "(orchestrator-only field; wrapper-emitted proposal carrying it "
2124
+ "is Pattern C3 attack surface)",
2125
+ str(entry)))
2126
+ if "verified_by" in verdict_block:
2127
+ schema_findings.append(LintError(
2128
+ "E4", "--mode proposal: entry.verdict.verified_by must be absent "
2129
+ "(orchestrator-only field; Pattern C3 attack surface)",
2130
+ str(entry)))
2131
+ if "acknowledgement" in entry_data:
2132
+ schema_findings.append(LintError(
2133
+ "A4", "--mode proposal: entry.acknowledgement must be absent "
2134
+ "(orchestrator-only write per §5.4)",
2135
+ str(entry)))
2136
+
2137
+ # Companion artifacts (sidecar + verdict + jsonl) are REQUIRED in
2138
+ # Codex round 16 P2 closure: spec §5.2 says the orchestrator follows
2139
+ # entry.artifact_paths, so when caller only passes `--entry` (and a
2140
+ # `--repo-root`), fall back to those declared paths to locate the
2141
+ # JSONL/sidecar/verdict before emitting "missing companion" errors.
2142
+ # The B7 path-resolution check (round 8) still verifies these match
2143
+ # the recorded paths; this just removes the ergonomic gap that
2144
+ # required redundant flags for a passport/proposal entry that
2145
+ # already declares its own artifact bundle.
2146
+ artifact_paths = entry_data.get("artifact_paths") if isinstance(entry_data, dict) else None
2147
+ if isinstance(artifact_paths, dict):
2148
+ repo_root = args.repo_root or REPO_ROOT
2149
+ if jsonl is None:
2150
+ recorded = artifact_paths.get("jsonl")
2151
+ if isinstance(recorded, str) and recorded:
2152
+ jsonl = (repo_root / recorded).resolve()
2153
+ if sidecar is None:
2154
+ recorded = artifact_paths.get("sidecar")
2155
+ if isinstance(recorded, str) and recorded:
2156
+ sidecar = (repo_root / recorded).resolve()
2157
+ if verdict is None:
2158
+ recorded = artifact_paths.get("verdict")
2159
+ if isinstance(recorded, str) and recorded:
2160
+ verdict = (repo_root / recorded).resolve()
2161
+
2162
+ # proposal/persisted modes — they ARE the Layer 2/3 evidence Phase 6.3
2163
+ # is supposed to gate (codex round 1 P1: silently treating missing
2164
+ # files as None let valid-looking entries return exit 0 with no audit
2165
+ # evidence). When auto-discovered or explicitly given path doesn't
2166
+ # exist, surface as an artifact-incomplete finding (B7 family — the
2167
+ # bundle is supposed to be complete by §4.9 step 9).
2168
+ def _missing_companion(role: str, path: Path | None) -> None:
2169
+ if path is None:
2170
+ schema_findings.append(LintError(
2171
+ "B7",
2172
+ f"audit bundle missing {role} (auto-discover failed and no explicit "
2173
+ f"--{role} given) — proposal/persisted mode requires {role} for "
2174
+ f"Layer 2/3 verification",
2175
+ f"<{args.mode}>",
2176
+ ))
2177
+ elif not path.exists():
2178
+ schema_findings.append(LintError(
2179
+ "B7",
2180
+ f"audit bundle missing {role} at {path} — proposal/persisted mode "
2181
+ f"requires {role} for Layer 2/3 verification",
2182
+ str(path),
2183
+ ))
2184
+
2185
+ _missing_companion("sidecar", sidecar)
2186
+ _missing_companion("verdict", verdict)
2187
+ _missing_companion("jsonl", jsonl)
2188
+
2189
+ sidecar_data = None
2190
+ if sidecar is not None and sidecar.exists():
2191
+ try:
2192
+ sidecar_data = _load_yaml_or_json(sidecar)
2193
+ except Exception as e:
2194
+ print(f"ERROR: cannot load sidecar {sidecar}: {e}", file=sys.stderr)
2195
+ return 2
2196
+ # Codex round 6 P2 closure: a non-object companion artifact (e.g.,
2197
+ # `[]` parsed as YAML) records a SCHEMA finding via
2198
+ # validate_against_schema but `.get(...)` calls in cross-field
2199
+ # checks would still raise AttributeError before the finding
2200
+ # renders. Coerce non-dict to None so cross-field rules see "no
2201
+ # companion" and skip cleanly; the SCHEMA finding still surfaces
2202
+ # the rejection and exit code remains 1.
2203
+ schema_findings.extend(
2204
+ validate_against_schema(sidecar_data, SIDECAR_SCHEMA_PATH)
2205
+ )
2206
+ if not isinstance(sidecar_data, dict):
2207
+ sidecar_data = None
2208
+
2209
+ verdict_data = None
2210
+ if verdict is not None and verdict.exists():
2211
+ try:
2212
+ verdict_data = _load_yaml_or_json(verdict)
2213
+ except Exception as e:
2214
+ print(f"ERROR: cannot load verdict {verdict}: {e}", file=sys.stderr)
2215
+ return 2
2216
+ schema_findings.extend(
2217
+ validate_against_schema(verdict_data, VERDICT_SCHEMA_PATH)
2218
+ )
2219
+ if not isinstance(verdict_data, dict):
2220
+ verdict_data = None
2221
+
2222
+ events = None
2223
+ if jsonl is not None and jsonl.exists():
2224
+ # Codex round 11 P2 closure: AUDIT_FAILED bundles can have a
2225
+ # partial / non-JSON line at the end (codex was SIGKILL'd
2226
+ # mid-write); _load_jsonl() raises ValueError on those, returning
2227
+ # exit 2 as internal error and contradicting the round-9 schema
2228
+ # suspension immediately below. Decide on AUDIT_FAILED before
2229
+ # _load_jsonl runs so failure-signaling proposals with
2230
+ # partially-written streams are handled cleanly.
2231
+ verdict_status_for_jsonl = (
2232
+ verdict_data.get("verdict_status") if isinstance(verdict_data, dict) else None
2233
+ )
2234
+ if verdict_status_for_jsonl == "AUDIT_FAILED":
2235
+ # Skip per-row schema validation entirely; A7 / stream-shape
2236
+ # are also suspended in run_checks. Best-effort load still
2237
+ # populates events for the entry-side family A rules that
2238
+ # don't depend on stream completeness — but a hard parse
2239
+ # failure is acceptable evidence of "audit was killed mid-
2240
+ # write" rather than a lint-blocking error.
2241
+ try:
2242
+ events = _load_jsonl(jsonl)
2243
+ except ValueError:
2244
+ events = None # forensic-only bundle; stream is unparseable
2245
+ else:
2246
+ try:
2247
+ events = _load_jsonl(jsonl)
2248
+ except ValueError as e:
2249
+ print(f"ERROR: cannot load jsonl {jsonl}: {e}", file=sys.stderr)
2250
+ return 2
2251
+ # Validate every JSONL row against the per-row schema. Stream-shape
2252
+ # rules (A7 + parse_audit_verdict.py probe-style stream invariants)
2253
+ # run separately in the family A check; the per-row schema gate
2254
+ # ensures malformed rows don't reach those stream checks.
2255
+ for row_idx, row in enumerate(events, start=1):
2256
+ schema_findings.extend([
2257
+ LintError(err.rule_id, err.message,
2258
+ f"{jsonl}:row={row_idx}")
2259
+ for err in validate_against_schema(row, JSONL_SCHEMA_PATH)
2260
+ ])
2261
+
2262
+ passport_audit_artifacts = None
2263
+ if args.passport_path is not None and args.passport_path.exists():
2264
+ try:
2265
+ passport_data = _load_yaml_or_json(args.passport_path)
2266
+ if isinstance(passport_data, dict):
2267
+ aa = passport_data.get("audit_artifact")
2268
+ if isinstance(aa, list):
2269
+ passport_audit_artifacts = aa
2270
+ except Exception as e:
2271
+ print(f"ERROR: cannot load passport {args.passport_path}: {e}", file=sys.stderr)
2272
+ return 2
2273
+
2274
+ ctx = LintContext(
2275
+ mode=args.mode,
2276
+ entry=entry_data, entry_path=entry,
2277
+ sidecar=sidecar_data, sidecar_path=sidecar,
2278
+ verdict=verdict_data, verdict_path=verdict,
2279
+ jsonl_events=events, jsonl_path=jsonl,
2280
+ output_dir=args.output_dir,
2281
+ passport_audit_artifacts=passport_audit_artifacts,
2282
+ passport_path=args.passport_path,
2283
+ repo_root=args.repo_root or REPO_ROOT,
2284
+ )
2285
+
2286
+ # Codex round 12 P2 closure: when any schema-level finding fires, we
2287
+ # must NOT proceed into run_checks. Cross-field rules call .get() /
2288
+ # set() on nested fields assuming the schema-defined types — a value
2289
+ # that satisfies the top-level isinstance(dict) check (added round 5/6)
2290
+ # but has a malformed nested field (e.g. timing: [1] satisfies dict at
2291
+ # the root but timing.get(...) crashes; finding_ids: [{}] satisfies
2292
+ # array but `set(finding_ids)` crashes building a set of unhashable
2293
+ # dicts) would trace back instead of producing the documented lint
2294
+ # rejection. Short-circuit on any error-severity SCHEMA finding —
2295
+ # the schema rejection is itself the lint result; the cross-field
2296
+ # rules have nothing to add when the input doesn't match the schema
2297
+ # contract they presuppose.
2298
+ has_schema_error = any(
2299
+ f.rule_id == "SCHEMA" and f.severity == "error" for f in schema_findings
2300
+ )
2301
+ if has_schema_error:
2302
+ for f in schema_findings:
2303
+ print(f.render())
2304
+ return 1
2305
+
2306
+ findings = schema_findings + run_checks(ctx)
2307
+ for f in findings:
2308
+ print(f.render())
2309
+ return 1 if any(f.severity == "error" for f in findings) else 0
2310
+
2311
+
2312
+ if __name__ == "__main__":
2313
+ sys.exit(main())