From 95475492a11f11aa2161f4f49c510dabd9b2fbfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Guilhermo=20de=20Barros=20Val=C3=A9rio?= Date: Thu, 15 Oct 2026 09:54:49 +0200 Subject: [PATCH] docs-lint rule g: an image link outside docs/ The umbrella publishes README.md and docs/** only, so its link rewrite refuses an image that resolves elsewhere. Rule g reports it before merge, with the umbrella's message. Closes #126 Tested: docs_lint.py --self-test (image_* cases red then green); docs-lint on marola-ml add187c (one (g) at README.md:3), and clean on marola-ml 8100330, marola-app, site, corpus, oods, skills mains and the umbrella; devkit-quality.sh rc=0 Co-Authored-By: Claude --- CHANGELOG.md | 2 ++ README.md | 3 ++- scripts/docs_lint.py | 43 ++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d35580..1aefd08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ marketplace `ref` at once. - `docs-lint` rule `f` checks the ADR shape the umbrella's docs build requires: `docs/adr/NNNN-.md`, an `# ADR-NNNN: ` heading with the file's number, a `| **Status** |` row, and no hand-written `index.md` or `README.md`. A consumer bump can newly fail its gate (#124). +- `docs-lint` rule `g` fails an image link (`![…](…)`, an HTML `src=`, or a target ending in an image suffix) + that stays in the repo and resolves outside `docs/`, as the umbrella's docs build does (#126). ## v0.11.1 — 2026-10-15 diff --git a/README.md b/README.md index 2810c10..ade34e1 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,8 @@ non-fork repo of an org (`--include-forks` adds forks). `just rulesets-check`/`j **Docs lint** — `docs-lint [repo-root]` runs MIP-0074 §7's rules over `README.md` and `docs/**/*.md`, skipping dated records (`docs/MIPs/**`, `docs/benchmarks/**`, `SPLIT.md`), and checks each -`docs/adr/*.md` for the umbrella's ADR shape (rule `f`); a repo +`docs/adr/*.md` for the umbrella's ADR shape (rule `f`); rule `g` fails an image link that resolves +outside `docs/`, as the umbrella's docs build does. A repo adds it to its quality recipe once its README is the landing. **The umbrella** — tools that read MIPs or `.tasks.md` look in the repo, then `../` (an umbrella diff --git a/scripts/docs_lint.py b/scripts/docs_lint.py index 64e39a1..89bbef3 100755 --- a/scripts/docs_lint.py +++ b/scripts/docs_lint.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Stale-content check over README.md and docs/**/*.md (MIP-0074 §7, rules a–f). +"""Stale-content check over README.md and docs/**/*.md (MIP-0074 §7, rules a–g). docs-lint [repo-root] docs-lint --self-test @@ -35,8 +35,9 @@ LINK_TARGETS = [ re.compile(r"!?\[[^\]]*\]\(\s*]+)"), re.compile(r"^\s{0,3}\[[^\]]+\]:\s*]+)"), - re.compile(r"""\b(?:href|src)\s*=\s*["']([^"']+)["']"""), + re.compile(r"""(?/docs/ images pass its build but fail here. + image = ( + m.group(0).startswith("!") + or m.group(0).lower().startswith("src") + or Path(resolved).suffix.lower() in IMAGE_SUFFIXES + ) + if image and resolved != "docs" and not resolved.startswith("docs/"): + msg = f"{m.group(1)}: an image outside docs/; move it under docs/" + out.append((start + k, "g", msg)) return out @@ -242,6 +255,24 @@ def lint(root: Path) -> list[tuple[str, int, str, str]]: {"docs/x.md": "[ok](../README.md) and [up](../../marola-app/README.md)\n", "README.md": ""}, {"e"}, ), + ("image_md_outside_docs_fails", {"README.md": "![a](drawings/x.svg)\n"}, {"g"}), + ("image_src_outside_docs_fails", {"README.md": '\n'}, {"g"}), + ("image_suffix_outside_docs_fails", {"README.md": "[a](assets/x.png)\n"}, {"g"}), + ("image_refdef_suffix_outside_docs_fails", {"README.md": "[a]: assets/x.PNG\n"}, {"g"}), + ("image_query_outside_docs_fails", {"README.md": "![a](drawings/x.svg?raw=1#f)\n"}, {"g"}), + ("image_under_docs_from_readme_ok", {"README.md": "![a](docs/img/x.svg)\n"}, set()), + ("image_under_docs_ok", {"docs/page.md": "![a](img/x.svg)\n"}, set()), + ("image_url_ok", {"README.md": "![a](https://example.com/x.svg)\n"}, set()), + ("image_in_fence_ok", {"README.md": "```md\n![a](drawings/x.svg)\n```\n"}, set()), + ("image_in_code_span_ok", {"README.md": "Write `![a](drawings/x.svg)` to embed.\n"}, set()), + ("link_to_non_image_outside_docs_ok", {"README.md": "[a](LICENSE)\n"}, set()), + ("image_upper_src_outside_docs_fails", {"README.md": '\n'}, {"g"}), + ( + "image_data_src_ignored_ok", + {"README.md": '\n'}, + set(), + ), + ("image_leaving_repo_is_e_only", {"docs/x.md": "![a](../../x.png)\n"}, {"e"}), ("adr_good_ok", {"docs/adr/0001-x.md": ADR_OK}, set()), ( "adr_space_heading_fails", @@ -309,6 +340,12 @@ def ok(name: str, got, want) -> None: {"not NNNN-.md", "hand-written; prepare-docs generates adr/index.md"}, ) + ok( + "image_message_matches_umbrella", + {m for _, _, _, m in _findings({"README.md": "![a](drawings/x.svg)\n"})}, + {"drawings/x.svg: an image outside docs/; move it under docs/"}, + ) + if fails: print(f"docs-lint self-test: {fails} failure(s)", file=sys.stderr) return 1