Skip to content

Commit 48bf632

Browse files
committed
Dedup cartridge loader into src/cartridges.js (SonarCloud duplication gate)
SonarCloud flagged 7.1% duplication on new code: the cartridge-walk loader was repeated across bin/evangeliser.js, test/run_all.js, gui/server.js, and scripts/validate-cartridges.js. Extract one shared module (src/cartridges.js: eachCartridgeFile / loadCartridges / verdict) and route all four readers through it. Verified (Deno 2.8.3): deno fmt --check, deno lint, deno task test (59/59), deno task build (1/1 cartridges), CLI + GUI /api/cartridges live probe (6 transitions, all six kinds) — all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1BaAhqxUjkgVb1yg1sZap
1 parent abe6330 commit 48bf632

7 files changed

Lines changed: 71 additions & 115 deletions

File tree

.claude/CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ JavaScript is allowed only when it is Deno/browser runtime glue or a launcher br
8080
| `bin/evangeliser.js` | 1 | Deno CLI reading correspondence cartridges (offline fallback) | Replace when AffineScript host CLI is runnable directly |
8181
| `gui/server.js` | 1 | Local Deno HTTP bridge serving the workspace + cartridge API | Replace when AffineScript-to-Deno/webview bridge is available |
8282
| `gui/app.js` | 1 | Browser-side event/render bridge for the GUI shell | Replace when AffineScript DOM/TEA bridge can drive this UI directly |
83+
| `src/cartridges.js` | 1 | Shared Deno cartridge loader (CLI, GUI server, validator, tests) | Replace when AffineScript can host the loader |
8384
| `scripts/validate-cartridges.js` | 1 | Deno cartridge-schema validator (ajv) | Replace when AffineScript can host the validator |
8485
| `test/run_all.js` | 1 | Deno cartridge invariant tests | Replace when AffineScript test runner is runnable directly |
8586

bin/evangeliser.js

Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
// The browser workspace ('just gui') is the primary surface; this CLI is the
1414
// offline-first fallback.
1515

