diff --git a/.github/workflows/rsr-antipattern.yml b/.github/workflows/rsr-antipattern.yml index 36e895e..be56af3 100644 --- a/.github/workflows/rsr-antipattern.yml +++ b/.github/workflows/rsr-antipattern.yml @@ -1,271 +1,385 @@ # SPDX-License-Identifier: PMPL-1.0-or-later + # RSR Anti-Pattern CI Check -# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-License-Id +entifier: 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 +# Enforces: No +TypeScript, No Go, No Python (except SaltStac +k), No npm +# Allows: ReScript, Deno, WASM, Ru +st, OCaml, Haskell, Guile/Scheme -name: RSR Anti-Pattern Check +name: RSR A +nti-Pattern Check on: push: - branches: [main, master, develop] + branches: +[main, master, develop] pull_request: - branches: [main, master, develop] + b +ranches: [main, master, develop] -permissions: +permission +s: contents: read jobs: - antipattern-check: + antipattern-chec +k: runs-on: ubuntu-latest - permissions: + permissions +: contents: read steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - use +s: actions/checkout@de0fac2e4500dabe0009e6721 +4ff5f5447ce83dd # v6.0.2 - - name: Check for TypeScript + - name: Check + for TypeScript run: | - python3 << 'PYEOF' - import re, sys, pathlib + pyth +on3 << 'PYEOF' + import re, sys, path +lib - # 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. + # Universal allowlist — brid +ges and conventions that need no per-repo dec +laration. + # Implemented as explicit + string predicates rather than glob patterns +so that + # top-level directories (e. +g. tests/foo.ts) are matched the same as nest +ed ones, + # which fnmatch's * cannot + do reliably. DIR_NAMES_ALLOWED = { - 'bindings', 'tests', 'test', 'scripts', - 'mcp-adapter', 'cli', 'vendor', 'examples', 'ffi', - 'node_modules', 'benchmarks', + + '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 ./ + + 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'): + + if p.endswith('.d.ts'): + + return True + # 2. Cano +nical Deno entrypoint filenames + + base = p.rsplit('/', 1)[-1] + if + base == 'mod.ts': + return T +rue + # 3. LSP server files (file +name suffixes) + if base in ('lsp +-server.ts', 'lsp_server.ts', 'lsp.ts') or ba +se.endswith('-lsp.ts'): + ret +urn True + # 4. Benchmark files ( +filename suffixes) + if base.ends +with('.bench.ts') or base.endswith('_bench.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: + + # 5. Any directory segment (excluding base +name) matches an allowed dir + se +gs = p.split('/') + for s in segs +[:-1]: + if s in DIR_NAMES_AL +LOWED: return True - # vscode-anything or anything-vscode + + # vscode-anything or anything-v +scode if 'vscode' in s: - return True - # deno-named subprojects - if s.startswith('deno-'): - return True + + 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): + + # Per-repo exemptions parsed from . +claude/CLAUDE.md "TypeScript Exemptions" tabl +e. + # This is the documented single +source of truth: adding one row here unblocks + CI. + # Glob characters: '*' and '** +' both mean "any chars including /". This loo +se + # interpretation matches user in +tent when an exemption row reads, e.g., + + # `affinescript-deno-test/*.ts` (coverin +g nested files too). + def glob_to_re +gex(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) + '$') + +for c in g.lstrip('./'): + if + c == '*': out.append('.*') + + elif c == '?': out.append('.') + + elif c in '.+(){}[]|^$\\': out.append(re +.escape(c)) + else: out.appen +d(c) + return re.compile('^' + '' +.join(out) + '$') - exemption_patterns = [] - claude_md = pathlib.Path('.claude/CLAUDE.md') - if claude_md.exists(): + exemption_patter +ns = [] + claude_md = pathlib.Path('. +claude/CLAUDE.md') + if claude_md.exi +sts(): in_table = False - for line in claude_md.read_text(encoding='utf-8').splitlines(): - if re.search(r'TypeScript [Ee]xemptions', line): + + for line in claude_md.read_text(encodi +ng='utf-8').splitlines(): + i +f re.search(r'TypeScript [Ee]xemptions', line +): in_table = True - continue - if in_table and line.startswith(('### ', '## ', '# ')): + + 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)))) + + if in_table and line.startswith(' +|'): + m = re.match(r'\|\ +s*`([^`]+)`', line) + if +m: + exemption_patter +ns.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 + + for raw, regex in exemption_patterns: + + if regex.match(p): + + return True + # Also +allow exact-path matches and prefix matches f +or paths # ending in `/` - if p == raw.lstrip('./'): - return True - if raw.endswith('/') and p.startswith(raw.lstrip('./')): - return True + + 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): + # +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('.').r +glob(f'*.{ext}'): + parts = p +.parts + if any(part.startswi +th('.') and part not in ('.', '..') for part +in parts): continue - found.append(p.as_posix().lstrip('./')) + + found.append(p.as_posix().lstr +ip('./')) - bad = sorted(f for f in found if not (builtin_allowed(f) or exempt(f))) + 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") + print("❌ + TypeScript files detected outside the allowl +ist.\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 + + print(f" {f}") + print +() + print("To resolve, choose on +e:") + print(" (a) migrate the f +ile to AffineScript") + print(" + (see Human_Programming_Guide.adoc 'Migrat +ing from -script Languages')") + +print(" (b) move to an allowlisted bridge pa +th") + print(" (bindings/, t +ests/, test/, scripts/, benchmarks/, mcp-adap +ter/,") + print(" *vscode*/ +, cli/, deno-*/, vendor/, examples/, ffi/)") - # 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).") + print(" (c) add an entry to th +e 'TypeScript Exemptions' table in .claude/CL +AUDE.md") + print(" with rat +ionale + unblock condition") + if + exemption_patterns: + print( +f"\n(Currently {len(exemption_patterns)} exem +ption(s) parsed from .claude/CLAUDE.md.)") + + sys.exit(1) + print(f"✅ + No TypeScript files outside allowlist ({len( +exemption_patterns)} per-repo exemption(s) pa +rsed).") PYEOF - name: Check for Go - run: | - if find . -name "*.go" | grep -q .; then - echo "❌ Go files detected - use Rust/WASM instead" - find . -name "*.go" + r +un: | + if find . -name "*.go" | grep + -q .; then + echo "❌ Go files de +tected - use Rust/WASM instead" + f +ind . -name "*.go" exit 1 - fi + + 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" + + - name: Check for Python (non-SaltStack) + + run: | + PY_FILES=$(find . -name + "*.py" | grep -v salt | grep -v _states | gr +ep -v _modules | grep -v pillar | grep -v ven +v | grep -v __pycache__ || true) + if + [ -n "$PY_FILES" ]; then + echo "� +�� Python files detected - only allowed for S +altStack" echo "$PY_FILES" - exit 1 + + exit 1 fi - echo "✅ No non-SaltStack Python files" + echo "✅ + No non-SaltStack Python files" - - name: Check for npm lockfiles + - 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 + + if [ -f "package-lock.json" ] || [ -f " +yarn.lock" ]; then + echo "❌ npm/ +yarn lockfile detected - use Deno instead" + + exit 1 fi - echo "✅ No npm lockfiles" + echo +"✅ No npm lockfiles" - - name: Check for tsconfig + - name: Check f +or tsconfig run: | - if [ -f "tsconfig.json" ]; then - echo "❌ tsconfig.json detected - use ReScript instead" + if [ -f +"tsconfig.json" ]; then + echo "❌ + tsconfig.json detected - use ReScript instea +d" exit 1 fi - echo "✅ No tsconfig.json" + +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 + - name: Ve +rify 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 d +eno.json - migration recommended" + + fi fi - echo "✅ Deno configuration check complete" + echo "✅ Deno con +figuration check complete" - - name: Summary + - name: Sum +mary 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 "╚════════════════════════════════════════════════════════════╝" + echo "╔══ +═══════════════ +═══════════════ +═══════════════ +═════════════╗" + + echo "║ RSR Anti-Pattern + Check Passed ✅ ║" + + echo "║ + ║" + ec +ho "║ Allowed: ReScript, Deno, WASM, Rust, + OCaml, Haskell, ║" + echo "║ + Guile/Scheme, SaltStack (Python) + ║" + echo "║ + + ║" + echo "║ Blocked: T +ypeScript, Go, npm, Python (non-Salt) + ║" + echo "╚══════� +��══════════════� +��══════════════� +��══════════════� +��════════╝" + +