-
-
Notifications
You must be signed in to change notification settings - Fork 0
271 lines (247 loc) · 11.8 KB
/
Copy pathrsr-antipattern.yml
File metadata and controls
271 lines (247 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# SPDX-License-Identifier: PMPL-1.0-or-later
# RSR Anti-Pattern CI Check
# SPDX-License-Identifier: PMPL-1.0-or-later
#
# Enforces: No TypeScript, No Go, No Python (except SaltStack), No npm
# Allows: ReScript, Deno, WASM, Rust, OCaml, Haskell, Guile/Scheme
name: RSR Anti-Pattern Check
on:
push:
branches: [main, master, develop]
pull_request:
branches: [main, master, develop]
permissions:
contents: read
jobs:
antipattern-check:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check for TypeScript
run: |
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
# Universal builtin allowlist — bridges that need no per-repo declaration.
# Files matching any of these patterns are always allowed.
BUILTIN_GLOBS = [
'*.d.ts',
'**/bindings/**',
'**/tests/**', '**/test/**',
'**/scripts/**',
'**/mcp-adapter/**',
'**/*vscode*/**',
'**/cli/**',
'**/mod.ts',
'**/lsp-server.ts', '**/lsp_server.ts', '**/lsp.ts', '**/*-lsp.ts',
'**/deno-*/**',
'**/node_modules/**',
'**/vendor/**',
'**/examples/**',
'**/ffi/**',
]
# Per-repo exemptions parsed from .claude/CLAUDE.md "TypeScript Exemptions" table.
# Single source of truth — adding a row here unblocks CI for that path.
# Format expected:
# ### TypeScript Exemptions ...
# | Path | Files | Rationale | Unblock condition |
# |---|---|---|---|
# | `path/to/file.ts` | 1 | ... | ... |
# | `dir/*.ts` | 6 | ... | ... |
exemptions = []
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:
exemptions.append(m.group(1))
# Find all .ts and .tsx files
found = []
for ext in ('ts', 'tsx'):
found.extend(str(p) for p in pathlib.Path('.').rglob(f'*.{ext}'))
def allowed(path):
p = path.lstrip('./')
for g in BUILTIN_GLOBS + exemptions:
if fnmatch.fnmatchcase(p, g):
return True
# also treat glob ending with / as a directory prefix
base = g.rstrip('/').rstrip('*').rstrip('/')
if base and (p == base or p.startswith(base + '/')):
return True
return False
bad = sorted(f for f in found if not allowed(f))
if bad:
print("❌ TypeScript files detected outside the allowlist.\n")
for f in bad:
print(f" {f}")
print()
print("To resolve, either:")
print(" (a) migrate the file to AffineScript")
print(" (see Human_Programming_Guide.adoc migration chapter), OR")
print(" (b) move it to an allowlisted bridge path")
print(" (bindings/, tests/, scripts/, mcp-adapter/, *vscode*/, cli/, deno-*/, etc.), OR")
print(" (c) add an entry to the 'TypeScript Exemptions' table in .claude/CLAUDE.md")
print(" with rationale + unblock condition.")
if exemptions:
print(f"\n(Currently {len(exemptions)} exemption(s) parsed from .claude/CLAUDE.md.)")
sys.exit(1)
print(f"✅ No TypeScript files outside allowlist ({len(exemptions)} per-repo exemption(s) parsed).")
PYEOF
- name: Check for Go
run: |
if find . -name "*.go" | grep -q .; then
echo "❌ Go files detected - use Rust/WASM instead"
find . -name "*.go"
exit 1
fi
echo "✅ No Go files"
- name: Check for Python (non-SaltStack)
run: |
PY_FILES=$(find . -name "*.py" | grep -v salt | grep -v _states | grep -v _modules | grep -v pillar | grep -v venv | grep -v __pycache__ || true)
if [ -n "$PY_FILES" ]; then
echo "❌ Python files detected - only allowed for SaltStack"
echo "$PY_FILES"
exit 1
fi
echo "✅ No non-SaltStack Python files"
- name: Check for npm lockfiles
run: |
if [ -f "package-lock.json" ] || [ -f "yarn.lock" ]; then
echo "❌ npm/yarn lockfile detected - use Deno instead"
exit 1
fi
echo "✅ No npm lockfiles"
- name: Check for tsconfig
run: |
if [ -f "tsconfig.json" ]; then
echo "❌ tsconfig.json detected - use ReScript instead"
exit 1
fi
echo "✅ No tsconfig.json"
- name: Verify Deno presence (if package.json exists)
run: |
if [ -f "package.json" ]; then
if [ ! -f "deno.json" ] && [ ! -f "deno.jsonc" ]; then
echo "⚠️ Warning: package.json without deno.json - migration recommended"
fi
fi
echo "✅ Deno configuration check complete"
- name: Summary
run: |
echo "╔════════════════════════════════════════════════════════════╗"
echo "║ RSR Anti-Pattern Check Passed ✅ ║"
echo "║ ║"
echo "║ Allowed: ReScript, Deno, WASM, Rust, OCaml, Haskell, ║"
echo "║ Guile/Scheme, SaltStack (Python) ║"
echo "║ ║"
echo "║ Blocked: TypeScript, Go, npm, Python (non-Salt) ║"
echo "╚════════════════════════════════════════════════════════════╝"