@@ -27,38 +27,193 @@ jobs:
2727
2828 - name : Check for TypeScript
2929 run : |
30- # Allowlist (TS legitimate as a bridge/adapter to a non-ReScript ecosystem):
31- # bindings/ - language bindings (Deno/TS/AssemblyScript FFI)
32- # *.d.ts - TypeScript type declarations for ReScript FFI
33- # tests/, test/ - Deno test runners
34- # scripts/ - Deno build scripts
35- # mcp-adapter/ - MCP server adapters (MCP is Deno/TS-typed by spec)
36- # *vscode* - VSCode extensions (TS is the ecosystem default)
37- # cli/ - CLI entry points (Deno scripts)
38- # mod.ts - canonical Deno module entrypoint
39- # *lsp-server.ts, *lsp.ts - Language Server Protocol implementations
40- # deno-*/ - subprojects explicitly named for Deno
41- TS_FILES=$(find . \( -name "*.ts" -o -name "*.tsx" \) \
42- | grep -v node_modules \
43- | grep -v '/bindings/' \
44- | grep -v '\.d\.ts$' \
45- | grep -v '/tests/' \
46- | grep -v '/test/' \
47- | grep -v '/scripts/' \
48- | grep -v '/mcp-adapter/' \
49- | grep -Ev '/[^/]*vscode[^/]*/' \
50- | grep -v '/cli/' \
51- | grep -v '/mod\.ts$' \
52- | grep -Ev 'lsp[-_]?server\.ts$' \
53- | grep -Ev '[/-]lsp\.ts$' \
54- | grep -Ev '/deno-[^/]+/' \
55- || true)
56- if [ -n "$TS_FILES" ]; then
57- echo "❌ TypeScript files detected - use ReScript instead"
58- echo "$TS_FILES"
59- exit 1
60- fi
61- echo "✅ No TypeScript files outside allowlisted bridge/adapter paths"
30+ python3 << 'PYEOF'
31+ import re, sys, pathlib
32+
33+ # Universal allowlist — bridges and conventions that need no per-repo declaration.
34+ # Implemented as explicit string predicates rather than glob patterns so that
35+ # top-level directories (e.g. tests/foo.ts) are matched the same as nested ones,
36+ # which fnmatch's * cannot do reliably.
37+ DIR_NAMES_ALLOWED = {
38+ 'bindings', 'tests', 'test', 'scripts',
39+ 'mcp-adapter', 'cli', 'vendor', 'examples', 'ffi',
40+ 'node_modules', 'benchmarks',
41+ }
42+
43+ def builtin_allowed(p):
44+ # `p` is a posix-style path with no leading ./
45+ # 1. Type declaration files
46+ if p.endswith('.d.ts'):
47+ return True
48+ # 2. Canonical Deno entrypoint filenames
49+ base = p.rsplit('/', 1)[-1]
50+ if base == 'mod.ts':
51+ return True
52+ # 3. LSP server files (filename suffixes)
53+ if base in ('lsp-server.ts', 'lsp_server.ts', 'lsp.ts') or base.endswith('-lsp.ts'):
54+ return True
55+ # 4. Benchmark files (filename suffixes)
56+ if base.endswith('.bench.ts') or base.endswith('_bench.ts'):
57+ return True
58+ # 5. Any directory segment (excluding basename) matches an allowed dir
59+ segs = p.split('/')
60+ for s in segs[:-1]:
61+ if s in DIR_NAMES_ALLOWED:
62+ return True
63+ # vscode-anything or anything-vscode
64+ if 'vscode' in s:
65+ return True
66+ # deno-named subprojects
67+ if s.startswith('deno-'):
68+ return True
69+ return False
70+
71+ # Per-repo exemptions parsed from .claude/CLAUDE.md "TypeScript Exemptions" table.
72+ # This is the documented single source of truth: adding one row here unblocks CI.
73+ # Glob characters: '*' and '**' both mean "any chars including /". This loose
74+ # interpretation matches user intent when an exemption row reads, e.g.,
75+ # `affinescript-deno-test/*.ts` (covering nested files too).
76+ def glob_to_regex(g):
77+ out = []
78+ for c in g.lstrip('./'):
79+ if c == '*': out.append('.*')
80+ elif c == '?': out.append('.')
81+ elif c in '.+(){}[]|^$\\': out.append(re.escape(c))
82+ else: out.append(c)
83+ return re.compile('^' + ''.join(out) + '$')
84+
85+ exemption_patterns = []
86+ claude_md = pathlib.Path('.claude/CLAUDE.md')
87+ if claude_md.exists():
88+ in_table = False
89+ for line in claude_md.read_text(encoding='utf-8').splitlines():
90+ if re.search(r'TypeScript [Ee]xemptions', line):
91+ in_table = True
92+ continue
93+ if in_table and line.startswith(('### ', '## ', '# ')):
94+ break
95+ if in_table and line.startswith('|'):
96+ m = re.match(r'\|\s*`([^`]+)`', line)
97+ if m:
98+ exemption_patterns.append((m.group(1), glob_to_regex(m.group(1))))
99+
100+ def exempt(p):
101+ for raw, regex in exemption_patterns:
102+ if regex.match(p):
103+ return True
104+ # Also allow exact-path matches and prefix matches for paths
105+ # ending in `/`
106+ if p == raw.lstrip('./'):
107+ return True
108+ if raw.endswith('/') and p.startswith(raw.lstrip('./')):
109+ return True
110+ return False
111+
112+ # Find all .ts and .tsx files (excluding common dot-dirs that find normally skips)
113+ found = []
114+ for ext in ('ts', 'tsx'):
115+ for p in pathlib.Path('.').rglob(f'*.{ext}'):
116+ parts = p.parts
117+ if any(part.startswith('.') and part not in ('.', '..') for part in parts):
118+ continue
119+ found.append(p.as_posix().lstrip('./'))
120+
121+ bad = sorted(f for f in found if not (builtin_allowed(f) or exempt(f)))
122+ if bad:
123+ print("❌ TypeScript files detected outside the allowlist.\n")
124+ for f in bad:
125+ print(f" {f}")
126+ print()
127+ print("To resolve, choose one:")
128+ print(" (a) migrate the file to AffineScript")
129+ print(" (see Human_Programming_Guide.adoc 'Migrating from -script Languages')")
130+ print(" (b) move to an allowlisted bridge path")
131+ print(" (bindings/, tests/, test/, scripts/, benchmarks/, mcp-adapter/,")
132+ print(" *vscode*/, cli/, deno-*/, vendor/, examples/, ffi/)")
133+ print(" (c) add an entry to the 'TypeScript Exemptions' table in .claude/CLAUDE.md")
134+ print(" with rationale + unblock condition")
135+ if exemption_patterns:
136+ print(f"\n(Currently {len(exemption_patterns)} exemption(s) parsed from .claude/CLAUDE.md.)")
137+ sys.exit(1)
138+ print(f"✅ No TypeScript files outside allowlist ({len(exemption_patterns)} per-repo exemption(s) parsed).")
139+ PYEOF
140+
141+ # Universal builtin allowlist — bridges that need no per-repo declaration.
142+ # Files matching any of these patterns are always allowed.
143+ BUILTIN_GLOBS = [
144+ '*.d.ts',
145+ '**/bindings/**',
146+ '**/tests/**', '**/test/**',
147+ '**/scripts/**',
148+ '**/mcp-adapter/**',
149+ '**/*vscode*/**',
150+ '**/cli/**',
151+ '**/mod.ts',
152+ '**/lsp-server.ts', '**/lsp_server.ts', '**/lsp.ts', '**/*-lsp.ts',
153+ '**/deno-*/**',
154+ '**/node_modules/**',
155+ '**/vendor/**',
156+ '**/examples/**',
157+ '**/ffi/**',
158+ ]
159+
160+ # Per-repo exemptions parsed from .claude/CLAUDE.md "TypeScript Exemptions" table.
161+ # Single source of truth — adding a row here unblocks CI for that path.
162+ # Format expected:
163+ # ### TypeScript Exemptions ...
164+ # | Path | Files | Rationale | Unblock condition |
165+ # |---|---|---|---|
166+ # | `path/to/file.ts` | 1 | ... | ... |
167+ # | `dir/*.ts` | 6 | ... | ... |
168+ exemptions = []
169+ claude_md = pathlib.Path('.claude/CLAUDE.md')
170+ if claude_md.exists():
171+ in_table = False
172+ for line in claude_md.read_text(encoding='utf-8').splitlines():
173+ if re.search(r'TypeScript [Ee]xemptions', line):
174+ in_table = True
175+ continue
176+ if in_table and line.startswith(('### ', '## ', '# ')):
177+ break
178+ if in_table and line.startswith('|'):
179+ m = re.match(r'\|\s*`([^`]+)`', line)
180+ if m:
181+ exemptions.append(m.group(1))
182+
183+ # Find all .ts and .tsx files
184+ found = []
185+ for ext in ('ts', 'tsx'):
186+ found.extend(str(p) for p in pathlib.Path('.').rglob(f'*.{ext}'))
187+
188+ def allowed(path):
189+ p = path.lstrip('./')
190+ for g in BUILTIN_GLOBS + exemptions:
191+ if fnmatch.fnmatchcase(p, g):
192+ return True
193+ # also treat glob ending with / as a directory prefix
194+ base = g.rstrip('/').rstrip('*').rstrip('/')
195+ if base and (p == base or p.startswith(base + '/')):
196+ return True
197+ return False
198+
199+ bad = sorted(f for f in found if not allowed(f))
200+ if bad:
201+ print("❌ TypeScript files detected outside the allowlist.\n")
202+ for f in bad:
203+ print(f" {f}")
204+ print()
205+ print("To resolve, either:")
206+ print(" (a) migrate the file to AffineScript")
207+ print(" (see Human_Programming_Guide.adoc migration chapter), OR")
208+ print(" (b) move it to an allowlisted bridge path")
209+ print(" (bindings/, tests/, scripts/, mcp-adapter/, *vscode*/, cli/, deno-*/, etc.), OR")
210+ print(" (c) add an entry to the 'TypeScript Exemptions' table in .claude/CLAUDE.md")
211+ print(" with rationale + unblock condition.")
212+ if exemptions:
213+ print(f"\n(Currently {len(exemptions)} exemption(s) parsed from .claude/CLAUDE.md.)")
214+ sys.exit(1)
215+ print(f"✅ No TypeScript files outside allowlist ({len(exemptions)} per-repo exemption(s) parsed).")
216+ PYEOF
62217
63218 - name : Check for Go
64219 run : |
0 commit comments