psyclaw 0.27.22 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -5
- package/dist/apps/panel/index.html +2 -2
- package/dist/src/adapters/pi/extension.js +225 -92
- package/dist/src/adapters/pi/extension.js.map +1 -1
- package/dist/src/agents/import.js +44 -32
- package/dist/src/agents/import.js.map +1 -1
- package/dist/src/analysis/hooks.d.ts +1 -0
- package/dist/src/analysis/hooks.js +20 -1
- package/dist/src/analysis/hooks.js.map +1 -1
- package/dist/src/ars/bridge.d.ts +20 -0
- package/dist/src/ars/bridge.js +132 -0
- package/dist/src/ars/bridge.js.map +1 -0
- package/dist/src/ars/contracts.d.ts +55 -0
- package/dist/src/ars/contracts.js +2 -0
- package/dist/src/ars/contracts.js.map +1 -0
- package/dist/src/ars/panel-plan.d.ts +4 -0
- package/dist/src/ars/panel-plan.js +11 -0
- package/dist/src/ars/panel-plan.js.map +1 -0
- package/dist/src/ars/pi-panel-executor.d.ts +30 -0
- package/dist/src/ars/pi-panel-executor.js +140 -0
- package/dist/src/ars/pi-panel-executor.js.map +1 -0
- package/dist/src/ars/profile.d.ts +36 -0
- package/dist/src/ars/profile.js +119 -0
- package/dist/src/ars/profile.js.map +1 -0
- package/dist/src/ars/re-review.d.ts +22 -0
- package/dist/src/ars/re-review.js +172 -0
- package/dist/src/ars/re-review.js.map +1 -0
- package/dist/src/branding.d.ts +2 -4
- package/dist/src/branding.js +3 -5
- package/dist/src/branding.js.map +1 -1
- package/dist/src/bundled-tools.d.ts +3 -0
- package/dist/src/bundled-tools.js +21 -0
- package/dist/src/bundled-tools.js.map +1 -0
- package/dist/src/chat.js +6 -5
- package/dist/src/chat.js.map +1 -1
- package/dist/src/creation/contracts.d.ts +38 -0
- package/dist/src/creation/contracts.js +2 -0
- package/dist/src/creation/contracts.js.map +1 -0
- package/dist/src/creation/service.d.ts +7 -0
- package/dist/src/creation/service.js +196 -0
- package/dist/src/creation/service.js.map +1 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +10 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/install/installer.js +32 -10
- package/dist/src/install/installer.js.map +1 -1
- package/dist/src/orchestration/personas.d.ts +15 -0
- package/dist/src/orchestration/personas.js +51 -0
- package/dist/src/orchestration/personas.js.map +1 -0
- package/dist/src/orchestration/pi-executor.d.ts +1 -0
- package/dist/src/orchestration/pi-executor.js +1 -1
- package/dist/src/orchestration/pi-executor.js.map +1 -1
- package/dist/src/panel/extension.js +29 -8
- package/dist/src/panel/extension.js.map +1 -1
- package/dist/src/panel/server.js +14 -5
- package/dist/src/panel/server.js.map +1 -1
- package/dist/src/project/paths.d.ts +3 -0
- package/dist/src/project/paths.js +7 -0
- package/dist/src/project/paths.js.map +1 -1
- package/dist/src/rules/user-rules.d.ts +8 -0
- package/dist/src/rules/user-rules.js +36 -0
- package/dist/src/rules/user-rules.js.map +1 -0
- package/dist/src/skills/contracts.d.ts +4 -4
- package/dist/src/skills/recommended.js +1 -1
- package/dist/src/skills/registry.js +49 -28
- package/dist/src/skills/registry.js.map +1 -1
- package/dist/src/style/cli-ui.js +0 -1
- package/dist/src/style/cli-ui.js.map +1 -1
- package/package.json +14 -3
- package/scripts/rebrand-pi.mjs +6 -0
- package/skills/recommended/catalog.json +2 -11
- package/vendor/ars/.claude/CLAUDE.md +371 -0
- package/vendor/ars/.command-invariants.toml +24 -0
- package/vendor/ars/CITATION.cff +35 -0
- package/vendor/ars/LICENSE +417 -0
- package/vendor/ars/MODE_REGISTRY.md +76 -0
- package/vendor/ars/NOTICE.md +26 -0
- package/vendor/ars/POSITIONING.md +99 -0
- package/vendor/ars/PSYCLAW_SOURCE.json +10 -0
- package/vendor/ars/README.md +751 -0
- package/vendor/ars/SECURITY.md +52 -0
- package/vendor/ars/THIRD_PARTY.md +70 -0
- package/vendor/ars/academic-paper/SKILL.md +542 -0
- package/vendor/ars/academic-paper/agents/abstract_bilingual_agent.md +171 -0
- package/vendor/ars/academic-paper/agents/argument_builder_agent.md +276 -0
- package/vendor/ars/academic-paper/agents/citation_compliance_agent.md +422 -0
- package/vendor/ars/academic-paper/agents/draft_writer_agent.md +656 -0
- package/vendor/ars/academic-paper/agents/formatter_agent.md +999 -0
- package/vendor/ars/academic-paper/agents/intake_agent.md +393 -0
- package/vendor/ars/academic-paper/agents/literature_strategist_agent.md +626 -0
- package/vendor/ars/academic-paper/agents/peer_reviewer_agent.md +516 -0
- package/vendor/ars/academic-paper/agents/revision_coach_agent.md +334 -0
- package/vendor/ars/academic-paper/agents/socratic_mentor_agent.md +527 -0
- package/vendor/ars/academic-paper/agents/structure_architect_agent.md +401 -0
- package/vendor/ars/academic-paper/agents/visualization_agent.md +441 -0
- package/vendor/ars/academic-paper/examples/chinese_paper_example.md +278 -0
- package/vendor/ars/academic-paper/examples/clinical_citation_verification_checklist.md +95 -0
- package/vendor/ars/academic-paper/examples/clinical_epistemic_status_example.md +100 -0
- package/vendor/ars/academic-paper/examples/commitment_ledger_example.md +147 -0
- package/vendor/ars/academic-paper/examples/imrad_hei_example.md +234 -0
- package/vendor/ars/academic-paper/examples/literature_review_example.md +260 -0
- package/vendor/ars/academic-paper/examples/plan_mode_guided_writing.md +600 -0
- package/vendor/ars/academic-paper/examples/revision_mode_example.md +344 -0
- package/vendor/ars/academic-paper/examples/revision_recovery_example.md +506 -0
- package/vendor/ars/academic-paper/examples/version_family_reconciliation_example.md +89 -0
- package/vendor/ars/academic-paper/references/abstract_writing_guide.md +169 -0
- package/vendor/ars/academic-paper/references/academic_writing_style.md +188 -0
- package/vendor/ars/academic-paper/references/anti_leakage_protocol.md +83 -0
- package/vendor/ars/academic-paper/references/apa7_chinese_citation_guide.md +364 -0
- package/vendor/ars/academic-paper/references/apa7_extended_guide.md +198 -0
- package/vendor/ars/academic-paper/references/changelog.md +11 -0
- package/vendor/ars/academic-paper/references/citation_format_switcher.md +228 -0
- package/vendor/ars/academic-paper/references/committee_correspondence_protocol.md +158 -0
- package/vendor/ars/academic-paper/references/credit_authorship_guide.md +308 -0
- package/vendor/ars/academic-paper/references/disclosure_mode_protocol.md +478 -0
- package/vendor/ars/academic-paper/references/domain_evidence_profiles.md +38 -0
- package/vendor/ars/academic-paper/references/failure_paths.md +349 -0
- package/vendor/ars/academic-paper/references/funding_statement_guide.md +319 -0
- package/vendor/ars/academic-paper/references/hei_domain_glossary.md +169 -0
- package/vendor/ars/academic-paper/references/intro_title_rhetoric_guide.md +114 -0
- package/vendor/ars/academic-paper/references/journal_submission_guide.md +249 -0
- package/vendor/ars/academic-paper/references/latex_template_reference.md +378 -0
- package/vendor/ars/academic-paper/references/mode_selection_guide.md +378 -0
- package/vendor/ars/academic-paper/references/paper_structure_patterns.md +330 -0
- package/vendor/ars/academic-paper/references/plan_mode_protocol.md +112 -0
- package/vendor/ars/academic-paper/references/policy_anchor_disclosure_protocol.md +200 -0
- package/vendor/ars/academic-paper/references/policy_anchor_table.md +157 -0
- package/vendor/ars/academic-paper/references/revision_patch_protocol.md +173 -0
- package/vendor/ars/academic-paper/references/statistical_visualization_standards.md +750 -0
- package/vendor/ars/academic-paper/references/venue_disclosure_policies.md +259 -0
- package/vendor/ars/academic-paper/references/vlm_figure_verification.md +126 -0
- package/vendor/ars/academic-paper/references/workflow_phase_details.md +135 -0
- package/vendor/ars/academic-paper/references/writing_judgment_framework.md +59 -0
- package/vendor/ars/academic-paper/references/writing_quality_check.md +173 -0
- package/vendor/ars/academic-paper/templates/bilingual_abstract_template.md +78 -0
- package/vendor/ars/academic-paper/templates/case_study_template.md +129 -0
- package/vendor/ars/academic-paper/templates/conference_paper_template.md +108 -0
- package/vendor/ars/academic-paper/templates/credit_statement_template.md +132 -0
- package/vendor/ars/academic-paper/templates/funding_statement_template.md +290 -0
- package/vendor/ars/academic-paper/templates/imrad_template.md +183 -0
- package/vendor/ars/academic-paper/templates/latex_article_template.tex +199 -0
- package/vendor/ars/academic-paper/templates/literature_review_template.md +135 -0
- package/vendor/ars/academic-paper/templates/policy_brief_template.md +139 -0
- package/vendor/ars/academic-paper/templates/revision_tracking_template.md +199 -0
- package/vendor/ars/academic-paper/templates/theoretical_paper_template.md +119 -0
- package/vendor/ars/academic-paper-reviewer/SKILL.md +491 -0
- package/vendor/ars/academic-paper-reviewer/agents/devils_advocate_reviewer_agent.md +443 -0
- package/vendor/ars/academic-paper-reviewer/agents/domain_reviewer_agent.md +412 -0
- package/vendor/ars/academic-paper-reviewer/agents/editorial_synthesizer_agent.md +478 -0
- package/vendor/ars/academic-paper-reviewer/agents/eic_agent.md +339 -0
- package/vendor/ars/academic-paper-reviewer/agents/field_analyst_agent.md +221 -0
- package/vendor/ars/academic-paper-reviewer/agents/methodology_reviewer_agent.md +449 -0
- package/vendor/ars/academic-paper-reviewer/agents/perspective_reviewer_agent.md +427 -0
- package/vendor/ars/academic-paper-reviewer/examples/hei_paper_review_example.md +391 -0
- package/vendor/ars/academic-paper-reviewer/examples/interdisciplinary_review_example.md +299 -0
- package/vendor/ars/academic-paper-reviewer/examples/subclaim_decomposition_example.md +80 -0
- package/vendor/ars/academic-paper-reviewer/references/calibration_mode_protocol.md +256 -0
- package/vendor/ars/academic-paper-reviewer/references/changelog.md +10 -0
- package/vendor/ars/academic-paper-reviewer/references/editorial_decision_standards.md +236 -0
- package/vendor/ars/academic-paper-reviewer/references/guided_mode_protocol.md +34 -0
- package/vendor/ars/academic-paper-reviewer/references/integration_guide.md +15 -0
- package/vendor/ars/academic-paper-reviewer/references/quality_rubrics.md +84 -0
- package/vendor/ars/academic-paper-reviewer/references/re_review_mode_protocol.md +340 -0
- package/vendor/ars/academic-paper-reviewer/references/review_criteria_framework.md +98 -0
- package/vendor/ars/academic-paper-reviewer/references/review_panel_provenance_protocol.md +197 -0
- package/vendor/ars/academic-paper-reviewer/references/review_quality_thinking.md +58 -0
- package/vendor/ars/academic-paper-reviewer/references/reviewer_sprint_prompt_source.md +324 -0
- package/vendor/ars/academic-paper-reviewer/references/sprint_contract_protocol.md +296 -0
- package/vendor/ars/academic-paper-reviewer/references/statistical_reporting_standards.md +505 -0
- package/vendor/ars/academic-paper-reviewer/references/top_journals_by_field.md +206 -0
- package/vendor/ars/academic-paper-reviewer/templates/editorial_decision_template.md +235 -0
- package/vendor/ars/academic-paper-reviewer/templates/peer_review_report_template.md +305 -0
- package/vendor/ars/academic-paper-reviewer/templates/revision_response_template.md +248 -0
- package/vendor/ars/academic-pipeline/SKILL.md +736 -0
- package/vendor/ars/academic-pipeline/agents/claim_ref_alignment_audit_agent.md +382 -0
- package/vendor/ars/academic-pipeline/agents/collaboration_depth_agent.md +164 -0
- package/vendor/ars/academic-pipeline/agents/integrity_verification_agent.md +870 -0
- package/vendor/ars/academic-pipeline/agents/pipeline_orchestrator_agent.md +1379 -0
- package/vendor/ars/academic-pipeline/agents/state_tracker_agent.md +622 -0
- package/vendor/ars/academic-pipeline/examples/full_pipeline_example.md +482 -0
- package/vendor/ars/academic-pipeline/examples/integrity_failure_recovery.md +389 -0
- package/vendor/ars/academic-pipeline/examples/mid_entry_example.md +414 -0
- package/vendor/ars/academic-pipeline/references/adapters/.gitkeep +0 -0
- package/vendor/ars/academic-pipeline/references/adapters/overview.md +153 -0
- package/vendor/ars/academic-pipeline/references/ai_research_failure_modes.md +185 -0
- package/vendor/ars/academic-pipeline/references/changelog.md +14 -0
- package/vendor/ars/academic-pipeline/references/claim_audit_calibration_protocol.md +175 -0
- package/vendor/ars/academic-pipeline/references/claim_verification_protocol.md +282 -0
- package/vendor/ars/academic-pipeline/references/external_review_protocol.md +131 -0
- package/vendor/ars/academic-pipeline/references/integrity_review_protocol.md +110 -0
- package/vendor/ars/academic-pipeline/references/literature_corpus_consumers.md +193 -0
- package/vendor/ars/academic-pipeline/references/mode_advisor.md +135 -0
- package/vendor/ars/academic-pipeline/references/passport_as_reset_boundary.md +132 -0
- package/vendor/ars/academic-pipeline/references/pipeline_state_machine.md +405 -0
- package/vendor/ars/academic-pipeline/references/plagiarism_detection_protocol.md +239 -0
- package/vendor/ars/academic-pipeline/references/process_summary_protocol.md +209 -0
- package/vendor/ars/academic-pipeline/references/progress_dashboard_template.md +38 -0
- package/vendor/ars/academic-pipeline/references/reinforcement_content.md +15 -0
- package/vendor/ars/academic-pipeline/references/reproducibility_audit.md +55 -0
- package/vendor/ars/academic-pipeline/references/score_trajectory_protocol.md +78 -0
- package/vendor/ars/academic-pipeline/references/team_collaboration_protocol.md +261 -0
- package/vendor/ars/academic-pipeline/references/two_stage_review_protocol.md +27 -0
- package/vendor/ars/academic-pipeline/templates/pipeline_status_template.md +146 -0
- package/vendor/ars/agents/report_compiler_agent.md +341 -0
- package/vendor/ars/agents/research_architect_agent.md +298 -0
- package/vendor/ars/agents/synthesis_agent.md +356 -0
- package/vendor/ars/commands/ars-3w.md +10 -0
- package/vendor/ars/commands/ars-abstract.md +10 -0
- package/vendor/ars/commands/ars-cache-invalidate.md +20 -0
- package/vendor/ars/commands/ars-citation-check.md +10 -0
- package/vendor/ars/commands/ars-disclosure.md +10 -0
- package/vendor/ars/commands/ars-format-convert.md +10 -0
- package/vendor/ars/commands/ars-full.md +9 -0
- package/vendor/ars/commands/ars-lit-review.md +12 -0
- package/vendor/ars/commands/ars-mark-read.md +18 -0
- package/vendor/ars/commands/ars-outline.md +10 -0
- package/vendor/ars/commands/ars-plan.md +10 -0
- package/vendor/ars/commands/ars-rebuttal-audit.md +12 -0
- package/vendor/ars/commands/ars-reviewer.md +9 -0
- package/vendor/ars/commands/ars-revision-coach.md +9 -0
- package/vendor/ars/commands/ars-revision.md +10 -0
- package/vendor/ars/commands/ars-unmark-read.md +16 -0
- package/vendor/ars/deep-research/SKILL.md +600 -0
- package/vendor/ars/deep-research/agents/bibliography_agent.md +473 -0
- package/vendor/ars/deep-research/agents/devils_advocate_agent.md +192 -0
- package/vendor/ars/deep-research/agents/editor_in_chief_agent.md +167 -0
- package/vendor/ars/deep-research/agents/ethics_review_agent.md +267 -0
- package/vendor/ars/deep-research/agents/meta_analysis_agent.md +325 -0
- package/vendor/ars/deep-research/agents/monitoring_agent.md +209 -0
- package/vendor/ars/deep-research/agents/report_compiler_agent.md +341 -0
- package/vendor/ars/deep-research/agents/research_architect_agent.md +298 -0
- package/vendor/ars/deep-research/agents/research_question_agent.md +216 -0
- package/vendor/ars/deep-research/agents/risk_of_bias_agent.md +231 -0
- package/vendor/ars/deep-research/agents/socratic_mentor_agent.md +764 -0
- package/vendor/ars/deep-research/agents/source_verification_agent.md +219 -0
- package/vendor/ars/deep-research/agents/synthesis_agent.md +356 -0
- package/vendor/ars/deep-research/agents/timeline_extraction_agent.md +99 -0
- package/vendor/ars/deep-research/examples/exploratory_research.md +157 -0
- package/vendor/ars/deep-research/examples/fact_check_mode.md +173 -0
- package/vendor/ars/deep-research/examples/handoff_to_paper.md +318 -0
- package/vendor/ars/deep-research/examples/idea_diversity_coverage_gap_advisory.md +64 -0
- package/vendor/ars/deep-research/examples/policy_analysis.md +161 -0
- package/vendor/ars/deep-research/examples/review_mode.md +253 -0
- package/vendor/ars/deep-research/examples/socratic_guided_research.md +331 -0
- package/vendor/ars/deep-research/examples/systematic_review.md +133 -0
- package/vendor/ars/deep-research/references/apa7_style_guide.md +162 -0
- package/vendor/ars/deep-research/references/argumentation_reasoning_framework.md +68 -0
- package/vendor/ars/deep-research/references/arxiv_api_protocol.md +76 -0
- package/vendor/ars/deep-research/references/changelog.md +22 -0
- package/vendor/ars/deep-research/references/chinese_literature_api_protocol.md +317 -0
- package/vendor/ars/deep-research/references/cross_agent_quality_definitions.md +14 -0
- package/vendor/ars/deep-research/references/crossref_api_protocol.md +84 -0
- package/vendor/ars/deep-research/references/equator_reporting_guidelines.md +482 -0
- package/vendor/ars/deep-research/references/ethics_checklist.md +282 -0
- package/vendor/ars/deep-research/references/failure_paths.md +355 -0
- package/vendor/ars/deep-research/references/interdisciplinary_bridges.md +292 -0
- package/vendor/ars/deep-research/references/irb_decision_tree.md +315 -0
- package/vendor/ars/deep-research/references/literature_monitoring_strategies.md +263 -0
- package/vendor/ars/deep-research/references/logical_fallacies.md +192 -0
- package/vendor/ars/deep-research/references/methodology_patterns.md +462 -0
- package/vendor/ars/deep-research/references/mode_selection_guide.md +331 -0
- package/vendor/ars/deep-research/references/openalex_api_protocol.md +82 -0
- package/vendor/ars/deep-research/references/preregistration_guide.md +324 -0
- package/vendor/ars/deep-research/references/semantic_scholar_api_protocol.md +107 -0
- package/vendor/ars/deep-research/references/socratic_mode_protocol.md +99 -0
- package/vendor/ars/deep-research/references/socratic_questioning_framework.md +232 -0
- package/vendor/ars/deep-research/references/source_quality_hierarchy.md +188 -0
- package/vendor/ars/deep-research/references/systematic_review_protocol.md +95 -0
- package/vendor/ars/deep-research/references/systematic_review_toolkit.md +353 -0
- package/vendor/ars/deep-research/templates/evidence_assessment_template.md +127 -0
- package/vendor/ars/deep-research/templates/literature_matrix_template.md +85 -0
- package/vendor/ars/deep-research/templates/preregistration_template.md +318 -0
- package/vendor/ars/deep-research/templates/prisma_protocol_template.md +248 -0
- package/vendor/ars/deep-research/templates/prisma_report_template.md +415 -0
- package/vendor/ars/deep-research/templates/research_brief_template.md +93 -0
- package/vendor/ars/package.json +24 -0
- package/vendor/ars/pi/README.md +161 -0
- package/vendor/ars/pi/package.json +26 -0
- package/vendor/ars/pi/wrapper.js +193 -0
- package/vendor/ars/pi/wrapper.test.mjs +201 -0
- package/vendor/ars/pyproject.toml +2 -0
- package/vendor/ars/requirements-pdf-content-classifier.txt +5 -0
- package/vendor/ars/scripts/_block_parser.py +396 -0
- package/vendor/ars/scripts/_ci_pytest_manifest.toml +661 -0
- package/vendor/ars/scripts/_claim_audit_constants.py +268 -0
- package/vendor/ars/scripts/_e4_evidence.py +110 -0
- package/vendor/ars/scripts/_eval_threshold_gate.py +73 -0
- package/vendor/ars/scripts/_markdown_lint_util.py +224 -0
- package/vendor/ars/scripts/_next_verified_at_ms.py +176 -0
- package/vendor/ars/scripts/_passport_yaml.py +53 -0
- package/vendor/ars/scripts/_skill_lint.py +254 -0
- package/vendor/ars/scripts/_text_similarity.py +141 -0
- package/vendor/ars/scripts/adapters/README.md +89 -0
- package/vendor/ars/scripts/adapters/_common.py +209 -0
- package/vendor/ars/scripts/adapters/examples/folder_scan/expected_passport.yaml +25 -0
- package/vendor/ars/scripts/adapters/examples/folder_scan/expected_rejection_log.yaml +18 -0
- package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/Chen2024_AIAssessment.pdf +0 -0
- package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/Wang_2023_formative_feedback.pdf +0 -0
- package/vendor/ars/scripts/adapters/examples/folder_scan/input_fixture/paper1.pdf +0 -0
- 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
- package/vendor/ars/scripts/adapters/examples/obsidian/expected_passport.yaml +40 -0
- package/vendor/ars/scripts/adapters/examples/obsidian/expected_rejection_log.yaml +13 -0
- package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/.gitkeep +0 -0
- package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/.obsidian/app.json +1 -0
- package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/_templates/tmpl.md +7 -0
- package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/chen2024ai.md +13 -0
- package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/invalid.md +1 -0
- package/vendor/ars/scripts/adapters/examples/obsidian/input_fixture/vault/wang2023formative.md +10 -0
- package/vendor/ars/scripts/adapters/examples/zotero/expected_passport.yaml +34 -0
- package/vendor/ars/scripts/adapters/examples/zotero/expected_rejection_log.yaml +34 -0
- package/vendor/ars/scripts/adapters/examples/zotero/input_fixture/.gitkeep +0 -0
- package/vendor/ars/scripts/adapters/examples/zotero/input_fixture/export.json +43 -0
- package/vendor/ars/scripts/adapters/folder_scan.py +214 -0
- package/vendor/ars/scripts/adapters/obsidian.py +336 -0
- package/vendor/ars/scripts/adapters/tests/.gitkeep +0 -0
- package/vendor/ars/scripts/adapters/tests/conftest.py +77 -0
- package/vendor/ars/scripts/adapters/tests/test_check_corpus_consumer_protocol.py +632 -0
- package/vendor/ars/scripts/adapters/tests/test_check_literature_corpus_schema.py +631 -0
- package/vendor/ars/scripts/adapters/tests/test_common.py +365 -0
- package/vendor/ars/scripts/adapters/tests/test_conftest.py +96 -0
- package/vendor/ars/scripts/adapters/tests/test_folder_scan.py +260 -0
- package/vendor/ars/scripts/adapters/tests/test_literature_corpus_entry_schema.py +745 -0
- package/vendor/ars/scripts/adapters/tests/test_obsidian.py +357 -0
- package/vendor/ars/scripts/adapters/tests/test_rejection_log_schema.py +271 -0
- package/vendor/ars/scripts/adapters/tests/test_sync_adapter_docs.py +88 -0
- package/vendor/ars/scripts/adapters/tests/test_zotero.py +454 -0
- package/vendor/ars/scripts/adapters/zotero.py +318 -0
- package/vendor/ars/scripts/adjudication_activity.py +1592 -0
- package/vendor/ars/scripts/announce-ars-loaded.sh +144 -0
- package/vendor/ars/scripts/ars_anchorize_draft.py +170 -0
- package/vendor/ars/scripts/ars_apply_revision_patch.py +912 -0
- package/vendor/ars/scripts/ars_cache_invalidate.py +40 -0
- package/vendor/ars/scripts/ars_mark_read.py +521 -0
- package/vendor/ars/scripts/ars_phase_scope_manifest.json +33 -0
- package/vendor/ars/scripts/ars_update_check.sh +215 -0
- package/vendor/ars/scripts/ars_write_scope_guard.py +506 -0
- package/vendor/ars/scripts/arxiv_client.py +222 -0
- package/vendor/ars/scripts/audit_snapshot.py +572 -0
- package/vendor/ars/scripts/bibliographic_integrity_signals.py +800 -0
- package/vendor/ars/scripts/bootstrap_timeline_yaml.py +146 -0
- package/vendor/ars/scripts/build_claim_standing_candidate_ledger.py +1238 -0
- package/vendor/ars/scripts/build_claim_standing_query_plan.py +643 -0
- package/vendor/ars/scripts/build_content_coverage_advisory.py +1205 -0
- package/vendor/ars/scripts/build_cross_document_consistency_advisory.py +2332 -0
- package/vendor/ars/scripts/build_review_pathway_rule_trace.py +830 -0
- package/vendor/ars/scripts/build_submission_packet_manifest.py +2310 -0
- package/vendor/ars/scripts/check_215_field_norm.py +173 -0
- package/vendor/ars/scripts/check_216_surface_form.py +250 -0
- package/vendor/ars/scripts/check_268_nested_commitment_ledger.py +180 -0
- package/vendor/ars/scripts/check_390_revision_patch_discipline.py +296 -0
- package/vendor/ars/scripts/check_392_citation_verification_intake.py +126 -0
- package/vendor/ars/scripts/check_394_submission_policy.py +178 -0
- package/vendor/ars/scripts/check_439_format_profile.py +307 -0
- package/vendor/ars/scripts/check_619_disclosure_closeout.py +237 -0
- package/vendor/ars/scripts/check_630_codex_subscription_transport.py +456 -0
- package/vendor/ars/scripts/check_669_review_pathway_rule_trace.py +662 -0
- package/vendor/ars/scripts/check_670_revision_roadmap_integration.py +518 -0
- package/vendor/ars/scripts/check_673_adjudication_activity.py +684 -0
- package/vendor/ars/scripts/check_684_review_criteria_binding.py +557 -0
- package/vendor/ars/scripts/check_agents_mirror_sync.py +115 -0
- package/vendor/ars/scripts/check_audit_artifact_consistency.py +2313 -0
- package/vendor/ars/scripts/check_benchmark_report.py +79 -0
- package/vendor/ars/scripts/check_bibliographic_integrity_signals.py +831 -0
- package/vendor/ars/scripts/check_calibration_tiers.py +235 -0
- package/vendor/ars/scripts/check_changelog_covers_merges.py +289 -0
- package/vendor/ars/scripts/check_ci_pytest_manifest.py +204 -0
- package/vendor/ars/scripts/check_claim_audit_consistency.py +1664 -0
- package/vendor/ars/scripts/check_claim_standing_candidate_ledger_integration.py +500 -0
- package/vendor/ars/scripts/check_claim_standing_freshness.py +253 -0
- package/vendor/ars/scripts/check_claim_standing_transmissions.py +449 -0
- package/vendor/ars/scripts/check_collaboration_depth_rubric.py +180 -0
- package/vendor/ars/scripts/check_command_frontmatter_name.py +116 -0
- package/vendor/ars/scripts/check_committee_correspondence.py +333 -0
- package/vendor/ars/scripts/check_compliance_report.py +108 -0
- package/vendor/ars/scripts/check_content_coverage_advisory_integration.py +796 -0
- package/vendor/ars/scripts/check_control_availability.py +172 -0
- package/vendor/ars/scripts/check_corpus_consumer_protocol.py +404 -0
- package/vendor/ars/scripts/check_cross_document_consistency_advisory_integration.py +1191 -0
- package/vendor/ars/scripts/check_cross_model_handoff_contract.py +234 -0
- package/vendor/ars/scripts/check_cross_model_verification_sync.py +261 -0
- package/vendor/ars/scripts/check_data_access_level.py +131 -0
- package/vendor/ars/scripts/check_data_flows.py +252 -0
- package/vendor/ars/scripts/check_decision_contract.py +464 -0
- package/vendor/ars/scripts/check_degradation_registry.py +326 -0
- package/vendor/ars/scripts/check_distribution_surface_claims.py +226 -0
- package/vendor/ars/scripts/check_domain_evidence_profile.py +538 -0
- package/vendor/ars/scripts/check_e4_promotion.py +195 -0
- package/vendor/ars/scripts/check_evals_gold_set.py +279 -0
- package/vendor/ars/scripts/check_evidence_row_integration.py +396 -0
- package/vendor/ars/scripts/check_experiment_provenance.py +117 -0
- package/vendor/ars/scripts/check_field_norm_severity.py +144 -0
- package/vendor/ars/scripts/check_firm_rules_sync.py +375 -0
- package/vendor/ars/scripts/check_heldout_measurement_report.py +1179 -0
- package/vendor/ars/scripts/check_human_subjects_output_contract.py +139 -0
- package/vendor/ars/scripts/check_human_subjects_reference_migration.py +844 -0
- package/vendor/ars/scripts/check_indirect_prompt_injection_no_call.py +328 -0
- package/vendor/ars/scripts/check_instruction_data_boundary.py +236 -0
- package/vendor/ars/scripts/check_judge_prompt_version.py +125 -0
- package/vendor/ars/scripts/check_literature_corpus_schema.py +402 -0
- package/vendor/ars/scripts/check_model_tiering.py +223 -0
- package/vendor/ars/scripts/check_panel_synthesis.py +1331 -0
- package/vendor/ars/scripts/check_passport_reset_contract.py +214 -0
- package/vendor/ars/scripts/check_pattern_eval_manifest.py +422 -0
- package/vendor/ars/scripts/check_persuasion_invariance_fixtures.py +637 -0
- package/vendor/ars/scripts/check_phase_conformance.py +2180 -0
- package/vendor/ars/scripts/check_pipeline_boundary_semantics.py +600 -0
- package/vendor/ars/scripts/check_pipeline_integrity.py +340 -0
- package/vendor/ars/scripts/check_policy_anchor_protocol.py +159 -0
- package/vendor/ars/scripts/check_policy_anchor_table.py +286 -0
- package/vendor/ars/scripts/check_preprint_venues_consistency.py +123 -0
- package/vendor/ars/scripts/check_prisma_trAIce_freshness.py +84 -0
- package/vendor/ars/scripts/check_promotion_bakeoff_preregistration.py +1303 -0
- package/vendor/ars/scripts/check_ranking_lift.py +323 -0
- package/vendor/ars/scripts/check_re_review_synthesis.py +2719 -0
- package/vendor/ars/scripts/check_receipt_enum_sync.py +203 -0
- package/vendor/ars/scripts/check_repro_lock.py +85 -0
- package/vendor/ars/scripts/check_review_pathway_output.py +276 -0
- package/vendor/ars/scripts/check_reviewer_data_fences.py +224 -0
- package/vendor/ars/scripts/check_reviewer_finding_contract.py +783 -0
- package/vendor/ars/scripts/check_reviewer_role_label.py +447 -0
- package/vendor/ars/scripts/check_reviewer_scoring_honesty.py +292 -0
- package/vendor/ars/scripts/check_reviewer_sprint_prompt_sync.py +409 -0
- package/vendor/ars/scripts/check_revision_claim_drift_suite_v2.py +1622 -0
- package/vendor/ars/scripts/check_revision_token_conservation.py +263 -0
- package/vendor/ars/scripts/check_risk_register.py +280 -0
- package/vendor/ars/scripts/check_role_scoped_contract.py +702 -0
- package/vendor/ars/scripts/check_rq_framing_patterns.py +184 -0
- package/vendor/ars/scripts/check_rubric_weight_consistency.py +32 -0
- package/vendor/ars/scripts/check_seeded_defect_fixtures.py +425 -0
- package/vendor/ars/scripts/check_setup_cross_model_parity.py +139 -0
- package/vendor/ars/scripts/check_spec_consistency.py +1249 -0
- package/vendor/ars/scripts/check_sprint_contract.py +371 -0
- package/vendor/ars/scripts/check_stage_capability_matrix.py +784 -0
- package/vendor/ars/scripts/check_submission_packet_manifest_integration.py +358 -0
- package/vendor/ars/scripts/check_surface_form_parity.py +445 -0
- package/vendor/ars/scripts/check_task_type.py +22 -0
- package/vendor/ars/scripts/check_tools_allowlist.py +538 -0
- package/vendor/ars/scripts/check_tortured_phrase_screening_integration.py +1974 -0
- package/vendor/ars/scripts/check_v3_10_134_write_scope.py +286 -0
- package/vendor/ars/scripts/check_v3_10_policy.py +656 -0
- package/vendor/ars/scripts/check_v3_6_6_ab_manifest.py +364 -0
- package/vendor/ars/scripts/check_v3_6_7_pattern_protection.py +1366 -0
- package/vendor/ars/scripts/check_v3_6_8_audit_scope_block.py +464 -0
- package/vendor/ars/scripts/check_v3_6_8_cite_provenance_pipeline.py +231 -0
- package/vendor/ars/scripts/check_v3_6_8_frontmatter_trust_schema.py +248 -0
- package/vendor/ars/scripts/check_v3_6_8_mark_read_commands.py +79 -0
- package/vendor/ars/scripts/check_v3_6_8_pattern_protection.py +941 -0
- package/vendor/ars/scripts/check_v3_7_3_three_layer_citation.py +318 -0
- package/vendor/ars/scripts/check_v3_8_annotation_literal_sync.py +228 -0
- package/vendor/ars/scripts/check_v3_9_0_triangulation.py +366 -0
- package/vendor/ars/scripts/check_v3_9_2_phase_boundary.py +270 -0
- package/vendor/ars/scripts/check_v3_9_4_temporal_verification.py +139 -0
- package/vendor/ars/scripts/check_venue_disclosure_policies.py +63 -0
- package/vendor/ars/scripts/check_version_consistency.py +826 -0
- package/vendor/ars/scripts/check_workflow_classification.py +223 -0
- package/vendor/ars/scripts/chinese_literature_client.py +1938 -0
- package/vendor/ars/scripts/citation_verification_summary.py +85 -0
- package/vendor/ars/scripts/claim_audit_calibration.py +517 -0
- package/vendor/ars/scripts/claim_audit_finalizer.py +456 -0
- package/vendor/ars/scripts/claim_audit_pipeline.py +1594 -0
- package/vendor/ars/scripts/claim_registry_coverage.py +493 -0
- package/vendor/ars/scripts/claim_standing_discovery.py +784 -0
- package/vendor/ars/scripts/claim_standing_stance_runner.py +758 -0
- package/vendor/ars/scripts/claim_standing_stance_scorer.py +239 -0
- package/vendor/ars/scripts/claim_strength_drift_disposition.py +666 -0
- package/vendor/ars/scripts/contamination_signals.py +689 -0
- package/vendor/ars/scripts/corpus_consumer_manifest.json +19 -0
- package/vendor/ars/scripts/cross_model_codex_transport.py +1374 -0
- package/vendor/ars/scripts/cross_model_codex_verify.sh +6 -0
- package/vendor/ars/scripts/cross_model_handoff.py +359 -0
- package/vendor/ars/scripts/cross_model_smoke_test.sh +183 -0
- package/vendor/ars/scripts/cross_model_smoke_test_codex.sh +35 -0
- package/vendor/ars/scripts/cross_model_verification/gemini_is_grounded.jq +45 -0
- package/vendor/ars/scripts/cross_model_verification/gemini_sources.jq +45 -0
- package/vendor/ars/scripts/cross_model_verification/normalize_compat_verdict.py +57 -0
- package/vendor/ars/scripts/cross_model_verification/openai_has_completed_web_search.jq +15 -0
- package/vendor/ars/scripts/cross_model_verification/openai_sources.jq +17 -0
- package/vendor/ars/scripts/cross_model_verification/openai_text.jq +11 -0
- package/vendor/ars/scripts/crossref_client.py +225 -0
- package/vendor/ars/scripts/dispatch_e4_panel.py +2731 -0
- package/vendor/ars/scripts/evidence_rows.py +2043 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3.json +32 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_input.json +27 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_prime.json +32 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/author_stage3_prime_input.json +27 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_override.json +50 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_override_action.json +14 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/compliance_pass.json +44 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/explicit_user_request_log.json +26 -0
- package/vendor/ars/scripts/fixtures/adjudication_activity/mandatory_checkpoint_log.json +116 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/README.md +56 -0
- 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
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a1_pass_with_p1/2026-04-30T15-22-04Z-d8f3.verdict.yaml +19 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/negative/a7_orphan_completion/2026-04-30T15-22-04Z-d8f3.jsonl +3 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.audit_artifact_entry.json +26 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/persisted_minor/2026-04-30T15-22-04Z-d8f3.verdict.yaml +19 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.audit_artifact_entry.json +24 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.jsonl +4 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.meta.json +43 -0
- package/vendor/ars/scripts/fixtures/audit_artifact_consistency/positive/proposal_pass/2026-04-30T15-22-04Z-d8f3.verdict.yaml +12 -0
- package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/retraction.json +46 -0
- package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/retraction_check_attestation.json +44 -0
- package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase.json +44 -0
- package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase_v1_2_abstract_missing.json +107 -0
- package/vendor/ars/scripts/fixtures/bibliographic_integrity_signals/tortured_phrase_v1_2_detected.json +124 -0
- package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/expected_outcomes.json +77 -0
- package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/manifest.yaml +37 -0
- package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/001-valid-doi-test.json +20 -0
- package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/002-valid-arxiv-test.json +19 -0
- package/vendor/ars/scripts/fixtures/check_evals_gold_set/clean/tuples/003-fabricated-test.json +20 -0
- package/vendor/ars/scripts/fixtures/claim_audit_calibration/gold_set.json +344 -0
- package/vendor/ars/scripts/fixtures/claim_standing_candidate_ledger/query_plan.json +108 -0
- package/vendor/ars/scripts/fixtures/claim_standing_candidate_ledger/retrieval_input.json +179 -0
- package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/concern_tracker.json +129 -0
- package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/response_skeleton.md +19 -0
- package/vendor/ars/scripts/fixtures/committee_correspondence/16fd83f6aec7/source_letter.txt +9 -0
- package/vendor/ars/scripts/fixtures/content_coverage_advisory/base_draft.json +48 -0
- package/vendor/ars/scripts/fixtures/content_coverage_advisory/base_inventory.json +38 -0
- package/vendor/ars/scripts/fixtures/content_coverage_advisory/packet/consent.txt +1 -0
- package/vendor/ars/scripts/fixtures/content_coverage_advisory/session_sources.json +3 -0
- package/vendor/ars/scripts/fixtures/cross_document_consistency/README.md +6 -0
- package/vendor/ars/scripts/fixtures/cross_document_consistency/accepted_draft.md +35 -0
- package/vendor/ars/scripts/fixtures/cross_document_consistency/cases.json +75 -0
- package/vendor/ars/scripts/fixtures/cross_document_consistency/preregistration.md +5 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/forbidden_event.jsonl +4 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/grounded_verified.jsonl +3 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/malformed.jsonl +3 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/missing_search.jsonl +2 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/multiple_finals.jsonl +4 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/not_found.jsonl +3 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/unbound_source.jsonl +3 -0
- package/vendor/ars/scripts/fixtures/cross_model_codex_transport/wrong_search_shape.jsonl +3 -0
- package/vendor/ars/scripts/fixtures/evidence_rows/phase_e_inputs.json +114 -0
- package/vendor/ars/scripts/fixtures/evidence_rows/session_sources.json +5 -0
- package/vendor/ars/scripts/fixtures/human_subjects_authority/cross-border-us-tw-gdpr.json +71 -0
- package/vendor/ars/scripts/fixtures/human_subjects_authority/gdpr-member-state-unresolved.json +62 -0
- package/vendor/ars/scripts/fixtures/human_subjects_authority/missing-data-axis.json +45 -0
- package/vendor/ars/scripts/fixtures/human_subjects_authority/no-profile.json +33 -0
- package/vendor/ars/scripts/fixtures/human_subjects_authority/tw-gdpr-two-axis.json +74 -0
- package/vendor/ars/scripts/fixtures/human_subjects_authority/us-gdpr-two-axis.json +74 -0
- package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/lint-near-misses.json +72 -0
- package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/no-profile-request.json +18 -0
- package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/tw-candidates-request.json +63 -0
- package/vendor/ars/scripts/fixtures/review_pathway_rule_trace/us-candidates-request.json +77 -0
- package/vendor/ars/scripts/fixtures/review_target_context/exact-declaration.json +24 -0
- package/vendor/ars/scripts/fixtures/review_target_context/field-general-declaration.json +24 -0
- package/vendor/ars/scripts/fixtures/review_target_context/msr-2027-technical-full-declaration.json +24 -0
- package/vendor/ars/scripts/fixtures/review_target_context/synthetic-registry.json +92 -0
- package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/README.md +21 -0
- package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_attested_only.json +55 -0
- package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_machine_supported.json +49 -0
- package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_not_isolated.json +56 -0
- package/vendor/ars/scripts/fixtures/revision_claim_drift_v2/subject_context_unknown.json +49 -0
- package/vendor/ars/scripts/fixtures/submission_package/clean/paper.md +16 -0
- package/vendor/ars/scripts/fixtures/submission_package/clean/references.bib +13 -0
- package/vendor/ars/scripts/fixtures/submission_package/fallback_authoryear/paper.md +11 -0
- package/vendor/ars/scripts/fixtures/submission_package/fallback_authoryear/references.bib +13 -0
- package/vendor/ars/scripts/fixtures/submission_package/fallback_latex/paper.tex +9 -0
- package/vendor/ars/scripts/fixtures/submission_package/fallback_latex/references.bib +13 -0
- package/vendor/ars/scripts/fixtures/submission_package/marker_no_join/paper.md +7 -0
- package/vendor/ars/scripts/fixtures/submission_package/orphan_intext/paper.md +8 -0
- package/vendor/ars/scripts/fixtures/submission_package/orphan_intext/references.bib +6 -0
- package/vendor/ars/scripts/fixtures/submission_package/passports/corpus_only.yaml +9 -0
- package/vendor/ars/scripts/fixtures/submission_package/passports/summary_join.yaml +16 -0
- package/vendor/ars/scripts/fixtures/submission_package/profiles/full.yaml +15 -0
- package/vendor/ars/scripts/fixtures/submission_package/profiles/tight.yaml +15 -0
- package/vendor/ars/scripts/fixtures/submission_package/summary_join/paper.md +7 -0
- package/vendor/ars/scripts/fixtures/submission_package/uncited_reference/paper.md +9 -0
- package/vendor/ars/scripts/fixtures/submission_package/uncited_reference/references.bib +13 -0
- package/vendor/ars/scripts/fixtures/submission_package/venue_clean/paper.md +26 -0
- package/vendor/ars/scripts/fixtures/submission_package/venue_clean/references.bib +13 -0
- package/vendor/ars/scripts/fixtures/submission_package/venue_violations/paper.md +20 -0
- package/vendor/ars/scripts/fixtures/submission_package/venue_violations/references.bib +13 -0
- package/vendor/ars/scripts/fixtures/submission_packet_manifest/base_inventory.json +101 -0
- package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/consent-materials.txt +3 -0
- package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/training-certificate.txt +2 -0
- package/vendor/ars/scripts/fixtures/submission_packet_manifest/packet/tw-consent-materials.txt +3 -0
- package/vendor/ars/scripts/fixtures/tortured_phrase_screening/corpus_input.yaml +30 -0
- package/vendor/ars/scripts/fixtures/tortured_phrase_screening/own_draft.md +36 -0
- package/vendor/ars/scripts/fixtures/tortured_phrase_screening/own_draft.tex +21 -0
- package/vendor/ars/scripts/fixtures/tortured_phrase_screening/seed_expectations.json +218 -0
- package/vendor/ars/scripts/fixtures/tortured_phrase_screening/snapshot.json +164 -0
- package/vendor/ars/scripts/fixtures/tortured_phrase_screening/snapshot_manifest.json +29 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/README.md +55 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/empty_feed.xml +10 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/error_5xx.html +7 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/arxiv/id_hit.xml +26 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/README.md +69 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/cnki_landing_page.html +13 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/error_5xx.html +2 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_ambiguous.json +16 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_hit.json +16 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coordinate_zero.json +14 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coverage_hit.json +15 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esearch_coverage_zero.json +13 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_hit.json +43 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_issn_mismatch.json +31 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_no_doi.json +31 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_unknown_ra.json +34 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/esummary_year_mismatch.json +31 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_absent.json +4 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_exists.json +16 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/handle_internal_error.json +4 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/istic_csl_hit.json +25 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/istic_csl_other_title.json +18 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_cnki.json +6 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_crossref.json +6 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_istic.json +6 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/chinese_literature/ra_unknown_prefix.json +6 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/crossref/doi_hit.json +24 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/crossref/error_5xx.html +7 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/crossref/title_search_miss.json +15 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/openalex/doi_hit.json +29 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/openalex/error_5xx.json +4 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/openalex/title_search_miss.json +11 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/doi_hit.json +14 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/error_5xx.json +3 -0
- package/vendor/ars/scripts/fixtures/transport_bodies/semantic_scholar/title_search_miss.json +5 -0
- package/vendor/ars/scripts/human_read_attestation_resolver.py +467 -0
- package/vendor/ars/scripts/ideation_diversity_assignment_gate.py +430 -0
- package/vendor/ars/scripts/inquiry_branch_ledger.py +2540 -0
- package/vendor/ars/scripts/legacy/ars_apply_revision_patch_v1_0.py +715 -0
- package/vendor/ars/scripts/legacy/check_re_review_synthesis_v1_0.py +2215 -0
- package/vendor/ars/scripts/migrate_literature_corpus_to_v3_10.py +223 -0
- package/vendor/ars/scripts/migrate_literature_corpus_to_v3_7_3.py +277 -0
- package/vendor/ars/scripts/migrate_literature_corpus_to_v3_9_0.py +304 -0
- package/vendor/ars/scripts/model_tiering_manifest.json +46 -0
- package/vendor/ars/scripts/openalex_client.py +232 -0
- package/vendor/ars/scripts/parse_audit_verdict.py +802 -0
- package/vendor/ars/scripts/pdf_content_classifier_worker.py +176 -0
- package/vendor/ars/scripts/pdf_read_preflight.py +1455 -0
- package/vendor/ars/scripts/policy_anchor_disclosure_referee.py +354 -0
- package/vendor/ars/scripts/recompute_receipts.py +1414 -0
- package/vendor/ars/scripts/render_claim_standing_view.py +374 -0
- package/vendor/ars/scripts/render_eval_comment.py +130 -0
- package/vendor/ars/scripts/render_harness_retirement_issue.py +167 -0
- package/vendor/ars/scripts/repro_lock_validation.py +90 -0
- package/vendor/ars/scripts/research_workflow_profile.py +1079 -0
- package/vendor/ars/scripts/resolve_human_subjects_authority.py +1158 -0
- package/vendor/ars/scripts/resolve_review_target_context.py +683 -0
- package/vendor/ars/scripts/resume_e4_record.py +509 -0
- package/vendor/ars/scripts/retraction_status.py +484 -0
- package/vendor/ars/scripts/review_criteria_binding.py +889 -0
- package/vendor/ars/scripts/review_panel_provenance.py +744 -0
- package/vendor/ars/scripts/revision_roadmap.py +1967 -0
- package/vendor/ars/scripts/run_ci_pytest_manifest.py +116 -0
- package/vendor/ars/scripts/run_codex_audit.sh +1191 -0
- package/vendor/ars/scripts/run_evals.py +513 -0
- package/vendor/ars/scripts/run_ideation_diversity_no_call.py +3505 -0
- package/vendor/ars/scripts/run_indirect_prompt_injection_no_call.py +3330 -0
- package/vendor/ars/scripts/run_indirect_prompt_injection_probe.py +399 -0
- package/vendor/ars/scripts/run_review_criteria_constructive_value.py +1895 -0
- package/vendor/ars/scripts/run_role_topology_utility_dry_run.py +606 -0
- package/vendor/ars/scripts/score_review_criteria_constructive_value.py +617 -0
- package/vendor/ars/scripts/semantic_scholar_client.py +291 -0
- package/vendor/ars/scripts/slr_lineage.py +59 -0
- package/vendor/ars/scripts/sync_adapter_docs.py +118 -0
- package/vendor/ars/scripts/temporal_integrity_audit.py +840 -0
- package/vendor/ars/scripts/test_431_exact_or_bust.py +253 -0
- package/vendor/ars/scripts/test__eval_threshold_gate.py +125 -0
- package/vendor/ars/scripts/test__markdown_lint_util.py +129 -0
- package/vendor/ars/scripts/test__next_verified_at_ms.py +256 -0
- package/vendor/ars/scripts/test_adjacent_framing_probe_lint.py +171 -0
- package/vendor/ars/scripts/test_adjudication_activity.py +1515 -0
- package/vendor/ars/scripts/test_ars_anchorize_draft.py +178 -0
- package/vendor/ars/scripts/test_ars_apply_revision_patch.py +1335 -0
- package/vendor/ars/scripts/test_ars_cache_invalidate.py +51 -0
- package/vendor/ars/scripts/test_ars_mark_read.py +910 -0
- package/vendor/ars/scripts/test_ars_update_check.py +816 -0
- package/vendor/ars/scripts/test_ars_write_scope_guard.py +790 -0
- package/vendor/ars/scripts/test_arxiv_client.py +374 -0
- package/vendor/ars/scripts/test_audit_schemas.py +560 -0
- package/vendor/ars/scripts/test_audit_snapshot_render_section_0.py +105 -0
- package/vendor/ars/scripts/test_block_parser.py +259 -0
- package/vendor/ars/scripts/test_bootstrap_timeline_yaml.py +148 -0
- package/vendor/ars/scripts/test_build_claim_standing_candidate_ledger.py +1203 -0
- package/vendor/ars/scripts/test_build_claim_standing_query_plan.py +791 -0
- package/vendor/ars/scripts/test_build_submission_packet_manifest.py +2644 -0
- package/vendor/ars/scripts/test_check_215_field_norm.py +238 -0
- package/vendor/ars/scripts/test_check_216_surface_form.py +341 -0
- package/vendor/ars/scripts/test_check_268_nested_commitment_ledger.py +194 -0
- package/vendor/ars/scripts/test_check_390_revision_patch_discipline.py +279 -0
- package/vendor/ars/scripts/test_check_392_citation_verification_intake.py +144 -0
- package/vendor/ars/scripts/test_check_394_submission_policy.py +200 -0
- package/vendor/ars/scripts/test_check_439_format_profile.py +252 -0
- package/vendor/ars/scripts/test_check_619_disclosure_closeout.py +158 -0
- package/vendor/ars/scripts/test_check_630_codex_subscription_transport.py +245 -0
- package/vendor/ars/scripts/test_check_669_review_pathway_rule_trace.py +304 -0
- package/vendor/ars/scripts/test_check_670_revision_roadmap_integration.py +260 -0
- package/vendor/ars/scripts/test_check_673_adjudication_activity.py +367 -0
- package/vendor/ars/scripts/test_check_684_review_criteria_binding.py +274 -0
- package/vendor/ars/scripts/test_check_agents_mirror_sync.py +137 -0
- package/vendor/ars/scripts/test_check_audit_artifact_consistency.py +2133 -0
- package/vendor/ars/scripts/test_check_benchmark_report.py +117 -0
- package/vendor/ars/scripts/test_check_bibliographic_integrity_signals.py +609 -0
- package/vendor/ars/scripts/test_check_calibration_tiers.py +376 -0
- package/vendor/ars/scripts/test_check_changelog_covers_merges.py +509 -0
- package/vendor/ars/scripts/test_check_ci_pytest_manifest.py +425 -0
- package/vendor/ars/scripts/test_check_claim_standing_candidate_ledger_integration.py +256 -0
- package/vendor/ars/scripts/test_check_claim_standing_freshness.py +277 -0
- package/vendor/ars/scripts/test_check_collaboration_depth_rubric.py +239 -0
- package/vendor/ars/scripts/test_check_command_frontmatter_name.py +178 -0
- package/vendor/ars/scripts/test_check_committee_correspondence.py +298 -0
- package/vendor/ars/scripts/test_check_compliance_report.py +381 -0
- package/vendor/ars/scripts/test_check_content_coverage_advisory_integration.py +308 -0
- package/vendor/ars/scripts/test_check_control_availability.py +361 -0
- package/vendor/ars/scripts/test_check_cross_document_consistency_advisory_integration.py +698 -0
- package/vendor/ars/scripts/test_check_cross_model_handoff_contract.py +301 -0
- package/vendor/ars/scripts/test_check_cross_model_verification_sync.py +203 -0
- package/vendor/ars/scripts/test_check_data_access_level.py +227 -0
- package/vendor/ars/scripts/test_check_data_flows.py +382 -0
- package/vendor/ars/scripts/test_check_decision_contract.py +387 -0
- package/vendor/ars/scripts/test_check_degradation_registry.py +270 -0
- package/vendor/ars/scripts/test_check_distribution_surface_claims.py +225 -0
- package/vendor/ars/scripts/test_check_domain_evidence_profile.py +439 -0
- package/vendor/ars/scripts/test_check_e4_promotion.py +163 -0
- package/vendor/ars/scripts/test_check_evals_gold_set.py +312 -0
- package/vendor/ars/scripts/test_check_evidence_row_integration.py +187 -0
- package/vendor/ars/scripts/test_check_field_norm_severity.py +173 -0
- package/vendor/ars/scripts/test_check_firm_rules_sync.py +342 -0
- package/vendor/ars/scripts/test_check_heldout_measurement_report.py +1508 -0
- package/vendor/ars/scripts/test_check_human_subjects_output_contract.py +129 -0
- package/vendor/ars/scripts/test_check_human_subjects_reference_migration.py +756 -0
- package/vendor/ars/scripts/test_check_instruction_data_boundary.py +204 -0
- package/vendor/ars/scripts/test_check_judge_prompt_version.py +90 -0
- package/vendor/ars/scripts/test_check_model_tiering.py +236 -0
- package/vendor/ars/scripts/test_check_panel_synthesis.py +1658 -0
- package/vendor/ars/scripts/test_check_passport_reset_contract.py +249 -0
- package/vendor/ars/scripts/test_check_pattern_eval_manifest.py +381 -0
- package/vendor/ars/scripts/test_check_persuasion_invariance_fixtures.py +591 -0
- package/vendor/ars/scripts/test_check_phase_conformance.py +4366 -0
- package/vendor/ars/scripts/test_check_pipeline_boundary_semantics.py +931 -0
- package/vendor/ars/scripts/test_check_pipeline_integrity.py +243 -0
- package/vendor/ars/scripts/test_check_policy_anchor_protocol.py +236 -0
- package/vendor/ars/scripts/test_check_policy_anchor_table.py +295 -0
- package/vendor/ars/scripts/test_check_prisma_trAIce_freshness.py +68 -0
- package/vendor/ars/scripts/test_check_promotion_bakeoff_preregistration.py +799 -0
- package/vendor/ars/scripts/test_check_ranking_lift.py +377 -0
- package/vendor/ars/scripts/test_check_re_review_synthesis.py +3398 -0
- package/vendor/ars/scripts/test_check_receipt_enum_sync.py +176 -0
- package/vendor/ars/scripts/test_check_repro_lock.py +107 -0
- package/vendor/ars/scripts/test_check_reviewer_data_fences.py +334 -0
- package/vendor/ars/scripts/test_check_reviewer_finding_contract.py +1107 -0
- package/vendor/ars/scripts/test_check_reviewer_role_label.py +491 -0
- package/vendor/ars/scripts/test_check_reviewer_scoring_honesty.py +222 -0
- package/vendor/ars/scripts/test_check_reviewer_sprint_prompt_sync.py +373 -0
- package/vendor/ars/scripts/test_check_revision_claim_drift_suite_v2.py +1925 -0
- package/vendor/ars/scripts/test_check_revision_token_conservation.py +398 -0
- package/vendor/ars/scripts/test_check_risk_register.py +336 -0
- package/vendor/ars/scripts/test_check_role_scoped_contract.py +1104 -0
- package/vendor/ars/scripts/test_check_rq_framing_patterns.py +110 -0
- package/vendor/ars/scripts/test_check_rubric_weight_consistency.py +32 -0
- package/vendor/ars/scripts/test_check_seeded_defect_fixtures.py +392 -0
- package/vendor/ars/scripts/test_check_setup_cross_model_parity.py +121 -0
- package/vendor/ars/scripts/test_check_spec_consistency.py +1081 -0
- package/vendor/ars/scripts/test_check_sprint_contract.py +458 -0
- package/vendor/ars/scripts/test_check_stage_capability_matrix.py +698 -0
- package/vendor/ars/scripts/test_check_submission_packet_manifest_integration.py +177 -0
- package/vendor/ars/scripts/test_check_surface_form_parity.py +417 -0
- package/vendor/ars/scripts/test_check_task_type.py +116 -0
- package/vendor/ars/scripts/test_check_tools_allowlist.py +835 -0
- package/vendor/ars/scripts/test_check_tortured_phrase_screening_integration.py +1306 -0
- package/vendor/ars/scripts/test_check_v3_10_134_write_scope.py +251 -0
- package/vendor/ars/scripts/test_check_v3_10_policy.py +547 -0
- package/vendor/ars/scripts/test_check_v3_6_7_pattern_protection.py +960 -0
- package/vendor/ars/scripts/test_check_v3_6_8_audit_scope_block.py +1000 -0
- package/vendor/ars/scripts/test_check_v3_6_8_cite_provenance_pipeline.py +454 -0
- package/vendor/ars/scripts/test_check_v3_6_8_frontmatter_trust_schema.py +581 -0
- package/vendor/ars/scripts/test_check_v3_6_8_mark_read_commands.py +120 -0
- package/vendor/ars/scripts/test_check_v3_6_8_pattern_protection.py +1138 -0
- package/vendor/ars/scripts/test_check_v3_7_3_three_layer_citation.py +566 -0
- package/vendor/ars/scripts/test_check_v3_8_annotation_literal_sync.py +262 -0
- package/vendor/ars/scripts/test_check_v3_9_0_triangulation.py +321 -0
- package/vendor/ars/scripts/test_check_v3_9_2_phase_boundary.py +190 -0
- package/vendor/ars/scripts/test_check_v3_9_4_temporal_verification.py +471 -0
- package/vendor/ars/scripts/test_check_version_consistency.py +1462 -0
- package/vendor/ars/scripts/test_check_workflow_classification.py +225 -0
- package/vendor/ars/scripts/test_chinese_literature_client.py +1889 -0
- package/vendor/ars/scripts/test_citation_existence_policy.py +480 -0
- package/vendor/ars/scripts/test_citation_verification_summary.py +342 -0
- package/vendor/ars/scripts/test_claim_audit_calibration.py +882 -0
- package/vendor/ars/scripts/test_claim_audit_finalizer.py +979 -0
- package/vendor/ars/scripts/test_claim_audit_pipeline.py +2398 -0
- package/vendor/ars/scripts/test_claim_audit_schema.py +1778 -0
- package/vendor/ars/scripts/test_claim_intent_manifest.py +666 -0
- package/vendor/ars/scripts/test_claim_registry_coverage.py +246 -0
- package/vendor/ars/scripts/test_claim_standing_discovery.py +579 -0
- package/vendor/ars/scripts/test_claim_standing_pipeline_wiring.py +290 -0
- package/vendor/ars/scripts/test_claim_standing_stance_assets.py +399 -0
- package/vendor/ars/scripts/test_claim_standing_stance_contracts.py +383 -0
- package/vendor/ars/scripts/test_claim_standing_stance_runner.py +349 -0
- package/vendor/ars/scripts/test_claim_standing_transmissions.py +605 -0
- package/vendor/ars/scripts/test_claim_strength_drift_disposition.py +517 -0
- package/vendor/ars/scripts/test_claim_verification_coverage_contract.py +104 -0
- package/vendor/ars/scripts/test_contamination_signals.py +1086 -0
- package/vendor/ars/scripts/test_content_coverage_advisory.py +1767 -0
- package/vendor/ars/scripts/test_cross_document_consistency_advisory.py +1270 -0
- package/vendor/ars/scripts/test_cross_model_codex_transport.py +1152 -0
- package/vendor/ars/scripts/test_cross_model_handoff.py +571 -0
- package/vendor/ars/scripts/test_cross_model_verification_guards.py +792 -0
- package/vendor/ars/scripts/test_crossref_client.py +393 -0
- package/vendor/ars/scripts/test_dispatch_e4_panel.py +4235 -0
- package/vendor/ars/scripts/test_e2e_claim_audit.py +540 -0
- package/vendor/ars/scripts/test_eval_harness_workflow.py +140 -0
- package/vendor/ars/scripts/test_evals_citation_extraction.py +150 -0
- package/vendor/ars/scripts/test_evals_lift_report_schema.py +108 -0
- package/vendor/ars/scripts/test_evidence_rows.py +2593 -0
- package/vendor/ars/scripts/test_experiment_provenance.py +915 -0
- package/vendor/ars/scripts/test_human_read_attestation_resolver.py +477 -0
- package/vendor/ars/scripts/test_ideation_diversity_assignment_gate.py +615 -0
- package/vendor/ars/scripts/test_indirect_prompt_injection_behavior_probe.py +250 -0
- package/vendor/ars/scripts/test_inquiry_branch_ledger.py +2296 -0
- package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_10.py +248 -0
- package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_7_3.py +545 -0
- package/vendor/ars/scripts/test_migrate_literature_corpus_to_v3_9_0.py +497 -0
- package/vendor/ars/scripts/test_normalize_compat_verdict.py +149 -0
- package/vendor/ars/scripts/test_openalex_client.py +490 -0
- package/vendor/ars/scripts/test_passport_yaml.py +104 -0
- package/vendor/ars/scripts/test_pattern_eval_runtime.py +1295 -0
- package/vendor/ars/scripts/test_pdf_read_preflight.py +1943 -0
- package/vendor/ars/scripts/test_policy_anchor_disclosure.py +666 -0
- package/vendor/ars/scripts/test_reading_probe_lint.py +218 -0
- package/vendor/ars/scripts/test_recompute_receipts.py +778 -0
- package/vendor/ars/scripts/test_render_claim_standing_view.py +192 -0
- package/vendor/ars/scripts/test_render_eval_comment.py +162 -0
- package/vendor/ars/scripts/test_render_harness_retirement_issue.py +110 -0
- package/vendor/ars/scripts/test_repro_lock_validation_drift.py +100 -0
- package/vendor/ars/scripts/test_research_workflow_profile.py +734 -0
- package/vendor/ars/scripts/test_resolve_human_subjects_authority.py +1219 -0
- package/vendor/ars/scripts/test_resolve_review_target_context.py +703 -0
- package/vendor/ars/scripts/test_resume_e4_record.py +315 -0
- package/vendor/ars/scripts/test_retraction_status.py +456 -0
- package/vendor/ars/scripts/test_review_criteria_binding.py +629 -0
- package/vendor/ars/scripts/test_review_panel_provenance.py +565 -0
- package/vendor/ars/scripts/test_review_pathway_rule_trace.py +821 -0
- package/vendor/ars/scripts/test_revision_roadmap.py +1255 -0
- package/vendor/ars/scripts/test_run_ci_pytest_manifest.py +186 -0
- package/vendor/ars/scripts/test_run_codex_audit_e2e.py +369 -0
- package/vendor/ars/scripts/test_run_evals.py +430 -0
- package/vendor/ars/scripts/test_run_guard_launcher.py +500 -0
- package/vendor/ars/scripts/test_run_ideation_diversity_no_call.py +1833 -0
- package/vendor/ars/scripts/test_run_indirect_prompt_injection_no_call.py +1889 -0
- package/vendor/ars/scripts/test_run_review_criteria_constructive_value.py +586 -0
- package/vendor/ars/scripts/test_run_role_topology_utility_dry_run.py +428 -0
- package/vendor/ars/scripts/test_score_review_criteria_constructive_value.py +340 -0
- package/vendor/ars/scripts/test_semantic_scholar_client.py +554 -0
- package/vendor/ars/scripts/test_slr_lineage_emission.py +230 -0
- package/vendor/ars/scripts/test_socratic_rq_non_generation_contract.py +173 -0
- package/vendor/ars/scripts/test_temporal_integrity_audit.py +438 -0
- package/vendor/ars/scripts/test_text_similarity.py +95 -0
- package/vendor/ars/scripts/test_title_fuzzy_false_positive.py +111 -0
- package/vendor/ars/scripts/test_tortured_phrase_screening.py +2959 -0
- package/vendor/ars/scripts/test_transport_fixture_citation_gate.py +338 -0
- package/vendor/ars/scripts/test_uncited_assertion.py +558 -0
- package/vendor/ars/scripts/test_v3_6_7_phase_6_6.py +1279 -0
- package/vendor/ars/scripts/test_validate_compliance_fixtures.py +36 -0
- package/vendor/ars/scripts/test_validate_ideation_diversity_assets.py +230 -0
- package/vendor/ars/scripts/test_venue_disclosure_contract.py +755 -0
- package/vendor/ars/scripts/test_verification_cache.py +280 -0
- package/vendor/ars/scripts/test_verification_gate.py +461 -0
- package/vendor/ars/scripts/test_verify_passport_cli.py +123 -0
- package/vendor/ars/scripts/test_verify_submission_package.py +1407 -0
- package/vendor/ars/scripts/test_version_records_schema.py +211 -0
- package/vendor/ars/scripts/tortured_phrase_screening.py +3502 -0
- package/vendor/ars/scripts/uncited_assertion_detector.py +254 -0
- package/vendor/ars/scripts/v3_6_7_inversion_manifest.json +9 -0
- package/vendor/ars/scripts/v3_6_8_inversion_manifest.json +10 -0
- package/vendor/ars/scripts/validate_claim_standing_stance_assets.py +249 -0
- package/vendor/ars/scripts/validate_compliance_fixtures.py +56 -0
- package/vendor/ars/scripts/validate_ideation_diversity_assets.py +303 -0
- package/vendor/ars/scripts/venue_disclosure_contract_harness.py +837 -0
- package/vendor/ars/scripts/verification_cache.py +276 -0
- package/vendor/ars/scripts/verification_gate/__init__.py +345 -0
- package/vendor/ars/scripts/verify_passport.py +133 -0
- package/vendor/ars/scripts/verify_submission_package.py +1657 -0
- package/vendor/ars/shared/agents/compliance_agent.md +136 -0
- package/vendor/ars/shared/artifact_reproducibility_pattern.md +173 -0
- package/vendor/ars/shared/benchmark_report.schema.json +81 -0
- package/vendor/ars/shared/benchmark_report_pattern.md +180 -0
- package/vendor/ars/shared/bibliographic_integrity_signals.md +142 -0
- package/vendor/ars/shared/collaboration_depth_rubric.md +154 -0
- package/vendor/ars/shared/compliance_checkpoint_protocol.md +162 -0
- package/vendor/ars/shared/compliance_report.schema.json +187 -0
- package/vendor/ars/shared/contracts/README.md +938 -0
- package/vendor/ars/shared/contracts/activity/adjudication_activity_input.schema.json +555 -0
- package/vendor/ars/shared/contracts/activity/adjudication_activity_store.schema.json +560 -0
- package/vendor/ars/shared/contracts/audit/audit_jsonl.schema.json +128 -0
- package/vendor/ars/shared/contracts/audit/audit_sidecar.schema.json +169 -0
- package/vendor/ars/shared/contracts/audit/audit_verdict.schema.json +133 -0
- package/vendor/ars/shared/contracts/audit/cross_document_consistency_advisory.schema.json +480 -0
- package/vendor/ars/shared/contracts/audit/cross_document_consistency_advisory_draft.schema.json +564 -0
- package/vendor/ars/shared/contracts/audit/cross_document_source_manifest.schema.json +202 -0
- package/vendor/ars/shared/contracts/audit/tortured_phrase_advisory.schema.json +1362 -0
- package/vendor/ars/shared/contracts/audit/tortured_phrase_snapshot.schema.json +208 -0
- package/vendor/ars/shared/contracts/audit/tortured_phrase_snapshot_manifest.schema.json +335 -0
- package/vendor/ars/shared/contracts/capability/stage_capability_matrix.json +434 -0
- package/vendor/ars/shared/contracts/claim_standing/candidate_ledger.schema.json +705 -0
- package/vendor/ars/shared/contracts/claim_standing/query_plan.schema.json +389 -0
- package/vendor/ars/shared/contracts/claim_standing/query_plan_v1_1.schema.json +782 -0
- package/vendor/ars/shared/contracts/claim_standing/retrieval_input.schema.json +440 -0
- package/vendor/ars/shared/contracts/claim_standing/stance_record.schema.json +460 -0
- package/vendor/ars/shared/contracts/claim_standing/transmission_ledger.schema.json +282 -0
- package/vendor/ars/shared/contracts/cross_model/codex_citation_receipt.schema.json +157 -0
- package/vendor/ars/shared/contracts/cross_model/codex_citation_request.schema.json +23 -0
- package/vendor/ars/shared/contracts/cross_model/promotion_bakeoff_sealed_commitment.schema.json +55 -0
- package/vendor/ars/shared/contracts/cross_model/promotion_bakeoff_sealed_reveal.schema.json +50 -0
- package/vendor/ars/shared/contracts/degradation_registry.json +428 -0
- package/vendor/ars/shared/contracts/evaluator/full.json +126 -0
- package/vendor/ars/shared/contracts/evidence/claim_registry.schema.json +50 -0
- package/vendor/ars/shared/contracts/evidence/claim_registry_coverage_report.schema.json +79 -0
- package/vendor/ars/shared/contracts/evidence/evidence_row.schema.json +504 -0
- package/vendor/ars/shared/contracts/evidence/evidence_row_v1_1.schema.json +364 -0
- package/vendor/ars/shared/contracts/evidence/evidence_row_v1_2.schema.json +714 -0
- package/vendor/ars/shared/contracts/evidence/evidence_row_v1_3.schema.json +524 -0
- package/vendor/ars/shared/contracts/human_subjects/authority_profile_registry.schema.json +497 -0
- package/vendor/ars/shared/contracts/human_subjects/committee_correspondence.schema.json +253 -0
- package/vendor/ars/shared/contracts/human_subjects/content_coverage_advisory.schema.json +620 -0
- package/vendor/ars/shared/contracts/human_subjects/irb_context_record.schema.json +329 -0
- package/vendor/ars/shared/contracts/human_subjects/resolved_authority_context.schema.json +347 -0
- package/vendor/ars/shared/contracts/human_subjects/review_pathway_rule_trace.schema.json +265 -0
- package/vendor/ars/shared/contracts/human_subjects/review_pathway_trace_request.schema.json +127 -0
- package/vendor/ars/shared/contracts/human_subjects/submission_packet_inventory.schema.json +240 -0
- package/vendor/ars/shared/contracts/human_subjects/submission_packet_manifest.schema.json +999 -0
- package/vendor/ars/shared/contracts/passport/audit_artifact_entry.schema.json +266 -0
- package/vendor/ars/shared/contracts/passport/bibliographic_integrity_signal.schema.json +1676 -0
- package/vendor/ars/shared/contracts/passport/citation_provenance.schema.json +107 -0
- package/vendor/ars/shared/contracts/passport/citation_verification_summary.schema.json +164 -0
- package/vendor/ars/shared/contracts/passport/claim_audit_result.schema.json +124 -0
- package/vendor/ars/shared/contracts/passport/claim_drift.schema.json +58 -0
- package/vendor/ars/shared/contracts/passport/claim_intent_manifest.schema.json +107 -0
- package/vendor/ars/shared/contracts/passport/constraint_violation.schema.json +65 -0
- package/vendor/ars/shared/contracts/passport/experiment_alignment_result.schema.json +69 -0
- package/vendor/ars/shared/contracts/passport/experiment_provenance_entry.schema.json +169 -0
- package/vendor/ars/shared/contracts/passport/human_read_log.schema.json +86 -0
- package/vendor/ars/shared/contracts/passport/inquiry_ledger_ref.schema.json +22 -0
- package/vendor/ars/shared/contracts/passport/literature_corpus_entry.schema.json +650 -0
- package/vendor/ars/shared/contracts/passport/preregistration_artifact.schema.json +132 -0
- package/vendor/ars/shared/contracts/passport/rejection_log.schema.json +89 -0
- package/vendor/ars/shared/contracts/passport/reset_ledger_entry.schema.json +158 -0
- package/vendor/ars/shared/contracts/passport/temporal_audit_results.schema.json +208 -0
- package/vendor/ars/shared/contracts/passport/terminal_policies.schema.json +50 -0
- package/vendor/ars/shared/contracts/passport/timeline.schema.json +102 -0
- package/vendor/ars/shared/contracts/passport/uncited_assertion.schema.json +56 -0
- package/vendor/ars/shared/contracts/passport/uncited_audit_failure.schema.json +72 -0
- package/vendor/ars/shared/contracts/passport/user_attested_read_resolution.schema.json +87 -0
- package/vendor/ars/shared/contracts/passport/version_records.schema.json +138 -0
- package/vendor/ars/shared/contracts/patch/block_manifest.schema.json +45 -0
- package/vendor/ars/shared/contracts/patch/legacy/v1_0/revision_patch.schema.json +110 -0
- package/vendor/ars/shared/contracts/patch/revision_patch.schema.json +250 -0
- package/vendor/ars/shared/contracts/pdf/pdf_content_classifier_diagnostic.schema.json +39 -0
- package/vendor/ars/shared/contracts/pdf/pdf_content_classifier_worker.schema.json +88 -0
- package/vendor/ars/shared/contracts/pdf/pdf_read_preflight.schema.json +227 -0
- package/vendor/ars/shared/contracts/re_review/input_manifest.schema.json +161 -0
- package/vendor/ars/shared/contracts/re_review/legacy/v1_0/input_manifest.schema.json +134 -0
- package/vendor/ars/shared/contracts/re_review/legacy/v1_0/precommitment.schema.json +193 -0
- package/vendor/ars/shared/contracts/re_review/legacy/v1_0/traceability.schema.json +841 -0
- package/vendor/ars/shared/contracts/re_review/legacy/v1_0/verdict_record.schema.json +244 -0
- package/vendor/ars/shared/contracts/re_review/precommitment.schema.json +193 -0
- package/vendor/ars/shared/contracts/re_review/traceability.schema.json +916 -0
- package/vendor/ars/shared/contracts/re_review/verdict_record.schema.json +245 -0
- package/vendor/ars/shared/contracts/research_workflow/inquiry_branch_ledger.schema.json +449 -0
- package/vendor/ars/shared/contracts/research_workflow/research_workflow_profile.schema.json +222 -0
- package/vendor/ars/shared/contracts/research_workflow/research_workflow_profile_selection_receipt.schema.json +111 -0
- package/vendor/ars/shared/contracts/review_target/constructive_review_findings.schema.json +172 -0
- package/vendor/ars/shared/contracts/review_target/criteria_registry.schema.json +97 -0
- package/vendor/ars/shared/contracts/review_target/review_criteria_binding_manifest.schema.json +235 -0
- package/vendor/ars/shared/contracts/review_target/review_criteria_source_receipt.schema.json +158 -0
- package/vendor/ars/shared/contracts/review_target/review_target_context.schema.json +106 -0
- package/vendor/ars/shared/contracts/review_target/review_target_declaration.schema.json +118 -0
- package/vendor/ars/shared/contracts/reviewer/full.json +114 -0
- package/vendor/ars/shared/contracts/reviewer/methodology_focus.json +75 -0
- package/vendor/ars/shared/contracts/reviewer/review_panel_provenance.schema.json +263 -0
- package/vendor/ars/shared/contracts/reviewer/review_panel_provenance_carrier.schema.json +122 -0
- package/vendor/ars/shared/contracts/reviewer/review_panel_provenance_input.schema.json +120 -0
- package/vendor/ars/shared/contracts/revision/author_adjudication.schema.json +251 -0
- package/vendor/ars/shared/contracts/revision/author_adjudication_input.schema.json +25 -0
- package/vendor/ars/shared/contracts/revision/claim_strength_drift_disposition.schema.json +97 -0
- package/vendor/ars/shared/contracts/revision/claim_strength_drift_disposition_input.schema.json +80 -0
- package/vendor/ars/shared/contracts/revision/claim_strength_drift_findings.schema.json +139 -0
- package/vendor/ars/shared/contracts/revision/claim_surface_manifest.schema.json +99 -0
- package/vendor/ars/shared/contracts/revision/integrity_correction_authorization.schema.json +140 -0
- package/vendor/ars/shared/contracts/revision/integrity_correction_authorization_input.schema.json +28 -0
- package/vendor/ars/shared/contracts/revision/integrity_correction_list.schema.json +48 -0
- package/vendor/ars/shared/contracts/revision/integrity_pass_receipt.schema.json +16 -0
- package/vendor/ars/shared/contracts/revision/revision_evidence_bundle.schema.json +134 -0
- package/vendor/ars/shared/contracts/revision/revision_roadmap.schema.json +334 -0
- package/vendor/ars/shared/contracts/submission/format_profile.example.yaml +33 -0
- package/vendor/ars/shared/contracts/submission/format_profile.schema.json +102 -0
- package/vendor/ars/shared/contracts/submission/submission_verification_report.schema.json +233 -0
- package/vendor/ars/shared/contracts/submission/venue_profile.schema.json +114 -0
- package/vendor/ars/shared/contracts/writer/full.json +87 -0
- package/vendor/ars/shared/cross_model_verification.md +714 -0
- package/vendor/ars/shared/evals_lift_report.schema.json +141 -0
- package/vendor/ars/shared/ground_truth_isolation_pattern.md +275 -0
- package/vendor/ars/shared/handoff_schemas.md +1209 -0
- package/vendor/ars/shared/human_subjects_authority_registry.json +1278 -0
- package/vendor/ars/shared/mode_spectrum.md +57 -0
- package/vendor/ars/shared/model_tiering.md +83 -0
- package/vendor/ars/shared/policy_data/nature_policy.md +56 -0
- package/vendor/ars/shared/prisma_trAIce_protocol.md +157 -0
- package/vendor/ars/shared/raise_framework.md +129 -0
- package/vendor/ars/shared/references/authority_content_coverage_advisory_protocol.md +275 -0
- package/vendor/ars/shared/references/claim_standing_candidate_ledger_protocol.md +66 -0
- package/vendor/ars/shared/references/claim_strength_ladder.md +93 -0
- package/vendor/ars/shared/references/cross_document_consistency_advisory_protocol.md +263 -0
- package/vendor/ars/shared/references/evidence_row_protocol.md +260 -0
- package/vendor/ars/shared/references/firm_rules.md +90 -0
- package/vendor/ars/shared/references/human_subjects_authority_protocol.md +274 -0
- package/vendor/ars/shared/references/intent_clarification_protocol.md +168 -0
- package/vendor/ars/shared/references/irb_terminology_glossary.md +229 -0
- package/vendor/ars/shared/references/protected_hedging_phrases.md +118 -0
- package/vendor/ars/shared/references/psychometric_terminology_glossary.md +109 -0
- package/vendor/ars/shared/references/review_criteria_consumer_protocol.md +238 -0
- package/vendor/ars/shared/references/review_pathway_rule_trace_protocol.md +166 -0
- package/vendor/ars/shared/references/submission_packet_manifest_protocol.md +292 -0
- package/vendor/ars/shared/references/word_count_conventions.md +124 -0
- package/vendor/ars/shared/research_workflow_profiles/field_general.json +1 -0
- package/vendor/ars/shared/review_criteria_registry.json +207 -0
- package/vendor/ars/shared/review_criteria_sources/msr-2027-technical-papers.2026-08-24.json +46 -0
- package/vendor/ars/shared/review_criteria_sources/sigsoft-empirical-standards.2026-08-24.json +31 -0
- package/vendor/ars/shared/sprint_contract.schema.json +482 -0
- package/vendor/ars/shared/style_calibration_protocol.md +151 -0
- package/vendor/ars/shared/templates/codex_audit_multifile_template.md +263 -0
- package/vendor/ars/tools/release-discipline/.toolkit-version +1 -0
- package/vendor/ars/tools/release-discipline/README.md +4 -0
- package/vendor/ars/tools/release-discipline/scripts/_release_doc_alignment_schema.py +1011 -0
- package/vendor/ars/tools/release-discipline/scripts/check_command_invariants.py +497 -0
- package/vendor/ars/tools/release-discipline/scripts/check_release_doc_alignment.py +263 -0
- package/vendor/ars/tools/release-discipline/scripts/sync-toolkit.sh +147 -0
- package/vendor/windows/NOTICE.md +12 -0
- package/vendor/windows/arm64/fd.exe +0 -0
- package/vendor/windows/arm64/licenses/fd/LICENSE-APACHE +201 -0
- package/vendor/windows/arm64/licenses/fd/LICENSE-MIT +21 -0
- package/vendor/windows/arm64/licenses/ripgrep/COPYING +3 -0
- package/vendor/windows/arm64/licenses/ripgrep/LICENSE-MIT +21 -0
- package/vendor/windows/arm64/licenses/ripgrep/UNLICENSE +24 -0
- package/vendor/windows/arm64/rg.exe +0 -0
- package/vendor/windows/x64/fd.exe +0 -0
- package/vendor/windows/x64/licenses/fd/LICENSE-APACHE +201 -0
- package/vendor/windows/x64/licenses/fd/LICENSE-MIT +21 -0
- package/vendor/windows/x64/licenses/ripgrep/COPYING +3 -0
- package/vendor/windows/x64/licenses/ripgrep/LICENSE-MIT +21 -0
- package/vendor/windows/x64/licenses/ripgrep/UNLICENSE +24 -0
- 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())
|