11// SPDX-License-Identifier: MPL-2.0
2- // SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
2+ // SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
33//
44// check-ts-allowlist.ts — Deno port of the inline python3 heredoc that used
55// to live in `.github/workflows/governance-reusable.yml` step
1212// self-loop fixed in hypatia#328. This script eliminates the violation.
1313//
1414// Behaviour MUST stay byte-identical to the previous Python implementation:
15- // * Walk every `*.ts` / `*.tsx` file under cwd, skipping dotted dirs.
15+ // * Walk every `*.ts` / `*.tsx` file under cwd, skipping dotted dirs
16+ // and treating `.ts.bak` / `.tsx.bak` backups as banned TS artifacts.
1617// * Allow files in the built-in directory/path allowlist
1718// (bindings/tests/scripts/vendor/examples/ffi/benchmarks/cli, plus any
1819// segment containing 'vscode' or starting with 'deno-').
@@ -72,6 +73,27 @@ function globToRegex(g: string): RegExp {
7273
7374interface Exemption { raw : string ; rx : RegExp ; }
7475
76+ function normalizeRepoPath ( p : string ) : string {
77+ let out = p . trim ( ) ;
78+ while ( out . length > 0 && ( out [ 0 ] === "." || out [ 0 ] === "/" ) ) {
79+ out = out . slice ( 1 ) ;
80+ }
81+ return out ;
82+ }
83+
84+ function normalizeExemptionCell ( cell : string ) : string {
85+ let out = cell . trim ( ) ;
86+ const codeSpan = out . match ( / ^ ` ( [ ^ ` ] + ) ` $ / ) ?? out . match ( / ^ ` ( [ ^ ` ] + ) ` / ) ;
87+ if ( codeSpan ) {
88+ out = codeSpan [ 1 ] . trim ( ) ;
89+ }
90+ return normalizeRepoPath ( out ) ;
91+ }
92+
93+ function nonExemptionCell ( cell : string ) : boolean {
94+ return cell === "" || / ^ : ? - { 3 , } : ? $ / . test ( cell ) || / ^ p a t h \b / i. test ( cell ) ;
95+ }
96+
7597async function loadExemptionsFromClaudeMd ( ) : Promise < Exemption [ ] > {
7698 // Layer 2 — heading-table exemptions parsed from `.claude/CLAUDE.md`.
7799 //
@@ -110,10 +132,14 @@ async function loadExemptionsFromClaudeMd(): Promise<Exemption[]> {
110132 inTable = false ;
111133 continue ;
112134 }
113- if ( inTable && line . startsWith ( "|" ) ) {
114- const m = line . match ( / ^ \| \s * ` ( [ ^ ` ] + ) ` / ) ;
115- if ( m ) {
116- exemptions . push ( { raw : m [ 1 ] , rx : globToRegex ( m [ 1 ] ) } ) ;
135+ const tableLine = line . trim ( ) ;
136+ if ( inTable && tableLine . startsWith ( "|" ) ) {
137+ const cells = tableLine . split ( "|" ) ;
138+ if ( cells . length >= 3 ) {
139+ const raw = normalizeExemptionCell ( cells [ 1 ] ) ;
140+ if ( ! nonExemptionCell ( raw ) ) {
141+ exemptions . push ( { raw, rx : globToRegex ( raw ) } ) ;
142+ }
117143 }
118144 }
119145 }
@@ -135,7 +161,7 @@ async function loadExemptionsFromAllowlistFile(): Promise<Exemption[]> {
135161 return exemptions ;
136162 }
137163 for ( const rawLine of text . split ( "\n" ) ) {
138- const line = rawLine . trim ( ) ;
164+ const line = normalizeExemptionCell ( rawLine ) ;
139165 if ( line === "" || line . startsWith ( "#" ) ) continue ;
140166 exemptions . push ( { raw : line , rx : globToRegex ( line ) } ) ;
141167 }
@@ -149,16 +175,21 @@ async function loadExemptions(): Promise<Exemption[]> {
149175}
150176
151177function exempt ( p : string , exemptions : Exemption [ ] ) : boolean {
178+ const target = normalizeRepoPath ( p ) ;
152179 for ( const e of exemptions ) {
153- if ( e . rx . test ( p ) ) return true ;
154- let bare = e . raw ;
155- while ( bare . length > 0 && ( bare [ 0 ] === "." || bare [ 0 ] === "/" ) ) bare = bare . slice ( 1 ) ;
156- if ( p === bare ) return true ;
157- if ( e . raw . endsWith ( "/" ) && p . startsWith ( bare ) ) return true ;
180+ if ( e . rx . test ( target ) ) return true ;
181+ const bare = normalizeRepoPath ( e . raw ) ;
182+ if ( target === bare ) return true ;
183+ if ( bare . endsWith ( "/" ) && target . startsWith ( bare ) ) return true ;
158184 }
159185 return false ;
160186}
161187
188+ function isTypeScriptArtifact ( name : string ) : boolean {
189+ return name . endsWith ( ".ts" ) || name . endsWith ( ".tsx" ) ||
190+ name . endsWith ( ".ts.bak" ) || name . endsWith ( ".tsx.bak" ) ;
191+ }
192+
162193async function * walkTs ( dir : string ) : AsyncIterable < string > {
163194 for await ( const entry of Deno . readDir ( dir ) ) {
164195 const name = entry . name ;
@@ -168,7 +199,7 @@ async function* walkTs(dir: string): AsyncIterable<string> {
168199 if ( entry . isDirectory ) {
169200 yield * walkTs ( full ) ;
170201 } else if ( entry . isFile ) {
171- if ( name . endsWith ( ".ts" ) || name . endsWith ( ".tsx" ) ) {
202+ if ( isTypeScriptArtifact ( name ) ) {
172203 yield full ;
173204 }
174205 }
0 commit comments