feat(scripts): add check_layer_contract.jl

scripts/check_layer_contract.jl | 127 ++++++++++++++++++++++++++++++++++++++++  1 file changed, 127 insertions(+)
This commit is contained in:
Jukka Aho
2026-05-09 16:30:28 +03:00
parent 56dc227499
commit d647a565f7
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env julia
#
# Static audit for docs/src/developer/architecture_layers.md (layers AC).
# 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())