{
  "schemaVersion": "1.0.0",
  "rulesVersion": "0.1.0",
  "rules": [
    {
      "id": "tokens/no-hardcoded-color",
      "axis": "tokens",
      "defaultSeverity": "warning",
      "shortDescription": "Disallow hardcoded color values",
      "fullDescription": "Hardcoded color values (#hex, rgb(), hsl(), oklch(), Tailwind arbitrary `bg-[#fff]`) bypass the design system. They survive token changes silently (a brand refresh becomes a manual hunt) and break dark-mode propagation through CSS variables.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/tokens-no-hardcoded-color.md",
      "rationale": "Why it matters\n\nHardcoded colors are the #1 signal that an AI agent ignored the design contract. They silently fork the design system: each #2563eb that should be color.action.primary is a token-rename bomb waiting to detonate.\n\nWhen the rule fires, the suggestion includes the matching token from the project's TokenMap when the reverse-lookup yields exactly one candidate. When multiple tokens map to the same color value (common with primitive vs semantic token layers), all candidates are listed — the agent or human picks.",
      "examples": [
        {
          "good": "<div className=\"bg-action-primary text-on-action\">",
          "bad": "<div style={{ background: \"#2563eb\", color: \"#fff\" }}>"
        },
        {
          "good": "<div className=\"bg-action-primary\">",
          "bad": "<div className=\"bg-[#2563eb]\">"
        },
        {
          "good": "color: var(--color-action-primary);",
          "bad": "color: hsl(214, 86%, 53%);"
        }
      ],
      "allowlist": [
        "currentColor",
        "transparent",
        "inherit",
        "initial",
        "unset",
        "none",
        "auto"
      ]
    },
    {
      "id": "tokens/no-hardcoded-spacing",
      "axis": "tokens",
      "defaultSeverity": "warning",
      "shortDescription": "Disallow off-scale spacing values",
      "fullDescription": "Padding, margin, gap, and similar properties using raw px/rem/em values outside the documented spacing scale (Tailwind config, DTCG dimension tokens, or CSS variables) produce inconsistent rhythm and break theming.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/tokens-no-hardcoded-spacing.md",
      "rationale": "Why it matters\n\nThe spacing scale encodes the rhythm of the product. A one-off `padding: 7px` survives every design-pass and slowly desynchronizes layouts. When the rule fires, the suggestion includes the matching scale step when the value maps to exactly one token.\n\nThe allowlist accommodates 1px borders (`border: 1px solid`), zero, and full-viewport keywords — these are not design-system tokens but pragmatic primitives.",
      "examples": [
        {
          "good": "<div className=\"p-2\">",
          "bad": "<div style={{ padding: \"7px\" }}>"
        },
        {
          "good": "gap: var(--spacing-4);",
          "bad": "gap: 13px;"
        }
      ],
      "allowlist": [
        "0",
        "auto",
        "100%",
        "100vh",
        "100vw"
      ]
    },
    {
      "id": "tokens/dtcg-conformance",
      "axis": "tokens",
      "defaultSeverity": "warning",
      "shortDescription": "Strict W3C DTCG validation for token JSON files",
      "fullDescription": "Validates token JSON files (`*.tokens.json`, files under `tokens/**`) against the W3C Design Tokens Community Group draft. Per-leaf checks: every token must declare `$value` and SHOULD declare `$type`; alias references `{group.name}` must resolve; type-specific values are validated (color = CSS color, dimension = number+unit, fontWeight integer 1-1000 or named, duration = number+unit, cubicBezier = 4-number array or named easing, number = finite number, fontFamily = non-empty string|array). Composite tokens (shadow, typography, border, transition, gradient) are shape-checked.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/tokens-dtcg-conformance.md",
      "rationale": "Why it matters\n\nDTCG conformance is the contract between design and code. Non-conformant token files don't survive round-trips through Style Dictionary, Tokens Studio, or Figma Tokens plugins — they silently corrupt theming and break dark-mode propagation.\n\nThe most common drift modes are: tokens with `$value` but no `$type` (Style Dictionary can't infer the right transform), aliases that point to renamed paths after a refactor, type-claimed but malformed values (`$type: \"color\"` with `$value: \"blu\"`, `$type: \"dimension\"` with `$value: \"16\"` — no unit), and composite shadow tokens with legacy string shapes that no longer parse.",
      "examples": [
        {
          "good": "{ \"color\": { \"brand\": { \"$value\": \"#2563eb\", \"$type\": \"color\" } } }",
          "bad": "{ \"color\": { \"brand\": { \"$value\": \"#2563eb\" } } }"
        },
        {
          "good": "{ \"spacing\": { \"sm\": { \"$value\": \"8px\", \"$type\": \"dimension\" } } }",
          "bad": "{ \"spacing\": { \"sm\": { \"$value\": \"8\", \"$type\": \"dimension\" } } } (no unit)"
        },
        {
          "good": "{ \"semantic\": { \"primary\": { \"$value\": \"{color.brand}\", \"$type\": \"color\" } } } (when color.brand exists)",
          "bad": "{ \"semantic\": { \"primary\": { \"$value\": \"{color.brandd}\", \"$type\": \"color\" } } } (typo, no such path)"
        }
      ],
      "allowlist": [
        "files matching `*.tokens.json` heuristic but containing only $-prefixed metadata (no $value anywhere) — skipped, not flagged",
        "files larger than 1 MB — skipped to avoid pathological cases",
        "files matching `ctx.excludePaths` config",
        "tokens declaring `$extensions.lyse.disable: [\"tokens/dtcg-conformance\"]` — skipped per the standard DTCG extension mechanism"
      ]
    },
    {
      "id": "tokens/description-coverage",
      "axis": "tokens",
      "defaultSeverity": "info",
      "shortDescription": "Semantic tokens should declare a $description",
      "fullDescription": "Measures the fraction of semantic-layer tokens (`action.*`, `surface.*`, `text.*`, `background.*`, `border.*`, `feedback.*`, `state.*`, `interactive.*`, `link.*`, or any token under a `semantic` group) that declare a non-empty `$description`. Emits a single summary finding when coverage falls below 80%.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/tokens-description-coverage.md",
      "rationale": "Why it matters\n\nSemantic tokens are the contract surface between design and code. Their primitive counterparts (`color.blue.500`, `spacing.16`) are self-explanatory — a number or a hex. But `action.primary` only makes sense if the token explains *what* it's for: \"the default action color for primary buttons, brand surfaces, and emphasized text\".\n\nUndocumented semantic tokens cause AI agents and humans alike to pick the wrong token. `$description` is the cheapest documentation surface in a DS — and the one most often skipped.\n\nThe rule is intentionally informational (severity: info) and computes coverage on the semantic layer only. Primitive tokens are excluded from the denominator.",
      "examples": [
        {
          "good": "{ \"action\": { \"primary\": { \"$value\": \"{color.brand.500}\", \"$type\": \"color\", \"$description\": \"Default action color for primary CTAs and emphasized text\" } } }",
          "bad": "{ \"action\": { \"primary\": { \"$value\": \"{color.brand.500}\", \"$type\": \"color\" } } }"
        }
      ],
      "allowlist": [
        "primitive tokens — `color.blue.500`, `spacing.16`, `radius.md` — excluded from the denominator",
        "repos with no DTCG file — rule is N/A (opportunities = 0)"
      ]
    },
    {
      "id": "tokens/theme-modes-present",
      "axis": "tokens",
      "defaultSeverity": "warning",
      "shortDescription": "Design system should define light/dark theme modes",
      "fullDescription": "Checks whether the repository defines theme modes (light/dark) via any of: a `prefers-color-scheme` media query in CSS/SCSS; a `[data-theme]`, `[data-mode]`, or `[data-color-mode]` attribute selector; a `.dark`/`.light` class convention; a DTCG/token JSON file with a `dark` or `light` group or `$extensions` mode split; or a Tailwind v4 `@variant dark` / `dark:` usage indicator. Emits one warning at repo level when no signal is found. Emits nothing when any signal is present.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/tokens-theme-modes-present.md",
      "rationale": "Why it matters\n\nDesign systems without explicit theme-mode declarations leave consumers to implement their own ad-hoc dark-mode strategies, leading to inconsistent behaviour across products. A repo-level signal — however simple — proves the design system has taken a position on color-scheme support.\n\nThe check is intentionally broad: any of the five detection signals (media query, data attribute, class convention, DTCG group, Tailwind v4 variant) counts as \"present\".",
      "examples": [
        {
          "good": ":root { --color-bg: #fff; } [data-theme=\"dark\"] { --color-bg: #111; }",
          "bad": ":root { --color-bg: #fff; }"
        },
        {
          "good": "@media (prefers-color-scheme: dark) { :root { --color-bg: #111; } }",
          "bad": "/* no color-scheme awareness */"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable tokens/theme-modes-present` in a README — rule is N/A",
        "token files larger than 1 MB — skipped to avoid pathological cases"
      ]
    },
    {
      "id": "tokens/css-custom-property-export",
      "axis": "tokens",
      "defaultSeverity": "warning",
      "shortDescription": "Design systems should export theme tokens as CSS custom properties",
      "fullDescription": "Checks, at repo level, whether a design system that paints CSS (any styling declaration in CSS / SCSS / extracted CSS-in-JS) also exports at least one CSS custom-property definition (`--name: value` in `:root`, a `[data-theme]` block, `html`, a `.theme-*` selector, or a Tailwind v4 `@theme` block). Consuming a variable (`var(--x)`) does not count — only a definition does. Emits one warning when the system styles in CSS but defines no custom property anywhere; emits nothing when at least one definition exists or when the system ships no CSS (N/A). A custom property mentioned only inside a comment does not count.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/tokens-css-custom-property-export.md",
      "rationale": "Why it matters\n\nCSS custom properties are the runtime-themeable surface of a design system: a consumer can read `--color-primary` and override it per brand, per mode, or per surface without rebuilding. A design system that locks its tokens in Sass variables or JS objects only — styling everything with literals — can't be re-themed at runtime and gives downstream products nothing to hook into.\n\nThe check is repo-level and broad: a single custom-property definition (or a Tailwind `@theme` block) anywhere clears it.",
      "examples": [
        {
          "good": ":root { --color-primary: #3b82f6; }\n.btn { color: var(--color-primary); }",
          "bad": ".btn { color: #3b82f6; background: #1e293b; }  /* no custom properties exported */"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable tokens/css-custom-property-export` in a README — rule is N/A",
        "design systems that ship no CSS at all — the check does not apply (N/A)"
      ]
    },
    {
      "id": "tokens/responsive-breakpoints",
      "axis": "tokens",
      "defaultSeverity": "warning",
      "shortDescription": "Responsive design systems should tokenize their breakpoints",
      "fullDescription": "Checks, at repo level, whether a design system that uses width-based `@media` queries (in CSS, SCSS, or CSS-in-JS) also defines a tokenized breakpoint scale — loaded breakpoint tokens (Tailwind `screens`, DTCG, CSS vars), SCSS / CSS breakpoint variables (`$breakpoint-*`, `--bp-*`), or a JS/TS `breakpoints` / `screens` object. Emits one warning when the system is responsive but no breakpoint scale is found anywhere; emits nothing when a scale exists or when there are no width media queries (N/A). The per-occurrence detection of hardcoded media-query values overlaps the hardcoded-value rule family and is intentionally not done here.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/tokens-responsive-breakpoints.md",
      "rationale": "Why it matters\n\nWhen breakpoints live as bare literals scattered across stylesheets (`600px` here, `640px` there, `768px` elsewhere), the design system has no single source of truth for its responsive grid. Layouts break at inconsistent widths and consumers can't reason about the system's breakpoints. A tokenized scale — however expressed — makes the breakpoints explicit and shared.\n\nThe check is repo-level and broad: any breakpoint-scale signal anywhere clears it.",
      "examples": [
        {
          "good": "$breakpoint-md: 768px;\n@media (min-width: $breakpoint-md) { .grid { … } }",
          "bad": "@media (min-width: 768px) { .grid { … } }  /* no breakpoint scale anywhere */"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable tokens/responsive-breakpoints` in a README — rule is N/A",
        "design systems that use no width media queries — the check does not apply (N/A)"
      ]
    },
    {
      "id": "tokens/no-hardcoded-media-query",
      "axis": "tokens",
      "defaultSeverity": "warning",
      "shortDescription": "Disallow hardcoded media-query breakpoint values",
      "fullDescription": "Flags raw px/rem/em literals used as breakpoint values inside `@media` width/height features (colon and range syntax) when they are not on the tokenized breakpoint scale. Tokenized breakpoints — SCSS `$breakpoint-*` interpolation, custom properties, or a JS `breakpoints` map — produce no raw literal and never fire. This is the per-occurrence complement to `tokens/responsive-breakpoints`, which checks at repo level whether a breakpoint scale exists at all. Sizing properties (`max-width:`) in normal rule bodies are not media-query breakpoints and are not scanned.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/tokens-no-hardcoded-media-query.md",
      "rationale": "Why it matters\n\nA design system's breakpoints are a shared vocabulary. When media queries hardcode `768px` here and `760px` there, layouts break at inconsistent widths and there is no single source of truth for the responsive grid. Referencing a tokenized breakpoint scale keeps every component snapping to the same widths.\n\nA literal that matches a defined breakpoint token value is treated as on-scale (consistent), and `min-width: 0` resets are ignored.",
      "examples": [
        {
          "good": "@media (min-width: $breakpoint-md) { .grid { display: grid; } }",
          "bad": "@media (min-width: 768px) { .grid { display: grid; } }"
        }
      ],
      "allowlist": [
        "`min-width: 0` and other zero resets",
        "values that match a defined breakpoint token (on-scale)",
        "repos containing `lyse-disable tokens/no-hardcoded-media-query` in a README — rule is N/A"
      ]
    },
    {
      "id": "tokens/container-query",
      "axis": "tokens",
      "defaultSeverity": "warning",
      "shortDescription": "Container queries must have a containment context",
      "fullDescription": "Checks, at repo level, whether a design system that uses CSS `@container` queries (in CSS, SCSS, or extracted CSS-in-JS) also declares a containment context — `container-type`, `container-name`, or the `container:` shorthand — on some ancestor. A `@container` query with no query container anywhere never matches and is dead CSS. Emits one warning when `@container` is used but no context is declared anywhere; emits nothing when a context exists or when the design system uses no container queries (N/A). The rule does NOT penalize design systems for not using container queries — it only checks that the ones present are wired correctly.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/tokens-container-query.md",
      "rationale": "Why it matters\n\nContainer queries let a component respond to the size of its container rather than the viewport — the right primitive for a reusable design system. But an `@container` rule only works if some ancestor establishes a containment context with `container-type` (or the `container` shorthand). Without it, the query silently never matches and the responsive behavior is dead code — a subtle bug that ships unnoticed.\n\nThe check is repo-level and broad: a single containment-context declaration anywhere clears it. It is intentionally non-prescriptive — not using container queries at all is fine (N/A).",
      "examples": [
        {
          "good": ".card-wrap { container-type: inline-size; }\n@container (min-width: 400px) { .card { display: grid; } }",
          "bad": "@container (min-width: 400px) { .card { display: grid; } }  /* no container-type anywhere */"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable tokens/container-query` in a README — rule is N/A",
        "design systems that use no `@container` queries — the check does not apply (N/A)"
      ]
    },
    {
      "id": "components/no-native-shadows",
      "axis": "components",
      "defaultSeverity": "warning",
      "shortDescription": "Disallow native HTML elements when a DS component exists",
      "fullDescription": "Native `<button>`, `<input>`, `<select>`, `<textarea>`, `<a>` used in a file that ALREADY imports from the configured DS module signals an intentional bypass of the design system's component primitives. Polymorphic `as=` props and `excludePaths` are honored.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/components-shadow-native.md",
      "rationale": "Why it matters\n\nDS components encapsulate accessibility, theming, focus-management, and brand-consistent variants. Replacing them with native HTML on a per-component basis fragments these guarantees and creates inconsistent UX.\n\nThe rule only flags when the file already imports from the DS module — this is high-signal (the team uses the DS in this file but bypassed it for this element). Files that don't use the DS at all are skipped.",
      "examples": [
        {
          "good": "<Button variant=\"primary\" onClick={save}>Save</Button>",
          "bad": "<button onClick={save}>Save</button>"
        },
        {
          "good": "<Link href=\"/about\">About</Link>",
          "bad": "<a href=\"/about\">About</a>"
        }
      ],
      "allowlist": [
        "polymorphic `as=\"button\"` in `<Box as=\"button\">`",
        "files matching `designSystem.excludePaths` config"
      ]
    },
    {
      "id": "components/no-icon-fonts",
      "axis": "components",
      "defaultSeverity": "warning",
      "shortDescription": "Deliver icons as SVG, not as an icon font",
      "fullDescription": "Checks, at repo level, whether a design system delivers its icons via an icon webfont rather than SVG. Detects (1) an icon-font dependency in package.json (`font-awesome`, `@fortawesome/fontawesome-free`, `material-icons`, `material-symbols`, `@mdi/font`, `glyphicons`, …); (2) an `@font-face` / `font-family` declaring a known icon-font family; or (3) icon-font ligature classes (`material-icons`, `glyphicon`, `fa fa-*`). Emits one warning at repo level when any signal is found. SVG-component libraries (`lucide-react`, `@fortawesome/react-fontawesome`, etc.) are not flagged.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/components-no-icon-fonts.md",
      "rationale": "Why it matters\n\nIcon fonts map glyphs to private-use Unicode code points. Screen readers announce those code points as meaningless characters, the icons disappear under Windows High Contrast / forced-colors mode, they can't be multi-colored, and they flash-of-unstyled-content until the font loads. SVG icons avoid every one of these: they carry an accessible name (or `aria-hidden`), respect forced-colors, and render instantly.\n\nThe check is repo-level and broad: any icon-font signal anywhere (dependency, `@font-face`, or ligature class) trips it.",
      "examples": [
        {
          "good": "import { Home } from \"lucide-react\";\n<Home aria-hidden />",
          "bad": "<span className=\"material-icons\">home</span>"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable components/no-icon-fonts` in a README — rule is N/A",
        "SVG-component icon libraries (`lucide-react`, `@fortawesome/react-fontawesome`, …) — not icon fonts, never flagged"
      ]
    },
    {
      "id": "components/svg-viewbox",
      "axis": "components",
      "defaultSeverity": "warning",
      "shortDescription": "Inline `<svg>` icons should declare a `viewBox`",
      "fullDescription": "Scans TypeScript/JavaScript JSX for inline `<svg>` opening tags and flags any without a `viewBox` attribute. A `<svg>` with fixed width/height but no `viewBox` does not scale cleanly and can crop its contents — an icon-quality defect. Counts every inline `<svg>` as one opportunity (tags carrying a `{...spread}` are skipped, since a viewBox may arrive at runtime); emits a warning per viewBox-less tag and nothing when all carry one. Self-gating: a design system with no inline SVG records zero opportunities and is N/A.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/components-svg-viewbox.md",
      "rationale": "Why it matters\n\nAn inline `<svg>` without a `viewBox` is locked to its intrinsic pixel size: scaling it (a larger icon, a high-DPI display, a zoom) crops or distorts the artwork instead of resizing the coordinate system. A `viewBox` makes the icon resolution-independent — the single most important attribute for a scalable icon.\n\nThe check is purely structural — it asks only whether the attribute is present, never inspecting its value — so synthetic precision equals real precision. Tags with a JSX spread are deliberately not counted, because the attribute could be supplied dynamically.",
      "examples": [
        {
          "good": "<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\"><path d=\"…\" /></svg>",
          "bad": "<svg width=\"24\" height=\"24\"><path d=\"…\" /></svg>"
        }
      ],
      "allowlist": [
        "inline `// lyse-disable-next-line components/svg-viewbox` above the element",
        "`<svg {...props}>` — skipped (viewBox may be supplied at runtime), not counted as an opportunity"
      ]
    },
    {
      "id": "components/icon-decorative-aria",
      "axis": "components",
      "defaultSeverity": "warning",
      "shortDescription": "Inline SVG icons need an accessible treatment",
      "fullDescription": "Flags inline `<svg>` elements in .tsx/.jsx that have no accessible treatment: no `aria-hidden`, `role`, `aria-label`, or `aria-labelledby` attribute and no `<title>` child. A decorative icon must be hidden from assistive tech (`aria-hidden`), and a meaningful one must be labelled (`role=\"img\"` + `aria-label` / `<title>`). A bare `<svg>` is ambiguous to screen readers — often announced as an unlabeled graphic. Any accessible attribute or a `<title>` child clears it.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/components-icon-decorative-aria.md",
      "rationale": "Why it matters\n\nAn icon is either decorative (it repeats adjacent text — should be silent to a screen reader) or meaningful (it stands alone — must be labelled). A bare `<svg>` declares neither, so assistive tech guesses: many screen readers announce \"graphic\" or read raw path data. Marking intent — `aria-hidden` for decorative, `role=\"img\"` + label for meaningful — is the single most common SVG-accessibility fix.\n\nThe rule is conservative: any of `aria-hidden` / `role` / `aria-label` / `aria-labelledby` / a `<title>` child clears it, so authors who made any accessibility decision are never nagged.",
      "examples": [
        {
          "good": "<svg aria-hidden=\"true\" viewBox=\"0 0 16 16\"><path d=\"…\" /></svg>",
          "bad": "<svg viewBox=\"0 0 16 16\"><path d=\"…\" /></svg>"
        }
      ],
      "allowlist": [
        "any `<svg>` with `aria-hidden`, `role`, `aria-label`, or `aria-labelledby`",
        "any `<svg>` with a `<title>` child",
        "repos containing `lyse-disable components/icon-decorative-aria` in a README — rule is N/A"
      ]
    },
    {
      "id": "components/contracts-strictness",
      "axis": "components",
      "defaultSeverity": "warning",
      "shortDescription": "Component prop contracts must be strictly typed and .d.ts shipped",
      "fullDescription": "Scans exported PascalCase components in .tsx/.jsx files for lax TypeScript prop contracts that hinder AI-agent code generation: props typed `any` or `unknown` (error), and variant-like props (name matching variant/size/intent/color/tone/appearance/kind) typed plain `string` instead of a string-literal union (warning). Also checks each publishable package.json for a `types` or `typings` field and that the referenced file exists post-build (warning when missing).",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/components-contracts-strictness.md",
      "rationale": "Why it matters\n\nAI coding agents and IDE tooling rely on TypeScript prop signatures to suggest valid usages. A prop typed `any` or `unknown` is a black hole — the agent has nothing to constrain its output and falls back to guesses. A variant prop typed plain `string` (`variant: string`) is just as bad: the agent has no way to know the component accepts only `\"primary\" | \"secondary\" | \"ghost\"` and will happily suggest `variant=\"huge\"`.\n\nShipping a `.d.ts` (declared via `package.json` `types` / `typings`) is the same problem at the package boundary: without a declaration file, downstream consumers and agents fall back to untyped any-mode and lose every guarantee the source code put in.\n\nThe rule errors on `any` / `unknown` because those are silent footguns. It warns on variant-string and missing `.d.ts` because there are legitimate (if narrow) reasons to leave them, and because the auto-fix path differs from the unsafe types.",
      "examples": [
        {
          "good": "type ButtonVariant = \"primary\" | \"secondary\" | \"ghost\";\ninterface ButtonProps { variant: ButtonVariant; size: \"sm\" | \"md\" | \"lg\"; }\nexport function Button(props: ButtonProps) { return <button />; }",
          "bad": "interface ButtonProps { variant: string; size: any; data: unknown; }\nexport function Button(props: ButtonProps) { return <button />; }"
        },
        {
          "good": "{ \"name\": \"@acme/ui\", \"main\": \"./dist/index.js\", \"types\": \"./dist/index.d.ts\" }",
          "bad": "{ \"name\": \"@acme/ui\", \"main\": \"./dist/index.js\" }"
        }
      ],
      "allowlist": [
        "framework-allowed props: `children`, `ref`, `key`, `as`, `asChild` (rest-spread `...rest` and ref-forwarded types are skipped)",
        "private packages (`\"private\": true`) and non-publishable package.json files (no `name`/`main`/`module`/`exports`/`types`/`typings`)",
        "test files (.test.tsx, .spec.tsx)",
        "inline `// lyse-disable-next-line components/contracts-strictness` directive (handled by the global suppression engine)"
      ]
    },
    {
      "id": "components/standardized-variant-props",
      "axis": "components",
      "defaultSeverity": "warning",
      "shortDescription": "Variants encoded as separate boolean props",
      "fullDescription": "Flags an exported PascalCase component that declares two or more mutually-exclusive visual-variant flags (primary, secondary, danger, ghost, outline, …) as separate `boolean` props — the 'boolean explosion' antipattern. Such props permit nonsensical combinations (`<Button primary danger>`) and give an AI agent no enumerable vocabulary; the standard is a single `variant` string-literal union. Only names in a curated style-modifier vocabulary, typed `boolean`, count — generic state booleans (`disabled`, `loading`, `fullWidth`, …) are never matched. Orthogonal to `components/contracts-strictness`, which checks the type of an existing `variant` prop.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/components-standardized-variant-props.md",
      "rationale": "Why it matters\n\nA component with `primary`, `secondary`, and `danger` boolean props lets a caller set several at once and offers an AI agent no closed set of valid values. One `variant` union (`\"primary\" | \"secondary\" | \"danger\"`) is mutually exclusive by construction and self-documenting.\n\nA single style boolean (e.g. just `primary`) is a common, acceptable shorthand, so the rule fires only at two or more.\n\nExperimental and unmeasured: real-world precision is pending a harvest measurement; the rule does not contribute to the Health Score.",
      "examples": [
        {
          "good": "interface ButtonProps { variant?: \"primary\" | \"secondary\" | \"danger\"; disabled?: boolean }",
          "bad": "interface ButtonProps { primary?: boolean; secondary?: boolean; danger?: boolean }"
        }
      ],
      "allowlist": [
        "generic state booleans (disabled, loading, active, selected, fullWidth, rounded, …) — not in the style-modifier vocabulary",
        "a single style-modifier boolean (below the >=2 threshold)",
        "style-modifier names that are not typed `boolean`"
      ]
    },
    {
      "id": "components/doc-comments",
      "axis": "components",
      "defaultSeverity": "info",
      "shortDescription": "Public-API components should carry a doc comment",
      "fullDescription": "Scans exported PascalCase components in .tsx/.jsx files (function declarations, arrow/function consts, HOC-wrapped consts via forwardRef/memo/observer/styled, and default-exported functions) and flags those with no leading JSDoc (`/** … */`) doc comment — but ONLY for components that are part of the package's PUBLIC API (re-exported from a resolved package entry; see loaders/public-exports). Internal, demo, and example components are not scanned. Presence only — the quality of the prose is out of scope for the static engine. If the public surface cannot be resolved (no parseable package entry / not a component library), the rule is N/A (0 findings) rather than flooding. Non-component PascalCase exports, non-Pascal exports (hooks, constants), re-exports without a local declaration, and non-.tsx/.jsx files are not scanned.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/components-doc-comments.md",
      "rationale": "Why it matters\n\nA design system's components are its public API. A JSDoc doc comment on a public component is what surfaces in IDE tooltips, what TypeDoc renders, and what AI coding agents read to decide whether and how to use the component. An undocumented public export forces every consumer to read the source. The check is presence-only: a one-line `/** A button. */` clears it — judging the prose is the LLM layer's job, not the static engine's.\n\nScope is deliberately the PUBLIC API only — the names a package actually re-exports from its entry. Internal building blocks and example/demo components carry no documentation obligation toward consumers, so flagging them is noise. When the public surface cannot be resolved, the rule abstains (N/A) rather than guess.",
      "examples": [
        {
          "good": "/** Primary action button. */\nexport function Button() { return <button />; }",
          "bad": "export function Button() { return <button />; }"
        }
      ],
      "allowlist": [
        "internal / demo / example components not re-exported from the package entry",
        "non-component PascalCase exports (objects, `createContext(...)`, theme constants)",
        "non-Pascal exports (hooks, SCREAMING_CASE constants)",
        "test / story / fixture files",
        "packages whose public surface cannot be resolved (rule is N/A)",
        "inline `// lyse-disable-next-line components/doc-comments` directive"
      ]
    },
    {
      "id": "naming/component-pascalcase",
      "axis": "components",
      "defaultSeverity": "warning",
      "shortDescription": "Exported React/Vue/Solid components must be PascalCase",
      "fullDescription": "Exported component functions or const arrow-function components (those returning JSX) that are not named in PascalCase violate React/Vue/Solid component naming conventions. Non-PascalCase names are silently treated as plain elements in JSX, causing components to render as unknown DOM elements and breaking the React component model.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/naming-component-pascalcase.md",
      "rationale": "Why it matters\n\nReact, Vue, and Solid distinguish between HTML element types and component types by case: `<myButton>` creates an unknown HTML element whereas `<MyButton>` invokes the component. A component named `myButton` instead of `MyButton` is silently broken when used as JSX — it renders nothing useful.\n\nAuto-fix renames the declaration and internal same-file references. Cross-file imports must be updated separately (the suggestion includes a warning when the name is exported).",
      "examples": [
        {
          "good": "export function MyButton() { return <button>Click</button>; }",
          "bad": "export function myButton() { return <button>Click</button>; }"
        },
        {
          "good": "export const MyCard = () => <div className=\"card\" />;",
          "bad": "export const my_card = () => <div className=\"card\" />;"
        }
      ],
      "allowlist": [
        "HOC patterns (withRouter, withTheme — start with `with` lowercase)",
        "test utilities in .test.tsx / .spec.tsx files",
        "hooks starting with `use` (handled by naming/hook-prefix)"
      ]
    },
    {
      "id": "naming/hook-prefix",
      "axis": "components",
      "defaultSeverity": "warning",
      "shortDescription": "Custom hooks must start with `use` + uppercase letter",
      "fullDescription": "Exported functions that call other React hooks internally (useState, useEffect, useMemo, useCallback, useRef, useContext, useReducer, useLayoutEffect, and custom use* hooks) are custom hooks by definition. React's rules-of-hooks linter and runtime depend on the `use` prefix to detect hooks — a function named `getMyData` that calls `useState` internally breaks lint, ESLint plugin react-hooks, and can cause subtle hook order violations.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/naming-hook-prefix.md",
      "rationale": "Why it matters\n\nReact's rules of hooks use the `use` prefix to determine if a function is a hook. A misnamed hook (`getMyData` calling `useState`) bypasses this detection, so:\n1. `eslint-plugin-react-hooks` won't apply its rules (silently broken)\n2. Calling the function conditionally becomes valid from ESLint's perspective — but will still crash at runtime\n3. Other developers don't know the function has hook semantics and may call it in non-hook contexts\n\nAuto-fix renames the declaration to `use<CapitalizedName>`. Cross-file callers must be updated separately.",
      "examples": [
        {
          "good": "export function useMyData() { const [d, setD] = useState(null); return d; }",
          "bad": "export function getMyData() { const [d, setD] = useState(null); return d; }"
        },
        {
          "good": "export const useAuth = () => { const ctx = useContext(AuthCtx); return ctx; };",
          "bad": "export const fetchAuth = () => { const ctx = useContext(AuthCtx); return ctx; };"
        }
      ],
      "allowlist": [
        "PascalCase components that happen to call hooks (those are components, not hooks)",
        "test utilities in .test.ts / .spec.ts files",
        "non-exported functions (internal helpers)"
      ]
    },
    {
      "id": "a11y/essentials",
      "axis": "a11y",
      "defaultSeverity": "warning",
      "shortDescription": "Essential accessibility checks (jsx-a11y subset)",
      "fullDescription": "Wraps the canonical `eslint-plugin-jsx-a11y` rules: `alt-text`, `anchor-has-content`, `label-has-associated-control`, `role-has-required-aria-props`, `aria-role`. Surface-level accessibility failures that AI agents most frequently introduce.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/a11y-essentials.md",
      "rationale": "Why it matters\n\nMissing alt text, empty anchors, unlabeled inputs, and invalid ARIA are the most common a11y regressions in AI-generated UI. They block screen-reader users and violate WCAG 2.1 SC 1.1.1, 2.4.4, 1.3.1, 4.1.2.\n\nLyse depends on the canonical `eslint-plugin-jsx-a11y` rather than re-porting these rules — the upstream impl is battle-tested across millions of repos.",
      "examples": [
        {
          "good": "<img src=\"/logo.png\" alt=\"Lyse logo\" />",
          "bad": "<img src=\"/logo.png\" />"
        },
        {
          "good": "<label htmlFor=\"email\">Email</label><input id=\"email\" />",
          "bad": "<label>Email</label><input />"
        }
      ],
      "allowlist": [
        "jsx-a11y allowlists per-rule (e.g., decorative `alt=\"\"` for purely-presentational images)"
      ]
    },
    {
      "id": "a11y/prefers-reduced-motion",
      "axis": "a11y",
      "defaultSeverity": "warning",
      "shortDescription": "Animated design systems should honor `prefers-reduced-motion`",
      "fullDescription": "Checks, at repo level, whether a design system that uses CSS transitions, animations, or `@keyframes` also ships a `prefers-reduced-motion` guard — either a `@media (prefers-reduced-motion: …)` block in CSS / CSS-in-JS, or a `matchMedia('(prefers-reduced-motion: …)')` call in JS/TS. Emits one warning when motion is present but no guard is found anywhere. Emits nothing when a guard exists or when the design system uses no motion (N/A). Motion is detected only from CSS sources (CSS files + extracted CSS-in-JS), not from TS, to avoid mistaking a framer-motion `transition` prop for a CSS animation.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/a11y-prefers-reduced-motion.md",
      "rationale": "Why it matters\n\nVestibular and motion-sensitivity disorders make large or fast animations actively harmful — they can trigger nausea, dizziness, and migraines. The `prefers-reduced-motion` media feature lets users opt out at the OS level; a design system that animates without honoring it ignores that signal for every product built on it.\n\nThe check is repo-level and broad: a single guard anywhere (CSS media query or JS `matchMedia`) is enough to clear it.",
      "examples": [
        {
          "good": ".btn { transition: transform .2s; }\n@media (prefers-reduced-motion: reduce) { .btn { transition: none; } }",
          "bad": ".btn { transition: transform .2s; }"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable a11y/prefers-reduced-motion` in a README — rule is N/A",
        "design systems that use no CSS motion at all — the check does not apply (N/A)"
      ]
    },
    {
      "id": "a11y/focus-visible",
      "axis": "a11y",
      "defaultSeverity": "warning",
      "shortDescription": "Removing the focus outline requires `:focus-visible` adoption",
      "fullDescription": "Checks, at repo level, whether a design system that suppresses the focus outline (`outline: none` / `outline: 0`, in CSS or CSS-in-JS) also adopts `:focus-visible` somewhere — the CSS pseudo-class, or the `focus-visible` polyfill (npm import, `.js-focus-visible` class, or `[data-focus-visible-added]`). Emits one warning when an outline is removed but no `:focus-visible` adoption is found anywhere. Emits nothing when `:focus-visible` is adopted or when no outline is suppressed (N/A). The modern `:focus:not(:focus-visible) { outline: none }` pattern is correct and clears the check because `:focus-visible` is present.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/a11y-focus-visible.md",
      "rationale": "Why it matters\n\nA visible focus indicator is how keyboard and switch users know where they are. Blanket `outline: none` resets — extremely common in design-system base styles — silently delete that indicator for every product downstream. `:focus-visible` is the modern fix: it lets you remove the outline for mouse users while keeping it for keyboard users.\n\nThe check is repo-level and conservative: it only fires when an outline is explicitly removed AND no `:focus-visible` adoption exists anywhere.",
      "examples": [
        {
          "good": "button:focus:not(:focus-visible) { outline: none; }\nbutton:focus-visible { outline: 2px solid; }",
          "bad": "button:focus { outline: none; }"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable a11y/focus-visible` in a README — rule is N/A",
        "design systems that never remove the focus outline — the check does not apply (N/A)"
      ]
    },
    {
      "id": "a11y/inclusive-language",
      "axis": "a11y",
      "defaultSeverity": "info",
      "shortDescription": "Prefer inclusive terminology in code and docs",
      "fullDescription": "Flags a small, high-confidence set of non-inclusive terms in TS/JS, CSS, and CSS-in-JS sources — `whitelist` (→ allowlist), `blacklist` (→ denylist), `sanity check` (→ quick check), `grandfathered` (→ legacy/exempt), and `slave` (→ replica/secondary). Each match is one `info` finding with a suggested replacement. Ambiguous terms (`master`, `dummy`) are deliberately NOT flagged to keep precision high. The block is repo-disablable via a README directive.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/a11y-inclusive-language.md",
      "rationale": "Why it matters\n\nA design system's vocabulary propagates into every product and every developer who consumes it. Terms like `whitelist`/`blacklist` and `master`/`slave` carry exclusionary connotations and have established, clearer replacements (`allowlist`/`denylist`, `primary`/`replica`). Fixing them in the source of truth fixes them everywhere downstream.\n\nThe blocklist is intentionally narrow and unambiguous to avoid false positives.",
      "examples": [
        {
          "good": "const allowlist: string[] = [];\nconst denylist: string[] = [];",
          "bad": "const whitelist: string[] = [];\nconst blacklist: string[] = [];"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable a11y/inclusive-language` in a README — rule is N/A",
        "`master` / `dummy` are never flagged (excluded to avoid false positives)"
      ]
    },
    {
      "id": "a11y/forced-colors",
      "axis": "a11y",
      "defaultSeverity": "warning",
      "shortDescription": "Color design systems should support forced-colors / high-contrast",
      "fullDescription": "Checks, at repo level, whether a design system that paints colors (color/background/border-color/fill/stroke/box-shadow/outline declarations in CSS or CSS-in-JS) also ships a forced-colors / high-contrast affordance — a `@media (forced-colors: active)` or `@media (prefers-contrast: …)` block, the `forced-color-adjust` property, the legacy `-ms-high-contrast` query, or a high-contrast theme selector (`.high-contrast`, `[data-theme*=\"contrast\"]`). A `matchMedia('(forced-colors: …)')` call in JS/TS also clears it. Emits one warning when color is painted but no affordance is found anywhere; emits nothing when an affordance exists or when the design system paints no colors (N/A).",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/a11y-forced-colors.md",
      "rationale": "Why it matters\n\nWindows High Contrast Mode (surfaced to CSS as `forced-colors: active`) replaces the author's palette with a small user-chosen set. Components that lean on background color alone for shape, on box-shadow for elevation, or on color alone to convey state can become invisible or meaningless. The `forced-colors` and `prefers-contrast` media features — plus `forced-color-adjust` and system color keywords — let a design system stay legible for low-vision users who depend on these modes.\n\nThe check is repo-level and broad: a single affordance anywhere clears it.",
      "examples": [
        {
          "good": ".btn { background: var(--accent); }\n@media (forced-colors: active) { .btn { border: 1px solid ButtonText; } }",
          "bad": ".btn { background: var(--accent); box-shadow: 0 1px 2px rgba(0,0,0,.2); }"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable a11y/forced-colors` in a README — rule is N/A",
        "design systems that paint no colors (layout-only CSS) — the check does not apply (N/A)"
      ]
    },
    {
      "id": "a11y/html-lang",
      "axis": "a11y",
      "defaultSeverity": "warning",
      "shortDescription": "The document root should declare a language",
      "fullDescription": "Checks, at repo level, whether the document `<html>` root declares a `lang` attribute. Scans JSX/TSX framework roots (Next.js `app/layout.tsx`, Remix `root.tsx`, Gatsby `html.js`) and real `.html` / `.htm` files for an opening `<html>` tag, and flags one that carries no `lang` (in any form: `lang=\"en\"`, `lang={locale}`, `:lang`, `xml:lang`). Emits one warning when an `<html>` root without `lang` is found; emits nothing when every `<html>` has a language or when the repo ships no `<html>` root at all (a pure component library — N/A). The `dir` attribute (RTL) is not required and is not penalized.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/a11y-html-lang.md",
      "rationale": "Why it matters\n\nWCAG 3.1.1 (Language of Page) requires the document language to be programmatically determinable. The `lang` attribute on `<html>` is how screen readers pick the right voice and pronunciation, how browsers choose hyphenation and quotation marks, and how language-scoped CSS (`:lang()`) and per-locale typography apply. A missing `lang` silently degrades the experience for assistive-tech and international users.\n\nThe check is repo-level and applies only when the design system actually ships an `<html>` root; a component library that never renders `<html>` is N/A.",
      "examples": [
        {
          "good": "export default function RootLayout({ children }) {\n  return <html lang=\"en\"><body>{children}</body></html>;\n}",
          "bad": "export default function RootLayout({ children }) {\n  return <html><body>{children}</body></html>;\n}"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable a11y/html-lang` in a README — rule is N/A",
        "design systems that ship no `<html>` root (pure component libraries) — the check does not apply (N/A)"
      ]
    },
    {
      "id": "a11y/semantic-html",
      "axis": "a11y",
      "defaultSeverity": "warning",
      "shortDescription": "Interactive handlers belong on semantic elements",
      "fullDescription": "Flags native lowercase elements (`div`, `span`, `li`, `section`, …) that carry a click handler (`onClick` / `onMouseDown` / `onMouseUp`) but no `role` attribute — the classic `no-static-element-interactions` accessibility bug. Keyboard and screen-reader users cannot operate a clickable `<div>` that has no semantic role. Native interactive elements (`button`, `a`, `input`, …) are exempt, as are custom PascalCase components (where `onClick` is a prop, not a DOM element) and elements that already declare a `role`.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/a11y-semantic-html.md",
      "rationale": "Why it matters\n\nA clickable `<div>` works for mouse users and no one else: it's not focusable, doesn't fire on Enter/Space, and a screen reader announces nothing actionable. Using the right element — `<button>` — gives keyboard operability, focus, and role for free. When a non-semantic element must be interactive, it needs `role`, `tabIndex`, and a key handler to be equivalent. This rule catches the missing-role case, which is the most common and the most broken.\n\nIt is scoped tightly to avoid false positives: only native lowercase elements with a click handler and no role; component props and already-roled elements are left alone.",
      "examples": [
        {
          "good": "<button onClick={save}>Save</button>",
          "bad": "<div onClick={save}>Save</div>"
        }
      ],
      "allowlist": [
        "native interactive elements (`button`, `a`, `input`, `select`, `textarea`, …) — exempt",
        "elements that declare a `role` (the author opted into explicit semantics)",
        "custom PascalCase components — `onClick` is a prop, not a DOM handler",
        "repos containing `lyse-disable a11y/semantic-html` in a README — rule is N/A"
      ]
    },
    {
      "id": "a11y/runtime-axe",
      "axis": "a11y",
      "defaultSeverity": "warning",
      "shortDescription": "Rendered components pass automated axe-core accessibility checks",
      "fullDescription": "Runs axe-core against a design system's real rendered components, sourced from a pre-built Storybook (`storybook-static/` or a running URL), under `lyse audit --render`. Emits one finding per axe violation (severity from axe impact: critical/serious → error, moderate/minor → warning). N/A when no Storybook is found or `--render` is not set. Covers axe-core's automatable subset (~30% of WCAG criteria) — it complements, never replaces, manual audits.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/a11y-runtime-axe.md",
      "rationale": "Many a11y defects (color contrast, missing alt text, ARIA misuse) only exist in the rendered DOM and are invisible to static analysis. Running axe-core on the design system's own Storybook stories catches them against the exact markup the DS ships.",
      "examples": [
        {
          "good": "<img src=\"logo.png\" alt=\"Acme logo\">",
          "bad": "<img src=\"logo.png\">"
        }
      ],
      "allowlist": [
        "design systems without a pre-built Storybook — the rule is N/A",
        "runs only under `lyse audit --render`; the default audit never invokes it"
      ]
    },
    {
      "id": "a11y/contrast-tokens",
      "axis": "a11y",
      "defaultSeverity": "warning",
      "shortDescription": "Static WCAG-AA contrast check on co-applied foreground/background color pairs",
      "fullDescription": "For each CSS rule, CSS-in-JS block, or inline `style` object that declares BOTH a foreground (`color`) AND a solid background (`background-color` or a solid `background` shorthand), checks WCAG 2.x contrast on literal color values. Emits a warning when the contrast ratio falls below 4.5:1 (normal text) or 3.0:1 (large text: `font-size` ≥ 24px, or ≥ 18.66px with `font-weight` ≥ 700). Skips when: only one of color/background is present; either side is `transparent`, `currentColor`, or `inherit`; the background is a gradient, `url()`, or multi-layer; either value uses `var()` (the DTCG forward map is not available in RuleContext — token-reference pairs are not yet checked); or `contrastRatio` returns null (alpha channel present). Experimental / off-score — not yet calibrated on real repos.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/a11y-contrast-tokens.md",
      "rationale": "Why it matters\n\nInsufficient text contrast is one of the most common WCAG 2.1 SC 1.4.3 failures (AA level). Design system tokens are the right place to enforce it: a DS that ships a co-applied color pair below threshold silently propagates that failure to every product built on it.\n\nThis rule operates statically — it inspects literal color pairs declared together in the same CSS rule, CSS-in-JS block, or inline style object. `var()` references are skipped: the DTCG forward map (token path → resolved value) is not available in RuleContext, so token-reference pairs are not yet checked.\n\nSkips are aggressive: any ambiguity (var(), alpha, gradient, multi-layer background) → no verdict. The rule never guesses.",
      "examples": [
        {
          "good": ".btn { color: var(--color-fg); background: var(--color-bg-action); } /* 7.5:1 */",
          "bad": ".btn { color: #999999; background: #ffffff; } /* 2.85:1 — fails AA */"
        },
        {
          "good": ".heading { color: #111111; background: #ffffff; font-size: 24px; } /* 18.9:1 */",
          "bad": ".caption { color: #aaaaaa; background: #ffffff; } /* 2.32:1 — fails AA */"
        }
      ],
      "allowlist": [
        "rules where only one of color/background is declared (can't check without both)",
        "var() references that can't be resolved via the project's DTCG token map",
        "backgrounds with alpha, gradients, url(), or multi-layer values",
        "color: transparent / currentColor / inherit (not concrete colors)"
      ]
    },
    {
      "id": "stories/coverage",
      "axis": "stories",
      "defaultSeverity": "warning",
      "shortDescription": "DS components without Storybook stories",
      "fullDescription": "DS components in the inventory (imported elsewhere in the codebase) that have no matching Storybook story are flagged. A DS component without a story is undocumented and untested for the team.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/storybook-coverage.md",
      "rationale": "Why it matters\n\nStorybook is the canonical documentation surface for a DS. Components without stories are invisible to designers, untestable visually, and an onboarding hazard for new engineers.\n\nThe rule scans `storybook-static/index.json` first (build output), falling back to filesystem scan of `**/*.stories.{ts,tsx,js,jsx}`. Coverage is computed per `componentInventory` entry — a DS component used N times in the codebase but with no story counts as one finding.",
      "examples": [
        {
          "good": "Button.tsx + Button.stories.tsx",
          "bad": "Button.tsx (no Button.stories.tsx)"
        }
      ],
      "allowlist": [
        "components not in `componentInventory` (i.e., never imported)"
      ]
    },
    {
      "id": "stories/props-documented",
      "axis": "stories",
      "defaultSeverity": "warning",
      "shortDescription": "Stories that document no component props",
      "fullDescription": "A DS component that HAS a Storybook story, HAS known props (from the component inventory), but whose story documents none of those props — neither an `argTypes` block in the default-export meta nor any named story carrying `args` — is flagged. Such a story renders the component but teaches a consumer (human or AI agent) nothing about its API. Prop-less components (e.g. `<Divider>`) and components whose props could not be parsed are excluded. Only components with a story AND known non-empty props are judged; absence of a story is owned by `stories/coverage`.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/stories-props-documented.md",
      "rationale": "Why it matters\n\nThe story is the canonical example surface for a DS component. A story that exercises no props documents nothing an integrator can act on. `argTypes` (explicit controls/docs) OR concrete `args` on a named story both satisfy the rule — autodocs users who set args are not penalized.\n\nThe rule only fires when the component is known to have props. Prop-less components (e.g. layout primitives like `<Divider>`) and components whose props were not parsed are skipped — flagging them would be a false positive since there is nothing to document.\n\nExperimental and unmeasured: real-world precision is pending a harvest measurement; the rule does not contribute to the Health Score.",
      "examples": [
        {
          "good": "export default { component: Button, argTypes: { variant: {...} } }",
          "bad": "export default { component: Button }; export const Primary = {};"
        }
      ],
      "allowlist": [
        "components not in `componentInventory`",
        "inventory components with no story (owned by `stories/coverage`)"
      ]
    },
    {
      "id": "stories/usage-examples",
      "axis": "stories",
      "defaultSeverity": "warning",
      "shortDescription": "Stories that show no usage examples",
      "fullDescription": "A DS component that HAS a Storybook story but whose story shows essentially nothing — fewer than two named story exports AND no export carrying concrete `args` — is flagged. A single undifferentiated render is not a usage example. Only components present in the inventory AND with a story are judged; absence of a story is owned by `stories/coverage`.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/stories-usage-examples.md",
      "rationale": "Why it matters\n\nA consumer (human or AI agent) learns how to use a component from its story examples. A story with a single bare render demonstrates no configuration or variant. Two or more named exports, OR at least one export with concrete `args`, counts as showing usage.\n\nExperimental and unmeasured: real-world precision is pending a harvest measurement; the rule does not contribute to the Health Score.",
      "examples": [
        {
          "good": "export const Primary = {...}; export const Disabled = {...};",
          "bad": "export const Primary = {};"
        }
      ],
      "allowlist": [
        "components not in `componentInventory`",
        "inventory components with no story (owned by `stories/coverage`)"
      ]
    },
    {
      "id": "ai-surface/agents-md-quality",
      "axis": "ai-surface",
      "defaultSeverity": "warning",
      "shortDescription": "AGENTS.md should be command-first and machine-actionable",
      "fullDescription": "Reads `AGENTS.md` at the repo root (with `.github/AGENTS.md` and `docs/AGENTS.md` fallbacks) and verifies three quality signals: (1) at least one fenced code block whose first line is a runnable shell command (pnpm/npm/yarn/bun/python/cargo/make/bash/sh/node/tsx/deno/...), (2) at least one mention of exit codes / status codes / return codes, (3) at least one reference to a toolchain config file the repo actually has (package.json, tsconfig.json, pyproject.toml, Makefile, .lyse.yaml, etc.). Each failing signal emits one warning; absence of AGENTS.md emits one info finding.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/ai-surface-agents-md-quality.md",
      "rationale": "Why it matters\n\nCoding agents (Claude Code, Cursor, Copilot Workspace, etc.) consume AGENTS.md as their bootstrap context. Research from the kodustech/agent-readiness study (Stream 2) shows that command-first AGENTS.md sections — containing runnable commands and explicit exit-code semantics — shift agent task success by 35–55%.\n\nMere presence is not enough. A prose-only AGENTS.md that explains the project in English without listing a single `pnpm test` block leaves the agent guessing at the toolchain. Worse, per Gloaguen et al. (2026), long unstructured context files *reduce* task success and *increase* token cost by 20%.\n\nThe rule enforces the cheapest, highest-leverage discipline: at least one runnable command, at least one explicit exit-code expectation, and at least one reference to the toolchain config the agent will encounter.",
      "examples": [
        {
          "good": "## Build\\n\\n\\`\\`\\`bash\\npnpm install && pnpm test\\n\\`\\`\\`\\n\\nExit code 0 means clean. Uses package.json.",
          "bad": "# Welcome\\n\\nThis repo is a TypeScript project. Please read the README before contributing."
        }
      ],
      "allowlist": [
        "files larger than 500 KB — skipped to avoid pathological cases",
        "repos with no AGENTS.md anywhere — emit a single info finding, not a warning"
      ]
    },
    {
      "id": "ai-surface/component-manifest-json",
      "axis": "ai-surface",
      "defaultSeverity": "info",
      "shortDescription": "Component manifest JSON for MCP cost reduction",
      "fullDescription": "Looks for a component manifest at the repo root (`components.json`, `lyse.components.json`) or in a monorepo (`apps/*/components.json`, `packages/*/components.json`). When found, validates the file is parseable JSON, has a top-level `components` array or object, and each entry has `{ name, sourceFile }` or `{ name, import }`. Absence emits an info finding; malformed manifests emit warnings. shadcn/ui-style `components.json` (the CLI config file, not a manifest) is detected and not counted as a lyse manifest.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/ai-surface-component-manifest-json.md",
      "rationale": "Why it matters\n\nThe Indeed MCP cost study (Stream 1) and the broader manifest-pattern literature (Stream 2) show that a static component manifest reduces MCP tool cost by ~5× compared to reading the underlying source files at lookup time. Agents calling `lyse_components` or equivalent get a deterministic, low-token list of components instead of paging through tsx files.\n\nThe manifest is also the source of truth for component discovery in code-connect and MCP-driven design-to-code workflows — without it, tools must heuristically scan exports or rely on conventions that break across monorepos.\n\nSeverity is intentionally informational for absence (the absence is a missed optimisation, not a bug). Malformed manifests are warnings because they will silently break consumers.",
      "examples": [
        {
          "good": "{ \"components\": [ { \"name\": \"Button\", \"sourceFile\": \"packages/ui/src/button.tsx\" } ] }",
          "bad": "{ \"stuff\": [] }"
        },
        {
          "good": "{ \"components\": { \"Button\": { \"import\": \"@acme/ui\" } } }",
          "bad": "{ \"components\": [ { \"sourceFile\": \"x.tsx\" } ] }"
        }
      ],
      "allowlist": [
        "shadcn/ui `components.json` (the CLI config file) — detected via `$schema` or `aliases` and skipped",
        "files larger than 2 MB — skipped to avoid pathological cases",
        "files matching `ctx.excludePaths` config"
      ]
    },
    {
      "id": "ai-surface/component-manifest-completeness",
      "axis": "ai-surface",
      "defaultSeverity": "info",
      "shortDescription": "Component manifest entry completeness (props / variants / examples)",
      "fullDescription": "For each component entry in a lyse-style component manifest (`components.json` / `lyse.components.json`), checks that the entry documents `props` (non-empty array), `examples` (non-empty array), and — when `variants` is present — that it is not an empty array. Silent when no manifest exists (the `ai-surface/component-manifest-json` rule owns the absence signal). Array-form manifests only (object-form manifests are structure-only in lyse convention).",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/ai-surface-component-manifest-completeness.md",
      "rationale": "Why it matters\n\nA component manifest that lists only `name` and `sourceFile` tells MCP servers WHERE a component lives but not WHAT it does. Agents still have to read the source file to discover props, variants, and usage examples — negating the 5× cost reduction the manifest is meant to deliver.\n\nThis rule closes the gap: a complete entry lets an MCP tool answer \"how do I use Button?\" with a 50-token lookup instead of a 500-token file read.\n\nSeverity is intentionally informational — the manifest works for discovery without completeness data; completeness is a quality improvement, not a correctness issue.",
      "examples": [
        {
          "good": "{\"components\":[{\"name\":\"Button\",\"sourceFile\":\"src/button.tsx\",\"props\":[{\"name\":\"variant\",\"type\":\"string\"}],\"variants\":[\"primary\",\"secondary\"],\"examples\":[\"<Button variant=\\\"primary\\\">Save</Button>\"]}]}",
          "bad": "{\"components\":[{\"name\":\"Button\",\"sourceFile\":\"src/button.tsx\"}]}"
        }
      ],
      "allowlist": [
        "shadcn/ui `components.json` (CLI config) — detected via `$schema` or `aliases` and skipped",
        "manifests with no `components` array (or empty array) — skipped silently",
        "object-form manifests (keyed by component name) — not checked for completeness (structure-only convention)",
        "entries without a `name` field — skipped (manifest-json owns the validation)"
      ]
    },
    {
      "id": "ai-surface/ds-index-exported",
      "axis": "ai-surface",
      "defaultSeverity": "warning",
      "shortDescription": "DS package must export a discoverable index entry",
      "fullDescription": "When `ctx.componentsModule` is configured (or auto-detected) and resolves to a workspace package, verifies that the package has an `src/index.ts` (or `src/index.tsx` / `index.ts` / `index.tsx`) file containing `export` statements (named, type, or `export * from`), with a meaningful surface (≥3 distinct named exports unless `export * from` is used). Rule is N/A when no DS module is configured or when the module is an external library (not a workspace package).",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/ai-surface-ds-index-exported.md",
      "rationale": "Why it matters\n\nA single, discoverable index entry point is the contract surface for MCP servers, code-connect tools, and humans alike. Without it, agents and IDEs must page through arbitrary file structures, guessing at where `Button` lives.\n\nThe rule is intentionally conservative: it accepts `export * from './components'` as an opaque-but-valid surface (we don't recursively follow it) and requires ≥3 distinct named exports only when no star re-exports are present. This avoids false positives on packages that legitimately re-export a single barrel module.\n\nWhen the configured DS module is external (e.g., `@mui/material`), the rule is N/A — there's nothing in the user's repo to fix.",
      "examples": [
        {
          "good": "// packages/ui/src/index.ts\\nexport { Button } from './button';\\nexport { Card } from './card';\\nexport { Modal } from './modal';",
          "bad": "// packages/ui/src/index.ts is missing entirely"
        },
        {
          "good": "// packages/ui/src/index.ts\\nexport * from './components';",
          "bad": "// packages/ui/src/index.ts\\nconst internal = 1;"
        }
      ],
      "allowlist": [
        "external libraries (componentsModule not resolvable to a workspace package) — rule is N/A",
        "repos with no `componentsModule` configured or auto-detected — rule is N/A",
        "indexes using `export * from ...` — accepted without counting named exports"
      ]
    },
    {
      "id": "ai-surface/mcp-config-present",
      "axis": "ai-surface",
      "defaultSeverity": "warning",
      "shortDescription": "Design system should declare at least one MCP server",
      "fullDescription": "Looks for an MCP (Model Context Protocol) configuration file at the repo root: `.mcp.json` (Claude Code convention), `.cursor/mcp.json` (Cursor convention), or `claude_desktop_config.json`. When found, validates the file is parseable JSON, has a top-level `mcpServers` object with at least one entry, and each server entry has a non-empty string key and a `command` (string); `args` (array) is optional. Absence emits one warning (DS not yet AI-agent-accessible). Malformed JSON, missing/empty `mcpServers`, or invalid server entries emit errors.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/ai-surface-mcp-config-present.md",
      "rationale": "Why it matters\n\nThe Model Context Protocol (MCP) is the de-facto standard for letting coding agents (Claude Code, Cursor, Claude Desktop) call tools that surface a design system's components, tokens, and docs at lookup time. A design system without an MCP server declaration leaves agents to scrape README and source files heuristically — the cost-vs-accuracy regression documented in Stream 1 of the AI-Consumable track.\n\nThe signal is binary and cheap to enforce: either the repo declares at least one valid `mcpServers` entry or it doesn't. A warning (not info) reflects the strategic importance of AI-Consumable readiness for Track 2 design systems: shipping a stable MCP surface is now table-stakes.\n\nSeverity escalates to error when a config file is present but malformed — a broken `.mcp.json` silently breaks every agent that tries to connect, which is worse than no config at all.",
      "examples": [
        {
          "good": "{ \"mcpServers\": { \"lyse\": { \"command\": \"npx\", \"args\": [\"@lyse-labs/lyse\", \"mcp\"] } } }",
          "bad": "{ \"mcpServers\": {} }"
        },
        {
          "good": "{ \"mcpServers\": { \"design-system\": { \"command\": \"node\", \"args\": [\"./mcp-server.js\"] } } }",
          "bad": "{ \"servers\": [] }"
        }
      ],
      "allowlist": [
        "files larger than 1 MB — skipped to avoid pathological cases",
        "repos containing `// lyse-disable ai-surface/mcp-config-present` in an adjacent README — rule is N/A"
      ]
    },
    {
      "id": "ai-surface/llms-txt-structure",
      "axis": "ai-surface",
      "defaultSeverity": "warning",
      "shortDescription": "llms.txt at repo root must follow the llmstxt.org structure",
      "fullDescription": "Detects whether a design-system repository ships an `llms.txt` file at the repo root and validates the file's structure against the llmstxt.org specification: a single `# <title>` H1, a `> <summary>` blockquote, and at least one `## <section>` heading whose list items follow `- [<title>](<url>): <description>`. Absence emits a warning. A present-but-malformed `llms.txt` emits one error per structural issue. The optional companion file `llms-full.txt` is neither checked nor required.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/ai-surface-llms-txt-structure.md",
      "rationale": "Why it matters\n\n`llms.txt` (llmstxt.org, Tantum 2024) is the emerging convention for handing AI agents a token-cheap, curated map of a project. For a design system this is the AI-Consumable surface: a single discoverable entry that lists the canonical Quickstart, API reference, component index, and policy docs without forcing the agent to crawl the whole repo.\n\nAbsence is a missed opportunity, not a bug — agents fall back to scanning the README and source tree, which is slower and more expensive. Structural errors (missing H1, missing summary, malformed link rows) are scored as errors because consumers (cursor, claude code, custom agents) parse the file on the assumption it follows the spec, and silent malformations break the contract.\n\nThe companion `llms-full.txt` — a single-file inlining of every linked document — is a useful convention, but this rule neither checks for it nor requires it; only `llms.txt` is inspected.",
      "examples": [
        {
          "good": "# Acme DS\n\n> A token-first React design system.\n\n## Docs\n\n- [Quickstart](https://acme.dev/quickstart): Get started in 3 minutes.\n- [API reference](https://acme.dev/api): Full method index.",
          "bad": "Welcome to Acme DS. We ship Buttons and Cards.\n\n- random link list"
        }
      ],
      "allowlist": [
        "files larger than 1 MB at `llms.txt` — skipped to avoid pathological cases",
        "repos whose README at the root contains the directive `lyse-disable ai-surface/llms-txt-structure` — rule is N/A"
      ]
    },
    {
      "id": "ai-surface/shadcn-registry-valid",
      "axis": "ai-surface",
      "defaultSeverity": "warning",
      "shortDescription": "shadcn-style registry.json must be present and valid",
      "fullDescription": "Detects whether the design system ships a shadcn-style component registry — `registry.json` at the repo root, `public/registry.json` (Next.js-hosted), or per-component `registry/*.json` files. Validates the minimal shadcn shape: each item must have `name` (string), `type` (e.g. \"registry:ui\") and a non-empty `files` array of `{ path, content?, type? }`. Optional fields (`dependencies`, `registryDependencies`, `tailwind`, `cssVars`) are not validated. When `components.json` exists but no registry is shipped, emits a warning for the missed AI-Consumable surface; malformed JSON or missing required fields are errors.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/ai-surface-shadcn-registry-valid.md",
      "rationale": "Why it matters\n\nA valid shadcn-style `registry.json` is the single most reliable signal that a design system is AI-Consumable today. The shadcn CLI uses it to install components into downstream apps; coding agents (Cursor, Claude, GPT) increasingly look for it to discover and pull components instead of scraping source files.\n\nDetection is conservative — we look for the canonical locations only (`registry.json`, `public/registry.json`, `registry/*.json`) and validate only the three required fields per the public shadcn schema. Optional fields like `tailwind` and `cssVars` vary across versions and are not part of the validity contract.\n\nA repo declaring `components.json` (the shadcn CLI marker) but no registry is a strong indicator the team is on the shadcn path but hasn't published the consumable surface — that's a warning, not an error. Malformed JSON or items missing `name`/`type`/`files` will silently break the shadcn CLI and any agent consumer, so they are errors.",
      "examples": [
        {
          "good": "{ \"name\": \"button\", \"type\": \"registry:ui\", \"files\": [{ \"path\": \"ui/button.tsx\" }] }",
          "bad": "{ \"name\": \"button\", \"type\": \"registry:ui\" }"
        },
        {
          "good": "{ \"items\": [{ \"name\": \"button\", \"type\": \"registry:ui\", \"files\": [{ \"path\": \"ui/button.tsx\" }] }] }",
          "bad": "{ \"items\": [{ \"name\": \"button\" }] }"
        }
      ],
      "allowlist": [
        "repos with no `components.json` AND no `registry.json` — rule is N/A",
        "files larger than 4 MB — skipped to avoid pathological cases",
        "files matching `ctx.excludePaths` config",
        "disable in `.lyse.yaml`: `rules: { ai-surface/shadcn-registry-valid: off }`"
      ]
    },
    {
      "id": "ai-surface/agent-instruction-files",
      "axis": "ai-surface",
      "defaultSeverity": "warning",
      "shortDescription": "Repo should ship Cursor rules or Claude skills with valid frontmatter",
      "fullDescription": "Scans for agent instruction bundles at the repo root: `.cursor/rules/*.mdc` (Cursor rules) and `.claude/skills/*/SKILL.md` (Anthropic Claude skills). When neither is present, emits a single warning — the repo gives coding agents no project-specific guidance signal. When found, each file is parsed for YAML frontmatter and validated: Cursor rules must declare `description` and `globs`; Claude skills must declare `name` (kebab-case) and `description` (≤200 chars). Files larger than 5 KB raise a token-budget warning (they cost agents context on every load). Malformed frontmatter and missing required keys raise errors; oversize, non-kebab `name`, and overlong descriptions raise warnings.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/ai-surface-agent-instruction-files.md",
      "rationale": "Why it matters\n\nCursor rules and Claude skills are the contract surface for two of the most-used coding agents in 2026. Without at least one of these bundles, the agent has no project-specific guidance beyond `AGENTS.md` or `CLAUDE.md` — and even those don't carry the same auto-attach semantics as Cursor's `globs` field or the same auto-load semantics as Claude's skill manifest.\n\nBeyond presence, the *frontmatter* matters. Cursor uses `globs` to decide which rule fires for which file edit; missing `globs` silently disables the rule. Claude skills are loaded by their `name` + `description` pair (the description is the agent's \"tool selection\" prompt); a missing description means the skill is invisible to the agent's decision loop.\n\nToken budget is the third signal. The Anthropic agent skills documentation (Oct 2026) and the Cursor rules documentation both recommend keeping individual files small — long instruction files crowd out the actual context the agent needs to read, and inflate per-call cost. The 5 KB heuristic is the same threshold the Claude skill examples use.",
      "examples": [
        {
          "good": "---\\ndescription: TypeScript style guide for this monorepo\\nglobs: [\"src/**/*.ts\", \"src/**/*.tsx\"]\\nalwaysApply: false\\n---\\n\\n# TypeScript style\\n\\nUse strict mode. Prefer type aliases over interfaces.",
          "bad": "---\\n# missing required `description` and `globs`\\n---\\n\\n# TypeScript style\\n\\nUse strict mode."
        },
        {
          "good": "---\\nname: pr-checklist\\ndescription: Generates a PR checklist from the diff (≤200 chars)\\nversion: 1.0.0\\n---\\n\\n# PR checklist skill\\n\\nProcedural instructions for the agent.",
          "bad": "---\\nname: PR_Checklist\\n# missing `description`; `name` is not kebab-case\\n---\\n\\n# PR checklist"
        }
      ],
      "allowlist": [
        "files larger than 500 KB — skipped to avoid pathological cases (and counted as an oversize warning)",
        "files matching `ctx.excludePaths` config",
        "repos that ship only AGENTS.md/CLAUDE.md but neither Cursor rules nor Claude skills — emit one warning (not error) to nudge adoption"
      ]
    },
    {
      "id": "versioning/changelog-present",
      "axis": "ai-surface",
      "defaultSeverity": "warning",
      "shortDescription": "Design system should ship a structured CHANGELOG",
      "fullDescription": "Checks whether the repository ships a version-structured changelog (`CHANGELOG.md`, `HISTORY.md`, `CHANGES.md`, …) with semver-style entry headings (`## [1.2.3]` / `## v1.2.3` / `## 1.2.3`). Emits one warning at repo level when no structured changelog is found; emits nothing when present. Part of the AI-consumable contract (Face A): an AI agent editing code against the design system needs the changelog to track what changed and what broke.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/versioning-changelog-present.md",
      "rationale": "Why it matters\n\nA design system without a structured changelog forces every consumer — human or AI agent — to reverse-engineer what changed from the git log or release tags. For AI-readiness specifically, an agent updating an app against a new DS version needs machine-readable change/breaking-change information to avoid silently breaking the app.\n\nThe check is intentionally lenient on format (any Keep-a-Changelog-style or `v`-prefixed version heading counts) and on filename (CHANGELOG / HISTORY / CHANGES). It is a deterministic presence/structure check, so synthetic precision equals real precision.",
      "examples": [
        {
          "good": "// CHANGELOG.md\n## [1.2.0] - 2026-01-01\n### Added\n- New Button variant",
          "bad": "// no CHANGELOG, or a CHANGELOG with only prose and no version headings"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable versioning/changelog-present` in a README — rule is N/A",
        "changelog files larger than 2 MB — skipped to avoid pathological cases"
      ]
    },
    {
      "id": "versioning/semver-versioning",
      "axis": "ai-surface",
      "defaultSeverity": "warning",
      "shortDescription": "Design system should declare a valid semver version",
      "fullDescription": "Checks whether the repository declares a valid semver `version` in `package.json` (root, or any workspace manifest in a monorepo). Accepts pre-release and build metadata; `0.x` is valid. Emits one warning at repo level when no manifest carries a valid-semver version; emits nothing when present. Part of the AI-consumable contract (Face A): an AI agent editing code against the design system needs a stable, machine-readable version to pin against.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/versioning-semver-versioning.md",
      "rationale": "Why it matters\n\nA design system without a valid semver version gives consumers — human or AI agent — nothing stable to pin against. An agent updating an app against the design system needs a machine-readable version to reason about compatibility and breaking changes.\n\nThe check is intentionally lenient: any semver-valid version passes, including pre-1.0 (`0.x`) versions, which are common for legitimately-maintained design systems. It is a deterministic presence/structure check, so synthetic precision equals real precision.",
      "examples": [
        {
          "good": "// package.json\n{ \"version\": \"1.2.0\" }",
          "bad": "// no version field, or a non-semver value like \"latest\" / \"1.0\" / a date"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable versioning/semver-versioning` in a README — rule is N/A",
        "package.json files larger than 1 MB — skipped to avoid pathological cases"
      ]
    },
    {
      "id": "versioning/migration-guide-present",
      "axis": "ai-surface",
      "defaultSeverity": "warning",
      "shortDescription": "Design system should ship a migration/upgrade guide",
      "fullDescription": "Checks whether the repository ships migration/upgrade guidance — a `MIGRATION.md` / `UPGRADING.md` file (at root or under `docs/`), or a `## Migration` / `## Upgrading` heading inside the CHANGELOG or README. Emits one warning at repo level when none is found; emits nothing when present. Part of the AI-consumable contract (Face A): an agent upgrading an app across a breaking design-system version needs a documented migration path.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/versioning-migration-guide-present.md",
      "rationale": "Why it matters\n\nA design system without any migration/upgrade guidance forces every consumer — human or AI agent — to reverse-engineer how to move across a breaking version from diffs and release notes. For AI-readiness specifically, an agent upgrading an app needs a documented migration path to apply breaking-change codemods safely.\n\nThe check is lenient on both filename (MIGRATION / MIGRATING / UPGRADING / UPGRADE, with or without extension) and location (repo root, `docs/`, or a migration/upgrade heading inside the CHANGELOG/README). It is a deterministic presence/structure check, so synthetic precision equals real precision.",
      "examples": [
        {
          "good": "// MIGRATION.md, or UPGRADING.md, or a `## Migrating to v2` section in CHANGELOG.md",
          "bad": "// no migration/upgrade guide anywhere — consumers must reverse-engineer breaking changes"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable versioning/migration-guide-present` in a README — rule is N/A",
        "guide files larger than 2 MB — skipped to avoid pathological cases"
      ]
    },
    {
      "id": "versioning/deprecation-markers",
      "axis": "ai-surface",
      "defaultSeverity": "warning",
      "shortDescription": "`@deprecated` JSDoc tags should carry migration guidance",
      "fullDescription": "Scans TypeScript/JavaScript JSDoc block comments for `@deprecated` tags and flags any that are bare — no inline description, no wrapped description, no `@see` sibling, and no inline `{@link}`. Counts every `@deprecated` tag as one opportunity; emits a warning per bare tag and nothing when guidance is present. Self-gating: a design system with no `@deprecated` tags records zero opportunities and is N/A (never penalized). Part of the AI-consumable contract (Face A): a coding agent that hits a deprecated symbol needs a machine-readable migration target, not a dead-end marker.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/versioning-deprecation-markers.md",
      "rationale": "Why it matters\n\nA bare `@deprecated` tag tells a consumer the symbol is going away but not what to use instead. A human can grep the changelog; a coding agent editing against the design system cannot reliably recover the migration target and will either keep the deprecated symbol or guess. A tag that carries a replacement pointer (inline description, `@see`, or `{@link}`) is machine-readable migration guidance.\n\nThe check is deliberately structural, not semantic: it only asks whether *some* guidance accompanies the tag, never whether the prose is correct. Detecting deprecation intent in free prose (without the structured tag) is irreducibly heuristic and is out of scope for this deterministic rule — it belongs to the LLM-graded layer. Because this is a pure structural check, synthetic precision equals real precision.",
      "examples": [
        {
          "good": "/** @deprecated Use {@link NewButton} instead. */\nexport const OldButton = () => null;",
          "bad": "/** @deprecated */\nexport const OldButton = () => null;"
        }
      ],
      "allowlist": [
        "block comments containing `lyse-disable versioning/deprecation-markers` — that tag is skipped (and not counted as an opportunity)",
        "any `@deprecated` carrying an inline/wrapped description, a `@see` sibling, or an inline `{@link}` — treated as compliant"
      ]
    },
    {
      "id": "tokens/deprecated-token-usage",
      "axis": "tokens",
      "defaultSeverity": "warning",
      "shortDescription": "Tokens should not alias a deprecated token",
      "fullDescription": "Walks the design system's DTCG token files and flags any token whose `$value` is an alias resolving to a token marked `$deprecated`. Aliasing a deprecated token silently propagates a deprecated value to every consumer of the aliasing token — including AI agents that resolve tokens. Deterministic structural check: synthetic precision equals real precision. Emits nothing when no token is deprecated, or when no token aliases a deprecated one.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/tokens-deprecated-token-usage.md",
      "rationale": "Why it matters\n\nDTCG supports `$deprecated` (boolean or a string reason) to mark a token as on the way out. The contract is that consumers stop referencing it. When another token *aliases* a deprecated token, the deprecation is defeated: every consumer of the aliasing token transitively depends on the deprecated value, and an AI agent resolving the alias has no signal that it landed on deprecated state.\n\nThe check resolves aliases across all token files in one address space, so a cross-file alias to a deprecated token is caught. It only fires when a deprecated token exists AND is aliased, so a system with no deprecations (or clean deprecations) produces no findings.",
      "examples": [
        {
          "good": "// tokens.json — alias points at a live token\n{ \"color\": { \"old\": { \"$value\": \"#000\", \"$deprecated\": \"use color.ink\" }, \"ink\": { \"$value\": \"#111\" }, \"text\": { \"$value\": \"{color.ink}\" } } }",
          "bad": "// text aliases the deprecated token\n{ \"color\": { \"old\": { \"$value\": \"#000\", \"$deprecated\": true }, \"text\": { \"$value\": \"{color.old}\" } } }"
        }
      ],
      "allowlist": [
        "token files matched by `excludePaths` in `.lyse.yaml`",
        "token files larger than 2 MB — skipped to avoid pathological cases"
      ]
    },
    {
      "id": "tokens/no-hardcoded-z-index",
      "axis": "tokens",
      "defaultSeverity": "warning",
      "shortDescription": "Stacking order should come from a z-index token scale",
      "fullDescription": "Flags hardcoded `z-index` integer literals in CSS and CSS-in-JS that are not drawn from a z-index token scale. Trivial local stacking values (`-1`, `0`, `1`) and tokenized references (`var(--z-*)`) are exempt. When a z-index token scale is loaded (`ctx.tokens.zIndex`), values on the scale are treated as compliant; off-scale values are flagged. This catches the classic 'z-index war' anti-pattern where arbitrary magic numbers (`9999`, `99999`) accrete across a codebase with no shared ordering.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/tokens-no-hardcoded-z-index.md",
      "rationale": "Why it matters\n\nZ-index without a shared scale is one of the most common sources of UI bugs in a design system: each component picks an arbitrary large number to \"win\", and overlays, dropdowns, tooltips and modals end up fighting unpredictably. A small, named z-index scale (`--z-dropdown`, `--z-modal`, `--z-toast`) makes stacking order an explicit, reviewable decision.\n\nThis is a value-drift rule, in the same family as the other hardcoded-value detectors.",
      "examples": [
        {
          "good": ":root { --z-modal: 400; }\n.modal { z-index: var(--z-modal); }",
          "bad": ".modal { z-index: 9999; }"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable tokens/no-hardcoded-z-index` in a README — rule is N/A",
        "trivial local stacking values `-1`, `0`, `1` — never flagged"
      ]
    },
    {
      "id": "tokens/no-hardcoded-opacity",
      "axis": "tokens",
      "defaultSeverity": "warning",
      "shortDescription": "Opacity should come from a token scale",
      "fullDescription": "Flags hardcoded fractional `opacity` values in CSS / CSS-in-JS that are not drawn from an opacity token scale. The semantic extremes `0` and `1` and tokenized references (`var(--opacity-*)`) are exempt. When an opacity token scale is loaded (`ctx.tokens.opacity`), on-scale values are compliant; off-scale values are flagged.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/tokens-no-hardcoded-opacity.md",
      "rationale": "Why it matters\n\nAd-hoc opacity values (`0.65`, `0.38`, `0.87`) scattered across a system produce subtly inconsistent muted/disabled/overlay states. A small named opacity scale keeps those states coherent. Value-drift rule.",
      "examples": [
        {
          "good": ":root { --opacity-muted: 0.6; }\n.muted { opacity: var(--opacity-muted); }",
          "bad": ".muted { opacity: 0.65; }"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable tokens/no-hardcoded-opacity` in a README — rule is N/A",
        "the extremes `0` and `1` — never flagged"
      ]
    },
    {
      "id": "tokens/no-hardcoded-border-radius",
      "axis": "tokens",
      "defaultSeverity": "warning",
      "shortDescription": "Corner radius should come from a radii token scale",
      "fullDescription": "Flags hardcoded `border-radius` length literals (px/rem/em) in CSS / CSS-in-JS that are not drawn from a radii token scale. `0`, percentages, the fully-rounded pill idiom (≥ 999px), and tokenized references (`var(--radius-*)`) are exempt. When a radii token scale is loaded (`ctx.tokens.radii`), on-scale values are compliant; off-scale values are flagged.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/tokens-no-hardcoded-border-radius.md",
      "rationale": "Why it matters\n\nInconsistent corner radii (4px here, 6px there, 8px elsewhere) make a system feel unpolished. A small named radii scale keeps roundedness consistent across components. Value-drift rule.",
      "examples": [
        {
          "good": ":root { --radius-md: 8px; }\n.card { border-radius: var(--radius-md); }",
          "bad": ".card { border-radius: 6px; }"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable tokens/no-hardcoded-border-radius` in a README — rule is N/A",
        "`0`, percentages, and the pill idiom (≥ 999px) — never flagged"
      ]
    },
    {
      "id": "tokens/no-hardcoded-border-width",
      "axis": "tokens",
      "defaultSeverity": "warning",
      "shortDescription": "Border thickness should come from a token scale",
      "fullDescription": "Flags hardcoded border-width length literals (px/rem/em) in CSS / CSS-in-JS — both the `border-width` / `border-<side>-width` longhands and the first length inside a `border` / `border-<side>` shorthand — that are not drawn from a border-width token scale. `0`, the ubiquitous `1px` hairline, and tokenized references (`var(--border-width-*)`) are exempt. When a border-width scale is loaded (`ctx.tokens.borderWidth`), on-scale values are compliant; off-scale values are flagged.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/tokens-no-hardcoded-border-width.md",
      "rationale": "Why it matters\n\nBorder thicknesses beyond the default hairline (`2px`, `3px`, `0.5px`) should be deliberate, named choices, not magic numbers sprinkled per component. A small border-width scale keeps emphasis borders consistent. Value-drift rule.",
      "examples": [
        {
          "good": ":root { --border-width-thick: 2px; }\n.active { border: var(--border-width-thick) solid; }",
          "bad": ".active { border: 3px solid; }"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable tokens/no-hardcoded-border-width` in a README — rule is N/A",
        "`0` and the `1px` hairline — never flagged"
      ]
    },
    {
      "id": "tokens/no-hardcoded-motion",
      "axis": "tokens",
      "defaultSeverity": "warning",
      "shortDescription": "Motion durations and easings should come from a token scale",
      "fullDescription": "Flags hardcoded motion values in CSS / CSS-in-JS: transition/animation **durations** (`<n>s` / `<n>ms`, from the longhand or the `transition`/`animation` shorthand) and custom **`cubic-bezier()` easing curves**, when they aren't drawn from a motion token scale. Zero durations, `var(...)` references, and standard easing keywords (`ease`, `linear`, `ease-in-out`, …) are exempt. When a motion token scale is loaded (`ctx.tokens.motion`, keys prefixed `duration/` / `easing/`), on-scale values are compliant (whitespace-insensitive); off-scale values are flagged.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/tokens-no-hardcoded-motion.md",
      "rationale": "Why it matters\n\nInconsistent durations (180ms here, 240ms there) and ad-hoc bezier curves make a system's motion feel incoherent and untunable. A small motion scale (`--duration-fast/base/slow`, `--easing-standard/emphasized`) makes timing a deliberate, shared decision. Value-drift rule.",
      "examples": [
        {
          "good": ":root { --duration-base: 200ms; }\n.x { transition-duration: var(--duration-base); }",
          "bad": ".x { transition: all 0.24s cubic-bezier(0.1, 0.2, 0.3, 0.4); }"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable tokens/no-hardcoded-motion` in a README — rule is N/A",
        "zero durations and standard easing keywords (`ease`, `linear`, …) — never flagged"
      ]
    },
    {
      "id": "tokens/no-hardcoded-shadow",
      "axis": "tokens",
      "defaultSeverity": "warning",
      "shortDescription": "Elevation should come from a shadow token scale",
      "fullDescription": "Flags hardcoded `box-shadow` values in CSS / CSS-in-JS that aren't drawn from a shadow token scale. Keyword values (`none`, `inherit`, …) and tokenized references (`var(--shadow-*)`) are exempt. When a shadow token scale is loaded (`ctx.tokens.shadows`), values matching a token (whitespace-insensitive) are compliant; everything else is flagged. The full declaration value is treated as one unit (a shadow is a composite token, not per-length drift).",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/tokens-no-hardcoded-shadow.md",
      "rationale": "Why it matters\n\nElevation is a system-level language: a handful of named shadows (`--shadow-sm/md/lg`) communicate depth consistently. Hand-rolled `box-shadow` values per component drift into a dozen near-identical-but-not blurs and opacities. Value-drift rule: experimental, does not contribute to the score until calibrated.",
      "examples": [
        {
          "good": ":root { --shadow-sm: 0 1px 3px rgba(0,0,0,0.1); }\n.card { box-shadow: var(--shadow-sm); }",
          "bad": ".card { box-shadow: 0 2px 8px rgba(0,0,0,0.2); }"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable tokens/no-hardcoded-shadow` in a README — rule is N/A",
        "`none` / keyword values — never flagged"
      ]
    },
    {
      "id": "tokens/no-hardcoded-gradient",
      "axis": "tokens",
      "defaultSeverity": "warning",
      "shortDescription": "Gradients should come from a token, not inline literals",
      "fullDescription": "Flags inline CSS gradient functions (`linear-gradient`, `radial-gradient`, `conic-gradient`, and their `repeating-` variants) used as property values in CSS / CSS-in-JS. A gradient defined ON a CSS custom property (`--gradient-brand: linear-gradient(…)`) is the token definition and is exempt, as are gradients referenced via `var(--gradient-*)` (no literal present), comments, and URLs. The gradient is treated as one unit — a composite design token, not per-color drift (raw colors inside are the `tokens/no-hardcoded-color` rule's concern).",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/tokens-no-hardcoded-gradient.md",
      "rationale": "Why it matters\n\nA brand gradient is a system decision — a named token (`--gradient-brand`, `--gradient-scrim`) keeps it consistent and themeable. Inline `linear-gradient(...)` literals scattered across components drift into a dozen near-identical-but-not sheens and can't be re-themed in one place. The good case is to define the gradient once as a custom property and reference it. Value-drift rule: experimental, does not contribute to the score until calibrated.",
      "examples": [
        {
          "good": ":root { --gradient-brand: linear-gradient(90deg, #f00, #00f); }\n.hero { background: var(--gradient-brand); }",
          "bad": ".hero { background: linear-gradient(90deg, #f00, #00f); }"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable tokens/no-hardcoded-gradient` in a README — rule is N/A",
        "gradients defined on a `--custom-property` (the token definition) — never flagged",
        "`var(--gradient-*)` references — no inline literal, so never flagged"
      ]
    },
    {
      "id": "tokens/no-hardcoded-typography",
      "axis": "tokens",
      "defaultSeverity": "warning",
      "shortDescription": "Typography should come from a type token scale",
      "fullDescription": "Flags hardcoded `font-size`, `font-weight`, and `letter-spacing` values in CSS / CSS-in-JS that aren't drawn from a typography token scale (`ctx.tokens.typography`, with `weight/` and `letter-spacing/` prefixed keys). Exemptions keep precision high: `font-size` only flags px/rem/em (percentages and keywords are exempt); `font-weight` exempts the canonical `400`/`700` and all keywords; `letter-spacing` exempts `0` and `normal`; `var(...)` is always exempt. `line-height` is intentionally out of scope — unitless line-heights are pervasive and rarely tokenized, so flagging them is noise.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/tokens-no-hardcoded-typography.md",
      "rationale": "Why it matters\n\nA type scale (`--font-size-sm/md/lg`, `--font-weight-regular/semibold`) is the backbone of a design system's voice. Ad-hoc `font-size: 13px` / `font-weight: 650` scattered per component erode that scale into dozens of near-duplicates. Value-drift rule.",
      "examples": [
        {
          "good": ":root { --font-size-sm: 13px; }\n.label { font-size: var(--font-size-sm); }",
          "bad": ".label { font-size: 13px; font-weight: 650; }"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable tokens/no-hardcoded-typography` in a README — rule is N/A",
        "`font-weight: 400`/`700`, percentage/keyword font-sizes, `letter-spacing: 0`, and `line-height` (out of scope) — never flagged"
      ]
    },
    {
      "id": "ai-governance/ai-marker-component-present",
      "axis": "ai-governance",
      "defaultSeverity": "warning",
      "shortDescription": "Detect AI-marker component in the DS export surface",
      "fullDescription": "Scans the design system's export surface (`src/index.ts`, `index.ts`, etc.) and component files (`**/*.{tsx,jsx,vue}`) for a dedicated AI-marker component — a label, badge, avatar, or indicator that visually marks AI-generated output. Recognised vocabularies: Carbon `AILabel`, generic `AIBadge` / `AITag` / `AIIndicator` / `AIAvatar`, `GenAI*` variants, `*AIMarker*`, Polaris `magic-*` components, and localized markers combining a structural word (label/badge/tag/indicator/marker/avatar/chip/pill) with an AI noun from any active locale (`BadgeIA`, `IALabel`, `KIBadge`, `人工知能Badge`). Emits `info` when a marker component is found; emits `warning` when reserved AI tokens exist (detected by the shared `detectReservedAiTokens` parser) but no marker component is present; emits nothing when the DS has no AI surface at all.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/ai-governance-ai-marker-component-present.md",
      "rationale": "Why it matters\n\nAI-marker components are the visual contract between the design system and consumers: they signal \"this content was produced by AI.\" Without a dedicated component, individual teams reinvent ad-hoc markers, breaking consistency and accessibility.\n\nThe most important case to flag is a DS that ships reserved AI tokens (signaling AI-surface intent) but provides no corresponding component — consumers have no standardised way to mark AI provenance visually.\n\nThis rule emits `info` when a marker component is detected (inventory), and `warning` when reserved tokens exist but no marker component is found. A DS with no AI surface emits nothing and is not penalised.\n\nThe exported `AI_MARKER_NAMES` constant is shared with sibling rules (Track 3.3 / 3.5) to ensure a single vocabulary source of truth.",
      "examples": [
        {
          "good": "// src/index.ts\nexport { AILabel } from './ai-label';\nexport { Button } from './button';",
          "bad": "// src/index.ts — no AI-marker component exported\nexport { Button } from './button';"
        },
        {
          "good": "// AILabel.tsx — component file named with the marker vocabulary",
          "bad": "// tokens.json has `color.ai.primary` but no AILabel/AIBadge component exists"
        },
        {
          "good": "// Polaris-style: magic-icon.tsx component file detected",
          "bad": "// Reserved tokens present, no marker component shipped"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable ai-governance/ai-marker-component-present` in an adjacent README or `.lyse.yaml` — rule is N/A",
        "repos with no reserved AI tokens AND no marker component — no AI surface detected, rule emits nothing",
        "files larger than 1 MB — skipped to avoid pathological cases",
        "files under `node_modules/`, `dist/`, `build/`, `.git/`, `.next/`, `out/`, `coverage/`"
      ]
    },
    {
      "id": "ai-governance/ai-loading-error-states",
      "axis": "ai-governance",
      "defaultSeverity": "warning",
      "shortDescription": "Named AI loading state with paired text + AI-specific error state present",
      "fullDescription": "Scans component files (`**/*.{tsx,jsx,vue}`) for (a) a named AI loading state that carries paired visible or accessible text — not a bare spinner — and (b) an AI-specific error state component. Recognised loading vocabulary: `*Generating*`, `*Thinking*`, `*AILoading*`, `*StreamingIndicator*`, `*AIStatus*`, `*LoadingState*`. A bare generic spinner (`Spinner`, `LoadingSpinner`) without an AI-named wrapper and without a `loadingText` prop or visible status string does NOT satisfy the requirement. Recognised error vocabulary: `*AIError*`, `*GenerationError*`, `*AIFailure*`, `*GenerationFailed*`, `*AITimeout*`, or any name combining an AI keyword (`ai`, `generation`, `genai`, `llm`, `generative`) with an error keyword (`error`, `failure`, `failed`, `timeout`). Emits `warning` for each absent state type when an AI marker surface is detected; emits `info` when both are present; emits nothing when no AI surface is detected. Recovery-flow detection (retry orchestration, post-error behavior) is out of scope — tracked in Track 4 (#16).",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/ai-governance-ai-loading-error-states.md",
      "rationale": "Why it matters\n\nAI-generating surfaces have two failure modes invisible to generic DS rules: (1) a loading state that gives no context — users see a spinner but don't know if the model is generating, stuck, or done — and (2) a generic error boundary that offers no AI-specific message, leaving users without guidance when a generation fails.\n\nAWS Cloudscape's generative-AI patterns mandate that every AI loading state carry named, visible text (e.g. \"Generating response…\") so users understand what the system is doing and can judge when to wait vs. cancel. A bare, unlabelled spinner violates this requirement.\n\nAn AI-specific error component is equally critical: it must communicate that the AI operation failed (not a generic network error) and ideally suggest next steps — though the recovery-flow logic itself is deferred to Track 4.\n\nThis rule detects the static presence of both states. It cross-conditions: if an AI marker is found but either state is absent, a warning is emitted; if both are present, an info finding confirms the DS is provisioned for AI-state handling.",
      "examples": [
        {
          "good": "// Generating.tsx\nexport const Generating = () => (\n  <div role=\"status\" aria-live=\"polite\">\n    <Spinner /> Generating response…\n  </div>\n);",
          "bad": "// LoadingSpinner.tsx — bare spinner, no AI name, no paired text\nexport const LoadingSpinner = () => <svg className=\"spin\" />;"
        },
        {
          "good": "// AILoading.tsx — loadingText prop satisfies paired-text requirement\nexport function AILoading({ loadingText }: { loadingText: string }) {\n  return <div><Spinner /><span>{loadingText}</span></div>;\n}",
          "bad": "// No AI-named loading state at all — only generic Spinner exported"
        },
        {
          "good": "// AIError.tsx\nexport const AIError = ({ message }: { message: string }) => (\n  <div role=\"alert\">\n    <strong>Generation failed</strong>\n    <p>{message}</p>\n  </div>\n);",
          "bad": "// ErrorBoundary.tsx — generic, gives no AI context\nexport class ErrorBoundary extends React.Component {\n  render() { return this.props.children; }\n}"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable ai-governance/ai-loading-error-states` in an adjacent README, README.md, README.mdx, .lyse.yaml, or .lyse.yml — rule is N/A",
        "repos with no AI marker surface detected (no AILabel, AIBadge, magic-* etc.) — no AI surface, rule emits nothing",
        "files larger than 1 MB — skipped to avoid pathological cases",
        "files under `node_modules/`, `dist/`, `build/`, `.git/`, `.next/`, `out/`, `coverage/`",
        "recovery-flow detection (retry orchestration, post-error navigation) — explicitly deferred to Track 4 (#16)"
      ]
    },
    {
      "id": "ai-governance/ai-content-live-region",
      "axis": "ai-governance",
      "defaultSeverity": "warning",
      "shortDescription": "Detect ARIA live region on AI output / streaming components",
      "fullDescription": "Globs `**/*.{tsx,jsx,vue}` and runs two per-file detectors: one that identifies AI-output/streaming surfaces (AI_MARKER_NAMES, *AIResponse*, *ChatMessage*, isStreaming, isGenerating props), another that detects live-region attributes (aria-live=\"polite|assertive\", role=\"status\", role=\"alert\", PatternFly isLiveRegion). Cross-condition: AI surface present but no live region → `warning`; AI surface inside a live region → `info` naming the mechanism; no AI/streaming surface → no finding.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/ai-governance-ai-content-live-region.md",
      "rationale": "Why it matters\n\nScreen-reader users rely on ARIA live regions to hear dynamically updated content. When an AI model streams a response token-by-token, the DOM updates silently unless the container carries aria-live=\"polite\", role=\"status\", role=\"alert\", or an equivalent framework mechanism. Without a live region, visually impaired users miss the entire AI response — a failure of both accessibility and AI-governance (the system produces output that some users cannot perceive).\n\nWAI-ARIA specifies that aria-live=\"polite\" announces changes after the user is idle (appropriate for non-urgent AI output); aria-live=\"assertive\" / role=\"alert\" interrupt immediately (use only for errors). PatternFly provides isLiveRegion as a prop on several container components to avoid hand-rolling the attribute.\n\nThis rule checks the static presence of a live-region mechanism in the same file as an AI-output or streaming component — it does not verify runtime behaviour or dynamic DOM updates.",
      "examples": [
        {
          "good": "// aria-live wraps AI output (React)\nexport function AiAnswer({ content }: { content: string }) {\n  return (\n    <div aria-live=\"polite\">\n      <AILabel>AI</AILabel>\n      <p>{content}</p>\n    </div>\n  );\n}",
          "bad": "// AI output with no live region\nexport function AiAnswer({ content }: { content: string }) {\n  return (\n    <div className=\"response\">\n      <ChatAIResponse content={content} />\n    </div>\n  );\n}"
        },
        {
          "good": "// PatternFly isLiveRegion\n<TextContent isLiveRegion>{streamedContent}</TextContent>",
          "bad": "// isStreaming prop but no live region wrapper\n<OutputBlock isGenerating={generating} />"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable ai-governance/ai-content-live-region` in an adjacent README or `.lyse.yaml` — rule is N/A",
        "repos with no AI-output or streaming component — no AI surface detected, rule emits nothing",
        "files larger than 1 MB — skipped to avoid pathological cases",
        "files under `node_modules/`, `dist/`, `build/`, `.git/`, `.next/`, `out/`, `coverage/`"
      ]
    },
    {
      "id": "ai-governance/feedback-control-present",
      "axis": "ai-governance",
      "defaultSeverity": "warning",
      "shortDescription": "Detect a feedback control on AI output",
      "fullDescription": "When an AI-marker component is detected in the design system, this rule checks whether a companion feedback control exists co-located in the same file. Detection is per-file: a feedback vocabulary match only earns credit when the same file also contains an AI-marker (component name or JSX tag). Detection is two-phase. Phase 1 — name-based scan: checks exported identifiers and file base names against the feedback vocabulary (case-insensitive substring, separator-normalised): `feedback`, `thumbsup`, `thumbsdown`, `rating`, `vote`, `helpful`. Names ending in `Icon`, `Count`, `Result`, `Total`, `Tally`, or `Text` suffixes (display counters / icon primitives) are excluded. Phase 2 — categorized bonus: for each matched feedback component file, checks whether the source exposes a reason vocabulary word (`inaccurate`, `unhelpful`, `offensive`, `tooLong`, `harmful`, `misleading`, `irrelevant`) alongside an enum object, union type, or options array. Three outcomes: AI-marker present + feedback control co-located → `info` (notes if categorized; HAX G15 / PAIR Feedback cited); AI-marker present + no co-located feedback control → `warning`; no AI-marker anywhere → no finding.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/ai-governance-feedback-control-present.md",
      "rationale": "Why it matters\n\nUsers interacting with AI-generated content need a structured way to signal output quality. HAX G15 (IBM Human-AI Experience guidelines, Granular feedback) and the Google PAIR Feedback & Control guidebook require that AI-powered interfaces expose a feedback control — thumbs up/down, rating, or helpful/unhelpful — so users can communicate when AI output is wrong, harmful, or unhelpful.\n\nVendor mandates: Microsoft Fluent 2 AI design guidelines mandate a feedback affordance on AI output; Amazon Cloudscape AI components documentation encourages it; Red Hat PatternFly AI component guidance recommends it. Without a dedicated component, teams implement ad-hoc controls with inconsistent UX, missing accessibility attributes, and no shared vocabulary for categorized negative feedback.\n\nCategorized feedback (why was it bad?) provides richer model-improvement signal than binary thumbs alone. This rule rewards designs that expose a reason enum (inaccurate, unhelpful, offensive) by noting the categorized bonus in the info message.\n\nThe rule uses per-file co-location: a feedback control only earns credit when it lives in the same file as an AI-marker component or JSX tag. Generic form-validation components (ValidationFeedback), display counters (VoteCount), or product review widgets (ProductRating) in unrelated files do not falsely count. The rule fires only when at least one AI-marker file exists. A DS with no AI surface is not penalized.",
      "examples": [
        {
          "good": "// AiFeedback.tsx — co-located with AI marker\nexport const AILabel = () => null;\nexport const ThumbsUp = () => null;\nexport const ThumbsDown = () => null;",
          "bad": "// ValidationFeedback.tsx — form errors, no AI marker\nexport const ValidationFeedback = () => null;\n// AILabel.tsx — separate file, no feedback control"
        },
        {
          "good": "// AiFeedback.tsx — exposes categorized reasons alongside AI marker\nexport const AIBadge = () => null;\nexport const AiFeedback = () => null;\nexport const FeedbackReason = { inaccurate: 'inaccurate', unhelpful: 'unhelpful', offensive: 'offensive' } as const;",
          "bad": "// AiFeedback.tsx — no reason categories\nexport const AiFeedback = () => null;"
        },
        {
          "good": "// StarRating.tsx present alongside AIBadge in the same file",
          "bad": "// AIBadge.tsx present but no rating, vote, or helpful component shipped in the same file"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable ai-governance/feedback-control-present` in an adjacent README or `.lyse.yaml` — rule is N/A",
        "repos with no AI-marker component — no AI surface detected, rule emits nothing",
        "files larger than 1 MB — skipped to avoid pathological cases",
        "files under `node_modules/`, `dist/`, `build/`, `.git/`, `.next/`, `out/`, `coverage/`"
      ]
    },
    {
      "id": "ai-governance/confidence-indicator-present",
      "axis": "ai-governance",
      "defaultSeverity": "warning",
      "shortDescription": "Detect a confidence/uncertainty indicator on AI output",
      "fullDescription": "When an AI-marker component is detected in the design system, this rule checks whether a companion confidence/uncertainty indicator exists co-located in the same file. Detection is per-file: a confidence vocabulary match only earns credit when the same file also contains an AI marker (component name or JSX tag). The scan checks exported identifiers and file base names against the confidence vocabulary (case-insensitive substring, separator-normalised): `confidence`, `uncertainty`, `certainty` — covering names like ConfidenceBadge, ConfidenceScore, ConfidenceLevel, UncertaintyIndicator, CertaintyMeter. Three outcomes: AI-marker present + confidence indicator co-located → `info`; AI-marker present + no co-located confidence indicator → `warning`; no AI-marker anywhere → no finding.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/ai-governance-confidence-indicator-present.md",
      "rationale": "Why it matters\n\nGenerative AI output is probabilistic — it can be confidently wrong. HAX G2 (IBM Human-AI Experience guidelines, \"Make clear how well the system can do what it can do\") and the Google PAIR Explainability + Trust guidebook require AI interfaces to communicate uncertainty, so users can calibrate how much to trust a given result rather than treating every answer as authoritative.\n\nA dedicated, reusable confidence affordance (badge, score, meter, or qualitative low/medium/high indicator) gives teams a consistent vocabulary and accessible UX for surfacing model uncertainty. Without one, teams either omit uncertainty entirely (over-trust) or hand-roll inconsistent ad-hoc indicators.\n\nThe rule uses per-file co-location: a confidence component only earns credit when it lives in the same file as an AI-marker component or JSX tag, so a statistical ConfidenceInterval chart component in an unrelated file does not falsely count. The rule fires only when at least one AI-marker file exists — a design system with no AI surface is not penalized.",
      "examples": [
        {
          "good": "// AiAnswer.tsx — confidence indicator co-located with AI marker\nexport const AILabel = () => null;\nexport const ConfidenceBadge = () => null;",
          "bad": "// AILabel.tsx — AI marker present but no confidence indicator shipped anywhere"
        },
        {
          "good": "// AIOutput.tsx — exposes an uncertainty indicator alongside the AI badge\nexport const AIBadge = () => null;\nexport const UncertaintyIndicator = () => null;",
          "bad": "// ConfidenceInterval.tsx — statistics chart, no AI marker in the file → does not count"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable ai-governance/confidence-indicator-present` in an adjacent README or `.lyse.yaml` — rule is N/A",
        "repos with no AI-marker component — no AI surface detected, rule emits nothing",
        "files larger than 1 MB — skipped to avoid pathological cases",
        "files under `node_modules/`, `dist/`, `build/`, `.git/`, `.next/`, `out/`, `coverage/`"
      ]
    },
    {
      "id": "ai-governance/source-attribution-present",
      "axis": "ai-governance",
      "defaultSeverity": "warning",
      "shortDescription": "Detect a source-attribution component on AI output",
      "fullDescription": "When an AI-marker component is detected in the design system, this rule checks whether a companion source-attribution / citation component exists co-located in the same file. Detection is per-file: an attribution vocabulary match only earns credit when the same file also contains an AI marker (component name or JSX tag). The scan checks exported identifiers and file base names against a distinctive attribution vocabulary (case-insensitive substring, separator-normalised): `citation`, `attribution`, `provenance` — covering names like Citation, Citations, SourceCitation, SourceAttribution, Provenance. The bare generic `source`/`reference` are deliberately excluded to avoid matching SourceCode / ReferenceDocs. Three outcomes: AI-marker present + attribution component co-located → `info`; AI-marker present + no co-located attribution component → `warning`; no AI-marker anywhere → no finding.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/ai-governance-source-attribution-present.md",
      "rationale": "Why it matters\n\nGenerative AI answers are most trustworthy when they cite where their claims come from. HAX G11 (IBM Human-AI Experience guidelines, \"Convey the consequences of user actions\" / explainability of outputs) and the Google PAIR Explainability + Trust guidebook call for AI interfaces to attribute sources, so users can verify generated claims rather than taking them on faith — directly mitigating hallucination harm.\n\nA dedicated, reusable source-attribution component (citation list, inline citations, provenance panel) gives teams a consistent, accessible pattern for surfacing the documents or data behind an answer. Without one, teams either omit attribution (unverifiable output) or hand-roll inconsistent citation UIs.\n\nThe rule uses per-file co-location: an attribution component only earns credit when it lives in the same file as an AI-marker component or JSX tag, so a generic bibliography or academic Citation component in an unrelated, non-AI file does not falsely count. The rule fires only when at least one AI-marker file exists — a design system with no AI surface is not penalized.",
      "examples": [
        {
          "good": "// AiAnswer.tsx — citation component co-located with AI marker\nexport const AILabel = () => null;\nexport const Citations = () => null;",
          "bad": "// AILabel.tsx — AI marker present but no source-attribution component shipped anywhere"
        },
        {
          "good": "// AIOutput.tsx — exposes a provenance panel alongside the AI badge\nexport const AIBadge = () => null;\nexport const SourceAttribution = () => null;",
          "bad": "// Bibliography.tsx — academic citation list, no AI marker in the file → does not count"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable ai-governance/source-attribution-present` in an adjacent README or `.lyse.yaml` — rule is N/A",
        "repos with no AI-marker component — no AI surface detected, rule emits nothing",
        "files larger than 1 MB — skipped to avoid pathological cases",
        "files under `node_modules/`, `dist/`, `build/`, `.git/`, `.next/`, `out/`, `coverage/`"
      ]
    },
    {
      "id": "ai-governance/bot-identity-labeling",
      "axis": "ai-governance",
      "defaultSeverity": "warning",
      "shortDescription": "Detect non-human (bot/avatar) identity labeling on AI surfaces",
      "fullDescription": "When an AI-marker component is detected in the design system, this rule checks whether a companion non-human identity label (bot/AI avatar or persona) exists co-located in the same file. Detection is per-file: an identity vocabulary match only earns credit when the same file also contains an AI marker (component name or JSX tag). The scan checks exported identifiers and file base names against a DISTINCTIVE COMPOUND vocabulary (case-insensitive substring, separator-normalised): `aiavatar`, `botavatar`, `assistantavatar`, `agentavatar`, `aipersona`, `botpersona`, `assistantpersona`, `aiidentity`, `botidentity`, `nonhuman`. A bare `bot` token is deliberately NOT used (it would false-fire on \"bottom\"/\"robot\"). Three outcomes: AI-marker present + identity label co-located → `info`; AI-marker present + no co-located identity label → `warning`; no AI-marker anywhere → no finding.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/ai-governance-bot-identity-labeling.md",
      "rationale": "Why it matters\n\nUsers have a right to know when they are interacting with an AI rather than a human. HAX G1 (IBM Human-AI Experience guidelines, \"Make clear what the system can do\") and the Google PAIR \"Set expectations\" guidance — echoed by emerging disclosure regulation (EU AI Act transparency obligations) — call for conversational AI surfaces to clearly label the agent as non-human, preventing deceptive anthropomorphism.\n\nA dedicated, reusable non-human identity affordance (a bot/AI avatar or a labeled persona) gives teams a consistent, accessible way to disclose the agent's nature. Without one, teams ship human-looking avatars with no disclosure, or hand-roll inconsistent labels.\n\nThe rule uses per-file co-location and distinctive compound vocabulary: an identity label only earns credit when it lives in the same file as an AI-marker component or JSX tag, and only compound names (AiAvatar, BotPersona, NonHumanBadge) match — a generic Avatar primitive does not. The rule fires only when at least one AI-marker file exists — a design system with no AI surface is not penalized.",
      "examples": [
        {
          "good": "// AiChat.tsx — non-human identity label co-located with AI marker\nexport const AILabel = () => null;\nexport const AiAvatar = () => null;",
          "bad": "// AILabel.tsx — AI marker present but no non-human identity label shipped anywhere"
        },
        {
          "good": "// Assistant.tsx — exposes a labeled bot persona alongside the AI badge\nexport const AIBadge = () => null;\nexport const BotPersona = () => null;",
          "bad": "// Avatar.tsx — generic user avatar primitive, no AI marker in the file → does not count"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable ai-governance/bot-identity-labeling` in an adjacent README or `.lyse.yaml` — rule is N/A",
        "repos with no AI-marker component — no AI surface detected, rule emits nothing",
        "files larger than 1 MB — skipped to avoid pathological cases",
        "files under `node_modules/`, `dist/`, `build/`, `.git/`, `.next/`, `out/`, `coverage/`"
      ]
    },
    {
      "id": "ai-governance/ai-token-misuse",
      "axis": "ai-governance",
      "defaultSeverity": "warning",
      "shortDescription": "Detect AI-reserved design tokens used outside AI surfaces",
      "fullDescription": "Flags reserved AI design tokens (Carbon `--cds-ai-*` / `$ai-aura-*`, Cloudscape `$*-gen-ai`, Polaris `magic-*`, etc.) that are USED (`var(--ai-*)`, `$ai-*`, `theme.$ai-*`) in a file that is not an AI surface. A file counts as an AI context — a legitimate place to use AI tokens — when it (1) contains an AI-marker component or JSX tag, (2) is AI-named by path (e.g. Carbon's `_ai-aura.scss`), or (3) defines reserved AI tokens itself. Token DECLARATIONS (`--ai-*:` / `$ai-*:`) are never flagged — defining a token is not misuse. A reserved AI token referenced in a file that is none of those (e.g. a generic `Button.css`) is flagged as misuse. The rule is silent on repos that use no reserved AI tokens.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/ai-governance-ai-token-misuse.md",
      "rationale": "Why it matters\n\nAI design systems reserve a distinct visual language — gradient auras, sparkle accents, \"magic\" tokens — to signal *this content was AI-generated*. The signal only works if it is exclusive: if the same AI-reserved tokens are reused to decorate ordinary, non-AI UI, users can no longer trust the visual cue to mean \"AI\". IBM Carbon, Salesforce, and Microsoft AI guidelines all treat the AI visual treatment as reserved.\n\nThis rule (Appendix A static signal `ai-token-misused-on-non-AI-element`) catches the dilution at the source: a reserved AI token referenced outside any AI surface. Detection is deliberately conservative — usage only (never token definitions), and three independent AI-context signals (marker component, AI-named path, or local AI-token definition) suppress the obvious legitimate cases (Carbon's `_ai-*.scss`, the token-source file) to keep precision high. The rule emits nothing on design systems that ship no reserved AI tokens, so non-AI systems are never penalized.",
      "examples": [
        {
          "good": "/* AiPanel.tsx — AI token used inside an AI surface */\nexport const AILabel = () => null;\nexport const Panel = () => <div style={{ background: 'var(--ai-gradient-1)' }} />;",
          "bad": "/* Button.css — AI-reserved token reused on generic UI */\n.btn { background: var(--ai-gradient-1); }"
        },
        {
          "good": "/* _ai-aura.scss — AI-named file legitimately uses the AI token */\n.aura { background: theme.$ai-aura-start; }",
          "bad": "/* Card.scss — non-AI component misusing the AI aura token */\n.card { box-shadow: 0 0 8px theme.$ai-aura-start; }"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable ai-governance/ai-token-misuse` in an adjacent README or `.lyse.yaml` — rule is N/A",
        "repos with no reserved AI tokens — rule emits nothing",
        "files that define reserved AI tokens, are AI-named, or contain an AI-marker — usage there is legitimate",
        "files under `node_modules/`, `dist/`, `build/`, `.git/`, `.next/`, `out/`, `coverage/`"
      ]
    },
    {
      "id": "ai-governance/interaction-pattern-docs",
      "axis": "ai-governance",
      "defaultSeverity": "warning",
      "shortDescription": "Detect in-repo docs for AI interaction patterns",
      "fullDescription": "When an AI surface (AI-marker component) is present, this rule checks whether the design system ships in-repo documentation of its AI interaction patterns. Detection is heading-based and AI-context-gated: it scans markdown (`**/*.{md,mdx}`) and counts the six Kavcic/HAX interaction-pattern types (suggestion, generation, authorization, handoff, regeneration, history) that appear as a `#` heading, but only in docs that reference an AI surface (path or content mentions ai / assistant / generative / copilot / llm / chatbot / prompt). This keeps generic `## History` (changelog) or `## Generation` (release notes) in non-AI docs from counting, and ignores pattern words in body text. `generation` uses a negative lookbehind so `## Regeneration` routes to `regeneration`. Three outcomes: AI surface + ≥1 pattern doc → `info` (lists coverage n/6); AI surface + no pattern docs → `warning`; no AI surface → no finding. Doc quality is out of scope (presence only).",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/ai-governance-interaction-pattern-docs.md",
      "rationale": "Why it matters\n\nA design system that ships AI features but never documents *how* its AI interaction patterns behave leaves product teams to reinvent — inconsistently — when and how the AI suggests, generates, asks for authorization, hands off to a human, regenerates, or exposes history. The Kavcic AI-design maturity model and IBM HAX guidelines treat documented, reusable interaction patterns as a core governance signal: the difference between an AI design system and a pile of AI components.\n\nThis rule (Track 9.9, docs-as-object, presence only) detects whether those patterns are documented in-repo. It is deliberately conservative on precision — heading-based detection in AI-context docs only, never body text — so it rewards genuine pattern documentation rather than incidental keyword matches. Quality of the docs is a separate, semantic concern (no NLP in the static engine). The rule is silent on design systems with no AI surface, so non-AI systems are never penalized.",
      "examples": [
        {
          "good": "<!-- docs/ai-patterns.md — AI-context doc with pattern headings -->\n# AI Assistant Patterns\n## Suggestions\n## Regeneration\n## Human Handoff",
          "bad": "<!-- AI surface shipped, but only a generic README with no documented AI interaction patterns -->\n# My Design System\n## Installation\n## Components"
        },
        {
          "good": "<!-- docs/copilot.md -->\n# Copilot\n## Content Generation\n## Authorization & Consent\n## Conversation History",
          "bad": "<!-- CHANGELOG.md — `## History` / `## Generation` here are NOT AI pattern docs (non-AI context) -->\n# Changelog\n## History"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable ai-governance/interaction-pattern-docs` in an adjacent README or `.lyse.yaml` — rule is N/A",
        "repos with no AI-marker component — no AI surface detected, rule emits nothing",
        "non-AI docs (path/content without an AI reference) — pattern headings there do not count",
        "files under `node_modules/`, `dist/`, `build/`, `.git/`, `.next/`, `out/`, `coverage/`"
      ]
    },
    {
      "id": "ai-governance/draft-attribution",
      "axis": "ai-governance",
      "defaultSeverity": "warning",
      "shortDescription": "Detect the AI draft-attribution convention",
      "fullDescription": "When an AI surface (AI-marker component) is present, this rule checks whether the design system adopts an AI-content attribution convention (Appendix A). Detection is conservative for precision: the phrase form requires \"first draft\" anchored to an authoring verb (created / made / generated / written / drafted) plus with/by/using — so generic \"Created with Sketch\" or \"first draft of the proposal\" do not match; the structured form matches distinctive markers (`data-ai-generated`, `ai-generated` / `aiGenerated`, `drafted-with`, or a `DraftAttribution` / `AiAttribution` / `GeneratedWith*` identifier). Scans `**/*.{tsx,jsx,vue,md,mdx,ts}`. Three outcomes: AI surface + convention present → `info`; AI surface + absent → `warning`; no AI surface → no finding.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/ai-governance-draft-attribution.md",
      "rationale": "Why it matters\n\nAs AI assists more design-system content — copy, docs, even component scaffolds — transparent provenance becomes a trust and governance requirement. The \"First draft created with [tool]\" convention (HAX / emerging AI-disclosure norms, Appendix A) gives teams a lightweight, consistent way to attribute AI-assisted content so reviewers and users know what was machine-drafted.\n\nThis rule detects whether the convention is adopted at all. It is deliberately precision-first — anchored phrases and distinctive structured markers, never bare \"created with\" — so it rewards genuine attribution rather than incidental text. The rule is silent on design systems with no AI surface, so non-AI systems are never penalized.",
      "examples": [
        {
          "good": "<!-- README.md — AI-assisted content attributed -->\n# Component spec\n\n_First draft created with Claude; reviewed by the design team._",
          "bad": "<!-- AI surface shipped, but no attribution convention anywhere -->\n# Component spec\n\nWritten by the team."
        },
        {
          "good": "// Structured marker on AI-assisted content\nexport const Doc = () => <article data-ai-generated=\"true\">…</article>;",
          "bad": "// Generic footer — not an attribution convention\nexport const Footer = () => <p>Created with Sketch</p>;"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable ai-governance/draft-attribution` in an adjacent README or `.lyse.yaml` — rule is N/A",
        "repos with no AI-marker component — no AI surface detected, rule emits nothing",
        "generic `created with` / unrelated `first draft` text — not matched (anchored detection)",
        "files under `node_modules/`, `dist/`, `build/`, `.git/`, `.next/`, `out/`, `coverage/`"
      ]
    },
    {
      "id": "ai-governance/product-analytics",
      "axis": "ai-governance",
      "defaultSeverity": "warning",
      "shortDescription": "Detect AI accept/reject/feedback surfaces shipped without product-analytics instrumentation",
      "fullDescription": "Scans component files (`**/*.{tsx,jsx,vue}`) for AI-marker surfaces (per the shared `fileHasAiMarker` predicate) that carry accept/reject/feedback interaction handlers (`onAccept`, `onReject`, `onApprove`, `onThumbsUp`, `onThumbsDown`, `onRate`, `onFeedback`, or `data-action=\"accept|reject|feedback|thumbs-up|thumbs-down|rate\"`). For each such file it checks, file-level, whether any product-analytics instrumentation call is present (curated, word-bounded set: `track(`, `trackEvent(`, `captureEvent(`, `logEvent(`, `gtag(`, `.track(`, `.capture(`, `dataLayer.push(`, the `posthog.`/`mixpanel.`/`amplitude.`/`segment.`/`analytics.` SDK prefixes, and `useAnalytics`). When the AI surface has the interaction handlers but no instrumentation, emits one `warning` per file at the first handler's location. Files that are not AI surfaces, or AI surfaces with no such handler, emit nothing. Presence only — one analytics call satisfies the check.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/ai-governance-product-analytics.md",
      "rationale": "Why it matters\n\nWhen a product ships AI accept/reject/feedback controls but never instruments them, it cannot measure acceptance and rejection rates — it flies blind on its own AI quality. Detecting the presence of product-analytics instrumentation on those surfaces is a cheap, high-signal static check.\n\nThis rule is presence-only: it verifies that some instrumentation exists in the file, not that it is correctly wired. A repo with no AI-marker surface emits nothing and is not penalised.",
      "examples": [
        {
          "good": "// AiSuggestion.tsx — AI surface with accept/reject + analytics\nimport { analytics } from './analytics';\nexport function AiSuggestion() {\n  return <Row onAccept={() => analytics.track('ai_accepted')} onReject={() => analytics.track('ai_rejected')} />;\n}",
          "bad": "// AiSuggestion.tsx — AI surface, accept/reject handlers, NO analytics\nexport function AiSuggestion() {\n  return <Row onAccept={accept} onReject={reject} />;\n}"
        }
      ],
      "allowlist": [
        "repos containing `lyse-disable ai-governance/product-analytics` in an adjacent README or `.lyse.yaml` — rule is N/A",
        "repos with no AI-marker component at all — no AI surface detected, rule emits nothing",
        "AI surfaces with no accept/reject/feedback handler — out of scope, rule emits nothing",
        "files under `node_modules/`, `dist/`, `build/`, `.git/`, `.next/`, `out/`, `coverage/`"
      ]
    },
    {
      "id": "tokens/rendered-token-fidelity",
      "axis": "tokens",
      "defaultSeverity": "warning",
      "shortDescription": "Rendered token value matches its DTCG canonical declaration",
      "fullDescription": "Detects design→CSS drift: a CSS custom property whose browser-computed value differs from its DTCG canonical token value. Runs only under `lyse audit --render`.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/tokens-rendered-token-fidelity.md",
      "rationale": "A token can be referenced correctly yet render a different value due to cascade, specificity, or a leaked override — drift static analysis cannot see.",
      "examples": [
        {
          "good": ":root { --bg: #fff } /* DTCG declares #fff; element computes rgb(255,255,255) */",
          "bad": ":root { --bg: #fff } .leak { --bg: #000 } /* DTCG declares #fff; element computes rgb(0,0,0) */"
        }
      ],
      "allowlist": []
    },
    {
      "id": "components/no-arbitrary-tailwind",
      "axis": "components",
      "defaultSeverity": "warning",
      "shortDescription": "Disallow non-color arbitrary Tailwind values",
      "fullDescription": "Arbitrary Tailwind utilities (e.g. `p-[12px]`, `text-[14px]`, `w-[37px]`) bypass the configured design scale. These literal bracket values embed hardcoded spacing, sizing, or typography outside any token contract — making token-based refactors miss them silently. Color bracket values (e.g. `bg-[#fff]`) are handled by `tokens/no-hardcoded-color`.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/components-no-arbitrary-tailwind.md",
      "rationale": "Why it matters\n\nArbitrary Tailwind values short-circuit the design system contract the same way inline styles do. A spacing change (4→5px base) or a typography scale update won't catch `text-[14px]` — the drift is invisible to token-based tooling.\n\nThe color variant (`bg-[#fff]`) is already handled by `tokens/no-hardcoded-color`. This rule covers the non-color remainder: spacing, sizing, typography, layout, and any other literal scale bypass.",
      "examples": [
        {
          "good": "<div className=\"p-4 text-sm\">",
          "bad": "<div className=\"p-[12px] text-[14px]\">"
        },
        {
          "good": "<div className=\"w-full\">",
          "bad": "<div className=\"w-[37px]\">"
        },
        {
          "good": "<div className=\"gap-4\">",
          "bad": "<div className=\"gap-[10px]\">"
        }
      ],
      "allowlist": []
    },
    {
      "id": "components/no-style-escape-hatch",
      "axis": "components",
      "defaultSeverity": "warning",
      "shortDescription": "Disallow inline `style` prop on DS components",
      "fullDescription": "An inline `style` prop on a design-system component bypasses the component's own prop API (variant/size/color props, CSS-variable theming). It makes one-off overrides invisible to token tooling, breaks dark-mode propagation, and forks the component's visual contract silently.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/components-no-style-escape-hatch.md",
      "rationale": "Why it matters\n\nDS components expose a deliberate prop API precisely so that consumers never need to reach for `style`. An inline `style` prop is the runtime equivalent of !important: it bypasses variant tokens, breaks dark-mode cascade, and survives token renames silently.\n\nThe rule is value-agnostic: `style={{ color: \"red\" }}` and `style={{ margin: 0 }}` are equally flagged. The fix is always to use the component's intended API (`variant`, `size`, `color`, `sx`, etc.) or a global token instead.",
      "examples": [
        {
          "good": "<Button variant=\"primary\" size=\"md\">Save</Button>",
          "bad": "<Button style={{ color: \"#2563eb\" }}>Save</Button>"
        },
        {
          "good": "<Badge color=\"success\" />",
          "bad": "<Badge style={{ background: \"green\" }} />"
        }
      ],
      "allowlist": []
    },
    {
      "id": "a11y/interactive-role-name",
      "axis": "a11y",
      "defaultSeverity": "warning",
      "shortDescription": "Accessible name on interactive controls",
      "fullDescription": "Wraps `jsx-a11y/control-has-associated-label`: every interactive control (`<button>`, `<input>`, `<select>`, `<textarea>`, `<a>`) must have an accessible name — via visible text, `aria-label`, `aria-labelledby`, or a `<label>`. This is the one accessible-name rule that `a11y/essentials` does not cover.",
      "helpUri": "https://github.com/lyse-labs/lyse/blob/main/docs/rules/a11y-interactive-role-name.md",
      "rationale": "Why it matters\n\nIcon-only buttons (a close button wrapping only an SVG, a toolbar action with no label) are the most frequent accessible-name omission in AI-generated UI. They block screen-reader users who cannot determine what the control does, violating WCAG 2.1 SC 4.1.2 (Name, Role, Value).\n\n`a11y/essentials` already covers image `alt`, form `<label>`, ARIA role validity, and anchor content. This rule covers the remaining interactive-control gap via the upstream `eslint-plugin-jsx-a11y` `control-has-associated-label` rule.",
      "examples": [
        {
          "good": "<button aria-label=\"Close dialog\"><svg aria-hidden=\"true\" /></button>",
          "bad": "<button><svg /></button>"
        },
        {
          "good": "<button>Save</button>",
          "bad": "<button><span class=\"icon-save\" /></button>"
        }
      ],
      "allowlist": [
        "Decorative controls that are intentionally hidden from assistive technology via aria-hidden=\"true\" on the control itself"
      ]
    }
  ]
}
