Skip to content

Commit 3c5dbb0

Browse files
ci(antipattern): TS check reads .claude/CLAUDE.md exemption table (#19)
Replaces the static grep-chain TS check with a python step that: - Applies the universal builtin allowlist (bindings/, tests/, scripts/, mcp-adapter/, *vscode*/, cli/, mod.ts, lsp-server.ts, deno-*/, etc.) - **Reads the per-repo `.claude/CLAUDE.md` 'TypeScript Exemptions' table at CI time** and treats each `Path` row as allowed. Why: the previous static allowlist required editing the workflow whenever a legitimate per-repo exemption was needed. This made the per-repo `.claude/CLAUDE.md` exemption table (the documented source of truth) and the workflow drift apart. Now they're the same single source. To exempt a path going forward: add one row to `.claude/CLAUDE.md`'s 'TypeScript Exemptions' table — no workflow edit required. To migrate off TS: see Human_Programming_Guide.adoc migration chapter (in affinescript repo).
1 parent 5d6acc8 commit 3c5dbb0

1 file changed

Lines changed: 79 additions & 32 deletions

File tree

.github/workflows/rsr-antipattern.yml

Lines changed: 79 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,38 +20,85 @@ jobs:
2020

2121
- name: Check for TypeScript
2222
run: |
23-
# Allowlist (TS legitimate as a bridge/adapter to a non-ReScript ecosystem):
24-
# bindings/ - language bindings (Deno/TS/AssemblyScript FFI)
25-
# *.d.ts - TypeScript type declarations for ReScript FFI
26-
# tests/, test/ - Deno test runners
27-
# scripts/ - Deno build scripts
28-
# mcp-adapter/ - MCP server adapters (MCP is Deno/TS-typed by spec)
29-
# *vscode* - VSCode extensions (TS is the ecosystem default)
30-
# cli/ - CLI entry points (Deno scripts)
31-
# mod.ts - canonical Deno module entrypoint
32-
# *lsp-server.ts, *lsp.ts - Language Server Protocol implementations
33-
# deno-*/ - subprojects explicitly named for Deno
34-
TS_FILES=$(find . \( -name "*.ts" -o -name "*.tsx" \) \
35-
| grep -v node_modules \
36-
| grep -v '/bindings/' \
37-
| grep -v '\.d\.ts$' \
38-
| grep -v '/tests/' \
39-
| grep -v '/test/' \
40-
| grep -v '/scripts/' \
41-
| grep -v '/mcp-adapter/' \
42-
| grep -Ev '/[^/]*vscode[^/]*/' \
43-
| grep -v '/cli/' \
44-
| grep -v '/mod\.ts$' \
45-
| grep -Ev 'lsp[-_]?server\.ts$' \
46-
| grep -Ev '[/-]lsp\.ts$' \
47-
| grep -Ev '/deno-[^/]+/' \
48-
|| true)
49-
if [ -n "$TS_FILES" ]; then
50-
echo "❌ TypeScript files detected - use ReScript instead"
51-
echo "$TS_FILES"
52-
exit 1
53-
fi
54-
echo "✅ No TypeScript files outside allowlisted bridge/adapter paths"
23+
python3 << 'PYEOF'
24+
import re, sys, fnmatch, pathlib
25+
26+
# Universal builtin allowlist — bridges that need no per-repo declaration.
27+
# Files matching any of these patterns are always allowed.
28+
BUILTIN_GLOBS = [
29+
'*.d.ts',
30+
'**/bindings/**',
31+
'**/tests/**', '**/test/**',
32+
'**/scripts/**',
33+
'**/mcp-adapter/**',
34+
'**/*vscode*/**',
35+
'**/cli/**',
36+
'**/mod.ts',
37+
'**/lsp-server.ts', '**/lsp_server.ts', '**/lsp.ts', '**/*-lsp.ts',
38+
'**/deno-*/**',
39+
'**/node_modules/**',
40+
'**/vendor/**',
41+
'**/examples/**',
42+
'**/ffi/**',
43+
]
44+
45+
# Per-repo exemptions parsed from .claude/CLAUDE.md "TypeScript Exemptions" table.
46+
# Single source of truth — adding a row here unblocks CI for that path.
47+
# Format expected:
48+
# ### TypeScript Exemptions ...
49+
# | Path | Files | Rationale | Unblock condition |
50+
# |---|---|---|---|
51+
# | `path/to/file.ts` | 1 | ... | ... |
52+
# | `dir/*.ts` | 6 | ... | ... |
53+
exemptions = []
54+
claude_md = pathlib.Path('.claude/CLAUDE.md')
55+
if claude_md.exists():
56+
in_table = False
57+
for line in claude_md.read_text(encoding='utf-8').splitlines():
58+
if re.search(r'TypeScript [Ee]xemptions', line):
59+
in_table = True
60+
continue
61+
if in_table and line.startswith(('### ', '## ', '# ')):
62+
break
63+
if in_table and line.startswith('|'):
64+
m = re.match(r'\|\s*`([^`]+)`', line)
65+
if m:
66+
exemptions.append(m.group(1))
67+
68+
# Find all .ts and .tsx files
69+
found = []
70+
for ext in ('ts', 'tsx'):
71+
found.extend(str(p) for p in pathlib.Path('.').rglob(f'*.{ext}'))
72+
73+
def allowed(path):
74+
p = path.lstrip('./')
75+
for g in BUILTIN_GLOBS + exemptions:
76+
if fnmatch.fnmatchcase(p, g):
77+
return True
78+
# also treat glob ending with / as a directory prefix
79+
base = g.rstrip('/').rstrip('*').rstrip('/')
80+
if base and (p == base or p.startswith(base + '/')):
81+
return True
82+
return False
83+
84+
bad = sorted(f for f in found if not allowed(f))
85+
if bad:
86+
print("❌ TypeScript files detected outside the allowlist.\n")
87+
for f in bad:
88+
print(f" {f}")
89+
print()
90+
print("To resolve, either:")
91+
print(" (a) migrate the file to AffineScript")
92+
print(" (see Human_Programming_Guide.adoc migration chapter), OR")
93+
print(" (b) move it to an allowlisted bridge path")
94+
print(" (bindings/, tests/, scripts/, mcp-adapter/, *vscode*/, cli/, deno-*/, etc.), OR")
95+
print(" (c) add an entry to the 'TypeScript Exemptions' table in .claude/CLAUDE.md")
96+
print(" with rationale + unblock condition.")
97+
if exemptions:
98+
print(f"\n(Currently {len(exemptions)} exemption(s) parsed from .claude/CLAUDE.md.)")
99+
sys.exit(1)
100+
print(f"✅ No TypeScript files outside allowlist ({len(exemptions)} per-repo exemption(s) parsed).")
101+
PYEOF
55102
56103
- name: Check for Go
57104
run: |

0 commit comments

Comments
 (0)