From 940a24fe305272fdebecb9af1cdbd4b361c826c6 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 3 May 2026 22:21:00 +0100 Subject: [PATCH] ci(antipattern): fix top-level dir matching + benchmarks/lsp/bench filename allowlists --- .github/workflows/rsr-antipattern.yml | 110 +++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rsr-antipattern.yml b/.github/workflows/rsr-antipattern.yml index 3c8401f..7976d32 100644 --- a/.github/workflows/rsr-antipattern.yml +++ b/.github/workflows/rsr-antipattern.yml @@ -28,7 +28,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.