16+
import { loadCartridges, verdict } from "../src/cartridges.js"
17+
1618
const KINDS = {
1719
"cognate": { glyph: "🤝", pedagogy: "transfer directly" },
1820
"false-friend": { glyph: "🎭", pedagogy: "flag the trap" },
@@ -54,42 +56,6 @@ function parseArgs(argv) {
5456
return opts
5557
}
5658

57-
async function loadCartridges(dirUrl) {
58-
const out = []
59-
async function walk(d) {
60-
let entries
61-
try {
62-
entries = Deno.readDir(d)
63-
} catch {
64-
return
65-
}
66-
for await (const e of entries) {
67-
const child = new URL(e.name + (e.isDirectory ? "/" : ""), d)
68-
if (e.isDirectory) {
69-
await walk(child)
70-
continue
71-
}
72-
if (!e.name.endsWith(".cartridge.json")) continue
73-
try {
74-
const c = JSON.parse(await Deno.readTextFile(child))
75-
out.push({
76-
cartridge: c.name ?? e.name,
77-
transitions: Array.isArray(c.transitions) ? c.transitions : [],
78-
})
79-
} catch (err) {
80-
console.error(`skip ${e.name}: ${err.message}`)
81-
}
82-
}
83-
}
84-
await walk(dirUrl)
85-
return out
86-
}
87-
88-
function verdict(strata, name) {
89-
const v = (strata ?? []).find((s) => s.stratum === name)
90-
return v ? v.holds : null
91-
}
92-
9359
function render(t) {
9460
const k = KINDS[t.kind] ?? { glyph: "?", pedagogy: "" }
9561
const residue = t.residue?.shape ? ` · residue: ${t.residue.shape}` : ""
@@ -119,7 +85,7 @@ async function main() {
11985

12086
const cartridges = await loadCartridges(root)
12187
let items = cartridges.flatMap((c) =>
122-
c.transitions.map((t) => ({ ...t, cartridge: c.cartridge }))
88+
(c.transitions ?? []).map((t) => ({ ...t, cartridge: c.name }))
12389
)
12490

12591
if (opts.kind) items = items.filter((t) => t.kind === opts.kind)

deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"semiColons": false,
2121
"singleQuote": false,
2222
"proseWrap": "preserve",
23-
"include": ["bin/", "gui/", "scripts/", "test/run_all.js", "deno.json"]
23+
"include": ["bin/", "gui/", "scripts/", "src/cartridges.js", "test/run_all.js", "deno.json"]
2424
},
2525
"lint": {
2626
"rules": {

gui/server.js

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
// deno run --allow-read --allow-net gui/server.js [--port N] [--host H] [--open]
1010
// (add --allow-env to honour EVANGELISER_GUI_PORT; --allow-run for --open)
1111

12+
import { loadCartridges as loadAll } from "../src/cartridges.js"
13+
1214
const root = new URL("../", import.meta.url)
1315

1416
function argValue(name, fallback) {
@@ -26,36 +28,12 @@ function envPort() {
2628

2729
async function loadCartridges() {
2830
const dir = new URL("cartridges/", root)
29-
const out = []
30-
async function walk(d) {
31-
let entries
32-
try {
33-
entries = Deno.readDir(d)
34-
} catch {
35-
return
36-
}
37-
for await (const e of entries) {
38-
const child = new URL(e.name + (e.isDirectory ? "/" : ""), d)
39-
if (e.isDirectory) {
40-
await walk(child)
41-
continue
42-
}
43-
if (!e.name.endsWith(".cartridge.json")) continue
44-
try {
45-
const c = JSON.parse(await Deno.readTextFile(child))
46-
out.push({
47-
cartridge: c.name ?? e.name,
48-
description: c.description ?? "",
49-
languages: c.languages ?? [],
50-
transitions: Array.isArray(c.transitions) ? c.transitions : [],
51-
})
52-
} catch (err) {
53-
console.error(`skip ${e.name}: ${err.message}`)
54-
}
55-
}
56-
}
57-
await walk(dir)
58-
return out
31+
return (await loadAll(dir)).map((c) => ({
32+
cartridge: c.name ?? "(unnamed)",
33+
description: c.description ?? "",
34+
languages: c.languages ?? [],
35+
transitions: Array.isArray(c.transitions) ? c.transitions : [],
36+
}))
5937
}
6038

6139
const contentTypes = {

scripts/validate-cartridges.js

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
// Exits non-zero if any cartridge fails — suitable for CI.
99

1010
import Ajv2020 from "npm:ajv@8/dist/2020.js"
11+
import { eachCartridgeFile } from "../src/cartridges.js"
1112

1213
const dir = new URL("../cartridges/", import.meta.url)
1314
const schema = JSON.parse(
@@ -20,26 +21,17 @@ const validate = new AjvCtor({ allErrors: true, strict: false }).compile(schema)
2021
let count = 0
2122
let bad = 0
2223

23-
async function walk(d) {
24-
for await (const entry of Deno.readDir(d)) {
25-
const child = new URL(entry.name + (entry.isDirectory ? "/" : ""), d)
26-
if (entry.isDirectory) {
27-
await walk(child)
28-
continue
29-
}
30-
if (!entry.name.endsWith(".cartridge.json")) continue
31-
count++
32-
const data = JSON.parse(await Deno.readTextFile(child))
33-
if (validate(data)) {
34-
console.log(`VALID ${entry.name} (${data.transitions.length} transitions)`)
35-
} else {
36-
bad++
37-
console.error(`INVALID ${entry.name}`)
38-
console.error(JSON.stringify(validate.errors, null, 2))
39-
}
24+
await eachCartridgeFile(dir, async (url, name) => {
25+
count++
26+
const data = JSON.parse(await Deno.readTextFile(url))
27+
if (validate(data)) {
28+
console.log(`VALID ${name} (${data.transitions.length} transitions)`)
29+
} else {
30+
bad++
31+
console.error(`INVALID ${name}`)
32+
console.error(JSON.stringify(validate.errors, null, 2))
4033
}
41-
}
34+
})
4235

43-
await walk(dir)
4436
console.log(`\n${count - bad}/${count} cartridges valid.`)
4537
Deno.exit(bad ? 1 : 0)

src/cartridges.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// Shared cartridge loader (Deno glue) for the CLI, the GUI server, the schema
3+
// validator, and the invariant tests. One walk of cartridges/**/*.cartridge.json
4+
// so the readers do not each re-implement it.
5+
6+
// Invoke `fn(fileUrl, fileName)` for every *.cartridge.json under `rootUrl`.
7+
export async function eachCartridgeFile(rootUrl, fn) {
8+
async function walk(d) {
9+
let entries
10+
try {
11+
entries = Deno.readDir(d)
12+
} catch {
13+
return
14+
}
15+
for await (const e of entries) {
16+
const child = new URL(e.name + (e.isDirectory ? "/" : ""), d)
17+
if (e.isDirectory) {
18+
await walk(child)
19+
continue
20+
}
21+
if (!e.name.endsWith(".cartridge.json")) continue
22+
await fn(child, e.name)
23+
}
24+
}
25+
await walk(rootUrl)
26+
}
27+
28+
// Parse every cartridge under `rootUrl`; returns the raw cartridge objects.
29+
export async function loadCartridges(rootUrl) {
30+
const out = []
31+
await eachCartridgeFile(rootUrl, async (url, name) => {
32+
try {
33+
out.push(JSON.parse(await Deno.readTextFile(url)))
34+
} catch (err) {
35+
console.error(`skip ${name}: ${err.message}`)
36+
}
37+
})
38+
return out
39+
}
40+
41+
// Whether a correspondence holds at the named stratum (true / false / null).
42+
export function verdict(strata, name) {
43+
const v = (strata ?? []).find((s) => s.stratum === name)
44+
return v ? v.holds : null
45+
}

test/run_all.js

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
// deno run --allow-read test/run_all.js
99
// deno task test / just test
1010

11+
import { loadCartridges, verdict } from "../src/cartridges.js"
12+
1113
const KINDS = [
1214
"cognate",
1315
"false-friend",
@@ -30,34 +32,6 @@ function check(label, cond) {
3032
}
3133
}
3234

33-
async function loadCartridges(dirUrl) {
34-
const out = []
35-
async function walk(d) {
36-
let entries
37-
try {
38-
entries = Deno.readDir(d)
39-
} catch {
40-
return
41-
}
42-
for await (const e of entries) {
43-
const child = new URL(e.name + (e.isDirectory ? "/" : ""), d)
44-
if (e.isDirectory) {
45-
await walk(child)
46-
continue
47-
}
48-
if (!e.name.endsWith(".cartridge.json")) continue
49-
out.push(JSON.parse(await Deno.readTextFile(child)))
50-
}
51-
}
52-
await walk(dirUrl)
53-
return out
54-
}
55-
56-
function verdict(strata, name) {
57-
const v = (strata ?? []).find((s) => s.stratum === name)
58-
return v ? v.holds : null
59-
}
60-
6135
const root = new URL("../cartridges/", import.meta.url)
6236
const cartridges = await loadCartridges(root)
6337

0 commit comments

Comments
 (0)