{
  "schemaVersion": 1,
  "kind": "velinstyle.scan-policy",
  "frameworkVersion": "1.4.0",
  "generatedAt": "2026-08-29T13:26:56.608Z",
  "sourceOfTruth": [
    "cli/scanner-rules-data.js",
    "cli/apply-fixes.js"
  ],
  "invariants": {
    "detectNotFixNotApply": true,
    "candidatesNeverImplyFixable": true,
    "candidateNeverApply": true,
    "applyFailClosed": true,
    "linksPrefixDetectOnly": "links/",
    "fixableIsRuntimeFindingField": true,
    "exactCandidateSummaryNotSafe": true
  },
  "safeApplyRuleIds": [
    "a11y/html-lang",
    "a11y/skip-link",
    "security/safe-external-link",
    "css/z-index-token",
    "pii/hardcoded-email"
  ],
  "rules": [
    {
      "id": "a11y/aria-controls-target",
      "category": "a11y",
      "severity": "error",
      "message": "aria-controls references a missing id.",
      "fixHint": "Add an element with matching id, or remove aria-controls. Dynamic/template refs are skipped.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/aria-idref-target",
      "category": "a11y",
      "severity": "error",
      "message": "aria-labelledby / aria-describedby references a missing id.",
      "fixHint": "Point IDREFs at existing ids, or remove the attribute. Dynamic/template refs are skipped.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/autocomplete-auth",
      "category": "a11y",
      "severity": "warning",
      "message": "Auth field missing autocomplete (WCAG 2.2).",
      "fixHint": "Add autocomplete=\"username\" or \"current-password\".",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/button-label",
      "category": "a11y",
      "severity": "warning",
      "message": "<button> without visible text or aria-label.",
      "fixHint": "Add text or aria-label.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/contrast-inline",
      "category": "a11y",
      "severity": "error",
      "message": "Inline or authored color pair fails WCAG AA contrast.",
      "fixHint": "Raise contrast to ≥4.5:1 (or ≥3:1 large text).",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/duplicate-id",
      "category": "a11y",
      "severity": "error",
      "message": "Duplicate id attribute in the document.",
      "fixHint": "Make every id unique.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/empty-link",
      "category": "a11y",
      "severity": "warning",
      "message": "<a> has no accessible name.",
      "fixHint": "Add visible text, aria-label, or labelled image/icon.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/focus-outline-suppressed",
      "category": "a11y",
      "severity": "warning",
      "message": ":focus removes outline without :focus-visible compensation.",
      "fixHint": "Prefer :focus-visible rings, or :focus:not(:focus-visible){outline:none} with a visible :focus-visible style.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/heading-order",
      "category": "a11y",
      "severity": "warning",
      "message": "Heading levels skip (e.g. h2 → h4).",
      "fixHint": "Use sequential heading levels.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/html-lang",
      "category": "a11y",
      "severity": "error",
      "message": "<html> missing lang attribute.",
      "fixHint": "Add lang=\"de\" or appropriate locale — --fix.",
      "detectOnly": false,
      "safeApply": true,
      "applyEligibility": "safe_apply_gate",
      "fixable": "runtime"
    },
    {
      "id": "a11y/iframe-title",
      "category": "a11y",
      "severity": "error",
      "message": "<iframe> without title.",
      "fixHint": "Add descriptive title attribute.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/img-alt",
      "category": "a11y",
      "severity": "error",
      "message": "<img> without alt.",
      "fixHint": "Add alt text or alt=\"\" for decorative images.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/img-decorative",
      "category": "a11y",
      "severity": "warning",
      "message": "Decorative <img alt=\"\"> without aria-hidden.",
      "fixHint": "Add aria-hidden=\"true\" when visible text carries the meaning.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/input-label",
      "category": "a11y",
      "severity": "warning",
      "message": "<input> without label association.",
      "fixHint": "Use <label for> or aria-label.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/interactive-aria-hidden",
      "category": "a11y",
      "severity": "error",
      "message": "Interactive element with aria-hidden=\"true\".",
      "fixHint": "Remove aria-hidden or use inert.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/invalid-describedby",
      "category": "a11y",
      "severity": "warning",
      "message": "aria-invalid without aria-describedby.",
      "fixHint": "Link to error message element id.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/landmark-main",
      "category": "a11y",
      "severity": "warning",
      "message": "No <main> landmark.",
      "fixHint": "Wrap primary content in <main>.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/nested-interactive",
      "category": "a11y",
      "severity": "error",
      "message": "Nested interactive elements (button/a/summary/role).",
      "fixHint": "Use a single control; do not nest button/a/role=button inside button/a/summary.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/positive-tabindex",
      "category": "a11y",
      "severity": "warning",
      "message": "Positive tabindex disrupts natural focus order.",
      "fixHint": "Use tabindex=\"0\" or \"-1\", or rely on native order.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/role-button-contract",
      "category": "a11y",
      "severity": "warning",
      "message": "role=button without tabindex and/or Enter/Space handling.",
      "fixHint": "Prefer native <button>, or tabindex=\"0\" + keydown for Enter/Space.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/scrollable-region-hint",
      "category": "a11y",
      "severity": "warning",
      "message": "Inline overflow scroll on <pre> without tabindex.",
      "fixHint": "Make the scroll region keyboard-reachable (e.g. tabindex=\"0\" + accessible name) when it overflows — do not tabindex every pre blindly.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/skeleton-text",
      "category": "a11y",
      "severity": "warning",
      "message": "velin-skeleton on non-empty element.",
      "fixHint": "Use skeleton only on empty placeholders.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/skip-link",
      "category": "a11y",
      "severity": "warning",
      "message": "No skip link for keyboard users.",
      "fixHint": "Add .velin-skip-link to #main — --fix when id=\"main\" exists.",
      "detectOnly": false,
      "safeApply": true,
      "applyEligibility": "safe_apply_gate",
      "fixable": "runtime"
    },
    {
      "id": "a11y/sparkline-label",
      "category": "a11y",
      "severity": "warning",
      "message": "velin-sparkline without accessible name.",
      "fixHint": "Add label attribute or wrap in <figure><figcaption>.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/tab-missing-controls",
      "category": "a11y",
      "severity": "warning",
      "message": "role=\"tab\" without aria-controls.",
      "fixHint": "Point aria-controls at the tabpanel id (APG tabs pattern). Dynamic panels still need a stable id target.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/target-size-min",
      "category": "a11y",
      "severity": "warning",
      "message": "Interactive control appears smaller than ~24px (WCAG 2.5.8 heuristic).",
      "fixHint": "Increase hit target to at least 24×24 CSS px.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "a11y/velin-icon-label",
      "category": "a11y",
      "severity": "warning",
      "message": "velin-icon in icon-only button without label.",
      "fixHint": "Add label=\"…\" on <velin-icon>.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "css/no-important",
      "category": "css",
      "severity": "info",
      "message": "!important usage.",
      "fixHint": "Prefer @layer and specificity.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "css/unknown-velin-class",
      "category": "css",
      "severity": "warning",
      "message": "Unknown velin-* utility/component class.",
      "fixHint": "Use classes from the CSS bundle or blueprint --strict.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "css/var-fallback",
      "category": "css",
      "severity": "info",
      "message": "CSS variable without fallback in var().",
      "fixHint": "Use var(--velin-*, fallback).",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "css/vendor-prefix",
      "category": "css",
      "severity": "info",
      "message": "Unnecessary vendor prefix.",
      "fixHint": "Lightning CSS handles autoprefixing.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "css/z-index-token",
      "category": "css",
      "severity": "warning",
      "message": "Raw z-index instead of --velin-z-* token.",
      "fixHint": "Use tokens from z-index.css — --fix suggests token.",
      "detectOnly": false,
      "safeApply": true,
      "applyEligibility": "safe_apply_gate",
      "fixable": "runtime"
    },
    {
      "id": "links/local-fragment-target",
      "category": "links",
      "severity": "warning",
      "message": "Local href fragment has no matching id in the target document.",
      "fixHint": "Add the id or fix the fragment. Detect only — do not invent ids.",
      "detectOnly": true,
      "safeApply": false,
      "applyEligibility": "never",
      "fixable": "runtime"
    },
    {
      "id": "links/local-href-target",
      "category": "links",
      "severity": "warning",
      "message": "Local href path does not resolve to an existing HTML file or index.",
      "fixHint": "Fix the path or publish the target. Detect only — no auto path rewrite.",
      "detectOnly": true,
      "safeApply": false,
      "applyEligibility": "never",
      "fixable": "runtime"
    },
    {
      "id": "perf/font-display-swap",
      "category": "perf",
      "severity": "info",
      "message": "@font-face without font-display: swap.",
      "fixHint": "Add font-display: swap.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "perf/img-missing-dimensions",
      "category": "perf",
      "severity": "warning",
      "message": "<img> without width/height causes CLS.",
      "fixHint": "Add width and height attributes — perf fix.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "perf/img-no-lazy",
      "category": "perf",
      "severity": "info",
      "message": "Consider loading=\"lazy\" for below-fold images.",
      "fixHint": "Add loading=\"lazy\".",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "perf/large-inline-style",
      "category": "perf",
      "severity": "info",
      "message": "Large inline style block.",
      "fixHint": "Move to external CSS.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "perf/script-no-defer",
      "category": "perf",
      "severity": "warning",
      "message": "External script without defer/async.",
      "fixHint": "Add defer or type=\"module\".",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "perf/unused-velin-import",
      "category": "perf",
      "severity": "info",
      "message": "Full component bundle loaded.",
      "fixHint": "Use @birdapi/velinstyle/runtime for tree-shaking.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "pii/hardcoded-email",
      "category": "pii",
      "severity": "warning",
      "message": "Hardcoded email in source.",
      "fixHint": "Use env/config or <velin-email>. --fix masks to placeholder.",
      "detectOnly": false,
      "safeApply": true,
      "applyEligibility": "safe_apply_gate",
      "fixable": "runtime"
    },
    {
      "id": "pii/hardcoded-secret",
      "category": "pii",
      "severity": "error",
      "message": "Possible API key or secret in source.",
      "fixHint": "Use environment variables or secret manager.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "pii/localstorage-pii",
      "category": "pii",
      "severity": "warning",
      "message": "Email-like data in Web Storage.",
      "fixHint": "Avoid storing PII in localStorage.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "pii/mailto-in-source",
      "category": "pii",
      "severity": "info",
      "message": "mailto: exposes email in HTML.",
      "fixHint": "Consider <velin-email> obfuscation.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "security/csp-meta",
      "category": "security",
      "severity": "info",
      "message": "No CSP meta tag found.",
      "fixHint": "Add Content-Security-Policy header or meta.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "security/dangerous-target",
      "category": "security",
      "severity": "warning",
      "message": "<form target=\"_blank\"> is unusual.",
      "fixHint": "Use same-tab navigation.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "security/integrity-missing",
      "category": "security",
      "severity": "info",
      "message": "External <script> without integrity (SRI).",
      "fixHint": "Add integrity + crossorigin for CDN scripts.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "security/no-data-html-uri",
      "category": "security",
      "severity": "error",
      "message": "data:text/html URI can execute script.",
      "fixHint": "Avoid data: HTML URIs in href/src.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "security/no-document-write",
      "category": "security",
      "severity": "error",
      "message": "document.write() is dangerous.",
      "fixHint": "Use DOM APIs or template elements.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "security/no-eval",
      "category": "security",
      "severity": "error",
      "message": "eval() is a critical risk.",
      "fixHint": "Never use eval in production code.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "security/no-function-constructor",
      "category": "security",
      "severity": "error",
      "message": "new Function() equals eval().",
      "fixHint": "Refactor to static functions.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "security/no-inline-handler",
      "category": "security",
      "severity": "warning",
      "message": "Inline event handler found. Use addEventListener() instead.",
      "fixHint": "Move handler to JS module.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "security/no-inline-style",
      "category": "security",
      "severity": "warning",
      "message": "Inline style attribute increases XSS surface.",
      "fixHint": "Use VelinStyle utility classes.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "security/no-javascript-url",
      "category": "security",
      "severity": "error",
      "message": "javascript: URL detected. XSS vector.",
      "fixHint": "Use button + script or href to safe URL.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "security/no-meta-refresh",
      "category": "security",
      "severity": "error",
      "message": "<meta http-equiv=\"refresh\"> can redirect without consent.",
      "fixHint": "Use server redirect or JS with user action.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "security/no-raw-innerhtml",
      "category": "security",
      "severity": "warning",
      "message": "Direct innerHTML without sanitization.",
      "fixHint": "Use escapeHTML() from @birdapi/velinstyle/sanitize.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "security/postmessage-wildcard",
      "category": "security",
      "severity": "warning",
      "message": "postMessage with targetOrigin \"*\".",
      "fixHint": "Specify exact target origin.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    },
    {
      "id": "security/safe-external-link",
      "category": "security",
      "severity": "warning",
      "message": "target=\"_blank\" without rel=\"noopener noreferrer\".",
      "fixHint": "Add rel=\"noopener noreferrer\" — auto-fix with --fix.",
      "detectOnly": false,
      "safeApply": true,
      "applyEligibility": "safe_apply_gate",
      "fixable": "runtime"
    },
    {
      "id": "wc/invalid-attribute",
      "category": "wc",
      "severity": "warning",
      "message": "Attribute is not part of this Web Component API.",
      "fixHint": "Use observed / documented attributes only.",
      "detectOnly": false,
      "safeApply": false,
      "applyEligibility": "review_only",
      "fixable": "runtime"
    }
  ],
  "counts": {
    "rules": 59,
    "safeApply": 5,
    "detectOnly": 2,
    "reviewOnly": 52
  }
}
