Skip to content

Commit 1477248

Browse files
Merge branch 'main' into js-strip-types
2 parents 89e4668 + 46266e5 commit 1477248

2 files changed

Lines changed: 196 additions & 32 deletions

File tree

.claude/CLAUDE.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,12 @@ Both are FOSS with independent governance (no Big Tech).
8181
- SHA-pinned dependencies
8282
- SPDX license headers on all files
8383

84+
### TypeScript Exemptions (Approved)
85+
86+
The hyperpolymath "no new TypeScript" policy has the following approved exemptions in this repo. These are *not* policy violations — they are documented carve-outs.
87+
88+
| Path | Files | Rationale | Unblock condition |
89+
|---|---|---|---|
90+
| `tools/**/*.ts` | 13 | tools/ subdirectory: monitoring-api (Express), stale scanner (Node CLI), github-action (Octokit). Each tool depends on a Node-native library that does not yet have an AffineScript binding. | AffineScript Node-target codegen (affinescript#35) + per-tool bindings (Octokit, Express, ArangoDB driver). |
91+
92+
Adding to this list requires explicit user approval and an unblock condition. New TypeScript files outside this list are blocked by the RSR antipattern check.

.github/workflows/rsr-antipattern.yml

Lines changed: 187 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)