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