From 29c111165401383273c7e8098bfbee3d81f1ca54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=86gir=20M=C3=A1ni=20Hauksson?= <54936225+sourcehawk@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:06:56 +0100 Subject: [PATCH 1/3] fix(skills/addressing-review-feedback): read JSX comments in .jsx and .tsx files The comment gate read a comment only when its line started with // or /*. A JSX comment between tags starts with {/*, so the gate did not list a JSX comment that changed, and did not flag one that grew. The .jsx and .tsx files now have their own comment family. It reads {/* ... */} as a delimited block, and it keeps // and /* ... */. The other files of the slash family do not change: in them, a line that starts with {/* is code. Fixes #87 Co-Authored-By: Claude Opus 5.5 --- .../scripts/comment_growth.py | 6 +- .../scripts/tests/test_comment_growth.py | 56 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/skills/addressing-review-feedback/scripts/comment_growth.py b/skills/addressing-review-feedback/scripts/comment_growth.py index 903c7f5..4e31e06 100644 --- a/skills/addressing-review-feedback/scripts/comment_growth.py +++ b/skills/addressing-review-feedback/scripts/comment_growth.py @@ -42,6 +42,9 @@ class Family: SLASH = Family("slash", ("//",), (("/*", "*/"),), AFTER) +# A comment between JSX tags stands in braces, {/* ... */}. Only these file +# types read a line that starts with {/* as a comment: in the others it is code. +JSX = Family("jsx", ("//",), (("/*", "*/"), ("{/*", "*/")), AFTER) HASH = Family("hash", ("#",), (), AFTER) HASH_ANGLE = Family("hash-angle", ("#",), (("<#", "#>"),), AFTER) HASH_BEGIN = Family("hash-begin", ("#",), (("=begin", "=end"),), AFTER) @@ -54,7 +57,8 @@ class Family: EXTENSIONS: Dict[str, Family] = {} for _family, _extensions in ( - (SLASH, ".c .h .cc .cpp .cxx .hpp .cs .java .js .jsx .mjs .cjs .ts .tsx .go .rs .swift .kt .kts .scala .dart .php .m .mm .proto .groovy .gradle .zig .css .scss .less .v .sv"), + (SLASH, ".c .h .cc .cpp .cxx .hpp .cs .java .js .mjs .cjs .ts .go .rs .swift .kt .kts .scala .dart .php .m .mm .proto .groovy .gradle .zig .css .scss .less .v .sv"), + (JSX, ".jsx .tsx"), (HASH, ".sh .bash .zsh .fish .pl .pm .yaml .yml .toml .tf .tfvars .hcl .r .ex .exs .nix .mk .cmake .conf .cfg .properties .tcl .awk"), (HASH_ANGLE, ".ps1 .psm1"), (HASH_BEGIN, ".rb"), diff --git a/skills/addressing-review-feedback/scripts/tests/test_comment_growth.py b/skills/addressing-review-feedback/scripts/tests/test_comment_growth.py index e40de07..a3dedd3 100644 --- a/skills/addressing-review-feedback/scripts/tests/test_comment_growth.py +++ b/skills/addressing-review-feedback/scripts/tests/test_comment_growth.py @@ -255,6 +255,48 @@ def test_line_comments_of_these_file_types_still_count(self): self.assertEqual(self.grown(path, old, new), [(gate.GREW, 1, 2, True)]) +class JsxCommentTest(unittest.TestCase): + def scan(self, path, source): + return gate.scan(source, gate.family_for(path)) + + def grown(self, path, old, new): + family = gate.family_for(path) + return [(f.status, f.old_length, f.new_length, f.flagged) for f in gate.compare(path, old, new, family)] + + def test_comment_in_braces_on_one_line_is_a_block(self): + source = "
\n {/* about */}\n \n
\n" + for path in ("a.jsx", "a.tsx"): + with self.subTest(path=path): + self.assertEqual(self.scan(path, source), [gate.Block(2, ("{/* about */}",), "")]) + + def test_comment_in_braces_runs_to_its_closing_delimiter(self): + source = "
\n {/*\n about\n */}\n \n
\n" + self.assertEqual(self.scan("a.tsx", source), [gate.Block(2, ("{/*", "about", "*/}"), "")]) + + def test_comment_in_braces_that_grew_is_flagged(self): + old = "
\n {/* about */}\n \n
\n" + new = "
\n {/*\n about\n and more\n */}\n \n
\n" + self.assertEqual(self.grown("a.tsx", old, new), [(gate.GREW, 1, 4, True)]) + + def test_comment_in_braces_that_changed_is_listed(self): + old = "
\n {/* about */}\n \n
\n" + new = "
\n {/* other */}\n \n
\n" + self.assertEqual(self.grown("a.jsx", old, new), [(gate.CHANGED, 1, 1, False)]) + + def test_slash_comments_still_count_in_these_file_types(self): + source = "// about\nconst a = 1;\n/* more */\nconst b = 2;\n" + self.assertEqual( + self.scan("a.tsx", source), + [gate.Block(1, ("// about",), "const a = 1;"), gate.Block(3, ("/* more */",), "const b = 2;")], + ) + + def test_braces_before_a_comment_are_code_in_other_slash_file_types(self): + source = "int run(void)\n{/* empty */}\n" + for path in ("a.ts", "a.js", "a.c"): + with self.subTest(path=path): + self.assertEqual(self.scan(path, source), []) + + def statuses(old, new, family=gate.SLASH): return [(f.status, f.old_length, f.new_length, f.flagged) for f in gate.compare("f", old, new, family)] @@ -827,6 +869,20 @@ def test_comparison_starts_at_the_merge_base(self): sh(self.repo.path, "git", "checkout", "-q", "topic") self.assertEqual(gate.run(self.repo.path, "main").findings, ()) + def test_changed_jsx_comment_is_listed_like_a_slash_comment(self): + self.repo.write("a.tsx", "export function A() {\n return (\n
\n {/* short note */}\n \n
\n );\n}\n") + self.repo.write("b.ts", "export function B() {\n // short note\n return 1;\n}\n") + base = self.repo.commit("add a and b") + longer = "a much longer note that grew well past the first one, with more words" + for name in ("a.tsx", "b.ts"): + with open(os.path.join(self.repo.path, name), encoding="utf-8") as handle: + self.repo.write(name, handle.read().replace("short note", longer)) + report = gate.run(self.repo.path, base) + self.assertEqual( + [(f.path, f.line, f.status) for f in report.findings], + [("a.tsx", 4, gate.CHANGED), ("b.ts", 2, gate.CHANGED)], + ) + def test_untracked_file_is_scanned(self): self.repo.write("new.go", OLD) self.assertEqual(self.summary(gate.run(self.repo.path, "HEAD")), [("new.go", gate.ADDED, False)]) From bcce93921d34c71487db503d44bafb430085ae32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=86gir=20M=C3=A1ni=20Hauksson?= <54936225+sourcehawk@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:28:53 +0100 Subject: [PATCH 2/3] fix(skills/addressing-review-feedback): read JSX comments in .js files and after a space The JSX family read only {/* ... */}. A JSX comment can also stand as { /* ... */ } with a space after the brace, or as a line comment {// ... on its own line, and the gate did not list those. JSX also stands in plain .js files, which were still in the slash family. The JSX family now reads both forms, and .js moves into it. In JS, a line that starts with {/* is almost always JSX. An empty body that holds a comment stands after ") " on its line, so it stays code. "{ //" stays code, because an object literal can open that way. The .mjs and .cjs files stay in the slash family. Co-Authored-By: Claude Opus 5.5 --- .../scripts/comment_growth.py | 7 +++-- .../scripts/tests/test_comment_growth.py | 30 ++++++++++++++++++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/skills/addressing-review-feedback/scripts/comment_growth.py b/skills/addressing-review-feedback/scripts/comment_growth.py index 4e31e06..24dff59 100644 --- a/skills/addressing-review-feedback/scripts/comment_growth.py +++ b/skills/addressing-review-feedback/scripts/comment_growth.py @@ -44,7 +44,8 @@ class Family: SLASH = Family("slash", ("//",), (("/*", "*/"),), AFTER) # A comment between JSX tags stands in braces, {/* ... */}. Only these file # types read a line that starts with {/* as a comment: in the others it is code. -JSX = Family("jsx", ("//",), (("/*", "*/"), ("{/*", "*/")), AFTER) +# "{ //" is not a marker, because an object literal can open that way. +JSX = Family("jsx", ("//", "{//"), (("/*", "*/"), ("{/*", "*/"), ("{ /*", "*/")), AFTER) HASH = Family("hash", ("#",), (), AFTER) HASH_ANGLE = Family("hash-angle", ("#",), (("<#", "#>"),), AFTER) HASH_BEGIN = Family("hash-begin", ("#",), (("=begin", "=end"),), AFTER) @@ -57,8 +58,8 @@ class Family: EXTENSIONS: Dict[str, Family] = {} for _family, _extensions in ( - (SLASH, ".c .h .cc .cpp .cxx .hpp .cs .java .js .mjs .cjs .ts .go .rs .swift .kt .kts .scala .dart .php .m .mm .proto .groovy .gradle .zig .css .scss .less .v .sv"), - (JSX, ".jsx .tsx"), + (SLASH, ".c .h .cc .cpp .cxx .hpp .cs .java .mjs .cjs .ts .go .rs .swift .kt .kts .scala .dart .php .m .mm .proto .groovy .gradle .zig .css .scss .less .v .sv"), + (JSX, ".js .jsx .tsx"), (HASH, ".sh .bash .zsh .fish .pl .pm .yaml .yml .toml .tf .tfvars .hcl .r .ex .exs .nix .mk .cmake .conf .cfg .properties .tcl .awk"), (HASH_ANGLE, ".ps1 .psm1"), (HASH_BEGIN, ".rb"), diff --git a/skills/addressing-review-feedback/scripts/tests/test_comment_growth.py b/skills/addressing-review-feedback/scripts/tests/test_comment_growth.py index a3dedd3..849c177 100644 --- a/skills/addressing-review-feedback/scripts/tests/test_comment_growth.py +++ b/skills/addressing-review-feedback/scripts/tests/test_comment_growth.py @@ -292,7 +292,35 @@ def test_slash_comments_still_count_in_these_file_types(self): def test_braces_before_a_comment_are_code_in_other_slash_file_types(self): source = "int run(void)\n{/* empty */}\n" - for path in ("a.ts", "a.js", "a.c"): + for path in ("a.ts", "a.mjs", "a.cjs", "a.c"): + with self.subTest(path=path): + self.assertEqual(self.scan(path, source), []) + + def test_js_files_read_comments_in_braces(self): + source = "
\n {/* about */}\n \n
\n" + self.assertEqual(self.scan("a.js", source), [gate.Block(2, ("{/* about */}",), "")]) + + def test_comment_in_braces_after_a_space_is_a_block(self): + source = "
\n { /* about */ }\n \n
\n" + for path in ("a.js", "a.jsx", "a.tsx"): + with self.subTest(path=path): + self.assertEqual(self.scan(path, source), [gate.Block(2, ("{ /* about */ }",), "")]) + + def test_line_comment_after_a_brace_is_a_block(self): + source = "
\n {// about\n }\n \n
\n" + for path in ("a.js", "a.jsx", "a.tsx"): + with self.subTest(path=path): + self.assertEqual(self.scan(path, source), [gate.Block(2, ("{// about",), "}")]) + + def test_empty_body_that_holds_a_comment_is_code(self): + source = "function stop() {/* nothing to free */}\nstop();\n" + for path in ("a.js", "a.jsx", "a.tsx"): + with self.subTest(path=path): + self.assertEqual(self.scan(path, source), []) + + def test_object_that_opens_with_a_line_comment_is_code(self): + source = "const routes = [\n { // home\n path: '/',\n },\n];\n" + for path in ("a.js", "a.jsx", "a.tsx"): with self.subTest(path=path): self.assertEqual(self.scan(path, source), []) From e728f81a9783ec82811298d6f4cf8169236b5c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=86gir=20M=C3=A1ni=20Hauksson?= <54936225+sourcehawk@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:32:57 +0100 Subject: [PATCH 3/3] fix(skills/addressing-review-feedback): keep object literals that open with {// as code The JSX family read a line that starts with {// as a line comment. An object literal can open that way, as in {// home in an array of routes, so the gate listed a code line as a comment. Only JSX context could tell the two apart, and the gate reads lines, not grammar. The marker is gone. A JSX line comment that stands inside braces on lines of its own is still read through //. Co-Authored-By: Claude Opus 5.5 --- .../scripts/comment_growth.py | 5 +++-- .../scripts/tests/test_comment_growth.py | 15 ++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/skills/addressing-review-feedback/scripts/comment_growth.py b/skills/addressing-review-feedback/scripts/comment_growth.py index 24dff59..3552350 100644 --- a/skills/addressing-review-feedback/scripts/comment_growth.py +++ b/skills/addressing-review-feedback/scripts/comment_growth.py @@ -44,8 +44,9 @@ class Family: SLASH = Family("slash", ("//",), (("/*", "*/"),), AFTER) # A comment between JSX tags stands in braces, {/* ... */}. Only these file # types read a line that starts with {/* as a comment: in the others it is code. -# "{ //" is not a marker, because an object literal can open that way. -JSX = Family("jsx", ("//", "{//"), (("/*", "*/"), ("{/*", "*/"), ("{ /*", "*/")), AFTER) +# A line that starts with "{//" or "{ //" is not a comment, because an object +# literal can open that way. +JSX = Family("jsx", ("//",), (("/*", "*/"), ("{/*", "*/"), ("{ /*", "*/")), AFTER) HASH = Family("hash", ("#",), (), AFTER) HASH_ANGLE = Family("hash-angle", ("#",), (("<#", "#>"),), AFTER) HASH_BEGIN = Family("hash-begin", ("#",), (("=begin", "=end"),), AFTER) diff --git a/skills/addressing-review-feedback/scripts/tests/test_comment_growth.py b/skills/addressing-review-feedback/scripts/tests/test_comment_growth.py index 849c177..a597b88 100644 --- a/skills/addressing-review-feedback/scripts/tests/test_comment_growth.py +++ b/skills/addressing-review-feedback/scripts/tests/test_comment_growth.py @@ -306,11 +306,11 @@ def test_comment_in_braces_after_a_space_is_a_block(self): with self.subTest(path=path): self.assertEqual(self.scan(path, source), [gate.Block(2, ("{ /* about */ }",), "")]) - def test_line_comment_after_a_brace_is_a_block(self): - source = "
\n {// about\n }\n \n
\n" + def test_line_comment_inside_braces_is_a_block(self): + source = "
\n {\n // about\n }\n \n
\n" for path in ("a.js", "a.jsx", "a.tsx"): with self.subTest(path=path): - self.assertEqual(self.scan(path, source), [gate.Block(2, ("{// about",), "}")]) + self.assertEqual(self.scan(path, source), [gate.Block(3, ("// about",), "}")]) def test_empty_body_that_holds_a_comment_is_code(self): source = "function stop() {/* nothing to free */}\nstop();\n" @@ -319,10 +319,11 @@ def test_empty_body_that_holds_a_comment_is_code(self): self.assertEqual(self.scan(path, source), []) def test_object_that_opens_with_a_line_comment_is_code(self): - source = "const routes = [\n { // home\n path: '/',\n },\n];\n" - for path in ("a.js", "a.jsx", "a.tsx"): - with self.subTest(path=path): - self.assertEqual(self.scan(path, source), []) + for opening in ("{ // home", "{// home"): + source = "const routes = [\n %s\n path: '/',\n },\n];\n" % opening + for path in ("a.js", "a.jsx", "a.tsx"): + with self.subTest(path=path, opening=opening): + self.assertEqual(self.scan(path, source), []) def statuses(old, new, family=gate.SLASH):