Skip to content
Merged
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion .github/workflows/hypatia-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
fetch-depth: 0 # Full history for better pattern analysis

- name: Setup Elixir for Hypatia scanner
uses: erlef/setup-beam@9d5b75ddfda22fb979d2270283237aef8aa68d6b # v1.18.2
uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.18.2
with:
elixir-version: '1.19.4'
otp-version: '28.3'
Expand Down
119 changes: 110 additions & 9 deletions .github/workflows/rsr-antipattern.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,116 @@ jobs:

- name: Check for TypeScript
run: |
# Exclude bindings/deno/ - those are Deno FFI files using Deno.dlopen, not plain TypeScript
# Exclude .d.ts files - those are TypeScript type declarations for ReScript FFI
TS_FILES=$(find . \( -name "*.ts" -o -name "*.tsx" \) | grep -v node_modules | grep -v 'bindings/deno' | grep -v '\.d\.ts$' || true)
if [ -n "$TS_FILES" ]; then
echo "❌ TypeScript files detected - use ReScript instead"
echo "$TS_FILES"
exit 1
fi
echo "✅ No TypeScript files (Deno FFI bindings excluded)"
python3 << 'PYEOF'
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

- name: Check for Go
run: |
Expand Down
Loading