|
1 | 1 | #!/usr/bin/env node |
2 | 2 | /** |
3 | | - * @file Lock-step header byte-equality gate. Mantra: the four impls |
4 | | - * of a quadruplet agree about WHAT THE FILE IS FOR. The `BEGIN |
5 | | - * LOCK-STEP HEADER` / `END LOCK-STEP HEADER` block names that |
6 | | - * contract; every member of the quadruplet carries the same block, |
7 | | - * byte-for-byte (after stripping the `// ` comment prefix). Drift |
8 | | - * on the contract is a different failure mode from a stale path |
9 | | - * reference (which `check-lock-step-refs.mts` catches) — this gate |
10 | | - * is the _intent_ tripwire. |
| 3 | + * @file Lock-step header byte-equality gate. Mantra: the four impls of a |
| 4 | + * quadruplet agree about WHAT THE FILE IS FOR. The `BEGIN LOCK-STEP HEADER` / |
| 5 | + * `END LOCK-STEP HEADER` block names that contract; every member of the |
| 6 | + * quadruplet carries the same block, byte-for-byte (after stripping the `// ` |
| 7 | + * comment prefix). Drift on the contract is a different failure mode from a |
| 8 | + * stale path reference (which `check-lock-step-refs.mts` catches) — this gate |
| 9 | + * is the _intent_ tripwire. Opt-in per repo: uses the same |
| 10 | + * `.config/lock-step-refs.json` as the path gate. Without the config, the |
| 11 | + * gate is a no-op. With the config, the gate walks every scanned source file, |
| 12 | + * looks for a `BEGIN LOCK-STEP HEADER` marker on the canonical side (a file |
| 13 | + * whose header contains one or more `Lock-step with <Lang>: <path>` refs), |
| 14 | + * extracts the header content, then opens each named peer and demands its |
| 15 | + * header block be byte-identical. "Canonical side" is determined by the |
| 16 | + * header content itself: |
11 | 17 | * |
12 | | - * Opt-in per repo: uses the same `.config/lock-step-refs.json` as |
13 | | - * the path gate. Without the config, the gate is a no-op. With the |
14 | | - * config, the gate walks every scanned source file, looks for a |
15 | | - * `BEGIN LOCK-STEP HEADER` marker on the canonical side (a file |
16 | | - * whose header contains one or more `Lock-step with <Lang>: <path>` |
17 | | - * refs), extracts the header content, then opens each named peer |
18 | | - * and demands its header block be byte-identical. |
19 | | - * |
20 | | - * "Canonical side" is determined by the header content itself: |
21 | | - * |
22 | | - * - A file with `Lock-step with <Lang>: <path>` is canonical for |
23 | | - * that peer. (The peer should reciprocate with |
24 | | - * `Lock-step from <Lang>: <my-path>`, but the gate doesn't rely |
25 | | - * on that — symmetry is a §5 rule, not a §7 rule.) |
26 | | - * - A file with only `Lock-step from <Lang>: <path>` is a port |
27 | | - * and is checked against its canonical source. |
28 | | - * |
29 | | - * Header format (single-line `// ` across every language): |
30 | | - * |
31 | | - * // BEGIN LOCK-STEP HEADER |
32 | | - * // Class Parsing (Declarations, Expressions, Elements, Methods) |
33 | | - * // |
34 | | - * // Lock-step with Go: src/parser/class.go |
35 | | - * // Lock-step with C++: src/parser/class.cpp |
36 | | - * // END LOCK-STEP HEADER |
37 | | - * |
38 | | - * Comparison strips the `// ` prefix from each line; an empty |
39 | | - * comment line (`//`) is preserved as an empty content line. The |
40 | | - * content between BEGIN and END is the contract. |
41 | | - * |
42 | | - * Usage: |
43 | | - * |
44 | | - * node scripts/check-lock-step-header.mts # report + fail |
45 | | - * node scripts/check-lock-step-header.mts --json # machine-readable |
46 | | - * node scripts/check-lock-step-header.mts --quiet # silent on clean |
47 | | - * |
48 | | - * Exit codes: |
49 | | - * |
50 | | - * 0 — clean (no quadruplets diverged, or config absent) |
51 | | - * 1 — at least one quadruplet has a header diff |
52 | | - * 2 — gate itself crashed |
| 18 | + * - A file with `Lock-step with <Lang>: <path>` is canonical for that peer. |
| 19 | + * (The peer should reciprocate with `Lock-step from <Lang>: <my-path>`, but |
| 20 | + * the gate doesn't rely on that — symmetry is a §5 rule, not a §7 rule.) |
| 21 | + * - A file with only `Lock-step from <Lang>: <path>` is a port and is checked |
| 22 | + * against its canonical source. Header format (single-line `// ` across |
| 23 | + * every language): // BEGIN LOCK-STEP HEADER // Class Parsing |
| 24 | + * (Declarations, Expressions, Elements, Methods) // // Lock-step with Go: |
| 25 | + * src/parser/class.go // Lock-step with C++: src/parser/class.cpp // END |
| 26 | + * LOCK-STEP HEADER Comparison strips the `// ` prefix from each line; an |
| 27 | + * empty comment line (`//`) is preserved as an empty content line. The |
| 28 | + * content between BEGIN and END is the contract. Usage: node |
| 29 | + * scripts/check-lock-step-header.mts # report + fail node |
| 30 | + * scripts/check-lock-step-header.mts --json # machine-readable node |
| 31 | + * scripts/check-lock-step-header.mts --quiet # silent on clean Exit codes: |
| 32 | + * 0 — clean (no quadruplets diverged, or config absent) 1 — at least one |
| 33 | + * quadruplet has a header diff 2 — gate itself crashed |
53 | 34 | */ |
54 | 35 |
|
55 | 36 | import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs' |
@@ -172,7 +153,8 @@ function extractHeader(file: string): HeaderBlock | undefined { |
172 | 153 | const stripped = stripCommentPrefix(raw) |
173 | 154 | bodyLines.push(stripped) |
174 | 155 | } |
175 | | - const withRe = /Lock-step with ([A-Za-z][A-Za-z0-9+#-]*): ([^\s:,]*[./][^\s:,]*)/g |
| 156 | + const withRe = |
| 157 | + /Lock-step with ([A-Za-z][A-Za-z0-9+#-]*): ([^\s:,]*[./][^\s:,]*)/g |
176 | 158 | const withRefs: Array<{ lang: string; refPath: string }> = [] |
177 | 159 | for (const line of bodyLines) { |
178 | 160 | withRe.lastIndex = 0 |
@@ -239,7 +221,9 @@ function bodyEqual(a: readonly string[], b: readonly string[]): boolean { |
239 | 221 | function formatDiff(d: Diff, repoRoot: string): string { |
240 | 222 | const out: string[] = [] |
241 | 223 | const rel = (p: string) => path.relative(repoRoot, p) |
242 | | - out.push(`\n${rel(d.canonical)} (canonical) ↔ ${rel(d.peer)} (${d.lang} peer):`) |
| 224 | + out.push( |
| 225 | + `\n${rel(d.canonical)} (canonical) ↔ ${rel(d.peer)} (${d.lang} peer):`, |
| 226 | + ) |
243 | 227 | if (d.reason === 'peer-not-found') { |
244 | 228 | out.push(` peer path doesn't exist on disk: ${rel(d.peer)}`) |
245 | 229 | return out.join('\n') |
@@ -274,9 +258,7 @@ function main(): void { |
274 | 258 | try { |
275 | 259 | config = loadConfig(repoRoot) |
276 | 260 | } catch (e) { |
277 | | - process.stderr.write( |
278 | | - `check-lock-step-header: ${(e as Error).message}\n`, |
279 | | - ) |
| 261 | + process.stderr.write(`check-lock-step-header: ${(e as Error).message}\n`) |
280 | 262 | process.exitCode = 2 |
281 | 263 | return |
282 | 264 | } |
@@ -307,12 +289,7 @@ function main(): void { |
307 | 289 | } |
308 | 290 | canonicalCount += 1 |
309 | 291 | for (const ref of header.withRefs) { |
310 | | - const peerPath = resolveRefPath( |
311 | | - config, |
312 | | - repoRoot, |
313 | | - ref.lang, |
314 | | - ref.refPath, |
315 | | - ) |
| 292 | + const peerPath = resolveRefPath(config, repoRoot, ref.lang, ref.refPath) |
316 | 293 | if (!peerPath) { |
317 | 294 | diffs.push({ |
318 | 295 | canonical: file, |
|
0 commit comments