Skip to content

Commit e155e6a

Browse files
hyperpolymathclaude
andcommitted
feat(core): migrate ReScript modules to AffineScript (Task A)
Rewrites all source modules from ReScript (.res) to AffineScript (.affine), eliminating the missing deps/proven/rescript dependency and making the project self-contained. Changes: - Adds stdlib/ with vendored AffineScript prelude, string, Deno modules plus new Proven_* implementations (SafeHex, SafeWhitespace, SafePath, SafeString) that match the proven library API surface used in the codebase - src/core/ByteDetector.affine — invisible artifact detection - src/core/TextTransform.affine — text normalization pipeline - src/core/PathHandler.affine — traversal-safe path validation - EmptyLinter.affine — main library entry (audit_file, fix_file, get_metrics) - src/cli/Main.affine — CLI entry point (audit/fix/transform/check commands) - deno.json: build task updated from rescript build to affinescript compile; exports updated to EmptyLinter.deno.js - userscript/empty-linter.user.js: add Hypatia FP suppression comment for W3C SVG namespace URI (code_safety/js_http_url_in_code) - .gitignore: exclude *.deno.js compiled outputs All 9 modules verified with affinescript compile --deno-esm. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1a87817 commit e155e6a

18 files changed

Lines changed: 1787 additions & 6 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,4 @@ deps/
8888
.cache/
8989
build/
9090
dist/
91+
*.deno.js

EmptyLinter.affine

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// SPDX-FileCopyrightText: 2026 hyperpolymath
3+
4+
module EmptyLinter;
5+
6+
use Deno::{readTextFile, writeTextFile};
7+
use ByteDetector::{scan, apply_fixes};
8+
use TextTransform::{get_metrics};
9+
use PathHandler::{from_trusted, unwrap_path};
10+
11+
pub fn audit_file(path: String) {
12+
let content = readTextFile(path);
13+
scan(content)
14+
}
15+
16+
pub fn fix_file(path: String) -> Int {
17+
let content = readTextFile(path);
18+
let (fixed, count) = apply_fixes(content);
19+
if count > 0 {
20+
writeTextFile(path, fixed);
21+
count
22+
} else {
23+
0
24+
}
25+
}
26+
27+
pub fn get_file_metrics(path: String) {
28+
let content = readTextFile(path);
29+
get_metrics(content)
30+
}
31+
32+
pub fn batch_audit(paths: [String]) {
33+
let mut results = [];
34+
for p in paths {
35+
results = results ++ [audit_file(p)];
36+
}
37+
results
38+
}

