@@ -131,94 +131,37 @@ jobs:
131131 repository : ${{ github.repository }}
132132 ref : ${{ github.ref }}
133133
134- - name : Check for TypeScript
135- run : |
136- python3 << 'PYEOF'
137- import re, sys, pathlib
138-
139- DIR_NAMES_ALLOWED = {
140- 'bindings', 'tests', 'test', 'scripts',
141- 'mcp-adapter', 'cli', 'vendor', 'examples', 'ffi',
142- 'node_modules', 'benchmarks',
143- }
144-
145- def builtin_allowed(p):
146- if p.endswith('.d.ts'):
147- return True
148- base = p.rsplit('/', 1)[-1]
149- if base == 'mod.ts':
150- return True
151- if base in ('lsp-server.ts', 'lsp_server.ts', 'lsp.ts') or base.endswith('-lsp.ts'):
152- return True
153- if base.endswith('.bench.ts') or base.endswith('_bench.ts'):
154- return True
155- segs = p.split('/')
156- for s in segs[:-1]:
157- if s in DIR_NAMES_ALLOWED:
158- return True
159- if 'vscode' in s:
160- return True
161- if s.startswith('deno-'):
162- return True
163- return False
164-
165- def glob_to_regex(g):
166- out = []
167- for c in g.lstrip('./'):
168- if c == '*': out.append('.*')
169- elif c == '?': out.append('.')
170- elif c in '.+(){}[]|^$\\': out.append(re.escape(c))
171- else: out.append(c)
172- return re.compile('^' + ''.join(out) + '$')
173-
174- exemption_patterns = []
175- claude_md = pathlib.Path('.claude/CLAUDE.md')
176- if claude_md.exists():
177- in_table = False
178- for line in claude_md.read_text(encoding='utf-8').splitlines():
179- if re.search(r'TypeScript [Ee]xemptions', line):
180- in_table = True
181- continue
182- if in_table and line.startswith(('### ', '## ', '# ')):
183- break
184- if in_table and line.startswith('|'):
185- m = re.match(r'\|\s*`([^`]+)`', line)
186- if m:
187- exemption_patterns.append((m.group(1), glob_to_regex(m.group(1))))
188-
189- def exempt(p):
190- for raw, regex in exemption_patterns:
191- if regex.match(p):
192- return True
193- if p == raw.lstrip('./'):
194- return True
195- if raw.endswith('/') and p.startswith(raw.lstrip('./')):
196- return True
197- return False
134+ # Estate language policy bans Python with no exceptions (CLAUDE.md
135+ # Language Policy; SaltStack exception removed 2026-01-03). The
136+ # previous in-line `python3 << PYEOF` heredoc made this very gate a
137+ # self-referential violation — same structural class as the CSA001
138+ # self-loop fixed in hypatia#328. Eradicated by porting the logic
139+ # to a Deno script that lives in this standards repo.
140+ #
141+ # Implementation note: a reusable workflow only auto-checks-out its
142+ # YAML, not sibling files in its repo. So we explicitly check out
143+ # this repo at the same ref the caller picked (via
144+ # `github.workflow_sha`, the SHA actually loaded by the runner)
145+ # into `.standards-checkout/`, then run the script from there.
146+ - name : Set up Deno
147+ uses : denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3
148+ with :
149+ deno-version : v2.x
198150
199- found = []
200- for ext in ('ts', 'tsx'):
201- for p in pathlib.Path('.').rglob(f'*.{ext}'):
202- parts = p.parts
203- if any(part.startswith('.') and part not in ('.', '..') for part in parts):
204- continue
205- found.append(p.as_posix().lstrip('./'))
151+ - name : Check out standards repo for shared scripts
152+ uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
153+ with :
154+ repository : hyperpolymath/standards
155+ ref : ${{ github.workflow_sha }}
156+ path : .standards-checkout
157+ # Sparse-checkout only the scripts dir to keep this fast.
158+ sparse-checkout : |
159+ scripts
160+ sparse-checkout-cone-mode : false
206161
207- bad = sorted(f for f in found if not (builtin_allowed(f) or exempt(f)))
208- if bad:
209- print("❌ TypeScript files detected outside the allowlist.\n")
210- for f in bad:
211- print(f" {f}")
212- print()
213- print("To resolve, choose one:")
214- print(" (a) migrate the file to AffineScript")
215- print(" (b) move to an allowlisted bridge path")
216- print(" (c) add an entry to the 'TypeScript Exemptions' table in .claude/CLAUDE.md")
217- if exemption_patterns:
218- print(f"\n(Currently {len(exemption_patterns)} exemption(s) parsed from .claude/CLAUDE.md.)")
219- sys.exit(1)
220- print(f"✅ No TypeScript files outside allowlist ({len(exemption_patterns)} per-repo exemption(s) parsed).")
221- PYEOF
162+ - name : Check for TypeScript
163+ # Read-only execution; never writes outside the runner workspace.
164+ run : deno run --allow-read .standards-checkout/scripts/check-ts-allowlist.ts
222165
223166 # Shared escape hatch for the banned-language-file checks below.
224167 # Honours three exemption mechanisms (see
@@ -244,20 +187,42 @@ jobs:
244187
245188 # Baseline lookup: returns 0 (exempt) if the file appears in
246189 # .hypatia-baseline.json with a matching rule_module + type.
247- # `file_pattern` glob match is intentionally NOT implemented
248- # here; the advisory-rollout window only honours exact `file`
249- # matches. Pattern support arrives with the blocking-mode
250- # flip and the apply-baseline.sh upgrade (see
251- # standards/scripts/apply-baseline.sh).
190+ # Honours both `file` (exact) and `file_pattern` (glob) entries.
191+ # The glob → regex translation mirrors apply-baseline.sh exactly:
192+ # `**` matches any depth (incl. `/`); `*` matches one segment.
193+ # Pattern support unblocks language-demo repos (absolute-zero
194+ # carries ~30 banned-language example files under `examples/`)
195+ # and any repo that vendors such subtrees, replacing per-file
196+ # `.hypatia-ignore` enumeration with one `file_pattern` entry.
252197 in_baseline() {
253198 local target="$1"
254199 [ -f .hypatia-baseline.json ] || return 1
255200 command -v jq >/dev/null 2>&1 || return 1
201+ # Note: the `as $pat` capture is essential — inside `test(...)`
202+ # the dot rebinds to test's input ($f, a string), so
203+ # `.file_pattern` would error with "Cannot index string". We
204+ # capture file_pattern in $pat first, then reference it inside
205+ # the test() argument.
256206 jq -e \
257207 --arg rm "$rule_module" \
258208 --arg rt "$rule_type" \
259209 --arg f "$target" \
260- 'any(.[]; .rule_module == $rm and .type == $rt and .file == $f)' \
210+ 'any(.[];
211+ .rule_module == $rm and .type == $rt
212+ and (
213+ (.file? // null) == $f
214+ or (
215+ (.file_pattern? // null) as $pat
216+ | $pat != null
217+ and ($f | test(
218+ $pat
219+ | gsub("\\*\\*"; "DOUBLESTAR")
220+ | gsub("\\*"; "[^/]*")
221+ | gsub("DOUBLESTAR"; ".*")
222+ | "^" + . + "$"
223+ ))
224+ )
225+ ))' \
261226 .hypatia-baseline.json >/dev/null 2>&1
262227 }
263228
0 commit comments