From fbb8bc40e0d03ffb8d2d9a0cefb4b7c62af2663c Mon Sep 17 00:00:00 2001 From: posidoni <89808617+posidoni@users.noreply.github.com> Date: Sun, 19 Jul 2026 18:29:42 +0400 Subject: [PATCH] fix(docs): repair 4 links broken by #18, add a doc-link gate Collapsing seven skills into one router deleted skills/{nushell,shebang, streams,zsh}/ but left reference/{nushell,shebang,streams,zsh}.md linking to them. All five gates stayed green through the merge because none of them reads a link -- in a repo whose product is its documentation, that is a real hole. The four now point at skills/shell/. tools/check-doc-links.sh resolves every relative Markdown link against its own file's directory and fails on any miss. External URLs are deliberately not fetched: needs network, slow, and turns someone else's outage into a red build. Verified both ways -- 83 links resolve, and an injected bad link fails the run. Wired into task ci, a standalone task doc-links, and lefthook pre-commit on *.md. The hook self-scans the whole tree rather than only staged files, because moving one file breaks links in files the commit never touches -- exactly how this shipped. Co-Authored-By: Claude Opus 4.8 --- Taskfile.yml | 6 +++++ lefthook.yml | 8 +++++++ reference/nushell.md | 2 +- reference/shebang.md | 2 +- reference/streams.md | 2 +- reference/zsh.md | 2 +- tools/check-doc-links.sh | 51 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 69 insertions(+), 4 deletions(-) create mode 100755 tools/check-doc-links.sh diff --git a/Taskfile.yml b/Taskfile.yml index 2738cae..1a31370 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -56,6 +56,11 @@ tasks: cmds: - ./tools/check-yaml-schemas.sh + doc-links: + desc: Check every relative Markdown link resolves + cmds: + - ./tools/check-doc-links.sh + ai-integrations: desc: Validate Codex, ChatGPT, and plugin discovery surfaces cmds: @@ -80,5 +85,6 @@ tasks: - task: nushell - task: nushell-demo - task: yaml-schemas + - task: doc-links - task: ai-integrations - task: test diff --git a/lefthook.yml b/lefthook.yml index b5b1407..aaf83ea 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -45,6 +45,14 @@ pre-commit: glob: "*.{yml,yaml,cff}" run: ./tools/check-yaml-schemas.sh + # Docs are the product here, so a dangling relative link is a defect. Runs + # on any markdown change; self-scans the whole tree because moving one file + # breaks links in files the commit never touched -- which is exactly how + # #18 shipped four of them. + doc-links: + glob: "*.md" + run: ./tools/check-doc-links.sh + # Codex/ChatGPT discovery surfaces. This is cheap and self-scans, so # it runs on every pre-commit to catch extensionless repo-scope skill links. ai-integrations: diff --git a/reference/nushell.md b/reference/nushell.md index 396bc4a..db8590a 100644 --- a/reference/nushell.md +++ b/reference/nushell.md @@ -234,7 +234,7 @@ the script — ideal for CI and pre-commit. This repo's `task nushell` ([nushell/nushell.github.io](https://github.com/nushell/nushell.github.io)); rules here are summarized, with links back to each source page. -See [`skills/nushell`](../skills/nushell/) for the agent-facing summary and +See [`skills/shell`](../skills/shell/) for the agent-facing summary and [`examples/nushell/`](../examples/nushell/) for parse-clean fragments. ## Invoking `nu` from an agent or a script diff --git a/reference/shebang.md b/reference/shebang.md index a58debb..da093fb 100644 --- a/reference/shebang.md +++ b/reference/shebang.md @@ -135,4 +135,4 @@ this guide exists for. - Linux shebang doc fix, commit `f718c9fa87be` (Alan Urmancheev, [@alurm](https://github.com/alurm)) — -See [`skills/shebang`](../skills/shebang/) and [`examples/shebang/`](../examples/shebang/). +See [`skills/shell`](../skills/shell/) and [`examples/shebang/`](../examples/shebang/). diff --git a/reference/streams.md b/reference/streams.md index c550b2c..688b1ad 100644 --- a/reference/streams.md +++ b/reference/streams.md @@ -137,4 +137,4 @@ assumes a green lint means the streams are right. - Greg's Wiki — BashFAQ/001, BashPitfalls — -See [`skills/streams`](../skills/streams/) and [`examples/streams/`](../examples/streams/). +See [`skills/shell`](../skills/shell/) and [`examples/streams/`](../examples/streams/). diff --git a/reference/zsh.md b/reference/zsh.md index 868a02a..5e9c5e9 100644 --- a/reference/zsh.md +++ b/reference/zsh.md @@ -178,4 +178,4 @@ Three independent reasons `"${TTY}"` is correct and `$(tty)` is not: [Powerlevel10k](https://github.com/romkatv/powerlevel10k), [gitstatus](https://github.com/romkatv/gitstatus) -See [`skills/zsh`](../skills/zsh/) for the summary. +See [`skills/shell`](../skills/shell/) for the router summary. diff --git a/tools/check-doc-links.sh b/tools/check-doc-links.sh new file mode 100755 index 0000000..9f5b705 --- /dev/null +++ b/tools/check-doc-links.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# +# check-doc-links.sh — every relative Markdown link must resolve. +# +# Why this exists: collapsing seven skills into one router (#18) deleted +# skills/{nushell,shebang,streams,zsh}/ but left four reference/*.md files +# linking to them. Every other gate stayed green -- lint, tests, examples, +# schemas, ai-integrations -- because none of them reads a link. In a repo +# whose product IS its documentation, a dangling link is a defect. +# +# Only relative targets are checked. External URLs are deliberately not +# fetched: that needs network, is slow, and turns someone else's outage into +# a red build. +# +# One grep+sed per file is a deliberate choice over a single clever pass. A +# rewrite that hoisted both out of the loop to avoid the subprocesses silently +# mangled its own field splitting and reported every link as broken. Two +# subprocesses per markdown file is cheap; a checker that lies is not. +set -euo pipefail + +status=0 +checked=0 + +while IFS= read -r file; do + dir=$(dirname "${file}") + + while IFS= read -r target; do + [[ -z ${target} ]] && continue + case "${target}" in + http://* | https://* | mailto:* | '#'*) continue ;; + *) ;; + esac + + target=${target%%#*} + [[ -z ${target} ]] && continue + + checked=$((checked + 1)) + if [[ ! -e "${dir}/${target}" ]]; then + printf 'broken link: %s -> %s\n' "${file}" "${target}" >&2 + status=1 + fi + # `[text](target)` with no spaces or parens inside the target, which is + # what every link in this repo looks like. + done < <(grep -o '\[[^][]*\]([^() ]*)' "${file}" | sed 's/.*(\(.*\))/\1/') +done < <(git ls-files '*.md') + +if [[ ${status} -eq 0 ]]; then + printf 'doc-links: OK (%s relative links resolve)\n' "${checked}" +fi + +exit "${status}"