//! Generators for new and unfinished verification work. //! //! Scaffolds are disposable authoring material written to standard output only. //! They never mutate a registry, never claim coverage, and never pass a gate. const std = @import("std"); const model_types = @import("model.zig"); const text = @import("text.zig"); const profile = @import("profile"); const tool = profile.tool_invocation; /// Render every registry delta required to introduce one new verified scenario. /// /// The output is a complete, ordered authoring plan: prose fixture, verification /// obligations, test specification, executable binding, and the commands that /// finish and verify the work. pub const NewTestOptions = struct { /// Explicit fixture path. Defaults to `.md`. fixture_path: ?[]const u8 = null, /// Write the fixture file when it does not exist yet. write_fixture: bool = false, }; pub fn newTest( graph: *const model_types.Graph, name: []const u8, requirement_id: []const u8, suite_name: []const u8, options: NewTestOptions, io: ?std.Io, writer: *std.Io.Writer, ) !void { if (!text.nameValid(name)) return error.InvalidScaffoldName; const requirement = model_types.findRequirement(graph.groups, requirement_id) orelse return error.RequirementNotFound; const suite = std.meta.stringToEnum(model_types.Suite, suite_name) orelse return error.UnknownSuite; if (!profile.suiteIsConcrete(suite)) return error.SuiteMustBeConcrete; if (!model_types.requirementHasSuite(&requirement, suite)) return error.SuiteNotRequired; var upper_buffer: [128]u8 = undefined; const upper = try upperName(&upper_buffer, name); var symbol_buffer: [128]u8 = undefined; const symbol = try lowerSymbol(&symbol_buffer, name); var default_path_buffer: [256]u8 = undefined; const fixture_file = try fixturePath(&default_path_buffer, name, options.fixture_path); try writer.print( \\Authoring plan for a new verified scenario. \\ \\name: {s} \\requirement: {s}@{d} \\suite: {s} \\ \\Nothing below has been written. Apply the steps in order. \\ \\ , .{ name, requirement.id, requirement.revision, @tagName(suite) }); try writer.print( \\Step 1. Create the prose fixture at {s} \\ \\---------------------------------------------------------------------- \\ , .{fixture_file}); try renderFixtureProse(name, upper, writer); try writer.writeAll( "----------------------------------------------------------------------\n\n", ); try writer.print( \\Step 2. Append two verification obligations to {s} \\ \\---------------------------------------------------------------------- \\ .{{ \\ .id = "obligation-{s}-holds", \\ .revision = 1, \\ .requirement = .{{ .id = "{s}", .revision = {d} }}, \\ .kind = .required, \\ .label = "STATE THE REQUIRED OBSERVATION.", \\ .suites = .{{.{s}}}, \\ }}, \\ .{{ \\ .id = "obligation-{s}-forbids", \\ .revision = 1, \\ .requirement = .{{ .id = "{s}", .revision = {d} }}, \\ .kind = .forbidden, \\ .label = "STATE THE FORBIDDEN EFFECT.", \\ .suites = .{{.{s}}}, \\ }}, \\---------------------------------------------------------------------- \\ \\ , .{ profile.paths.verification, name, requirement.id, requirement.revision, @tagName(suite), name, requirement.id, requirement.revision, @tagName(suite), }); try writer.print( \\Step 3. Append the test specification to {s} \\ \\---------------------------------------------------------------------- \\ .{{ \\ .id = "test-{s}", \\ .revision = 1, \\ .suite = .{s}, \\ .fixture = .{{ \\ .id = "fixture-{s}", \\ .revision = 1, \\ .path = "{s}", \\ .sha256 = "OBTAIN WITH THE COMMAND IN STEP 5", \\ }}, \\ .claims = .{{ \\ .{{ \\ .oracle = "INV-{s}", \\ .kind = .required, \\ .obligations = .{{ \\ .{{ .id = "obligation-{s}-holds", .revision = 1 }}, \\ }}, \\ }}, \\ .{{ \\ .oracle = "NO-{s}", \\ .kind = .forbidden, \\ .obligations = .{{ \\ .{{ .id = "obligation-{s}-forbids", .revision = 1 }}, \\ }}, \\ }}, \\ }}, \\ .production_boundary = .process, \\ }}, \\---------------------------------------------------------------------- \\ \\ , .{ profile.paths.test_specs, name, @tagName(suite), name, fixture_file, upper, name, upper, name, }); try writer.print( \\Step 4. Only when the fixture carries instrumentation, append the binding to {s} \\ \\---------------------------------------------------------------------- \\ .{{ \\ .test_spec = .{{ .id = "test-{s}", .revision = 1 }}, \\ .fixture_revision = 1, \\ .source = "{s}{s}.zig", \\ .source_sha256 = "OBTAIN WITH THE COMMAND IN STEP 5", \\ .module = "fixture_{s}", \\ }}, \\---------------------------------------------------------------------- \\ \\A prose fixture without an instrumentation fence is a valid specified \\test and takes no binding. \\ \\ , .{ profile.paths.test_bindings, name, profile.paths.binding_inventory_root ++ "/", symbol, symbol, }); if (options.write_fixture) { const handle = io orelse return error.FixtureWriteUnavailable; try writeFixtureFile(handle, fixture_file, name, upper); try writer.print("Wrote {s}. Nothing else was written.\n\n", .{fixture_file}); } try writer.print( \\Step 5. Finish and verify \\ \\ {s} scaffold-test test-{s} \\ {s} fingerprint {s} \\ {s} fingerprint {s}{s}.zig \\ {s} impact {s} \\ {s} \\ \\Every gate reports its own owner, inspection commands, and resolution. \\ , .{ tool, name, tool, fixture_file, tool, profile.paths.binding_inventory_root ++ "/", symbol, tool, requirement.id, profile.verify_command, }); } fn fixturePath(buffer: []u8, name: []const u8, explicit: ?[]const u8) ![]const u8 { if (explicit) |value| { if (!std.mem.startsWith(u8, value, profile.paths.fixture_root)) { return error.FixtureOutsideFixtureRoot; } if (!std.mem.endsWith(u8, value, ".md")) return error.FixtureIsNotMarkdown; return value; } return std.fmt.bufPrint(buffer, "{s}{s}.md", .{ profile.paths.fixture_root, name }) catch error.ScaffoldNameTooLong; } fn writeFixtureFile(io: std.Io, path: []const u8, name: []const u8, upper: []const u8) !void { const cwd = std.Io.Dir.cwd(); if (cwd.access(io, path, .{})) |_| { return error.FixtureAlreadyExists; } else |_| {} var buffer: [8192]u8 = undefined; var prose = std.Io.Writer.fixed(&buffer); try renderFixtureProse(name, upper, &prose); try cwd.writeFile(io, .{ .sub_path = path, .data = prose.buffered() }); } fn renderFixtureProse(name: []const u8, upper: []const u8, writer: *std.Io.Writer) !void { var symbol_buffer: [128]u8 = undefined; const symbol = try lowerSymbol(&symbol_buffer, name); try writer.print( \\# TITLE THIS SCENARIO \\ \\**Fixture:** `fixture-{s}@1` \\ \\## Given \\ \\STATE THE INITIAL CONDITIONS. \\ \\## When \\ \\STATE THE SINGLE STIMULUS. \\ \\## Required invariants \\ \\- **`INV-{s}`:** STATE THE REQUIRED OBSERVATION. \\ \\## Forbidden effects \\ \\- **`NO-{s}`:** STATE THE FORBIDDEN EFFECT. \\ \\## Instrumentation \\ \\```zig tj-test \\try fixture.given_SOMETHING(); \\try fixture.when_SOMETHING(); \\try fixture.then_inv_{s}(); \\try fixture.forbid_no_{s}(); \\``` \\ \\## Variations \\ \\STATE RELATED SCENARIOS OWNED ELSEWHERE. \\ \\## Limitations \\ \\STATE WHAT THIS FIXTURE CANNOT OBSERVE. \\ , .{ name, upper, upper, symbol, symbol }); } fn upperName(buffer: []u8, name: []const u8) ![]const u8 { if (name.len > buffer.len) return error.ScaffoldNameTooLong; for (name, 0..) |byte, index| buffer[index] = std.ascii.toUpper(byte); return buffer[0..name.len]; } /// Fixture methods use underscore symbols derived from the hyphenated name. fn lowerSymbol(buffer: []u8, name: []const u8) ![]const u8 { if (name.len > buffer.len) return error.ScaffoldNameTooLong; for (name, 0..) |byte, index| buffer[index] = if (byte == '-') '_' else byte; return buffer[0..name.len]; } /// Render a disposable fixture implementation that cannot pass unfinished. pub fn renderFixtureScaffold( test_spec: *const model_types.TestSpec, writer: *std.Io.Writer, ) !void { try writer.print("// Authoring scaffold for {s}@{d}.\n", .{ test_spec.id, test_spec.revision, }); try writer.print("// Read {s}.\n", .{test_spec.fixture.path}); try writer.writeAll("pub const Fixture = struct {\n"); try writer.writeAll(" pub fn init() !Fixture { return .{}; }\n"); try writer.writeAll(" pub fn deinit(fixture: *Fixture) void { _ = fixture; }\n"); try writer.writeAll( "};\ncomptime {\n" ++ " @compileError(\"Complete this fixture before tracking it.\");\n" ++ "}\n", ); } pub fn scaffoldTest( graph: *const model_types.Graph, id: []const u8, writer: *std.Io.Writer, ) !void { const test_spec = model_types.findTestSpec(graph.test_specs, id) orelse return error.TestSpecNotFound; try renderFixtureScaffold(&test_spec, writer); } test "the authoring plan names every registry it touches" { const requirements = [_]model_types.Requirement{.{ .id = "REQ-EXAMPLE-01", .revision = 3, .status = .accepted, .label = "Example.", .owner = "owner.md", .suites = &.{model_types.testSuite(0)}, .limitation_status = .unassessed, }}; const groups = [_]model_types.RequirementGroup{.{ .title = "Example", .requirements = &requirements, }}; const graph: model_types.Graph = .{ .documents = &.{}, .concepts = &.{}, .groups = &groups, .test_specs = &.{}, .test_bindings = &.{}, .verification = &.{}, .implementations = &.{}, }; var output = std.Io.Writer.Allocating.init(std.testing.allocator); defer output.deinit(); try newTest( &graph, "example-scenario", "REQ-EXAMPLE-01", @tagName(model_types.testSuite(0)), .{}, null, &output.writer, ); const written = output.written(); for ([_][]const u8{ profile.paths.verification, profile.paths.test_specs, profile.paths.test_bindings, "REQ-EXAMPLE-01@3", "INV-EXAMPLE-SCENARIO", "NO-EXAMPLE-SCENARIO", "```zig tj-test", "then_inv_example_scenario", "fixture_example_scenario", "example_scenario.zig", profile.verify_command, }) |needle| { try std.testing.expect(std.mem.indexOf(u8, written, needle) != null); } try std.testing.expectError( error.RequirementNotFound, newTest(&graph, "example", "REQ-MISSING-01", "u", .{}, null, &output.writer), ); try std.testing.expectError( error.UnknownSuite, newTest(&graph, "example", "REQ-EXAMPLE-01", "zz", .{}, null, &output.writer), ); }