From d25fb2117d7b4cbda2c2a90a784c6b98362b6453 Mon Sep 17 00:00:00 2001 From: Mike Stankavich Date: Mon, 31 Aug 2026 07:24:58 -0500 Subject: [PATCH] chore: give CONTRIBUTING.md a consumer TRA-1219 item 3. Items 1 and 2 fixed the file; nothing stopped it rotting again. It rotted in the first place because no gate, no session and no CI read it -- platform's README stayed correct while CONTRIBUTING.md three directories away did not. Same repo, same vintage, different consumers. Checks two claims, both chosen because they are the ones that actually broke and because neither can fire on a correct file: 1. every `just` recipe the doc names still exists, root and workspace 2. every repo path it names is TRACKED Tracked, not merely present. `existsSync` passes on a file that exists only on the author's machine, which is a false green for exactly the audience a contributor guide is written for. `.env` and `.env.local` are named in the doc, exist locally and are correctly absent from a fresh clone; the tracked test handles them without a skip list. Hung off `lint` rather than given its own job, so it runs inside the existing `lint-test` required check instead of adding a context the main-branch-protection ruleset would have to be taught -- same reasoning as the changelog gate. Deliberately NOT checked: pnpm script names. The root package.json has no scripts, so that rule would have nothing to catch here. A rule with nothing to catch is unexercised surface that breaks unnoticed. The path rule requires the first segment to be a real top-level entry. trakrf/docs rejected that rule for its repo and was right to: it has no vocabulary that needs it. Platform does -- `/api/v1/`, `ghcr.io/...`, `${assetId}`, and a frontend-relative `services/` all appear in this file and must be skipped. The rule set belongs to the repo, not to the idea. Verified by deliberate breakage, because a check that has not been shown to fail has not been shown to work. Five injected faults, five caught: the original `backend/.env.example` bug, a dead root recipe, a dead workspace recipe, a moved path, and a file present locally but untracked. Two bugs found that way, both of which would have shipped a check that passed on the bug it was built for: - `.example` was outside the extension whitelist, so the historical `cp backend/.env.example` fault was invisible on the first run - the no-workspace case emitted a leading empty field, and `read` strips leading IFS whitespace -- with either a space or a tab delimiter the recipe name landed in $ws and $name came back empty, which the -z guard then skipped. Every single-word recipe was silently unchecked while the workspace form kept working. Now an explicit "-" sentinel. `just validate` green. Refs: TRA-1219 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01W7p2hFrE6ZFWS91XvyFQcV --- justfile | 9 +- scripts/assert-contributing-paths.sh | 128 +++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100755 scripts/assert-contributing-paths.sh diff --git a/justfile b/justfile index 15d18fa6..fc0ccce0 100644 --- a/justfile +++ b/justfile @@ -48,7 +48,14 @@ alias be := backend # ============================================================================ # Run checks across all workspaces -lint: (frontend "lint") (backend "lint") (cli "lint") +# TRA-1219 item 3 — CONTRIBUTING.md had no consumer and rotted silently: every +# command in its quickstart was dead. A dependency of `lint` rather than its own +# job, so it runs inside the existing `lint-test` required check instead of +# adding a context the branch-protection ruleset would have to be taught. +check-contributing: + @./scripts/assert-contributing-paths.sh + +lint: check-contributing (frontend "lint") (backend "lint") (cli "lint") test: test-ops test-release-guards test-db-init test-env-drift test-bootstrap (frontend "test") (backend "test") (cli "test") diff --git a/scripts/assert-contributing-paths.sh b/scripts/assert-contributing-paths.sh new file mode 100755 index 00000000..161264a6 --- /dev/null +++ b/scripts/assert-contributing-paths.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# TRA-1219 item 3 — CONTRIBUTING.md had no consumer, so it rotted silently. +# +# Every command in its quickstart was dead: two .env.example files that never +# existed, `go run cmd/migrate/main.go`, a tests/ directory the repo does not +# have. Nothing caught it because nothing read the file. This is what reads it. +# +# Two claims are checked, both chosen because they are the ones that actually +# broke and because neither can fire on a correct file: +# +# 1. every `just` recipe the doc names still exists +# 2. every repo path it names is TRACKED — not merely present. `existsSync` +# would pass on a file that exists only on the author's machine, which is +# a false green for exactly the audience this doc is written for. +# +# Deliberately NOT checked: pnpm script names. The root package.json has no +# scripts, so that rule would have nothing to catch here, and a rule with +# nothing to catch is unexercised surface that breaks unnoticed. +set -euo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +doc="${repo_root}/CONTRIBUTING.md" +problems=() + +# --- 1. just recipes -------------------------------------------------------- +# `just ` and `just `. Workspace recipes live in +# that workspace's justfile, so ask the right one. +recipes=$(just --list 2>/dev/null | tail -n +2 | awk '{print $1}') +workspaces="frontend backend cli database fe be db" + +while IFS='|' read -r ws name; do + [[ -z "${name}" ]] && continue + if [[ "${ws}" != "-" ]]; then + if just "${ws}" --list 2>/dev/null | tail -n +2 | awk '{print $1}' | grep -qx "${name}"; then + continue + fi + problems+=("just recipe \`just ${ws} ${name}\` is named but does not exist") + else + grep -qx "${name}" <<< "${recipes}" || + problems+=("just recipe \`just ${name}\` is named but does not exist") + fi +done < <( + grep -oE '\bjust [a-z][a-z0-9_-]*( [a-z][a-z0-9_-]*)?' "${doc}" | + sed 's/^just //' | + sort -u | + while read -r first second; do + if [[ " ${workspaces} " == *" ${first} "* && -n "${second}" ]]; then + printf '%s|%s\n' "${first}" "${second}" + else + # "-" is an explicit no-workspace sentinel, not an empty field. + # An empty leading field does not survive `read`: leading IFS + # WHITESPACE is stripped, so both a space and a tab delimiter shift + # the recipe name into ${ws} and leave ${name} empty, which the + # -z guard then skips. That silently disabled the check for every + # single-word recipe while the workspace form kept working — so it + # passed on a doc naming a recipe that did not exist. + printf -- '-|%s\n' "${first}" + fi + done +) + +# --- 2. repo paths ---------------------------------------------------------- +# Inline code spans only: fenced blocks carry examples, spans carry claims. +# +# A span is a path claim when its first segment is a real top-level entry in +# this repo. That test is what makes the check safe here — it skips the API +# routes (`/api/v1/`), image refs (`ghcr.io/...`), placeholders and the +# frontend-relative `services/` that this file legitimately contains. The same +# rule is wrong for trakrf/docs, whose vocabulary has no such shapes; the rule +# set belongs to the repo, not to the idea. +tracked=$(git -C "${repo_root}" ls-files) + +while read -r span; do + [[ -z "${span}" ]] && continue + [[ "${span}" =~ ^(https?:|/|\<|\$) ]] && continue # URLs, routes, placeholders + [[ "${span}" == *' '* ]] && continue # prose, not a path + + # A span is a path claim only if it carries a real file extension or ends + # in a slash. Without this the check fires on `X.Y.Z`, `info.version` and + # `vX.Y.Z` — version placeholders and identifiers this doc legitimately + # contains. A check that fails on a correct file is a check someone turns + # off, so the bar is deliberately narrow. + # `.env` and `.env.local` are gitignored by convention and correctly absent + # from a fresh clone; `.env.example` is the tracked sample and IS a claim. + # Omitting the sample extensions is what let this check pass on the exact + # bug it was built for — `cp backend/.env.example` — on its first run. + [[ "${span}" == ".env" || "${span}" == ".env.local" ]] && continue + + if [[ "${span}" != */ ]] && + [[ ! "${span}" =~ \.(md|json|ts|tsx|js|mjs|yml|yaml|sql|sh|go|mod|sum|toml|example|sample)$ ]]; then + continue + fi + + first="${span%%/*}" + if [[ "${span}" == */* ]]; then + [[ -e "${repo_root}/${first}" ]] || continue # not a repo-root path + fi + + clean="${span%/}" + if grep -qx "${clean}" <<< "${tracked}"; then continue; fi # tracked file + if grep -q "^${clean}/" <<< "${tracked}"; then continue; fi # tracked dir + problems+=("path \`${span}\` is named but is not tracked in this repo") +done < <( + grep -oE '`[^`]+`' "${doc}" | tr -d '`' | + # expand `a.{json,yaml}` into its members + awk '{ if (match($0, /\{[^}]*\}/)) { + pre=substr($0,1,RSTART-1); post=substr($0,RSTART+RLENGTH); + n=split(substr($0,RSTART+1,RLENGTH-2), parts, ","); + for (i=1;i<=n;i++) print pre parts[i] post; + } else print }' | + sort -u +) + +# --- report ----------------------------------------------------------------- +if (( ${#problems[@]} > 0 )); then + echo "CONTRIBUTING.md names things this repo no longer has:" >&2 + echo >&2 + printf ' - %s\n' "${problems[@]}" >&2 + cat >&2 <<'MSG' + +Fix the document or the reference. This check exists because nothing else reads +CONTRIBUTING.md, so drift here is otherwise silent until an outside contributor +follows it and fails. +MSG + exit 1 +fi + +echo "CONTRIBUTING.md: every just recipe and repo path it names still resolves."