psyclaw 0.27.23 → 0.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1069) hide show
  1. package/README.md +11 -5
  2. package/agents/recommended/catalog.json +99 -0
  3. package/dist/apps/panel/index.html +5 -5
  4. package/dist/src/adapters/pi/extension.js +339 -94
  5. package/dist/src/adapters/pi/extension.js.map +1 -1
  6. package/dist/src/adapters/pi/rpc.d.ts +2 -0
  7. package/dist/src/adapters/pi/rpc.js +9 -2
  8. package/dist/src/adapters/pi/rpc.js.map +1 -1
  9. package/dist/src/agents/import.js +44 -32
  10. package/dist/src/agents/import.js.map +1 -1
  11. package/dist/src/agents/recommended-personas.d.ts +42 -0
  12. package/dist/src/agents/recommended-personas.js +135 -0
  13. package/dist/src/agents/recommended-personas.js.map +1 -0
  14. package/dist/src/analysis/hooks.d.ts +1 -0
  15. package/dist/src/analysis/hooks.js +20 -1
  16. package/dist/src/analysis/hooks.js.map +1 -1
  17. package/dist/src/ars/bridge.d.ts +20 -0
  18. package/dist/src/ars/bridge.js +132 -0
  19. package/dist/src/ars/bridge.js.map +1 -0
  20. package/dist/src/ars/contracts.d.ts +55 -0
  21. package/dist/src/ars/contracts.js +2 -0
  22. package/dist/src/ars/contracts.js.map +1 -0
  23. package/dist/src/ars/panel-plan.d.ts +4 -0
  24. package/dist/src/ars/panel-plan.js +11 -0
  25. package/dist/src/ars/panel-plan.js.map +1 -0
  26. package/dist/src/ars/pi-panel-executor.d.ts +30 -0
  27. package/dist/src/ars/pi-panel-executor.js +140 -0
  28. package/dist/src/ars/pi-panel-executor.js.map +1 -0
  29. package/dist/src/ars/profile.d.ts +36 -0
  30. package/dist/src/ars/profile.js +119 -0
  31. package/dist/src/ars/profile.js.map +1 -0
  32. package/dist/src/ars/re-review.d.ts +22 -0
  33. package/dist/src/ars/re-review.js +172 -0
  34. package/dist/src/ars/re-review.js.map +1 -0
  35. package/dist/src/branding.d.ts +2 -4
  36. package/dist/src/branding.js +3 -5
  37. package/dist/src/branding.js.map +1 -1
  38. package/dist/src/bundled-tools.d.ts +3 -0
  39. package/dist/src/bundled-tools.js +21 -0
  40. package/dist/src/bundled-tools.js.map +1 -0
  41. package/dist/src/chat.js +6 -5
  42. package/dist/src/chat.js.map +1 -1
  43. package/dist/src/cli.js +6 -3
  44. package/dist/src/cli.js.map +1 -1
  45. package/dist/src/creation/contracts.d.ts +41 -0
  46. package/dist/src/creation/contracts.js +2 -0
  47. package/dist/src/creation/contracts.js.map +1 -0
  48. package/dist/src/creation/service.d.ts +7 -0
  49. package/dist/src/creation/service.js +208 -0
  50. package/dist/src/creation/service.js.map +1 -0
  51. package/dist/src/index.d.ts +11 -0
  52. package/dist/src/index.js +11 -0
  53. package/dist/src/index.js.map +1 -1
  54. package/dist/src/install/installer.js +32 -10
  55. package/dist/src/install/installer.js.map +1 -1
  56. package/dist/src/orchestration/bundled-personas.d.ts +16 -0
  57. package/dist/src/orchestration/bundled-personas.js +101 -0
  58. package/dist/src/orchestration/bundled-personas.js.map +1 -0
  59. package/dist/src/orchestration/effects.d.ts +8 -0
  60. package/dist/src/orchestration/effects.js +39 -0
  61. package/dist/src/orchestration/effects.js.map +1 -0
  62. package/dist/src/orchestration/personas.d.ts +16 -0
  63. package/dist/src/orchestration/personas.js +64 -0
  64. package/dist/src/orchestration/personas.js.map +1 -0
  65. package/dist/src/orchestration/pi-executor.d.ts +9 -4
  66. package/dist/src/orchestration/pi-executor.js +55 -22
  67. package/dist/src/orchestration/pi-executor.js.map +1 -1
  68. package/dist/src/orchestration/research-agents.d.ts +3 -7
  69. package/dist/src/orchestration/research-agents.js +5 -69
  70. package/dist/src/orchestration/research-agents.js.map +1 -1
  71. package/dist/src/panel/server.js +19 -4
  72. package/dist/src/panel/server.js.map +1 -1
  73. package/dist/src/project/paths.d.ts +3 -0
  74. package/dist/src/project/paths.js +7 -0
  75. package/dist/src/project/paths.js.map +1 -1
  76. package/dist/src/rules/user-rules.d.ts +8 -0
  77. package/dist/src/rules/user-rules.js +36 -0
  78. package/dist/src/rules/user-rules.js.map +1 -0
  79. package/dist/src/skills/contracts.d.ts +4 -4
  80. package/dist/src/skills/recommended.js +1 -1
  81. package/dist/src/skills/registry.js +49 -28
  82. package/dist/src/skills/registry.js.map +1 -1
  83. package/dist/src/style/cli-ui.d.ts +14 -0
  84. package/dist/src/style/cli-ui.js +45 -1
  85. package/dist/src/style/cli-ui.js.map +1 -1
  86. package/dist/src/tui/skill-manager.js +1 -1
  87. package/dist/src/tui/skill-manager.js.map +1 -1
  88. package/dist/src/updates/update.js +9 -3
  89. package/dist/src/updates/update.js.map +1 -1
  90. package/package.json +15 -3
  91. package/scripts/rebrand-pi.mjs +6 -0
  92. package/skills/recommended/catalog.json +2 -11
  93. package/vendor/ars/.claude/CLAUDE.md +371 -0
  94. package/vendor/ars/.command-invariants.toml +24 -0
  95. package/vendor/ars/CITATION.cff +35 -0
  96. package/vendor/ars/LICENSE +417 -0
  97. package/vendor/ars/MODE_REGISTRY.md +76 -0
  98. package/vendor/ars/NOTICE.md +26 -0
  99. package/vendor/ars/POSITIONING.md +99 -0
  100. package/vendor/ars/PSYCLAW_SOURCE.json +10 -0
  101. package/vendor/ars/README.md +751 -0
  102. package/vendor/ars/SECURITY.md +52 -0
  103. package/vendor/ars/THIRD_PARTY.md +70 -0
  104. package/vendor/ars/academic-paper/SKILL.md +542 -0
  105. package/vendor/ars/academic-paper/agents/abstract_bilingual_agent.md +171 -0
  106. package/vendor/ars/academic-paper/agents/argument_builder_agent.md +276 -0
  107. package/vendor/ars/academic-paper/agents/citation_compliance_agent.md +422 -0
  108. package/vendor/ars/academic-paper/agents/draft_writer_agent.md +656 -0
  109. package/vendor/ars/academic-paper/agents/formatter_agent.md +999 -0
  110. package/vendor/ars/academic-paper/agents/intake_agent.md +393 -0
  111. package/vendor/ars/academic-paper/agents/literature_strategist_agent.md +626 -0
  112. package/vendor/ars/academic-paper/agents/peer_reviewer_agent.md +516 -0
  113. package/vendor/ars/academic-paper/agents/revision_coach_agent.md +334 -0
  114. package/vendor/ars/academic-paper/agents/socratic_mentor_agent.md +527 -0
  115. package/vendor/ars/academic-paper/agents/structure_architect_agent.md +401 -0
  116. package/vendor/ars/academic-paper/agents/visualization_agent.md +441 -0
  117. package/vendor/ars/academic-paper/examples/chinese_paper_example.md +278 -0
  118. package/vendor/ars/academic-paper/examples/clinical_citation_verification_checklist.md +95 -0
  119. package/vendor/ars/academic-paper/examples/clinical_epistemic_status_example.md +100 -0
  120. package/vendor/ars/academic-paper/examples/commitment_ledger_example.md +147 -0
  121. package/vendor/ars/academic-paper/examples/imrad_hei_example.md +234 -0
  122. package/vendor/ars/academic-paper/examples/literature_review_example.md +260 -0
  123. package/vendor/ars/academic-paper/examples/plan_mode_guided_writing.md +600 -0
  124. package/vendor/ars/academic-paper/examples/revision_mode_example.md +344 -0
  125. package/vendor/ars/academic-paper/examples/revision_recovery_example.md +506 -0
  126. package/vendor/ars/academic-paper/examples/version_family_reconciliation_example.md +89 -0
  127. package/vendor/ars/academic-paper/references/abstract_writing_guide.md +169 -0
  128. package/vendor/ars/academic-paper/references/academic_writing_style.md +188 -0
  129. package/vendor/ars/academic-paper/references/anti_leakage_protocol.md +83 -0
  130. package/vendor/ars/academic-paper/references/apa7_chinese_citation_guide.md +364 -0
  131. package/vendor/ars/academic-paper/references/apa7_extended_guide.md +198 -0
  132. package/vendor/ars/academic-paper/references/changelog.md +11 -0
  133. package/vendor/ars/academic-paper/references/citation_format_switcher.md +228 -0
  134. package/vendor/ars/academic-paper/references/committee_correspondence_protocol.md +158 -0
  135. package/vendor/ars/academic-paper/references/credit_authorship_guide.md +308 -0
  136. package/vendor/ars/academic-paper/references/disclosure_mode_protocol.md +478 -0
  137. package/vendor/ars/academic-paper/references/domain_evidence_profiles.md +38 -0
  138. package/vendor/ars/academic-paper/references/failure_paths.md +349 -0
  139. package/vendor/ars/academic-paper/references/funding_statement_guide.md +319 -0
  140. package/vendor/ars/academic-paper/references/hei_domain_glossary.md +169 -0
  141. package/vendor/ars/academic-paper/references/intro_title_rhetoric_guide.md +114 -0
  142. package/vendor/ars/academic-paper/references/journal_submission_guide.md +249 -0
  143. package/vendor/ars/academic-paper/references/latex_template_reference.md +378 -0
  144. package/vendor/ars/academic-paper/references/mode_selection_guide.md +378 -0
  145. package/vendor/ars/academic-paper/references/paper_structure_patterns.md +330 -0
  146. package/vendor/ars/academic-paper/references/plan_mode_protocol.md +112 -0
  147. package/vendor/ars/academic-paper/references/policy_anchor_disclosure_protocol.md +200 -0
  148. package/vendor/ars/academic-paper/references/policy_anchor_table.md +157 -0
  149. package/vendor/ars/academic-paper/references/revision_patch_protocol.md +173 -0
  150. package/vendor/ars/academic-paper/references/statistical_visualization_standards.md +750 -0
  151. package/vendor/ars/academic-paper/references/venue_disclosure_policies.md +259 -0
  152. package/vendor/ars/academic-paper/references/vlm_figure_verification.md +126 -0
  153. package/vendor/ars/academic-paper/references/workflow_phase_details.md +135 -0
  154. package/vendor/ars/academic-paper/references/writing_judgment_framework.md +59 -0
  155. package/vendor/ars/academic-paper/references/writing_quality_check.md +173 -0
  156. package/vendor/ars/academic-paper/templates/bilingual_abstract_template.md +78 -0
  157. package/vendor/ars/academic-paper/templates/case_study_template.md +129 -0
  158. package/vendor/ars/academic-paper/templates/conference_paper_template.md +108 -0
  159. package/vendor/ars/academic-paper/templates/credit_statement_template.md +132 -0
  160. package/vendor/ars/academic-paper/templates/funding_statement_template.md +290 -0
  161. package/vendor/ars/academic-paper/templates/imrad_template.md +183 -0
  162. package/vendor/ars/academic-paper/templates/latex_article_template.tex +199 -0
  163. package/vendor/ars/academic-paper/templates/literature_review_template.md +135 -0
  164. package/vendor/ars/academic-paper/templates/policy_brief_template.md +139 -0
  165. package/vendor/ars/academic-paper/templates/revision_tracking_template.md +199 -0
  166. package/vendor/ars/academic-paper/templates/theoretical_paper_template.md +119 -0
  167. package/vendor/ars/academic-paper-reviewer/SKILL.md +491 -0
  168. package/vendor/ars/academic-paper-reviewer/agents/devils_advocate_reviewer_agent.md +443 -0
  169. package/vendor/ars/academic-paper-reviewer/agents/domain_reviewer_agent.md +412 -0
  170. package/vendor/ars/academic-paper-reviewer/agents/editorial_synthesizer_agent.md +478 -0
  171. package/vendor/ars/academic-paper-reviewer/agents/eic_agent.md +339 -0
  172. package/vendor/ars/academic-paper-reviewer/agents/field_analyst_agent.md +221 -0
  173. package/vendor/ars/academic-paper-reviewer/agents/methodology_reviewer_agent.md +449 -0
  174. package/vendor/ars/academic-paper-reviewer/agents/perspective_reviewer_agent.md +427 -0
  175. package/vendor/ars/academic-paper-reviewer/examples/hei_paper_review_example.md +391 -0
  176. package/vendor/ars/academic-paper-reviewer/examples/interdisciplinary_review_example.md +299 -0
  177. package/vendor/ars/academic-paper-reviewer/examples/subclaim_decomposition_example.md +80 -0
  178. package/vendor/ars/academic-paper-reviewer/references/calibration_mode_protocol.md +256 -0
  179. package/vendor/ars/academic-paper-reviewer/references/changelog.md +10 -0
  180. package/vendor/ars/academic-paper-reviewer/references/editorial_decision_standards.md +236 -0
  181. package/vendor/ars/academic-paper-reviewer/references/guided_mode_protocol.md +34 -0
  182. package/vendor/ars/academic-paper-reviewer/references/integration_guide.md +15 -0
  183. package/vendor/ars/academic-paper-reviewer/references/quality_rubrics.md +84 -0
  184. package/vendor/ars/academic-paper-reviewer/references/re_review_mode_protocol.md +340 -0
  185. package/vendor/ars/academic-paper-reviewer/references/review_criteria_framework.md +98 -0
  186. package/vendor/ars/academic-paper-reviewer/references/review_panel_provenance_protocol.md +197 -0
  187. package/vendor/ars/academic-paper-reviewer/references/review_quality_thinking.md +58 -0
  188. package/vendor/ars/academic-paper-reviewer/references/reviewer_sprint_prompt_source.md +324 -0
  189. package/vendor/ars/academic-paper-reviewer/references/sprint_contract_protocol.md +296 -0
  190. package/vendor/ars/academic-paper-reviewer/references/statistical_reporting_standards.md +505 -0
  191. package/vendor/ars/academic-paper-reviewer/references/top_journals_by_field.md +206 -0
  192. package/vendor/ars/academic-paper-reviewer/templates/editorial_decision_template.md +235 -0
  193. package/vendor/ars/academic-paper-reviewer/templates/peer_review_report_template.md +305 -0
  194. package/vendor/ars/academic-paper-reviewer/templates/revision_response_template.md +248 -0
  195. package/vendor/ars/academic-pipeline/SKILL.md +736 -0
  196. package/vendor/ars/academic-pipeline/agents/claim_ref_alignment_audit_agent.md +382 -0
  197. package/vendor/ars/academic-pipeline/agents/collaboration_depth_agent.md +164 -0
  198. package/vendor/ars/academic-pipeline/agents/integrity_verification_agent.md +870 -0
  199. package/vendor/ars/academic-pipeline/agents/pipeline_orchestrator_agent.md +1379 -0
  200. package/vendor/ars/academic-pipeline/agents/state_tracker_agent.md +622 -0
  201. package/vendor/ars/academic-pipeline/examples/full_pipeline_example.md +482 -0
  202. package/vendor/ars/academic-pipeline/examples/integrity_failure_recovery.md +389 -0
  203. package/vendor/ars/academic-pipeline/examples/mid_entry_example.md +414 -0
  204. package/vendor/ars/academic-pipeline/references/adapters/.gitkeep +0 -0
  205. package/vendor/ars/academic-pipeline/references/adapters/overview.md +153 -0
  206. package/vendor/ars/academic-pipeline/references/ai_research_failure_modes.md +185 -0
  207. package/vendor/ars/academic-pipeline/references/changelog.md +14 -0
  208. package/vendor/ars/academic-pipeline/references/claim_audit_calibration_protocol.md +175 -0
  209. package/vendor/ars/academic-pipeline/references/claim_verification_protocol.md +282 -0
  210. package/vendor/ars/academic-pipeline/references/external_review_protocol.md +131 -0
  211. package/vendor/ars/academic-pipeline/references/integrity_review_protocol.md +110 -0
  212. package/vendor/ars/academic-pipeline/references/literature_corpus_consumers.md +193 -0
  213. package/vendor/ars/academic-pipeline/references/mode_advisor.md +135 -0
  214. package/vendor/ars/academic-pipeline/references/passport_as_reset_boundary.md +132 -0
  215. package/vendor/ars/academic-pipeline/references/pipeline_state_machine.md +405 -0
  216. package/vendor/ars/academic-pipeline/references/plagiarism_detection_protocol.md +239 -0
  217. package/vendor/ars/academic-pipeline/references/process_summary_protocol.md +209 -0
  218. package/vendor/ars/academic-pipeline/references/progress_dashboard_template.md +38 -0
  219. package/vendor/ars/academic-pipeline/references/reinforcement_content.md +15 -0
  220. package/vendor/ars/academic-pipeline/references/reproducibility_audit.md +55 -0
  221. package/vendor/ars/academic-pipeline/references/score_trajectory_protocol.md +78 -0
  222. package/vendor/ars/academic-pipeline/references/team_collaboration_protocol.md +261 -0
  223. package/vendor/ars/academic-pipeline/references/two_stage_review_protocol.md +27 -0
  224. package/vendor/ars/academic-pipeline/templates/pipeline_status_template.md +146 -0
  225. package/vendor/ars/agents/report_compiler_agent.md +341 -0
  226. package/vendor/ars/agents/research_architect_agent.md +298 -0
  227. package/vendor/ars/agents/synthesis_agent.md +356 -0
  228. package/vendor/ars/commands/ars-3w.md +10 -0
  229. package/vendor/ars/commands/ars-abstract.md +10 -0
  230. package/vendor/ars/commands/ars-cache-invalidate.md +20 -0
  231. package/vendor/ars/commands/ars-citation-check.md +10 -0
  232. package/vendor/ars/commands/ars-disclosure.md +10 -0
  233. package/vendor/ars/commands/ars-format-convert.md +10 -0
  234. package/vendor/ars/commands/ars-full.md +9 -0
  235. package/vendor/ars/commands/ars-lit-review.md +12 -0
  236. package/vendor/ars/commands/ars-mark-read.md +18 -0
  237. package/vendor/ars/commands/ars-outline.md +10 -0
  238. package/vendor/ars/commands/ars-plan.md +10 -0
  239. package/vendor/ars/commands/ars-rebuttal-audit.md +12 -0
  240. package/vendor/ars/commands/ars-reviewer.md +9 -0
  241. package/vendor/ars/commands/ars-revision-coach.md +9 -0
  242. package/vendor/ars/commands/ars-revision.md +10 -0
  243. package/vendor/ars/commands/ars-unmark-read.md +16 -0
  244. package/vendor/ars/deep-research/SKILL.md +600 -0
  245. package/vendor/ars/deep-research/agents/bibliography_agent.md +473 -0
  246. package/vendor/ars/deep-research/agents/devils_advocate_agent.md +192 -0
  247. package/vendor/ars/deep-research/agents/editor_in_chief_agent.md +167 -0
  248. package/vendor/ars/deep-research/agents/ethics_review_agent.md +267 -0
  249. package/vendor/ars/deep-research/agents/meta_analysis_agent.md +325 -0
  250. package/vendor/ars/deep-research/agents/monitoring_agent.md +209 -0
  251. package/vendor/ars/deep-research/agents/report_compiler_agent.md +341 -0
  252. package/vendor/ars/deep-research/agents/research_architect_agent.md +298 -0
  253. package/vendor/ars/deep-research/agents/research_question_agent.md +216 -0
  254. package/vendor/ars/deep-research/agents/risk_of_bias_agent.md +231 -0
  255. package/vendor/ars/deep-research/agents/socratic_mentor_agent.md +764 -0
  256. package/vendor/ars/deep-research/agents/source_verification_agent.md +219 -0
  257. package/vendor/ars/deep-research/agents/synthesis_agent.md +356 -0
  258. package/vendor/ars/deep-research/agents/timeline_extraction_agent.md +99 -0
  259. package/vendor/ars/deep-research/examples/exploratory_research.md +157 -0
  260. package/vendor/ars/deep-research/examples/fact_check_mode.md +173 -0
  261. package/vendor/ars/deep-research/examples/handoff_to_paper.md +318 -0
  262. package/vendor/ars/deep-research/examples/idea_diversity_coverage_gap_advisory.md +64 -0
  263. package/vendor/ars/deep-research/examples/policy_analysis.md +161 -0
  264. package/vendor/ars/deep-research/examples/review_mode.md +253 -0
  265. package/vendor/ars/deep-research/examples/socratic_guided_research.md +331 -0
  266. package/vendor/ars/deep-research/examples/systematic_review.md +133 -0
  267. package/vendor/ars/deep-research/references/apa7_style_guide.md +162 -0
  268. package/vendor/ars/deep-research/references/argumentation_reasoning_framework.md +68 -0
  269. package/vendor/ars/deep-research/references/arxiv_api_protocol.md +76 -0
  270. package/vendor/ars/deep-research/references/changelog.md +22 -0
  271. package/vendor/ars/deep-research/references/chinese_literature_api_protocol.md +317 -0
  272. package/vendor/ars/deep-research/references/cross_agent_quality_definitions.md +14 -0
  273. package/vendor/ars/deep-research/references/crossref_api_protocol.md +84 -0
  274. package/vendor/ars/deep-research/references/equator_reporting_guidelines.md +482 -0
  275. package/vendor/ars/deep-research/references/ethics_checklist.md +282 -0
  276. package/vendor/ars/deep-research/references/failure_paths.md +355 -0
  277. package/vendor/ars/deep-research/references/interdisciplinary_bridges.md +292 -0
  278. package/vendor/ars/deep-research/references/irb_decision_tree.md +315 -0
  279. package/vendor/ars/deep-research/references/literature_monitoring_strategies.md +263 -0
  280. package/vendor/ars/deep-research/references/logical_fallacies.md +192 -0
  281. package/vendor/ars/deep-research/references/methodology_patterns.md +462 -0
  282. package/vendor/ars/deep-research/references/mode_selection_guide.md +331 -0
  283. package/vendor/ars/deep-research/references/openalex_api_protocol.md +82 -0
  284. package/vendor/ars/deep-research/references/preregistration_guide.md +324 -0
  285. package/vendor/ars/deep-research/references/semantic_scholar_api_protocol.md +107 -0
  286. package/vendor/ars/deep-research/references/socratic_mode_protocol.md +99 -0
  287. package/vendor/ars/deep-research/references/socratic_questioning_framework.md +232 -0
  288. package/vendor/ars/deep-research/references/source_quality_hierarchy.md +188 -0
  289. package/vendor/ars/deep-research/references/systematic_review_protocol.md +95 -0
  290. package/vendor/ars/deep-research/references/systematic_review_toolkit.md +353 -0
  291. package/vendor/ars/deep-research/templates/evidence_assessment_template.md +127 -0
  292. package/vendor/ars/deep-research/templates/literature_matrix_template.md +85 -0
  293. package/vendor/ars/deep-research/templates/preregistration_template.md +318 -0
  294. package/vendor/ars/deep-research/templates/prisma_protocol_template.md +248 -0
  295. package/vendor/ars/deep-research/templates/prisma_report_template.md +415 -0
  296. package/vendor/ars/deep-research/templates/research_brief_template.md +93 -0
  297. package/vendor/ars/package.json +24 -0
  298. package/vendor/ars/pi/README.md +161 -0
  299. package/vendor/ars/pi/package.json +26 -0
  300. package/vendor/ars/pi/wrapper.js +193 -0
  301. package/vendor/ars/pi/wrapper.test.mjs +201 -0
  302. package/vendor/ars/pyproject.toml +2 -0
  303. package/vendor/ars/requirements-pdf-content-classifier.txt +5 -0
  304. package/vendor/ars/scripts/_block_parser.py +396 -0
  305. package/vendor/ars/scripts/_ci_pytest_manifest.toml +661 -0
  306. package/vendor/ars/scripts/_claim_audit_constants.py +268 -0
  307. package/vendor/ars/scripts/_e4_evidence.py +110 -0
  308. package/vendor/ars/scripts/_eval_threshold_gate.py +73 -0
  309. package/vendor/ars/scripts/_markdown_lint_util.py +224 -0
  310. package/vendor/ars/scripts/_next_verified_at_ms.py +176 -0
  311. package/vendor/ars/scripts/_passport_yaml.py +53 -0
  312. package/vendor/ars/scripts/_skill_lint.py +254 -0
  313. package/vendor/ars/scripts/_text_similarity.py +141 -0
  314. package/vendor/ars/scripts/adapters/README.md +89 -0
  315. package/vendor/ars/scripts/adapters/_common.py +209 -0
  316. package/vendor/ars/scripts/adapters/examples/folder_scan/expected_passport.yaml +25 -0
  317. package/vendor/ars/scripts/adapters/examples/folder_scan/expected_rejection_log.yaml +18 -0
  318. package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/Chen2024_AIAssessment.pdf +0 -0
  319. package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/Wang_2023_formative_feedback.pdf +0 -0
  320. package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/paper1.pdf +0 -0
  321. package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture//344/270/255/346/226/207/346/252/224/345/220/215_2024.pdf +0 -0
  322. package/vendor/ars/scripts/adapters/examples/obsidian/expected_passport.yaml +40 -0
  323. package/vendor/ars/scripts/adapters/examples/obsidian/expected_rejection_log.yaml +13 -0
  324. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/.gitkeep +0 -0
  325. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/.obsidian/app.json +1 -0
  326. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/_templates/tmpl.md +7 -0
  327. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/chen2024ai.md +13 -0
  328. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/invalid.md +1 -0
  329. package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/wang2023formative.md +10 -0
  330. package/vendor/ars/scripts/adapters/examples/zotero/expected_passport.yaml +34 -0
  331. package/vendor/ars/scripts/adapters/examples/zotero/expected_rejection_log.yaml +34 -0
  332. package/vendor/ars/scripts/adapters/examples/zotero/input_fixture/.gitkeep +0 -0
  333. package/vendor/ars/scripts/adapters/examples/zotero/input_fixture/export.json +43 -0
  334. package/vendor/ars/scripts/adapters/folder_scan.py +214 -0
  335. package/vendor/ars/scripts/adapters/obsidian.py +336 -0
  336. package/vendor/ars/scripts/adapters/tests/.gitkeep +0 -0
  337. package/vendor/ars/scripts/adapters/tests/conftest.py +77 -0
  338. package/vendor/ars/scripts/adapters/tests/test_check_corpus_consumer_protocol.py +632 -0
  339. package/vendor/ars/scripts/adapters/tests/test_check_literature_corpus_schema.py +631 -0
  340. package/vendor/ars/scripts/adapters/tests/test_common.py +365 -0
  341. package/vendor/ars/scripts/adapters/tests/test_conftest.py +96 -0
  342. package/vendor/ars/scripts/adapters/tests/test_folder_scan.py +260 -0
  343. package/vendor/ars/scripts/adapters/tests/test_literature_corpus_entry_schema.py +745 -0
  344. package/vendor/ars/scripts/adapters/tests/test_obsidian.py +357 -0
  345. package/vendor/ars/scripts/adapters/tests/test_rejection_log_schema.py +271 -0
  346. package/vendor/ars/scripts/adapters/tests/test_sync_adapter_docs.py +88 -0
  347. package/vendor/ars/scripts/adapters/tests/test_zotero.py +454 -0
  348. package/vendor/ars/scripts/adapters/zotero.py +318 -0
  349. package/vendor/ars/scripts/adjudication_activity.py +1592 -0
  350. package/vendor/ars/scripts/announce-ars-loaded.sh +144 -0
  351. package/vendor/ars/scripts/ars_anchorize_draft.py +170 -0
  352. package/vendor/ars/scripts/ars_apply_revision_patch.py +912 -0
  353. package/vendor/ars/scripts/ars_cache_invalidate.py +40 -0
  354. package/vendor/ars/scripts/ars_mark_read.py +521 -0
  355. package/vendor/ars/scripts/ars_phase_scope_manifest.json +33 -0
  356. package/vendor/ars/scripts/ars_update_check.sh +215 -0
  357. package/vendor/ars/scripts/ars_write_scope_guard.py +506 -0
  358. package/vendor/ars/scripts/arxiv_client.py +222 -0
  359. package/vendor/ars/scripts/audit_snapshot.py +572 -0
  360. package/vendor/ars/scripts/bibliographic_integrity_signals.py +800 -0
  361. package/vendor/ars/scripts/bootstrap_timeline_yaml.py +146 -0
  362. package/vendor/ars/scripts/build_claim_standing_candidate_ledger.py +1238 -0
  363. package/vendor/ars/scripts/build_claim_standing_query_plan.py +643 -0
  364. package/vendor/ars/scripts/build_content_coverage_advisory.py +1205 -0
  365. package/vendor/ars/scripts/build_cross_document_consistency_advisory.py +2332 -0
  366. package/vendor/ars/scripts/build_review_pathway_rule_trace.py +830 -0
  367. package/vendor/ars/scripts/build_submission_packet_manifest.py +2310 -0
  368. package/vendor/ars/scripts/check_215_field_norm.py +173 -0
  369. package/vendor/ars/scripts/check_216_surface_form.py +250 -0
  370. package/vendor/ars/scripts/check_268_nested_commitment_ledger.py +180 -0
  371. package/vendor/ars/scripts/check_390_revision_patch_discipline.py +296 -0
  372. package/vendor/ars/scripts/check_392_citation_verification_intake.py +126 -0
  373. package/vendor/ars/scripts/check_394_submission_policy.py +178 -0
  374. package/vendor/ars/scripts/check_439_format_profile.py +307 -0
  375. package/vendor/ars/scripts/check_619_disclosure_closeout.py +237 -0
  376. package/vendor/ars/scripts/check_630_codex_subscription_transport.py +456 -0
  377. package/vendor/ars/scripts/check_669_review_pathway_rule_trace.py +662 -0
  378. package/vendor/ars/scripts/check_670_revision_roadmap_integration.py +518 -0
  379. package/vendor/ars/scripts/check_673_adjudication_activity.py +684 -0
  380. package/vendor/ars/scripts/check_684_review_criteria_binding.py +557 -0
  381. package/vendor/ars/scripts/check_agents_mirror_sync.py +115 -0
  382. package/vendor/ars/scripts/check_audit_artifact_consistency.py +2313 -0
  383. package/vendor/ars/scripts/check_benchmark_report.py +79 -0
  384. package/vendor/ars/scripts/check_bibliographic_integrity_signals.py +831 -0
  385. package/vendor/ars/scripts/check_calibration_tiers.py +235 -0
  386. package/vendor/ars/scripts/check_changelog_covers_merges.py +289 -0
  387. package/vendor/ars/scripts/check_ci_pytest_manifest.py +204 -0
  388. package/vendor/ars/scripts/check_claim_audit_consistency.py +1664 -0
  389. package/vendor/ars/scripts/check_claim_standing_candidate_ledger_integration.py +500 -0
  390. package/vendor/ars/scripts/check_claim_standing_freshness.py +253 -0
  391. package/vendor/ars/scripts/check_claim_standing_transmissions.py +449 -0
  392. package/vendor/ars/scripts/check_collaboration_depth_rubric.py +180 -0
  393. package/vendor/ars/scripts/check_command_frontmatter_name.py +116 -0
  394. package/vendor/ars/scripts/check_committee_correspondence.py +333 -0
  395. package/vendor/ars/scripts/check_compliance_report.py +108 -0
  396. package/vendor/ars/scripts/check_content_coverage_advisory_integration.py +796 -0
  397. package/vendor/ars/scripts/check_control_availability.py +172 -0
  398. package/vendor/ars/scripts/check_corpus_consumer_protocol.py +404 -0
  399. package/vendor/ars/scripts/check_cross_document_consistency_advisory_integration.py +1191 -0
  400. package/vendor/ars/scripts/check_cross_model_handoff_contract.py +234 -0
  401. package/vendor/ars/scripts/check_cross_model_verification_sync.py +261 -0
  402. package/vendor/ars/scripts/check_data_access_level.py +131 -0
  403. package/vendor/ars/scripts/check_data_flows.py +252 -0
  404. package/vendor/ars/scripts/check_decision_contract.py +464 -0
  405. package/vendor/ars/scripts/check_degradation_registry.py +326 -0
  406. package/vendor/ars/scripts/check_distribution_surface_claims.py +226 -0
  407. package/vendor/ars/scripts/check_domain_evidence_profile.py +538 -0
  408. package/vendor/ars/scripts/check_e4_promotion.py +195 -0
  409. package/vendor/ars/scripts/check_evals_gold_set.py +279 -0
  410. package/vendor/ars/scripts/check_evidence_row_integration.py +396 -0
  411. package/vendor/ars/scripts/check_experiment_provenance.py +117 -0
  412. package/vendor/ars/scripts/check_field_norm_severity.py +144 -0
  413. package/vendor/ars/scripts/check_firm_rules_sync.py +375 -0
  414. package/vendor/ars/scripts/check_heldout_measurement_report.py +1179 -0
  415. package/vendor/ars/scripts/check_human_subjects_output_contract.py +139 -0
  416. package/vendor/ars/scripts/check_human_subjects_reference_migration.py +844 -0
  417. package/vendor/ars/scripts/check_indirect_prompt_injection_no_call.py +328 -0
  418. package/vendor/ars/scripts/check_instruction_data_boundary.py +236 -0
  419. package/vendor/ars/scripts/check_judge_prompt_version.py +125 -0
  420. package/vendor/ars/scripts/check_literature_corpus_schema.py +402 -0
  421. package/vendor/ars/scripts/check_model_tiering.py +223 -0
  422. package/vendor/ars/scripts/check_panel_synthesis.py +1331 -0
  423. package/vendor/ars/scripts/check_passport_reset_contract.py +214 -0
  424. package/vendor/ars/scripts/check_pattern_eval_manifest.py +422 -0
  425. package/vendor/ars/scripts/check_persuasion_invariance_fixtures.py +637 -0
  426. package/vendor/ars/scripts/check_phase_conformance.py +2180 -0
  427. package/vendor/ars/scripts/check_pipeline_boundary_semantics.py +600 -0
  428. package/vendor/ars/scripts/check_pipeline_integrity.py +340 -0
  429. package/vendor/ars/scripts/check_policy_anchor_protocol.py +159 -0
  430. package/vendor/ars/scripts/check_policy_anchor_table.py +286 -0
  431. package/vendor/ars/scripts/check_preprint_venues_consistency.py +123 -0
  432. package/vendor/ars/scripts/check_prisma_trAIce_freshness.py +84 -0
  433. package/vendor/ars/scripts/check_promotion_bakeoff_preregistration.py +1303 -0
  434. package/vendor/ars/scripts/check_ranking_lift.py +323 -0
  435. package/vendor/ars/scripts/check_re_review_synthesis.py +2719 -0
  436. package/vendor/ars/scripts/check_receipt_enum_sync.py +203 -0
  437. package/vendor/ars/scripts/check_repro_lock.py +85 -0
  438. package/vendor/ars/scripts/check_review_pathway_output.py +276 -0
  439. package/vendor/ars/scripts/check_reviewer_data_fences.py +224 -0
  440. package/vendor/ars/scripts/check_reviewer_finding_contract.py +783 -0
  441. package/vendor/ars/scripts/check_reviewer_role_label.py +447 -0
  442. package/vendor/ars/scripts/check_reviewer_scoring_honesty.py +292 -0
  443. package/vendor/ars/scripts/check_reviewer_sprint_prompt_sync.py +409 -0
  444. package/vendor/ars/scripts/check_revision_claim_drift_suite_v2.py +1622 -0
  445. package/vendor/ars/scripts/check_revision_token_conservation.py +263 -0
  446. package/vendor/ars/scripts/check_risk_register.py +280 -0
  447. package/vendor/ars/scripts/check_role_scoped_contract.py +702 -0
  448. package/vendor/ars/scripts/check_rq_framing_patterns.py +184 -0
  449. package/vendor/ars/scripts/check_rubric_weight_consistency.py +32 -0
  450. package/vendor/ars/scripts/check_seeded_defect_fixtures.py +425 -0
  451. package/vendor/ars/scripts/check_setup_cross_model_parity.py +139 -0
  452. package/vendor/ars/scripts/check_spec_consistency.py +1249 -0
  453. package/vendor/ars/scripts/check_sprint_contract.py +371 -0
  454. package/vendor/ars/scripts/check_stage_capability_matrix.py +784 -0
  455. package/vendor/ars/scripts/check_submission_packet_manifest_integration.py +358 -0
  456. package/vendor/ars/scripts/check_surface_form_parity.py +445 -0
  457. package/vendor/ars/scripts/check_task_type.py +22 -0
  458. package/vendor/ars/scripts/check_tools_allowlist.py +538 -0
  459. package/vendor/ars/scripts/check_tortured_phrase_screening_integration.py +1974 -0
  460. package/vendor/ars/scripts/check_v3_10_134_write_scope.py +286 -0
  461. package/vendor/ars/scripts/check_v3_10_policy.py +656 -0
  462. package/vendor/ars/scripts/check_v3_6_6_ab_manifest.py +364 -0
  463. package/vendor/ars/scripts/check_v3_6_7_pattern_protection.py +1366 -0
  464. package/vendor/ars/scripts/check_v3_6_8_audit_scope_block.py +464 -0
  465. package/vendor/ars/scripts/check_v3_6_8_cite_provenance_pipeline.py +231 -0
  466. package/vendor/ars/scripts/check_v3_6_8_frontmatter_trust_schema.py +248 -0
  467. package/vendor/ars/scripts/check_v3_6_8_mark_read_commands.py +79 -0
  468. package/vendor/ars/scripts/check_v3_6_8_pattern_protection.py +941 -0
  469. package/vendor/ars/scripts/check_v3_7_3_three_layer_citation.py +318 -0
  470. package/vendor/ars/scripts/check_v3_8_annotation_literal_sync.py +228 -0
  471. package/vendor/ars/scripts/check_v3_9_0_triangulation.py +366 -0
  472. package/vendor/ars/scripts/check_v3_9_2_phase_boundary.py +270 -0
  473. package/vendor/ars/scripts/check_v3_9_4_temporal_verification.py +139 -0
  474. package/vendor/ars/scripts/check_venue_disclosure_policies.py +63 -0
  475. package/vendor/ars/scripts/check_version_consistency.py +826 -0
  476. package/vendor/ars/scripts/check_workflow_classification.py +223 -0
  477. package/vendor/ars/scripts/chinese_literature_client.py +1938 -0
  478. package/vendor/ars/scripts/citation_verification_summary.py +85 -0
  479. package/vendor/ars/scripts/claim_audit_calibration.py +517 -0
  480. package/vendor/ars/scripts/claim_audit_finalizer.py +456 -0
  481. package/vendor/ars/scripts/claim_audit_pipeline.py +1594 -0
  482. package/vendor/ars/scripts/claim_registry_coverage.py +493 -0
  483. package/vendor/ars/scripts/claim_standing_discovery.py +784 -0
  484. package/vendor/ars/scripts/claim_standing_stance_runner.py +758 -0
  485. package/vendor/ars/scripts/claim_standing_stance_scorer.py +239 -0
  486. package/vendor/ars/scripts/claim_strength_drift_disposition.py +666 -0
  487. package/vendor/ars/scripts/contamination_signals.py +689 -0
  488. package/vendor/ars/scripts/corpus_consumer_manifest.json +19 -0
  489. package/vendor/ars/scripts/cross_model_codex_transport.py +1374 -0
  490. package/vendor/ars/scripts/cross_model_codex_verify.sh +6 -0
  491. package/vendor/ars/scripts/cross_model_handoff.py +359 -0
  492. package/vendor/ars/scripts/cross_model_smoke_test.sh +183 -0
  493. package/vendor/ars/scripts/cross_model_smoke_test_codex.sh +35 -0
  494. package/vendor/ars/scripts/cross_model_verification/gemini_is_grounded.jq +45 -0
  495. package/vendor/ars/scripts/cross_model_verification/gemini_sources.jq +45 -0
  496. package/vendor/ars/scripts/cross_model_verification/normalize_compat_verdict.py +57 -0
  497. package/vendor/ars/scripts/cross_model_verification/openai_has_completed_web_search.jq +15 -0
  498. package/vendor/ars/scripts/cross_model_verification/openai_sources.jq +17 -0
  499. package/vendor/ars/scripts/cross_model_verification/openai_text.jq +11 -0
  500. package/vendor/ars/scripts/crossref_client.py +225 -0
  501. package/vendor/ars/scripts/dispatch_e4_panel.py +2731 -0
  502. package/vendor/ars/scripts/evidence_rows.py +2043 -0
  503. package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3.json +32 -0
  504. package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_input.json +27 -0
  505. package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_prime.json +32 -0
  506. package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_prime_input.json +27 -0
  507. package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_override.json +50 -0
  508. package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_override_action.json +14 -0
  509. package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_pass.json +44 -0
  510. package/vendor/ars/scripts/fixtures/adjudication_activity/explicit_user_request_log.json +26 -0
  511. package/vendor/ars/scripts/fixtures/adjudication_activity/mandatory_checkpoint_log.json +116 -0
  512. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/README.md +56 -0
  513. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.audit_artifact_entry.json +24 -0
  514. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
  515. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
  516. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.verdict.yaml +19 -0
  517. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a7_orphan_completion/2026-04-30T15-22-04Z-d8f3.jsonl +3 -0
  518. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.audit_artifact_entry.json +26 -0
  519. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
  520. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
  521. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.verdict.yaml +19 -0
  522. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.audit_artifact_entry.json +24 -0
  523. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
  524. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
  525. package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.verdict.yaml +12 -0
  526. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/retraction.json +46 -0
  527. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/retraction_check_attestation.json +44 -0
  528. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase.json +44 -0
  529. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase_v1_2_abstract_missing.json +107 -0
  530. package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase_v1_2_detected.json +124 -0
  531. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/expected_outcomes.json +77 -0
  532. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/manifest.yaml +37 -0
  533. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/001-valid-doi-test.json +20 -0
  534. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/002-valid-arxiv-test.json +19 -0
  535. package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/003-fabricated-test.json +20 -0
  536. package/vendor/ars/scripts/fixtures/claim_audit_calibration/gold_set.json +344 -0
  537. package/vendor/ars/scripts/fixtures/claim_standing_candidate_ledger/query_plan.json +108 -0
  538. package/vendor/ars/scripts/fixtures/claim_standing_candidate_ledger/retrieval_input.json +179 -0
  539. package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/concern_tracker.json +129 -0
  540. package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/response_skeleton.md +19 -0
  541. package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/source_letter.txt +9 -0
  542. package/vendor/ars/scripts/fixtures/content_coverage_advisory/base_draft.json +48 -0
  543. package/vendor/ars/scripts/fixtures/content_coverage_advisory/base_inventory.json +38 -0
  544. package/vendor/ars/scripts/fixtures/content_coverage_advisory/packet/consent.txt +1 -0
  545. package/vendor/ars/scripts/fixtures/content_coverage_advisory/session_sources.json +3 -0
  546. package/vendor/ars/scripts/fixtures/cross_document_consistency/README.md +6 -0
  547. package/vendor/ars/scripts/fixtures/cross_document_consistency/accepted_draft.md +35 -0
  548. package/vendor/ars/scripts/fixtures/cross_document_consistency/cases.json +75 -0
  549. package/vendor/ars/scripts/fixtures/cross_document_consistency/preregistration.md +5 -0
  550. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/forbidden_event.jsonl +4 -0
  551. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/grounded_verified.jsonl +3 -0
  552. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/malformed.jsonl +3 -0
  553. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/missing_search.jsonl +2 -0
  554. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/multiple_finals.jsonl +4 -0
  555. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/not_found.jsonl +3 -0
  556. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/unbound_source.jsonl +3 -0
  557. package/vendor/ars/scripts/fixtures/cross_model_codex_transport/wrong_search_shape.jsonl +3 -0
  558. package/vendor/ars/scripts/fixtures/evidence_rows/phase_e_inputs.json +114 -0
  559. package/vendor/ars/scripts/fixtures/evidence_rows/session_sources.json +5 -0
  560. package/vendor/ars/scripts/fixtures/human_subjects_authority/cross-border-us-tw-gdpr.json +71 -0
  561. package/vendor/ars/scripts/fixtures/human_subjects_authority/gdpr-member-state-unresolved.json +62 -0
  562. package/vendor/ars/scripts/fixtures/human_subjects_authority/missing-data-axis.json +45 -0
  563. package/vendor/ars/scripts/fixtures/human_subjects_authority/no-profile.json +33 -0
  564. package/vendor/ars/scripts/fixtures/human_subjects_authority/tw-gdpr-two-axis.json +74 -0
  565. package/vendor/ars/scripts/fixtures/human_subjects_authority/us-gdpr-two-axis.json +74 -0
  566. package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/lint-near-misses.json +72 -0
  567. package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/no-profile-request.json +18 -0
  568. package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/tw-candidates-request.json +63 -0
  569. package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/us-candidates-request.json +77 -0
  570. package/vendor/ars/scripts/fixtures/review_target_context/exact-declaration.json +24 -0
  571. package/vendor/ars/scripts/fixtures/review_target_context/field-general-declaration.json +24 -0
  572. package/vendor/ars/scripts/fixtures/review_target_context/msr-2027-technical-full-declaration.json +24 -0
  573. package/vendor/ars/scripts/fixtures/review_target_context/synthetic-registry.json +92 -0
  574. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/README.md +21 -0
  575. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_attested_only.json +55 -0
  576. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_machine_supported.json +49 -0
  577. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_not_isolated.json +56 -0
  578. package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_unknown.json +49 -0
  579. package/vendor/ars/scripts/fixtures/submission_package/clean/paper.md +16 -0
  580. package/vendor/ars/scripts/fixtures/submission_package/clean/references.bib +13 -0
  581. package/vendor/ars/scripts/fixtures/submission_package/fallback_authoryear/paper.md +11 -0
  582. package/vendor/ars/scripts/fixtures/submission_package/fallback_authoryear/references.bib +13 -0
  583. package/vendor/ars/scripts/fixtures/submission_package/fallback_latex/paper.tex +9 -0
  584. package/vendor/ars/scripts/fixtures/submission_package/fallback_latex/references.bib +13 -0
  585. package/vendor/ars/scripts/fixtures/submission_package/marker_no_join/paper.md +7 -0
  586. package/vendor/ars/scripts/fixtures/submission_package/orphan_intext/paper.md +8 -0
  587. package/vendor/ars/scripts/fixtures/submission_package/orphan_intext/references.bib +6 -0
  588. package/vendor/ars/scripts/fixtures/submission_package/passports/corpus_only.yaml +9 -0
  589. package/vendor/ars/scripts/fixtures/submission_package/passports/summary_join.yaml +16 -0
  590. package/vendor/ars/scripts/fixtures/submission_package/profiles/full.yaml +15 -0
  591. package/vendor/ars/scripts/fixtures/submission_package/profiles/tight.yaml +15 -0
  592. package/vendor/ars/scripts/fixtures/submission_package/summary_join/paper.md +7 -0
  593. package/vendor/ars/scripts/fixtures/submission_package/uncited_reference/paper.md +9 -0
  594. package/vendor/ars/scripts/fixtures/submission_package/uncited_reference/references.bib +13 -0
  595. package/vendor/ars/scripts/fixtures/submission_package/venue_clean/paper.md +26 -0
  596. package/vendor/ars/scripts/fixtures/submission_package/venue_clean/references.bib +13 -0
  597. package/vendor/ars/scripts/fixtures/submission_package/venue_violations/paper.md +20 -0
  598. package/vendor/ars/scripts/fixtures/submission_package/venue_violations/references.bib +13 -0
  599. package/vendor/ars/scripts/fixtures/submission_packet_manifest/base_inventory.json +101 -0
  600. package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/consent-materials.txt +3 -0
  601. package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/training-certificate.txt +2 -0
  602. package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/tw-consent-materials.txt +3 -0
  603. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/corpus_input.yaml +30 -0
  604. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/own_draft.md +36 -0
  605. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/own_draft.tex +21 -0
  606. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/seed_expectations.json +218 -0
  607. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/snapshot.json +164 -0
  608. package/vendor/ars/scripts/fixtures/tortured_phrase_screening/snapshot_manifest.json +29 -0
  609. package/vendor/ars/scripts/fixtures/transport_bodies/README.md +55 -0
  610. package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/empty_feed.xml +10 -0
  611. package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/error_5xx.html +7 -0
  612. package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/id_hit.xml +26 -0
  613. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/README.md +69 -0
  614. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/cnki_landing_page.html +13 -0
  615. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/error_5xx.html +2 -0
  616. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_ambiguous.json +16 -0
  617. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_hit.json +16 -0
  618. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_zero.json +14 -0
  619. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coverage_hit.json +15 -0
  620. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coverage_zero.json +13 -0
  621. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_hit.json +43 -0
  622. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_issn_mismatch.json +31 -0
  623. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_no_doi.json +31 -0
  624. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_unknown_ra.json +34 -0
  625. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_year_mismatch.json +31 -0
  626. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_absent.json +4 -0
  627. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_exists.json +16 -0
  628. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_internal_error.json +4 -0
  629. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/istic_csl_hit.json +25 -0
  630. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/istic_csl_other_title.json +18 -0
  631. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_cnki.json +6 -0
  632. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_crossref.json +6 -0
  633. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_istic.json +6 -0
  634. package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_unknown_prefix.json +6 -0
  635. package/vendor/ars/scripts/fixtures/transport_bodies/crossref/doi_hit.json +24 -0
  636. package/vendor/ars/scripts/fixtures/transport_bodies/crossref/error_5xx.html +7 -0
  637. package/vendor/ars/scripts/fixtures/transport_bodies/crossref/title_search_miss.json +15 -0
  638. package/vendor/ars/scripts/fixtures/transport_bodies/openalex/doi_hit.json +29 -0
  639. package/vendor/ars/scripts/fixtures/transport_bodies/openalex/error_5xx.json +4 -0
  640. package/vendor/ars/scripts/fixtures/transport_bodies/openalex/title_search_miss.json +11 -0
  641. package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/doi_hit.json +14 -0
  642. package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/error_5xx.json +3 -0
  643. package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/title_search_miss.json +5 -0
  644. package/vendor/ars/scripts/human_read_attestation_resolver.py +467 -0
  645. package/vendor/ars/scripts/ideation_diversity_assignment_gate.py +430 -0
  646. package/vendor/ars/scripts/inquiry_branch_ledger.py +2540 -0
  647. package/vendor/ars/scripts/legacy/ars_apply_revision_patch_v1_0.py +715 -0
  648. package/vendor/ars/scripts/legacy/check_re_review_synthesis_v1_0.py +2215 -0
  649. package/vendor/ars/scripts/migrate_literature_corpus_to_v3_10.py +223 -0
  650. package/vendor/ars/scripts/migrate_literature_corpus_to_v3_7_3.py +277 -0
  651. package/vendor/ars/scripts/migrate_literature_corpus_to_v3_9_0.py +304 -0
  652. package/vendor/ars/scripts/model_tiering_manifest.json +46 -0
  653. package/vendor/ars/scripts/openalex_client.py +232 -0
  654. package/vendor/ars/scripts/parse_audit_verdict.py +802 -0
  655. package/vendor/ars/scripts/pdf_content_classifier_worker.py +176 -0
  656. package/vendor/ars/scripts/pdf_read_preflight.py +1455 -0
  657. package/vendor/ars/scripts/policy_anchor_disclosure_referee.py +354 -0
  658. package/vendor/ars/scripts/recompute_receipts.py +1414 -0
  659. package/vendor/ars/scripts/render_claim_standing_view.py +374 -0
  660. package/vendor/ars/scripts/render_eval_comment.py +130 -0
  661. package/vendor/ars/scripts/render_harness_retirement_issue.py +167 -0
  662. package/vendor/ars/scripts/repro_lock_validation.py +90 -0
  663. package/vendor/ars/scripts/research_workflow_profile.py +1079 -0
  664. package/vendor/ars/scripts/resolve_human_subjects_authority.py +1158 -0
  665. package/vendor/ars/scripts/resolve_review_target_context.py +683 -0
  666. package/vendor/ars/scripts/resume_e4_record.py +509 -0
  667. package/vendor/ars/scripts/retraction_status.py +484 -0
  668. package/vendor/ars/scripts/review_criteria_binding.py +889 -0
  669. package/vendor/ars/scripts/review_panel_provenance.py +744 -0
  670. package/vendor/ars/scripts/revision_roadmap.py +1967 -0
  671. package/vendor/ars/scripts/run_ci_pytest_manifest.py +116 -0
  672. package/vendor/ars/scripts/run_codex_audit.sh +1191 -0
  673. package/vendor/ars/scripts/run_evals.py +513 -0
  674. package/vendor/ars/scripts/run_ideation_diversity_no_call.py +3505 -0
  675. package/vendor/ars/scripts/run_indirect_prompt_injection_no_call.py +3330 -0
  676. package/vendor/ars/scripts/run_indirect_prompt_injection_probe.py +399 -0
  677. package/vendor/ars/scripts/run_review_criteria_constructive_value.py +1895 -0
  678. package/vendor/ars/scripts/run_role_topology_utility_dry_run.py +606 -0
  679. package/vendor/ars/scripts/score_review_criteria_constructive_value.py +617 -0
  680. package/vendor/ars/scripts/semantic_scholar_client.py +291 -0
  681. package/vendor/ars/scripts/slr_lineage.py +59 -0
  682. package/vendor/ars/scripts/sync_adapter_docs.py +118 -0
  683. package/vendor/ars/scripts/temporal_integrity_audit.py +840 -0
  684. package/vendor/ars/scripts/test_431_exact_or_bust.py +253 -0
  685. package/vendor/ars/scripts/test__eval_threshold_gate.py +125 -0
  686. package/vendor/ars/scripts/test__markdown_lint_util.py +129 -0
  687. package/vendor/ars/scripts/test__next_verified_at_ms.py +256 -0
  688. package/vendor/ars/scripts/test_adjacent_framing_probe_lint.py +171 -0
  689. package/vendor/ars/scripts/test_adjudication_activity.py +1515 -0
  690. package/vendor/ars/scripts/test_ars_anchorize_draft.py +178 -0
  691. package/vendor/ars/scripts/test_ars_apply_revision_patch.py +1335 -0
  692. package/vendor/ars/scripts/test_ars_cache_invalidate.py +51 -0
  693. package/vendor/ars/scripts/test_ars_mark_read.py +910 -0
  694. package/vendor/ars/scripts/test_ars_update_check.py +816 -0
  695. package/vendor/ars/scripts/test_ars_write_scope_guard.py +790 -0
  696. package/vendor/ars/scripts/test_arxiv_client.py +374 -0
  697. package/vendor/ars/scripts/test_audit_schemas.py +560 -0
  698. package/vendor/ars/scripts/test_audit_snapshot_render_section_0.py +105 -0
  699. package/vendor/ars/scripts/test_block_parser.py +259 -0
  700. package/vendor/ars/scripts/test_bootstrap_timeline_yaml.py +148 -0
  701. package/vendor/ars/scripts/test_build_claim_standing_candidate_ledger.py +1203 -0
  702. package/vendor/ars/scripts/test_build_claim_standing_query_plan.py +791 -0
  703. package/vendor/ars/scripts/test_build_submission_packet_manifest.py +2644 -0
  704. package/vendor/ars/scripts/test_check_215_field_norm.py +238 -0
  705. package/vendor/ars/scripts/test_check_216_surface_form.py +341 -0
  706. package/vendor/ars/scripts/test_check_268_nested_commitment_ledger.py +194 -0
  707. package/vendor/ars/scripts/test_check_390_revision_patch_discipline.py +279 -0
  708. package/vendor/ars/scripts/test_check_392_citation_verification_intake.py +144 -0
  709. package/vendor/ars/scripts/test_check_394_submission_policy.py +200 -0
  710. package/vendor/ars/scripts/test_check_439_format_profile.py +252 -0
  711. package/vendor/ars/scripts/test_check_619_disclosure_closeout.py +158 -0
  712. package/vendor/ars/scripts/test_check_630_codex_subscription_transport.py +245 -0
  713. package/vendor/ars/scripts/test_check_669_review_pathway_rule_trace.py +304 -0
  714. package/vendor/ars/scripts/test_check_670_revision_roadmap_integration.py +260 -0
  715. package/vendor/ars/scripts/test_check_673_adjudication_activity.py +367 -0
  716. package/vendor/ars/scripts/test_check_684_review_criteria_binding.py +274 -0
  717. package/vendor/ars/scripts/test_check_agents_mirror_sync.py +137 -0
  718. package/vendor/ars/scripts/test_check_audit_artifact_consistency.py +2133 -0
  719. package/vendor/ars/scripts/test_check_benchmark_report.py +117 -0
  720. package/vendor/ars/scripts/test_check_bibliographic_integrity_signals.py +609 -0
  721. package/vendor/ars/scripts/test_check_calibration_tiers.py +376 -0
  722. package/vendor/ars/scripts/test_check_changelog_covers_merges.py +509 -0
  723. package/vendor/ars/scripts/test_check_ci_pytest_manifest.py +425 -0
  724. package/vendor/ars/scripts/test_check_claim_standing_candidate_ledger_integration.py +256 -0
  725. package/vendor/ars/scripts/test_check_claim_standing_freshness.py +277 -0
  726. package/vendor/ars/scripts/test_check_collaboration_depth_rubric.py +239 -0
  727. package/vendor/ars/scripts/test_check_command_frontmatter_name.py +178 -0
  728. package/vendor/ars/scripts/test_check_committee_correspondence.py +298 -0
  729. package/vendor/ars/scripts/test_check_compliance_report.py +381 -0
  730. package/vendor/ars/scripts/test_check_content_coverage_advisory_integration.py +308 -0
  731. package/vendor/ars/scripts/test_check_control_availability.py +361 -0
  732. package/vendor/ars/scripts/test_check_cross_document_consistency_advisory_integration.py +698 -0
  733. package/vendor/ars/scripts/test_check_cross_model_handoff_contract.py +301 -0
  734. package/vendor/ars/scripts/test_check_cross_model_verification_sync.py +203 -0
  735. package/vendor/ars/scripts/test_check_data_access_level.py +227 -0
  736. package/vendor/ars/scripts/test_check_data_flows.py +382 -0
  737. package/vendor/ars/scripts/test_check_decision_contract.py +387 -0
  738. package/vendor/ars/scripts/test_check_degradation_registry.py +270 -0
  739. package/vendor/ars/scripts/test_check_distribution_surface_claims.py +225 -0
  740. package/vendor/ars/scripts/test_check_domain_evidence_profile.py +439 -0
  741. package/vendor/ars/scripts/test_check_e4_promotion.py +163 -0
  742. package/vendor/ars/scripts/test_check_evals_gold_set.py +312 -0
  743. package/vendor/ars/scripts/test_check_evidence_row_integration.py +187 -0
  744. package/vendor/ars/scripts/test_check_field_norm_severity.py +173 -0
  745. package/vendor/ars/scripts/test_check_firm_rules_sync.py +342 -0
  746. package/vendor/ars/scripts/test_check_heldout_measurement_report.py +1508 -0
  747. package/vendor/ars/scripts/test_check_human_subjects_output_contract.py +129 -0
  748. package/vendor/ars/scripts/test_check_human_subjects_reference_migration.py +756 -0
  749. package/vendor/ars/scripts/test_check_instruction_data_boundary.py +204 -0
  750. package/vendor/ars/scripts/test_check_judge_prompt_version.py +90 -0
  751. package/vendor/ars/scripts/test_check_model_tiering.py +236 -0
  752. package/vendor/ars/scripts/test_check_panel_synthesis.py +1658 -0
  753. package/vendor/ars/scripts/test_check_passport_reset_contract.py +249 -0
  754. package/vendor/ars/scripts/test_check_pattern_eval_manifest.py +381 -0
  755. package/vendor/ars/scripts/test_check_persuasion_invariance_fixtures.py +591 -0
  756. package/vendor/ars/scripts/test_check_phase_conformance.py +4366 -0
  757. package/vendor/ars/scripts/test_check_pipeline_boundary_semantics.py +931 -0
  758. package/vendor/ars/scripts/test_check_pipeline_integrity.py +243 -0
  759. package/vendor/ars/scripts/test_check_policy_anchor_protocol.py +236 -0
  760. package/vendor/ars/scripts/test_check_policy_anchor_table.py +295 -0
  761. package/vendor/ars/scripts/test_check_prisma_trAIce_freshness.py +68 -0
  762. package/vendor/ars/scripts/test_check_promotion_bakeoff_preregistration.py +799 -0
  763. package/vendor/ars/scripts/test_check_ranking_lift.py +377 -0
  764. package/vendor/ars/scripts/test_check_re_review_synthesis.py +3398 -0
  765. package/vendor/ars/scripts/test_check_receipt_enum_sync.py +176 -0
  766. package/vendor/ars/scripts/test_check_repro_lock.py +107 -0
  767. package/vendor/ars/scripts/test_check_reviewer_data_fences.py +334 -0
  768. package/vendor/ars/scripts/test_check_reviewer_finding_contract.py +1107 -0
  769. package/vendor/ars/scripts/test_check_reviewer_role_label.py +491 -0
  770. package/vendor/ars/scripts/test_check_reviewer_scoring_honesty.py +222 -0
  771. package/vendor/ars/scripts/test_check_reviewer_sprint_prompt_sync.py +373 -0
  772. package/vendor/ars/scripts/test_check_revision_claim_drift_suite_v2.py +1925 -0
  773. package/vendor/ars/scripts/test_check_revision_token_conservation.py +398 -0
  774. package/vendor/ars/scripts/test_check_risk_register.py +336 -0
  775. package/vendor/ars/scripts/test_check_role_scoped_contract.py +1104 -0
  776. package/vendor/ars/scripts/test_check_rq_framing_patterns.py +110 -0
  777. package/vendor/ars/scripts/test_check_rubric_weight_consistency.py +32 -0
  778. package/vendor/ars/scripts/test_check_seeded_defect_fixtures.py +392 -0
  779. package/vendor/ars/scripts/test_check_setup_cross_model_parity.py +121 -0
  780. package/vendor/ars/scripts/test_check_spec_consistency.py +1081 -0
  781. package/vendor/ars/scripts/test_check_sprint_contract.py +458 -0
  782. package/vendor/ars/scripts/test_check_stage_capability_matrix.py +698 -0
  783. package/vendor/ars/scripts/test_check_submission_packet_manifest_integration.py +177 -0
  784. package/vendor/ars/scripts/test_check_surface_form_parity.py +417 -0
  785. package/vendor/ars/scripts/test_check_task_type.py +116 -0
  786. package/vendor/ars/scripts/test_check_tools_allowlist.py +835 -0
  787. package/vendor/ars/scripts/test_check_tortured_phrase_screening_integration.py +1306 -0
  788. package/vendor/ars/scripts/test_check_v3_10_134_write_scope.py +251 -0
  789. package/vendor/ars/scripts/test_check_v3_10_policy.py +547 -0
  790. package/vendor/ars/scripts/test_check_v3_6_7_pattern_protection.py +960 -0
  791. package/vendor/ars/scripts/test_check_v3_6_8_audit_scope_block.py +1000 -0
  792. package/vendor/ars/scripts/test_check_v3_6_8_cite_provenance_pipeline.py +454 -0
  793. package/vendor/ars/scripts/test_check_v3_6_8_frontmatter_trust_schema.py +581 -0
  794. package/vendor/ars/scripts/test_check_v3_6_8_mark_read_commands.py +120 -0
  795. package/vendor/ars/scripts/test_check_v3_6_8_pattern_protection.py +1138 -0
  796. package/vendor/ars/scripts/test_check_v3_7_3_three_layer_citation.py +566 -0
  797. package/vendor/ars/scripts/test_check_v3_8_annotation_literal_sync.py +262 -0
  798. package/vendor/ars/scripts/test_check_v3_9_0_triangulation.py +321 -0
  799. package/vendor/ars/scripts/test_check_v3_9_2_phase_boundary.py +190 -0
  800. package/vendor/ars/scripts/test_check_v3_9_4_temporal_verification.py +471 -0
  801. package/vendor/ars/scripts/test_check_version_consistency.py +1462 -0
  802. package/vendor/ars/scripts/test_check_workflow_classification.py +225 -0
  803. package/vendor/ars/scripts/test_chinese_literature_client.py +1889 -0
  804. package/vendor/ars/scripts/test_citation_existence_policy.py +480 -0
  805. package/vendor/ars/scripts/test_citation_verification_summary.py +342 -0
  806. package/vendor/ars/scripts/test_claim_audit_calibration.py +882 -0
  807. package/vendor/ars/scripts/test_claim_audit_finalizer.py +979 -0
  808. package/vendor/ars/scripts/test_claim_audit_pipeline.py +2398 -0
  809. package/vendor/ars/scripts/test_claim_audit_schema.py +1778 -0
  810. package/vendor/ars/scripts/test_claim_intent_manifest.py +666 -0
  811. package/vendor/ars/scripts/test_claim_registry_coverage.py +246 -0
  812. package/vendor/ars/scripts/test_claim_standing_discovery.py +579 -0
  813. package/vendor/ars/scripts/test_claim_standing_pipeline_wiring.py +290 -0
  814. package/vendor/ars/scripts/test_claim_standing_stance_assets.py +399 -0
  815. package/vendor/ars/scripts/test_claim_standing_stance_contracts.py +383 -0
  816. package/vendor/ars/scripts/test_claim_standing_stance_runner.py +349 -0
  817. package/vendor/ars/scripts/test_claim_standing_transmissions.py +605 -0
  818. package/vendor/ars/scripts/test_claim_strength_drift_disposition.py +517 -0
  819. package/vendor/ars/scripts/test_claim_verification_coverage_contract.py +104 -0
  820. package/vendor/ars/scripts/test_contamination_signals.py +1086 -0
  821. package/vendor/ars/scripts/test_content_coverage_advisory.py +1767 -0
  822. package/vendor/ars/scripts/test_cross_document_consistency_advisory.py +1270 -0
  823. package/vendor/ars/scripts/test_cross_model_codex_transport.py +1152 -0
  824. package/vendor/ars/scripts/test_cross_model_handoff.py +571 -0
  825. package/vendor/ars/scripts/test_cross_model_verification_guards.py +792 -0
  826. package/vendor/ars/scripts/test_crossref_client.py +393 -0
  827. package/vendor/ars/scripts/test_dispatch_e4_panel.py +4235 -0
  828. package/vendor/ars/scripts/test_e2e_claim_audit.py +540 -0
  829. package/vendor/ars/scripts/test_eval_harness_workflow.py +140 -0
  830. package/vendor/ars/scripts/test_evals_citation_extraction.py +150 -0
  831. package/vendor/ars/scripts/test_evals_lift_report_schema.py +108 -0
  832. package/vendor/ars/scripts/test_evidence_rows.py +2593 -0
  833. package/vendor/ars/scripts/test_experiment_provenance.py +915 -0
  834. package/vendor/ars/scripts/test_human_read_attestation_resolver.py +477 -0
  835. package/vendor/ars/scripts/test_ideation_diversity_assignment_gate.py +615 -0
  836. package/vendor/ars/scripts/test_indirect_prompt_injection_behavior_probe.py +250 -0
  837. package/vendor/ars/scripts/test_inquiry_branch_ledger.py +2296 -0
  838. package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_10.py +248 -0
  839. package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_7_3.py +545 -0
  840. package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_9_0.py +497 -0
  841. package/vendor/ars/scripts/test_normalize_compat_verdict.py +149 -0
  842. package/vendor/ars/scripts/test_openalex_client.py +490 -0
  843. package/vendor/ars/scripts/test_passport_yaml.py +104 -0
  844. package/vendor/ars/scripts/test_pattern_eval_runtime.py +1295 -0
  845. package/vendor/ars/scripts/test_pdf_read_preflight.py +1943 -0
  846. package/vendor/ars/scripts/test_policy_anchor_disclosure.py +666 -0
  847. package/vendor/ars/scripts/test_reading_probe_lint.py +218 -0
  848. package/vendor/ars/scripts/test_recompute_receipts.py +778 -0
  849. package/vendor/ars/scripts/test_render_claim_standing_view.py +192 -0
  850. package/vendor/ars/scripts/test_render_eval_comment.py +162 -0
  851. package/vendor/ars/scripts/test_render_harness_retirement_issue.py +110 -0
  852. package/vendor/ars/scripts/test_repro_lock_validation_drift.py +100 -0
  853. package/vendor/ars/scripts/test_research_workflow_profile.py +734 -0
  854. package/vendor/ars/scripts/test_resolve_human_subjects_authority.py +1219 -0
  855. package/vendor/ars/scripts/test_resolve_review_target_context.py +703 -0
  856. package/vendor/ars/scripts/test_resume_e4_record.py +315 -0
  857. package/vendor/ars/scripts/test_retraction_status.py +456 -0
  858. package/vendor/ars/scripts/test_review_criteria_binding.py +629 -0
  859. package/vendor/ars/scripts/test_review_panel_provenance.py +565 -0
  860. package/vendor/ars/scripts/test_review_pathway_rule_trace.py +821 -0
  861. package/vendor/ars/scripts/test_revision_roadmap.py +1255 -0
  862. package/vendor/ars/scripts/test_run_ci_pytest_manifest.py +186 -0
  863. package/vendor/ars/scripts/test_run_codex_audit_e2e.py +369 -0
  864. package/vendor/ars/scripts/test_run_evals.py +430 -0
  865. package/vendor/ars/scripts/test_run_guard_launcher.py +500 -0
  866. package/vendor/ars/scripts/test_run_ideation_diversity_no_call.py +1833 -0
  867. package/vendor/ars/scripts/test_run_indirect_prompt_injection_no_call.py +1889 -0
  868. package/vendor/ars/scripts/test_run_review_criteria_constructive_value.py +586 -0
  869. package/vendor/ars/scripts/test_run_role_topology_utility_dry_run.py +428 -0
  870. package/vendor/ars/scripts/test_score_review_criteria_constructive_value.py +340 -0
  871. package/vendor/ars/scripts/test_semantic_scholar_client.py +554 -0
  872. package/vendor/ars/scripts/test_slr_lineage_emission.py +230 -0
  873. package/vendor/ars/scripts/test_socratic_rq_non_generation_contract.py +173 -0
  874. package/vendor/ars/scripts/test_temporal_integrity_audit.py +438 -0
  875. package/vendor/ars/scripts/test_text_similarity.py +95 -0
  876. package/vendor/ars/scripts/test_title_fuzzy_false_positive.py +111 -0
  877. package/vendor/ars/scripts/test_tortured_phrase_screening.py +2959 -0
  878. package/vendor/ars/scripts/test_transport_fixture_citation_gate.py +338 -0
  879. package/vendor/ars/scripts/test_uncited_assertion.py +558 -0
  880. package/vendor/ars/scripts/test_v3_6_7_phase_6_6.py +1279 -0
  881. package/vendor/ars/scripts/test_validate_compliance_fixtures.py +36 -0
  882. package/vendor/ars/scripts/test_validate_ideation_diversity_assets.py +230 -0
  883. package/vendor/ars/scripts/test_venue_disclosure_contract.py +755 -0
  884. package/vendor/ars/scripts/test_verification_cache.py +280 -0
  885. package/vendor/ars/scripts/test_verification_gate.py +461 -0
  886. package/vendor/ars/scripts/test_verify_passport_cli.py +123 -0
  887. package/vendor/ars/scripts/test_verify_submission_package.py +1407 -0
  888. package/vendor/ars/scripts/test_version_records_schema.py +211 -0
  889. package/vendor/ars/scripts/tortured_phrase_screening.py +3502 -0
  890. package/vendor/ars/scripts/uncited_assertion_detector.py +254 -0
  891. package/vendor/ars/scripts/v3_6_7_inversion_manifest.json +9 -0
  892. package/vendor/ars/scripts/v3_6_8_inversion_manifest.json +10 -0
  893. package/vendor/ars/scripts/validate_claim_standing_stance_assets.py +249 -0
  894. package/vendor/ars/scripts/validate_compliance_fixtures.py +56 -0
  895. package/vendor/ars/scripts/validate_ideation_diversity_assets.py +303 -0
  896. package/vendor/ars/scripts/venue_disclosure_contract_harness.py +837 -0
  897. package/vendor/ars/scripts/verification_cache.py +276 -0
  898. package/vendor/ars/scripts/verification_gate/__init__.py +345 -0
  899. package/vendor/ars/scripts/verify_passport.py +133 -0
  900. package/vendor/ars/scripts/verify_submission_package.py +1657 -0
  901. package/vendor/ars/shared/agents/compliance_agent.md +136 -0
  902. package/vendor/ars/shared/artifact_reproducibility_pattern.md +173 -0
  903. package/vendor/ars/shared/benchmark_report.schema.json +81 -0
  904. package/vendor/ars/shared/benchmark_report_pattern.md +180 -0
  905. package/vendor/ars/shared/bibliographic_integrity_signals.md +142 -0
  906. package/vendor/ars/shared/collaboration_depth_rubric.md +154 -0
  907. package/vendor/ars/shared/compliance_checkpoint_protocol.md +162 -0
  908. package/vendor/ars/shared/compliance_report.schema.json +187 -0
  909. package/vendor/ars/shared/contracts/README.md +938 -0
  910. package/vendor/ars/shared/contracts/activity/adjudication_activity_input.schema.json +555 -0
  911. package/vendor/ars/shared/contracts/activity/adjudication_activity_store.schema.json +560 -0
  912. package/vendor/ars/shared/contracts/audit/audit_jsonl.schema.json +128 -0
  913. package/vendor/ars/shared/contracts/audit/audit_sidecar.schema.json +169 -0
  914. package/vendor/ars/shared/contracts/audit/audit_verdict.schema.json +133 -0
  915. package/vendor/ars/shared/contracts/audit/cross_document_consistency_advisory.schema.json +480 -0
  916. package/vendor/ars/shared/contracts/audit/cross_document_consistency_advisory_draft.schema.json +564 -0
  917. package/vendor/ars/shared/contracts/audit/cross_document_source_manifest.schema.json +202 -0
  918. package/vendor/ars/shared/contracts/audit/tortured_phrase_advisory.schema.json +1362 -0
  919. package/vendor/ars/shared/contracts/audit/tortured_phrase_snapshot.schema.json +208 -0
  920. package/vendor/ars/shared/contracts/audit/tortured_phrase_snapshot_manifest.schema.json +335 -0
  921. package/vendor/ars/shared/contracts/capability/stage_capability_matrix.json +434 -0
  922. package/vendor/ars/shared/contracts/claim_standing/candidate_ledger.schema.json +705 -0
  923. package/vendor/ars/shared/contracts/claim_standing/query_plan.schema.json +389 -0
  924. package/vendor/ars/shared/contracts/claim_standing/query_plan_v1_1.schema.json +782 -0
  925. package/vendor/ars/shared/contracts/claim_standing/retrieval_input.schema.json +440 -0
  926. package/vendor/ars/shared/contracts/claim_standing/stance_record.schema.json +460 -0
  927. package/vendor/ars/shared/contracts/claim_standing/transmission_ledger.schema.json +282 -0
  928. package/vendor/ars/shared/contracts/cross_model/codex_citation_receipt.schema.json +157 -0
  929. package/vendor/ars/shared/contracts/cross_model/codex_citation_request.schema.json +23 -0
  930. package/vendor/ars/shared/contracts/cross_model/promotion_bakeoff_sealed_commitment.schema.json +55 -0
  931. package/vendor/ars/shared/contracts/cross_model/promotion_bakeoff_sealed_reveal.schema.json +50 -0
  932. package/vendor/ars/shared/contracts/degradation_registry.json +428 -0
  933. package/vendor/ars/shared/contracts/evaluator/full.json +126 -0
  934. package/vendor/ars/shared/contracts/evidence/claim_registry.schema.json +50 -0
  935. package/vendor/ars/shared/contracts/evidence/claim_registry_coverage_report.schema.json +79 -0
  936. package/vendor/ars/shared/contracts/evidence/evidence_row.schema.json +504 -0
  937. package/vendor/ars/shared/contracts/evidence/evidence_row_v1_1.schema.json +364 -0
  938. package/vendor/ars/shared/contracts/evidence/evidence_row_v1_2.schema.json +714 -0
  939. package/vendor/ars/shared/contracts/evidence/evidence_row_v1_3.schema.json +524 -0
  940. package/vendor/ars/shared/contracts/human_subjects/authority_profile_registry.schema.json +497 -0
  941. package/vendor/ars/shared/contracts/human_subjects/committee_correspondence.schema.json +253 -0
  942. package/vendor/ars/shared/contracts/human_subjects/content_coverage_advisory.schema.json +620 -0
  943. package/vendor/ars/shared/contracts/human_subjects/irb_context_record.schema.json +329 -0
  944. package/vendor/ars/shared/contracts/human_subjects/resolved_authority_context.schema.json +347 -0
  945. package/vendor/ars/shared/contracts/human_subjects/review_pathway_rule_trace.schema.json +265 -0
  946. package/vendor/ars/shared/contracts/human_subjects/review_pathway_trace_request.schema.json +127 -0
  947. package/vendor/ars/shared/contracts/human_subjects/submission_packet_inventory.schema.json +240 -0
  948. package/vendor/ars/shared/contracts/human_subjects/submission_packet_manifest.schema.json +999 -0
  949. package/vendor/ars/shared/contracts/passport/audit_artifact_entry.schema.json +266 -0
  950. package/vendor/ars/shared/contracts/passport/bibliographic_integrity_signal.schema.json +1676 -0
  951. package/vendor/ars/shared/contracts/passport/citation_provenance.schema.json +107 -0
  952. package/vendor/ars/shared/contracts/passport/citation_verification_summary.schema.json +164 -0
  953. package/vendor/ars/shared/contracts/passport/claim_audit_result.schema.json +124 -0
  954. package/vendor/ars/shared/contracts/passport/claim_drift.schema.json +58 -0
  955. package/vendor/ars/shared/contracts/passport/claim_intent_manifest.schema.json +107 -0
  956. package/vendor/ars/shared/contracts/passport/constraint_violation.schema.json +65 -0
  957. package/vendor/ars/shared/contracts/passport/experiment_alignment_result.schema.json +69 -0
  958. package/vendor/ars/shared/contracts/passport/experiment_provenance_entry.schema.json +169 -0
  959. package/vendor/ars/shared/contracts/passport/human_read_log.schema.json +86 -0
  960. package/vendor/ars/shared/contracts/passport/inquiry_ledger_ref.schema.json +22 -0
  961. package/vendor/ars/shared/contracts/passport/literature_corpus_entry.schema.json +650 -0
  962. package/vendor/ars/shared/contracts/passport/preregistration_artifact.schema.json +132 -0
  963. package/vendor/ars/shared/contracts/passport/rejection_log.schema.json +89 -0
  964. package/vendor/ars/shared/contracts/passport/reset_ledger_entry.schema.json +158 -0
  965. package/vendor/ars/shared/contracts/passport/temporal_audit_results.schema.json +208 -0
  966. package/vendor/ars/shared/contracts/passport/terminal_policies.schema.json +50 -0
  967. package/vendor/ars/shared/contracts/passport/timeline.schema.json +102 -0
  968. package/vendor/ars/shared/contracts/passport/uncited_assertion.schema.json +56 -0
  969. package/vendor/ars/shared/contracts/passport/uncited_audit_failure.schema.json +72 -0
  970. package/vendor/ars/shared/contracts/passport/user_attested_read_resolution.schema.json +87 -0
  971. package/vendor/ars/shared/contracts/passport/version_records.schema.json +138 -0
  972. package/vendor/ars/shared/contracts/patch/block_manifest.schema.json +45 -0
  973. package/vendor/ars/shared/contracts/patch/legacy/v1_0/revision_patch.schema.json +110 -0
  974. package/vendor/ars/shared/contracts/patch/revision_patch.schema.json +250 -0
  975. package/vendor/ars/shared/contracts/pdf/pdf_content_classifier_diagnostic.schema.json +39 -0
  976. package/vendor/ars/shared/contracts/pdf/pdf_content_classifier_worker.schema.json +88 -0
  977. package/vendor/ars/shared/contracts/pdf/pdf_read_preflight.schema.json +227 -0
  978. package/vendor/ars/shared/contracts/re_review/input_manifest.schema.json +161 -0
  979. package/vendor/ars/shared/contracts/re_review/legacy/v1_0/input_manifest.schema.json +134 -0
  980. package/vendor/ars/shared/contracts/re_review/legacy/v1_0/precommitment.schema.json +193 -0
  981. package/vendor/ars/shared/contracts/re_review/legacy/v1_0/traceability.schema.json +841 -0
  982. package/vendor/ars/shared/contracts/re_review/legacy/v1_0/verdict_record.schema.json +244 -0
  983. package/vendor/ars/shared/contracts/re_review/precommitment.schema.json +193 -0
  984. package/vendor/ars/shared/contracts/re_review/traceability.schema.json +916 -0
  985. package/vendor/ars/shared/contracts/re_review/verdict_record.schema.json +245 -0
  986. package/vendor/ars/shared/contracts/research_workflow/inquiry_branch_ledger.schema.json +449 -0
  987. package/vendor/ars/shared/contracts/research_workflow/research_workflow_profile.schema.json +222 -0
  988. package/vendor/ars/shared/contracts/research_workflow/research_workflow_profile_selection_receipt.schema.json +111 -0
  989. package/vendor/ars/shared/contracts/review_target/constructive_review_findings.schema.json +172 -0
  990. package/vendor/ars/shared/contracts/review_target/criteria_registry.schema.json +97 -0
  991. package/vendor/ars/shared/contracts/review_target/review_criteria_binding_manifest.schema.json +235 -0
  992. package/vendor/ars/shared/contracts/review_target/review_criteria_source_receipt.schema.json +158 -0
  993. package/vendor/ars/shared/contracts/review_target/review_target_context.schema.json +106 -0
  994. package/vendor/ars/shared/contracts/review_target/review_target_declaration.schema.json +118 -0
  995. package/vendor/ars/shared/contracts/reviewer/full.json +114 -0
  996. package/vendor/ars/shared/contracts/reviewer/methodology_focus.json +75 -0
  997. package/vendor/ars/shared/contracts/reviewer/review_panel_provenance.schema.json +263 -0
  998. package/vendor/ars/shared/contracts/reviewer/review_panel_provenance_carrier.schema.json +122 -0
  999. package/vendor/ars/shared/contracts/reviewer/review_panel_provenance_input.schema.json +120 -0
  1000. package/vendor/ars/shared/contracts/revision/author_adjudication.schema.json +251 -0
  1001. package/vendor/ars/shared/contracts/revision/author_adjudication_input.schema.json +25 -0
  1002. package/vendor/ars/shared/contracts/revision/claim_strength_drift_disposition.schema.json +97 -0
  1003. package/vendor/ars/shared/contracts/revision/claim_strength_drift_disposition_input.schema.json +80 -0
  1004. package/vendor/ars/shared/contracts/revision/claim_strength_drift_findings.schema.json +139 -0
  1005. package/vendor/ars/shared/contracts/revision/claim_surface_manifest.schema.json +99 -0
  1006. package/vendor/ars/shared/contracts/revision/integrity_correction_authorization.schema.json +140 -0
  1007. package/vendor/ars/shared/contracts/revision/integrity_correction_authorization_input.schema.json +28 -0
  1008. package/vendor/ars/shared/contracts/revision/integrity_correction_list.schema.json +48 -0
  1009. package/vendor/ars/shared/contracts/revision/integrity_pass_receipt.schema.json +16 -0
  1010. package/vendor/ars/shared/contracts/revision/revision_evidence_bundle.schema.json +134 -0
  1011. package/vendor/ars/shared/contracts/revision/revision_roadmap.schema.json +334 -0
  1012. package/vendor/ars/shared/contracts/submission/format_profile.example.yaml +33 -0
  1013. package/vendor/ars/shared/contracts/submission/format_profile.schema.json +102 -0
  1014. package/vendor/ars/shared/contracts/submission/submission_verification_report.schema.json +233 -0
  1015. package/vendor/ars/shared/contracts/submission/venue_profile.schema.json +114 -0
  1016. package/vendor/ars/shared/contracts/writer/full.json +87 -0
  1017. package/vendor/ars/shared/cross_model_verification.md +714 -0
  1018. package/vendor/ars/shared/evals_lift_report.schema.json +141 -0
  1019. package/vendor/ars/shared/ground_truth_isolation_pattern.md +275 -0
  1020. package/vendor/ars/shared/handoff_schemas.md +1209 -0
  1021. package/vendor/ars/shared/human_subjects_authority_registry.json +1278 -0
  1022. package/vendor/ars/shared/mode_spectrum.md +57 -0
  1023. package/vendor/ars/shared/model_tiering.md +83 -0
  1024. package/vendor/ars/shared/policy_data/nature_policy.md +56 -0
  1025. package/vendor/ars/shared/prisma_trAIce_protocol.md +157 -0
  1026. package/vendor/ars/shared/raise_framework.md +129 -0
  1027. package/vendor/ars/shared/references/authority_content_coverage_advisory_protocol.md +275 -0
  1028. package/vendor/ars/shared/references/claim_standing_candidate_ledger_protocol.md +66 -0
  1029. package/vendor/ars/shared/references/claim_strength_ladder.md +93 -0
  1030. package/vendor/ars/shared/references/cross_document_consistency_advisory_protocol.md +263 -0
  1031. package/vendor/ars/shared/references/evidence_row_protocol.md +260 -0
  1032. package/vendor/ars/shared/references/firm_rules.md +90 -0
  1033. package/vendor/ars/shared/references/human_subjects_authority_protocol.md +274 -0
  1034. package/vendor/ars/shared/references/intent_clarification_protocol.md +168 -0
  1035. package/vendor/ars/shared/references/irb_terminology_glossary.md +229 -0
  1036. package/vendor/ars/shared/references/protected_hedging_phrases.md +118 -0
  1037. package/vendor/ars/shared/references/psychometric_terminology_glossary.md +109 -0
  1038. package/vendor/ars/shared/references/review_criteria_consumer_protocol.md +238 -0
  1039. package/vendor/ars/shared/references/review_pathway_rule_trace_protocol.md +166 -0
  1040. package/vendor/ars/shared/references/submission_packet_manifest_protocol.md +292 -0
  1041. package/vendor/ars/shared/references/word_count_conventions.md +124 -0
  1042. package/vendor/ars/shared/research_workflow_profiles/field_general.json +1 -0
  1043. package/vendor/ars/shared/review_criteria_registry.json +207 -0
  1044. package/vendor/ars/shared/review_criteria_sources/msr-2027-technical-papers.2026-08-24.json +46 -0
  1045. package/vendor/ars/shared/review_criteria_sources/sigsoft-empirical-standards.2026-08-24.json +31 -0
  1046. package/vendor/ars/shared/sprint_contract.schema.json +482 -0
  1047. package/vendor/ars/shared/style_calibration_protocol.md +151 -0
  1048. package/vendor/ars/shared/templates/codex_audit_multifile_template.md +263 -0
  1049. package/vendor/ars/tools/release-discipline/.toolkit-version +1 -0
  1050. package/vendor/ars/tools/release-discipline/README.md +4 -0
  1051. package/vendor/ars/tools/release-discipline/scripts/_release_doc_alignment_schema.py +1011 -0
  1052. package/vendor/ars/tools/release-discipline/scripts/check_command_invariants.py +497 -0
  1053. package/vendor/ars/tools/release-discipline/scripts/check_release_doc_alignment.py +263 -0
  1054. package/vendor/ars/tools/release-discipline/scripts/sync-toolkit.sh +147 -0
  1055. package/vendor/windows/NOTICE.md +12 -0
  1056. package/vendor/windows/arm64/fd.exe +0 -0
  1057. package/vendor/windows/arm64/licenses/fd/LICENSE-APACHE +201 -0
  1058. package/vendor/windows/arm64/licenses/fd/LICENSE-MIT +21 -0
  1059. package/vendor/windows/arm64/licenses/ripgrep/COPYING +3 -0
  1060. package/vendor/windows/arm64/licenses/ripgrep/LICENSE-MIT +21 -0
  1061. package/vendor/windows/arm64/licenses/ripgrep/UNLICENSE +24 -0
  1062. package/vendor/windows/arm64/rg.exe +0 -0
  1063. package/vendor/windows/x64/fd.exe +0 -0
  1064. package/vendor/windows/x64/licenses/fd/LICENSE-APACHE +201 -0
  1065. package/vendor/windows/x64/licenses/fd/LICENSE-MIT +21 -0
  1066. package/vendor/windows/x64/licenses/ripgrep/COPYING +3 -0
  1067. package/vendor/windows/x64/licenses/ripgrep/LICENSE-MIT +21 -0
  1068. package/vendor/windows/x64/licenses/ripgrep/UNLICENSE +24 -0
  1069. package/vendor/windows/x64/rg.exe +0 -0
