Skip to content

Commit c3b087e

Browse files
Merge branch 'main' into affinescript-node-design
2 parents 2d18cbd + 050767e commit c3b087e

37 files changed

Lines changed: 797 additions & 556 deletions

.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: |

tools/cli/package.json

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
"name": "@accessibility-everywhere/cli",
33
"version": "1.0.0",
44
"description": "Command-line tool for accessibility scanning",
5-
"main": "dist/index.js",
5+
"main": "src/cli.js",
6+
"type": "module",
67
"bin": {
7-
"a11y-scan": "dist/cli.js",
8-
"accessibility-scan": "dist/cli.js"
8+
"a11y-scan": "src/cli.js",
9+
"accessibility-scan": "src/cli.js"
910
},
1011
"scripts": {
11-
"build": "tsc",
12-
"dev": "ts-node src/cli.ts",
12+
"start": "node src/cli.js",
1313
"test": "jest"
1414
},
1515
"dependencies": {
@@ -21,9 +21,6 @@
2121
"fs-extra": "^11.2.0"
2222
},
2323
"devDependencies": {
24-
"@types/node": "^20.10.0",
25-
"@types/fs-extra": "^11.0.4",
26-
"typescript": "^5.3.2",
27-
"ts-node": "^10.9.2"
24+
"jest": "^29.7.0"
2825
}
2926
}
Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ program
2424
.option('-o, --output <file>', 'Output file for results (JSON)')
2525
.option('-f, --format <format>', 'Output format (json, table, markdown)', 'table')
2626
.option('--screenshot', 'Take screenshot')
27-
.action(async (url: string, options: any) => {
27+
.action(async (url , options ) => {
2828
const spinner = ora('Scanning for accessibility issues...').start();
2929

3030
try {
@@ -70,7 +70,7 @@ program
7070
colWidths: [12, 50, 10, 15],
7171
});
7272

73-
result.violations.forEach((v: any) => {
73+
result.violations.forEach((v ) => {
7474
violationsTable.push([
7575
getImpactColor(v.impact) + v.impact + chalk.reset(),
7676
v.description,
@@ -97,7 +97,7 @@ program
9797

9898
// Exit with error code if violations found
9999
process.exit(result.violations.length > 0 ? 1 : 0);
100-
} catch (error: any) {
100+
} catch (error ) {
101101
spinner.fail('Scan failed');
102102
console.error(chalk.red(error.message));
103103
process.exit(1);
@@ -112,7 +112,7 @@ program
112112
.option('-l, --level <level>', 'WCAG level (A, AA, AAA)', 'AA')
113113
.option('--min-score <score>', 'Minimum required score', '70')
114114
.option('--fail-on-violations', 'Fail if any violations found')
115-
.action(async (url: string, options: any) => {
115+
.action(async (url , options ) => {
116116
const spinner = ora('Running CI scan...').start();
117117

118118
try {
@@ -142,7 +142,7 @@ program
142142

143143
console.log(chalk.green('✓ Passed all checks'));
144144
process.exit(0);
145-
} catch (error: any) {
145+
} catch (error ) {
146146
spinner.fail('CI scan failed');
147147
console.error(chalk.red(error.message));
148148
process.exit(1);
@@ -156,7 +156,7 @@ program
156156
.argument('<file>', 'File containing URLs (one per line)')
157157
.option('-l, --level <level>', 'WCAG level (A, AA, AAA)', 'AA')
158158
.option('-o, --output <dir>', 'Output directory for results', './scan-results')
159-
.action(async (file: string, options: any) => {
159+
.action(async (file , options ) => {
160160
try {
161161
const urls = (await fs.readFile(file, 'utf-8'))
162162
.split('\n')
@@ -186,7 +186,7 @@ program
186186

187187
spinner.succeed(`${url} - Score: ${result.score}`);
188188
completed++;
189-
} catch (error: any) {
189+
} catch (error ) {
190190
spinner.fail(`${url} - ${error.message}`);
191191
failed++;
192192
}
@@ -196,29 +196,29 @@ program
196196
if (failed > 0) {
197197
console.log(chalk.red(`✗ Failed: ${failed}`));
198198
}
199-
} catch (error: any) {
199+
} catch (error ) {
200200
console.error(chalk.red(error.message));
201201
process.exit(1);
202202
}
203203
});
204204

205205
// Helper functions
206-
function getGrade(score: number): string {
206+
function getGrade(score ) {
207207
if (score >= 90) return 'A';
208208
if (score >= 80) return 'B';
209209
if (score >= 70) return 'C';
210210
if (score >= 60) return 'D';
211211
return 'F';
212212
}
213213

214-
function getScoreColor(score: number): string {
214+
function getScoreColor(score ) {
215215
if (score >= 90) return chalk.green.bold('');
216216
if (score >= 70) return chalk.yellow.bold('');
217217
return chalk.red.bold('');
218218
}
219219

220-
function getImpactColor(impact: string): string {
221-
const colors: Record<string, any> = {
220+
function getImpactColor(impact ) {
221+
const colors = {
222222
critical: chalk.red.bold(''),
223223
serious: chalk.red(''),
224224
moderate: chalk.yellow(''),
@@ -227,13 +227,13 @@ function getImpactColor(impact: string): string {
227227
return colors[impact] || '';
228228
}
229229

230-
function generateMarkdown(result: any, url: string): string {
230+
function generateMarkdown(result , url ) {
231231
let md = `# Accessibility Report\n\n`;
232232
md += `**URL:** ${url}\n`;
233233
md += `**Score:** ${result.score}/100\n\n`;
234234
md += `## Violations\n\n`;
235235

236-
result.violations.forEach((v: any, i: number) => {
236+
result.violations.forEach((v , i ) => {
237237
md += `### ${i + 1}. ${v.help}\n\n`;
238238
md += `- **Impact:** ${v.impact}\n`;
239239
md += `- **Instances:** ${v.nodes.length}\n`;

0 commit comments

Comments
 (0)