From 288ac0d4128759aaf020c27c00444573ab69a427 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Sat, 30 May 2026 19:05:59 +0100 Subject: [PATCH 1/2] feat(scripts+ci): swap governance TS-allowlist check to AffineScript-compiled .deno.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Depends on: #310 Closes the second half of the seed-PR plan from #283: now that the .affine source actually compiles and runs (#310), wire the workflow at the AffineScript-compiled output. ## Changes 1. `scripts/check-ts-allowlist.deno.js` (NEW, 344 lines) - Generated by `affinescript compile --deno-esm` from the post-#310 `.affine` source. Self-contained Deno-ESM bundle with inlined runtime shims for the codegen-known externs (walkRecursive, regexMatch, readTextFile, exit, consoleError) and the public `split` / `ends_with` helpers. Auto-invokes `await main()` at module load — same shape Deno runs the existing `.ts` with. 2. `.github/workflows/governance-reusable.yml:193` - `deno run ... check-ts-allowlist.ts` → `... check-ts-allowlist.deno.js` - Comment block explains the dual-target window: .ts is kept for the regression suite (`scripts/tests/check-ts-allowlist-test.sh`) and for parallel-validation; retirement is a separate follow-up. ## Why ship the compiled artifact (vs compile-in-CI) Compile-in-CI would require an AffineScript toolchain in the workflow (currently OCaml + dune + the affinescript repo checked out as a dep). The compiler is deterministic for this surface; committing the output keeps CI provider-free and matches the precedent in `affinescript/tests/codegen-deno/*.deno.js` (committed golden files). Drift between source and committed output is the obvious risk. A follow-up PR will add a `just check-ts-allowlist-drift` recipe (recompile + diff) wired into a non-blocking CI lint, mirroring the golden-file maintenance pattern in the affinescript repo. ## Verification End-to-end runtime test of the compiled output (per PR #310 commit msg b046787): empty dir -> exit 0, success line src/Foo.ts violation -> exit 1, error block + file list mod.ts (filename ok) -> exit 0 scripts/x.ts (dir ok) -> exit 0 Workflow YAML linting: this change is one line of `run:` text; the governance-reusable.yml workflow is unchanged in shape, only its script target moves. ## Test plan - [ ] CI green on this branch (note: AS scanner / Hypatia / SPARK Theatre Gate may need to acknowledge the new committed .deno.js as a generated artifact; ignore findings tagged as generated) - [ ] `scripts/tests/check-ts-allowlist-test.sh` still passes against the unchanged .ts archetype - [ ] Caller repos using `governance-reusable.yml` show green "Check for TypeScript" steps after merge Refs #239 (umbrella TS→AffineScript), #241 (STEP 2 tail-batch-1). Follow-up plan: add drift-detection lint; then a separate PR retires the .ts archetype after a parallel-validation window. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/governance-reusable.yml | 9 +- scripts/check-ts-allowlist.deno.js | 344 ++++++++++++++++++++++ 2 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 scripts/check-ts-allowlist.deno.js diff --git a/.github/workflows/governance-reusable.yml b/.github/workflows/governance-reusable.yml index 9e66be0e..cd802085 100644 --- a/.github/workflows/governance-reusable.yml +++ b/.github/workflows/governance-reusable.yml @@ -190,7 +190,14 @@ jobs: # `deno run` before the file-walker even starts — the script does not # import anything, so the lockfile is irrelevant to its execution. # See standards#294. - run: deno run --allow-read --no-lock .standards-checkout/scripts/check-ts-allowlist.ts + # + # Runs the AffineScript-compiled `.deno.js` (source of truth: + # `scripts/check-ts-allowlist.affine`). The .ts archetype is kept + # alongside for the regression suite (`scripts/tests/check-ts- + # allowlist-test.sh`) and for parallel-validation during the + # TS→AffineScript migration (standards#239 / #241). Retirement of + # the .ts is a separate follow-up after the dual-target window. + run: deno run --allow-read --no-lock .standards-checkout/scripts/check-ts-allowlist.deno.js # Shared escape hatch for the banned-language-file checks below. # Honours three exemption mechanisms (see diff --git a/scripts/check-ts-allowlist.deno.js b/scripts/check-ts-allowlist.deno.js new file mode 100644 index 00000000..91748338 --- /dev/null +++ b/scripts/check-ts-allowlist.deno.js @@ -0,0 +1,344 @@ +// Generated by AffineScript compiler (Deno-ESM target, issue #122) +// SPDX-License-Identifier: MPL-2.0 +// ---- AffineScript Deno-ESM runtime ---- +const Some = (value) => ({ tag: "Some", value }); +const None = { tag: "None" }; +const Ok = (value) => ({ tag: "Ok", value }); +const Err = (error) => ({ tag: "Err", error }); +const Unit = null; +const print = (s) => { Deno.stdout.writeSync(new TextEncoder().encode(String(s))); }; +const println = (s) => { console.log(String(s)); }; +// ---- Deno host shims (extern fn lowering targets, issue #122) ---- +// Kept tiny + inlined so emitted modules are genuinely drop-in (no extra +// package to publish or resolve). The same surface is mirrored, for +// standalone `deno test`, by packages/affine-deno/mod.js. +const __as_ensureDir = (p) => { + try { Deno.mkdirSync(p, { recursive: true }); } + catch (e) { if (!(e instanceof Deno.errors.AlreadyExists)) throw e; } +}; +const __as_pathJoin = (a, b) => { + if (a.length === 0) return b; + const sep = a.endsWith("/") || a.endsWith("\\") ? "" : "/"; + return a + sep + b; +}; +const __as_readDirNames = (p) => { + const names = []; + for (const entry of Deno.readDirSync(p)) { + if (entry.isFile) names.push(entry.name); + } + return names; +}; +const __as_isNotFound = (e) => (e instanceof Deno.errors.NotFound); +const __as_walkRecursive = (root) => { + const out = []; + const rec = (dir) => { + for (const entry of Deno.readDirSync(dir)) { + const full = (dir.endsWith("/") ? dir : dir + "/") + entry.name; + if (entry.isFile) out.push(full); + else if (entry.isDirectory) rec(full); + } + }; + rec(root); + return out; +}; +const __as_regexMatch = (s, pat) => new RegExp(pat).test(String(s)); +const __as_wasmInstance = (bytes) => + new WebAssembly.Instance(new WebAssembly.Module(bytes)).exports; +const __as_wasmCall = (exports, name, args) => Number(exports[name](...(args || []))); +// ---- motion (bindings #4): consumer-provided import ---- +// Host JS environment must expose globalThis.__as_motion (the motion +// library or a compatible mock). Tests set it in the harness before +// importing the generated module; production consumers typically do +// `import * as m from "motion"; globalThis.__as_motion = m;` once at +// module-init time. The AffineScript-side externs (stdlib/Motion.affine) +// don't see this indirection — they call __as_motion* helpers directly. +const __as_motionAnimate = (target, keyframes, options) => + globalThis.__as_motion.animate(target, keyframes, options); +const __as_motionAwait = (controls) => + Promise.resolve(controls).then(() => 0); +const __as_motionCancel = (controls) => { + if (controls && typeof controls.cancel === "function") controls.cancel(); + return 0; +}; +// `animateMini` / `tween` / `spring` / `ease` — bindings #4 follow-up +// surface. Each helper resolves the host method on globalThis.__as_motion +// at call time so a mock that only stubs a subset still works for the +// rest (the smoke harness exercises every variant). +const __as_motionAnimateMini = (target, keyframes, options) => + globalThis.__as_motion.animateMini(target, keyframes, options); +const __as_motionTween = (target, from, to, options) => + globalThis.__as_motion.tween(target, from, to, options); +const __as_motionSpring = (target, keyframes, springConfig) => + globalThis.__as_motion.spring(target, keyframes, springConfig); +const __as_motionEase = (name) => + globalThis.__as_motion.ease(name); +// ---- pixi.js (bindings #1): consumer-provided import ---- +// Host JS environment exposes globalThis.__as_pixi (the PIXI namespace +// from `import * as PIXI from "pixi.js"`). Tests set it in the harness +// before importing the generated module. +const __as_pixiAppInit = async (options) => { + const app = new globalThis.__as_pixi.Application(); + await app.init(options); + return app; +}; +const __as_pixiAppCanvas = (app) => app.canvas; +const __as_pixiAppStage = (app) => app.stage; +const __as_pixiAppTicker = (app) => app.ticker; +const __as_pixiAppDestroy = (app) => { app.destroy(); return 0; }; +const __as_pixiContainerNew = () => new globalThis.__as_pixi.Container(); +const __as_pixiContainerAddChild = (p, c) => { p.addChild(c); return 0; }; +const __as_pixiContainerRemoveChild = (p, c) => { p.removeChild(c); return 0; }; +const __as_pixiContainerSetPosition = (c, x, y) => { c.x = x; c.y = y; return 0; }; +const __as_pixiContainerSetVisible = (c, v) => { c.visible = v; return 0; }; +const __as_pixiContainerDestroy = (c) => { c.destroy(); return 0; }; +const __as_pixiSpriteFrom = (t) => new globalThis.__as_pixi.Sprite(t); +// Upcasts are identity — PIXI's class hierarchy makes Sprite/Graphics/ +// Text actual Container subclasses, so the JS object is the same. +const __as_pixiSpriteAsContainer = (s) => s; +const __as_pixiTextureFromUrl = (url) => globalThis.__as_pixi.Texture.from(url); +const __as_pixiGraphicsNew = () => new globalThis.__as_pixi.Graphics(); +const __as_pixiGraphicsRect = (g, x, y, w, h) => { g.rect(x, y, w, h); return 0; }; +const __as_pixiGraphicsFill = (g, color) => { g.fill({ color }); return 0; }; +const __as_pixiGraphicsClear = (g) => { g.clear(); return 0; }; +const __as_pixiGraphicsAsContainer = (g) => g; +const __as_pixiTextNew = (options) => new globalThis.__as_pixi.Text(options); +const __as_pixiTextSetText = (t, content) => { t.text = content; return 0; }; +const __as_pixiTextAsContainer = (t) => t; +const __as_pixiTickerAdd = (t, cb) => { t.add(cb); return 0; }; +const __as_pixiTickerStart = (t) => { t.start(); return 0; }; +const __as_pixiTickerStop = (t) => { t.stop(); return 0; }; +// ---- @pixi/ui (bindings #3): consumer-provided import ---- +// Host JS environment exposes globalThis.__as_pixi_ui (the namespace +// from `import * as PixiUI from "@pixi/ui"`). Tests set it in the +// harness before importing the generated module; production +// consumers typically do once at module-init time. The +// AffineScript-side externs (stdlib/PixiUI.affine) don't see this +// indirection — they call __as_pixiUi* helpers directly. +// +// Upcasts to Container are identity — @pixi/ui's Button / +// FancyButton / Slider / Switch are all real PIXI.Container +// subclasses, so the JS object is the same. +const __as_pixiUiButtonNew = (options) => new globalThis.__as_pixi_ui.Button(options); +const __as_pixiUiButtonOnPress = (b, cb) => { b.onPress.connect(cb); return 0; }; +const __as_pixiUiButtonAsContainer = (b) => b; +const __as_pixiUiFancyButtonNew = (options) => new globalThis.__as_pixi_ui.FancyButton(options); +const __as_pixiUiFancyButtonAsContainer = (b) => b; +const __as_pixiUiSliderNew = (options) => new globalThis.__as_pixi_ui.Slider(options); +const __as_pixiUiSliderOnUpdate = (s, cb) => { s.onUpdate.connect(cb); return 0; }; +const __as_pixiUiSliderAsContainer = (s) => s; +const __as_pixiUiSwitchNew = (options) => new globalThis.__as_pixi_ui.Switch(options); +const __as_pixiUiSwitchOnChange = (sw, cb) => { sw.onChange.connect(cb); return 0; }; +const __as_pixiUiSwitchAsContainer = (sw) => sw; +// ---- @pixi/sound (bindings #2): consumer-provided import ---- +// Host JS environment exposes globalThis.__as_pixi_sound (the `Sound` +// named export from `@pixi/sound`). Tests set it in the harness before +// importing the generated module; production consumers typically do +// `import { Sound } from "@pixi/sound"; globalThis.__as_pixi_sound = Sound;` +// once at module-init time. The AffineScript-side externs +// (stdlib/PixiSound.affine) don't see this indirection — they call +// __as_pixiSound* helpers directly. +const __as_pixiSoundFrom = (url) => globalThis.__as_pixi_sound.from(url); +const __as_pixiSoundPlay = (s) => { s.play(); return 0; }; +const __as_pixiSoundStop = (s) => { s.stop(); return 0; }; +const __as_pixiSoundPause = (s) => { s.pause(); return 0; }; +const __as_pixiSoundResume = (s) => { s.resume(); return 0; }; +const __as_pixiSoundSetVolume = (s, vol) => { s.volume = vol; return 0; }; +const __as_pixiSoundSetLoop = (s, loop) => { s.loop = loop; return 0; }; +// `++` is overloaded (string concat / array concat); `a + b` would +// stringify arrays. Dispatch on shape so stdlib/string.affine's +// `result ++ [x]` and `a ++ b` are both correct. +const __as_concat = (a, b) => Array.isArray(a) ? a.concat(b) : (a + b); +// Honest host/runtime primitives underpinning the AffineScript-level +// stdlib/string.affine (its is_empty/starts_with/ends_with/split/join/ +// replace/... are real AffineScript on top of these). +const __as_strSub = (s, start, n) => String(s).slice(start, start + n); +const __as_strGet = (s, i) => String(s)[i]; +const __as_strFind = (s, n) => String(s).indexOf(n); +const __as_charToInt = (c) => String(c).codePointAt(0); +const __as_intToChar = (n) => String.fromCodePoint(n); +const __as_parseInt = (s) => { + const n = parseInt(String(s), 10); + return Number.isNaN(n) ? None : Some(n); +}; +const __as_parseFloat = (s) => { + const n = parseFloat(String(s)); + return Number.isNaN(n) ? None : Some(n); +}; +const __as_show = (v) => (typeof v === "string" ? v : JSON.stringify(v)); +// ---- Http (issue #160): portable fetch round-trip ---- +// `headers` crosses the boundary as an AffineScript [(String, String)] +// assoc list == JS array of [name, value] pairs. `body` is an +// AffineScript Option == { tag: "Some", value } | { tag: "None" }. +// The result is the `Response` record shape { status, headers, body }. +const __as_httpHeadersToObject = (pairs) => { + const o = {}; + for (const kv of (pairs || [])) o[kv[0]] = kv[1]; + return o; +}; +const __as_httpHeadersFromResponse = (res) => { + const out = []; + res.headers.forEach((value, key) => out.push([key, value])); + return out; +}; +// ---- hpm-json-rsr Zig FFI shims (stdlib/json.affine v0.3) ---- +// `HpmJsonValue` is opaque to AffineScript; on Deno-ESM it's just the +// underlying JS value from JSON.parse. The shims mirror the sentinel +// conventions of the Zig exports so the AffineScript-side wrappers +// (`to_json`, `parse`) behave identically across backends. +const __as_hpmJsonParse = (s) => { + try { return Some(JSON.parse(String(s))); } catch (_e) { return None; } +}; +const __as_hpmJsonFree = (_v) => 0; +const __as_hpmJsonType = (v) => { + if (v === null || v === undefined) return 0; + if (typeof v === "boolean") return 1; + if (typeof v === "number") return Number.isInteger(v) ? 2 : 3; + if (typeof v === "string") return 4; + if (Array.isArray(v)) return 5; + if (typeof v === "object") return 6; + return -1; +}; +const __as_hpmJsonBool = (v) => (typeof v === "boolean" ? (v ? 1 : 0) : -1); +const __as_hpmJsonInt = (v) => + (typeof v === "number" ? Math.trunc(v) : Number.MIN_SAFE_INTEGER); +const __as_hpmJsonFloat = (v) => (typeof v === "number" ? v : NaN); +const __as_hpmJsonString = (v) => (typeof v === "string" ? v : ""); +const __as_hpmJsonObjectGet = (v, k) => { + if (v === null || typeof v !== "object" || Array.isArray(v)) return None; + return Object.prototype.hasOwnProperty.call(v, String(k)) + ? Some(v[String(k)]) : None; +}; +const __as_hpmJsonArrayLen = (v) => (Array.isArray(v) ? v.length : 0); +const __as_hpmJsonArrayGet = (v, i) => { + if (!Array.isArray(v)) return None; + const idx = Number(i); + return (idx >= 0 && idx < v.length) ? Some(v[idx]) : None; +}; +const __as_hpmJsonEscapeString = (s) => { + let out = ""; + const src = String(s); + for (let i = 0; i < src.length; i++) { + const c = src.charCodeAt(i); + if (c === 0x22) out += "\\\""; + else if (c === 0x5c) out += "\\\\"; + else if (c === 0x0a) out += "\\n"; + else if (c === 0x0d) out += "\\r"; + else if (c === 0x09) out += "\\t"; + else if (c === 0x08) out += "\\b"; + else if (c === 0x0c) out += "\\f"; + else if (c < 0x20) out += "\\u00" + c.toString(16).padStart(2, "0"); + else out += src[i]; + } + return out; +}; +const __as_httpFetch = async (url, method, headers, bodyOpt) => { + const init = { method, headers: __as_httpHeadersToObject(headers) }; + if (bodyOpt && bodyOpt.tag === "Some") init.body = bodyOpt.value; + // `globalThis.fetch` explicitly: the stdlib `Http.fetch` compiles to a + // module-level `function fetch`, which would otherwise shadow the host. + const res = await globalThis.fetch(url, init); + const text = await res.text(); + return { + status: res.status, + headers: __as_httpHeadersFromResponse(res), + body: text, + }; +}; +// ---- end runtime ---- + +export function split(s, delimiter) { + const slen = ((s).length); + const dlen = ((delimiter).length); + if ((dlen === 0)) { let result = []; let i = 0; while ((i < slen)) { result = __as_concat(result, [__as_strSub(s, i, 1)]); i = (i + 1); } return result; } + let result = []; + let current_start = 0; + let i = 0; + while ((i <= (slen - dlen))) { if ((__as_strSub(s, i, dlen) === delimiter)) { result = __as_concat(result, [__as_strSub(s, current_start, (i - current_start))]); current_start = (i + dlen); i = (i + dlen); } else { i = (i + 1); } } + result = __as_concat(result, [__as_strSub(s, current_start, (slen - current_start))]); + return result; +} + +function ends_with(s, suffix) { + const slen = ((s).length); + const sfxlen = ((suffix).length); + return ((sfxlen > slen) ? (() => { return false; })() : (() => { return (__as_strSub(s, (slen - sfxlen), sfxlen) === suffix); })()); +} + +const DIR_NAMES_ALLOWED = ["bindings", "tests", "test", "scripts", "mcp-adapter", "cli", "vendor", "examples", "ffi", "node_modules", "benchmarks"]; +function builtinAllowed(p) { + if (ends_with(p, ".d.ts")) { return true; } + const segs = split(p, "/"); + const segs_len = ((segs).length); + const base = segs[(segs_len - 1)]; + if ((base === "mod.ts")) { return true; } + if (((((base === "lsp-server.ts") || (base === "lsp_server.ts")) || (base === "lsp.ts")) || ends_with(base, "-lsp.ts"))) { return true; } + if ((ends_with(base, ".bench.ts") || ends_with(base, "_bench.ts"))) { return true; } + let i = 0; + while ((i < (segs_len - 1))) { const s = segs[i]; let j = 0; const dn_len = ((DIR_NAMES_ALLOWED).length); while ((j < dn_len)) { if ((s === DIR_NAMES_ALLOWED[j])) { return true; } j = (j + 1); } if (__as_regexMatch(s, "vscode")) { return true; } if (__as_regexMatch(s, "^deno-")) { return true; } i = (i + 1); } + return false; +} + +function globToRegex(g) { + let g2 = g; + while (((((g2).length) > 0) && ((__as_strSub(g2, 0, 1) === ".") || (__as_strSub(g2, 0, 1) === "/")))) { g2 = __as_strSub(g2, 1, (((g2).length) - 1)); } + let out = ""; + let i = 0; + const g2_len = ((g2).length); + while ((i < g2_len)) { const c = __as_strSub(g2, i, 1); if ((c === "*")) { out = __as_concat(out, ".*"); } else { if ((c === "?")) { out = __as_concat(out, "."); } else { if (((((((((((((c === ".") || (c === "+")) || (c === "(")) || (c === ")")) || (c === "{")) || (c === "}")) || (c === "[")) || (c === "]")) || (c === "|")) || (c === "^")) || (c === "$")) || (c === "\\"))) { out = __as_concat(__as_concat(out, "\\"), c); } else { out = __as_concat(out, c); } } } i = (i + 1); } + return __as_concat(__as_concat("^", out), "$"); +} + +// type Exemption +function loadExemptionsFromClaudeMd() { + let exemptions = []; + const text = (() => { try { return (() => { return Deno.readTextFileSync(".claude/CLAUDE.md"); })(); } catch (__e) { return ""; } })(); + if ((text === "")) { return exemptions; } + const tsHeading = "^#{1,4}\\s+.*(?:TypeScript|JavaScript|TS|JS|\\.tsx?)\\b[^#\\n]*[Ee]xemption"; + const anyHeading = "^#{1,4}\\s"; + let inTable = false; + const lines = split(text, "\n"); + let i = 0; + const lines_len = ((lines).length); + while ((i < lines_len)) { const line = lines[i]; if (__as_regexMatch(line, tsHeading)) { inTable = true; i = (i + 1); continue; } if ((inTable && __as_regexMatch(line, anyHeading))) { inTable = false; i = (i + 1); continue; } if (((inTable && (((line).length) > 0)) && (__as_strSub(line, 0, 1) === "|"))) { if (__as_regexMatch(line, "^\\|\\s*`[^`]+`")) { const parts = split(line, "`"); if ((((parts).length) >= 3)) { const raw = parts[1]; exemptions = __as_concat(exemptions, [({ raw: raw, rx: globToRegex(raw) })]); } } } i = (i + 1); } + return exemptions; +} + +function loadExemptionsFromAllowlistFile() { + let exemptions = []; + const text = (() => { try { return (() => { return Deno.readTextFileSync(".governance-allowlist"); })(); } catch (__e) { return ""; } })(); + if ((text === "")) { return exemptions; } + const lines = split(text, "\n"); + let i = 0; + const lines_len = ((lines).length); + while ((i < lines_len)) { const rawLine = lines[i]; const line = String(rawLine).trim(); if (((line === "") || (__as_strSub(line, 0, 1) === "#"))) { i = (i + 1); continue; } exemptions = __as_concat(exemptions, [({ raw: line, rx: globToRegex(line) })]); i = (i + 1); } + return exemptions; +} + +function loadExemptions() { + return __as_concat(loadExemptionsFromClaudeMd(), loadExemptionsFromAllowlistFile()); +} + +function isExempt(p, exemptions) { + let i = 0; + const ex_len = ((exemptions).length); + while ((i < ex_len)) { const e = exemptions[i]; if (__as_regexMatch(p, e.rx)) { return true; } let bare = e.raw; while (((((bare).length) > 0) && ((__as_strSub(bare, 0, 1) === ".") || (__as_strSub(bare, 0, 1) === "/")))) { bare = __as_strSub(bare, 1, (((bare).length) - 1)); } if ((p === bare)) { return true; } if ((ends_with(e.raw, "/") && __as_regexMatch(p, __as_concat("^", bare)))) { return true; } i = (i + 1); } + return false; +} + +export function main() { + const exemptions = loadExemptions(); + let found = []; + const all_files = (() => { try { return (() => { return __as_walkRecursive("."); })(); } catch (__e) { return []; } })(); + let i = 0; + const af_len = ((all_files).length); + while ((i < af_len)) { const f = all_files[i]; if ((ends_with(f, ".ts") || ends_with(f, ".tsx"))) { let skip = false; const segs = split(f, "/"); let j = 0; const segs_len = ((segs).length); while ((j < segs_len)) { const seg = segs[j]; if (((((((seg).length) > 0) && (__as_strSub(seg, 0, 1) === ".")) && (seg !== ".")) && (seg !== ".."))) { skip = true; } j = (j + 1); } if ((!skip)) { found = __as_concat(found, [f]); } } i = (i + 1); } + let bad = []; + let k = 0; + const found_len = ((found).length); + while ((k < found_len)) { const f = found[k]; if (((!builtinAllowed(f)) && (!isExempt(f, exemptions)))) { bad = __as_concat(bad, [f]); } k = (k + 1); } + if ((((bad).length) > 0)) { (console.error("\u274C TypeScript files detected outside the allowlist.\n"), 0); let m = 0; const bad_len = ((bad).length); while ((m < bad_len)) { const f = bad[m]; (console.error(__as_concat(" ", f)), 0); m = (m + 1); } (console.error(""), 0); (console.error("To resolve, choose one:"), 0); (console.error(" (a) migrate the file to AffineScript"), 0); (console.error(" (b) move to an allowlisted bridge path"), 0); (console.error(" (c) add an entry to a 'TypeScript Exemptions' table in .claude/CLAUDE.md (Layer 2)"), 0); (console.error(" (d) add a line to .governance-allowlist at the repo root (Layer 2.5 \u2014 typed infrastructure file)"), 0); (console.error(""), 0); (console.error("See docs/EXEMPTION-MECHANISMS.adoc for the full mechanism reference."), 0); if ((((exemptions).length) > 0)) { (console.error(__as_concat(__as_concat("\n(Currently ", String(((exemptions).length))), " exemption(s) parsed across both layers.)")), 0); } return Deno.exit(1); } + println(__as_concat(__as_concat("\u2705 No TypeScript files outside allowlist (", String(((exemptions).length))), " per-repo exemption(s) parsed across CLAUDE.md + .governance-allowlist).")); + return 0; +} + +await main(); From 4ab55da40b5ce649eed4625ddf83b8602112a9f2 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Sat, 30 May 2026 19:49:13 +0100 Subject: [PATCH 2/2] docs(exemptions): align Layer-4 references with .affine source / .deno.js runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the workflow swap in this PR. Three updates to `docs/EXEMPTION-MECHANISMS.adoc`: 1. Layer 4a description — point at `scripts/check-ts-allowlist.affine` as the source of truth, note the compiled `.deno.js` is what the workflow actually runs. 2. History — add a fourth bullet covering standards#283 / #310 / #311: the TS→AffineScript port arc, with the dual-target rationale spelled out (`.ts` kept for regression suite + parallel-validation; `.deno.js` is the workflow target). 3. Cross-references — replace the single `.ts` line with three lines covering all three files (.affine source / .deno.js runtime / .ts archetype) and what each is for. Reduces "which file should I edit?" ambiguity for the next contributor. No behavioural change; pure doc alignment so the prose matches the workflow + source layout after #311 lands. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/EXEMPTION-MECHANISMS.adoc | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/EXEMPTION-MECHANISMS.adoc b/docs/EXEMPTION-MECHANISMS.adoc index 1db1acdb..8378d3e2 100644 --- a/docs/EXEMPTION-MECHANISMS.adoc +++ b/docs/EXEMPTION-MECHANISMS.adoc @@ -137,8 +137,10 @@ across the estate. Three sub-layers: === 4a: Built-in path / filename allowlist -Hard-coded in `scripts/check-ts-allowlist.ts`. Covers paths that are -*always* exempt regardless of per-repo configuration: +Hard-coded in `scripts/check-ts-allowlist.affine` (source of truth; +compiled to `scripts/check-ts-allowlist.deno.js` which the workflow +invokes). Covers paths that are *always* exempt regardless of per-repo +configuration: * Directory segments: `bindings`, `tests`, `test`, `scripts`, `mcp-adapter`, `cli`, `vendor`, `examples`, `ffi`, `node_modules`, @@ -230,11 +232,23 @@ sufficient. Most repos will pick one or the other. + new typed-infrastructure file. Owner picked the Layer-2.5 approach (standards#185) over the minimal regex-only fix (standards#183). This document seeds the doctrine. +* AffineScript port (standards#283 seed, #310 compile/runtime fixes, + #311 workflow swap): `.ts` → `.affine` self-referential port under + the TS→AffineScript campaign (#239 / #241 STEP 2). The `.ts` + archetype is kept for the regression suite and parallel-validation; + the workflow now runs the compiled `.deno.js`. Retirement of the + `.ts` is a follow-up after the dual-target window. == Cross-references * `docs/HYPATIA-BASELINE-FORMAT.adoc` — the baseline file format. * `.machine_readable/hypatia-baseline.schema.json` — machine schema. -* `scripts/check-ts-allowlist.ts` — the Deno detector behind Layer 4. +* `scripts/check-ts-allowlist.affine` — the AffineScript source of + truth for the Layer 4 detector (since standards#283 / #310 / #311). +* `scripts/check-ts-allowlist.deno.js` — the compiled artifact the + governance workflow runs. +* `scripts/check-ts-allowlist.ts` — the Deno archetype, retained as the + regression-suite target (`scripts/tests/check-ts-allowlist-test.sh`) + and for parallel-validation during the TS→AS dual-target window. * `hyperpolymath/standards#????` — proposal that landed this consumer. * `hyperpolymath/hypatia` — the scanner that emits findings.