const std = @import("std"); const model_types = @import("model.zig"); const diag = @import("diag.zig"); const fail = diag.fail; const on = diag.subject; const text = @import("text.zig"); const fixtures = @import("fixture.zig"); const scaffold = @import("scaffold.zig"); const profile = @import("profile"); /// Validate every generic relationship in the graph. /// /// Product-domain registries are validated by their owning profile consumer. pub fn graph( allocator: std.mem.Allocator, io: std.Io, model: *const model_types.Graph, stderr: *std.Io.Writer, ) !void { if (model.documents.len == 0) { return fail(stderr, on.document, "document catalog is empty", .{}); } if (model.documents.len > model_types.document_count_max) { return fail(stderr, on.document, "document count exceeds {d}", .{ model_types.document_count_max, }); } if (model.concepts.len > model_types.concept_count_max) { return fail(stderr, on.concept, "concept count exceeds {d}", .{ model_types.concept_count_max, }); } if (model.groups.len > model_types.group_count_max) { return fail( stderr, on.requirement, "group count exceeds {d}", .{model_types.group_count_max}, ); } try validateDocuments(io, model.documents, stderr); try validateRequirements(allocator, io, model, stderr); try validateConcepts(allocator, io, model, stderr); try validateVerification(model, stderr); try validateTestSpecs(allocator, io, model, stderr); try validateTestBindings(allocator, io, model, stderr); try validateImplementations(allocator, io, model, stderr); try validateSourceInventories(allocator, io, model, stderr); } pub fn validateDocuments( io: std.Io, documents: []const model_types.Document, stderr: *std.Io.Writer, ) !void { for (documents, 0..) |document, index| { if (!text.nameValid(document.name)) { return fail(stderr, on.document, "invalid document name: {s}", .{document.name}); } if (std.mem.trim(u8, document.description, " \t\r\n").len == 0) { return fail(stderr, on.document, "empty description: {s}", .{document.name}); } try diag.validateRepositoryPath(io, document.location, stderr); for (documents[0..index]) |previous| { if (std.mem.eql(u8, previous.name, document.name)) { return fail(stderr, on.document, "duplicate document name: {s}", .{document.name}); } if (std.mem.eql(u8, previous.location, document.location)) { return fail( stderr, on.document, "duplicate document location: {s}", .{document.location}, ); } } } } pub fn validateConcepts( allocator: std.mem.Allocator, io: std.Io, model: *const model_types.Graph, stderr: *std.Io.Writer, ) !void { for (model.concepts, 0..) |concept, index| { if (!text.conceptIdValid(concept.id)) { return fail(stderr, on.concept, "invalid concept ID: {s}", .{concept.id}); } if (std.mem.trim(u8, concept.term, " \t\r\n").len == 0) { return fail(stderr, on.concept, "empty concept term: {s}", .{concept.id}); } if (std.mem.trim(u8, concept.definition, " \t\r\n").len == 0) { return fail(stderr, on.concept, "empty concept definition: {s}", .{concept.id}); } if (concept.invariants.len == 0) { return fail(stderr, on.concept, "concept has no invariant: {s}", .{concept.id}); } try validateOwner(allocator, io, concept.owner, stderr); for (model.concepts[0..index]) |previous| { if (std.mem.eql(u8, previous.id, concept.id)) { return fail(stderr, on.concept, "duplicate concept ID: {s}", .{concept.id}); } if (std.ascii.eqlIgnoreCase(previous.term, concept.term)) { return fail(stderr, on.concept, "duplicate concept term: {s}", .{concept.term}); } } try validateConceptAliases(model.concepts, &concept, stderr); try validateConceptReferences(model, &concept, stderr); } } pub fn validateConceptAliases( concepts: []const model_types.Concept, concept: *const model_types.Concept, stderr: *std.Io.Writer, ) !void { for (concept.aliases, 0..) |alias, index| { if (alias.len == 0) return fail(stderr, on.concept, "empty alias on {s}", .{concept.id}); if (std.ascii.eqlIgnoreCase(alias, concept.term)) { return fail(stderr, on.concept, "canonical term repeated as alias: {s}", .{concept.id}); } for (concept.aliases[0..index]) |previous| { if (std.ascii.eqlIgnoreCase(previous, alias)) { return fail( stderr, on.concept, "duplicate alias on {s}: {s}", .{ concept.id, alias }, ); } } for (concepts) |candidate| { if (std.ascii.eqlIgnoreCase(candidate.term, alias)) { return fail( stderr, on.concept, "alias conflicts with canonical term: {s}", .{alias}, ); } } } } pub fn validateConceptReferences( model: *const model_types.Graph, concept: *const model_types.Concept, stderr: *std.Io.Writer, ) !void { for (concept.non_equivalent) |reference| { var found = false; for (model.concepts) |candidate| { if (std.mem.eql(u8, candidate.id, reference)) found = true; } if (!found) { return fail( stderr, on.concept, "unknown concept reference {s} from {s}", .{ reference, concept.id }, ); } if (std.mem.eql(u8, concept.id, reference)) { return fail( stderr, on.concept, "concept is non-equivalent to itself: {s}", .{concept.id}, ); } const target = findConcept(model.concepts, reference).?; if (!stringListContains(target.non_equivalent, concept.id)) { return fail(stderr, on.concept, "asymmetric non-equivalence: {s} and {s}", .{ concept.id, reference, }); } } for (concept.requirements, 0..) |requirement_id, index| { if (!requirementExists(model, requirement_id)) { return fail( stderr, on.concept, "unknown requirement {s} on {s}", .{ requirement_id, concept.id }, ); } if (stringListContains(concept.requirements[0..index], requirement_id)) { return fail(stderr, on.concept, "duplicate requirement on concept: {s}", .{concept.id}); } } } pub fn findConcept(concepts: []const model_types.Concept, id: []const u8) ?model_types.Concept { for (concepts) |concept| if (std.mem.eql(u8, concept.id, id)) return concept; return null; } pub fn stringListContains(values: []const []const u8, expected: []const u8) bool { for (values) |value| if (std.mem.eql(u8, value, expected)) return true; return false; } pub fn requirementExists(model: *const model_types.Graph, id: []const u8) bool { for (model.groups) |group| { for (group.requirements) |requirement| { if (std.mem.eql(u8, requirement.id, id)) return true; } } return false; } pub const ClaimedRequirements = struct { ids: std.StringHashMapUnmanaged(void) = .empty, pub fn deinit(self: *ClaimedRequirements, allocator: std.mem.Allocator) void { self.ids.deinit(allocator); } pub fn has(self: *const ClaimedRequirements, id: []const u8) bool { return self.ids.contains(id); } }; /// Resolves every claim's obligation references once so per-requirement /// traceability checks are set lookups instead of repeated graph scans. pub fn buildClaimedRequirements( allocator: std.mem.Allocator, model: *const model_types.Graph, ) !ClaimedRequirements { var claimed: ClaimedRequirements = .{}; errdefer claimed.deinit(allocator); for (model.test_specs) |test_spec| { for (test_spec.claims) |claim| { for (claim.obligations) |reference| { const obligation = model_types.findObligation(model.verification, reference.id) orelse continue; try claimed.ids.put(allocator, obligation.requirement.id, {}); } } } return claimed; } pub fn validateRequirements( allocator: std.mem.Allocator, io: std.Io, model: *const model_types.Graph, stderr: *std.Io.Writer, ) !void { const requirement_count = model.requirementCount(); if (requirement_count == 0) { return fail(stderr, on.requirement, "requirement registry is empty", .{}); } if (requirement_count > model_types.requirement_count_max) { return fail( stderr, on.requirement, "requirement count exceeds {d}", .{model_types.requirement_count_max}, ); } var claimed = try buildClaimedRequirements(allocator, model); defer claimed.deinit(allocator); for (model.groups, 0..) |group, group_index| { if (group.requirements.len == 0) { return fail(stderr, on.requirement, "empty requirement group: {s}", .{group.title}); } for (model.groups[0..group_index]) |previous| { if (std.mem.eql(u8, previous.title, group.title)) { return fail( stderr, on.requirement, "duplicate requirement group: {s}", .{group.title}, ); } } for (group.requirements) |requirement| { try validateRequirement(allocator, io, model, &claimed, &requirement, stderr); } } } pub fn validateRequirement( allocator: std.mem.Allocator, io: std.Io, model: *const model_types.Graph, claimed: *const ClaimedRequirements, requirement: *const model_types.Requirement, stderr: *std.Io.Writer, ) !void { if (!profile.requirementIdValid(requirement.id)) { return fail(stderr, on.requirement, "invalid requirement ID: {s}", .{requirement.id}); } if (requirement.revision == 0) { return fail(stderr, on.requirement, "zero requirement revision: {s}", .{requirement.id}); } if (requirement.label.len == 0) { return fail(stderr, on.requirement, "empty requirement label: {s}", .{requirement.id}); } if (requirement.suites.len == 0) { return fail(stderr, on.requirement, "requirement has no suites: {s}", .{requirement.id}); } try validateOwner(allocator, io, requirement.owner, stderr); try validateUniqueSuites(requirement, stderr); try validateRequirementTraceability(allocator, io, claimed, requirement, stderr); try validateUniqueRequirement(model, requirement, stderr); } pub fn validateRequirementTraceability( allocator: std.mem.Allocator, io: std.Io, claimed: *const ClaimedRequirements, requirement: *const model_types.Requirement, stderr: *std.Io.Writer, ) !void { if (requirement.invariant_status == .mapped and !claimed.has(requirement.id)) { return fail(stderr, on.requirement, "mapped requirement has no claimed verification path: {s}", .{ requirement.id, }); } try validateTraceabilityReferences( allocator, io, requirement.platform_evidence_status, requirement.platform_evidence_references, "platform evidence", requirement.id, stderr, ); try validateTraceabilityReferences( allocator, io, requirement.limitation_status, requirement.limitation_references, "limitation", requirement.id, stderr, ); } fn validateTraceabilityReferences( allocator: std.mem.Allocator, io: std.Io, status: anytype, references: []const []const u8, kind: []const u8, requirement_id: []const u8, stderr: *std.Io.Writer, ) !void { const requires_reference = switch (@TypeOf(status)) { model_types.PlatformEvidenceStatus => status == .recorded, model_types.LimitationStatus => status == .recorded, else => @compileError("unsupported traceability status"), }; if (requires_reference and references.len == 0) { return fail(stderr, on.requirement, "recorded {s} has no reference: {s}", .{ kind, requirement_id, }); } if (!requires_reference and references.len != 0) { return fail(stderr, on.requirement, "unrecorded {s} has references: {s}", .{ kind, requirement_id, }); } for (references, 0..) |reference, index| { if (reference.len == 0) { return fail(stderr, on.requirement, "empty {s} reference: {s}", .{ kind, requirement_id }); } try validateOwner(allocator, io, reference, stderr); for (references[0..index]) |previous| { if (std.mem.eql(u8, previous, reference)) { return fail(stderr, on.requirement, "duplicate {s} reference: {s}", .{ kind, requirement_id }); } } } } pub fn validateUniqueSuites( requirement: *const model_types.Requirement, stderr: *std.Io.Writer, ) !void { for (requirement.suites, 0..) |suite, index| { for (requirement.suites[0..index]) |previous| { if (suite == previous) { return fail(stderr, on.requirement, "duplicate suite on {s}", .{requirement.id}); } } if (!profile.suiteIsConcrete(suite) and requirement.suites.len != 1) { return fail( stderr, on.requirement, "suite '{s}' must stand alone on {s}", .{ @tagName(suite), requirement.id }, ); } } } pub fn validateUniqueRequirement( model: *const model_types.Graph, requirement: *const model_types.Requirement, stderr: *std.Io.Writer, ) !void { var seen = false; for (model.groups) |group| { for (group.requirements) |candidate| { if (!std.mem.eql(u8, candidate.id, requirement.id)) continue; if (seen) { return fail( stderr, on.requirement, "duplicate requirement ID: {s}", .{requirement.id}, ); } seen = true; } } std.debug.assert(seen); } pub fn validateVerification( model: *const model_types.Graph, stderr: *std.Io.Writer, ) !void { if (model.verification.len == 0) { return fail(stderr, on.obligation, "verification-obligation registry is empty", .{}); } if (model.verification.len > model_types.verification_count_max) { return fail(stderr, on.obligation, "verification-obligation count exceeds {d}", .{ model_types.verification_count_max, }); } for (model.verification, 0..) |obligation, index| { if (!text.prefixedNameValid(obligation.id, "obligation-")) { return fail(stderr, on.obligation, "invalid obligation ID: {s}", .{obligation.id}); } if (obligation.revision == 0) { return fail(stderr, on.obligation, "zero obligation revision: {s}", .{obligation.id}); } if (obligation.label.len == 0) { return fail(stderr, on.obligation, "empty obligation label: {s}", .{obligation.id}); } try validateObligationRequirement(model, &obligation, stderr); try validateObligationSuites(&obligation, stderr); for (model.verification[0..index]) |previous| { if (std.mem.eql(u8, previous.id, obligation.id)) { return fail( stderr, on.obligation, "duplicate obligation ID: {s}", .{obligation.id}, ); } } } } pub fn validateObligationRequirement( model: *const model_types.Graph, obligation: *const model_types.VerificationObligation, stderr: *std.Io.Writer, ) !void { const reference = obligation.requirement; const requirement = model_types.findRequirement(model.groups, reference.id) orelse { return fail( stderr, on.obligation, "obligation references unknown requirement: {s}", .{reference.id}, ); }; if (reference.revision != requirement.revision) { return diag.staleRequirement(stderr, reference, requirement.revision, obligation.id); } for (obligation.suites) |suite| { if (!model_types.requirementHasSuite(&requirement, suite)) { return fail( stderr, on.obligation, "obligation suite is absent from requirement: {s}", .{obligation.id}, ); } } } pub fn validateObligationSuites( obligation: *const model_types.VerificationObligation, stderr: *std.Io.Writer, ) !void { if (obligation.suites.len == 0) { return fail(stderr, on.obligation, "obligation has no suite: {s}", .{obligation.id}); } for (obligation.suites, 0..) |suite, index| { if (!profile.suiteIsConcrete(suite)) { return fail( stderr, on.obligation, "obligation requires a concrete suite: {s}", .{obligation.id}, ); } for (obligation.suites[0..index]) |previous| { if (suite == previous) { return fail( stderr, on.obligation, "duplicate suite on obligation: {s}", .{obligation.id}, ); } } } } pub fn validateTestSpecs( allocator: std.mem.Allocator, io: std.Io, model: *const model_types.Graph, stderr: *std.Io.Writer, ) !void { if (model.test_specs.len == 0) { return fail(stderr, on.test_spec, "test-spec registry is empty", .{}); } if (model.test_specs.len > model_types.test_spec_count_max) { return fail( stderr, on.test_spec, "test-spec count exceeds {d}", .{model_types.test_spec_count_max}, ); } for (model.test_specs, 0..) |test_spec, index| { try validateTestSpecIdentity(model.test_specs[0..index], &test_spec, stderr); try validateTestSpecClaims(model, &test_spec, stderr); try validateTestSpecFixture(allocator, io, &test_spec, stderr); } } pub fn validateTestSpecIdentity( previous_specs: []const model_types.TestSpec, test_spec: *const model_types.TestSpec, stderr: *std.Io.Writer, ) !void { if (!text.prefixedNameValid(test_spec.id, "test-")) { return fail(stderr, on.test_spec, "invalid test-spec ID: {s}", .{test_spec.id}); } if (test_spec.revision == 0) { return fail(stderr, on.test_spec, "zero test-spec revision: {s}", .{test_spec.id}); } if (!profile.suiteIsConcrete(test_spec.suite)) { return fail( stderr, on.test_spec, "test spec requires a concrete suite: {s}", .{test_spec.id}, ); } if (test_spec.claims.len == 0) { return fail( stderr, on.test_spec, "test spec has no verification claim: {s}", .{test_spec.id}, ); } for (previous_specs) |previous| { if (std.mem.eql(u8, previous.id, test_spec.id)) { return fail(stderr, on.test_spec, "duplicate test-spec ID: {s}", .{test_spec.id}); } } } pub fn validateTestSpecClaims( model: *const model_types.Graph, test_spec: *const model_types.TestSpec, stderr: *std.Io.Writer, ) !void { var required_count: u32 = 0; var forbidden_count: u32 = 0; for (test_spec.claims, 0..) |claim, claim_index| { if (!text.oracleIdValid(claim.oracle)) { return fail(stderr, on.test_spec, "invalid claim oracle: {s}", .{claim.oracle}); } if (claim.kind == .required) required_count += 1 else forbidden_count += 1; try validateClaimOracleKind(&claim, stderr); if (claim.obligations.len == 0) { return fail(stderr, on.test_spec, "claim has no obligation: {s}", .{claim.oracle}); } for (test_spec.claims[0..claim_index]) |previous| { if (std.mem.eql(u8, previous.oracle, claim.oracle)) { return fail(stderr, on.test_spec, "duplicate claim oracle: {s}", .{claim.oracle}); } } try validateClaimObligations(model, test_spec, &claim, stderr); } if (required_count == 0) { return fail(stderr, on.test_spec, "test spec has no required claim: {s}", .{test_spec.id}); } if (forbidden_count == 0) { return fail(stderr, on.test_spec, "test spec has no forbidden claim: {s}", .{test_spec.id}); } } pub fn validateClaimOracleKind( claim: *const model_types.VerificationClaim, stderr: *std.Io.Writer, ) !void { const prefix = if (claim.kind == .required) "INV-" else "NO-"; if (!std.mem.startsWith(u8, claim.oracle, prefix)) { return fail( stderr, on.test_spec, "claim polarity conflicts with oracle: {s}", .{claim.oracle}, ); } } pub fn validateClaimObligations( model: *const model_types.Graph, test_spec: *const model_types.TestSpec, claim: *const model_types.VerificationClaim, stderr: *std.Io.Writer, ) !void { for (claim.obligations, 0..) |reference, index| { const obligation = model_types.findObligation(model.verification, reference.id) orelse { return fail( stderr, on.test_spec, "unknown obligation {s} on {s}", .{ reference.id, claim.oracle }, ); }; if (reference.revision != obligation.revision) { return diag.staleObligation(stderr, reference, obligation.revision, claim.oracle); } if (claim.kind != obligation.kind) { return fail( stderr, on.test_spec, "claim polarity conflicts with obligation: {s}", .{reference.id}, ); } if (!model_types.suiteListContains(obligation.suites, test_spec.suite)) { return fail( stderr, on.test_spec, "test suite is not required by obligation: {s}", .{reference.id}, ); } for (claim.obligations[0..index]) |previous| { if (std.mem.eql(u8, previous.id, reference.id)) { return fail( stderr, on.test_spec, "duplicate obligation on claim: {s}", .{claim.oracle}, ); } } } } pub fn validateTestSpecFixture( allocator: std.mem.Allocator, io: std.Io, test_spec: *const model_types.TestSpec, stderr: *std.Io.Writer, ) !void { const fixture = &test_spec.fixture; if (!text.prefixedNameValid(fixture.id, "fixture-")) { return fail(stderr, on.fixture, "invalid fixture ID: {s}", .{fixture.id}); } if (fixture.revision == 0) { return fail(stderr, on.fixture, "zero fixture revision: {s}", .{fixture.id}); } if (!std.mem.startsWith(u8, fixture.path, profile.paths.fixture_root)) { return fail(stderr, on.fixture, "fixture outside {s}: {s}", .{ profile.paths.fixture_root, fixture.path, }); } if (!std.mem.endsWith(u8, fixture.path, ".md")) { return fail(stderr, on.fixture, "fixture is not Markdown: {s}", .{fixture.path}); } try diag.validateRepositoryPath(io, fixture.path, stderr); const source = try std.Io.Dir.cwd().readFileAlloc( io, fixture.path, allocator, .limited(model_types.file_size_max), ); defer allocator.free(source); try diag.checkFingerprint( source, fixture.sha256, .fixture, fixture.id, fixture.path, stderr, ); try fixtures.validateFixtureSource(source, test_spec, stderr); } pub fn validateTestBindings( allocator: std.mem.Allocator, io: std.Io, model: *const model_types.Graph, stderr: *std.Io.Writer, ) !void { if (model.test_bindings.len > model_types.test_binding_count_max) { return fail( stderr, on.binding, "test-binding count exceeds {d}", .{model_types.test_binding_count_max}, ); } for (model.test_bindings, 0..) |binding, index| { try validateTestBindingIdentity(model.test_specs, &binding, stderr); for (model.test_bindings[0..index]) |previous| { if (std.mem.eql(u8, previous.test_spec.id, binding.test_spec.id)) { return fail( stderr, on.binding, "duplicate test binding: {s}", .{binding.test_spec.id}, ); } } const test_spec = model_types.findTestSpec(model.test_specs, binding.test_spec.id).?; try validateTestBindingSource(allocator, io, &test_spec, &binding, stderr); } for (model.test_specs) |test_spec| { try validateInstrumentationBinding(allocator, io, model.test_bindings, &test_spec, stderr); } } pub fn validateInstrumentationBinding( allocator: std.mem.Allocator, io: std.Io, bindings: []const model_types.TestBinding, test_spec: *const model_types.TestSpec, stderr: *std.Io.Writer, ) !void { const source = try std.Io.Dir.cwd().readFileAlloc( io, test_spec.fixture.path, allocator, .limited(model_types.file_size_max), ); defer allocator.free(source); const instrumented = std.mem.indexOf(u8, source, "```zig tj-test") != null; const bound = model_types.findTestBinding(bindings, test_spec.id) != null; if (instrumented and !bound) { try stderr.print( "TJSP-E240: instrumented fixture has no executable binding\n\n" ++ "test: {s}@{d}\nfixture: {s}@{d}\npath: {s}\n\n" ++ "Create a fixture implementation with:\n" ++ " " ++ profile.tool_invocation ++ " scaffold-test {s}\n" ++ "Then register it in {s}.\n", .{ test_spec.id, test_spec.revision, test_spec.fixture.id, test_spec.fixture.revision, test_spec.fixture.path, test_spec.id, profile.paths.test_bindings, }, ); return error.InvalidSpecification; } if (!instrumented and bound) { return fail( stderr, on.binding, "binding targets an uninstrumented fixture: {s}", .{test_spec.id}, ); } } pub fn validateTestBindingIdentity( test_specs: []const model_types.TestSpec, binding: *const model_types.TestBinding, stderr: *std.Io.Writer, ) !void { const test_spec = model_types.findTestSpec(test_specs, binding.test_spec.id) orelse { return fail( stderr, on.binding, "binding references unknown test spec: {s}", .{binding.test_spec.id}, ); }; if (binding.test_spec.revision != test_spec.revision) { return fail(stderr, on.binding, "stale test-spec binding: {s}@{d}", .{ binding.test_spec.id, binding.test_spec.revision, }); } if (binding.fixture_revision != test_spec.fixture.revision) { return fail(stderr, on.binding, "stale fixture binding: {s}@{d}", .{ test_spec.fixture.id, binding.fixture_revision, }); } } pub fn validateTestBindingSource( allocator: std.mem.Allocator, io: std.Io, test_spec: *const model_types.TestSpec, binding: *const model_types.TestBinding, stderr: *std.Io.Writer, ) !void { if (!std.mem.startsWith(u8, binding.source, profile.paths.binding_root)) { return fail(stderr, on.binding, "test binding outside {s}: {s}", .{ profile.paths.binding_root, binding.source, }); } if (!std.mem.endsWith(u8, binding.source, ".zig")) { return fail(stderr, on.binding, "test binding is not Zig: {s}", .{binding.source}); } if (!text.symbolValid(binding.module)) { return fail(stderr, on.binding, "invalid test-binding module: {s}", .{binding.module}); } try diag.validateRepositoryPath(io, binding.source, stderr); const source = try std.Io.Dir.cwd().readFileAlloc( io, binding.source, allocator, .limited(model_types.file_size_max), ); defer allocator.free(source); try diag.checkFingerprint( source, binding.source_sha256, .binding, test_spec.id, binding.source, stderr, ); if (std.mem.indexOf(u8, source, "error.SkipZigTest") != null) { return fail(stderr, on.binding, "bound test is skipped: {s}", .{binding.source}); } if (std.mem.indexOf(u8, source, "@compileError") != null) { return fail(stderr, on.binding, "bound test is unfinished: {s}", .{binding.source}); } try rejectProductTestDeclarations(allocator, source, binding.source, stderr); var parsed = fixtures.parse(allocator, io, test_spec.fixture.path) catch |err| { return fail(stderr, on.binding, "cannot instrument {s}: {s}", .{ test_spec.fixture.id, @errorName(err), }); }; defer parsed.deinit(allocator); try fixtures.validateFixtureInstrumentation(test_spec, source, parsed.instrumentation, stderr); } pub fn rejectProductTestDeclarations( allocator: std.mem.Allocator, source: []const u8, path: []const u8, stderr: *std.Io.Writer, ) !void { const sentinel_source = try allocator.dupeZ(u8, source); defer allocator.free(sentinel_source); var tree = try std.zig.Ast.parse(allocator, sentinel_source, .zig); defer tree.deinit(allocator); if (tree.errors.len != 0) { return fail(stderr, on.binding, "cannot inspect malformed Zig source: {s}", .{path}); } for (tree.rootDecls()) |declaration| { if (tree.nodeTag(declaration) == .test_decl) { return fail( stderr, on.product_test, "handwritten test declaration outside the fixture graph: {s}", .{path}, ); } } } pub fn validateImplementations( allocator: std.mem.Allocator, io: std.Io, model: *const model_types.Graph, stderr: *std.Io.Writer, ) !void { if (model.implementations.len == 0) { return fail(stderr, on.implementation, "implementation-unit registry is empty", .{}); } if (model.implementations.len > model_types.implementation_count_max) { return fail(stderr, on.implementation, "implementation-unit count exceeds {d}", .{ model_types.implementation_count_max, }); } for (model.implementations, 0..) |unit, index| { try validateImplementationIdentity(model.implementations[0..index], &unit, stderr); try validateImplementationSources(allocator, io, model.implementations, &unit, stderr); try validateImplementationObligations(model.verification, &unit, stderr); } } pub fn validateImplementationIdentity( previous_units: []const model_types.ImplementationUnit, unit: *const model_types.ImplementationUnit, stderr: *std.Io.Writer, ) !void { if (!text.prefixedNameValid(unit.id, "implementation-")) { return fail(stderr, on.implementation, "invalid implementation-unit ID: {s}", .{unit.id}); } if (unit.revision == 0) { return fail( stderr, on.implementation, "zero implementation-unit revision: {s}", .{unit.id}, ); } if (unit.sources.len == 0) { return fail(stderr, on.implementation, "implementation has no source: {s}", .{unit.id}); } if (unit.realizes.len == 0) { return fail(stderr, on.implementation, "implementation has no obligation: {s}", .{unit.id}); } for (previous_units) |previous| { if (std.mem.eql(u8, previous.id, unit.id)) { return fail( stderr, on.implementation, "duplicate implementation-unit ID: {s}", .{unit.id}, ); } } } pub fn validateImplementationSources( allocator: std.mem.Allocator, io: std.Io, units: []const model_types.ImplementationUnit, unit: *const model_types.ImplementationUnit, stderr: *std.Io.Writer, ) !void { for (unit.sources, 0..) |source_reference, index| { if (!std.mem.startsWith(u8, source_reference.path, profile.paths.implementation_root)) { return fail(stderr, on.implementation, "implementation source outside {s}: {s}", .{ profile.paths.implementation_root, source_reference.path, }); } if (!std.mem.endsWith(u8, source_reference.path, ".zig")) { return fail( stderr, on.implementation, "implementation source is not Zig: {s}", .{source_reference.path}, ); } try diag.validateRepositoryPath(io, source_reference.path, stderr); for (unit.sources[0..index]) |previous| { if (std.mem.eql(u8, previous.path, source_reference.path)) { return fail( stderr, on.implementation, "duplicate source on implementation: {s}", .{unit.id}, ); } } try validateUniqueImplementationSource(units, unit.id, source_reference.path, stderr); const source = try std.Io.Dir.cwd().readFileAlloc( io, source_reference.path, allocator, .limited(model_types.file_size_max), ); defer allocator.free(source); try rejectProductTestDeclarations(allocator, source, source_reference.path, stderr); try diag.checkFingerprint( source, source_reference.sha256, .implementation, unit.id, source_reference.path, stderr, ); } } pub fn validateUniqueImplementationSource( units: []const model_types.ImplementationUnit, owner_id: []const u8, path: []const u8, stderr: *std.Io.Writer, ) !void { var count: u32 = 0; for (units) |unit| { for (unit.sources) |source| { if (std.mem.eql(u8, source.path, path)) count += 1; } } if (count != 1) { return fail( stderr, on.implementation, "source belongs to multiple implementation units: {s} ({s})", .{ path, owner_id }, ); } } pub fn validateImplementationObligations( obligations: []const model_types.VerificationObligation, unit: *const model_types.ImplementationUnit, stderr: *std.Io.Writer, ) !void { for (unit.realizes, 0..) |reference, index| { const obligation = model_types.findObligation(obligations, reference.id) orelse { return fail( stderr, on.implementation, "implementation references unknown obligation: {s}", .{reference.id}, ); }; if (reference.revision != obligation.revision) { return diag.staleObligation(stderr, reference, obligation.revision, unit.id); } for (unit.realizes[0..index]) |previous| { if (std.mem.eql(u8, previous.id, reference.id)) { return fail( stderr, on.implementation, "duplicate obligation on implementation: {s}", .{unit.id}, ); } } } } pub fn validateSourceInventories( allocator: std.mem.Allocator, io: std.Io, model: *const model_types.Graph, stderr: *std.Io.Writer, ) !void { try validateSourceInventory( allocator, io, model, profile.paths.implementation_inventory_root, .implementation, stderr, ); try validateSourceInventory( allocator, io, model, profile.paths.binding_inventory_root, .binding, stderr, ); } pub fn validateSourceInventory( allocator: std.mem.Allocator, io: std.Io, model: *const model_types.Graph, root: []const u8, domain: diag.FingerprintDomain, stderr: *std.Io.Writer, ) !void { const directory = try std.Io.Dir.cwd().openDir(io, root, .{ .iterate = true }); defer directory.close(io); var walker = try directory.walk(allocator); defer walker.deinit(); while (try walker.next(io)) |entry| { if (entry.kind != .file or !std.mem.endsWith(u8, entry.path, ".zig")) continue; const path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ root, entry.path }); defer allocator.free(path); const owned = switch (domain) { .implementation => implementationSourceOwned(model.implementations, path), .binding => bindingSourceOwned(model.test_bindings, path), .fixture => unreachable, }; if (!owned) { const context = switch (domain) { .implementation => on.unregistered_implementation, .binding => on.unregistered_binding, .fixture => unreachable, }; return fail(stderr, context, "source is in no registry: {s}", .{path}); } } } pub fn implementationSourceOwned( units: []const model_types.ImplementationUnit, path: []const u8, ) bool { for (units) |unit| { for (unit.sources) |source| { if (std.mem.eql(u8, source.path, path)) return true; } } return false; } pub fn bindingSourceOwned(bindings: []const model_types.TestBinding, path: []const u8) bool { for (bindings) |binding| { if (std.mem.eql(u8, binding.source, path)) return true; } return false; } pub fn validateOwner( allocator: std.mem.Allocator, io: std.Io, owner: []const u8, stderr: *std.Io.Writer, ) !void { const separator = std.mem.indexOfScalar(u8, owner, '#'); const path = if (separator) |index| owner[0..index] else owner; const anchor = if (separator) |index| owner[index + 1 ..] else ""; if (path.len == 0) return fail(stderr, on.document, "owner path is empty: {s}", .{owner}); try diag.validateRepositoryPath(io, path, stderr); if (anchor.len == 0) return; const source = std.Io.Dir.cwd().readFileAlloc( io, path, allocator, .limited(model_types.file_size_max), ) catch return fail(stderr, on.document, "cannot read owner: {s}", .{path}); defer allocator.free(source); if (!text.markdownHasAnchor(source, anchor)) { return fail(stderr, on.document, "missing owner anchor: {s}", .{owner}); } } test "requirement traceability rejects an unmapped invariant claim" { const requirement: model_types.Requirement = .{ .id = "REQ-EXAMPLE-01", .revision = 1, .status = .accepted, .label = "Example.", .owner = "spec/PRODUCT.md", .suites = &.{model_types.testSuite(1)}, .invariant_status = .mapped, .limitation_status = .unassessed, }; var output = std.Io.Writer.Allocating.init(std.testing.allocator); defer output.deinit(); try std.testing.expectError( error.InvalidSpecification, validateRequirementTraceability( std.testing.allocator, undefined, &.{}, &requirement, &output.writer, ), ); } test "requirement traceability requires references for recorded status" { const requirement: model_types.Requirement = .{ .id = "REQ-EXAMPLE-01", .revision = 1, .status = .accepted, .label = "Example.", .owner = "spec/PRODUCT.md", .suites = &.{model_types.testSuite(1)}, .platform_evidence_status = .recorded, .limitation_status = .unassessed, }; var output = std.Io.Writer.Allocating.init(std.testing.allocator); defer output.deinit(); try std.testing.expectError( error.InvalidSpecification, validateRequirementTraceability( std.testing.allocator, undefined, &.{}, &requirement, &output.writer, ), ); } test "handwritten product test declarations are rejected" { var output = std.Io.Writer.Allocating.init(std.testing.allocator); defer output.deinit(); try rejectProductTestDeclarations( std.testing.allocator, "const text = \"test \\\"not a declaration\\\"\";\n", "sources/example.zig", &output.writer, ); try std.testing.expectError( error.InvalidSpecification, rejectProductTestDeclarations( std.testing.allocator, "test \"unregistered\" {}\n", "sources/example.zig", &output.writer, ), ); try std.testing.expect(std.mem.indexOf(u8, output.written(), "TJSP-E250") != null); } test "verification obligations reject stale requirement revisions" { const requirements = [_]model_types.Requirement{.{ .id = "REQ-EXAMPLE-01", .revision = 2, .status = .accepted, .label = "Example.", .owner = "spec/PRODUCT.md", .suites = &.{model_types.testSuite(1)}, .limitation_status = .unassessed, }}; const groups = [_]model_types.RequirementGroup{.{ .title = "Example", .requirements = &requirements, }}; const obligation: model_types.VerificationObligation = .{ .id = "obligation-example", .revision = 1, .requirement = .{ .id = "REQ-EXAMPLE-01", .revision = 1 }, .kind = .required, .label = "Example.", .suites = &.{model_types.testSuite(1)}, }; const model: model_types.Graph = .{ .documents = &.{}, .concepts = &.{}, .groups = &groups, .test_specs = &.{}, .test_bindings = &.{}, .verification = &.{obligation}, .implementations = &.{}, }; var output = std.Io.Writer.Allocating.init(std.testing.allocator); defer output.deinit(); try std.testing.expectError( error.InvalidSpecification, validateObligationRequirement(&model, &obligation, &output.writer), ); } test "test scaffold is deterministic and cannot pass unfinished" { const claims = [_]model_types.VerificationClaim{ .{ .oracle = "INV-EXAMPLE", .kind = .required, .obligations = &.{.{ .id = "obligation-required", .revision = 1 }}, }, .{ .oracle = "NO-EXAMPLE", .kind = .forbidden, .obligations = &.{.{ .id = "obligation-forbidden", .revision = 1 }}, }, }; const test_specs = [_]model_types.TestSpec{.{ .id = "test-example", .revision = 1, .suite = model_types.testSuite(1), .fixture = .{ .id = "fixture-example", .revision = 1, .path = "fixture.md", .sha256 = "0000000000000000000000000000000000000000000000000000000000000000", }, .claims = &claims, .production_boundary = .state_transition, }}; var first = std.Io.Writer.Allocating.init(std.testing.allocator); defer first.deinit(); var second = std.Io.Writer.Allocating.init(std.testing.allocator); defer second.deinit(); try scaffold.renderFixtureScaffold(&test_specs[0], &first.writer); try scaffold.renderFixtureScaffold(&test_specs[0], &second.writer); try std.testing.expectEqualStrings(first.written(), second.written()); try std.testing.expect(std.mem.indexOf(u8, first.written(), "@compileError") != null); try std.testing.expect(std.mem.indexOf(u8, first.written(), "SkipZigTest") == null); try std.testing.expect(std.mem.indexOf(u8, first.written(), "TODO") == null); const stale_binding: model_types.TestBinding = .{ .test_spec = .{ .id = "test-example", .revision = 1 }, .fixture_revision = 2, .source = "bindings/example.zig", .source_sha256 = "0000000000000000000000000000000000000000000000000000000000000000", .module = "fixture_example", }; try std.testing.expectError( error.InvalidSpecification, validateTestBindingIdentity(&test_specs, &stale_binding, &first.writer), ); }