forked from Sub-Rosa-Issue/sub-rosa-issue
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcheck-links.js
More file actions
162 lines (138 loc) · 4.35 KB
/
Copy pathcheck-links.js
File metadata and controls
162 lines (138 loc) · 4.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#!/usr/bin/env node
// Copyright (c) 2026 Sub Rosa contributors
'use strict';
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '..');
// Markdown files to scan (relative to ROOT)
const FILES = [
'README.md',
'ARCHITECTURE.md',
'docs/TECH_DESIGN.md',
'docs/THREAT_MODEL.md',
'docs/DEPLOY.md',
'docs/DEMO_SCRIPT.md',
'docs/INTEGRATION.md',
'docs/RECEIPTS.md',
'docs/SCF_TRANCHE_PLAN.md',
'docs/CV_LABS_APPLICATION.md',
'docs/LIMITATIONS.md',
'docs/ECOSYSTEM.md',
'docs/TRACK_ANSWERS.md',
'docs/PILOT_PLAYBOOK.md',
'packages/round-bindings/README.md',
];
// Allowlist for intentional placeholder links.
// Format: "source-file:link-target" (both relative to ROOT).
// Add entries here when a link is deliberately forward-looking.
const ALLOWLIST = new Set([]);
// GitHub-compatible heading slug (matches GitHub Markdown rendering)
function headingSlug(text) {
return text
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.trim()
.replace(/\s+/g, '-');
}
// Extract all heading slugs from a markdown string
function extractSlugs(content) {
const slugs = new Set();
const re = /^#{1,6}\s+(.+)$/gm;
let m;
while ((m = re.exec(content)) !== null) {
slugs.add(headingSlug(m[1]));
}
return slugs;
}
// Cache of slug sets keyed by absolute path
const slugCache = new Map();
function getSlugs(absPath) {
if (!slugCache.has(absPath)) {
const content = fs.readFileSync(absPath, 'utf8');
slugCache.set(absPath, extractSlugs(content));
}
return slugCache.get(absPath);
}
const LINK_RE = /\[([^\]]*)\]\(([^)\s]+)\)/g;
let broken = 0;
let checked = 0;
let skipped = 0;
const external = [];
const showExternal = process.argv.includes('--external');
for (const relFile of FILES) {
const absFile = path.join(ROOT, relFile);
if (!fs.existsSync(absFile)) {
console.error(`ERROR ${relFile}:0 — source file not found`);
broken++;
continue;
}
const content = fs.readFileSync(absFile, 'utf8');
const lines = content.split('\n');
const fileDir = path.dirname(absFile);
lines.forEach((line, idx) => {
const lineNum = idx + 1;
LINK_RE.lastIndex = 0;
let m;
while ((m = LINK_RE.exec(line)) !== null) {
const href = m[2];
// Skip external links
if (/^https?:\/\/|^mailto:/.test(href)) {
external.push({ file: relFile, line: lineNum, href });
continue;
}
// Split path from anchor
const hashIdx = href.indexOf('#');
const filePart = hashIdx === -1 ? href : href.slice(0, hashIdx);
const anchor = hashIdx === -1 ? null : href.slice(hashIdx + 1);
const allowKey = `${relFile}:${href}`;
if (ALLOWLIST.has(allowKey)) {
skipped++;
continue;
}
// Pure anchor link (#section) — check within same file
if (!filePart) {
checked++;
const ownSlugs = getSlugs(absFile);
if (!ownSlugs.has(anchor)) {
console.error(`BROKEN ${relFile}:${lineNum} — anchor #${anchor} not found in same file`);
broken++;
}
continue;
}
// Resolve file path
const absTarget = path.resolve(fileDir, filePart);
checked++;
if (!fs.existsSync(absTarget)) {
const rel = path.relative(ROOT, absTarget);
console.error(`BROKEN ${relFile}:${lineNum} — file not found: ${filePart} (→ ${rel})`);
broken++;
continue;
}
// Check anchor in target file (only for .md files)
if (anchor && /\.md$/i.test(absTarget)) {
const targetSlugs = getSlugs(absTarget);
if (!targetSlugs.has(anchor.toLowerCase())) {
console.error(`BROKEN ${relFile}:${lineNum} — anchor #${anchor} not found in ${path.relative(ROOT, absTarget)}`);
broken++;
}
}
}
});
}
if (external.length > 0) {
if (showExternal) {
console.log(`\nExternal links (${external.length}, not validated):`);
for (const { file, line, href } of external) {
console.log(` ${file}:${line}: ${href}`);
}
} else {
console.log(`External links: ${external.length} (pass --external to list)`);
}
}
if (broken === 0) {
console.log(`OK ${checked} local link(s) checked, ${skipped} allowlisted`);
process.exit(0);
} else {
console.error(`\nFAIL ${broken} broken link(s) — fix the paths above or add to ALLOWLIST in scripts/check-links.js`);
process.exit(1);
}