//! Actionable diagnostics. //! //! Every engine failure names a stable code, the canonical owner of the //! disputed fact, the commands that inspect it, and the permitted resolution. //! Nothing in this file assumes product knowledge; repository-specific text //! arrives through the `profile` module. const std = @import("std"); const model_types = @import("model.zig"); const profile = @import("profile"); const tool = profile.tool_invocation; /// The canonical owner of a disputed fact, plus how to inspect and resolve it. pub const Subject = struct { /// Stable, greppable diagnostic code. code: []const u8, /// What kind of fact is inconsistent. kind: []const u8, /// Canonical owner of the fact. owner: []const u8, /// Commands that inspect the fact and its dependents. inspect: []const []const u8 = &.{}, /// The permitted resolution, stated as an instruction to the author. resolve: []const u8, }; pub const subject = struct { pub const document: Subject = .{ .code = "TJSP-E010", .kind = "document catalog", .owner = profile.paths.documents, .inspect = &.{tool ++ " query document NAME"}, .resolve = "Correct the catalog entry, then regenerate projections with " ++ profile.generate_command ++ ".", }; pub const concept: Subject = .{ .code = "TJSP-E020", .kind = "concept", .owner = profile.paths.concepts, .inspect = &.{ tool ++ " query concept ID", tool ++ " query owner OWNER" }, .resolve = "Correct the concept in its registry, then regenerate projections with " ++ profile.generate_command ++ ".", }; pub const requirement: Subject = .{ .code = "TJSP-E110", .kind = "requirement", .owner = profile.paths.requirements, .inspect = &.{ tool ++ " query requirement ID", tool ++ " status", tool ++ " impact ID" }, .resolve = "Correct the requirement record, or revise the owning document it points at.", }; pub const obligation: Subject = .{ .code = "TJSP-E310", .kind = "verification obligation", .owner = profile.paths.verification, .inspect = &.{ tool ++ " query obligation ID", tool ++ " status", tool ++ " impact ID" }, .resolve = "Correct the obligation, or revise the requirement revision it verifies.", }; pub const test_spec: Subject = .{ .code = "TJSP-E210", .kind = "test specification", .owner = profile.paths.test_specs, .inspect = &.{ tool ++ " query test-spec ID", tool ++ " status", tool ++ " impact ID" }, .resolve = "Correct the test specification, or scaffold a consistent one with " ++ tool ++ " new-test NAME.", }; pub const fixture: Subject = .{ .code = "TJSP-E220", .kind = "prose fixture", .owner = profile.paths.fixture_root, .inspect = &.{ tool ++ " query test-spec ID", tool ++ " status", tool ++ " impact FIXTURE-PATH" }, .resolve = "Correct the fixture prose or its instrumentation so that both express the " ++ "same declared oracles.", }; pub const binding: Subject = .{ .code = "TJSP-E240", .kind = "executable binding", .owner = profile.paths.test_bindings, .inspect = &.{ tool ++ " query test-spec ID", tool ++ " status", tool ++ " impact BINDING-PATH" }, .resolve = "Correct the binding registration, or produce a fresh implementation with " ++ tool ++ " scaffold-test TEST-ID.", }; pub const implementation: Subject = .{ .code = "TJSP-E400", .kind = "implementation unit", .owner = profile.paths.implementations, .inspect = &.{ tool ++ " query implementation ID", tool ++ " status", tool ++ " impact SOURCE-PATH" }, .resolve = "Register the source with exactly one implementation unit " ++ "and the obligations it realizes.", }; pub const unregistered_implementation: Subject = .{ .code = "TJSP-E420", .kind = "unregistered implementation source", .owner = profile.paths.implementations, .inspect = &.{tool ++ " status"}, .resolve = "Register the source with the obligations it realizes:\n" ++ " " ++ tool ++ " new-source PATH OBLIGATION-ID...", }; pub const unregistered_binding: Subject = .{ .code = "TJSP-E251", .kind = "unregistered fixture implementation", .owner = profile.paths.test_bindings, .inspect = &.{tool ++ " status"}, .resolve = "Register the binding for its instrumented fixture, or delete the source. " ++ "A plan is printed by\n " ++ tool ++ " new-test NAME REQUIREMENT SUITE.", }; pub const product_test: Subject = .{ .code = "TJSP-E250", .kind = "executable product test", .owner = profile.paths.test_specs, .inspect = &.{tool ++ " status"}, .resolve = "Move the assertions into a prose fixture and its instrumentation block:\n" ++ " " ++ tool ++ " new-test NAME REQUIREMENT SUITE", }; pub const projection: Subject = .{ .code = "TJSP-E500", .kind = "generated projection", .owner = "generated output", .inspect = &.{}, .resolve = "Regenerate projections with " ++ profile.generate_command ++ "; never edit generated files by hand.", }; pub const path: Subject = .{ .code = "TJSP-E600", .kind = "repository path", .owner = "repository layout", .inspect = &.{}, .resolve = "Reference an existing repository-relative path without parent components.", }; }; /// Report an inconsistency with its owner, inspection commands, and resolution. pub fn fail( writer: *std.Io.Writer, context: Subject, comptime format: []const u8, args: anytype, ) !void { try writer.print("{s}: " ++ format ++ "\n\n", .{context.code} ++ args); try writer.print("kind: {s}\nowner: {s}\n", .{ context.kind, context.owner }); for (context.inspect, 0..) |command, index| { const label = if (index == 0) "inspect: " else " "; try writer.print("{s}{s}\n", .{ label, command }); } try writer.print("resolve: {s}\n", .{context.resolve}); try writer.print("verify: {s}\n", .{profile.verify_command}); return error.InvalidSpecification; } pub const FingerprintDomain = enum { fixture, binding, implementation }; pub fn checkFingerprint( source: []const u8, expected: []const u8, domain: FingerprintDomain, id: []const u8, file_path: []const u8, stderr: *std.Io.Writer, ) !void { const context = switch (domain) { .fixture => subject.fixture, .binding => subject.binding, .implementation => subject.implementation, }; if (!sha256Valid(expected)) { return fail(stderr, context, "invalid SHA-256 fingerprint on {s}: {s}", .{ id, expected }); } var digest: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined; std.crypto.hash.sha2.Sha256.hash(source, &digest, .{}); var actual_buffer: [digest.len * 2]u8 = undefined; const actual = std.fmt.bufPrint(&actual_buffer, "{x}", .{&digest}) catch unreachable; if (std.mem.eql(u8, expected, actual)) return; const code = switch (domain) { .fixture => "TJSP-E230", .binding => "TJSP-E260", .implementation => "TJSP-E410", }; try stderr.print( "{s}: reviewed {s} changed without synchronization\n\n" ++ "owner: {s}\npath: {s}\nexpected: {s}\nactual: {s}\n\n" ++ "inspect: {s} impact {s}\n" ++ " {s} fingerprint {s}\n" ++ "resolve: classify the change. Presentation-only changes keep the revision and " ++ "record the current fingerprint.\n" ++ " Semantic changes increment the owning revision and migrate every " ++ "dependent link.\n" ++ "verify: {s}\n", .{ code, @tagName(domain), id, file_path, expected, actual, tool, id, tool, file_path, profile.verify_command, }, ); return error.InvalidSpecification; } pub fn sha256Valid(value: []const u8) bool { if (value.len != std.crypto.hash.sha2.Sha256.digest_length * 2) return false; for (value) |byte| { if (!std.ascii.isDigit(byte) and !(byte >= 'a' and byte <= 'f')) return false; } return true; } pub fn staleRequirement( stderr: *std.Io.Writer, reference: model_types.RequirementReference, current_revision: u32, dependent: []const u8, ) !void { try stderr.print( "TJSP-E120: stale requirement reference\n\n" ++ "dependent: {s}\nreference: {s}@{d}\ncurrent: {s}@{d}\n\n" ++ "inspect: {s} query requirement {s}\n" ++ " {s} impact {s}\n" ++ "resolve: read the current requirement, decide whether the dependent still holds, " ++ "then migrate or remove the link.\n" ++ " Never advance the referenced revision mechanically.\n" ++ "verify: {s}\n", .{ dependent, reference.id, reference.revision, reference.id, current_revision, tool, reference.id, tool, reference.id, profile.verify_command, }, ); return error.InvalidSpecification; } pub fn staleObligation( stderr: *std.Io.Writer, reference: model_types.ObligationReference, current_revision: u32, dependent: []const u8, ) !void { try stderr.print( "TJSP-E320: stale verification-obligation reference\n\n" ++ "dependent: {s}\nreference: {s}@{d}\ncurrent: {s}@{d}\n\n" ++ "inspect: {s} query obligation {s}\n" ++ " {s} impact {s}\n" ++ "resolve: decide whether the dependent still verifies the revised obligation, " ++ "then migrate or remove the link.\n" ++ "verify: {s}\n", .{ dependent, reference.id, reference.revision, reference.id, current_revision, tool, reference.id, tool, reference.id, profile.verify_command, }, ); return error.InvalidSpecification; } pub fn validateRepositoryPath(io: std.Io, file_path: []const u8, stderr: *std.Io.Writer) !void { if (std.fs.path.isAbsolute(file_path)) { return fail(stderr, subject.path, "absolute repository path: {s}", .{file_path}); } if (std.mem.indexOf(u8, file_path, "..") != null) { return fail(stderr, subject.path, "parent component in repository path: {s}", .{file_path}); } std.Io.Dir.cwd().access(io, file_path, .{}) catch { return fail(stderr, subject.path, "missing repository path: {s}", .{file_path}); }; } test "diagnostics name the owner, an inspection command, and a resolution" { var output = std.Io.Writer.Allocating.init(std.testing.allocator); defer output.deinit(); try std.testing.expectError( error.InvalidSpecification, fail(&output.writer, subject.test_spec, "duplicate test-spec ID: {s}", .{"test-example"}), ); const written = output.written(); try std.testing.expect(std.mem.indexOf(u8, written, "TJSP-E210") != null); try std.testing.expect(std.mem.indexOf(u8, written, "test-example") != null); try std.testing.expect(std.mem.indexOf(u8, written, "owner:") != null); try std.testing.expect(std.mem.indexOf(u8, written, "inspect:") != null); try std.testing.expect(std.mem.indexOf(u8, written, "status") != null); try std.testing.expect(std.mem.indexOf(u8, written, "resolve:") != null); try std.testing.expect(std.mem.indexOf(u8, written, "verify:") != null); } test "strict fingerprints report actionable drift" { var output = std.Io.Writer.Allocating.init(std.testing.allocator); defer output.deinit(); try checkFingerprint( "abc", "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", .fixture, "fixture-example", "fixtures/example.md", &output.writer, ); try std.testing.expectError( error.InvalidSpecification, checkFingerprint( "changed", "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", .fixture, "fixture-example", "fixtures/example.md", &output.writer, ), ); try std.testing.expect(std.mem.indexOf(u8, output.written(), "TJSP-E230") != null); try std.testing.expect(std.mem.indexOf(u8, output.written(), "impact fixture-example") != null); }