@@ -0,0 +1,2540 @@
1
+ #!/usr/bin/env python3
2
+ """Validate, replay, and durably publish inquiry-branch-ledger/1.0 (#743).
3
+
4
+ The ledger is an opt-in, event-sourced user-project artifact. This module is
5
+ deliberately manuscript-blind and model-free: callers supply exact profile
6
+ documents, timestamps, event payloads, workspace paths, and project identity.
7
+ It never infers a research family, branch, condition, or author decision.
8
+
9
+ Runtime interpretations needed to make the frozen design executable:
10
+
11
+ * profile bindings are resolved only through an explicit catalog of fully
12
+ validated research-workflow-profile/1.0 documents. A digest is not a
13
+ branch budget, and the current shipped fallback is never substituted for an
14
+ unresolved historical binding;
15
+ * a non-null parent_id must already exist when a branch is introduced;
16
+ * downstream_refs are identifier lists, so duplicates are rejected. This
17
+ makes "one stale event per identifier" and merge/supersession order unique;
18
+ * a newly emitted reopen-condition signal must name a condition still present
19
+ on a non-terminal branch. Removed condition ids remain retired and can be
20
+ resolved in historical events, but cannot motivate a new event;
21
+ * a branch_reopened event is followed immediately, in current downstream-ref
22
+ order, by exactly one system artifact_marked_stale event per identifier.
23
+ A missing, delayed, reordered, or orphan system event fails replay;
24
+ * repeated supersession resolutions for distinct stale causes of the same
25
+ artifact must name the same replacement. Replacing an identifier with
26
+ itself is not a replacement;
27
+ * persisted publication requires at least two introduced branches, matching
28
+ the frozen rule that the ledger/pointer do not materialize on the simple
29
+ zero-or-one-branch path. In-memory replay remains available before that;
30
+ * ledger_path is an explicitly workspace-relative POSIX path and the ledger
31
+ must be beside the passport. No path is resolved relative to the process
32
+ working directory;
33
+ * two stable filesystem paths cannot be renamed atomically together. A
34
+ shared passport-domain sidecar lock, full-byte compare-and-swap checks, and
35
+ a durable recovery journal make the two-file update atomic for cooperating
36
+ ARS readers. Without a valid recoverable journal, an unreadable/mismatched
37
+ pointer is surfaced as
38
+ LEDGER-BINDING-BROKEN and is never silently continued.
39
+
40
+ The hash chain is tamper-evident relative to the separately trusted passport
41
+ pointer. It is not authentication: a party able to rewrite both artifacts
42
+ can recompute both hashes, just as the design's within-session author labels
43
+ do not authenticate a human identity.
44
+ """
45
+ from __future__ import annotations
46
+
47
+ import argparse
48
+ import copy
49
+ import hashlib
50
+ import io
51
+ import json
52
+ import math
53
+ import os
54
+ import re
55
+ import stat
56
+ import sys
57
+ import time
58
+ import unicodedata
59
+ import uuid
60
+ from contextlib import contextmanager
61
+ from datetime import datetime
62
+ from pathlib import Path, PurePosixPath
63
+ from typing import Any, Callable, Iterable, Iterator, Mapping, NoReturn, Sequence
64
+
65
+ try: # POSIX is the supported durable-publication platform for this alpha.
66
+ import fcntl
67
+ except ImportError: # pragma: no cover - exercised only on non-POSIX hosts
68
+ fcntl = None # type: ignore[assignment]
69
+
70
+ try: # Dual-path import: script invocation vs package import under pytest.
71
+ from research_workflow_profile import (
72
+ JCS_SAFE_INTEGER_MAX,
73
+ ContractError as ProfileContractError,
74
+ canonical_bytes as _profile_canonical_bytes,
75
+ load_profile,
76
+ profile_binding,
77
+ validate_profile,
78
+ )
79
+ except ImportError: # pragma: no cover - package-import path
80
+ from scripts.research_workflow_profile import (
81
+ JCS_SAFE_INTEGER_MAX,
82
+ ContractError as ProfileContractError,
83
+ canonical_bytes as _profile_canonical_bytes,
84
+ load_profile,
85
+ profile_binding,
86
+ validate_profile,
87
+ )
88
+
89
+
90
+ LEDGER_SCHEMA_VERSION = "inquiry-branch-ledger/1.0"
91
+ POINTER_LEDGER_VERSION = LEDGER_SCHEMA_VERSION
92
+ TRANSACTION_SCHEMA_VERSION = "inquiry-ledger-transaction/1.0"
93
+ ZERO_SHA256 = "0" * 64
94
+ ENV_FLAG = "ARS_INQUIRY_LEDGER"
95
+
96
+ EVENT_KINDS = (
97
+ "branch_created",
98
+ "facet_surfaced",
99
+ "branch_adopted",
100
+ "branch_annotated",
101
+ "branch_parked",
102
+ "branch_rejected",
103
+ "branch_reopened",
104
+ "branch_merged",
105
+ "branch_archived",
106
+ "reopen_condition_signal",
107
+ "profile_rebound",
108
+ "artifact_marked_stale",
109
+ "artifact_reconfirmed",
110
+ "artifact_superseded",
111
+ )
112
+ AI_EVENT_KINDS = frozenset({"facet_surfaced", "reopen_condition_signal"})
113
+ SYSTEM_EVENT_KINDS = frozenset({"artifact_marked_stale"})
114
+ ARTIFACT_EVENT_KINDS = frozenset(
115
+ {"artifact_marked_stale", "artifact_reconfirmed", "artifact_superseded"}
116
+ )
117
+ NULL_BRANCH_EVENT_KINDS = ARTIFACT_EVENT_KINDS | {"profile_rebound"}
118
+ LIVE_STATUSES = frozenset({"active", "reopened"})
119
+ TERMINAL_STATUSES = frozenset({"merged", "archived"})
120
+ BRANCH_STATUSES = (
121
+ "active",
122
+ "parked",
123
+ "rejected",
124
+ "reopened",
125
+ "merged",
126
+ "archived",
127
+ )
128
+ AUTHOR_OWNED_PROVENANCE = frozenset({"author_originated", "author_adopted"})
129
+ AUTHOR_REOPENABLE_STATUSES = frozenset({"parked", "rejected"})
130
+ PROVENANCE_VALUES = (
131
+ "author_originated",
132
+ "ai_surfaced_facet",
133
+ "author_adopted",
134
+ )
135
+ ANNOTATION_FIELDS = (
136
+ "assumptions",
137
+ "evidence_sought",
138
+ "reopen_conditions",
139
+ "downstream_refs",
140
+ )
141
+ SUMMARY_MOMENTS = (
142
+ "design_freeze",
143
+ "stage_2_5",
144
+ "stage_4_5",
145
+ "reopen_condition_signal",
146
+ )
147
+ _SUMMARY_TEXT_LIMIT = 160
148
+ _MARKDOWN_META = frozenset("\\`*_[]<>|")
149
+
150
+ _LEDGER_FIELDS = {
151
+ "schema_version",
152
+ "project_ref",
153
+ "initial_profile_binding",
154
+ "events",
155
+ }
156
+ _EVENT_FIELDS = {
157
+ "event_id",
158
+ "recorded_at",
159
+ "actor",
160
+ "kind",
161
+ "branch_id",
162
+ "payload",
163
+ "prev_event_sha256",
164
+ }
165
+ _EVENT_INPUT_FIELDS = {
166
+ "recorded_at",
167
+ "actor",
168
+ "kind",
169
+ "branch_id",
170
+ "payload",
171
+ }
172
+ _BINDING_FIELDS = {"profile_id", "profile_version", "content_sha256"}
173
+ _POINTER_FIELDS = {"ledger_path", "ledger_version", "content_sha256"}
174
+
175
+ _SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
176
+ _SEMVER_RE = re.compile(
177
+ r"^(0|[1-9][0-9]*)\."
178
+ r"(0|[1-9][0-9]*)\."
179
+ r"(0|[1-9][0-9]*)"
180
+ r"(?:-((?:0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)"
181
+ r"(?:\.(?:0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))*))?"
182
+ r"(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$"
183
+ )
184
+ _SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
185
+ _RFC3339_RE = re.compile(
186
+ r"^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt]"
187
+ r"(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]"
188
+ r"(?:\.[0-9]+)?(?:[Zz]|[+-](?:[01][0-9]|2[0-3]):[0-5][0-9])$"
189
+ )
190
+ _FORBIDDEN_PATH_CHARS_RE = re.compile(
191
+ "[\\x00-\\x1f\\x7f-\\x9f"
192
+ "\u00ad\u0600-\u0605\u061c\u06dd\u070f\u0890-\u0891\u08e2"
193
+ "\u180e\u200b-\u200f\u202a-\u202e\u2060-\u2064\u2066-\u206f"
194
+ "\ufeff\ufff9-\ufffb]"
195
+ )
196
+
197
+
198
+ class ContractError(ValueError):
199
+ """Raised when ledger input or replay violates the frozen contract."""
200
+
201
+
202
+ class LedgerBindingError(ContractError):
203
+ """Raised for the design's visible LEDGER-BINDING-BROKEN load state."""
204
+
205
+ def __init__(self, message: str):
206
+ super().__init__(f"LEDGER-BINDING-BROKEN: {message}")
207
+
208
+
209
+ class TransactionRecoveryError(ContractError):
210
+ """Raised when a durable journal exists but cannot be safely recovered."""
211
+
212
+ def __init__(self, message: str):
213
+ super().__init__(f"LEDGER-TRANSACTION-RECOVERY-REQUIRED: {message}")
214
+
215
+
216
+ def _fail(path: str, message: str) -> NoReturn:
217
+ raise ContractError(f"{path}: {message}")
218
+
219
+
220
+ def canonical_bytes(value: Any) -> bytes:
221
+ """Restricted JCS bytes shared byte-for-byte with the #742 runtime."""
222
+
223
+ try:
224
+ return _profile_canonical_bytes(value)
225
+ except ProfileContractError as exc:
226
+ raise ContractError(str(exc)) from exc
227
+
228
+
229
+ def _reject_constant(value: str) -> NoReturn:
230
+ raise ContractError(f"non-finite JSON number is not allowed: {value}")
231
+
232
+
233
+ def _reject_duplicate_pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
234
+ result: dict[str, Any] = {}
235
+ for key, value in pairs:
236
+ if key in result:
237
+ raise ContractError(f"duplicate JSON key is not allowed: {key}")
238
+ result[key] = value
239
+ return result
240
+
241
+
242
+ def _parse_json_bytes(raw: bytes, *, source: str) -> dict[str, Any]:
243
+ try:
244
+ text = raw.decode("utf-8")
245
+ except UnicodeDecodeError as exc:
246
+ raise ContractError(f"JSON input is not UTF-8: {source}") from exc
247
+ try:
248
+ value = json.loads(
249
+ text,
250
+ object_pairs_hook=_reject_duplicate_pairs,
251
+ parse_constant=_reject_constant,
252
+ )
253
+ except (json.JSONDecodeError, ContractError) as exc:
254
+ raise ContractError(f"cannot parse JSON input {source}: {exc}") from exc
255
+ if not isinstance(value, dict):
256
+ raise ContractError(f"top-level JSON value must be an object: {source}")
257
+ return value
258
+
259
+
260
+ def load_ledger(path: Path | str, *, require_canonical: bool = True) -> dict[str, Any]:
261
+ """Load a ledger with duplicate-key rejection and exact canonical storage."""
262
+
263
+ ledger_path = Path(path)
264
+ try:
265
+ raw = ledger_path.read_bytes()
266
+ except OSError as exc:
267
+ raise ContractError(f"cannot read ledger {ledger_path}: {exc}") from exc
268
+ value = _parse_json_bytes(raw, source=str(ledger_path))
269
+ if require_canonical and raw != canonical_bytes(value):
270
+ raise ContractError(
271
+ f"ledger file must be stored as exact JSON Canonical Form bytes: {ledger_path}"
272
+ )
273
+ return value
274
+
275
+
276
+ def _object(
277
+ value: Any,
278
+ path: str,
279
+ required: set[str],
280
+ optional: set[str] | None = None,
281
+ ) -> Mapping[str, Any]:
282
+ if not isinstance(value, Mapping):
283
+ _fail(path, "must be an object")
284
+ optional = optional or set()
285
+ keys = set(value)
286
+ missing = required - keys
287
+ extra = keys - required - optional
288
+ if missing:
289
+ _fail(path, f"missing field(s): {', '.join(sorted(missing))}")
290
+ if extra:
291
+ _fail(path, f"undeclared field(s): {', '.join(sorted(extra))}")
292
+ return value
293
+
294
+
295
+ def _text(value: Any, path: str) -> str:
296
+ if not isinstance(value, str) or not value.strip():
297
+ _fail(path, "must be a non-empty string")
298
+ return value
299
+
300
+
301
+ def _slug(value: Any, path: str) -> str:
302
+ text = _text(value, path)
303
+ if _SLUG_RE.fullmatch(text) is None:
304
+ _fail(path, "must be a lowercase slug using letters, digits, '_' or '-'")
305
+ return text
306
+
307
+
308
+ def _semver(value: Any, path: str) -> str:
309
+ text = _text(value, path)
310
+ if _SEMVER_RE.fullmatch(text) is None:
311
+ _fail(path, "must be a SemVer 2.0.0 version")
312
+ return text
313
+
314
+
315
+ def _sha256(value: Any, path: str) -> str:
316
+ text = _text(value, path)
317
+ if _SHA256_RE.fullmatch(text) is None:
318
+ _fail(path, "must be a lowercase 64-hex SHA-256 digest")
319
+ return text
320
+
321
+
322
+ def _positive_int(value: Any, path: str) -> int:
323
+ if (
324
+ isinstance(value, bool)
325
+ or not isinstance(value, int)
326
+ or value < 1
327
+ or value > JCS_SAFE_INTEGER_MAX
328
+ ):
329
+ _fail(path, f"must be an integer from 1 through {JCS_SAFE_INTEGER_MAX}")
330
+ return value
331
+
332
+
333
+ def _nonnegative_int(value: Any, path: str) -> int:
334
+ if (
335
+ isinstance(value, bool)
336
+ or not isinstance(value, int)
337
+ or value < 0
338
+ or value > JCS_SAFE_INTEGER_MAX
339
+ ):
340
+ _fail(path, f"must be an integer from 0 through {JCS_SAFE_INTEGER_MAX}")
341
+ return value
342
+
343
+
344
+ def _rfc3339(value: Any, path: str) -> str:
345
+ text = _text(value, path)
346
+ if _RFC3339_RE.fullmatch(text) is None:
347
+ _fail(path, "must be an ISO 8601 date-time with offset")
348
+ normalized = text[:10] + "T" + text[11:]
349
+ if normalized.endswith(("Z", "z")):
350
+ normalized = normalized[:-1] + "+00:00"
351
+ try:
352
+ parsed = datetime.fromisoformat(normalized)
353
+ except ValueError as exc:
354
+ raise ContractError(f"{path}: must be an ISO 8601 date-time with offset") from exc
355
+ if parsed.tzinfo is None or parsed.utcoffset() is None:
356
+ _fail(path, "must be an ISO 8601 date-time with offset")
357
+ return text
358
+
359
+
360
+ def _enum(value: Any, path: str, choices: Sequence[str]) -> str:
361
+ if value not in choices:
362
+ _fail(path, f"must be one of: {', '.join(choices)}")
363
+ return value
364
+
365
+
366
+ def _nullable_slug(value: Any, path: str) -> str | None:
367
+ if value is None:
368
+ return None
369
+ return _slug(value, path)
370
+
371
+
372
+ def _text_list(
373
+ value: Any, path: str, *, unique: bool = False
374
+ ) -> list[str]:
375
+ if not isinstance(value, list):
376
+ _fail(path, "must be an array")
377
+ result = [_text(item, f"{path}[{index}]") for index, item in enumerate(value)]
378
+ if unique and len(set(result)) != len(result):
379
+ _fail(path, "must not contain duplicate identifiers")
380
+ return result
381
+
382
+
383
+ def _validate_binding(value: Any, path: str) -> dict[str, str]:
384
+ binding = _object(value, path, _BINDING_FIELDS)
385
+ return {
386
+ "profile_id": _slug(binding["profile_id"], f"{path}.profile_id"),
387
+ "profile_version": _semver(
388
+ binding["profile_version"], f"{path}.profile_version"
389
+ ),
390
+ "content_sha256": _sha256(
391
+ binding["content_sha256"], f"{path}.content_sha256"
392
+ ),
393
+ }
394
+
395
+
396
+ def _validate_conditions(value: Any, path: str) -> list[dict[str, str]]:
397
+ if not isinstance(value, list):
398
+ _fail(path, "must be an array")
399
+ result: list[dict[str, str]] = []
400
+ seen: set[str] = set()
401
+ for index, raw in enumerate(value):
402
+ item_path = f"{path}[{index}]"
403
+ condition = _object(
404
+ raw,
405
+ item_path,
406
+ {"condition_id", "statement"},
407
+ {"evidence_pointer"},
408
+ )
409
+ condition_id = _text(condition["condition_id"], f"{item_path}.condition_id")
410
+ if condition_id in seen:
411
+ _fail(path, f"duplicate condition_id: {condition_id}")
412
+ seen.add(condition_id)
413
+ normalized = {
414
+ "condition_id": condition_id,
415
+ "statement": _text(condition["statement"], f"{item_path}.statement"),
416
+ }
417
+ if "evidence_pointer" in condition:
418
+ normalized["evidence_pointer"] = _text(
419
+ condition["evidence_pointer"], f"{item_path}.evidence_pointer"
420
+ )
421
+ result.append(normalized)
422
+ return result
423
+
424
+
425
+ def _validate_event_shape(raw_event: Any, path: str) -> None:
426
+ event = _object(raw_event, path, _EVENT_FIELDS)
427
+ _positive_int(event["event_id"], f"{path}.event_id")
428
+ _rfc3339(event["recorded_at"], f"{path}.recorded_at")
429
+ actor = _enum(event["actor"], f"{path}.actor", ("author", "ai", "system"))
430
+ kind = _enum(event["kind"], f"{path}.kind", EVENT_KINDS)
431
+ _sha256(event["prev_event_sha256"], f"{path}.prev_event_sha256")
432
+
433
+ expected_actor = "ai" if kind in AI_EVENT_KINDS else (
434
+ "system" if kind in SYSTEM_EVENT_KINDS else "author"
435
+ )
436
+ if actor != expected_actor:
437
+ _fail(f"{path}.actor", f"must equal {expected_actor} for {kind}")
438
+
439
+ if kind in NULL_BRANCH_EVENT_KINDS:
440
+ if event["branch_id"] is not None:
441
+ _fail(f"{path}.branch_id", f"must be null for {kind}")
442
+ else:
443
+ _slug(event["branch_id"], f"{path}.branch_id")
444
+
445
+ payload_path = f"{path}.payload"
446
+ payload = event["payload"]
447
+ if kind == "branch_created":
448
+ value = _object(
449
+ payload,
450
+ payload_path,
451
+ {
452
+ "parent_id",
453
+ "statement",
454
+ "assumptions",
455
+ "evidence_sought",
456
+ "reopen_conditions",
457
+ "downstream_refs",
458
+ },
459
+ )
460
+ _nullable_slug(value["parent_id"], f"{payload_path}.parent_id")
461
+ _text(value["statement"], f"{payload_path}.statement")
462
+ _text_list(value["assumptions"], f"{payload_path}.assumptions")
463
+ _text_list(value["evidence_sought"], f"{payload_path}.evidence_sought")
464
+ _validate_conditions(
465
+ value["reopen_conditions"], f"{payload_path}.reopen_conditions"
466
+ )
467
+ _text_list(
468
+ value["downstream_refs"],
469
+ f"{payload_path}.downstream_refs",
470
+ unique=True,
471
+ )
472
+ elif kind == "facet_surfaced":
473
+ value = _object(payload, payload_path, {"parent_id", "surfaced_text"})
474
+ _nullable_slug(value["parent_id"], f"{payload_path}.parent_id")
475
+ _text(value["surfaced_text"], f"{payload_path}.surfaced_text")
476
+ elif kind == "branch_adopted":
477
+ value = _object(
478
+ payload,
479
+ payload_path,
480
+ {"source_event_id", "surfaced_text", "author_formulation"},
481
+ )
482
+ _positive_int(value["source_event_id"], f"{payload_path}.source_event_id")
483
+ surfaced = _text(value["surfaced_text"], f"{payload_path}.surfaced_text")
484
+ formulation = _text(
485
+ value["author_formulation"], f"{payload_path}.author_formulation"
486
+ )
487
+ if formulation == surfaced:
488
+ _fail(
489
+ f"{payload_path}.author_formulation",
490
+ "must not be byte-identical to surfaced_text",
491
+ )
492
+ if formulation.strip().casefold() == "ok":
493
+ _fail(
494
+ f"{payload_path}.author_formulation",
495
+ "a bare 'ok' is not an adoption formulation",
496
+ )
497
+ elif kind == "branch_annotated":
498
+ value = _object(payload, payload_path, {"field", "value"})
499
+ field = _enum(value["field"], f"{payload_path}.field", ANNOTATION_FIELDS)
500
+ if field == "reopen_conditions":
501
+ _validate_conditions(value["value"], f"{payload_path}.value")
502
+ else:
503
+ _text_list(
504
+ value["value"],
505
+ f"{payload_path}.value",
506
+ unique=field == "downstream_refs",
507
+ )
508
+ elif kind in {"branch_parked", "branch_rejected", "branch_archived"}:
509
+ value = _object(payload, payload_path, {"reason"})
510
+ _text(value["reason"], f"{payload_path}.reason")
511
+ elif kind == "branch_reopened":
512
+ value = _object(
513
+ payload,
514
+ payload_path,
515
+ {"reason"},
516
+ {"condition_id", "evidence_pointer"},
517
+ )
518
+ _text(value["reason"], f"{payload_path}.reason")
519
+ if "condition_id" in value:
520
+ _text(value["condition_id"], f"{payload_path}.condition_id")
521
+ if "evidence_pointer" in value:
522
+ _text(value["evidence_pointer"], f"{payload_path}.evidence_pointer")
523
+ elif kind == "branch_merged":
524
+ value = _object(payload, payload_path, {"merged_into", "reason"})
525
+ _slug(value["merged_into"], f"{payload_path}.merged_into")
526
+ _text(value["reason"], f"{payload_path}.reason")
527
+ elif kind == "reopen_condition_signal":
528
+ value = _object(
529
+ payload,
530
+ payload_path,
531
+ {"branch_id_ref", "condition_id", "evidence_pointer"},
532
+ )
533
+ _slug(value["branch_id_ref"], f"{payload_path}.branch_id_ref")
534
+ _text(value["condition_id"], f"{payload_path}.condition_id")
535
+ _text(value["evidence_pointer"], f"{payload_path}.evidence_pointer")
536
+ if value["branch_id_ref"] != event["branch_id"]:
537
+ _fail(
538
+ f"{payload_path}.branch_id_ref",
539
+ "must equal the event-level branch_id",
540
+ )
541
+ elif kind == "profile_rebound":
542
+ value = _object(
543
+ payload,
544
+ payload_path,
545
+ _BINDING_FIELDS | {"selection_receipt_ref"},
546
+ )
547
+ _validate_binding(
548
+ {key: value[key] for key in _BINDING_FIELDS},
549
+ payload_path,
550
+ )
551
+ _text(value["selection_receipt_ref"], f"{payload_path}.selection_receipt_ref")
552
+ elif kind == "artifact_marked_stale":
553
+ value = _object(
554
+ payload, payload_path, {"artifact_ref", "reopening_event_id"}
555
+ )
556
+ _text(value["artifact_ref"], f"{payload_path}.artifact_ref")
557
+ _positive_int(
558
+ value["reopening_event_id"], f"{payload_path}.reopening_event_id"
559
+ )
560
+ elif kind == "artifact_reconfirmed":
561
+ value = _object(
562
+ payload,
563
+ payload_path,
564
+ {"artifact_ref", "resolves_stale_event_id", "note"},
565
+ )
566
+ _text(value["artifact_ref"], f"{payload_path}.artifact_ref")
567
+ _positive_int(
568
+ value["resolves_stale_event_id"],
569
+ f"{payload_path}.resolves_stale_event_id",
570
+ )
571
+ _text(value["note"], f"{payload_path}.note")
572
+ elif kind == "artifact_superseded":
573
+ value = _object(
574
+ payload,
575
+ payload_path,
576
+ {"artifact_ref", "resolves_stale_event_id", "note", "replaced_by"},
577
+ )
578
+ artifact_ref = _text(value["artifact_ref"], f"{payload_path}.artifact_ref")
579
+ _positive_int(
580
+ value["resolves_stale_event_id"],
581
+ f"{payload_path}.resolves_stale_event_id",
582
+ )
583
+ _text(value["note"], f"{payload_path}.note")
584
+ replaced_by = _text(value["replaced_by"], f"{payload_path}.replaced_by")
585
+ if replaced_by == artifact_ref:
586
+ _fail(f"{payload_path}.replaced_by", "must differ from artifact_ref")
587
+
588
+
589
+ def validate_ledger_shape(ledger: Mapping[str, Any]) -> None:
590
+ """Validate only the closed document/event shapes (not replay semantics)."""
591
+
592
+ value = _object(ledger, "ledger", _LEDGER_FIELDS)
593
+ if value["schema_version"] != LEDGER_SCHEMA_VERSION:
594
+ _fail("ledger.schema_version", f"must equal {LEDGER_SCHEMA_VERSION}")
595
+ _text(value["project_ref"], "ledger.project_ref")
596
+ _validate_binding(value["initial_profile_binding"], "ledger.initial_profile_binding")
597
+ events = value["events"]
598
+ if not isinstance(events, list):
599
+ _fail("ledger.events", "must be an array")
600
+ for index, event in enumerate(events):
601
+ _validate_event_shape(event, f"ledger.events[{index}]")
602
+
603
+
604
+ def _binding_key(binding: Mapping[str, Any]) -> tuple[str, str, str]:
605
+ return (
606
+ str(binding["profile_id"]),
607
+ str(binding["profile_version"]),
608
+ str(binding["content_sha256"]),
609
+ )
610
+
611
+
612
+ def build_profile_catalog(
613
+ profiles: Iterable[Mapping[str, Any]] | Mapping[Any, Mapping[str, Any]],
614
+ ) -> dict[tuple[str, str, str], Mapping[str, Any]]:
615
+ """Build an exact-binding catalog; never supplies an implicit fallback."""
616
+
617
+ if isinstance(profiles, Mapping) and "schema_version" in profiles:
618
+ candidates: Iterable[Mapping[str, Any]] = [profiles]
619
+ elif isinstance(profiles, Mapping):
620
+ candidates = profiles.values()
621
+ else:
622
+ candidates = profiles
623
+ catalog: dict[tuple[str, str, str], Mapping[str, Any]] = {}
624
+ for index, profile in enumerate(candidates):
625
+ try:
626
+ validate_profile(profile)
627
+ binding = profile_binding(profile)
628
+ except (ProfileContractError, TypeError, ValueError) as exc:
629
+ raise ContractError(f"profiles[{index}]: invalid profile: {exc}") from exc
630
+ key = _binding_key(binding)
631
+ if key in catalog and canonical_bytes(catalog[key]) != canonical_bytes(profile):
632
+ _fail(f"profiles[{index}]", "duplicate binding has different content")
633
+ catalog[key] = profile
634
+ if not catalog:
635
+ _fail("profiles", "must explicitly supply at least one exact profile document")
636
+ return catalog
637
+
638
+
639
+ def load_profile_catalog(
640
+ paths: Sequence[Path | str],
641
+ ) -> dict[tuple[str, str, str], Mapping[str, Any]]:
642
+ if not paths:
643
+ _fail("profiles", "at least one --profile path is required")
644
+ loaded: list[Mapping[str, Any]] = []
645
+ for raw_path in paths:
646
+ try:
647
+ loaded.append(load_profile(Path(raw_path)))
648
+ except (ProfileContractError, OSError) as exc:
649
+ raise ContractError(f"cannot load profile {raw_path}: {exc}") from exc
650
+ return build_profile_catalog(loaded)
651
+
652
+
653
+ def _resolve_profile(
654
+ binding: Mapping[str, Any],
655
+ catalog: Mapping[tuple[str, str, str], Mapping[str, Any]],
656
+ path: str,
657
+ ) -> Mapping[str, Any]:
658
+ key = _binding_key(binding)
659
+ profile = catalog.get(key)
660
+ if profile is None:
661
+ _fail(
662
+ path,
663
+ "binding is unresolved in the explicit profile catalog "
664
+ f"({key[0]}@{key[1]} {key[2]})",
665
+ )
666
+ return profile
667
+
668
+
669
+ def _is_terminal(branch: Mapping[str, Any]) -> bool:
670
+ if branch["status"] in TERMINAL_STATUSES:
671
+ return True
672
+ return (
673
+ branch["status"] == "rejected"
674
+ and branch["provenance"] == "ai_surfaced_facet"
675
+ )
676
+
677
+
678
+ def _require_branch(
679
+ branches: Mapping[str, dict[str, Any]], branch_id: Any, path: str
680
+ ) -> dict[str, Any]:
681
+ branch = branches.get(str(branch_id))
682
+ if branch is None:
683
+ _fail(path, f"unknown branch_id: {branch_id}")
684
+ return branch
685
+
686
+
687
+ def _condition_map(branch: Mapping[str, Any]) -> dict[str, Mapping[str, Any]]:
688
+ return {
689
+ condition["condition_id"]: condition
690
+ for condition in branch["reopen_conditions"]
691
+ }
692
+
693
+
694
+ def _dedupe_ordered(values: Iterable[str]) -> list[str]:
695
+ return list(dict.fromkeys(values))
696
+
697
+
698
+ def _public_branch(branch: Mapping[str, Any]) -> dict[str, Any]:
699
+ return {
700
+ key: copy.deepcopy(value)
701
+ for key, value in branch.items()
702
+ if not key.startswith("_")
703
+ }
704
+
705
+
706
+ def replay_ledger(
707
+ ledger: Mapping[str, Any],
708
+ profiles: Iterable[Mapping[str, Any]] | Mapping[Any, Mapping[str, Any]],
709
+ *,
710
+ expected_project_ref: str | None = None,
711
+ require_materialized: bool = False,
712
+ ) -> dict[str, Any]:
713
+ """Purely replay a ledger and return its deterministic projection.
714
+
715
+ No file, environment, clock, model, or network state is consulted.
716
+ """
717
+
718
+ validate_ledger_shape(ledger)
719
+ if expected_project_ref is not None:
720
+ _text(expected_project_ref, "expected_project_ref")
721
+ if ledger["project_ref"] != expected_project_ref:
722
+ _fail(
723
+ "ledger.project_ref",
724
+ f"does not equal expected project_ref {expected_project_ref!r}",
725
+ )
726
+ catalog = build_profile_catalog(profiles)
727
+ initial_binding = _validate_binding(
728
+ ledger["initial_profile_binding"], "ledger.initial_profile_binding"
729
+ )
730
+ current_profile = _resolve_profile(
731
+ initial_binding, catalog, "ledger.initial_profile_binding"
732
+ )
733
+ effective_binding = copy.deepcopy(initial_binding)
734
+
735
+ branches: dict[str, dict[str, Any]] = {}
736
+ artifacts: dict[str, dict[str, Any]] = {}
737
+ stale_causes: dict[int, dict[str, Any]] = {}
738
+ signals: list[dict[str, Any]] = []
739
+ supersession_targets: dict[str, str] = {}
740
+ expected_stale: list[tuple[str, int]] = []
741
+ introduced_count = 0
742
+
743
+ events = ledger["events"]
744
+ for index, raw_event in enumerate(events):
745
+ event = raw_event
746
+ path = f"ledger.events[{index}]"
747
+ expected_id = index + 1
748
+ if event["event_id"] != expected_id:
749
+ _fail(f"{path}.event_id", f"must equal dense event id {expected_id}")
750
+ expected_prev = (
751
+ ZERO_SHA256
752
+ if index == 0
753
+ else hashlib.sha256(canonical_bytes(events[index - 1])).hexdigest()
754
+ )
755
+ if event["prev_event_sha256"] != expected_prev:
756
+ _fail(
757
+ f"{path}.prev_event_sha256",
758
+ "does not equal the canonical SHA-256 of the previous event",
759
+ )
760
+
761
+ kind = event["kind"]
762
+ branch_id = event["branch_id"]
763
+ payload = event["payload"]
764
+
765
+ if expected_stale:
766
+ expected_artifact, reopening_id = expected_stale.pop(0)
767
+ if kind != "artifact_marked_stale":
768
+ _fail(
769
+ path,
770
+ "must be the contiguous artifact_marked_stale event "
771
+ f"for {expected_artifact!r} after reopening event {reopening_id}",
772
+ )
773
+ if payload["artifact_ref"] != expected_artifact:
774
+ _fail(
775
+ f"{path}.payload.artifact_ref",
776
+ f"must equal {expected_artifact!r} in downstream_refs order",
777
+ )
778
+ if payload["reopening_event_id"] != reopening_id:
779
+ _fail(
780
+ f"{path}.payload.reopening_event_id",
781
+ f"must equal reopening event {reopening_id}",
782
+ )
783
+ artifact = artifacts.setdefault(
784
+ expected_artifact,
785
+ {
786
+ "artifact_ref": expected_artifact,
787
+ "outstanding_stale_causes": [],
788
+ "resolution_history": [],
789
+ },
790
+ )
791
+ artifact["outstanding_stale_causes"].append(event["event_id"])
792
+ stale_causes[event["event_id"]] = {
793
+ "artifact_ref": expected_artifact,
794
+ "reopening_event_id": reopening_id,
795
+ }
796
+ elif kind == "artifact_marked_stale":
797
+ _fail(path, "orphan artifact_marked_stale event")
798
+ elif kind in {"branch_created", "facet_surfaced"}:
799
+ assert isinstance(branch_id, str)
800
+ if branch_id in branches:
801
+ _fail(f"{path}.branch_id", "branch ids are stable and cannot be reused")
802
+ parent_id = payload["parent_id"]
803
+ if parent_id is not None and parent_id not in branches:
804
+ _fail(
805
+ f"{path}.payload.parent_id",
806
+ "must name a branch introduced by an earlier event",
807
+ )
808
+ introduced_count += 1
809
+ if kind == "branch_created":
810
+ conditions = copy.deepcopy(payload["reopen_conditions"])
811
+ branches[branch_id] = {
812
+ "branch_id": branch_id,
813
+ "parent_id": parent_id,
814
+ "provenance": "author_originated",
815
+ "statement": payload["statement"],
816
+ "surfaced_text": None,
817
+ "adoption_receipt": None,
818
+ "assumptions": copy.deepcopy(payload["assumptions"]),
819
+ "evidence_sought": copy.deepcopy(payload["evidence_sought"]),
820
+ "status": "active",
821
+ "disposition_reason": None,
822
+ "reopen_conditions": conditions,
823
+ "downstream_refs": copy.deepcopy(payload["downstream_refs"]),
824
+ "merged_into": None,
825
+ "_origin_event_id": event["event_id"],
826
+ "_condition_history": {
827
+ item["condition_id"]: item["statement"] for item in conditions
828
+ },
829
+ "_retired_condition_ids": set(),
830
+ }
831
+ else:
832
+ branches[branch_id] = {
833
+ "branch_id": branch_id,
834
+ "parent_id": parent_id,
835
+ "provenance": "ai_surfaced_facet",
836
+ "statement": None,
837
+ "surfaced_text": payload["surfaced_text"],
838
+ "adoption_receipt": None,
839
+ "assumptions": [],
840
+ "evidence_sought": [],
841
+ "status": "parked",
842
+ "disposition_reason": None,
843
+ "reopen_conditions": [],
844
+ "downstream_refs": [],
845
+ "merged_into": None,
846
+ "_origin_event_id": event["event_id"],
847
+ "_condition_history": {},
848
+ "_retired_condition_ids": set(),
849
+ }
850
+ elif kind == "branch_adopted":
851
+ branch = _require_branch(branches, branch_id, f"{path}.branch_id")
852
+ if branch["provenance"] != "ai_surfaced_facet" or branch["status"] != "parked":
853
+ _fail(path, "branch_adopted is lawful only on a parked unadopted facet")
854
+ if payload["source_event_id"] != branch["_origin_event_id"]:
855
+ _fail(
856
+ f"{path}.payload.source_event_id",
857
+ "must name the facet_surfaced event that introduced this branch",
858
+ )
859
+ if payload["surfaced_text"] != branch["surfaced_text"]:
860
+ _fail(
861
+ f"{path}.payload.surfaced_text",
862
+ "must retain the originating surfaced_text verbatim",
863
+ )
864
+ branch["provenance"] = "author_adopted"
865
+ branch["statement"] = payload["author_formulation"]
866
+ branch["adoption_receipt"] = copy.deepcopy(payload)
867
+ branch["status"] = "active"
868
+ elif kind == "branch_annotated":
869
+ branch = _require_branch(branches, branch_id, f"{path}.branch_id")
870
+ if _is_terminal(branch):
871
+ _fail(path, "terminal branches cannot be annotated")
872
+ field = payload["field"]
873
+ replacement = copy.deepcopy(payload["value"])
874
+ if field == "reopen_conditions":
875
+ old = _condition_map(branch)
876
+ new = {item["condition_id"]: item for item in replacement}
877
+ history: dict[str, str] = branch["_condition_history"]
878
+ retired: set[str] = branch["_retired_condition_ids"]
879
+ for condition_id, condition in new.items():
880
+ if condition_id in retired:
881
+ _fail(
882
+ f"{path}.payload.value",
883
+ f"retired condition_id cannot be reused: {condition_id}",
884
+ )
885
+ historical = history.get(condition_id)
886
+ if historical is not None and historical != condition["statement"]:
887
+ _fail(
888
+ f"{path}.payload.value",
889
+ f"condition_id {condition_id!r} cannot be rebound to new text",
890
+ )
891
+ history.setdefault(condition_id, condition["statement"])
892
+ retired.update(set(old) - set(new))
893
+ branch[field] = replacement
894
+ elif kind == "branch_parked":
895
+ branch = _require_branch(branches, branch_id, f"{path}.branch_id")
896
+ if branch["status"] not in {"active", "reopened"}:
897
+ _fail(path, "branch_parked requires active or reopened status")
898
+ branch["status"] = "parked"
899
+ branch["disposition_reason"] = payload["reason"]
900
+ elif kind == "branch_rejected":
901
+ branch = _require_branch(branches, branch_id, f"{path}.branch_id")
902
+ if branch["status"] not in {"active", "reopened", "parked"}:
903
+ _fail(path, "branch_rejected requires active, reopened, or parked status")
904
+ branch["status"] = "rejected"
905
+ branch["disposition_reason"] = payload["reason"]
906
+ elif kind == "branch_reopened":
907
+ branch = _require_branch(branches, branch_id, f"{path}.branch_id")
908
+ if branch["status"] not in {"parked", "rejected"}:
909
+ _fail(path, "branch_reopened requires parked or rejected status")
910
+ if branch["provenance"] not in {"author_originated", "author_adopted"}:
911
+ _fail(path, "an unadopted AI facet cannot be reopened")
912
+ condition_id = payload.get("condition_id")
913
+ if condition_id is not None and condition_id not in _condition_map(branch):
914
+ _fail(
915
+ f"{path}.payload.condition_id",
916
+ "must name a currently stored reopen condition",
917
+ )
918
+ branch["status"] = "reopened"
919
+ branch["disposition_reason"] = payload["reason"]
920
+ expected_stale = [
921
+ (artifact_ref, event["event_id"])
922
+ for artifact_ref in branch["downstream_refs"]
923
+ ]
924
+ elif kind == "branch_merged":
925
+ branch = _require_branch(branches, branch_id, f"{path}.branch_id")
926
+ if branch["status"] not in {"active", "reopened"}:
927
+ _fail(path, "branch_merged requires active or reopened source status")
928
+ target_id = payload["merged_into"]
929
+ if target_id == branch_id:
930
+ _fail(f"{path}.payload.merged_into", "a branch cannot merge into itself")
931
+ target = _require_branch(branches, target_id, f"{path}.payload.merged_into")
932
+ if target["status"] not in LIVE_STATUSES:
933
+ _fail(f"{path}.payload.merged_into", "must name a currently-live branch")
934
+ target["downstream_refs"] = _dedupe_ordered(
935
+ [*target["downstream_refs"], *branch["downstream_refs"]]
936
+ )
937
+ branch["status"] = "merged"
938
+ branch["disposition_reason"] = payload["reason"]
939
+ branch["merged_into"] = target_id
940
+ elif kind == "branch_archived":
941
+ branch = _require_branch(branches, branch_id, f"{path}.branch_id")
942
+ if _is_terminal(branch):
943
+ _fail(path, "branch_archived requires a non-terminal branch")
944
+ branch["status"] = "archived"
945
+ branch["disposition_reason"] = payload["reason"]
946
+ elif kind == "reopen_condition_signal":
947
+ branch = _require_branch(branches, branch_id, f"{path}.branch_id")
948
+ if _is_terminal(branch):
949
+ _fail(path, "a terminal branch cannot receive a new reopen signal")
950
+ conditions = _condition_map(branch)
951
+ condition = conditions.get(payload["condition_id"])
952
+ if condition is None:
953
+ _fail(
954
+ f"{path}.payload.condition_id",
955
+ "must name a currently stored reopen condition",
956
+ )
957
+ signals.append(
958
+ {
959
+ "event_id": event["event_id"],
960
+ "branch_id": branch_id,
961
+ "condition_id": payload["condition_id"],
962
+ "condition_statement": condition["statement"],
963
+ "evidence_pointer": payload["evidence_pointer"],
964
+ }
965
+ )
966
+ elif kind == "profile_rebound":
967
+ rebound = _validate_binding(
968
+ {key: payload[key] for key in _BINDING_FIELDS},
969
+ f"{path}.payload",
970
+ )
971
+ if rebound == effective_binding:
972
+ _fail(path, "profile_rebound must change the effective profile binding")
973
+ replacement = _resolve_profile(rebound, catalog, f"{path}.payload")
974
+ live_count = sum(
975
+ branch["status"] in LIVE_STATUSES for branch in branches.values()
976
+ )
977
+ if live_count > replacement["branch_budget"]:
978
+ _fail(
979
+ path,
980
+ "profile_rebound would exceed the replacement profile's "
981
+ "branch_budget; dispose live branches first",
982
+ )
983
+ effective_binding = copy.deepcopy(rebound)
984
+ current_profile = replacement
985
+ elif kind in {"artifact_reconfirmed", "artifact_superseded"}:
986
+ cause_id = payload["resolves_stale_event_id"]
987
+ cause = stale_causes.get(cause_id)
988
+ if cause is None:
989
+ _fail(
990
+ f"{path}.payload.resolves_stale_event_id",
991
+ "does not name an earlier artifact_marked_stale event",
992
+ )
993
+ if cause["artifact_ref"] != payload["artifact_ref"]:
994
+ _fail(
995
+ f"{path}.payload.artifact_ref",
996
+ "does not match the named stale cause's artifact_ref",
997
+ )
998
+ artifact = artifacts[payload["artifact_ref"]]
999
+ if cause_id not in artifact["outstanding_stale_causes"]:
1000
+ _fail(path, "the named stale cause has already been resolved")
1001
+ artifact["outstanding_stale_causes"].remove(cause_id)
1002
+ resolution = {
1003
+ "event_id": event["event_id"],
1004
+ "kind": kind,
1005
+ "resolves_stale_event_id": cause_id,
1006
+ "note": payload["note"],
1007
+ }
1008
+ if kind == "artifact_superseded":
1009
+ old_ref = payload["artifact_ref"]
1010
+ replacement_ref = payload["replaced_by"]
1011
+ prior_target = supersession_targets.get(old_ref)
1012
+ if prior_target is not None and prior_target != replacement_ref:
1013
+ _fail(
1014
+ f"{path}.payload.replaced_by",
1015
+ f"must equal prior replacement {prior_target!r} for {old_ref!r}",
1016
+ )
1017
+ supersession_targets[old_ref] = replacement_ref
1018
+ resolution["replaced_by"] = replacement_ref
1019
+ for branch in branches.values():
1020
+ if old_ref in branch["downstream_refs"]:
1021
+ branch["downstream_refs"] = _dedupe_ordered(
1022
+ replacement_ref if item == old_ref else item
1023
+ for item in branch["downstream_refs"]
1024
+ )
1025
+ artifact["resolution_history"].append(resolution)
1026
+ else: # pragma: no cover - shape validation closes the enum
1027
+ _fail(path, f"unhandled event kind: {kind}")
1028
+
1029
+ live_count = sum(
1030
+ branch["status"] in LIVE_STATUSES for branch in branches.values()
1031
+ )
1032
+ if live_count > current_profile["branch_budget"]:
1033
+ _fail(
1034
+ path,
1035
+ f"post-event live branch count {live_count} exceeds effective "
1036
+ f"branch_budget {current_profile['branch_budget']}",
1037
+ )
1038
+
1039
+ if expected_stale:
1040
+ artifact_ref, reopening_id = expected_stale[0]
1041
+ _fail(
1042
+ "ledger.events",
1043
+ "ends before the required artifact_marked_stale event "
1044
+ f"for {artifact_ref!r} after reopening event {reopening_id}",
1045
+ )
1046
+ if require_materialized and introduced_count < 2:
1047
+ _fail(
1048
+ "ledger.events",
1049
+ "persisted ledger publication requires at least two introduced branches",
1050
+ )
1051
+
1052
+ public_artifacts: list[dict[str, Any]] = []
1053
+ for artifact in artifacts.values():
1054
+ public_artifacts.append(
1055
+ {
1056
+ "artifact_ref": artifact["artifact_ref"],
1057
+ "stale": bool(artifact["outstanding_stale_causes"]),
1058
+ "outstanding_stale_causes": copy.deepcopy(
1059
+ artifact["outstanding_stale_causes"]
1060
+ ),
1061
+ "resolution_history": copy.deepcopy(artifact["resolution_history"]),
1062
+ }
1063
+ )
1064
+ live_count = sum(
1065
+ branch["status"] in LIVE_STATUSES for branch in branches.values()
1066
+ )
1067
+ head = ZERO_SHA256 if not events else hashlib.sha256(
1068
+ canonical_bytes(events[-1])
1069
+ ).hexdigest()
1070
+ return {
1071
+ "project_ref": ledger["project_ref"],
1072
+ "effective_profile_binding": copy.deepcopy(effective_binding),
1073
+ "branch_budget": current_profile["branch_budget"],
1074
+ "live_count": live_count,
1075
+ "introduced_branch_count": introduced_count,
1076
+ "branches": [_public_branch(branch) for branch in branches.values()],
1077
+ "artifacts": public_artifacts,
1078
+ "reopen_condition_signals": copy.deepcopy(signals),
1079
+ "event_count": len(events),
1080
+ "head_event_sha256": head,
1081
+ }
1082
+
1083
+
1084
+ def new_ledger(
1085
+ project_ref: str, initial_profile: Mapping[str, Any]
1086
+ ) -> dict[str, Any]:
1087
+ """Create an in-memory empty ledger; publication waits for branch two."""
1088
+
1089
+ _text(project_ref, "project_ref")
1090
+ try:
1091
+ validate_profile(initial_profile)
1092
+ binding = profile_binding(initial_profile)
1093
+ except ProfileContractError as exc:
1094
+ raise ContractError(f"initial_profile: {exc}") from exc
1095
+ return {
1096
+ "schema_version": LEDGER_SCHEMA_VERSION,
1097
+ "project_ref": project_ref,
1098
+ "initial_profile_binding": binding,
1099
+ "events": [],
1100
+ }
1101
+
1102
+
1103
+ def _complete_event(
1104
+ events: list[Mapping[str, Any]], event_input: Mapping[str, Any]
1105
+ ) -> dict[str, Any]:
1106
+ _object(event_input, "event", _EVENT_INPUT_FIELDS)
1107
+ completed = copy.deepcopy(dict(event_input))
1108
+ completed["event_id"] = len(events) + 1
1109
+ completed["prev_event_sha256"] = (
1110
+ ZERO_SHA256
1111
+ if not events
1112
+ else hashlib.sha256(canonical_bytes(events[-1])).hexdigest()
1113
+ )
1114
+ # Put fields into one stable human-readable order. Canonical serialization
1115
+ # itself sorts keys, but stable insertion order keeps replay output tidy.
1116
+ result = {
1117
+ "event_id": completed["event_id"],
1118
+ "recorded_at": completed["recorded_at"],
1119
+ "actor": completed["actor"],
1120
+ "kind": completed["kind"],
1121
+ "branch_id": completed["branch_id"],
1122
+ "payload": completed["payload"],
1123
+ "prev_event_sha256": completed["prev_event_sha256"],
1124
+ }
1125
+ _validate_event_shape(result, "event")
1126
+ return result
1127
+
1128
+
1129
+ def append_event(
1130
+ ledger: Mapping[str, Any],
1131
+ event: Mapping[str, Any],
1132
+ profiles: Iterable[Mapping[str, Any]] | Mapping[Any, Mapping[str, Any]],
1133
+ *,
1134
+ expected_project_ref: str | None = None,
1135
+ ) -> dict[str, Any]:
1136
+ """Append one author/AI event and mechanically append reopen stale marks.
1137
+
1138
+ ``event`` is the five-field pre-chain shape: recorded_at, actor, kind,
1139
+ branch_id, payload. Direct system stale-mark appends are refused because
1140
+ those events are emitted only as part of a branch_reopened operation.
1141
+ """
1142
+
1143
+ _object(event, "event", _EVENT_INPUT_FIELDS)
1144
+ catalog = build_profile_catalog(profiles)
1145
+ before = replay_ledger(
1146
+ ledger, catalog, expected_project_ref=expected_project_ref
1147
+ )
1148
+ if event.get("kind") == "artifact_marked_stale":
1149
+ _fail("event.kind", "artifact_marked_stale is emitted mechanically")
1150
+ updated = copy.deepcopy(dict(ledger))
1151
+ updated["events"] = copy.deepcopy(list(ledger["events"]))
1152
+ completed = _complete_event(updated["events"], event)
1153
+ updated["events"].append(completed)
1154
+
1155
+ if completed["kind"] == "branch_reopened":
1156
+ branch = next(
1157
+ (
1158
+ item
1159
+ for item in before["branches"]
1160
+ if item["branch_id"] == completed["branch_id"]
1161
+ ),
1162
+ None,
1163
+ )
1164
+ if branch is None:
1165
+ # Final replay would fail too; this gives a local, comprehensible
1166
+ # error before trying to derive system events from unknown state.
1167
+ _fail("event.branch_id", "unknown branch_id")
1168
+ for artifact_ref in branch["downstream_refs"]:
1169
+ stale_input = {
1170
+ "recorded_at": completed["recorded_at"],
1171
+ "actor": "system",
1172
+ "kind": "artifact_marked_stale",
1173
+ "branch_id": None,
1174
+ "payload": {
1175
+ "artifact_ref": artifact_ref,
1176
+ "reopening_event_id": completed["event_id"],
1177
+ },
1178
+ }
1179
+ stale_event = _complete_event(updated["events"], stale_input)
1180
+ updated["events"].append(stale_event)
1181
+
1182
+ replay_ledger(updated, catalog, expected_project_ref=expected_project_ref)
1183
+ return updated
1184
+
1185
+
1186
+ def inquiry_ledger_enabled(env: Mapping[str, str] | None = None) -> bool:
1187
+ """The alpha is enabled only by the exact value ``ARS_INQUIRY_LEDGER=1``."""
1188
+
1189
+ source = os.environ if env is None else env
1190
+ return source.get(ENV_FLAG) == "1"
1191
+
1192
+
1193
+ def _summary_text(value: Any, path: str) -> str:
1194
+ """Return one bounded line of inert Markdown-safe display text."""
1195
+
1196
+ text = _text(value, path)
1197
+ collapsed = " ".join(text.split())
1198
+ escaped: list[str] = []
1199
+ escaped_length = 0
1200
+ for character in collapsed:
1201
+ if unicodedata.category(character).startswith("C"):
1202
+ width = 4 if ord(character) <= 0xFFFF else 8
1203
+ token = f"\\{'u' if width == 4 else 'U'}{ord(character):0{width}x}"
1204
+ elif character in _MARKDOWN_META:
1205
+ token = "\\" + character
1206
+ else:
1207
+ token = character
1208
+ if escaped_length + len(token) > _SUMMARY_TEXT_LIMIT:
1209
+ while escaped and escaped_length >= _SUMMARY_TEXT_LIMIT:
1210
+ escaped_length -= len(escaped.pop())
1211
+ return "".join(escaped).rstrip() + "…"
1212
+ escaped.append(token)
1213
+ escaped_length += len(token)
1214
+ return "".join(escaped)
1215
+
1216
+
1217
+ def render_summary(
1218
+ projection: Mapping[str, Any], *, signal_event_id: int | None = None
1219
+ ) -> str:
1220
+ """Render the compact, provenance-labelled branch summary."""
1221
+
1222
+ branches = projection.get("branches")
1223
+ if not isinstance(branches, list):
1224
+ _fail("projection.branches", "must be an array")
1225
+ live_count = _nonnegative_int(projection.get("live_count"), "projection.live_count")
1226
+ branch_budget = _positive_int(
1227
+ projection.get("branch_budget"), "projection.branch_budget"
1228
+ )
1229
+ branch_by_id: dict[str, Mapping[str, Any]] = {}
1230
+ for index, branch in enumerate(branches):
1231
+ if not isinstance(branch, Mapping):
1232
+ _fail(f"projection.branches[{index}]", "must be an object")
1233
+ branch_id = _slug(
1234
+ branch.get("branch_id"), f"projection.branches[{index}].branch_id"
1235
+ )
1236
+ if branch_id in branch_by_id:
1237
+ _fail("projection.branches", f"duplicate branch_id: {branch_id}")
1238
+ branch_by_id[branch_id] = branch
1239
+
1240
+ lines = [f"Inquiry branches (live {live_count}/{branch_budget}):"]
1241
+ if signal_event_id is not None:
1242
+ _positive_int(signal_event_id, "signal_event_id")
1243
+ signals = projection.get("reopen_condition_signals")
1244
+ if not isinstance(signals, list):
1245
+ _fail("projection.reopen_condition_signals", "must be an array")
1246
+ signal = None
1247
+ for index, item in enumerate(signals):
1248
+ signal_path = f"projection.reopen_condition_signals[{index}]"
1249
+ value = _object(
1250
+ item,
1251
+ signal_path,
1252
+ {
1253
+ "event_id",
1254
+ "branch_id",
1255
+ "condition_id",
1256
+ "condition_statement",
1257
+ "evidence_pointer",
1258
+ },
1259
+ )
1260
+ _positive_int(value["event_id"], f"{signal_path}.event_id")
1261
+ if value["event_id"] == signal_event_id:
1262
+ if signal is not None:
1263
+ _fail("signal_event_id", "matches more than one replayed signal")
1264
+ signal = value
1265
+ if signal is None:
1266
+ _fail("signal_event_id", "does not name a replayed reopen-condition signal")
1267
+
1268
+ signal_branch_id = _slug(signal["branch_id"], "signal.branch_id")
1269
+ signal_branch = branch_by_id.get(signal_branch_id)
1270
+ if signal_branch is None:
1271
+ _fail("signal_event_id", "names a branch absent from the current projection")
1272
+ status = _enum(signal_branch.get("status"), "signal.branch.status", BRANCH_STATUSES)
1273
+ provenance = _enum(
1274
+ signal_branch.get("provenance"),
1275
+ "signal.branch.provenance",
1276
+ PROVENANCE_VALUES,
1277
+ )
1278
+ if (
1279
+ status not in AUTHOR_REOPENABLE_STATUSES
1280
+ or provenance not in AUTHOR_OWNED_PROVENANCE
1281
+ ):
1282
+ _fail(
1283
+ "signal_event_id",
1284
+ "branch is not currently eligible for an author-owned reopen",
1285
+ )
1286
+ conditions = _validate_conditions(
1287
+ signal_branch.get("reopen_conditions"),
1288
+ "signal.branch.reopen_conditions",
1289
+ )
1290
+ condition_id = _text(signal["condition_id"], "signal.condition_id")
1291
+ current_condition = next(
1292
+ (item for item in conditions if item["condition_id"] == condition_id),
1293
+ None,
1294
+ )
1295
+ if current_condition is None:
1296
+ _fail("signal_event_id", "condition_id is no longer current on the branch")
1297
+ historical_statement = _text(
1298
+ signal["condition_statement"], "signal.condition_statement"
1299
+ )
1300
+ if historical_statement != current_condition["statement"]:
1301
+ _fail(
1302
+ "signal_event_id",
1303
+ "historical condition text does not equal the current condition text",
1304
+ )
1305
+ lines.append(
1306
+ f"- {signal_branch_id} [{status}; {provenance}] — "
1307
+ "AUTHOR JUDGMENT REQUIRED: "
1308
+ f"{_summary_text(condition_id, 'signal.condition_id')} — "
1309
+ f"{_summary_text(current_condition['statement'], 'signal.condition_statement')} "
1310
+ f"(evidence: {_summary_text(signal['evidence_pointer'], 'signal.evidence_pointer')})"
1311
+ )
1312
+
1313
+ live = [branch for branch in branches if branch.get("status") in LIVE_STATUSES]
1314
+ if not live:
1315
+ lines.append("- No live branches.")
1316
+ for branch in live:
1317
+ statement = branch.get("statement") or branch.get("surfaced_text") or "(unstated)"
1318
+ branch_id = _slug(branch["branch_id"], "branch.branch_id")
1319
+ status = _enum(branch["status"], "branch.status", ("active", "reopened"))
1320
+ provenance = _enum(
1321
+ branch["provenance"], "branch.provenance", PROVENANCE_VALUES
1322
+ )
1323
+ lines.append(
1324
+ f"- {branch_id} [{status}; {provenance}]: "
1325
+ f"{_summary_text(statement, 'branch.statement')}"
1326
+ )
1327
+ artifacts = projection.get("artifacts", [])
1328
+ if not isinstance(artifacts, list):
1329
+ _fail("projection.artifacts", "must be an array")
1330
+ stale_items: list[tuple[str, list[int]]] = []
1331
+ for index, artifact in enumerate(artifacts):
1332
+ if not isinstance(artifact, Mapping):
1333
+ _fail(f"projection.artifacts[{index}]", "must be an object")
1334
+ if not artifact.get("stale"):
1335
+ continue
1336
+ cause_ids = artifact.get("outstanding_stale_causes")
1337
+ if not isinstance(cause_ids, list):
1338
+ _fail(
1339
+ f"projection.artifacts[{index}].outstanding_stale_causes",
1340
+ "must be an array",
1341
+ )
1342
+ for cause_index, cause_id in enumerate(cause_ids):
1343
+ _positive_int(
1344
+ cause_id,
1345
+ f"projection.artifacts[{index}].outstanding_stale_causes[{cause_index}]",
1346
+ )
1347
+ if not cause_ids:
1348
+ _fail(
1349
+ f"projection.artifacts[{index}].outstanding_stale_causes",
1350
+ "a stale artifact must retain at least one outstanding cause",
1351
+ )
1352
+ stale_items.append(
1353
+ (
1354
+ _summary_text(
1355
+ artifact.get("artifact_ref"),
1356
+ f"projection.artifacts[{index}].artifact_ref",
1357
+ ),
1358
+ cause_ids,
1359
+ )
1360
+ )
1361
+ if stale_items:
1362
+ lines.append(f"- Stale artifacts ({len(stale_items)}):")
1363
+ for artifact_ref, cause_ids in stale_items:
1364
+ lines.append(f" - {artifact_ref} [outstanding={len(cause_ids)}]")
1365
+ lines.extend(f" - cause_event_id={cause_id}" for cause_id in cause_ids)
1366
+ parked = sum(branch.get("status") == "parked" for branch in branches)
1367
+ rejected = sum(branch.get("status") == "rejected" for branch in branches)
1368
+ if parked or rejected:
1369
+ lines.append(f"- Other dispositions: parked={parked}; rejected={rejected}.")
1370
+ lines.append("Controls: skip | off | reset-to-simple-path (ledger is preserved).")
1371
+ return "\n".join(lines)
1372
+
1373
+
1374
+ def checkpoint_summary(
1375
+ projection: Mapping[str, Any],
1376
+ *,
1377
+ moment: str,
1378
+ env: Mapping[str, str] | None = None,
1379
+ signal_event_id: int | None = None,
1380
+ ) -> str | None:
1381
+ """Return a summary only at the frozen moments and only when opt-in is on."""
1382
+
1383
+ _enum(moment, "moment", SUMMARY_MOMENTS)
1384
+ if not inquiry_ledger_enabled(env):
1385
+ return None
1386
+ if int(projection.get("introduced_branch_count", 0)) <= 1:
1387
+ return None
1388
+ if moment == "reopen_condition_signal":
1389
+ if signal_event_id is None:
1390
+ _fail("signal_event_id", "is required at reopen_condition_signal moment")
1391
+ elif signal_event_id is not None:
1392
+ _fail("signal_event_id", "is lawful only at reopen_condition_signal moment")
1393
+ return render_summary(projection, signal_event_id=signal_event_id)
1394
+
1395
+
1396
+ def validate_pointer(pointer: Mapping[str, Any]) -> dict[str, str]:
1397
+ value = _object(pointer, "inquiry_ledger_ref", _POINTER_FIELDS)
1398
+ ledger_path = _workspace_relative_path(
1399
+ value["ledger_path"], "inquiry_ledger_ref.ledger_path"
1400
+ )
1401
+ if value["ledger_version"] != POINTER_LEDGER_VERSION:
1402
+ _fail(
1403
+ "inquiry_ledger_ref.ledger_version",
1404
+ f"must equal {POINTER_LEDGER_VERSION}",
1405
+ )
1406
+ return {
1407
+ "ledger_path": ledger_path,
1408
+ "ledger_version": POINTER_LEDGER_VERSION,
1409
+ "content_sha256": _sha256(
1410
+ value["content_sha256"], "inquiry_ledger_ref.content_sha256"
1411
+ ),
1412
+ }
1413
+
1414
+
1415
+ def _workspace_relative_path(value: Any, path: str) -> str:
1416
+ text = _text(value, path)
1417
+ if "\\" in text or _FORBIDDEN_PATH_CHARS_RE.search(text):
1418
+ _fail(path, "must be a normalized relative POSIX path without unsafe characters")
1419
+ pure = PurePosixPath(text)
1420
+ if pure.is_absolute() or text.startswith("/"):
1421
+ _fail(path, "must be workspace-relative")
1422
+ if any(part in {"", ".", ".."} for part in pure.parts):
1423
+ _fail(path, "must not contain empty, '.' or '..' path segments")
1424
+ normalized = pure.as_posix()
1425
+ if normalized != text or "//" in text or text.endswith("/"):
1426
+ _fail(path, "must be a normalized relative POSIX path")
1427
+ # PurePosixPath treats ``C:/x`` as relative; it is absolute on Windows and
1428
+ # remains forbidden even though durable publication is POSIX-only.
1429
+ if re.match(r"^[A-Za-z]:", text):
1430
+ _fail(path, "must not be a platform-absolute path")
1431
+ return text
1432
+
1433
+
1434
+ def _workspace_root(path: Path | str) -> Path:
1435
+ root = Path(path)
1436
+ if not root.is_absolute():
1437
+ _fail("workspace_root", "must be an explicit absolute path")
1438
+ try:
1439
+ resolved = root.resolve(strict=True)
1440
+ except OSError as exc:
1441
+ raise ContractError(f"workspace_root is not readable: {root}: {exc}") from exc
1442
+ if not resolved.is_dir():
1443
+ _fail("workspace_root", "must be a directory")
1444
+ return resolved
1445
+
1446
+
1447
+ def _authoritative_passport_path(
1448
+ path: Path | str, root: Path
1449
+ ) -> tuple[str, Path]:
1450
+ """Resolve a passport without erasing evidence of a symlinked parent."""
1451
+
1452
+ raw = Path(path)
1453
+ if raw.is_absolute():
1454
+ try:
1455
+ relative = raw.relative_to(root).as_posix()
1456
+ except ValueError:
1457
+ _fail("passport_path", "is outside workspace_root")
1458
+ else:
1459
+ relative = raw.as_posix()
1460
+ normalized = _workspace_relative_path(relative, "passport_path")
1461
+ passport = root.joinpath(*PurePosixPath(normalized).parts)
1462
+ try:
1463
+ resolved_parent = passport.parent.resolve(strict=True)
1464
+ except OSError as exc:
1465
+ raise ContractError(
1466
+ f"passport_path: parent directory is not readable: {exc}"
1467
+ ) from exc
1468
+ if resolved_parent != passport.parent:
1469
+ _fail(
1470
+ "passport_path",
1471
+ "parent directory must not resolve through a symlink",
1472
+ )
1473
+ _require_regular_nonsymlink(passport, "passport_path")
1474
+ return normalized, passport
1475
+
1476
+
1477
+ def _contained_path(root: Path, relative: str, path: str) -> Path:
1478
+ normalized = _workspace_relative_path(relative, path)
1479
+ candidate = root.joinpath(*PurePosixPath(normalized).parts)
1480
+ resolved = candidate.resolve(strict=False)
1481
+ try:
1482
+ resolved.relative_to(root)
1483
+ except ValueError:
1484
+ _fail(path, "resolves outside workspace_root")
1485
+ return candidate
1486
+
1487
+
1488
+ def _relative_to_workspace(path: Path | str, root: Path, field: str) -> str:
1489
+ candidate = Path(path)
1490
+ if candidate.is_absolute():
1491
+ try:
1492
+ # Preserve the caller's lexical target. Resolving here would erase
1493
+ # a final-component or parent symlink before the non-symlink and
1494
+ # beside-passport checks have a chance to reject it.
1495
+ relative = candidate.relative_to(root).as_posix()
1496
+ except ValueError:
1497
+ _fail(field, "is outside workspace_root")
1498
+ else:
1499
+ relative = candidate.as_posix()
1500
+ return _workspace_relative_path(relative, field)
1501
+
1502
+
1503
+ def default_ledger_relative_path(
1504
+ passport_path: Path | str, workspace_root: Path | str
1505
+ ) -> str:
1506
+ """Return the deterministic beside-passport orphan-discovery candidate."""
1507
+
1508
+ root = _workspace_root(workspace_root)
1509
+ passport_relative = _relative_to_workspace(passport_path, root, "passport_path")
1510
+ passport = PurePosixPath(passport_relative)
1511
+ filename = f"{passport.stem}.inquiry-branch-ledger.json"
1512
+ return (passport.parent / filename).as_posix()
1513
+
1514
+
1515
+ def _require_regular_nonsymlink(path: Path, field: str, *, may_absent: bool = False) -> None:
1516
+ try:
1517
+ mode = path.lstat().st_mode
1518
+ except FileNotFoundError:
1519
+ if may_absent:
1520
+ return
1521
+ _fail(field, f"file does not exist: {path}")
1522
+ except OSError as exc:
1523
+ raise ContractError(f"{field}: cannot inspect {path}: {exc}") from exc
1524
+ if stat.S_ISLNK(mode) or not stat.S_ISREG(mode):
1525
+ _fail(field, "must be a regular non-symlink file")
1526
+
1527
+
1528
+ def _yaml_runtime():
1529
+ try:
1530
+ from ruamel.yaml import YAML
1531
+ except ImportError as exc: # pragma: no cover - dependency is CI-pinned
1532
+ raise ContractError("ruamel.yaml is required for passport transactions") from exc
1533
+ yaml = YAML(typ="rt")
1534
+ yaml.preserve_quotes = True
1535
+ yaml.allow_duplicate_keys = False
1536
+ yaml.indent(mapping=2, sequence=4, offset=2)
1537
+ return yaml
1538
+
1539
+
1540
+ def _parse_passport_bytes(path: Path, raw: bytes) -> Any:
1541
+ try:
1542
+ text = raw.decode("utf-8")
1543
+ except UnicodeDecodeError as exc:
1544
+ raise ContractError(f"passport YAML is not UTF-8: {path}") from exc
1545
+ yaml = _yaml_runtime()
1546
+ try:
1547
+ document = yaml.load(text)
1548
+ except Exception as exc:
1549
+ raise ContractError(f"cannot parse passport YAML {path}: {exc}") from exc
1550
+ if not isinstance(document, Mapping):
1551
+ _fail("passport", "top-level YAML value must be an object")
1552
+ return document
1553
+
1554
+
1555
+ def _load_passport_snapshot(path: Path) -> tuple[bytes, Any]:
1556
+ _require_regular_nonsymlink(path, "passport_path")
1557
+ try:
1558
+ raw = path.read_bytes()
1559
+ except OSError as exc:
1560
+ raise ContractError(f"cannot read passport YAML {path}: {exc}") from exc
1561
+ return raw, _parse_passport_bytes(path, raw)
1562
+
1563
+
1564
+ def _load_passport_doc(path: Path) -> Any:
1565
+ return _load_passport_snapshot(path)[1]
1566
+
1567
+
1568
+ def _passport_bytes(document: Any) -> bytes:
1569
+ yaml = _yaml_runtime()
1570
+ stream = io.StringIO()
1571
+ try:
1572
+ yaml.dump(document, stream)
1573
+ return stream.getvalue().encode("utf-8")
1574
+ except Exception as exc:
1575
+ raise ContractError(f"cannot serialize passport YAML: {exc}") from exc
1576
+
1577
+
1578
+ def _passport_pointer(document: Mapping[str, Any]) -> dict[str, str] | None:
1579
+ if "inquiry_ledger_ref" not in document:
1580
+ return None
1581
+ raw = document["inquiry_ledger_ref"]
1582
+ if raw is None:
1583
+ raise LedgerBindingError(
1584
+ "inquiry_ledger_ref is present but null; omit the field for no ledger"
1585
+ )
1586
+ try:
1587
+ return validate_pointer(raw)
1588
+ except ContractError as exc:
1589
+ raise LedgerBindingError(f"invalid inquiry_ledger_ref: {exc}") from exc
1590
+
1591
+
1592
+ def _fsync_directory(path: Path) -> None:
1593
+ flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
1594
+ fd = os.open(path, flags)
1595
+ try:
1596
+ os.fsync(fd)
1597
+ finally:
1598
+ os.close(fd)
1599
+
1600
+
1601
+ def _write_exclusive_durable(path: Path, data: bytes, *, mode: int = 0o600) -> None:
1602
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
1603
+ fd = os.open(path, flags, mode)
1604
+ try:
1605
+ offset = 0
1606
+ while offset < len(data):
1607
+ written = os.write(fd, data[offset:])
1608
+ if written <= 0: # pragma: no cover - defensive kernel/I/O guard
1609
+ raise OSError(f"short write while staging transaction file: {path}")
1610
+ offset += written
1611
+ os.fsync(fd)
1612
+ except BaseException:
1613
+ try:
1614
+ path.unlink()
1615
+ except OSError:
1616
+ pass
1617
+ raise
1618
+ finally:
1619
+ os.close(fd)
1620
+
1621
+
1622
+ def _sidecars(passport: Path) -> dict[str, Path]:
1623
+ feature_base = f".{passport.name}.inquiry-ledger"
1624
+ return {
1625
+ # The lock is deliberately feature-neutral: every passport writer,
1626
+ # including future companion transactions, shares this domain.
1627
+ "lock": passport.parent / f".{passport.name}.lock",
1628
+ "journal": passport.parent / f"{feature_base}.transaction.json",
1629
+ "journal_temp": passport.parent / f"{feature_base}.transaction.tmp",
1630
+ "passport_temp": passport.parent / f"{feature_base}.passport.tmp",
1631
+ "ledger_temp": passport.parent / f"{feature_base}.ledger.tmp",
1632
+ # Reserved now so a pre-#744 ledger can never occupy the companion
1633
+ # register transaction's deterministic staged pathname.
1634
+ "register_temp": passport.parent / f"{feature_base}.register.tmp",
1635
+ }
1636
+
1637
+
1638
+ def _ledger_temp(ledger: Path, passport: Path) -> Path:
1639
+ if ledger.parent != passport.parent:
1640
+ _fail("ledger_path", "must be in the same directory as the Material Passport")
1641
+ return _sidecars(passport)["ledger_temp"]
1642
+
1643
+
1644
+ def _assert_safe_ledger_target(passport: Path, ledger: Path) -> None:
1645
+ def name_key(path: Path) -> str:
1646
+ # APFS and HFS+ commonly compare names case-insensitively and with
1647
+ # canonical-equivalence folding. Reject those aliases conservatively
1648
+ # even when the current test filesystem happens to be case-sensitive.
1649
+ return unicodedata.normalize("NFD", path.name).casefold()
1650
+
1651
+ ledger_key = name_key(ledger)
1652
+ if ledger == passport or ledger_key == name_key(passport):
1653
+ _fail("ledger_path", "must not equal passport_path")
1654
+ if ledger.parent != passport.parent:
1655
+ _fail("ledger_path", "must be beside the Material Passport")
1656
+ collisions = [
1657
+ name
1658
+ for name, path in _sidecars(passport).items()
1659
+ if ledger == path or ledger_key == name_key(path)
1660
+ ]
1661
+ if collisions:
1662
+ _fail(
1663
+ "ledger_path",
1664
+ "collides with reserved passport transaction path(s): "
1665
+ + ", ".join(sorted(collisions)),
1666
+ )
1667
+
1668
+
1669
+ @contextmanager
1670
+ def _transaction_lock(passport: Path, *, timeout_seconds: float = 30.0) -> Iterator[None]:
1671
+ if fcntl is None:
1672
+ raise ContractError(
1673
+ "concurrency protection unavailable on this platform; refusing ledger access"
1674
+ )
1675
+ if isinstance(timeout_seconds, bool) or not isinstance(
1676
+ timeout_seconds, (int, float)
1677
+ ):
1678
+ _fail("lock_timeout", "must be a finite number between 0 and 60 seconds")
1679
+ try:
1680
+ normalized_timeout = float(timeout_seconds)
1681
+ except (OverflowError, ValueError):
1682
+ _fail("lock_timeout", "must be a finite number between 0 and 60 seconds")
1683
+ if not math.isfinite(normalized_timeout) or not 0 <= normalized_timeout <= 60:
1684
+ _fail("lock_timeout", "must be a finite number between 0 and 60 seconds")
1685
+ lock_path = _sidecars(passport)["lock"]
1686
+ if lock_path.exists() or lock_path.is_symlink():
1687
+ _require_regular_nonsymlink(lock_path, "transaction_lock")
1688
+ flags = os.O_RDWR | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0)
1689
+ fd = os.open(lock_path, flags, 0o600)
1690
+ deadline = time.monotonic() + normalized_timeout
1691
+ try:
1692
+ if not stat.S_ISREG(os.fstat(fd).st_mode):
1693
+ raise ContractError(f"transaction_lock: must be a regular file: {lock_path}")
1694
+ while True:
1695
+ try:
1696
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1697
+ break
1698
+ except BlockingIOError:
1699
+ if time.monotonic() >= deadline:
1700
+ raise ContractError(
1701
+ f"passport locked by another session: {passport}"
1702
+ )
1703
+ time.sleep(0.05)
1704
+ yield
1705
+ finally:
1706
+ try:
1707
+ fcntl.flock(fd, fcntl.LOCK_UN)
1708
+ finally:
1709
+ os.close(fd)
1710
+
1711
+
1712
+ def _plain_pointer(pointer: Mapping[str, Any] | None) -> dict[str, str] | None:
1713
+ if pointer is None:
1714
+ return None
1715
+ return {key: str(pointer[key]) for key in sorted(_POINTER_FIELDS)}
1716
+
1717
+
1718
+ def _journal_shape(value: Mapping[str, Any]) -> Mapping[str, Any]:
1719
+ fields = {
1720
+ "schema_version",
1721
+ "transaction_id",
1722
+ "passport_path",
1723
+ "ledger_path",
1724
+ "passport_temp_path",
1725
+ "ledger_temp_path",
1726
+ "old_pointer",
1727
+ "new_pointer",
1728
+ "old_ledger_sha256",
1729
+ "new_ledger_sha256",
1730
+ "old_passport_sha256",
1731
+ "new_passport_sha256",
1732
+ }
1733
+ journal = _object(value, "transaction", fields)
1734
+ if journal["schema_version"] != TRANSACTION_SCHEMA_VERSION:
1735
+ _fail("transaction.schema_version", f"must equal {TRANSACTION_SCHEMA_VERSION}")
1736
+ _text(journal["transaction_id"], "transaction.transaction_id")
1737
+ for field in (
1738
+ "passport_path",
1739
+ "ledger_path",
1740
+ "passport_temp_path",
1741
+ "ledger_temp_path",
1742
+ ):
1743
+ _workspace_relative_path(journal[field], f"transaction.{field}")
1744
+ old_pointer = (
1745
+ None
1746
+ if journal["old_pointer"] is None
1747
+ else validate_pointer(journal["old_pointer"])
1748
+ )
1749
+ new_pointer = validate_pointer(journal["new_pointer"])
1750
+ if journal["old_ledger_sha256"] is not None:
1751
+ _sha256(journal["old_ledger_sha256"], "transaction.old_ledger_sha256")
1752
+ _sha256(journal["new_ledger_sha256"], "transaction.new_ledger_sha256")
1753
+ _sha256(journal["old_passport_sha256"], "transaction.old_passport_sha256")
1754
+ _sha256(journal["new_passport_sha256"], "transaction.new_passport_sha256")
1755
+ if new_pointer["ledger_path"] != journal["ledger_path"]:
1756
+ _fail("transaction.new_pointer", "ledger_path does not equal transaction ledger_path")
1757
+ if new_pointer["content_sha256"] != journal["new_ledger_sha256"]:
1758
+ _fail("transaction.new_pointer", "digest does not equal new_ledger_sha256")
1759
+ if old_pointer is None:
1760
+ if journal["old_ledger_sha256"] is not None:
1761
+ _fail(
1762
+ "transaction.old_ledger_sha256",
1763
+ "must be null when old_pointer is null",
1764
+ )
1765
+ else:
1766
+ if old_pointer["ledger_path"] != journal["ledger_path"]:
1767
+ _fail(
1768
+ "transaction.old_pointer",
1769
+ "ledger_path does not equal transaction ledger_path",
1770
+ )
1771
+ if journal["old_ledger_sha256"] != old_pointer["content_sha256"]:
1772
+ _fail(
1773
+ "transaction.old_ledger_sha256",
1774
+ "must equal old_pointer.content_sha256",
1775
+ )
1776
+ return journal
1777
+
1778
+
1779
+ def _path_digest(path: Path, *, field: str = "transaction_target") -> str | None:
1780
+ try:
1781
+ mode = path.lstat().st_mode
1782
+ except FileNotFoundError:
1783
+ return None
1784
+ except OSError as exc:
1785
+ raise TransactionRecoveryError(
1786
+ f"cannot inspect {field} {path}: {exc}"
1787
+ ) from exc
1788
+ if stat.S_ISLNK(mode) or not stat.S_ISREG(mode):
1789
+ raise TransactionRecoveryError(
1790
+ f"{field} must be a regular non-symlink file: {path}"
1791
+ )
1792
+ try:
1793
+ raw = path.read_bytes()
1794
+ except OSError as exc:
1795
+ raise TransactionRecoveryError(f"cannot read {field} {path}: {exc}") from exc
1796
+ return hashlib.sha256(raw).hexdigest()
1797
+
1798
+
1799
+ def _cleanup_unjournaled_temps(passport: Path, ledger: Path | None = None) -> None:
1800
+ paths = [_sidecars(passport)["journal_temp"], _sidecars(passport)["passport_temp"]]
1801
+ if ledger is not None:
1802
+ paths.append(_ledger_temp(ledger, passport))
1803
+ for path in paths:
1804
+ if not path.exists() and not path.is_symlink():
1805
+ continue
1806
+ _require_regular_nonsymlink(path, "transaction_temp")
1807
+ try:
1808
+ path.unlink()
1809
+ except OSError as exc:
1810
+ raise TransactionRecoveryError(f"cannot clear unjournaled temp {path}: {exc}") from exc
1811
+ _fsync_directory(path.parent)
1812
+
1813
+
1814
+ def _write_journal(path: Path, temp: Path, journal: Mapping[str, Any]) -> None:
1815
+ if path.exists() or path.is_symlink():
1816
+ raise TransactionRecoveryError(f"existing transaction journal: {path}")
1817
+ if temp.exists() or temp.is_symlink():
1818
+ raise TransactionRecoveryError(f"stale transaction journal temp: {temp}")
1819
+ journal_bytes = canonical_bytes(journal)
1820
+ _write_exclusive_durable(temp, journal_bytes)
1821
+ _require_regular_nonsymlink(temp, "transaction_journal_temp")
1822
+ try:
1823
+ if temp.read_bytes() != journal_bytes:
1824
+ raise TransactionRecoveryError(
1825
+ "transaction journal temp changed before publication"
1826
+ )
1827
+ except OSError as exc:
1828
+ raise TransactionRecoveryError(
1829
+ f"cannot verify transaction journal temp {temp}: {exc}"
1830
+ ) from exc
1831
+ if path.exists() or path.is_symlink():
1832
+ raise TransactionRecoveryError(f"transaction journal appeared concurrently: {path}")
1833
+ os.replace(temp, path)
1834
+ _fsync_directory(path.parent)
1835
+
1836
+
1837
+ def _recovery_passport_snapshot(
1838
+ path: Path, field: str
1839
+ ) -> tuple[bytes, Mapping[str, Any]]:
1840
+ try:
1841
+ raw, document = _load_passport_snapshot(path)
1842
+ except ContractError as exc:
1843
+ raise TransactionRecoveryError(f"invalid {field} {path}: {exc}") from exc
1844
+ return raw, document
1845
+
1846
+
1847
+ def _recovery_passport_pointer(
1848
+ document: Mapping[str, Any], field: str
1849
+ ) -> dict[str, str] | None:
1850
+ try:
1851
+ return _passport_pointer(document)
1852
+ except ContractError as exc:
1853
+ raise TransactionRecoveryError(f"invalid {field} pointer: {exc}") from exc
1854
+
1855
+
1856
+ def _recover_transaction_locked(passport: Path, root: Path) -> bool:
1857
+ sidecars = _sidecars(passport)
1858
+ journal_path = sidecars["journal"]
1859
+ if not journal_path.exists():
1860
+ if journal_path.is_symlink():
1861
+ raise TransactionRecoveryError("journal path is a symlink")
1862
+ return False
1863
+ try:
1864
+ _require_regular_nonsymlink(journal_path, "transaction_journal")
1865
+ except ContractError as exc:
1866
+ raise TransactionRecoveryError(f"invalid journal {journal_path}: {exc}") from exc
1867
+ try:
1868
+ raw = journal_path.read_bytes()
1869
+ value = _parse_json_bytes(raw, source=str(journal_path))
1870
+ if raw != canonical_bytes(value):
1871
+ raise ContractError("journal is not exact canonical JSON")
1872
+ journal = _journal_shape(value)
1873
+ except (OSError, ContractError) as exc:
1874
+ raise TransactionRecoveryError(f"invalid journal {journal_path}: {exc}") from exc
1875
+
1876
+ expected_passport_rel = _relative_to_workspace(passport, root, "passport_path")
1877
+ if journal["passport_path"] != expected_passport_rel:
1878
+ raise TransactionRecoveryError("journal passport_path does not match lock owner")
1879
+ ledger = _contained_path(root, journal["ledger_path"], "transaction.ledger_path")
1880
+ passport_temp = _contained_path(
1881
+ root, journal["passport_temp_path"], "transaction.passport_temp_path"
1882
+ )
1883
+ ledger_temp = _contained_path(
1884
+ root, journal["ledger_temp_path"], "transaction.ledger_temp_path"
1885
+ )
1886
+ try:
1887
+ _assert_safe_ledger_target(passport, ledger)
1888
+ except ContractError as exc:
1889
+ raise TransactionRecoveryError(f"invalid journal ledger target: {exc}") from exc
1890
+ expected_passport_temp = sidecars["passport_temp"]
1891
+ expected_ledger_temp = _ledger_temp(ledger, passport)
1892
+ if passport_temp != expected_passport_temp:
1893
+ raise TransactionRecoveryError(
1894
+ "journal passport_temp_path is not the deterministic reserved path"
1895
+ )
1896
+ if ledger_temp != expected_ledger_temp:
1897
+ raise TransactionRecoveryError(
1898
+ "journal ledger_temp_path is not the deterministic reserved path"
1899
+ )
1900
+
1901
+ new_digest = journal["new_ledger_sha256"]
1902
+ old_digest = journal["old_ledger_sha256"]
1903
+ ledger_digest = _path_digest(ledger, field="ledger target")
1904
+ ledger_temp_present = ledger_temp.exists() or ledger_temp.is_symlink()
1905
+ if ledger_temp_present:
1906
+ temp_digest = _path_digest(ledger_temp, field="staged ledger")
1907
+ if temp_digest != new_digest:
1908
+ raise TransactionRecoveryError(
1909
+ "staged ledger is not the journal's complete new byte image"
1910
+ )
1911
+ if ledger_digest == new_digest:
1912
+ publish_ledger = False
1913
+ elif ledger_digest == old_digest:
1914
+ if not ledger_temp_present:
1915
+ raise TransactionRecoveryError("new ledger temp is absent")
1916
+ publish_ledger = True
1917
+ else:
1918
+ raise TransactionRecoveryError(
1919
+ "ledger target is neither the recorded old nor new generation"
1920
+ )
1921
+
1922
+ old_pointer = (
1923
+ None if journal["old_pointer"] is None else validate_pointer(journal["old_pointer"])
1924
+ )
1925
+ new_pointer = validate_pointer(journal["new_pointer"])
1926
+ old_passport_digest = journal["old_passport_sha256"]
1927
+ new_passport_digest = journal["new_passport_sha256"]
1928
+
1929
+ staged_passport_present = passport_temp.exists() or passport_temp.is_symlink()
1930
+ if staged_passport_present:
1931
+ staged_raw, staged_document = _recovery_passport_snapshot(
1932
+ passport_temp, "staged passport"
1933
+ )
1934
+ if hashlib.sha256(staged_raw).hexdigest() != new_passport_digest:
1935
+ raise TransactionRecoveryError(
1936
+ "staged passport is not the journal's complete new byte image"
1937
+ )
1938
+ if _recovery_passport_pointer(staged_document, "staged passport") != new_pointer:
1939
+ raise TransactionRecoveryError("staged passport does not carry new pointer")
1940
+
1941
+ live_raw, live_document = _recovery_passport_snapshot(passport, "live passport")
1942
+ live_passport_digest = hashlib.sha256(live_raw).hexdigest()
1943
+ current_pointer = _recovery_passport_pointer(live_document, "live passport")
1944
+ if live_passport_digest == new_passport_digest:
1945
+ publish_passport = False
1946
+ if current_pointer != new_pointer:
1947
+ raise TransactionRecoveryError(
1948
+ "new passport byte image does not carry the recorded new pointer"
1949
+ )
1950
+ elif live_passport_digest == old_passport_digest:
1951
+ publish_passport = True
1952
+ if current_pointer != old_pointer:
1953
+ raise TransactionRecoveryError(
1954
+ "old passport byte image does not carry the recorded old pointer"
1955
+ )
1956
+ if not staged_passport_present:
1957
+ raise TransactionRecoveryError("new passport temp is absent")
1958
+ else:
1959
+ raise TransactionRecoveryError(
1960
+ "live passport is neither the recorded old nor new byte generation"
1961
+ )
1962
+
1963
+ # All journal relations, sources, and live generations are valid before
1964
+ # recovery advances either member of the pair.
1965
+ if publish_ledger:
1966
+ if _path_digest(ledger, field="ledger target CAS") != old_digest:
1967
+ raise TransactionRecoveryError("ledger target changed during recovery")
1968
+ if _path_digest(ledger_temp, field="staged ledger CAS") != new_digest:
1969
+ raise TransactionRecoveryError("staged ledger changed during recovery")
1970
+ os.replace(ledger_temp, ledger)
1971
+ _fsync_directory(ledger.parent)
1972
+ if publish_passport:
1973
+ check_raw, check_document = _recovery_passport_snapshot(
1974
+ passport, "live passport CAS"
1975
+ )
1976
+ if hashlib.sha256(check_raw).hexdigest() != old_passport_digest:
1977
+ raise TransactionRecoveryError("live passport changed during recovery")
1978
+ if _recovery_passport_pointer(check_document, "live passport CAS") != old_pointer:
1979
+ raise TransactionRecoveryError("live passport pointer changed during recovery")
1980
+ check_staged_raw, check_staged_document = _recovery_passport_snapshot(
1981
+ passport_temp, "staged passport CAS"
1982
+ )
1983
+ if hashlib.sha256(check_staged_raw).hexdigest() != new_passport_digest:
1984
+ raise TransactionRecoveryError("staged passport changed during recovery")
1985
+ if (
1986
+ _recovery_passport_pointer(
1987
+ check_staged_document, "staged passport CAS"
1988
+ )
1989
+ != new_pointer
1990
+ ):
1991
+ raise TransactionRecoveryError("staged passport pointer changed during recovery")
1992
+ os.replace(passport_temp, passport)
1993
+ _fsync_directory(passport.parent)
1994
+
1995
+ final_raw, final_document = _recovery_passport_snapshot(
1996
+ passport, "recovered passport"
1997
+ )
1998
+ if hashlib.sha256(final_raw).hexdigest() != new_passport_digest:
1999
+ raise TransactionRecoveryError(
2000
+ "recovered passport digest is not the new passport digest"
2001
+ )
2002
+ if _recovery_passport_pointer(final_document, "recovered passport") != new_pointer:
2003
+ raise TransactionRecoveryError("recovered passport pointer is not the new pointer")
2004
+ if _path_digest(ledger, field="recovered ledger") != new_digest:
2005
+ raise TransactionRecoveryError("recovered ledger digest is not the new digest")
2006
+
2007
+ for temp in (passport_temp, ledger_temp):
2008
+ if temp.exists() or temp.is_symlink():
2009
+ try:
2010
+ _require_regular_nonsymlink(temp, "transaction_temp")
2011
+ except ContractError as exc:
2012
+ raise TransactionRecoveryError(
2013
+ f"invalid transaction temp during cleanup: {exc}"
2014
+ ) from exc
2015
+ temp.unlink()
2016
+ try:
2017
+ _require_regular_nonsymlink(journal_path, "transaction_journal")
2018
+ except ContractError as exc:
2019
+ raise TransactionRecoveryError(
2020
+ f"transaction journal changed during recovery: {exc}"
2021
+ ) from exc
2022
+ journal_path.unlink()
2023
+ _fsync_directory(passport.parent)
2024
+ return True
2025
+
2026
+
2027
+ def recover_ledger_transaction(
2028
+ passport_path: Path | str,
2029
+ workspace_root: Path | str,
2030
+ *,
2031
+ lock_timeout: float = 30.0,
2032
+ ) -> bool:
2033
+ """Recover one interrupted two-file transaction, if a journal exists."""
2034
+
2035
+ root = _workspace_root(workspace_root)
2036
+ _, passport = _authoritative_passport_path(passport_path, root)
2037
+ with _transaction_lock(passport, timeout_seconds=lock_timeout):
2038
+ return _recover_transaction_locked(passport, root)
2039
+
2040
+
2041
+ def _load_bound_locked(
2042
+ passport: Path,
2043
+ root: Path,
2044
+ catalog: Mapping[tuple[str, str, str], Mapping[str, Any]],
2045
+ *,
2046
+ expected_project_ref: str,
2047
+ orphan_relative: str,
2048
+ ) -> dict[str, Any]:
2049
+ document = _load_passport_doc(passport)
2050
+ pointer = _passport_pointer(document)
2051
+ if pointer is None:
2052
+ orphan = _contained_path(root, orphan_relative, "orphan_candidate")
2053
+ if orphan.exists() or orphan.is_symlink():
2054
+ return {
2055
+ "state": "orphan_ignored",
2056
+ "notice": (
2057
+ "ledger file exists without inquiry_ledger_ref; passport is "
2058
+ "authoritative, so the file is ignored"
2059
+ ),
2060
+ "ledger": None,
2061
+ "projection": None,
2062
+ "pointer": None,
2063
+ "ledger_path": str(orphan),
2064
+ }
2065
+ return {
2066
+ "state": "absent",
2067
+ "notice": None,
2068
+ "ledger": None,
2069
+ "projection": None,
2070
+ "pointer": None,
2071
+ "ledger_path": None,
2072
+ }
2073
+
2074
+ ledger_path = _contained_path(
2075
+ root, pointer["ledger_path"], "inquiry_ledger_ref.ledger_path"
2076
+ )
2077
+ try:
2078
+ _assert_safe_ledger_target(passport, ledger_path)
2079
+ except ContractError as exc:
2080
+ raise LedgerBindingError(str(exc)) from exc
2081
+ try:
2082
+ _require_regular_nonsymlink(ledger_path, "ledger_path")
2083
+ raw = ledger_path.read_bytes()
2084
+ ledger = _parse_json_bytes(raw, source=str(ledger_path))
2085
+ if raw != canonical_bytes(ledger):
2086
+ raise ContractError("ledger storage is not exact canonical JSON")
2087
+ digest = hashlib.sha256(raw).hexdigest()
2088
+ if digest != pointer["content_sha256"]:
2089
+ raise ContractError(
2090
+ f"pointer digest {pointer['content_sha256']} does not match ledger {digest}"
2091
+ )
2092
+ if ledger.get("schema_version") != pointer["ledger_version"]:
2093
+ raise ContractError("pointer ledger_version does not match ledger schema_version")
2094
+ projection = replay_ledger(
2095
+ ledger,
2096
+ catalog,
2097
+ expected_project_ref=expected_project_ref,
2098
+ require_materialized=True,
2099
+ )
2100
+ except (OSError, ContractError) as exc:
2101
+ if isinstance(exc, LedgerBindingError):
2102
+ raise
2103
+ raise LedgerBindingError(str(exc)) from exc
2104
+ return {
2105
+ "state": "bound",
2106
+ "notice": None,
2107
+ "ledger": ledger,
2108
+ "projection": projection,
2109
+ "pointer": pointer,
2110
+ "ledger_path": str(ledger_path),
2111
+ }
2112
+
2113
+
2114
+ def load_bound_ledger(
2115
+ passport_path: Path | str,
2116
+ workspace_root: Path | str,
2117
+ profiles: Iterable[Mapping[str, Any]] | Mapping[Any, Mapping[str, Any]],
2118
+ *,
2119
+ expected_project_ref: str | None = None,
2120
+ orphan_candidate: Path | str | None = None,
2121
+ recover: bool = True,
2122
+ lock_timeout: float = 30.0,
2123
+ ) -> dict[str, Any]:
2124
+ """Load the passport-authoritative ledger binding under the stable lock."""
2125
+
2126
+ root = _workspace_root(workspace_root)
2127
+ expected = _text(expected_project_ref, "expected_project_ref")
2128
+ _, passport = _authoritative_passport_path(passport_path, root)
2129
+ catalog = build_profile_catalog(profiles)
2130
+ orphan_relative = (
2131
+ default_ledger_relative_path(passport, root)
2132
+ if orphan_candidate is None
2133
+ else _relative_to_workspace(orphan_candidate, root, "orphan_candidate")
2134
+ )
2135
+ with _transaction_lock(passport, timeout_seconds=lock_timeout):
2136
+ journal = _sidecars(passport)["journal"]
2137
+ if journal.exists() or journal.is_symlink():
2138
+ if not recover:
2139
+ raise TransactionRecoveryError("pending journal; recovery disabled")
2140
+ _recover_transaction_locked(passport, root)
2141
+ return _load_bound_locked(
2142
+ passport,
2143
+ root,
2144
+ catalog,
2145
+ expected_project_ref=expected,
2146
+ orphan_relative=orphan_relative,
2147
+ )
2148
+
2149
+
2150
+ def _assert_append_only(old: Mapping[str, Any], new: Mapping[str, Any]) -> None:
2151
+ for field in ("schema_version", "project_ref", "initial_profile_binding"):
2152
+ if new[field] != old[field]:
2153
+ _fail(f"ledger.{field}", "is immutable after materialization")
2154
+ old_events = old["events"]
2155
+ new_events = new["events"]
2156
+ if len(new_events) <= len(old_events):
2157
+ _fail("ledger.events", "publication must append at least one new event")
2158
+ if new_events[: len(old_events)] != old_events:
2159
+ _fail("ledger.events", "prior event history was rewritten or reordered")
2160
+
2161
+
2162
+ def commit_ledger_transaction(
2163
+ passport_path: Path | str,
2164
+ ledger_path: Path | str,
2165
+ workspace_root: Path | str,
2166
+ ledger: Mapping[str, Any],
2167
+ profiles: Iterable[Mapping[str, Any]] | Mapping[Any, Mapping[str, Any]],
2168
+ project_ref: str | None = None,
2169
+ *,
2170
+ lock_timeout: float = 30.0,
2171
+ crash_hook: Callable[[str], None] | None = None,
2172
+ ) -> dict[str, str]:
2173
+ """Durably publish ledger bytes and their passport pointer as one transaction.
2174
+
2175
+ ``crash_hook`` is a deterministic test seam called after these durable
2176
+ boundaries: ``temps_durable``, ``journal_durable``, ``ledger_published``,
2177
+ ``passport_published``, and ``journal_cleared``. Raising at or after
2178
+ ``journal_durable`` deliberately leaves recovery state intact.
2179
+ """
2180
+
2181
+ root = _workspace_root(workspace_root)
2182
+ expected_project_ref = _text(project_ref, "expected_project_ref")
2183
+ passport_relative, passport = _authoritative_passport_path(passport_path, root)
2184
+ ledger_relative = _relative_to_workspace(ledger_path, root, "ledger_path")
2185
+ ledger_target = _contained_path(root, ledger_relative, "ledger_path")
2186
+ _assert_safe_ledger_target(passport, ledger_target)
2187
+ _require_regular_nonsymlink(ledger_target, "ledger_path", may_absent=True)
2188
+ catalog = build_profile_catalog(profiles)
2189
+ replay_ledger(
2190
+ ledger,
2191
+ catalog,
2192
+ expected_project_ref=expected_project_ref,
2193
+ require_materialized=True,
2194
+ )
2195
+ ledger_bytes = canonical_bytes(ledger)
2196
+ new_digest = hashlib.sha256(ledger_bytes).hexdigest()
2197
+ new_pointer = {
2198
+ "ledger_path": ledger_relative,
2199
+ "ledger_version": POINTER_LEDGER_VERSION,
2200
+ "content_sha256": new_digest,
2201
+ }
2202
+ validate_pointer(new_pointer)
2203
+
2204
+ hook = crash_hook or (lambda _phase: None)
2205
+ sidecars = _sidecars(passport)
2206
+ ledger_temp = _ledger_temp(ledger_target, passport)
2207
+ journal_durable = False
2208
+ with _transaction_lock(passport, timeout_seconds=lock_timeout):
2209
+ if sidecars["journal"].exists() or sidecars["journal"].is_symlink():
2210
+ _recover_transaction_locked(passport, root)
2211
+ _cleanup_unjournaled_temps(passport, ledger_target)
2212
+
2213
+ old_passport_raw, document = _load_passport_snapshot(passport)
2214
+ old_passport_digest = hashlib.sha256(old_passport_raw).hexdigest()
2215
+ old_pointer = _passport_pointer(document)
2216
+ old_digest: str | None = None
2217
+ if old_pointer is None:
2218
+ if ledger_target.exists() or ledger_target.is_symlink():
2219
+ raise ContractError(
2220
+ "ledger file exists without a passport pointer; it is an ignored "
2221
+ "orphan and will not be overwritten or silently adopted"
2222
+ )
2223
+ else:
2224
+ if old_pointer["ledger_path"] != ledger_relative:
2225
+ _fail(
2226
+ "ledger_path",
2227
+ "must equal the existing passport pointer's stable ledger_path",
2228
+ )
2229
+ try:
2230
+ _require_regular_nonsymlink(ledger_target, "ledger_path")
2231
+ old_raw = ledger_target.read_bytes()
2232
+ old_ledger = _parse_json_bytes(old_raw, source=str(ledger_target))
2233
+ if old_raw != canonical_bytes(old_ledger):
2234
+ raise ContractError("existing ledger is not exact canonical JSON")
2235
+ old_digest = hashlib.sha256(old_raw).hexdigest()
2236
+ if old_digest != old_pointer["content_sha256"]:
2237
+ raise ContractError("existing pointer digest does not match ledger")
2238
+ replay_ledger(
2239
+ old_ledger,
2240
+ catalog,
2241
+ expected_project_ref=expected_project_ref,
2242
+ require_materialized=True,
2243
+ )
2244
+ _assert_append_only(old_ledger, ledger)
2245
+ except (OSError, ContractError) as exc:
2246
+ raise LedgerBindingError(str(exc)) from exc
2247
+
2248
+ document["inquiry_ledger_ref"] = copy.deepcopy(new_pointer)
2249
+ passport_bytes = _passport_bytes(document)
2250
+ new_passport_digest = hashlib.sha256(passport_bytes).hexdigest()
2251
+ _write_exclusive_durable(ledger_temp, ledger_bytes)
2252
+ try:
2253
+ passport_mode = stat.S_IMODE(passport.stat().st_mode)
2254
+ _write_exclusive_durable(
2255
+ sidecars["passport_temp"], passport_bytes, mode=passport_mode
2256
+ )
2257
+ except BaseException:
2258
+ ledger_temp.unlink(missing_ok=True)
2259
+ raise
2260
+ hook("temps_durable")
2261
+
2262
+ journal = {
2263
+ "schema_version": TRANSACTION_SCHEMA_VERSION,
2264
+ "transaction_id": uuid.uuid4().hex,
2265
+ "passport_path": passport_relative,
2266
+ "ledger_path": ledger_relative,
2267
+ "passport_temp_path": _relative_to_workspace(
2268
+ sidecars["passport_temp"], root, "passport_temp_path"
2269
+ ),
2270
+ "ledger_temp_path": _relative_to_workspace(
2271
+ ledger_temp, root, "ledger_temp_path"
2272
+ ),
2273
+ "old_pointer": _plain_pointer(old_pointer),
2274
+ "new_pointer": _plain_pointer(new_pointer),
2275
+ "old_ledger_sha256": old_digest,
2276
+ "new_ledger_sha256": new_digest,
2277
+ "old_passport_sha256": old_passport_digest,
2278
+ "new_passport_sha256": new_passport_digest,
2279
+ }
2280
+ _journal_shape(journal)
2281
+ try:
2282
+ _write_journal(sidecars["journal"], sidecars["journal_temp"], journal)
2283
+ journal_durable = True
2284
+ hook("journal_durable")
2285
+
2286
+ if _path_digest(ledger_temp, field="staged ledger") != new_digest:
2287
+ raise TransactionRecoveryError("staged ledger changed before publication")
2288
+ if _path_digest(ledger_target, field="live ledger") != old_digest:
2289
+ raise TransactionRecoveryError("live ledger changed before publication")
2290
+ preflight_passport_raw, preflight_passport = _recovery_passport_snapshot(
2291
+ sidecars["passport_temp"], "staged passport preflight"
2292
+ )
2293
+ if (
2294
+ hashlib.sha256(preflight_passport_raw).hexdigest()
2295
+ != new_passport_digest
2296
+ ):
2297
+ raise TransactionRecoveryError(
2298
+ "staged passport changed before publication"
2299
+ )
2300
+ if (
2301
+ _recovery_passport_pointer(
2302
+ preflight_passport, "staged passport preflight"
2303
+ )
2304
+ != new_pointer
2305
+ ):
2306
+ raise TransactionRecoveryError(
2307
+ "staged passport pointer changed before publication"
2308
+ )
2309
+ preflight_live_raw, preflight_live = _recovery_passport_snapshot(
2310
+ passport, "live passport preflight"
2311
+ )
2312
+ if (
2313
+ hashlib.sha256(preflight_live_raw).hexdigest()
2314
+ != old_passport_digest
2315
+ ):
2316
+ raise TransactionRecoveryError("live passport changed before publication")
2317
+ if (
2318
+ _recovery_passport_pointer(preflight_live, "live passport preflight")
2319
+ != old_pointer
2320
+ ):
2321
+ raise TransactionRecoveryError(
2322
+ "live passport pointer changed before publication"
2323
+ )
2324
+ os.replace(ledger_temp, ledger_target)
2325
+ _fsync_directory(ledger_target.parent)
2326
+ hook("ledger_published")
2327
+
2328
+ staged_passport_raw, staged_passport = _recovery_passport_snapshot(
2329
+ sidecars["passport_temp"], "staged passport"
2330
+ )
2331
+ if hashlib.sha256(staged_passport_raw).hexdigest() != new_passport_digest:
2332
+ raise TransactionRecoveryError(
2333
+ "staged passport changed before publication"
2334
+ )
2335
+ if (
2336
+ _recovery_passport_pointer(staged_passport, "staged passport")
2337
+ != new_pointer
2338
+ ):
2339
+ raise TransactionRecoveryError(
2340
+ "staged passport pointer changed before publication"
2341
+ )
2342
+ live_passport_raw, live_passport = _recovery_passport_snapshot(
2343
+ passport, "live passport CAS"
2344
+ )
2345
+ if hashlib.sha256(live_passport_raw).hexdigest() != old_passport_digest:
2346
+ raise TransactionRecoveryError("live passport changed before publication")
2347
+ if (
2348
+ _recovery_passport_pointer(live_passport, "live passport CAS")
2349
+ != old_pointer
2350
+ ):
2351
+ raise TransactionRecoveryError(
2352
+ "live passport pointer changed before publication"
2353
+ )
2354
+ os.replace(sidecars["passport_temp"], passport)
2355
+ _fsync_directory(passport.parent)
2356
+ hook("passport_published")
2357
+
2358
+ # Verify the committed pair before deleting its recovery witness.
2359
+ committed_raw, committed_document = _recovery_passport_snapshot(
2360
+ passport, "committed passport"
2361
+ )
2362
+ if hashlib.sha256(committed_raw).hexdigest() != new_passport_digest:
2363
+ raise TransactionRecoveryError("committed passport digest mismatch")
2364
+ if (
2365
+ _recovery_passport_pointer(committed_document, "committed passport")
2366
+ != new_pointer
2367
+ ):
2368
+ raise TransactionRecoveryError("committed passport pointer mismatch")
2369
+ if _path_digest(ledger_target, field="committed ledger") != new_digest:
2370
+ raise TransactionRecoveryError("committed ledger digest mismatch")
2371
+ _require_regular_nonsymlink(sidecars["journal"], "transaction_journal")
2372
+ sidecars["journal"].unlink()
2373
+ _fsync_directory(passport.parent)
2374
+ hook("journal_cleared")
2375
+ except BaseException:
2376
+ # os.replace(journal_temp, journal) may have succeeded before a
2377
+ # directory-fsync error was reported. If the journal name is
2378
+ # visible, preserve both staged generations for recovery instead
2379
+ # of converting a recoverable transaction into a broken one.
2380
+ journal_durable = journal_durable or sidecars["journal"].exists()
2381
+ if not journal_durable:
2382
+ for temp in (
2383
+ ledger_temp,
2384
+ sidecars["passport_temp"],
2385
+ sidecars["journal_temp"],
2386
+ ):
2387
+ try:
2388
+ temp.unlink(missing_ok=True)
2389
+ except OSError:
2390
+ pass
2391
+ raise
2392
+ return new_pointer
2393
+
2394
+
2395
+ def _emit_json(value: Any) -> None:
2396
+ print(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True))
2397
+
2398
+
2399
+ def _event_input(path: Path) -> Mapping[str, Any]:
2400
+ try:
2401
+ raw = path.read_bytes()
2402
+ except OSError as exc:
2403
+ raise ContractError(f"cannot read event input {path}: {exc}") from exc
2404
+ value = _parse_json_bytes(raw, source=str(path))
2405
+ _object(value, "event", _EVENT_INPUT_FIELDS)
2406
+ return value
2407
+
2408
+
2409
+ def main(argv: list[str] | None = None) -> int:
2410
+ parser = argparse.ArgumentParser(description=__doc__)
2411
+ subparsers = parser.add_subparsers(dest="command", required=True)
2412
+
2413
+ def add_ledger_inputs(
2414
+ command_parser: argparse.ArgumentParser, *, authoritative: bool = False
2415
+ ) -> None:
2416
+ command_parser.add_argument("ledger", type=Path)
2417
+ command_parser.add_argument(
2418
+ "--profile", action="append", required=True, type=Path
2419
+ )
2420
+ command_parser.add_argument(
2421
+ "--project-ref",
2422
+ required=authoritative,
2423
+ help=(
2424
+ "required project identity for authoritative mutation/display; "
2425
+ "optional only for diagnostic validate/replay"
2426
+ ),
2427
+ )
2428
+
2429
+ validate_parser = subparsers.add_parser("validate")
2430
+ add_ledger_inputs(validate_parser)
2431
+ validate_parser.add_argument("--allow-in-memory", action="store_true")
2432
+
2433
+ replay_parser = subparsers.add_parser("replay")
2434
+ add_ledger_inputs(replay_parser)
2435
+ replay_parser.add_argument("--allow-in-memory", action="store_true")
2436
+
2437
+ append_parser = subparsers.add_parser("append")
2438
+ add_ledger_inputs(append_parser, authoritative=True)
2439
+ append_parser.add_argument("--event", required=True, type=Path)
2440
+
2441
+ summary_parser = subparsers.add_parser("summary")
2442
+ add_ledger_inputs(summary_parser, authoritative=True)
2443
+ summary_parser.add_argument("--moment", choices=SUMMARY_MOMENTS, required=True)
2444
+ summary_parser.add_argument("--signal-event-id", type=int)
2445
+ summary_parser.add_argument(
2446
+ "--force",
2447
+ action="store_true",
2448
+ help="render for diagnostics even when ARS_INQUIRY_LEDGER is not 1",
2449
+ )
2450
+
2451
+ load_parser = subparsers.add_parser("load-bound")
2452
+ load_parser.add_argument("--passport", required=True, type=Path)
2453
+ load_parser.add_argument("--workspace-root", required=True, type=Path)
2454
+ load_parser.add_argument("--profile", action="append", required=True, type=Path)
2455
+ load_parser.add_argument("--project-ref", required=True)
2456
+ load_parser.add_argument("--orphan-candidate", type=Path)
2457
+ load_parser.add_argument("--no-recover", action="store_true")
2458
+
2459
+ commit_parser = subparsers.add_parser("commit")
2460
+ commit_parser.add_argument("--passport", required=True, type=Path)
2461
+ commit_parser.add_argument("--workspace-root", required=True, type=Path)
2462
+ commit_parser.add_argument("--ledger-path", required=True, type=Path)
2463
+ commit_parser.add_argument("--ledger", required=True, type=Path)
2464
+ commit_parser.add_argument("--profile", action="append", required=True, type=Path)
2465
+ commit_parser.add_argument("--project-ref", required=True)
2466
+
2467
+ args = parser.parse_args(argv)
2468
+ try:
2469
+ if args.command in {"validate", "replay", "append", "summary"}:
2470
+ catalog = load_profile_catalog(args.profile)
2471
+ ledger = load_ledger(args.ledger)
2472
+ if args.command == "validate":
2473
+ replay_ledger(
2474
+ ledger,
2475
+ catalog,
2476
+ expected_project_ref=args.project_ref,
2477
+ require_materialized=not args.allow_in_memory,
2478
+ )
2479
+ _emit_json({"status": "valid", "schema_version": LEDGER_SCHEMA_VERSION})
2480
+ elif args.command == "replay":
2481
+ _emit_json(
2482
+ replay_ledger(
2483
+ ledger,
2484
+ catalog,
2485
+ expected_project_ref=args.project_ref,
2486
+ require_materialized=not args.allow_in_memory,
2487
+ )
2488
+ )
2489
+ elif args.command == "append":
2490
+ updated = append_event(
2491
+ ledger,
2492
+ _event_input(args.event),
2493
+ catalog,
2494
+ expected_project_ref=args.project_ref,
2495
+ )
2496
+ sys.stdout.buffer.write(canonical_bytes(updated))
2497
+ elif args.command == "summary":
2498
+ projection = replay_ledger(
2499
+ ledger, catalog, expected_project_ref=args.project_ref
2500
+ )
2501
+ env = {ENV_FLAG: "1"} if args.force else None
2502
+ rendered = checkpoint_summary(
2503
+ projection,
2504
+ moment=args.moment,
2505
+ env=env,
2506
+ signal_event_id=args.signal_event_id,
2507
+ )
2508
+ if rendered is not None:
2509
+ print(rendered)
2510
+ elif args.command == "load-bound":
2511
+ catalog = load_profile_catalog(args.profile)
2512
+ result = load_bound_ledger(
2513
+ args.passport,
2514
+ args.workspace_root,
2515
+ catalog,
2516
+ expected_project_ref=args.project_ref,
2517
+ orphan_candidate=args.orphan_candidate,
2518
+ recover=not args.no_recover,
2519
+ )
2520
+ _emit_json(result)
2521
+ elif args.command == "commit":
2522
+ catalog = load_profile_catalog(args.profile)
2523
+ ledger = load_ledger(args.ledger)
2524
+ pointer = commit_ledger_transaction(
2525
+ args.passport,
2526
+ args.ledger_path,
2527
+ args.workspace_root,
2528
+ ledger,
2529
+ catalog,
2530
+ args.project_ref,
2531
+ )
2532
+ _emit_json(pointer)
2533
+ return 0
2534
+ except (ContractError, ProfileContractError) as exc:
2535
+ print(f"[inquiry_branch_ledger ERROR] {exc}", file=sys.stderr)
2536
+ return 2
2537
+
2538
+
2539
+ if __name__ == "__main__":
2540
+ sys.exit(main())