repositories / termux-janitor
termux-janitor
Interactive cleanup assistant for Termux: transparent, safe, confirmed disk reclamation.
owned by admin
tools/spec-engine/src/validate.zig
Rawconst 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),
);
}