change_impact: treat src/**/*.py as non-code (#4235)

dwc2_info.py is a hand-run helper no build reads; the port rule sent a one-line edit to it to 22 dwc2 families and 9 HIL boards.
diff --git a/test/hil/test/test_change_impact.py b/test/hil/test/test_change_impact.py
index be07339..d66b347 100644
--- a/test/hil/test/test_change_impact.py
+++ b/test/hil/test/test_change_impact.py
@@ -215,8 +215,8 @@
         s = sel(['tools/random_new_script.py'])
         self.assertTrue(s['full'])
 
-    def test_docs_only_is_empty_not_full(self):
-        s = sel(['docs/info/contributing.rst', 'README.rst'])
+    def test_noncode_only_is_empty_not_full(self):
+        s = sel(['docs/info/contributing.rst', 'README.rst', 'src/portable/synopsys/dwc2/dwc2_info.py'])
         self.assertFalse(s['full'])
         self.assertEqual(s['boards'], {})
 
@@ -1838,7 +1838,8 @@
         return change_impact.classify_build(files, REPO)
 
     def test_noncode_and_test_hil_contribute_nothing(self):        # rules 1, 2
-        s = self.b(['docs/info/index.rst', 'README.rst', 'test/hil/hil_test.py', '.claude/skills/hil/SKILL.md'])
+        s = self.b(['docs/info/index.rst', 'README.rst', 'test/hil/hil_test.py', '.claude/skills/hil/SKILL.md',
+                    'src/portable/synopsys/dwc2/dwc2_info.py'])
         self.assertFalse(s['full'])
         self.assertEqual(s['families'], [])
         self.assertEqual(s['family_examples'], {})
diff --git a/tools/change_impact.py b/tools/change_impact.py
index 42a52b6..5058cdc 100755
--- a/tools/change_impact.py
+++ b/tools/change_impact.py
@@ -22,7 +22,7 @@
 can actually build.
 
 | # | Changed path | Build families | Build examples | HIL boards → tests |
-| 1 | `docs/`, `.claude/`, `*.md`, `*.rst`, `LICENSE` | — | — | — |
+| 1 | `docs/`, `.claude/`, `*.md`, `*.rst`, `src/**/*.py`, `LICENSE` | — | — | — |
 | 1b | `.gitignore`, `.clang-format`, `.agents`, `.codex/**`, `.idea/**`, `test/{fuzz,unit-test}/**`, `test/hil/test/**`, non-build `.github/**`, packaging manifests | — | — | — |
 | 2 | `test/hil/**` (not `test/hil/test/**`) | — | — | all boards → all tests |
 | 2b | `tools/drivers_coverage_check.py` | — (local-only tooling, no CI build runs it) | — | — (nothing on the rig runs it) |
@@ -90,8 +90,9 @@
 _NONCODE_RE = re.compile(
     # LICENSE is anchored and LICENSES/ named separately: a bare `LICENSE` alternative
     # also swallowed anything merely STARTING with it (a future LICENSE_extra.c),
-    # which is the silent-under-selection direction
-    r'^(docs/|\.claude/|.*\.(md|rst)$|LICENSE$|LICENSES/)')
+    # which is the silent-under-selection direction. src/ builds from C sources only; its
+    # Python is hand-run helpers (dwc2_info.py)
+    r'^(docs/|\.claude/|.*\.(md|rst)$|src/.*\.py$|LICENSE$|LICENSES/)')
 # Repo metadata and tooling that no CI build reads. Enumerated rather than left to
 # rule 17, which widens BOTH axes: a PR touching only .gitignore and a README was
 # creating 74 cmake legs (each a runner doing checkout + toolchain + get_deps before