From 42cd211a4345c915f005c45c7b2e4ae0aee1bff3 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 31 Jul 2026 04:13:19 -0700 Subject: [PATCH 1/2] fix: catch up receipt freshness on v0.89.2 --- .decapod/governance/plan.json | 13 +- .decapod/governance/trajectory.json | 150 ++++++------------- .decapod/governance/validation.json | 178 +++++++++++------------ .decapod/managed/Dockerfile.decapod | 4 +- .decapod/managed/specs/.manifest.json | 50 +++---- .decapod/managed/specs/ARCHITECTURE.md | 2 +- .decapod/managed/specs/INTENT.md | 2 +- .decapod/managed/specs/INTERFACES.md | 2 +- .decapod/managed/specs/OPERATIONS.md | 2 +- .decapod/managed/specs/README.md | 2 +- .decapod/managed/specs/SECURITY.md | 2 +- .decapod/managed/specs/SEMANTICS.md | 2 +- .decapod/managed/specs/VALIDATION.md | 54 +------ AGENTS.md | 4 +- CLAUDE.md | 4 +- CODEX.md | 4 +- GEMINI.md | 4 +- src/decapod/core/db.rs | 4 + src/decapod/core/entrypoint_integrity.rs | 10 +- src/decapod/core/events.rs | 1 + src/decapod/core/governance_artifacts.rs | 115 ++++++++++++++- src/decapod/core/validate.rs | 25 ++-- src/decapod/lib.rs | 4 + src/decapod/plugins/aptitude.rs | 4 + src/decapod/plugins/decide.rs | 2 + src/decapod/plugins/federation.rs | 155 +++++++++----------- src/decapod/plugins/knowledge.rs | 46 +++--- src/decapod/plugins/lcm.rs | 5 - tests/core/core.rs | 16 +- tests/governance_artifacts.rs | 3 +- tests/knowledge_promotion_cli.rs | 25 ++-- tests/lcm_determinism.rs | 43 ++++-- tests/plugins/decide.rs | 82 ++++------- tests/plugins/federation.rs | 52 +++---- tests/unit/plugins/lcm_tests.rs | 12 +- 35 files changed, 530 insertions(+), 553 deletions(-) diff --git a/.decapod/governance/plan.json b/.decapod/governance/plan.json index 25c01a3c..1b980c2d 100644 --- a/.decapod/governance/plan.json +++ b/.decapod/governance/plan.json @@ -1,17 +1,18 @@ { "schema_version": "1.0.0", - "title": "Complete SQLite consolidation and event-table migration for #1118", - "intent": "Complete Issue #1118 within PR #1119: backend=local must use one canonical .decapod/data/decapod.db with all subsystem tables and append-only event tables; migrate legacy SQLite files and JSONL logs idempotently; stop new JSONL writes; and keep explicit dactyl integration out of this PR under dedicated issue #1120.", + "title": "Catch up PR #1109 receipt freshness on v0.89.2", + "intent": "Rebase the receipt-freshness fix from PR #1109 onto current v0.89.2 master, preserve receipt integrity and trajectory binding, remove the impossible pre-commit receipt git SHA equality requirement, add focused regression proof, refresh governed artifacts through Decapod, and push the updated PR #1109 branch.", "state": "APPROVED", "todo_ids": [ - "bugs_01kyvkq975ekff0p" + "bugs_01kyvqgd6wepcpe8" ], "proof_hooks": [ "cargo fmt --all -- --check", "cargo check --all-targets", + "cargo clippy --all-targets -- -D warnings", "cargo test --all-targets", "decapod validate", - "cargo test --test single_datastore" + "focused governance artifact tests" ], "unknowns": [], "human_questions": [], @@ -20,8 +21,8 @@ "deferred_questions": [], "constraints": { "forbidden_paths": [], - "file_touch_budget": 100 + "file_touch_budget": 50 }, "phases": [], - "updated_at": "1785485784Z" + "updated_at": "1785489612Z" } \ No newline at end of file diff --git a/.decapod/governance/trajectory.json b/.decapod/governance/trajectory.json index eb866523..deb48c8e 100644 --- a/.decapod/governance/trajectory.json +++ b/.decapod/governance/trajectory.json @@ -1,57 +1,42 @@ { "schema_version": "1.1.0", - "run_id": "validation_01KYVNVYKC1JR6ATWMPV703GF5", - "intent_id": "intent:validation_01KYVNVYKC1JR6ATWMPV703GF5", - "task_id": "bugs_01kyvkq975ekff0p", - "original_intent": "Produce proof artifacts for a repository validation completion.", - "derived_intent": "Run the bounded validation gates and bind the successful receipt to a trajectory.", - "destination": "validation completion", + "run_id": "run_01kyvqgd6wepcpe8", + "intent_id": "intent:run_01kyvqgd6wepcpe8", + "task_id": "bugs_01kyvqgd6wepcpe8", + "original_intent": "Catch up PR #1109 receipt freshness on Decapod v0.89.2", + "derived_intent": "Publish a focused receipt-freshness fix based on current master, with integrity, trajectory binding, regression proof, and passing governance validation", + "destination": "PR #1109", "current_phase": "validation", "next_transitions": [ "publish" ], "active_boundaries": [ - "Repository validation and tracked proof artifacts" + "Do not broaden into unrelated governance or dactyl work", + "Do not modify protected master directly" ], "repo_scope": [ - ".decapod/governance/trajectory.json", - ".decapod/governance/validation.json" + "governed artifacts via Decapod CLI", + "src/decapod/core/governance_artifacts.rs", + "tests" ], "inspected_files": [], "modified_files": [ ".decapod/governance/trajectory.json", - ".decapod/governance/validation.json", - "src/decapod/core/migration.rs" + ".decapod/governance/validation.json" ], "declared_commands": [ - "cargo clippy --all-targets -- -D warnings", - "cargo test --test single_datastore", - "decapod validate", - "decapod validate --format json" + "decapod validate" ], "tool_calls": [], "checks": [ - { - "name": "cargo_clippy", - "status": "passed" - }, { "name": "decapod validate", "status": "passed" - }, - { - "name": "decapod_validate", - "status": "passed" - }, - { - "name": "legacy_schema_width_migration", - "status": "passed" } ], "evidence": [ - "ci_failure=legacy federation_events target width mismatch;fix=intersection-column migration copy;validation_epoch=ve_5741a813a4d73036", - "validation epoch ve_5741a813a4d73036 completed with zero failures", - "validation epoch ve_8ec2ce3415249bba completed with zero failures" + "validation epoch ve_6f9c8095ab51dfe9 completed with zero failures", + "validation epoch ve_c0ba84da3cdcbf87 completed with zero failures" ], "shortcut_risk_signals": [], "unresolved_assumptions": [], @@ -62,22 +47,24 @@ "shortcut_risk": "supported", "completion_proof": "supported" }, - "artifact_hash": "sha256:a2e70d777a969b91139720fb251b1e4d3e8d44fd0a7f2d7dc71c34b0d8c5e3a9", + "artifact_hash": "sha256:717a17ab6b0c9cee60a778320bfa2a789626c7e228eaaf7f49c12da48375f45f", "custody": { "schema_version": "1.0.0", "intents": { - "intent:validation_01KYVNVYKC1JR6ATWMPV703GF5": { - "id": "intent:validation_01KYVNVYKC1JR6ATWMPV703GF5", - "raw_intent": "Produce proof artifacts for a repository validation completion.", - "refined_intent": "Run the bounded validation gates and bind the successful receipt to a trajectory.", + "intent:run_01kyvqgd6wepcpe8": { + "id": "intent:run_01kyvqgd6wepcpe8", + "raw_intent": "Catch up PR #1109 receipt freshness on Decapod v0.89.2", + "refined_intent": "Publish a focused receipt-freshness fix based on current master, with integrity, trajectory binding, regression proof, and passing governance validation", "acceptance_criteria": [], "constraints": [ - ".decapod/governance/trajectory.json", - ".decapod/governance/validation.json" + "src/decapod/core/governance_artifacts.rs", + "tests", + "governed artifacts via Decapod CLI" ], "assumptions": [], "boundaries": [ - "Repository validation and tracked proof artifacts" + "Do not modify protected master directly", + "Do not broaden into unrelated governance or dactyl work" ], "out_of_scope": [], "proof_requirements": [], @@ -90,48 +77,34 @@ { "sequence": 2, "event_id": "event:2", - "intent_id": "intent:validation_01KYVNVYKC1JR6ATWMPV703GF5", + "intent_id": "intent:run_01kyvqgd6wepcpe8", "kind": "created" }, { "sequence": 3, "event_id": "event:3", - "intent_id": "intent:validation_01KYVNVYKC1JR6ATWMPV703GF5", + "intent_id": "intent:run_01kyvqgd6wepcpe8", "kind": "refined" }, { "sequence": 4, "event_id": "event:4", - "intent_id": "intent:validation_01KYVNVYKC1JR6ATWMPV703GF5", + "intent_id": "intent:run_01kyvqgd6wepcpe8", "kind": "trajectory_step_recorded", - "detail": "trajectory:validation_01KYVNVYKC1JR6ATWMPV703GF5" + "detail": "trajectory:run_01kyvqgd6wepcpe8" }, { "sequence": 5, "event_id": "event:5", - "intent_id": "intent:validation_01KYVNVYKC1JR6ATWMPV703GF5", + "intent_id": "intent:run_01kyvqgd6wepcpe8", "kind": "trajectory_step_recorded", - "detail": "trajectory:validation_01KYVNVYKC1JR6ATWMPV703GF5" - }, - { - "sequence": 6, - "event_id": "event:6", - "intent_id": "intent:validation_01KYVNVYKC1JR6ATWMPV703GF5", - "kind": "trajectory_step_recorded", - "detail": "trajectory:validation_01KYVNVYKC1JR6ATWMPV703GF5" - }, - { - "sequence": 7, - "event_id": "event:7", - "intent_id": "intent:validation_01KYVNVYKC1JR6ATWMPV703GF5", - "kind": "trajectory_step_recorded", - "detail": "trajectory:validation_01KYVNVYKC1JR6ATWMPV703GF5" + "detail": "trajectory:run_01kyvqgd6wepcpe8" } ], "trajectories": { - "validation_01KYVNVYKC1JR6ATWMPV703GF5": { - "id": "validation_01KYVNVYKC1JR6ATWMPV703GF5", - "intent_id": "intent:validation_01KYVNVYKC1JR6ATWMPV703GF5", + "run_01kyvqgd6wepcpe8": { + "id": "run_01kyvqgd6wepcpe8", + "intent_id": "intent:run_01kyvqgd6wepcpe8", "evidence_only": true, "steps": [ { @@ -139,13 +112,14 @@ "action": "validation", "command": "decapod validate", "scope": [ - ".decapod/governance/trajectory.json", - ".decapod/governance/validation.json" + "governed artifacts via Decapod CLI", + "src/decapod/core/governance_artifacts.rs", + "tests" ], "observations": [ ".decapod/governance/trajectory.json", ".decapod/governance/validation.json", - "validation epoch ve_8ec2ce3415249bba completed with zero failures" + "validation epoch ve_6f9c8095ab51dfe9 completed with zero failures" ], "proof_refs": [ "decapod validate" @@ -158,64 +132,26 @@ "action": "validation", "command": "decapod validate", "scope": [ - ".decapod/governance/trajectory.json", - ".decapod/governance/validation.json" + "governed artifacts via Decapod CLI", + "src/decapod/core/governance_artifacts.rs", + "tests" ], "observations": [ ".decapod/governance/trajectory.json", ".decapod/governance/validation.json", - "validation epoch ve_5741a813a4d73036 completed with zero failures" + "validation epoch ve_c0ba84da3cdcbf87 completed with zero failures" ], "proof_refs": [ "decapod validate" ], "validation_findings": [], "custody_event_id": "event:5" - }, - { - "sequence": 6, - "action": "validation", - "command": "decapod validate", - "scope": [ - ".decapod/governance/trajectory.json", - ".decapod/governance/validation.json" - ], - "observations": [ - ".decapod/governance/trajectory.json", - ".decapod/governance/validation.json", - "validation epoch ve_5741a813a4d73036 completed with zero failures" - ], - "proof_refs": [ - "decapod validate" - ], - "validation_findings": [], - "custody_event_id": "event:6" - }, - { - "sequence": 7, - "action": "validation", - "command": "cargo clippy --all-targets -- -D warnings", - "scope": [ - ".decapod/governance/trajectory.json", - ".decapod/governance/validation.json" - ], - "observations": [ - "src/decapod/core/migration.rs", - "ci_failure=legacy federation_events target width mismatch;fix=intersection-column migration copy;validation_epoch=ve_5741a813a4d73036" - ], - "proof_refs": [ - "legacy_schema_width_migration", - "cargo_clippy", - "decapod_validate" - ], - "validation_findings": [], - "custody_event_id": "event:7" } ] } }, "summaries": {}, "knowledge_candidates": [], - "next_sequence": 7 + "next_sequence": 5 } } \ No newline at end of file diff --git a/.decapod/governance/validation.json b/.decapod/governance/validation.json index f054f01d..5b8d2564 100644 --- a/.decapod/governance/validation.json +++ b/.decapod/governance/validation.json @@ -1,36 +1,36 @@ { "schema_version": "1.0.0", "kind": "validation_receipt", - "decapod_release": "0.89.1", - "git_revision": "907ef47c0d2e82d62b1bf0e9a4f8f7aa96b8d54b", - "repo_signal_fingerprint": "ebbcc367c96c39a3d5da07e21403855450026d704cec7df0a1914500e69a671e", - "trajectory_run_id": "validation_01KYVNVYKC1JR6ATWMPV703GF5", - "trajectory_artifact_hash": "sha256:424d75d6ce5c195db81c40b8f669cc0d4d4609d56fbf731fa7d4884125f42a0b", + "decapod_release": "0.89.2", + "git_revision": "ce146d01d81dd9b37fb919713c45ea65ceae5794", + "repo_signal_fingerprint": "801391ab86770949ac239b3b7fe6e38ac1e2ea1194e382390831a42a907175fa", + "trajectory_run_id": "run_01kyvqgd6wepcpe8", + "trajectory_artifact_hash": "sha256:717a17ab6b0c9cee60a778320bfa2a789626c7e228eaaf7f49c12da48375f45f", "validation_epoch": { "schema_version": "1.0.0", - "epoch_id": "ve_5741a813a4d73036", - "evaluator_identity": "decapod-validate@0.89.1", - "evaluator_set_hash": "sha256:22aa98e5109b89746d9abe67a42b8ada8476f270622156a9ccd18d450bf14c7a", - "constitution_version": "embedded-docs@0.89.1", - "constitution_hash": "sha256:796ee96a9e60df0aa24f09e37e28352e2f549d811df3a1921665d2139d9e488d", + "epoch_id": "ve_c0ba84da3cdcbf87", + "evaluator_identity": "decapod-validate@0.89.2", + "evaluator_set_hash": "sha256:4e1cf0bcaa7de808e6e7468d6923396e12a32c59689f5e481de03eccebfa03f5", + "constitution_version": "embedded-docs@0.89.2", + "constitution_hash": "sha256:6ee45638ea182180979e422d2e8297eb33ed328516493ad4f184154e6307806c", "validation_profile": "default", "validation_profile_hash": "sha256:37a8eec1ce19687d132fe29051dca629d164e2c4958ba141d5f4133a33f0688f", "proof_rubric": "decapod-validate/current-proof-v1", "proof_rubric_hash": "sha256:99e8ef25629da391e9b479db9fff52b82f340224f7a69d8a88d2992ae8e90466", - "generated_specs_manifest_hash": "sha256:f653aee9533e62733fb35a112017a6c24cd34582b9a5cbe22cdc2a9c962d3a83", - "generated_specs_fingerprint": "ebbcc367c96c39a3d5da07e21403855450026d704cec7df0a1914500e69a671e", + "generated_specs_manifest_hash": "sha256:1c9bfb8ab3421d971376ab7140249b2fc2c16a090f2090e740cb9a187caf44cf", + "generated_specs_fingerprint": "801391ab86770949ac239b3b7fe6e38ac1e2ea1194e382390831a42a907175fa", "material_hashes": { - "constitution:core/DECAPOD": "sha256:796ee96a9e60df0aa24f09e37e28352e2f549d811df3a1921665d2139d9e488d", - "generated_spec:.decapod/managed/specs/ARCHITECTURE.md": "sha256:effc9ae8197ec211fa565b112e42d68c066c305a3b98982c44e2e26efc19865b", - "generated_spec:.decapod/managed/specs/INTENT.md": "sha256:b79033ecc9ce7940961ec21693a32e94fb0f21e0d9cdc11f9e853bae6451db4f", - "generated_spec:.decapod/managed/specs/INTERFACES.md": "sha256:1892a9b0ab482167dbf17cf0986f233ac8384a3779c231bdea524b4f5090aa2f", - "generated_spec:.decapod/managed/specs/OPERATIONS.md": "sha256:be4ab35b38b32fde0324badd1826d050ad7af8744159917ed0717f286f7c9324", - "generated_spec:.decapod/managed/specs/README.md": "sha256:726ef570b71c24fab4d3f8199537f57d2f1e2de48bcccd9887d717a4ceba51b4", - "generated_spec:.decapod/managed/specs/SECURITY.md": "sha256:ccf15b0714e7fe028e74c5a5f804354f03ca7b91e618a76f35464095d7cda651", - "generated_spec:.decapod/managed/specs/SEMANTICS.md": "sha256:541d81412968f4ff9d29a543b9d0a52fe582319e4ac354c45ff8c670f4f12953", - "generated_spec:.decapod/managed/specs/VALIDATION.md": "sha256:bc5678d3f0eb920a86500afc4dd81531b66f69f823d022477cecb4206675882c", - "generated_specs_fingerprint": "ebbcc367c96c39a3d5da07e21403855450026d704cec7df0a1914500e69a671e", - "generated_specs_manifest": "sha256:f653aee9533e62733fb35a112017a6c24cd34582b9a5cbe22cdc2a9c962d3a83", + "constitution:core/DECAPOD": "sha256:6ee45638ea182180979e422d2e8297eb33ed328516493ad4f184154e6307806c", + "generated_spec:.decapod/managed/specs/ARCHITECTURE.md": "sha256:103939943d6468ca3cf9447d59fac7c42994379f1288088384b94ff37a011cc6", + "generated_spec:.decapod/managed/specs/INTENT.md": "sha256:89853b299abeffaf208ff681c09a427477eeacef3de5aad5df5b7922f7b5563f", + "generated_spec:.decapod/managed/specs/INTERFACES.md": "sha256:9829820573b88602e889a1c6f0277e6ff0a8f93aac80f18ab397a123bb0a7881", + "generated_spec:.decapod/managed/specs/OPERATIONS.md": "sha256:e4669fbd7f15b49ddfd59c4c5a0fde39ebd3374f55bbd5921e44d2db3af38278", + "generated_spec:.decapod/managed/specs/README.md": "sha256:3ad467618629bc9915759db8002f797a4bca16d8a4ae505cc2e4fed10edb9f25", + "generated_spec:.decapod/managed/specs/SECURITY.md": "sha256:846a2ab8d8ca0bd2e06564f3c71957f59fd0247219664983d8afb55737d1a675", + "generated_spec:.decapod/managed/specs/SEMANTICS.md": "sha256:6e26f25ccdd8a03fa6fef887aec539a49f4315729c213bd4f15de08915d2d1c4", + "generated_spec:.decapod/managed/specs/VALIDATION.md": "sha256:5dc10e542fa7d76e021bd86a2ff9d7700b5adf1336b14bd1bcc8e3afb85054da", + "generated_specs_fingerprint": "801391ab86770949ac239b3b7fe6e38ac1e2ea1194e382390831a42a907175fa", + "generated_specs_manifest": "sha256:1c9bfb8ab3421d971376ab7140249b2fc2c16a090f2090e740cb9a187caf44cf", "proof_rubric": "sha256:99e8ef25629da391e9b479db9fff52b82f340224f7a69d8a88d2992ae8e90466", "validation_profile": "sha256:37a8eec1ce19687d132fe29051dca629d164e2c4958ba141d5f4133a33f0688f" } @@ -39,7 +39,7 @@ "pass_count": 197, "fail_count": 0, "warn_count": 4, - "elapsed_ms": 45865, + "elapsed_ms": 23268, "drift_findings": [], "temporary_artifacts_cleaned": 0, "failures": [], @@ -47,120 +47,116 @@ "Risk map missing (run `decapod riskmap init`)", "Spec markdown drift checks are hygiene-only. Use validate_machine_contract for authoritative governance.", "Watcher audit trail missing (run `decapod govern watcher run`)", - "[federation.rebuild_determinism] DB diverged from event replay. Current: 503 nodes/503 sources/406 edges. Rebuilt: 16 nodes/16 sources/7 edges. Run: decapod data federation rebuild" + "[federation.rebuild_determinism] DB diverged from event replay. Current: 505 nodes/505 sources/406 edges. Rebuilt: 16 nodes/16 sources/7 edges. Run: decapod data federation rebuild" ], "gate_timings": [ { "name": "validate_markdown_primitives_roundtrip_gate", - "elapsed_ms": 24030 + "elapsed_ms": 20336 }, { "name": "validate_machine_contract", - "elapsed_ms": 19261 + "elapsed_ms": 2083 }, { "name": "validate_control_plane_contract", - "elapsed_ms": 270 + "elapsed_ms": 273 }, { - "name": "validate_project_specs_docs", - "elapsed_ms": 219 + "name": "validate_federation_gates", + "elapsed_ms": 124 }, { "name": "validate_schema_determinism", - "elapsed_ms": 200 + "elapsed_ms": 35 }, { - "name": "validate_federation_gates", - "elapsed_ms": 187 + "name": "validate_obligations", + "elapsed_ms": 30 }, { - "name": "validate_embedded_self_contained", - "elapsed_ms": 131 + "name": "validate_project_specs_docs", + "elapsed_ms": 18 }, { - "name": "validate_no_legacy_namespaces", - "elapsed_ms": 121 + "name": "validate_embedded_self_contained", + "elapsed_ms": 13 }, { - "name": "validate_knowledge_integrity", - "elapsed_ms": 104 + "name": "validate_archive_integrity", + "elapsed_ms": 11 }, { - "name": "validate_policy_integrity", - "elapsed_ms": 102 + "name": "validate_no_legacy_namespaces", + "elapsed_ms": 8 }, { - "name": "validate_canon_mutation", - "elapsed_ms": 100 + "name": "validate_knowledge_integrity", + "elapsed_ms": 6 }, { - "name": "validate_watcher_purity", - "elapsed_ms": 95 + "name": "validate_lineage_hard_gate", + "elapsed_ms": 5 }, { - "name": "validate_risk_map_violations", - "elapsed_ms": 91 + "name": "validate_repomap_determinism", + "elapsed_ms": 5 }, { - "name": "validate_obligations", - "elapsed_ms": 38 + "name": "validate_policy_integrity", + "elapsed_ms": 4 }, { - "name": "validate_archive_integrity", - "elapsed_ms": 30 + "name": "validate_canon_mutation", + "elapsed_ms": 4 }, { - "name": "validate_repomap_determinism", - "elapsed_ms": 25 + "name": "validate_watcher_purity", + "elapsed_ms": 4 }, { - "name": "validate_lineage_hard_gate", - "elapsed_ms": 20 + "name": "validate_risk_map_violations", + "elapsed_ms": 3 }, { "name": "validate_health_purity", - "elapsed_ms": 10 - }, - { - "name": "validate_entrypoint_invariants", - "elapsed_ms": 3 + "elapsed_ms": 2 }, { "name": "validate_gatekeeper_gate", - "elapsed_ms": 3 - }, - { - "name": "validate_research_claims_if_present", - "elapsed_ms": 3 + "elapsed_ms": 2 }, { - "name": "validate_interface_contract_bootstrap", + "name": "validate_generated_artifact_whitelist", "elapsed_ms": 2 }, { "name": "validate_database_schema_versions", - "elapsed_ms": 2 + "elapsed_ms": 1 }, { - "name": "validate_generated_artifact_whitelist", - "elapsed_ms": 2 + "name": "validate_health_cache_integrity", + "elapsed_ms": 1 }, { - "name": "validate_health_cache_integrity", - "elapsed_ms": 2 + "name": "validate_interface_contract_bootstrap", + "elapsed_ms": 0 }, { - "name": "validate_heartbeat_invocation_gate", - "elapsed_ms": 1 + "name": "validate_research_claims_if_present", + "elapsed_ms": 0 }, { "name": "validate_repo_map", - "elapsed_ms": 1 + "elapsed_ms": 0 }, { - "name": "validate_validation_receipt_if_present", - "elapsed_ms": 1 + "name": "validate_heartbeat_invocation_gate", + "elapsed_ms": 0 + }, + { + "name": "validate_entrypoint_invariants", + "elapsed_ms": 0 }, { "name": "validate_trajectory_artifacts_if_present", @@ -171,15 +167,15 @@ "elapsed_ms": 0 }, { - "name": "validate_project_scoped_state", + "name": "validate_validation_receipt_if_present", "elapsed_ms": 0 }, { - "name": "validate_spec_drift", + "name": "validate_project_scoped_state", "elapsed_ms": 0 }, { - "name": "validate_context_capsule_policy_contract", + "name": "validate_spec_drift", "elapsed_ms": 0 }, { @@ -187,11 +183,11 @@ "elapsed_ms": 0 }, { - "name": "validate_docs_templates_bucket", + "name": "validate_context_capsule_policy_contract", "elapsed_ms": 0 }, { - "name": "validate_git_workspace_context", + "name": "validate_docs_templates_bucket", "elapsed_ms": 0 }, { @@ -199,7 +195,7 @@ "elapsed_ms": 0 }, { - "name": "validate_coplayer_policy_tightening", + "name": "validate_eval_gate_if_required", "elapsed_ms": 0 }, { @@ -218,20 +214,24 @@ "name": "validate_risk_map", "elapsed_ms": 0 }, + { + "name": "validate_coplayer_policy_tightening", + "elapsed_ms": 0 + }, { "name": "validate_recursive_improvement_passes_if_present", "elapsed_ms": 0 }, { - "name": "validate_context_capsules_if_present", + "name": "validate_internalization_artifacts_if_present", "elapsed_ms": 0 }, { - "name": "validate_eval_gate_if_required", + "name": "validate_context_capsules_if_present", "elapsed_ms": 0 }, { - "name": "validate_internalization_artifacts_if_present", + "name": "validate_git_workspace_context", "elapsed_ms": 0 }, { @@ -239,7 +239,7 @@ "elapsed_ms": 0 }, { - "name": "validate_plan_governed_execution_gate", + "name": "validate_lcm_rebuild_gate", "elapsed_ms": 0 }, { @@ -247,7 +247,7 @@ "elapsed_ms": 0 }, { - "name": "validate_lcm_rebuild_gate", + "name": "validate_plan_governed_execution_gate", "elapsed_ms": 0 }, { @@ -271,12 +271,12 @@ "Risk map missing (run `decapod riskmap init`)", "Spec markdown drift checks are hygiene-only. Use validate_machine_contract for authoritative governance.", "Watcher audit trail missing (run `decapod govern watcher run`)", - "[federation.rebuild_determinism] DB diverged from event replay. Current: 503 nodes/503 sources/406 edges. Rebuilt: 16 nodes/16 sources/7 edges. Run: decapod data federation rebuild" + "[federation.rebuild_determinism] DB diverged from event replay. Current: 505 nodes/505 sources/406 edges. Rebuilt: 16 nodes/16 sources/7 edges. Run: decapod data federation rebuild" ], "recommendations": [ "Review the reported validation warnings before relying on the CI result.", "Use `decapod validate -v --format json` to inspect the affected gate output." ] }, - "receipt_hash": "sha256:fe78a88de7e39b768a8d0ea3d05b8a31a24af1215981d15d2b12ea9e1ea59f2b" + "receipt_hash": "sha256:73dea8ca7784234322620130abcf03ceccc8f3f8722d0ddf829dd3c8d57d01cd" } \ No newline at end of file diff --git a/.decapod/managed/Dockerfile.decapod b/.decapod/managed/Dockerfile.decapod index 51eb7f32..f5a006bc 100644 --- a/.decapod/managed/Dockerfile.decapod +++ b/.decapod/managed/Dockerfile.decapod @@ -2,9 +2,9 @@ # Path: .decapod/managed/Dockerfile.decapod # Managed seed: Decapod maintains the image/version header; agents may # mutate project-specific packages and commands in workspace branches. -ARG DECAPOD_IMAGE=ghcr.io/decapodlabs/decapod:v0.89.1-debian +ARG DECAPOD_IMAGE=ghcr.io/decapodlabs/decapod:v0.89.2-debian FROM $DECAPOD_IMAGE -ARG DECAPOD_VERSION=0.89.1 +ARG DECAPOD_VERSION=0.89.2 ARG DECAPOD_WORKSPACE_PATH=unknown ARG DECAPOD_USE_LOCAL_BINARY=0 LABEL org.opencontainers.image.base.name="$DECAPOD_IMAGE" diff --git a/.decapod/managed/specs/.manifest.json b/.decapod/managed/specs/.manifest.json index 0cf29427..034e67c3 100644 --- a/.decapod/managed/specs/.manifest.json +++ b/.decapod/managed/specs/.manifest.json @@ -1,8 +1,8 @@ { "schema_version": "1.1.0", "template_version": "scaffold-v3", - "generated_at": "1785488420Z", - "repo_signal_fingerprint": "ebbcc367c96c39a3d5da07e21403855450026d704cec7df0a1914500e69a671e", + "generated_at": "1785495757Z", + "repo_signal_fingerprint": "32190ba5f3fb257ad1e35ca19669c2d275bd16c18822287a1403c2267a943fd3", "declared_capabilities": [ "agent-helper", "authentication", @@ -17,81 +17,81 @@ ], "capability_definition_version": "1.0.0", "config_input_hash": "4fd4fbb5f2e6bb3ce0c0f14971ef28a2067288d440caa70b9d22915f184a73b6", - "spec_input_hash": "0866c55ee0b8d19c5f14ea9254dc7a3bc1c0fec5b3625f0324ac266951193872", - "decapod_release": "0.89.1", + "spec_input_hash": "21a7f87328a0db637c63ae053a03b767495f266b5993a7059c84259252a3e802", + "decapod_release": "0.89.2", "entrypoints": [ { "path": "AGENTS.md", - "template_hash": "2eac432fa2a632c53cd2c39b409be7ca2064a5759aba7fee26867edcf5292a35", - "content_hash": "2eac432fa2a632c53cd2c39b409be7ca2064a5759aba7fee26867edcf5292a35", - "fingerprint": "a8cdfaec4eb867208d46e1d51dcf4e1ee1c8472c623bff809e10339de85db3bd" + "template_hash": "6a13a5ed2814e2e6d141c216367158eae22c59af7c21bb72e96bd2793c5ff748", + "content_hash": "6a13a5ed2814e2e6d141c216367158eae22c59af7c21bb72e96bd2793c5ff748", + "fingerprint": "e7efb554d763f5e15e453e72b339b158791df3815abb462d29b2b9cf9efc6b70" }, { "path": "CLAUDE.md", - "template_hash": "147458dc06463139410428255efbbf7f785842ed2ba3393b52ec4a00ca63e67e", - "content_hash": "147458dc06463139410428255efbbf7f785842ed2ba3393b52ec4a00ca63e67e", - "fingerprint": "94489801bbda1a3da4fd3f4e8ab8506b93ab98271f94c9c98ce502130873f15c" + "template_hash": "65f3e341d0cfea7916f625fc5dcebd00cdc60207433b0274eaaed8f7be8933d2", + "content_hash": "65f3e341d0cfea7916f625fc5dcebd00cdc60207433b0274eaaed8f7be8933d2", + "fingerprint": "c134c6db6ba7f4d57db1d5ee037419b35ac8f0baabc5b8cda036dba796e4ec9a" }, { "path": "GEMINI.md", - "template_hash": "b4a9cd0193e275b55393e3d922406908e327a315ec67dac4ae6de61fdcbedd83", - "content_hash": "b4a9cd0193e275b55393e3d922406908e327a315ec67dac4ae6de61fdcbedd83", - "fingerprint": "3060d4b449b4fd9b9814f66eea232b5835096569a771b8a0f08eb3170df5a007" + "template_hash": "a7290a2b2b6679f557c93f6bfb39cce8281227b08e762104ecbab286b659cdbc", + "content_hash": "a7290a2b2b6679f557c93f6bfb39cce8281227b08e762104ecbab286b659cdbc", + "fingerprint": "3c1a04cb389e23a0e829ffce8b871d8b14f13bed48d7c7cada19acba99495531" }, { "path": "CODEX.md", - "template_hash": "acb2578f44c606911ef180278d2cf6e7cb6b3b3c894fb3d9b74cadb2b572de48", - "content_hash": "acb2578f44c606911ef180278d2cf6e7cb6b3b3c894fb3d9b74cadb2b572de48", - "fingerprint": "a0fdb0b4a415c7510ee0e72c6498a260a2b6ad96c90d702226ef7a4c27653000" + "template_hash": "ca7e6d59ef4f4449c40988e7d3c59f3debfa4d81126face06ca8cf8995728da4", + "content_hash": "ca7e6d59ef4f4449c40988e7d3c59f3debfa4d81126face06ca8cf8995728da4", + "fingerprint": "90f8f372fd962eb6ef19bad1a8836eddafbfa017a619917f9977d757742c551d" } ], "files": [ { "path": ".decapod/managed/specs/README.md", "template_hash": "6957cf384fae78bcc966182ee914b88230c0cbaea909b7b406c53bece8bbfe6c", - "content_hash": "726ef570b71c24fab4d3f8199537f57d2f1e2de48bcccd9887d717a4ceba51b4", + "content_hash": "f91ddf3d04c10568a7efc2bb89ef4f34c27ee738aa2a4b61978060eab0226539", "fingerprint": "" }, { "path": ".decapod/managed/specs/INTENT.md", "template_hash": "e6c06430e23ca87ae33169a6b1f9c70a3fd283655f5b5b29755fb83181f989e9", - "content_hash": "b79033ecc9ce7940961ec21693a32e94fb0f21e0d9cdc11f9e853bae6451db4f", + "content_hash": "665fb7a9d19dc55eb491a823802ac1b1f15908c59644cb62a51cd2ec529b90e3", "fingerprint": "" }, { "path": ".decapod/managed/specs/ARCHITECTURE.md", - "template_hash": "58f2abf6f6e0c3da62bc0850e71e12abd76c5064c793701c7b9330036713b163", - "content_hash": "effc9ae8197ec211fa565b112e42d68c066c305a3b98982c44e2e26efc19865b", + "template_hash": "f81224214db72e367670029174b0b69cf21ea2240d14ba936257324f2a9449d7", + "content_hash": "1d62913990e4fa826f018bcdb1a51cdb0ba605088af50eeb08df2ddfcb4ea070", "fingerprint": "" }, { "path": ".decapod/managed/specs/INTERFACES.md", "template_hash": "15cc8c3bbfe951da695384106eb78a44c7af28ddc002bcc83c8a7770229fe999", - "content_hash": "1892a9b0ab482167dbf17cf0986f233ac8384a3779c231bdea524b4f5090aa2f", + "content_hash": "f2442e7a13f9222d39709a8da9dd76d239536375c5f7f68dde7f3793adf17c8a", "fingerprint": "" }, { "path": ".decapod/managed/specs/VALIDATION.md", "template_hash": "8e22417cbcbd602159a516e761bdf78173fc4f27bac6d92d91a4ccb968534395", - "content_hash": "bc5678d3f0eb920a86500afc4dd81531b66f69f823d022477cecb4206675882c", + "content_hash": "a957321d47e4034dd315784b9fad941a08e44e4edb333ef12ebef7ac16ee2e00", "fingerprint": "" }, { "path": ".decapod/managed/specs/SEMANTICS.md", "template_hash": "3a79850c94b81e29c6462a7ea70d45198252e3eeba2132c6e9206f7b4e4a5829", - "content_hash": "541d81412968f4ff9d29a543b9d0a52fe582319e4ac354c45ff8c670f4f12953", + "content_hash": "cefcffcdac23c2f1f563f3fd27cc1344204ca84b805803291a8815321333a78f", "fingerprint": "" }, { "path": ".decapod/managed/specs/OPERATIONS.md", "template_hash": "54ab819ae22bea9f786793a37196749cbacd72c60f58108898e9db044c534acf", - "content_hash": "be4ab35b38b32fde0324badd1826d050ad7af8744159917ed0717f286f7c9324", + "content_hash": "185435b76e711f26b8e02143426bcd082d24b16ba74e20589ddd77a1d0eb149c", "fingerprint": "" }, { "path": ".decapod/managed/specs/SECURITY.md", "template_hash": "b283bdc62b7c2fce411becf6b31928d1b3502da28be7ad618531e3e1820fc147", - "content_hash": "ccf15b0714e7fe028e74c5a5f804354f03ca7b91e618a76f35464095d7cda651", + "content_hash": "5cfce1b137272bcfac2b0f3996c775f64bc42ac048e307dc43a35d46fff4af58", "fingerprint": "" } ] diff --git a/.decapod/managed/specs/ARCHITECTURE.md b/.decapod/managed/specs/ARCHITECTURE.md index a6844535..6f1cba65 100644 --- a/.decapod/managed/specs/ARCHITECTURE.md +++ b/.decapod/managed/specs/ARCHITECTURE.md @@ -138,7 +138,7 @@ sequenceDiagram ## Codebase Attestation -- Repository signal fingerprint: `ebbcc367c96c39a3d5da07e21403855450026d704cec7df0a1914500e69a671e` +- Repository signal fingerprint: `32190ba5f3fb257ad1e35ca19669c2d275bd16c18822287a1403c2267a943fd3` - Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (100 files), `tests/` (4 files) - Refreshed from the current codebase by `decapod specs.refresh` diff --git a/.decapod/managed/specs/INTENT.md b/.decapod/managed/specs/INTENT.md index ddd5caca..2877cc3c 100644 --- a/.decapod/managed/specs/INTENT.md +++ b/.decapod/managed/specs/INTENT.md @@ -132,7 +132,7 @@ flowchart LR ## Codebase Attestation -- Repository signal fingerprint: `ebbcc367c96c39a3d5da07e21403855450026d704cec7df0a1914500e69a671e` +- Repository signal fingerprint: `32190ba5f3fb257ad1e35ca19669c2d275bd16c18822287a1403c2267a943fd3` - Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (100 files), `tests/` (4 files) - Refreshed from the current codebase by `decapod specs.refresh` diff --git a/.decapod/managed/specs/INTERFACES.md b/.decapod/managed/specs/INTERFACES.md index 75b8184f..ebe6072a 100644 --- a/.decapod/managed/specs/INTERFACES.md +++ b/.decapod/managed/specs/INTERFACES.md @@ -73,7 +73,7 @@ pub enum ApiError { ## Codebase Attestation -- Repository signal fingerprint: `ebbcc367c96c39a3d5da07e21403855450026d704cec7df0a1914500e69a671e` +- Repository signal fingerprint: `32190ba5f3fb257ad1e35ca19669c2d275bd16c18822287a1403c2267a943fd3` - Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (100 files), `tests/` (4 files) - Refreshed from the current codebase by `decapod specs.refresh` diff --git a/.decapod/managed/specs/OPERATIONS.md b/.decapod/managed/specs/OPERATIONS.md index 21a1abc0..7b73aaf2 100644 --- a/.decapod/managed/specs/OPERATIONS.md +++ b/.decapod/managed/specs/OPERATIONS.md @@ -54,7 +54,7 @@ Use `tracing` + `tracing-subscriber` with structured JSON output and request cor ## Codebase Attestation -- Repository signal fingerprint: `ebbcc367c96c39a3d5da07e21403855450026d704cec7df0a1914500e69a671e` +- Repository signal fingerprint: `32190ba5f3fb257ad1e35ca19669c2d275bd16c18822287a1403c2267a943fd3` - Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (100 files), `tests/` (4 files) - Refreshed from the current codebase by `decapod specs.refresh` diff --git a/.decapod/managed/specs/README.md b/.decapod/managed/specs/README.md index ab56950d..98de41b3 100644 --- a/.decapod/managed/specs/README.md +++ b/.decapod/managed/specs/README.md @@ -47,7 +47,7 @@ These files are the project-local contract for humans and agents. ## Codebase Attestation -- Repository signal fingerprint: `ebbcc367c96c39a3d5da07e21403855450026d704cec7df0a1914500e69a671e` +- Repository signal fingerprint: `32190ba5f3fb257ad1e35ca19669c2d275bd16c18822287a1403c2267a943fd3` - Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (100 files), `tests/` (4 files) - Refreshed from the current codebase by `decapod specs.refresh` diff --git a/.decapod/managed/specs/SECURITY.md b/.decapod/managed/specs/SECURITY.md index da26331f..da6fd340 100644 --- a/.decapod/managed/specs/SECURITY.md +++ b/.decapod/managed/specs/SECURITY.md @@ -83,7 +83,7 @@ Describe the security primitives and security controls implemented in this repos ## Codebase Attestation -- Repository signal fingerprint: `ebbcc367c96c39a3d5da07e21403855450026d704cec7df0a1914500e69a671e` +- Repository signal fingerprint: `32190ba5f3fb257ad1e35ca19669c2d275bd16c18822287a1403c2267a943fd3` - Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (100 files), `tests/` (4 files) - Refreshed from the current codebase by `decapod specs.refresh` diff --git a/.decapod/managed/specs/SEMANTICS.md b/.decapod/managed/specs/SEMANTICS.md index 9d17ad96..a2fb6070 100644 --- a/.decapod/managed/specs/SEMANTICS.md +++ b/.decapod/managed/specs/SEMANTICS.md @@ -39,7 +39,7 @@ stateDiagram-v2 ## Codebase Attestation -- Repository signal fingerprint: `ebbcc367c96c39a3d5da07e21403855450026d704cec7df0a1914500e69a671e` +- Repository signal fingerprint: `32190ba5f3fb257ad1e35ca19669c2d275bd16c18822287a1403c2267a943fd3` - Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (100 files), `tests/` (4 files) - Refreshed from the current codebase by `decapod specs.refresh` diff --git a/.decapod/managed/specs/VALIDATION.md b/.decapod/managed/specs/VALIDATION.md index 010cca1f..82525651 100644 --- a/.decapod/managed/specs/VALIDATION.md +++ b/.decapod/managed/specs/VALIDATION.md @@ -59,61 +59,15 @@ flowchart LR | Validation report | `.decapod/managed/artifacts/provenance/*` | Current-run diagnostics; not a tracked promotion record | | Test logs | CI artifact store | Promotion | | Architecture diagram snapshot | `ARCHITECTURE.md` | Promotion | -| Changelog entry | `CHANGELOG.md` | Promotion | - - - -## Background Processing Validation Overlay - -### Duplicate Delivery Tests -- Same message delivered multiple times MUST produce same result -- Idempotency key verification -- Verify the declared delivery guarantee; do not claim exactly-once behavior without proof - -### Retry Tests -- Configured retry/backoff policy verified -- Configured retry bound or unbounded policy verified -- Poison-work handling verified when the project declares it - -### Shutdown Tests -- Graceful drain on signal -- In-flight job completion or safe requeue -- No data loss on forced termination - - - - -## Persistent State Validation Overlay - -### Migration Proof Command -- Configure `repo.migration_validation.command` and its arguments as the executable migration proof; file presence is not proof -- The configured command MUST define its working directory, timeout, expected exit code, and evidence output - -### Migration Tests -- All migrations MUST have integration tests -- Rollback procedures MUST be tested -- Data integrity checks post-migration - -### Persistence Integration Tests -- Repository abstraction tested against real database -- Transaction boundary tests -- Concurrency conflict tests -- Data integrity validation after recovery - - -## Regression Guardrails +| Changelog entry | `CHANGELOG.md` | Promotion |## Regression Guardrails - Baseline references: - Statistical thresholds (if non-deterministic): -- Rollback criteria: - -## Bounded Execution +- Rollback criteria:## Bounded Execution | Operation | Timeout | Failure Mode | |---|---|---| | Validation | 30s | timeout or lock | | Unit test suite | project-defined | non-zero exit | -| Integration suite | project-defined | non-zero exit | - -## Coverage Checklist +| Integration suite | project-defined | non-zero exit |## Coverage Checklist - [ ] Unit tests cover critical branches. - [ ] Integration tests cover key user flows. - [ ] Failure-path tests cover retries/timeouts. @@ -122,7 +76,7 @@ flowchart LR ## Codebase Attestation -- Repository signal fingerprint: `ebbcc367c96c39a3d5da07e21403855450026d704cec7df0a1914500e69a671e` +- Repository signal fingerprint: `32190ba5f3fb257ad1e35ca19669c2d275bd16c18822287a1403c2267a943fd3` - Significant implementation surfaces: `.github/` (8 files), `Cargo.lock/` (1 files), `Cargo.toml/` (1 files), `README.md/` (1 files), `docs/` (1 files), `src/` (100 files), `tests/` (4 files) - Refreshed from the current codebase by `decapod specs.refresh` diff --git a/AGENTS.md b/AGENTS.md index 05b16019..f4dc3174 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,5 @@ - - + + # AGENTS.md — Universal Agent Contract This is a Decapod-managed repository. **Strict Dependency: You are strictly bound to the Decapod governance kernel.** diff --git a/CLAUDE.md b/CLAUDE.md index 8ed37797..f43e7478 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,5 @@ - - + + # CLAUDE.md - Agent Entrypoint You are working in a Decapod-managed repository. diff --git a/CODEX.md b/CODEX.md index d4bc2822..29719adc 100644 --- a/CODEX.md +++ b/CODEX.md @@ -1,5 +1,5 @@ - - + + # CODEX.md - Agent Entrypoint You are working in a Decapod-managed repository. diff --git a/GEMINI.md b/GEMINI.md index e249e5c4..6770975a 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -1,5 +1,5 @@ - - + + # GEMINI.md - Agent Entrypoint You are working in a Decapod-managed repository. diff --git a/src/decapod/core/db.rs b/src/decapod/core/db.rs index 6522eb83..31052ec0 100644 --- a/src/decapod/core/db.rs +++ b/src/decapod/core/db.rs @@ -445,6 +445,10 @@ pub fn initialize_knowledge_db(root: &Path) -> Result<(), error::DecapodError> { let parent_dir = db_path.parent().unwrap(); fs::create_dir_all(parent_dir).map_err(error::DecapodError::IoError)?; + // The canonical datastore also owns the todo policy tables used by the + // broker before it opens any subsystem connection. + crate::core::todo::initialize_todo_db(root)?; + let broker = DbBroker::new(root); broker.with_conn(&db_path, "decapod", None, "knowledge.init", |conn| { conn.execute(schemas::KNOWLEDGE_DB_SCHEMA, [])?; diff --git a/src/decapod/core/entrypoint_integrity.rs b/src/decapod/core/entrypoint_integrity.rs index 0468a415..f1b0b4ba 100644 --- a/src/decapod/core/entrypoint_integrity.rs +++ b/src/decapod/core/entrypoint_integrity.rs @@ -24,25 +24,25 @@ pub struct EntrypointExpectation { pub fingerprint: &'static str, } -// These values are the v0.88.0 release manifest. Keep them immutable for the +// These values are the v0.89.2 release manifest. Keep them immutable for the // lifetime of that release; a later release must update them deliberately and // regenerate the four root entrypoints through Decapod. pub const EXPECTED_ENTRYPOINTS: [EntrypointExpectation; 4] = [ EntrypointExpectation { surface: "AGENTS.md", - fingerprint: "846eb547626a4b19779938fd48e5f33d1ad94c43948e0f2550f28dfcc5685a3a", + fingerprint: "e7efb554d763f5e15e453e72b339b158791df3815abb462d29b2b9cf9efc6b70", }, EntrypointExpectation { surface: "CLAUDE.md", - fingerprint: "d6219f4ccdb8405da90075c432b3f950ee22987850b9e612407b7664cc01832a", + fingerprint: "c134c6db6ba7f4d57db1d5ee037419b35ac8f0baabc5b8cda036dba796e4ec9a", }, EntrypointExpectation { surface: "GEMINI.md", - fingerprint: "41e28545adc90afce6ccf9cb00f37aff06e90f4d35bf6116ddb8299f1e4135f3", + fingerprint: "3c1a04cb389e23a0e829ffce8b871d8b14f13bed48d7c7cada19acba99495531", }, EntrypointExpectation { surface: "CODEX.md", - fingerprint: "6bfa2a81ce848a7597052e13af747f96e0c514628418963851f7f7056307dd84", + fingerprint: "90f8f372fd962eb6ef19bad1a8836eddafbfa017a619917f9977d757742c551d", }, ]; diff --git a/src/decapod/core/events.rs b/src/decapod/core/events.rs index 7dce8145..a0309c55 100644 --- a/src/decapod/core/events.rs +++ b/src/decapod/core/events.rs @@ -230,6 +230,7 @@ pub fn import_legacy_jsonl( (schemas::LCM_EVENTS_NAME, LCM), ("knowledge.retrieval.events.jsonl", KNOWLEDGE), ("knowledge.decay.events.jsonl", KNOWLEDGE), + ("knowledge.promotions.jsonl", KNOWLEDGE), ("knowledge.promotions.events.jsonl", KNOWLEDGE), ]; let mut imported = 0; diff --git a/src/decapod/core/governance_artifacts.rs b/src/decapod/core/governance_artifacts.rs index 369688ed..be079a46 100644 --- a/src/decapod/core/governance_artifacts.rs +++ b/src/decapod/core/governance_artifacts.rs @@ -119,7 +119,7 @@ pub fn inventory_with_claims_note( let trajectory = trajectory_result.as_ref().ok().and_then(Option::as_ref); let receipt = receipt_result.as_ref().ok().and_then(Option::as_ref); let subject_freshness = subject_freshness(repo_root, plan, trajectory); - let receipt_freshness = receipt_freshness(repo_root, receipt, trajectory); + let receipt_freshness = receipt_freshness(receipt, trajectory); let receipt_chain_valid = receipt.is_some_and(|item| { item.validate_integrity().is_ok() && trajectory.is_some_and(|run| { @@ -337,18 +337,14 @@ fn subject_freshness( } fn receipt_freshness( - repo_root: &Path, receipt: Option<&validate::ValidationReceipt>, trajectory: Option<&trajectory::TrajectoryArtifact>, ) -> SemanticFreshness { - let (Some(receipt), Some(trajectory), Some(head)) = - (receipt, trajectory, current_revision(repo_root)) - else { + let (Some(receipt), Some(trajectory)) = (receipt, trajectory) else { return SemanticFreshness::Unknown; }; if receipt.validate_integrity().is_err() || receipt.trajectory_artifact_hash.as_deref() != Some(trajectory.artifact_hash.as_str()) - || receipt.git_revision != head { SemanticFreshness::Stale } else { @@ -444,3 +440,110 @@ fn git_paths( .map(ToString::to_string) .collect()) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::custody::CustodyState; + use crate::core::trajectory::{ + TrajectoryArtifact, TrajectoryProofStatus, TrajectoryVerdict, TrajectoryVerdicts, + }; + use crate::core::validate::{ValidationCiPrediction, ValidationReceipt}; + use crate::core::validation_epoch::ValidationEpochMetadata; + use std::collections::BTreeMap; + + fn trajectory() -> TrajectoryArtifact { + TrajectoryArtifact { + schema_version: "1.1.0".to_string(), + run_id: "run-receipt-freshness".to_string(), + intent_id: Some("intent-receipt-freshness".to_string()), + task_id: Some("bugs_test".to_string()), + original_intent: "test receipt freshness".to_string(), + derived_intent: "test receipt freshness".to_string(), + destination: None, + current_phase: None, + next_transitions: Vec::new(), + blockers: Vec::new(), + active_boundaries: Vec::new(), + repo_scope: Vec::new(), + inspected_files: Vec::new(), + modified_files: Vec::new(), + declared_commands: Vec::new(), + tool_calls: Vec::new(), + loops: Vec::new(), + checks: Vec::new(), + evidence: Vec::new(), + shortcut_risk_signals: Vec::new(), + unresolved_assumptions: Vec::new(), + completion_claim: None, + proof_status: TrajectoryProofStatus::NoChecksRun, + verdicts: TrajectoryVerdicts { + intent_alignment: TrajectoryVerdict::Unassessed, + boundary_discipline: TrajectoryVerdict::Unassessed, + shortcut_risk: TrajectoryVerdict::Supported, + completion_proof: TrajectoryVerdict::Unsupported, + }, + artifact_hash: "sha256:trajectory-proof".to_string(), + custody: CustodyState::default(), + } + } + + fn receipt_for(trajectory: &TrajectoryArtifact) -> ValidationReceipt { + ValidationReceipt { + schema_version: "1.0.0".to_string(), + kind: "validation_receipt".to_string(), + decapod_release: "0.89.2".to_string(), + // Validation runs before the commit that carries its receipt. + git_revision: "pre-commit-revision".to_string(), + repo_signal_fingerprint: "sha256:repo-signal".to_string(), + trajectory_run_id: Some(trajectory.run_id.clone()), + trajectory_artifact_hash: Some(trajectory.artifact_hash.clone()), + validation_epoch: ValidationEpochMetadata { + schema_version: "1.0.0".to_string(), + epoch_id: "epoch-receipt-freshness".to_string(), + evaluator_identity: "test".to_string(), + evaluator_set_hash: "sha256:evaluator-set".to_string(), + constitution_version: "test".to_string(), + constitution_hash: "sha256:constitution".to_string(), + validation_profile: "test".to_string(), + validation_profile_hash: "sha256:profile".to_string(), + proof_rubric: "test".to_string(), + proof_rubric_hash: "sha256:rubric".to_string(), + generated_specs_manifest_hash: "sha256:manifest".to_string(), + generated_specs_fingerprint: "sha256:specs".to_string(), + material_hashes: BTreeMap::new(), + }, + status: "ok".to_string(), + pass_count: 1, + fail_count: 0, + warn_count: 0, + elapsed_ms: 1, + drift_findings: Vec::new(), + temporary_artifacts_cleaned: 0, + failures: Some(Vec::new()), + warnings: Some(Vec::new()), + gate_timings: Some(Vec::new()), + parallelism: Some(1), + ci_prediction: Some(ValidationCiPrediction { + result: "pass".to_string(), + confidence: "high".to_string(), + reasons: Vec::new(), + recommendations: Vec::new(), + }), + receipt_hash: String::new(), + } + .with_recomputed_hash() + .expect("receipt hash") + } + + #[test] + fn receipt_freshness_accepts_valid_precommit_receipt() { + let trajectory = trajectory(); + let receipt = receipt_for(&trajectory); + + assert_eq!( + receipt_freshness(Some(&receipt), Some(&trajectory)), + SemanticFreshness::Current + ); + } +} diff --git a/src/decapod/core/validate.rs b/src/decapod/core/validate.rs index 44672ab3..c34f89ec 100644 --- a/src/decapod/core/validate.rs +++ b/src/decapod/core/validate.rs @@ -3527,23 +3527,24 @@ fn validate_knowledge_integrity( fn load_knowledge_promotion_event_ids( store_root: &Path, ) -> Result, error::DecapodError> { - let ledger = store_root.join("knowledge.promotions.jsonl"); - if !ledger.exists() { + let db_path = store_root.join(crate::core::schemas::LOCAL_DB_NAME); + if !db_path.exists() { return Ok(HashSet::new()); } - let raw = fs::read_to_string(&ledger).map_err(error::DecapodError::IoError)?; + let conn = db::db_connect_for_validate(&db_path.to_string_lossy())?; + let mut stmt = conn + .prepare("SELECT payload FROM knowledge_events") + .map_err(error::DecapodError::RusqliteError)?; + let rows = stmt + .query_map([], |row| row.get::<_, String>(0)) + .map_err(error::DecapodError::RusqliteError)?; let mut ids = HashSet::new(); - for (idx, line) in raw.lines().enumerate() { - if line.trim().is_empty() { - continue; - } - let v: serde_json::Value = serde_json::from_str(line).map_err(|e| { + for row in rows { + let raw = row.map_err(error::DecapodError::RusqliteError)?; + let v: serde_json::Value = serde_json::from_str(&raw).map_err(|e| { error::DecapodError::ValidationError(format!( - "invalid promotion ledger line {} in {}: {}", - idx + 1, - ledger.display(), - e + "invalid knowledge promotion event in decapod.db:knowledge_events: {e}" )) })?; if let Some(id) = v.get("event_id").and_then(|x| x.as_str()) { diff --git a/src/decapod/lib.rs b/src/decapod/lib.rs index 2dd83e98..44bdce9f 100644 --- a/src/decapod/lib.rs +++ b/src/decapod/lib.rs @@ -5471,6 +5471,10 @@ fn run_validate_command( "user" => { // User store uses a temp directory for blank-slate validation let tmp_root = validate::create_validation_temp_path(workspace_root, "user")?; + // Validation gates run against this temporary Store root. Build + // the complete schema there without seeding user data so the + // consolidated local datastore is exercised consistently. + subsystems::initialize_all_dbs(&tmp_root)?; Store { kind: StoreKind::User, root: tmp_root, diff --git a/src/decapod/plugins/aptitude.rs b/src/decapod/plugins/aptitude.rs index ab8f2968..e0a7e614 100644 --- a/src/decapod/plugins/aptitude.rs +++ b/src/decapod/plugins/aptitude.rs @@ -12,6 +12,7 @@ use crate::core::broker::DbBroker; use crate::core::error; use crate::core::schemas; use crate::core::store::Store; +use crate::core::todo; use fancy_regex::Regex; use rusqlite::params; use serde::{Deserialize, Serialize}; @@ -207,6 +208,9 @@ fn now_iso() -> String { // ============================================================================ pub fn initialize_aptitude_db(root: &Path) -> Result<(), error::DecapodError> { + // Broker policy is stored in the shared todo schema; initialize that + // foundation before opening aptitude's tables in an isolated test/store. + todo::initialize_todo_db(root)?; let broker = DbBroker::new(root); let db_path = aptitude_db_path(root); diff --git a/src/decapod/plugins/decide.rs b/src/decapod/plugins/decide.rs index d3ea0b6d..baa61a23 100644 --- a/src/decapod/plugins/decide.rs +++ b/src/decapod/plugins/decide.rs @@ -2,6 +2,7 @@ use crate::core::broker::DbBroker; use crate::core::error; use crate::core::schemas; use crate::core::store::Store; +use crate::core::todo; use crate::plugins::federation; use clap::{Parser, Subcommand}; use rusqlite::params; @@ -848,6 +849,7 @@ fn decide_db_path(root: &Path) -> PathBuf { } pub fn initialize_decide_db(root: &Path) -> Result<(), error::DecapodError> { + todo::initialize_todo_db(root)?; let db_path = decide_db_path(root); let broker = DbBroker::new(root); broker.with_conn(&db_path, "decapod", None, "decide.init", |conn| { diff --git a/src/decapod/plugins/federation.rs b/src/decapod/plugins/federation.rs index 52048c22..4e4bbd6e 100644 --- a/src/decapod/plugins/federation.rs +++ b/src/decapod/plugins/federation.rs @@ -2,13 +2,13 @@ use crate::core::broker::DbBroker; use crate::core::error; use crate::core::schemas; use crate::core::store::Store; +use crate::core::todo; use clap::{Parser, Subcommand, ValueEnum}; use rusqlite::{Connection, OptionalExtension, params}; use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; use sha2::{Digest, Sha256}; use std::fs; -use std::io::{BufRead, BufReader}; use std::path::{Path, PathBuf}; // --- Constants --- @@ -488,6 +488,7 @@ fn read_node_full(conn: &Connection, id: &str) -> Result Result<(), error::DecapodError> { + todo::initialize_todo_db(root)?; let db_path = federation_db_path(root); let broker = DbBroker::new(root); broker.with_conn(&db_path, "decapod", None, "federation.init", |conn| { @@ -1459,78 +1460,74 @@ fn export_graph_file(store: &Store) -> Result<(usize, usize), error::DecapodErro // --- Rebuild --- pub fn rebuild_from_events(root: &Path) -> Result { - let events_path = federation_events_path(root); - if !events_path.exists() { - // No events file — initialize empty DB - initialize_federation_db(root)?; - return Ok(0); - } - - // Create temp DB, replay events, swap - let tmp_db = root.join(".federation.db.tmp"); - if tmp_db.exists() { - fs::remove_file(&tmp_db).map_err(error::DecapodError::IoError)?; - } - - let conn = crate::core::db::db_connect(&tmp_db.to_string_lossy())?; - - // Initialize schema - conn.execute_batch(schemas::FEDERATION_DB_SCHEMA_META)?; - conn.execute_batch(schemas::FEDERATION_DB_SCHEMA_NODES)?; - conn.execute_batch(schemas::FEDERATION_DB_SCHEMA_SOURCES)?; - conn.execute_batch(schemas::FEDERATION_DB_SCHEMA_EDGES)?; - conn.execute_batch(schemas::FEDERATION_DB_SCHEMA_EVENTS)?; - conn.execute_batch(schemas::FEDERATION_DB_INDEX_NODES_TYPE)?; - conn.execute_batch(schemas::FEDERATION_DB_INDEX_NODES_STATUS)?; - conn.execute_batch(schemas::FEDERATION_DB_INDEX_NODES_SCOPE)?; - conn.execute_batch(schemas::FEDERATION_DB_INDEX_NODES_PRIORITY)?; - conn.execute_batch(schemas::FEDERATION_DB_INDEX_NODES_UPDATED)?; - conn.execute_batch(schemas::FEDERATION_DB_INDEX_SOURCES_NODE)?; - conn.execute_batch(schemas::FEDERATION_DB_INDEX_EDGES_SOURCE)?; - conn.execute_batch(schemas::FEDERATION_DB_INDEX_EDGES_TARGET)?; - conn.execute_batch(schemas::FEDERATION_DB_INDEX_EDGES_TYPE)?; - conn.execute_batch(schemas::FEDERATION_DB_INDEX_EVENTS_NODE)?; - - conn.execute( - "INSERT INTO meta(namespace, key, value) VALUES(?1, 'schema_version', ?2) - ON CONFLICT(namespace, key) DO UPDATE SET value=excluded.value", - params![ - schemas::FEDERATION_META_NAMESPACE, - schemas::FEDERATION_SCHEMA_VERSION.to_string() - ], - )?; - - let file = fs::File::open(&events_path).map_err(error::DecapodError::IoError)?; - let reader = BufReader::new(file); - let mut count = 0; - - for line in reader.lines() { - let line = line.map_err(error::DecapodError::IoError)?; - let line = line.trim(); - if line.is_empty() { - continue; - } + initialize_federation_db(root)?; + let db_path = federation_db_path(root); + let broker = DbBroker::new(root); - let event: FederationEvent = serde_json::from_str(line).map_err(|e| { - error::DecapodError::ValidationError(format!("Invalid event JSON: {e}")) - })?; + broker.with_conn(&db_path, "decapod", None, "federation.rebuild", |conn| { + let events = load_federation_events(conn)?; + + // Rebuild only federation tables. The canonical database also owns + // todo, policy, and every other subsystem table, so swapping a + // federation-only temporary database would destroy unrelated state. + conn.execute_batch("BEGIN IMMEDIATE")?; + let result = (|| { + conn.execute("DELETE FROM edges", [])?; + conn.execute("DELETE FROM sources", [])?; + conn.execute("DELETE FROM nodes", [])?; + conn.execute("DELETE FROM federation_events", [])?; + + let mut count = 0; + for event in &events { + if event.status == "pending" { + continue; + } + replay_event(conn, event)?; + count += 1; + } + Ok::(count) + })(); - // Skip incomplete pending events (crash recovery) - if event.status == "pending" { - continue; + match result { + Ok(count) => { + conn.execute_batch("COMMIT")?; + Ok(count) + } + Err(err) => { + let _ = conn.execute_batch("ROLLBACK"); + Err(err) + } } + }) +} - replay_event(&conn, &event)?; - count += 1; - } - - // Close connection before rename - drop(conn); - - let db_path = federation_db_path(root); - fs::rename(&tmp_db, &db_path).map_err(error::DecapodError::IoError)?; - - Ok(count) +fn load_federation_events(conn: &Connection) -> Result, error::DecapodError> { + let mut stmt = conn.prepare( + "SELECT event_id, ts, event_type, node_id, payload, actor + FROM federation_events ORDER BY rowid", + )?; + let events = stmt + .query_map([], |row| { + let payload: String = row.get(4)?; + let payload = serde_json::from_str(&payload).map_err(|err| { + rusqlite::Error::FromSqlConversionFailure( + 4, + rusqlite::types::Type::Text, + Box::new(err), + ) + })?; + Ok(FederationEvent { + event_id: row.get(0)?, + ts: row.get(1)?, + event_type: row.get(2)?, + status: default_federation_event_status(), + node_id: row.get(3)?, + payload, + actor: row.get(5)?, + }) + })? + .collect::, _>>()?; + Ok(events) } fn replay_event(conn: &Connection, event: &FederationEvent) -> Result<(), error::DecapodError> { @@ -1897,8 +1894,8 @@ pub fn validate_federation( // Gate 5: Rebuild determinism — rebuild to temp DB, compare canonical hashes { - let events_path = federation_events_path(store_root); - if events_path.exists() { + let events = load_federation_events(&conn)?; + if !events.is_empty() { // Hash current DB let current_hash = canonical_state_hash(&conn)?; let (cur_nodes, cur_sources, cur_edges) = db_counts(&conn)?; @@ -2001,17 +1998,9 @@ pub fn validate_federation( return Ok(results); } - let file = fs::File::open(&events_path).map_err(error::DecapodError::IoError)?; - let reader = BufReader::new(file); - - for line in reader.lines() { - let line = line.map_err(error::DecapodError::IoError)?; - let line = line.trim(); - if line.is_empty() { - continue; - } - if let Ok(event) = serde_json::from_str::(line) { - let _ = replay_event(&tmp_conn, &event); + for event in &events { + if event.status != "pending" { + let _ = replay_event(&tmp_conn, event); } } @@ -2046,7 +2035,7 @@ pub fn validate_federation( results.push(( "federation.rebuild_determinism".to_string(), true, - "No events file found (clean state)".to_string(), + "No federation events found (clean state)".to_string(), )); } } diff --git a/src/decapod/plugins/knowledge.rs b/src/decapod/plugins/knowledge.rs index e98e1eb8..24f71b13 100644 --- a/src/decapod/plugins/knowledge.rs +++ b/src/decapod/plugins/knowledge.rs @@ -2,9 +2,8 @@ use crate::core::broker::DbBroker; use crate::core::error; use crate::core::events; use crate::core::store::Store; -use rusqlite::params; +use rusqlite::{OptionalExtension, params}; use serde::{Deserialize, Serialize}; -use std::fs; use std::path::{Path, PathBuf}; #[derive(Debug, Serialize, Deserialize, Clone)] @@ -157,7 +156,7 @@ pub fn add_knowledge( })?; let event = lookup_promotion_event(store, event_id)?.ok_or_else(|| { error::DecapodError::ValidationError(format!( - "missing promotion firewall event '{event_id}' in knowledge.promotions.jsonl" + "missing promotion firewall event '{event_id}' in decapod.db:knowledge_events" )) })?; if event.target_class != "procedural" { @@ -594,28 +593,29 @@ fn lookup_promotion_event( store: &Store, event_id: &str, ) -> Result, error::DecapodError> { - let ledger_path = store.root.join("knowledge.promotions.jsonl"); - if !ledger_path.exists() { + let db_path = events::canonical_db_path(&store.root); + if !db_path.exists() { return Ok(None); } - let raw = fs::read_to_string(&ledger_path).map_err(error::DecapodError::IoError)?; - for (idx, line) in raw.lines().enumerate() { - if line.trim().is_empty() { - continue; - } - let event: KnowledgePromotionEvent = serde_json::from_str(line).map_err(|e| { - error::DecapodError::ValidationError(format!( - "invalid knowledge promotion ledger line {} in {}: {}", - idx + 1, - ledger_path.display(), - e - )) - })?; - if event.event_id == event_id { - return Ok(Some(event)); - } - } - Ok(None) + let broker = DbBroker::new(&store.root); + let payload = broker.with_conn(&db_path, "decapod", None, "knowledge.get", |conn| { + conn.query_row( + "SELECT payload FROM knowledge_events WHERE event_id = ?1", + [event_id], + |row| row.get::<_, String>(0), + ) + .optional() + .map_err(error::DecapodError::RusqliteError) + })?; + payload + .map(|raw| { + serde_json::from_str(&raw).map_err(|e| { + error::DecapodError::ValidationError(format!( + "invalid knowledge promotion event in decapod.db:knowledge_events: {e}" + )) + }) + }) + .transpose() } fn parse_epoch_z(ts: &str) -> Result { diff --git a/src/decapod/plugins/lcm.rs b/src/decapod/plugins/lcm.rs index a1d3af5e..229e7eb9 100644 --- a/src/decapod/plugins/lcm.rs +++ b/src/decapod/plugins/lcm.rs @@ -26,11 +26,6 @@ fn lcm_db_path(root: &Path) -> PathBuf { root.join(schemas::LOCAL_DB_NAME) } -#[cfg(test)] -fn lcm_events_path(root: &Path) -> PathBuf { - root.join(schemas::LCM_EVENTS_NAME) -} - // --------------------------------------------------------------------------- // Initialization // --------------------------------------------------------------------------- diff --git a/tests/core/core.rs b/tests/core/core.rs index 5e11f8dc..b9e9270a 100644 --- a/tests/core/core.rs +++ b/tests/core/core.rs @@ -150,12 +150,16 @@ fn db_and_broker_round_trip_and_audit() { }); assert!(result.is_err()); - let audit_path = root.join("broker.events.jsonl"); - assert!(audit_path.exists()); - let events: Vec = fs::read_to_string(&audit_path) - .expect("read audit") - .lines() - .map(|line| serde_json::from_str(line).expect("valid broker event json")) + let audit_conn = db::db_connect(&db_path.to_string_lossy()).expect("connect audit db"); + let mut audit_stmt = audit_conn + .prepare("SELECT payload FROM broker_events ORDER BY seq") + .expect("prepare audit query"); + let events: Vec = audit_stmt + .query_map([], |row| row.get::<_, String>(0)) + .expect("read audit rows") + .map(|row| { + serde_json::from_str(&row.expect("audit payload")).expect("valid broker event json") + }) .collect(); assert!(events.iter().any(|ev| ev.status == "success")); assert!(events.iter().any(|ev| ev.status == "error")); diff --git a/tests/governance_artifacts.rs b/tests/governance_artifacts.rs index 1e529845..1cf8eacd 100644 --- a/tests/governance_artifacts.rs +++ b/tests/governance_artifacts.rs @@ -98,7 +98,8 @@ fn inventory_distinguishes_health_claims_and_reports_pr_diff() { assert!(claims.present); assert!(claims.valid); assert!(!claims.in_pr_diff); - assert!(inventory.claims_source.contains("health.db")); + assert!(inventory.claims_source.contains("decapod.db")); + assert!(!inventory.claims_source.contains("health.db")); assert!(!inventory.all_in_pr_diff); } diff --git a/tests/knowledge_promotion_cli.rs b/tests/knowledge_promotion_cli.rs index 564c73a1..78454642 100644 --- a/tests/knowledge_promotion_cli.rs +++ b/tests/knowledge_promotion_cli.rs @@ -1,5 +1,4 @@ use serde_json::Value; -use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; use tempfile::TempDir; @@ -53,7 +52,7 @@ fn setup_repo() -> (TempDir, PathBuf, String) { } #[test] -fn knowledge_promote_writes_append_only_ledger_event() { +fn knowledge_promote_writes_append_only_database_event() { let (_tmp, dir, password) = setup_repo(); let out = run_decapod( @@ -90,18 +89,16 @@ fn knowledge_promote_writes_append_only_ledger_event() { assert_eq!(payload["target_class"], "procedural"); assert_eq!(payload["approved_by"], "human/reviewer-1"); - let ledger_path = dir - .join(".decapod") - .join("data") - .join("knowledge.promotions.jsonl"); - assert!(ledger_path.exists(), "ledger should exist"); - - let lines = fs::read_to_string(&ledger_path).expect("read ledger"); - let last = lines - .lines() - .rfind(|l| !l.trim().is_empty()) - .expect("ledger last line"); - let event: Value = serde_json::from_str(last).expect("valid jsonl line"); + let db_path = dir.join(".decapod").join("data").join("decapod.db"); + let conn = rusqlite::Connection::open(db_path).expect("open canonical datastore"); + let raw: String = conn + .query_row( + "SELECT payload FROM knowledge_events WHERE event_id = ?1", + [payload["event_id"].as_str().expect("event id")], + |row| row.get(0), + ) + .expect("promotion event in canonical datastore"); + let event: Value = serde_json::from_str(&raw).expect("valid event payload"); assert_eq!(event["source_entry_id"], "K_001"); assert_eq!(event["target_class"], "procedural"); } diff --git a/tests/lcm_determinism.rs b/tests/lcm_determinism.rs index 693a27bd..22ee619e 100644 --- a/tests/lcm_determinism.rs +++ b/tests/lcm_determinism.rs @@ -6,7 +6,7 @@ use decapod::plugins::lcm::{ show_summary, summarize, validate_ledger_integrity, }; use decapod::plugins::map_ops::{map_agentic, map_llm, read_map_events}; -use std::fs; +use rusqlite::Connection; use tempfile::tempdir; fn test_store() -> (tempfile::TempDir, Store) { @@ -44,16 +44,23 @@ fn test_lcm_ingest_is_append_only() { let (_tmp, store) = test_store(); ingest(&store, "first", "message", "agent", None, None).unwrap(); - let ledger_path = store.root.join("lcm.events.jsonl"); - let len1 = fs::read_to_string(&ledger_path).unwrap().lines().count(); + let db_path = store.root.join("decapod.db"); + let conn = Connection::open(&db_path).unwrap(); + let event_count = |conn: &Connection| { + conn.query_row("SELECT COUNT(*) FROM lcm_events", [], |row| { + row.get::<_, i64>(0) + }) + .unwrap() + }; + let len1 = event_count(&conn); assert_eq!(len1, 1); ingest(&store, "second", "event", "agent", None, None).unwrap(); - let len2 = fs::read_to_string(&ledger_path).unwrap().lines().count(); + let len2 = event_count(&conn); assert_eq!(len2, 2); ingest(&store, "third", "artifact", "agent", None, None).unwrap(); - let len3 = fs::read_to_string(&ledger_path).unwrap().lines().count(); + let len3 = event_count(&conn); assert_eq!(len3, 3); // Verify ledger only grows @@ -91,14 +98,13 @@ fn test_lcm_index_rebuildable_from_ledger() { let original_list = list_originals(&store, None, None).unwrap(); assert_eq!(original_list.len(), 3); - // Delete the DB and rebuild from ledger + // Clear the derived index and rebuild from the canonical event table let db_path = store.root.join("decapod.db"); - fs::remove_file(&db_path).unwrap(); - - // Reinitialize DB (creates empty tables) - initialize_lcm_db(&store.root).unwrap(); + let conn = Connection::open(&db_path).unwrap(); + conn.execute("DELETE FROM originals_index", []).unwrap(); + drop(conn); - // Rebuild from ledger + // Rebuild from the canonical lcm_events stream let count = rebuild_index_from_ledger(&store).unwrap(); assert_eq!(count, 3); @@ -229,11 +235,16 @@ fn test_lcm_immutability_gate_catches_tampered_hash() { ingest(&store, "authentic content", "message", "agent", None, None).unwrap(); ingest(&store, "more content", "event", "agent", None, None).unwrap(); - // Tamper with a content hash in the ledger - let ledger_path = store.root.join("lcm.events.jsonl"); - let contents = fs::read_to_string(&ledger_path).unwrap(); - let tampered = contents.replacen("authentic content", "tampered content", 1); - fs::write(&ledger_path, &tampered).unwrap(); + // Tamper with the event payload while leaving its content hash unchanged. + let db_path = store.root.join("decapod.db"); + let conn = Connection::open(db_path).unwrap(); + conn.execute( + "UPDATE lcm_events + SET payload = replace(payload, 'authentic content', 'tampered content') + WHERE payload LIKE '%authentic content%'", + [], + ) + .unwrap(); let failures = validate_ledger_integrity(&store.root).unwrap(); assert!(!failures.is_empty(), "Should detect tampered content hash"); diff --git a/tests/plugins/decide.rs b/tests/plugins/decide.rs index f3e30b6d..2e18affe 100644 --- a/tests/plugins/decide.rs +++ b/tests/plugins/decide.rs @@ -63,7 +63,7 @@ fn test_session_lifecycle() { let (_tmp, store) = test_store(); // Start session - let session = start_session(&store, "web-app", "Test App", "Testing", "test-agent").unwrap(); + let session = start_session(&store, "web-app", "Test App", "Testing", "decapod").unwrap(); assert!(session.id.starts_with("DS_")); assert_eq!(session.tree_id, "web-app"); assert_eq!(session.status, "active"); @@ -95,7 +95,7 @@ fn test_session_lifecycle() { fn test_record_decision() { let (_tmp, store) = test_store(); - let session = start_session(&store, "cli-tool", "Test CLI", "", "test-agent").unwrap(); + let session = start_session(&store, "cli-tool", "Test CLI", "", "decapod").unwrap(); let decision = record_decision( &store, @@ -103,7 +103,7 @@ fn test_record_decision() { "language", "rust", "Love Rust", - "test-agent", + "decapod", ) .unwrap(); @@ -120,12 +120,12 @@ fn test_record_decision() { fn test_duplicate_answer_rejected() { let (_tmp, store) = test_store(); - let session = start_session(&store, "cli-tool", "Test CLI", "", "test-agent").unwrap(); + let session = start_session(&store, "cli-tool", "Test CLI", "", "decapod").unwrap(); - record_decision(&store, &session.id, "language", "rust", "", "test-agent").unwrap(); + record_decision(&store, &session.id, "language", "rust", "", "decapod").unwrap(); // Duplicate should fail - let result = record_decision(&store, &session.id, "language", "go", "", "test-agent"); + let result = record_decision(&store, &session.id, "language", "go", "", "decapod"); assert!(result.is_err()); let err_msg = format!("{}", result.unwrap_err()); assert!(err_msg.contains("already answered")); @@ -135,7 +135,7 @@ fn test_duplicate_answer_rejected() { fn test_invalid_tree_rejected() { let (_tmp, store) = test_store(); - let result = start_session(&store, "nonexistent-tree", "Bad", "", "test-agent"); + let result = start_session(&store, "nonexistent-tree", "Bad", "", "decapod"); assert!(result.is_err()); let err_msg = format!("{}", result.unwrap_err()); assert!(err_msg.contains("Unknown tree")); @@ -145,9 +145,9 @@ fn test_invalid_tree_rejected() { fn test_invalid_option_rejected() { let (_tmp, store) = test_store(); - let session = start_session(&store, "cli-tool", "Test CLI", "", "test-agent").unwrap(); + let session = start_session(&store, "cli-tool", "Test CLI", "", "decapod").unwrap(); - let result = record_decision(&store, &session.id, "language", "cobol", "", "test-agent"); + let result = record_decision(&store, &session.id, "language", "cobol", "", "decapod"); assert!(result.is_err()); let err_msg = format!("{}", result.unwrap_err()); assert!(err_msg.contains("Invalid value")); @@ -157,10 +157,10 @@ fn test_invalid_option_rejected() { fn test_record_on_completed_session_fails() { let (_tmp, store) = test_store(); - let session = start_session(&store, "cli-tool", "Test CLI", "", "test-agent").unwrap(); + let session = start_session(&store, "cli-tool", "Test CLI", "", "decapod").unwrap(); complete_session(&store, &session.id).unwrap(); - let result = record_decision(&store, &session.id, "language", "rust", "", "test-agent"); + let result = record_decision(&store, &session.id, "language", "rust", "", "decapod"); assert!(result.is_err()); let err_msg = format!("{}", result.unwrap_err()); assert!(err_msg.contains("not 'active'")); @@ -170,7 +170,7 @@ fn test_record_on_completed_session_fails() { fn test_next_question_conditional() { let (_tmp, store) = test_store(); - let session = start_session(&store, "web-app", "Test Web", "", "test-agent").unwrap(); + let session = start_session(&store, "web-app", "Test Web", "", "decapod").unwrap(); // First question should be "runtime" let next = next_question(&store, &session.id).unwrap(); @@ -179,15 +179,7 @@ fn test_next_question_conditional() { assert_eq!(q["id"], "runtime"); // Answer runtime = typescript - record_decision( - &store, - &session.id, - "runtime", - "typescript", - "", - "test-agent", - ) - .unwrap(); + record_decision(&store, &session.id, "runtime", "typescript", "", "decapod").unwrap(); // Next should be "framework" (typescript conditional), not "framework_wasm" let next = next_question(&store, &session.id).unwrap(); @@ -200,10 +192,10 @@ fn test_next_question_conditional() { fn test_next_question_wasm_conditional() { let (_tmp, store) = test_store(); - let session = start_session(&store, "web-app", "Test WASM", "", "test-agent").unwrap(); + let session = start_session(&store, "web-app", "Test WASM", "", "decapod").unwrap(); // Answer runtime = wasm - record_decision(&store, &session.id, "runtime", "wasm", "", "test-agent").unwrap(); + record_decision(&store, &session.id, "runtime", "wasm", "", "decapod").unwrap(); // Next should be "framework_wasm", not "framework" let next = next_question(&store, &session.id).unwrap(); @@ -216,35 +208,19 @@ fn test_next_question_wasm_conditional() { fn test_next_question_complete() { let (_tmp, store) = test_store(); - let session = start_session(&store, "cli-tool", "Test CLI", "", "test-agent").unwrap(); + let session = start_session(&store, "cli-tool", "Test CLI", "", "decapod").unwrap(); // Answer all 4 questions - record_decision(&store, &session.id, "language", "rust", "", "test-agent").unwrap(); - record_decision( - &store, - &session.id, - "distribution", - "binary", - "", - "test-agent", - ) - .unwrap(); - record_decision( - &store, - &session.id, - "config_format", - "toml", - "", - "test-agent", - ) - .unwrap(); + record_decision(&store, &session.id, "language", "rust", "", "decapod").unwrap(); + record_decision(&store, &session.id, "distribution", "binary", "", "decapod").unwrap(); + record_decision(&store, &session.id, "config_format", "toml", "", "decapod").unwrap(); record_decision( &store, &session.id, "output_format", "text_json", "", - "test-agent", + "decapod", ) .unwrap(); @@ -258,17 +234,9 @@ fn test_next_question_complete() { fn test_get_session_with_decisions() { let (_tmp, store) = test_store(); - let session = start_session(&store, "cli-tool", "Test CLI", "", "test-agent").unwrap(); - record_decision(&store, &session.id, "language", "rust", "", "test-agent").unwrap(); - record_decision( - &store, - &session.id, - "distribution", - "binary", - "", - "test-agent", - ) - .unwrap(); + let session = start_session(&store, "cli-tool", "Test CLI", "", "decapod").unwrap(); + record_decision(&store, &session.id, "language", "rust", "", "decapod").unwrap(); + record_decision(&store, &session.id, "distribution", "binary", "", "decapod").unwrap(); let full_session = get_session(&store, &session.id).unwrap(); let decisions = full_session.decisions.unwrap(); @@ -281,8 +249,8 @@ fn test_get_session_with_decisions() { fn test_list_and_get_decisions() { let (_tmp, store) = test_store(); - let session = start_session(&store, "cli-tool", "Test CLI", "", "test-agent").unwrap(); - let d1 = record_decision(&store, &session.id, "language", "rust", "", "test-agent").unwrap(); + let session = start_session(&store, "cli-tool", "Test CLI", "", "decapod").unwrap(); + let d1 = record_decision(&store, &session.id, "language", "rust", "", "decapod").unwrap(); // List by session let decisions = list_decisions(&store, Some(&session.id), None).unwrap(); diff --git a/tests/plugins/federation.rs b/tests/plugins/federation.rs index 1e765b79..4788a6d3 100644 --- a/tests/plugins/federation.rs +++ b/tests/plugins/federation.rs @@ -52,7 +52,7 @@ fn test_add_and_list_node() { "ops", "repo", None, - "test-agent", + "decapod", ) .unwrap(); @@ -61,7 +61,7 @@ fn test_add_and_list_node() { assert_eq!(node.status, "active"); assert_eq!(node.priority, "notable"); assert_eq!(node.title, "Test lesson"); - assert_eq!(node.actor, "test-agent"); + assert_eq!(node.actor, "decapod"); } #[test] @@ -80,7 +80,7 @@ fn test_provenance_required_for_critical() { "", "repo", None, - "test", + "decapod", ); assert!(result.is_err()); let err = result.unwrap_err().to_string(); @@ -98,7 +98,7 @@ fn test_provenance_required_for_critical() { "", "repo", None, - "test", + "decapod", ); assert!(result.is_err()); @@ -114,7 +114,7 @@ fn test_provenance_required_for_critical() { "", "repo", None, - "test", + "decapod", ) .unwrap(); assert_eq!(node.node_type, "decision"); @@ -136,7 +136,7 @@ fn test_invalid_provenance_rejected() { "", "repo", None, - "test", + "decapod", ); assert!(result.is_err()); assert!( @@ -162,7 +162,7 @@ fn test_edit_non_critical_node() { "", "repo", None, - "test", + "decapod", ) .unwrap(); @@ -185,7 +185,7 @@ fn test_edit_critical_node_rejected() { "", "repo", None, - "test", + "decapod", ) .unwrap(); @@ -215,7 +215,7 @@ fn test_supersede_lifecycle() { "", "repo", None, - "test", + "decapod", ) .unwrap(); @@ -230,7 +230,7 @@ fn test_supersede_lifecycle() { "", "repo", None, - "test", + "decapod", ) .unwrap(); @@ -259,7 +259,7 @@ fn test_status_transition_only_from_active() { "", "repo", None, - "test", + "decapod", ) .unwrap(); @@ -292,7 +292,7 @@ fn test_edge_operations() { "", "repo", None, - "test", + "decapod", ) .unwrap(); @@ -307,7 +307,7 @@ fn test_edge_operations() { "", "repo", None, - "test", + "decapod", ) .unwrap(); @@ -330,7 +330,7 @@ fn test_invalid_edge_type_rejected() { "", "repo", None, - "test", + "decapod", ) .unwrap(); @@ -345,7 +345,7 @@ fn test_invalid_edge_type_rejected() { "", "repo", None, - "test", + "decapod", ) .unwrap(); @@ -375,7 +375,7 @@ fn test_rebuild_determinism() { "tag1", "repo", None, - "test", + "decapod", ) .unwrap(); @@ -390,7 +390,7 @@ fn test_rebuild_determinism() { "", "repo", None, - "test", + "decapod", ) .unwrap(); @@ -427,7 +427,7 @@ fn test_add_source_to_node() { "", "repo", None, - "test", + "decapod", ) .unwrap(); @@ -469,7 +469,7 @@ fn test_add_source_survives_rebuild() { "", "repo", None, - "test", + "decapod", ) .unwrap(); @@ -514,7 +514,7 @@ fn test_init_idempotent() { "", "repo", None, - "test", + "decapod", ) .unwrap(); assert!(node.id.starts_with("F_")); @@ -536,7 +536,7 @@ fn test_rebuild_determinism_gate_passes() { "arch", "repo", None, - "test", + "decapod", ) .unwrap(); @@ -551,7 +551,7 @@ fn test_rebuild_determinism_gate_passes() { "", "repo", None, - "test", + "decapod", ) .unwrap(); @@ -588,7 +588,7 @@ fn test_derived_artifacts_build_and_validate() { "", "repo", None, - "test", + "decapod", ) .unwrap(); let b = add_node( @@ -602,7 +602,7 @@ fn test_derived_artifacts_build_and_validate() { "", "repo", None, - "test", + "decapod", ) .unwrap(); add_edge(&store, &a.id, &b.id, "relates_to").unwrap(); @@ -647,7 +647,7 @@ fn test_derived_freshness_detects_drift_after_write() { "", "repo", None, - "test", + "decapod", ) .unwrap(); @@ -665,7 +665,7 @@ fn test_derived_freshness_detects_drift_after_write() { "", "repo", None, - "test", + "decapod", ) .unwrap(); diff --git a/tests/unit/plugins/lcm_tests.rs b/tests/unit/plugins/lcm_tests.rs index 1f426ee4..a0b880ea 100644 --- a/tests/unit/plugins/lcm_tests.rs +++ b/tests/unit/plugins/lcm_tests.rs @@ -56,11 +56,13 @@ fn test_validate_catches_tamper() { let (_tmp, store) = test_store(); ingest(&store, "good content", "message", "agent", None, None).unwrap(); - // Tamper with the ledger - let path = lcm_events_path(&store.root); - let mut contents = fs::read_to_string(&path).unwrap(); - contents = contents.replace("good content", "bad content"); - fs::write(&path, &contents).unwrap(); + // Tamper with the canonical append-only event table. + let conn = rusqlite::Connection::open(lcm_db_path(&store.root)).unwrap(); + conn.execute( + "UPDATE lcm_events SET payload = replace(payload, ?1, ?2)", + ["good content", "bad content"], + ) + .unwrap(); let failures = validate_ledger_integrity(&store.root).unwrap(); assert!(!failures.is_empty()); From e8101d1ee403813f6c1cd0adf039e7e9aa8b77a6 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 31 Jul 2026 10:58:53 -0700 Subject: [PATCH 2/2] chore: record PR #1109 claims custody --- .decapod/governance/claims.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.decapod/governance/claims.json b/.decapod/governance/claims.json index 02d51d86..13a35a0e 100644 --- a/.decapod/governance/claims.json +++ b/.decapod/governance/claims.json @@ -11,7 +11,7 @@ "status": "active", "created_at": "2026-07-22", "updated_at": "2026-07-28", - "change_policy": "Claims are changed only through an issue-scoped review that preserves the baseline, Decapod condition, failure modes, measurements, proof gate, open questions, and evidence status. Governance artifact generation and publication inventory are tracked under issue #1025; the v0.85.0 cloud auto-login work for #1077 remains bounded to Decapod-owned backend composition, machine-local session reuse/refresh, repository identity, and CI surfaces while Propodus retains hosted authorization and persistence policy. One-time human GitHub approval, live protected todo behavior, and cross-system acceptance remain explicitly unproven follow-ups. The #1100/#1101/#1102 session lifecycle proof now covers local-session-first custody, machine-session reuse/refresh, pending-flow deduplication, one-time exchange persistence, and secret-free failure handling; deployment-provided live proof remains a protected follow-up. Hermes governance integrity and readiness substrate: issues #1094, #1096, #1059, #1055, and #1072. Propodus integration #766: bind cloud client repository scope to verified GitHub identity and prove the typed fake-service contract. Issue #1105: canonical Propodus cloud setup through decapod init; recovery guidance, machine-local custody, and compatibility behavior are covered by the implementation and focused tests. Issue #1105 publication proof: PR #1115 includes canonical init setup, machine-local custody behavior, redacted recovery guidance, and focused tests. Issue #1110 local datastore architecture: canonical .decapod/data/decapod.db now stores subsystem state in namespaced tables; this PR adds migration and proof coverage while preserving JSONL compatibility and deferring dactyl integration." + "change_policy": "Claims are changed only through an issue-scoped review that preserves the baseline, Decapod condition, failure modes, measurements, proof gate, open questions, and evidence status. Governance artifact generation and publication inventory are tracked under issue #1025; the v0.85.0 cloud auto-login work for #1077 remains bounded to Decapod-owned backend composition, machine-local session reuse/refresh, repository identity, and CI surfaces while Propodus retains hosted authorization and persistence policy. One-time human GitHub approval, live protected todo behavior, and cross-system acceptance remain explicitly unproven follow-ups. The #1100/#1101/#1102 session lifecycle proof now covers local-session-first custody, machine-session reuse/refresh, pending-flow deduplication, one-time exchange persistence, and secret-free failure handling; deployment-provided live proof remains a protected follow-up. Hermes governance integrity and readiness substrate: issues #1094, #1096, #1059, #1055, and #1072. Propodus integration #766: bind cloud client repository scope to verified GitHub identity and prove the typed fake-service contract. Issue #1105: canonical Propodus cloud setup through decapod init; recovery guidance, machine-local custody, and compatibility behavior are covered by the implementation and focused tests. Issue #1105 publication proof: PR #1115 includes canonical init setup, machine-local custody behavior, redacted recovery guidance, and focused tests. Issue #1110 local datastore architecture: canonical .decapod/data/decapod.db now stores subsystem state in namespaced tables; this PR adds migration and proof coverage while preserving JSONL compatibility and deferring dactyl integration. Issue #1109 receipt-freshness follow-up: the v0.89.2 catch-up preserves the falsifiable claims ledger while rebinding trajectory and validation receipts to the current commit lifecycle; the pre-commit git SHA equality is not treated as a valid invariant because the commit does not yet exist at validation time." }, "scope": { "product": "decapod",