Conversation
Adds a validator that checks usage-rules-managed files for broken references: - Module, Module.function/arity, and :erlang.module/arity references in code spans and elixir/iex code blocks, resolved against compiled beams of the project and its deps with a static lib/ source scan fallback - Function checks cover exports, macros, and behaviour callbacks - mix task.name references, resolved against dep task modules and project aliases - Relative markdown links, resolved against the containing file By default only rules-managed files are checked (the composed :file and skills marked managed-by: usage-rules); --all validates every project markdown file. Supports --format json, --strict, and exits nonzero on violations for CI use.
|
|
Resolve references with ex_doc's own machinery instead of a custom implementation, per review feedback: - inline code spans are extracted with ex_doc's markdown pipeline (ExDoc.Markdown + EarmarkParser), so span detection matches what a docs build would autolink - candidates (spans, elixir/iex fence tokens, mix task commands) are parsed and resolved with ExDoc.Autolink.url/3 in strict custom-link mode, backed by ExDoc.Refs; ex_doc's own warning messages become violations, reported with file and line - references now validate exactly like docs-build references: only documented API passes, @doc false/@moduledoc false targets are flagged, callbacks require the c: prefix, and m:/t: references are supported - validation requires ex_doc to be compiled and fails with an actionable error otherwise; usage_rules still compiles in projects without ex_doc - relative markdown link checks are unchanged mix task name mentions in spans/fences are still validated including command lines like `mix task --flags` (only the task name), and bare atoms, all-caps document basenames, and bare lowercase function mentions are skipped as before to avoid false positives.
|
Done — validation now delegates to ex_doc instead of rolling our own resolution (d53e059). How it works
When ex_doc isn't compiled ex_doc is loaded via Kept identical: file scope, flags ( Two deliberate behavior narrowings worth noting: references without explicit arity ( |
| * `*.md` files under skills managed by usage-rules (skills whose | ||
| `SKILL.md` contains `managed-by: usage-rules`) | ||
|
|
||
| Use `--all` to validate every markdown file in the project instead |
There was a problem hiding this comment.
Don't think we really need a --all
| location = Keyword.get(skills_config, :location, ".claude/skills") | ||
|
|
||
| Path.wildcard(Path.join(location, "*/SKILL.md")) | ||
| |> Enum.filter(&(File.read!(&1) =~ "managed-by: usage-rules")) |
There was a problem hiding this comment.
Should we do a simpler String.contains? here?
| defp ex_doc_config(context, path) do | ||
| # Built with struct/2 (not struct syntax) so this module still compiles | ||
| # in projects that do not have ex_doc compiled. | ||
| struct(ExDoc.Autolink, |
There was a problem hiding this comment.
We can make this compile conditionally on ExDoc being available
|
I don't think this really uses ex_doc the way that I had hoped. ExDoc already emits warnings, and I don't see a case where we would need a json output, so ideally in my mind we can say something like |
|
There is also a world where maybe this should be an enhancement made to ex_doc actually? Like a |
Drive each file through ExDoc.Extras.build + ExDoc.Formatter.autolink and surface ex_doc's own warnings (file:line, native format), exiting nonzero via ex_doc's warned flag. This drops our own fence state machine, candidate extraction, violation mapping, and the JSON report (--all, --strict and --format are gone; explicit file paths are accepted instead). Two scoped complements cover what docs builds stay silent about, pushing spans through ex_doc's own resolution and re-emitting its warning text: mix task mentions and bare undefined dotted module mentions in code spans.
|
Great call — way simpler like this, but it misses a few checks like bare undefined module spans (easy to special-case, and I've done exactly that here). Proposed the ex_doc home for the general feature here: elixir-lang/ex_doc#2272. Suggest sticking with this thinned prototype in the interim. |
Gate everything that touches ex_doc modules behind Code.ensure_loaded?(ExDoc) so usage_rules still compiles cleanly in projects without ex_doc compiled; validate/1 there fails fast with the existing actionable error. Also use String.contains?/2 for the managed-by marker check.
Drop the custom span-complement machinery: mix task mentions and bare undefined module mentions in plain code spans now stay silent, matching ex_doc's behavior inside moduledocs (per elixir-lang/ex_doc#2272). The validator is now thin glue — feed files through ExDoc.Extras.build + ExDoc.Formatter.autolink and surface ex_doc's own warnings — with file scope discovery staying in the mix task. Reword docs mentions of hidden ex_doc functions so mix docs builds without warnings, and document the zero-dependency EXTRA_DOCS docs-extras alternative in the README.
|
Pushed a995915 + b894b5b addressing your comments and taking the "lean on ex_doc" direction to its conclusion:
Relative-filesystem-link checks remain, since ex_doc's extras model doesn't cover those. Verification: 143 tests 0 failures · The README also gained a "zero-dependency alternative" section documenting the |
Implements
mix usage_rules.validatefrom #87.What it does
Validates references in usage-rules-managed markdown so rotting refs break CI instead of quietly mis-teaching agents:
Module,Module.fun/arity,:erl_mod.fun/1) extracted from inline code spans andelixir/iexfences (fence-state aware, strings/comments/URLs stripped; all-caps doc basenames like README/SKILL not treated as modules). Resolution: compiled beams (project + deps) + staticdefmodulescan of lib/umbrella/deps sources for uncompiled projects (warnings, not errors). Function checks viamodule_info(:exports)+__info__(:macros)+behaviour_info(:callbacks)— arity mismatches report available arities.mix task.namefrom spans + shell fences), including project aliases.:file+*.mdunder skills whose SKILL.md carriesmanaged-by: usage-rules.--allescapes scope (excludes deps/_build/doc/hidden).--strict,--format human|json, nonzero exit on violations.Verification
--allflags README refs.Open to reshaping API/scoping to fit project direction (e.g. wiring into
syncas a post-step instead of a separate task).References #87.