@@ -21,7 +21,115 @@ jobs:
2121 - name : Check for TypeScript
2222 run : |
2323 python3 << 'PYEOF'
24- import re, sys, fnmatch, pathlib
24+ import re, sys, pathlib
25+
26+ # Universal allowlist — bridges and conventions that need no per-repo declaration.
27+ # Implemented as explicit string predicates rather than glob patterns so that
28+ # top-level directories (e.g. tests/foo.ts) are matched the same as nested ones,
29+ # which fnmatch's * cannot do reliably.
30+ DIR_NAMES_ALLOWED = {
31+ 'bindings', 'tests', 'test', 'scripts',
32+ 'mcp-adapter', 'cli', 'vendor', 'examples', 'ffi',
33+ 'node_modules', 'benchmarks',
34+ }
35+
36+ def builtin_allowed(p):
37+ # `p` is a posix-style path with no leading ./
38+ # 1. Type declaration files
39+ if p.endswith('.d.ts'):
40+ return True
41+ # 2. Canonical Deno entrypoint filenames
42+ base = p.rsplit('/', 1)[-1]
43+ if base == 'mod.ts':
44+ return True
45+ # 3. LSP server files (filename suffixes)
46+ if base in ('lsp-server.ts', 'lsp_server.ts', 'lsp.ts') or base.endswith('-lsp.ts'):
47+ return True
48+ # 4. Benchmark files (filename suffixes)
49+ if base.endswith('.bench.ts') or base.endswith('_bench.ts'):
50+ return True
51+ # 5. Any directory segment (excluding basename) matches an allowed dir
52+ segs = p.split('/')
53+ for s in segs[:-1]:
54+ if s in DIR_NAMES_ALLOWED:
55+ return True
56+ # vscode-anything or anything-vscode
57+ if 'vscode' in s:
58+ return True
59+ # deno-named subprojects
60+ if s.startswith('deno-'):
61+ return True
62+ return False
63+
64+ # Per-repo exemptions parsed from .claude/CLAUDE.md "TypeScript Exemptions" table.
65+ # This is the documented single source of truth: adding one row here unblocks CI.
66+ # Glob characters: '*' and '**' both mean "any chars including /". This loose
67+ # interpretation matches user intent when an exemption row reads, e.g.,
68+ # `affinescript-deno-test/*.ts` (covering nested files too).
69+ def glob_to_regex(g):
70+ out = []
71+ for c in g.lstrip('./'):
72+ if c == '*': out.append('.*')
73+ elif c == '?': out.append('.')
74+ elif c in '.+(){}[]|^$\\': out.append(re.escape(c))
75+ else: out.append(c)
76+ return re.compile('^' + ''.join(out) + '$')
77+
78+ exemption_patterns = []
79+ claude_md = pathlib.Path('.claude/CLAUDE.md')
80+ if claude_md.exists():
81+ in_table = False
82+ for line in claude_md.read_text(encoding='utf-8').splitlines():
83+ if re.search(r'TypeScript [Ee]xemptions', line):
84+ in_table = True
85+ continue
86+ if in_table and line.startswith(('### ', '## ', '# ')):
87+ break
88+ if in_table and line.startswith('|'):
89+ m = re.match(r'\|\s*`([^`]+)`', line)
90+ if m:
91+ exemption_patterns.append((m.group(1), glob_to_regex(m.group(1))))
92+
93+ def exempt(p):
94+ for raw, regex in exemption_patterns:
95+ if regex.match(p):
96+ return True
97+ # Also allow exact-path matches and prefix matches for paths
98+ # ending in `/`
99+ if p == raw.lstrip('./'):
100+ return True
101+ if raw.endswith('/') and p.startswith(raw.lstrip('./')):
102+ return True
103+ return False
104+
105+ # Find all .ts and .tsx files (excluding common dot-dirs that find normally skips)
106+ found = []
107+ for ext in ('ts', 'tsx'):
108+ for p in pathlib.Path('.').rglob(f'*.{ext}'):
109+ parts = p.parts
110+ if any(part.startswith('.') and part not in ('.', '..') for part in parts):
111+ continue
112+ found.append(p.as_posix().lstrip('./'))
113+
114+ bad = sorted(f for f in found if not (builtin_allowed(f) or exempt(f)))
115+ if bad:
116+ print("❌ TypeScript files detected outside the allowlist.\n")
117+ for f in bad:
118+ print(f" {f}")
119+ print()
120+ print("To resolve, choose one:")
121+ print(" (a) migrate the file to AffineScript")
122+ print(" (see Human_Programming_Guide.adoc 'Migrating from -script Languages')")
123+ print(" (b) move to an allowlisted bridge path")
124+ print(" (bindings/, tests/, test/, scripts/, benchmarks/, mcp-adapter/,")
125+ print(" *vscode*/, cli/, deno-*/, vendor/, examples/, ffi/)")
126+ print(" (c) add an entry to the 'TypeScript Exemptions' table in .claude/CLAUDE.md")
127+ print(" with rationale + unblock condition")
128+ if exemption_patterns:
129+ print(f"\n(Currently {len(exemption_patterns)} exemption(s) parsed from .claude/CLAUDE.md.)")
130+ sys.exit(1)
131+ print(f"✅ No TypeScript files outside allowlist ({len(exemption_patterns)} per-repo exemption(s) parsed).")
132+ PYEOF
25133
26134 # Universal builtin allowlist — bridges that need no per-repo declaration.
27135 # Files matching any of these patterns are always allowed.
0 commit comments