Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-2459Done

The ink-contrast SCANNER — an AST guard that can tell a faint GLYPH from faint TEXT, proven on fixtures before it is pointed at the repo

⚠️ RE-SCOPED 2026-08-08, mid-motir run. This card was authored as "build a repo-wide guard AND fix every violation", sized 5 points / 90 minutes, against a defect count its own description said nobody had. The count was measured before any code was written: 262 defects across 134 files, plus the discovery that the DOM-reading guard the card prescribed cannot be built repo-wide. So the card keeps the mechanism — which is the only part that was ever a 90-minute job — and the two sweeps became MOTIR-2475 and MOTIR-2477. Filed as a planning bug: MOTIR-2481. The coverage gap it surfaced is MOTIR-2482.

Filed by MOTIR-2455, which settled what --el-text-faint means and fixed the one surface it had measured. This card builds the thing that can find the rest.

Repo: motir-core. One PR.

The rule, already settled and already written down

Measured with axe on a real route, light theme (the binding one — every ink clears AA on every dark surface):

inkpage / card #ffffff--el-surface--el-muted--el-surface-soft
--el-text-faint2.61 ✗2.39 ✗2.37 ✗2.50 ✗
--el-text-muted4.54 ✓4.17 ✗4.12 ✗4.34 ✗
--el-text-secondary6.80 ✓6.24 ✓6.18 ✓6.51 ✓

--el-text-faint is legitimate for exactly two things: decorative glyphs (aria-hidden, or a labelled role="img") and disabled / inactive text, which WCAG 1.4.3 exempts. --el-text-muted is safe only on the white page/card.

The rule is in motir-core/CLAUDE.md and in the token's own comment. Writing it down is what MOTIR-2455 already did; this card builds what enforces it.

Why an AST scanner, and why the original card's mechanism could not be built

The card originally said to model the guard on tests/settings/rolesPermissionsScreens.test.tsx, "that one reads rendered DOM, which is what makes the aria/disabled distinction visible; a source grep cannot make it." The first half is right and the second half is right, and together they do not yield a repo-wide guard: a DOM guard can only see components something RENDERS, so making it repo-wide means rendering every component in the product, which is not a guard, it is a second test suite.

The resolution is that "a grep cannot see it" is not the same as "only a rendered DOM can". A parser sees the element the class lands on — its aria-hidden, its role, its disabled, the ternary its class sits in, and the ancestors that paint a background — which is precisely the structure the distinction turns on. It cannot see across a file boundary; that limit is real, is stated in the scanner's own header, and is what MOTIR-2477 inherits.

Deliverables

  • tests/theme/inkContrastScan.tsscanSource(fileName, text) → InkFinding[]. For each text-(--el-text-faint) site: decorative (the element is aria-hidden, a labelled role="img", or a labelled control whose content is glyphs only), disabled (a disabled / aria-disabled element, or the disabled branch of a ternary), unattributable (a class constant attached to no element), else violation. For each text-(--el-text-muted) site: filter out glyphs and disabled controls first — 1.4.3 measures neither — then walk up to the nearest background-painting ancestor within the file and flag the three tinted surfaces.
  • tests/theme/inkContrastScan.test.ts — the scanner's own fixtures, including its negative case.

Acceptance criteria

  • The scanner exports scanSource, and a violations() helper that treats unattributable as failing — "I cannot see what this is" is a violation, not a pass (the stance tests/work-items/activity-registry-totality.test.ts already takes on unanalyzable keys).
  • The negative case is a real fixture, not a comment: a synthetic source carrying faint ink on a <p> of text makes the scanner report a violation. A guard whose failure path is never exercised is a guard nobody knows is running.
  • Fixtures also pin each PASS: an aria-hidden glyph, a labelled role="img", a labelled glyph-only control, a disabled element, a disabled ? faint : ink ternary — each clears; and an unlabelled role="img" does NOT clear, because nothing else states its meaning.
  • Muted fixtures pin both directions: muted text under bg-(--el-surface) is a violation; the same text under bg-(--el-card) is not; muted on an aria-hidden glyph over a tinted surface is not.
  • The scanner's header states, in prose, the limit it cannot pass: surface resolution stops at the file boundary, so a background painted by another module is invisible to it.
  • This card ships no repo-wide assertion and changes no product code — pointing the scanner at the tree is MOTIR-2475 (faint) and MOTIR-2477 (muted), each landing with the sweep that makes its arm green.
  • The PR body records the measured inventory over origin/main — the four verdict counts per ink, and the count of violation files that sit on routes tests/e2e/shell-a11y*.spec.ts never loads — so the two sweeps start from a number rather than re-deriving one.

Context refs

  • tests/settings/rolesPermissionsScreens.test.tsx — the component-local guard this generalises, and the reason it cannot be generalised by rendering.
  • tests/theme/swapLayerLint.test.ts — the repo-wide token-lint mould the sweeps will follow.
  • tests/work-items/activity-registry-totality.test.ts — the existing TypeScript-AST structural guard in this repo.
  • packages/design-system/theme.css — the two tokens and their comments.
  • motir-core/CLAUDE.md § The token map — the measured table and the rule.
  • MOTIR-2455 — the incident and the decision.