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