diff --git a/scripts/check_layer_contract.jl b/scripts/check_layer_contract.jl new file mode 100644 index 0000000..cc295d1 --- /dev/null +++ b/scripts/check_layer_contract.jl @@ -0,0 +1,127 @@ +#!/usr/bin/env julia +# +# Static audit for docs/src/developer/architecture_layers.md (layers A–C). +# Run from anywhere: +# julia scripts/check_layer_contract.jl [REPO_ROOT] +# +# Uses only Base — no --project required. + +const REPO_ROOT = isempty(ARGS) ? dirname(dirname(@__FILE__)) : abspath(ARGS[1]) + +const INCLUDE_IO_REGEX = r"include\s*\(\s*\"[^\"]*\bio/" + +function jl_files(dir::AbstractString) + list = String[] + isdir(dir) || return list + for (root, _, files) in walkdir(dir) + for f in files + endswith(f, ".jl") || continue + push!(list, joinpath(root, f)) + end + end + sort!(list) +end + +function code_fragment(line::AbstractString)::SubString{String} + # Ignore trailing `# …` so notes do not false-positive the auditors. + parts = split(line, '#'; limit = 2) + return rstrip(parts[1]) +end + +function skip_line(line::AbstractString)::Bool + s = strip(line) + isempty(s) && return true + startswith(s, '#') && return true + return false +end + +const DOMAIN_FORBIDDEN_SUBSTRINGS = [ + "GmshReader", + "read_gmsh_mesh", + "GmshMesh", + "JuliaFEM.Legacy", + "KernelAbstractions", + "CUDABackend", + "MetalBackend", + "AMDGPUBackend", + "oneAPIBackend", +] + +const FOUNDATION_EXTRA_SUBSTRINGS = [ + "DOFHandler", + "create_structured_box_mesh", +] + +function violations_for_line(line::AbstractString, extra_substrings)::Vector{String} + hits = String[] + if occursin(INCLUDE_IO_REGEX, line) + push!(hits, "include(\"…io/…\")") + end + for tok in DOMAIN_FORBIDDEN_SUBSTRINGS + if occursin(tok, line) + push!(hits, tok) + end + end + for tok in extra_substrings + if occursin(tok, line) + push!(hits, tok) + end + end + return hits +end + +function audit_tree(label::AbstractString, dir::AbstractString, extra_substrings)::Bool + ok = true + rel = relpath(dir, REPO_ROOT) + for path in jl_files(dir) + lineno = 0 + for line in eachline(path) + lineno += 1 + skip_line(line) && continue + frag = code_fragment(line) + isempty(strip(frag)) && continue + hits = violations_for_line(frag, extra_substrings) + isempty(hits) && continue + ok = false + relfile = relpath(path, REPO_ROOT) + println(stderr, "$relfile:$lineno: [layer contract] $(join(sort!(unique(hits)), ", "))") + end + end + ok || println(stderr, "($label) violations under $rel") + return ok +end + +function main()::Int + src = joinpath(REPO_ROOT, "src") + isdir(src) || error("expected src/ under $REPO_ROOT") + + println("Layer contract audit (see docs/src/developer/architecture_layers.md)") + println("Repository: ", REPO_ROOT) + + all_ok = true + + domains = joinpath(src, "domains") + all_ok &= audit_tree("Layer C (domains)", domains, String[]) + + foundation_dirs = [ + joinpath(src, "topology"), + joinpath(src, "quadrature"), + joinpath(src, "geometry"), + joinpath(src, "basis"), + joinpath(src, "sparse"), + ] + for d in foundation_dirs + all_ok &= audit_tree("Layer A (foundation)", d, FOUNDATION_EXTRA_SUBSTRINGS) + end + + if all_ok + println("OK — no forbidden coupling patterns found.") + return 0 + else + println(stderr, "") + println(stderr, "FAILED — fix imports/includes or move code to an outer layer.") + return 1 + end +end + +exit(main())