Skip to content

Commit eff6aca

Browse files
authored
Make humanize findings accurate and actionable (#282)
1 parent 7353509 commit eff6aca

8 files changed

Lines changed: 444 additions & 77 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"name": "humanize",
2828
"source": "./plugins/humanize",
2929
"description": "Block a curated list of AI buzzwords on every write with a hook.",
30-
"version": "1.2.0",
30+
"version": "1.2.1",
3131
"author": {
3232
"name": "Fatih C. Akyon"
3333
},

.cursor-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"name": "humanize",
1414
"source": "./plugins/humanize",
1515
"description": "Block a curated list of AI buzzwords on every write with a hook.",
16-
"version": "1.2.0",
16+
"version": "1.2.1",
1717
"license": "Apache-2.0"
1818
},
1919
{

.github/scripts/test_humanize.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
#!/usr/bin/env python3
2+
"""Test humanize extraction and source locations."""
3+
4+
import json
5+
import subprocess
6+
import tempfile
7+
import unittest
8+
from pathlib import Path
9+
10+
HOOK = Path(__file__).parents[2] / "plugins/humanize/hooks/scripts/humanize.py"
11+
SEMICOLON = chr(59)
12+
13+
14+
def run_hook(tool, tool_input):
15+
"""Run humanize with one tool payload."""
16+
output = subprocess.run(
17+
["python3", HOOK],
18+
input=json.dumps({"tool_name": tool, "tool_input": tool_input}),
19+
capture_output=True,
20+
check=True,
21+
text=True,
22+
).stdout
23+
return json.loads(output)["hookSpecificOutput"]["permissionDecisionReason"] if output else ""
24+
25+
26+
class HumanizeTest(unittest.TestCase):
27+
"""Test that humanize checks writing without mistaking code for it."""
28+
29+
def test_masks_markdown_code(self):
30+
"""Mask closed, incomplete, and multi-backtick Markdown code."""
31+
fence = "`" * 3
32+
for content in (
33+
f"Intro.\n{fence}js\nconst a = 1{SEMICOLON}\n{fence}\n",
34+
f"Intro.\n{fence}js\nconst a = 1{SEMICOLON}\n",
35+
f"Use ``leverage`this{SEMICOLON}`` in a sentence.\n",
36+
):
37+
self.assertEqual(run_hook("Write", {"file_path": "README.md", "content": content}), "")
38+
39+
def test_ignores_ambiguous_text_files(self):
40+
"""Ignore generic text files that may contain logs or fixtures."""
41+
self.assertEqual(
42+
run_hook("Write", {"file_path": "fixture.txt", "content": f"GET /a 200{SEMICOLON} GET /b 404{SEMICOLON}"}),
43+
"",
44+
)
45+
46+
def test_comments_are_quote_aware(self):
47+
"""Ignore comment markers inside quoted values and strings."""
48+
cases = (
49+
("config.yml", f'motd: "welcome # to prod{SEMICOLON} be careful"\n'),
50+
("app.ts", f'const note = "see {"/" * 2} ref{SEMICOLON} here"{SEMICOLON}\n'),
51+
)
52+
for path, content in cases:
53+
self.assertEqual(run_hook("Write", {"file_path": path, "content": content}), "")
54+
55+
def test_checks_real_writing(self):
56+
"""Keep blocking marks in Markdown and code comments."""
57+
cases = (
58+
("README.md", f"This sentence has a semicolon{SEMICOLON} replace it."),
59+
("config.yml", f"# This comment has a semicolon{SEMICOLON} replace it."),
60+
("app.ts", f"{'/' * 2} This comment has a semicolon{SEMICOLON} replace it."),
61+
)
62+
for path, content in cases:
63+
self.assertTrue(run_hook("Write", {"file_path": path, "content": content}))
64+
65+
def test_applies_markdown_edits_with_file_context(self):
66+
"""Use the full Markdown file to classify edited text."""
67+
with tempfile.TemporaryDirectory() as directory:
68+
path = Path(directory) / "README.md"
69+
path.write_text(f"Intro.\n```js\nconst value = 1{SEMICOLON}\n```\nOld sentence.\n")
70+
code_edit = {
71+
"file_path": str(path),
72+
"old_string": f"const value = 1{SEMICOLON}",
73+
"new_string": f"const value = 2{SEMICOLON}",
74+
}
75+
prose_edit = {
76+
"file_path": str(path),
77+
"old_string": "Old sentence.",
78+
"new_string": f"New{SEMICOLON} sentence.",
79+
}
80+
self.assertEqual(run_hook("Edit", code_edit), "")
81+
self.assertIn(f"{path}:5:4", run_hook("Edit", prose_edit))
82+
83+
def test_applies_markdown_patches_with_file_context(self):
84+
"""Use the full Markdown file to classify patched text."""
85+
with tempfile.TemporaryDirectory() as directory:
86+
path = Path(directory) / "README.md"
87+
path.write_text(f"Intro.\n```js\nconst value = 1{SEMICOLON}\n```\nOld sentence.\n")
88+
code_patch = (
89+
f"*** Begin Patch\n*** Update File: {path}\n@@\n ```js\n"
90+
f"-const value = 1{SEMICOLON}\n+const value = 2{SEMICOLON}\n ```\n*** End Patch"
91+
)
92+
prose_patch = (
93+
f"*** Begin Patch\n*** Update File: {path}\n@@\n ```\n"
94+
f"-Old sentence.\n+New{SEMICOLON} sentence.\n*** End Patch"
95+
)
96+
self.assertEqual(run_hook("apply_patch", {"command": code_patch}), "")
97+
self.assertIn(f"{path}:5:4", run_hook("apply_patch", {"command": prose_patch}))
98+
99+
def test_reports_each_file_location_with_context(self):
100+
"""Report every file finding with line, column, fix, and context."""
101+
reason = run_hook(
102+
"Write",
103+
{
104+
"file_path": "README.md",
105+
"content": f"First line.\nAI sections{SEMICOLON} it does not{SEMICOLON}\nWe leverage tools.\nIn conclusion, done.\n",
106+
},
107+
)
108+
self.assertIn(
109+
f'- semicolon at README.md:2:12, use a period or comma: "AI sections{SEMICOLON} it does not{SEMICOLON}"',
110+
reason,
111+
)
112+
self.assertEqual(reason.count("- semicolon at"), 2)
113+
self.assertIn('"leverage" at README.md:3:4, use "use"', reason)
114+
self.assertIn('"In conclusion" at README.md:4:1, drop it', reason)
115+
116+
def test_caps_pileup_locations(self):
117+
"""Report five pile-up locations and count the remainder."""
118+
content = "\n".join(f"crucial item {i}" for i in range(7))
119+
reason = run_hook("Write", {"file_path": "README.md", "content": content})
120+
self.assertIn('"crucial" used 7 times', reason)
121+
self.assertIn("README.md:5:1, +2 more", reason)
122+
self.assertNotIn("README.md:6:1", reason)
123+
124+
def test_labels_non_file_sources(self):
125+
"""Label PR, Slack, and heredoc findings by their real source."""
126+
cases = (
127+
("Bash", {"command": f'gh pr create -b "One{SEMICOLON} two"'}, "PR body:1:4"),
128+
(
129+
"mcp__claude_ai_Slack__slack_send_message",
130+
{"message": f"One{SEMICOLON} two"},
131+
"Slack message:1:4",
132+
),
133+
(
134+
"mcp__codex_apps__slack_slack_send_message",
135+
{"message": {"markdown_text": f"One{SEMICOLON} two"}},
136+
"Slack message:1:4",
137+
),
138+
(
139+
"Bash",
140+
{"command": f"cat > notes.md <<'EOF'\nOne{SEMICOLON} two\nEOF"},
141+
"notes.md:1:4",
142+
),
143+
)
144+
for tool, tool_input, source in cases:
145+
self.assertIn(source, run_hook(tool, tool_input))
146+
147+
148+
if __name__ == "__main__":
149+
unittest.main()

plugins/humanize/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "humanize",
3-
"version": "1.2.0",
3+
"version": "1.2.1",
44
"description": "Block a curated list of AI buzzwords on every write with a hook.",
55
"author": {
66
"name": "Fatih C. Akyon"

plugins/humanize/.codex-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "humanize",
3-
"version": "1.2.0",
3+
"version": "1.2.1",
44
"description": "Block a curated list of AI buzzwords on every write with a hook.",
55
"author": {
66
"name": "Fatih C. Akyon"

plugins/humanize/.cursor-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "humanize",
3-
"version": "1.2.0",
3+
"version": "1.2.1",
44
"description": "Block a curated list of AI buzzwords on every write with a hook.",
55
"author": {
66
"name": "Fatih C. Akyon"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"name": "humanize",
3-
"version": "1.2.0",
3+
"version": "1.2.1",
44
"description": "Block a curated list of AI buzzwords on every write with a hook."
55
}

0 commit comments

Comments
 (0)