diff --git a/src/interface/ffi/src/main.zig b/src/interface/ffi/src/main.zig index 2c6ef0a..649c7b4 100644 --- a/src/interface/ffi/src/main.zig +++ b/src/interface/ffi/src/main.zig @@ -246,10 +246,11 @@ export fn verisimiser_enable_dimension( return .invalid_param; }; - // TODO: actually enable the dimension in the overlay index - - clearError(); - return .ok; + // The overlay index that persists per-entity dimension state is not yet + // wired into the FFI. Fail loudly rather than report a dimension as enabled + // when it is not (soundness: no silent success). + setError("octad dimension overlay not yet wired into the FFI"); + return .sidecar_unavailable; } /// Get the active dimension bitmask for an entity. @@ -296,10 +297,11 @@ export fn verisimiser_record_provenance( return .invalid_param; }; - // TODO: compute SHA-256 hash, chain from previous, write to sidecar - - clearError(); - return .ok; + // The SHA-256 hash-chain sidecar append is not yet wired into the FFI. + // Fail loudly rather than report a provenance event as recorded when no + // entry was written (soundness: no phantom audit trail). + setError("provenance sidecar not yet wired into the FFI"); + return .sidecar_unavailable; } /// Verify the integrity of an entity's provenance hash chain. @@ -319,11 +321,12 @@ export fn verisimiser_verify_provenance( _ = entity_id; - // TODO: walk the hash chain, verify each link - // Return .chain_corrupted if any link fails verification - - clearError(); - return .ok; + // The hash-chain store is not yet wired into the FFI, so integrity cannot + // be confirmed. Return an error rather than .ok — reporting "verified" for + // an unchecked (possibly tampered) chain would be the worst kind of + // soundness hole. + setError("provenance sidecar not yet wired into the FFI; cannot verify integrity"); + return .sidecar_unavailable; } /// Get the length of an entity's provenance chain. @@ -364,10 +367,11 @@ export fn verisimiser_record_version( _ = snapshot_ptr; _ = snapshot_len; - // TODO: store snapshot in temporal sidecar with current timestamp - - clearError(); - return .ok; + // The temporal sidecar that stores version snapshots is not yet wired into + // the FFI. Fail loudly rather than report a version as recorded when no + // snapshot was stored (soundness: no phantom history). + setError("temporal sidecar not yet wired into the FFI"); + return .sidecar_unavailable; } /// Query entity state at a specific point in time. @@ -614,3 +618,46 @@ test "enable dimension with invalid dimension" { const result = verisimiser_enable_dimension(handle, 42, 99); try std.testing.expectEqual(Result.invalid_param, result); } + +// Soundness: the persistence-backed octad operations are not yet wired into +// the FFI, so they must fail loudly rather than report a false success. + +test "verify provenance does not falsely report verified" { + const handle = verisimiser_init() orelse return error.InitFailed; + defer verisimiser_free(handle); + + // A valid handle + entity must NOT return .ok while verification is unwired: + // claiming a chain is verified without checking it would be unsound. + const result = verisimiser_verify_provenance(handle, 42); + try std.testing.expect(result != Result.ok); + try std.testing.expectEqual(Result.sidecar_unavailable, result); +} + +test "record provenance does not falsely report recorded" { + const handle = verisimiser_init() orelse return error.InitFailed; + defer verisimiser_free(handle); + + const result = verisimiser_record_provenance(handle, 42, 0, 0); + try std.testing.expect(result != Result.ok); + try std.testing.expectEqual(Result.sidecar_unavailable, result); +} + +test "record version does not falsely report stored" { + const handle = verisimiser_init() orelse return error.InitFailed; + defer verisimiser_free(handle); + + const result = verisimiser_record_version(handle, 42, 0, 0); + try std.testing.expect(result != Result.ok); + try std.testing.expectEqual(Result.sidecar_unavailable, result); +} + +test "enable dimension does not falsely report enabled" { + const handle = verisimiser_init() orelse return error.InitFailed; + defer verisimiser_free(handle); + + // Dimension 2 (provenance) is a valid enum value; the call must still fail + // loudly because the overlay index is not wired in. + const result = verisimiser_enable_dimension(handle, 42, 2); + try std.testing.expect(result != Result.ok); + try std.testing.expectEqual(Result.sidecar_unavailable, result); +} diff --git a/src/manifest/mod.rs b/src/manifest/mod.rs index 750d4f9..8ba2408 100644 --- a/src/manifest/mod.rs +++ b/src/manifest/mod.rs @@ -372,6 +372,43 @@ mod validate_manifest_tests { assert!(report.failed_count() == 0); } + /// A manifest that sets both `[database].backend` and the legacy + /// `target-db` to conflicting values must fail validation up front, not + /// silently pass and blow up later at generate time (V-L2-E1). + #[test] + fn conflicting_backend_fails_validation() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join("verisimiser.toml"); + let sidecar_path = dir.path().join("sidecar.db"); + let body = format!( + "[project]\n\ + name = \"test\"\n\ + [database]\n\ + backend = \"sqlite\"\n\ + target-db = \"postgresql\"\n\ + [sidecar]\n\ + storage = \"sqlite\"\n\ + path = \"{}\"\n", + sidecar_path.display().to_string().replace('\\', "/") + ); + std::fs::write(&path, body).expect("write"); + + let report = validate_manifest(path.to_str().unwrap()); + assert!( + !report.passed, + "conflicting backend/target-db must fail validation; checks: {:?}", + report.checks + ); + assert!( + report + .checks + .iter() + .any(|c| c.name == "backend-unambiguous" && !c.passed), + "expected a failed 'backend-unambiguous' check; checks: {:?}", + report.checks + ); + } + /// A schema-source pointing at a missing file must fail /// `schema-source-exists`. #[test] @@ -960,6 +997,26 @@ pub fn validate_manifest(path: &str) -> ValidationReport { }, }, ); + + // 5. Backend selection is unambiguous. `effective_backend()` rejects a + // manifest that sets both [database].backend and the legacy + // [database].target-db to conflicting values (V-L2-E1). Validation must + // exercise it, otherwise a latent conflict passes `validate` only to + // fail later at generate time. + let backend_check = ValidationCheck { + name: "backend-unambiguous".to_string(), + description: "[database].backend and legacy target-db do not conflict".to_string(), + passed: true, + detail: None, + }; + checks.push(match m.database.effective_backend() { + Ok(_) => backend_check, + Err(e) => ValidationCheck { + passed: false, + detail: Some(e.to_string()), + ..backend_check + }, + }); } let passed = checks.iter().all(|c| c.passed);