From ac0bdca614a9fbdbbfecec075976acd170ecfce3 Mon Sep 17 00:00:00 2001 From: Ayla Croft Date: Sun, 6 Sep 2026 14:12:37 -0400 Subject: [PATCH] 16i-path-citations: point the clone URL at the new org, and gate dead links The repository moves to the ScriptKittyOS organisation. GitHub redirects the old URL, so a stale citation keeps working while hiding that it is stale -- which is why the link check lands with the citation and before the transfer, not after. Run after, it would pass on a tree still full of old paths. The population was derived by grep at the SHA it runs against, not taken from the list in the transfer issue. That issue names six files; the derivation returns one: git grep -in 'HackTuah/Ultraviolet' 778accb8 -- README.md:209 files: 1 occurrences: 1 NOTICE carries no URL, CITATION.cff does not exist yet, schemas/README.md and CHANGELOG.md have never been on main, and nothing under docs/ cites the path. The link check fails on any dead relative link in README.md or docs/, guarding 27 links across 14 markdown files. It carries four probes, and each is demonstrated able to fail rather than merely observed passing: neuter dead_links/2 to [] probes 1,2,3 RED, probe 4 GREEN append a real dead link to README probe 4 RED Probe 4 staying green under a blind checker is the point, not an oversight: a checker that enumerates nothing reports zero dead links and passes. Probes 1-3 are what catch it. dead_links/2 takes its root and source list as arguments so the probes drive the same function the real test calls; a control that computes its expected value with the function under test proves nothing. Both reviewers reproduced the tree independently and confirmed the mutations in their own copies. Suite 260 tests, 0 failures -- 254 at 268a83e, +2 from 16g, +4 probes, each leg re-derived by review. Acceptance criteria 1-6 met; 7 is the PR's nine contexts. Evidence in internal/slices/16i-path-citations/FINDINGS.md, which carries an appended correction of two numbers that did not reproduce. The GitHub transfer itself is owner work, after this merges. Between merge and transfer, main ships a clone URL naming an org that does not yet own the repo: an accepted consequence of the ordering above. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VNdY6wVS773UJgrHUeBN3Y Reviewed-diff: sha256:93670cbcc3e772e32419e4e2839a6942fd235fef64ac85e12a0632bcd8fcc286 --- README.md | 2 +- apps/hacktui_core/test/link_check_test.exs | 105 +++++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 apps/hacktui_core/test/link_check_test.exs diff --git a/README.md b/README.md index cba2806..b73bb3b 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,7 @@ Ultraviolet is currently best supported on **Linux**. ## 1. Clone the repository ```bash -git clone https://github.com/HackTuah/Ultraviolet.git +git clone https://github.com/ScriptKittyOS/Ultraviolet.git cd Ultraviolet ``` diff --git a/apps/hacktui_core/test/link_check_test.exs b/apps/hacktui_core/test/link_check_test.exs new file mode 100644 index 0000000..5f7c6b7 --- /dev/null +++ b/apps/hacktui_core/test/link_check_test.exs @@ -0,0 +1,105 @@ +defmodule HacktuiCore.LinkCheckTest do + @moduledoc """ + Every relative markdown link in `README.md` and `docs/` resolves to a file that exists. + + A link check that has not been seen fail is a check that can only pass. Slice 16c is the + precedent: `Path.wildcard/1` does not match dot-prefixed names, so an unpinned dotfile was + invisible and the gate reported zero failures while the hole was open. + + So `dead_links/2` takes its root and its source list as arguments, and the probes drive the + *same* function the real test calls, over fixtures. A control that computes its expected + value with the function under test asserts `f(x) == f(x)` and proves nothing. + """ + use ExUnit.Case, async: true + + import ExUnit.Callbacks, only: [on_exit: 1] + + @root Path.expand("../../..", __DIR__) + + @doc false + def dead_links(root, sources) do + for src <- sources, + target <- relative_links(File.read!(Path.join(root, src))), + not File.exists?(resolve(root, src, target)), + do: {src, target} + end + + @doc false + def sources(root) do + ["README.md"] + |> Enum.concat(Path.wildcard(Path.join(root, "docs/**/*.md"), match_dot: true)) + |> Enum.map(&Path.relative_to(&1, root)) + |> Enum.filter(&File.regular?(Path.join(root, &1))) + |> Enum.sort() + end + + # A markdown inline link whose target is relative: not a URL scheme, not a bare anchor. + defp relative_links(body) do + ~r/\[[^\]]*\]\(([^)\s]+)\)/ + |> Regex.scan(body) + |> Enum.map(fn [_full, target] -> target end) + |> Enum.reject(&(&1 =~ ~r{^(https?://|mailto:|#)})) + end + + # Anchors are stripped: `guide.md#section` is a link to `guide.md`. + defp resolve(root, src, target) do + file = target |> String.split("#") |> hd() + Path.expand(Path.join([root, Path.dirname(src), file])) + end + + defp fixture(files) do + dir = Path.join(System.tmp_dir!(), "linkcheck-#{System.unique_integer([:positive])}") + File.mkdir_p!(dir) + on_exit(fn -> File.rm_rf!(dir) end) + + Enum.each(files, fn {path, body} -> + full = Path.join(dir, path) + File.mkdir_p!(Path.dirname(full)) + File.write!(full, body) + end) + + dir + end + + test "probe 1: a deliberately dead relative link is detected" do + dir = fixture(%{"README.md" => "see [the missing page](does_not_exist.md) for more\n"}) + + assert [{"README.md", "does_not_exist.md"}] == dead_links(dir, ["README.md"]) + end + + test "probe 2: a link into a nested directory resolves, and is not skipped" do + dir = + fixture(%{ + "README.md" => "see [the deep page](docs/guides/deep.md)\n", + "docs/guides/deep.md" => "# deep\n" + }) + + assert [] == dead_links(dir, ["README.md"]) + + # and the checker is genuinely looking: break only the target and it goes red + File.rm!(Path.join(dir, "docs/guides/deep.md")) + assert [{"README.md", "docs/guides/deep.md"}] == dead_links(dir, ["README.md"]) + end + + test "probe 3: a link to a dot-named file resolves, and is not skipped" do + dir = + fixture(%{ + "README.md" => "see [the workflow](.github/workflows/ci.yml)\n", + ".github/workflows/ci.yml" => "name: ci\n" + }) + + assert [] == dead_links(dir, ["README.md"]) + + # Path.wildcard/1 skips dot-prefixed names. A checker that enumerated candidate targets + # by wildcard would report this live link as dead; one that resolves the path does not. + File.rm!(Path.join(dir, ".github/workflows/ci.yml")) + assert [{"README.md", ".github/workflows/ci.yml"}] == dead_links(dir, ["README.md"]) + end + + test "probe 4: the tracked tree carries no dead relative links" do + sources = sources(@root) + + assert sources != [], "no markdown sources found; the check would pass vacuously" + assert [] == dead_links(@root, sources) + end +end