|
5 | 5 | # Enforces: No TypeScript, No Go, No Python (except SaltStack), No npm |
6 | 6 | # Allows: ReScript, Deno, WASM, Rust, OCaml, Haskell, Guile/Scheme |
7 | 7 |
|
8 | | -permissions: |
9 | | - contents: read |
10 | | - |
11 | 8 | name: RSR Anti-Pattern Check |
12 | 9 |
|
13 | 10 | on: |
|
16 | 13 | pull_request: |
17 | 14 | branches: [main, master, develop] |
18 | 15 |
|
| 16 | + |
| 17 | +permissions: |
| 18 | + contents: read |
| 19 | + |
19 | 20 | jobs: |
20 | 21 | antipattern-check: |
21 | 22 | runs-on: ubuntu-latest |
| 23 | + permissions: |
| 24 | + contents: read |
22 | 25 | steps: |
23 | | - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 |
| 26 | + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 |
24 | 27 |
|
25 | 28 | - name: Check for TypeScript |
26 | 29 | run: | |
27 | | - if find . -name "*.ts" -o -name "*.tsx" | grep -v node_modules | grep -q .; then |
28 | | - echo "❌ TypeScript files detected - use ReScript instead" |
29 | | - find . -name "*.ts" -o -name "*.tsx" | grep -v node_modules |
30 | | - exit 1 |
31 | | - fi |
32 | | - echo "✅ No TypeScript files" |
| 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 |
33 | 140 |
|
34 | 141 | - name: Check for Go |
35 | 142 | run: | |
|
0 commit comments