grep somebody added to CI in 2022.
.aspect/policy.axl is 222 lines, and three places in it carry the lesson: the RULES list, which is the data; the one-line _OFFENDERS query template below it; and _check, which evaluates one rule and attributes the offending edge. The two record() declarations and the two reporting functions are worth a skim and nothing more.
Rules as data
record() and field() are AXL, not Bazel Starlark — one of the places the dialects differ. The rules themselves are a list at the top of the file, so adding one is a list entry and no code:
rationale is printed when the rule fires, so the person who tripped it learns the intent rather than just the verdict.
Run it
bazel query each, under a second.
Break it
lib/validate starts logging. It compiles — bazel build //lib/validate:validate is perfectly happy. The compiler has no opinion about your architecture.
The query
//... and not targets as the universe. It reads like it matters and it doesn’t — the two are equivalent here, which is worth knowing because the reasoning is a trap people fall into. rdeps(u, x) searches the reverse dependencies of x within the transitive closure of u, not within the literal patterns you wrote. So a universe of just targets still walks everything the layer depends on, including an intermediate outside the layer. Try it: give lib/validate a dependency on lib/retry, which reaches lib/log, and both spellings return the same two targets even though lib/retry is in no rule’s targets. The intersect is what narrows the answer, so //... is the plainest universe to write.
except (deny) drops the seed set, which rdeps always returns. For every rule in this file it changes nothing, because no rule’s targets overlaps its deny — targets intersect deny is empty, so the intersect has already removed them. It earns its place the moment you write a rule where the two patterns do overlap, which is easy to do by accident:
except (deny), //lib/log:log reports itself as its own violation.
Typed results, again
Naming the offending edge needs no text parsing:
Intersecting
.rule_input with the denied set identifies the edge directly. Only an indirect violation needs a second somepath query.
The queries run with --noimplicit_deps, which matters twice: a toolchain edge isn’t an architecture decision anybody made, and without it .rule_input comes back with ten labels instead of four — most of them @rules_go and @bazel_tools noise.
Keeping the top level tidy
changed, impact and policy have all landed at the top level, beside build, test and everything the CLI already ships. That does not scale: a repository with twenty commands of its own buries the ones people use daily.
group nests a task under a parent. Had policy.axl declared the task this way —
aspect arch check rather than aspect check. The variable name is still the last word; the group is the path to it.
The CLI already does this with its own commands, which is what aspect --help shows under Task Groups:
aspect worktree on its own lists what is in that group. Seven of the CLI’s own groups are built this way, so the top level stays a page of categories rather than a wall of thirty-three verbs. Groups nest up to five levels — group = ["arch", "deps"] gives aspect arch deps check.
This one is a read, not an exercise. The task in
.aspect/policy.axl is named policy with no group, and the next section still asks you to run aspect policy — so leave it alone for now. Renaming it to check under arch would work, and would also make aspect policy stop existing.Your turn
Open.aspect/policy.axl and append a fourth rule:
aspect policy again. It fails — and the output tells you why, which is a genuine fact about this repository you have not been told yet.
Next: stop asking Bazel questions, and start watching it work.