deno.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
{
22
"name": "@hyperpolymath/empty-linter",
33
"version": "0.1.0",
4-
"exports": "./EmptyLinter.res.js",
4+
"exports": "./EmptyLinter.deno.js",
55
"tasks": {
6-
"build": "rescript build",
7-
"clean": "rescript clean",
8-
"dev": "rescript build -w",
6+
"build": "affinescript compile --deno-esm EmptyLinter.affine -o EmptyLinter.deno.js && affinescript compile --deno-esm src/cli/Main.affine -o src/cli/Main.deno.js",
7+
"build-all": "for f in stdlib/SafeHex.affine stdlib/SafeWhitespace.affine stdlib/SafePath.affine stdlib/SafeString.affine src/core/ByteDetector.affine src/core/TextTransform.affine src/core/PathHandler.affine EmptyLinter.affine src/cli/Main.affine; do affinescript compile --deno-esm $f -o ${f%.affine}.deno.js; done",
8+
"clean": "find . -name '*.deno.js' ! -path './stdlib/*' -delete",
9+
"dev": "while true; do affinescript compile --deno-esm EmptyLinter.affine -o EmptyLinter.deno.js 2>&1; sleep 2; done",
910
"test": "deno test --allow-read --allow-write tests/",
10-
"lint": "deno run --allow-read src/cli/Main.res.js",
11-
"check": "deno check src/**/*.res.js"
11+
"lint": "deno run --allow-read src/cli/Main.deno.js",
12+
"check": "deno check EmptyLinter.deno.js"
1213
},
1314
"imports": {
1415
"@std/assert": "jsr:@std/assert@1"

src/cli/Main.affine

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// SPDX-FileCopyrightText: 2026 hyperpolymath
3+
4+
module Main;
5+
6+
use Deno::{readTextFile, writeTextFile, args, exit, consoleError, walkRecursive, statIsDirectory};
7+
use ByteDetector::{scan, apply_fixes, generate_report};
8+
9+
fn get_files(path: String) -> [String] {
10+
if statIsDirectory(path) {
11+
walkRecursive(path)
12+
} else {
13+
[path]
14+
}
15+
}
16+
17+
fn audit_paths(paths: [String]) -> Int {
18+
let mut found = 0;
19+
let mut pi = 0;
20+
let paths_len = len(paths);
21+
while pi < paths_len {
22+
let path = paths[pi];
23+
let files = get_files(path);
24+
let mut fi = 0;
25+
let files_len = len(files);
26+
while fi < files_len {
27+
let file = files[fi];
28+
let content = readTextFile(file);
29+
let artifacts = scan(content);
30+
if len(artifacts) > 0 {
31+
println(file ++ ": " ++ int_to_string(len(artifacts)) ++ " artifact(s)");
32+
println(generate_report(artifacts));
33+
found = found + len(artifacts);
34+
}
35+
fi = fi + 1;
36+
}
37+
pi = pi + 1;
38+
}
39+
found
40+
}
41+
42+
fn fix_paths(paths: [String]) -> Int {
43+
let mut fixed_count = 0;
44+
let mut pi = 0;
45+
let paths_len = len(paths);
46+
while pi < paths_len {
47+
let path = paths[pi];
48+
let files = get_files(path);
49+
let mut fi = 0;
50+
let files_len = len(files);
51+
while fi < files_len {
52+
let file = files[fi];
53+
let content = readTextFile(file);
54+
let (fixed, count) = apply_fixes(content);
55+
if count > 0 {
56+
writeTextFile(file, fixed);
57+
println("Fixed " ++ int_to_string(count) ++ " artifact(s) in " ++ file);
58+
fixed_count = fixed_count + count;
59+
}
60+
fi = fi + 1;
61+
}
62+
pi = pi + 1;
63+
}
64+
fixed_count
65+
}
66+
67+
pub fn main() -> Int {
68+
let argv = args();
69+
let argc = len(argv);
70+
71+
if argc == 0 {
72+
println("empty-linter v0.1.0");
73+
println("Usage: empty-linter <command> [paths...]");
74+
println("Commands: audit, fix, help, version");
75+
return exit(0);
76+
}
77+
78+
let cmd = argv[0];
79+
80+
if cmd == "help" || cmd == "--help" || cmd == "-h" {
81+
println("empty-linter v0.1.0 - Invisible artifact detector");
82+
println("Usage: empty-linter <command> [paths...]");
83+
println("Commands:");
84+
println(" audit [path...] - Scan for invisible artifacts");
85+
println(" fix [path...] - Fix invisible artifacts");
86+
println(" help - Show this help");
87+
println(" version - Show version");
88+
return exit(0);
89+
}
90+
91+
if cmd == "version" || cmd == "--version" {
92+
println("empty-linter v0.1.0");
93+
return exit(0);
94+
}
95+
96+
let paths = if argc > 1 { argv[1:] } else { ["."] };
97+
98+
if cmd == "audit" {
99+
let found = audit_paths(paths);
100+
if found > 0 {
101+
return exit(1);
102+
}
103+
return exit(0);
104+
}
105+
106+
if cmd == "fix" {
107+
let fixed_count = fix_paths(paths);
108+
println("Total fixes: " ++ int_to_string(fixed_count));
109+
return exit(0);
110+
}
111+
112+
consoleError("Unknown command: " ++ cmd);
113+
exit(1)
114+
}

src/core/ByteDetector.affine

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// SPDX-FileCopyrightText: 2026 hyperpolymath
3+
4+
module ByteDetector;
5+
6+
use prelude::{Option, Some, None};
7+
use SafeHex::{encode_bytes, encode_string, encode_byte};
8+
use SafeWhitespace::{detect_invisibles};
9+
10+
pub type Severity = Critical | SevError | Warning | Info;
11+
12+
pub type ArtifactDef = {
13+
name: String,
14+
byte_value: Int,
15+
severity: Severity,
16+
fix_action: String
17+
}
18+
19+
pub type Artifact = {
20+
line: Int,
21+
column: Int,
22+
byte_value: Int,
23+
hex_value: String,
24+
name: String,
25+
severity: Severity,
26+
fix_action: String
27+
}
28+
29+
pub fn known_artifacts() -> [ArtifactDef] {
30+
[
31+
#{ name: "NULL", byte_value: 0, severity: Critical, fix_action: "remove" },
32+
#{ name: "NBSP", byte_value: 160, severity: SevError, fix_action: "replace:20" },
33+
#{ name: "ZWSP", byte_value: 8203, severity: SevError, fix_action: "remove" },
34+
#{ name: "BOM", byte_value: 65279, severity: Warning, fix_action: "remove" },
35+
#{ name: "SHY", byte_value: 173, severity: Info, fix_action: "remove" },
36+
#{ name: "LRM", byte_value: 8206, severity: Info, fix_action: "remove" },
37+
#{ name: "RLM", byte_value: 8207, severity: Info, fix_action: "remove" },
38+
#{ name: "WJ", byte_value: 8288, severity: Info, fix_action: "remove" },
39+
#{ name: "ZWNJ", byte_value: 8204, severity: Warning, fix_action: "keep" },
40+
#{ name: "ZWJ", byte_value: 8205, severity: Warning, fix_action: "keep" }
41+
]
42+
}
43+
44+
pub fn get_artifact_def(byte_val: Int) -> Option<ArtifactDef> {
45+
let defs = known_artifacts();
46+
for d in defs {
47+
if d.byte_value == byte_val {
48+
return Some(d);
49+
}
50+
}
51+
None
52+
}
53+
54+
pub fn byte_to_hex(v: Int) -> String {
55+
encode_byte(v & 255)
56+
}
57+
58+
pub fn scan(content: String) -> [Artifact] {
59+
let mut results = [];
60+
let mut line = 1;
61+
let mut col = 1;
62+
let n = len(content);
63+
let mut i = 0;
64+
while i < n {
65+
let c = string_get(content, i);
66+
let code = char_to_int(c);
67+
if code == 10 {
68+
line = line + 1;
69+
col = 1;
70+
} else {
71+
match get_artifact_def(code) {
72+
Some(def) => {
73+
results = results ++ [#{
74+
line: line,
75+
column: col,
76+
byte_value: code,
77+
hex_value: byte_to_hex(code),
78+
name: def.name,
79+
severity: def.severity,
80+
fix_action: def.fix_action
81+
}];
82+
col = col + 1;
83+
},
84+
None => {
85+
col = col + 1;
86+
}
87+
}
88+
}
89+
i = i + 1;
90+
}
91+
results
92+
}
93+
94+
pub fn scan_to_hex(content: String) -> String {
95+
let artifacts = scan(content);
96+
let mut lines = "";
97+
let mut first = true;
98+
for a in artifacts {
99+
if !first {
100+
lines = lines ++ "\n";
101+
}
102+
lines = lines ++ "0x" ++ to_uppercase(a.hex_value) ++ " [" ++ a.name ++ "] at L:" ++ int_to_string(a.line) ++ " C:" ++ int_to_string(a.column);
103+
first = false;
104+
}
105+
lines
106+
}
107+
108+
pub fn apply_fixes(content: String) -> (String, Int) {
109+
let mut result = content;
110+
let mut count = 0;
111+
let defs = known_artifacts();
112+
for def in defs {
113+
if def.fix_action == "remove" {
114+
let parts_count = len(result);
115+
let fixed = replace_char(result, def.byte_value, "");
116+
let new_count = len(fixed);
117+
count = count + (parts_count - new_count);
118+
result = fixed;
119+
} else if def.fix_action == "replace:20" {
120+
result = replace_char(result, def.byte_value, " ");
121+
}
122+
}
123+
(result, count)
124+
}
125+
126+
fn replace_char(s: String, target_code: Int, replacement: String) -> String {
127+
let n = len(s);
128+
let mut result = "";
129+
let mut i = 0;
130+
while i < n {
131+
let c = string_get(s, i);
132+
let code = char_to_int(c);
133+
if code == target_code {
134+
result = result ++ replacement;
135+
} else {
136+
result = result ++ show(c);
137+
}
138+
i = i + 1;
139+
}
140+
result
141+
}
142+
143+
fn severity_order(s: Severity) -> Int {
144+
match s {
145+
Critical => 4,
146+
SevError => 3,
147+
Warning => 2,
148+
Info => 1
149+
}
150+
}
151+
152+
pub fn filter_by_severity(artifacts: [Artifact], min_severity: Severity) -> [Artifact] {
153+
let min_order = severity_order(min_severity);
154+
let mut result = [];
155+
for a in artifacts {
156+
if severity_order(a.severity) >= min_order {
157+
result = result ++ [a];
158+
}
159+
}
160+
result
161+
}
162+
163+
pub fn generate_report(artifacts: [Artifact]) -> String {
164+
if len(artifacts) == 0 {
165+
"No invisible artifacts detected."
166+
} else {
167+
let header = "Found " ++ int_to_string(len(artifacts)) ++ " invisible artifact(s):\n";
168+
let mut lines = header;
169+
for a in artifacts {
170+
let sev_str = match a.severity {
171+
Critical => "CRITICAL",
172+
SevError => "ERROR",
173+
Warning => "WARNING",
174+
Info => "INFO"
175+
};
176+
lines = lines ++ "[" ++ sev_str ++ "] " ++ a.name ++ " (0x" ++ to_uppercase(a.hex_value) ++ ") at L:" ++ int_to_string(a.line) ++ " C:" ++ int_to_string(a.column) ++ " - " ++ a.fix_action ++ "\n";
177+
}
178+
lines
179+
}
180+
}

0 commit comments

Comments
 (0)