Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 109 additions & 1 deletion .github/workflows/rsr-antipattern.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,115 @@ jobs:
- name: Check for TypeScript
run: |
python3 << 'PYEOF'
import re, sys, fnmatch, pathlib
import re, sys, pathlib

# Universal allowlist — bridges and conventions that need no per-repo declaration.
# Implemented as explicit string predicates rather than glob patterns so that
# top-level directories (e.g. tests/foo.ts) are matched the same as nested ones,
# which fnmatch's * cannot do reliably.
DIR_NAMES_ALLOWED = {
'bindings', 'tests', 'test', 'scripts',
'mcp-adapter', 'cli', 'vendor', 'examples', 'ffi',
'node_modules', 'benchmarks',
}

def builtin_allowed(p):
# `p` is a posix-style path with no leading ./
# 1. Type declaration files
if p.endswith('.d.ts'):
return True
# 2. Canonical Deno entrypoint filenames
base = p.rsplit('/', 1)[-1]
if base == 'mod.ts':
return True
# 3. LSP server files (filename suffixes)
if base in ('lsp-server.ts', 'lsp_server.ts', 'lsp.ts') or base.endswith('-lsp.ts'):
return True
# 4. Benchmark files (filename suffixes)
if base.endswith('.bench.ts') or base.endswith('_bench.ts'):
return True
# 5. Any directory segment (excluding basename) matches an allowed dir
segs = p.split('/')
for s in segs[:-1]:
if s in DIR_NAMES_ALLOWED:
return True
# vscode-anything or anything-vscode
if 'vscode' in s:
return True
# deno-named subprojects
if s.startswith('deno-'):
return True
return False

# Per-repo exemptions parsed from .claude/CLAUDE.md "TypeScript Exemptions" table.
# This is the documented single source of truth: adding one row here unblocks CI.
# Glob characters: '*' and '**' both mean "any chars including /". This loose
# interpretation matches user intent when an exemption row reads, e.g.,
# `affinescript-deno-test/*.ts` (covering nested files too).
def glob_to_regex(g):
out = []
for c in g.lstrip('./'):
if c == '*': out.append('.*')
elif c == '?': out.append('.')
elif c in '.+(){}[]|^$\\': out.append(re.escape(c))
else: out.append(c)
return re.compile('^' + ''.join(out) + '$')

exemption_patterns = []
claude_md = pathlib.Path('.claude/CLAUDE.md')
if claude_md.exists():
in_table = False
for line in claude_md.read_text(encoding='utf-8').splitlines():
if re.search(r'TypeScript [Ee]xemptions', line):
in_table = True
continue
if in_table and line.startswith(('### ', '## ', '# ')):
break
if in_table and line.startswith('|'):
m = re.match(r'\|\s*`([^`]+)`', line)
if m:
exemption_patterns.append((m.group(1), glob_to_regex(m.group(1))))

def exempt(p):
for raw, regex in exemption_patterns:
if regex.match(p):
return True
# Also allow exact-path matches and prefix matches for paths
# ending in `/`
if p == raw.lstrip('./'):
return True
if raw.endswith('/') and p.startswith(raw.lstrip('./')):
return True
return False

# Find all .ts and .tsx files (excluding common dot-dirs that find normally skips)
found = []
for ext in ('ts', 'tsx'):
for p in pathlib.Path('.').rglob(f'*.{ext}'):
parts = p.parts
if any(part.startswith('.') and part not in ('.', '..') for part in parts):
continue
found.append(p.as_posix().lstrip('./'))

bad = sorted(f for f in found if not (builtin_allowed(f) or exempt(f)))
if bad:
print("❌ TypeScript files detected outside the allowlist.\n")
for f in bad:
print(f" {f}")
print()
print("To resolve, choose one:")
print(" (a) migrate the file to AffineScript")
print(" (see Human_Programming_Guide.adoc 'Migrating from -script Languages')")
print(" (b) move to an allowlisted bridge path")
print(" (bindings/, tests/, test/, scripts/, benchmarks/, mcp-adapter/,")
print(" *vscode*/, cli/, deno-*/, vendor/, examples/, ffi/)")
print(" (c) add an entry to the 'TypeScript Exemptions' table in .claude/CLAUDE.md")
print(" with rationale + unblock condition")
if exemption_patterns:
print(f"\n(Currently {len(exemption_patterns)} exemption(s) parsed from .claude/CLAUDE.md.)")
sys.exit(1)
print(f"✅ No TypeScript files outside allowlist ({len(exemption_patterns)} per-repo exemption(s) parsed).")
PYEOF

# Universal builtin allowlist — bridges that need no per-repo declaration.
# Files matching any of these patterns are always allowed.
Expand Down
Loading