{
  "no-inline-css": [
    {
      "id": "no-wp-add-inline-style",
      "title": "wp_add_inline_style() call",
      "category": "convention",
      "severity": "critical",
      "pattern": "wp_add_inline_style\\s*\\(",
      "description": "Monorepo CLAUDE.md §4 bans inline CSS: wp_add_inline_style() is forbidden — all styles belong in SCSS/CSS files.",
      "resolution": "Move the style into the plugin's SCSS build and enqueue the compiled stylesheet instead.",
      "excludeGlobs": [
        "**/EmailTemplates/**"
      ],
      "canary": "wp_add_inline_style('handle', $css);"
    },
    {
      "id": "no-inline-style-tag",
      "title": "<style> block",
      "category": "convention",
      "severity": "critical",
      "flags": "i",
      "pattern": "<style[\\s>]",
      "description": "Monorepo CLAUDE.md §4 bans inline CSS: raw <style> blocks are forbidden outside transactional email markup.",
      "resolution": "Move the rules into a SCSS/CSS file. Email HTML (includes/Workflow/EmailTemplates/**) is the sole documented exception — mail clients require inline/embedded CSS.",
      "excludeGlobs": [
        "**/EmailTemplates/**"
      ],
      "canary": "<style>.my-class { color: red; }</style>"
    },
    {
      "id": "no-inline-style-attribute",
      "title": "style=\"...\" attribute",
      "category": "convention",
      "severity": "critical",
      "pattern": "\\bstyle\\s*=\\s*[\"']",
      "description": "Monorepo CLAUDE.md §4 bans inline CSS: style=\"...\" attributes are forbidden outside transactional email markup.",
      "resolution": "Use a CSS class + SCSS rule, or a data attribute driven by CSS. Email HTML (includes/Workflow/EmailTemplates/**) is the sole documented exception — mail clients strip <style> and external stylesheets.",
      "excludeGlobs": [
        "**/EmailTemplates/**"
      ],
      "canary": "<div style=\"margin-top: 10px;\">"
    }
  ],
  "no-license-strings": [
    {
      "id": "no-license-identifier",
      "title": "\"license\"/\"licence\"-shaped identifier, path, or string",
      "category": "convention",
      "severity": "critical",
      "flags": "i",
      "pattern": "licen[sc]",
      "description": "Product decision: license keys are retired from this plugin (.claude/skills/backend-integration.md:118-119). No identifier, option name, path, or user-visible string may reference licensing — the plugin activates through the connect surface instead. Deliberately unanchored (no \\b) so it also catches identifiers like ipz_license_key or LICENSE_KEY where underscores suppress a word boundary.",
      "resolution": "Remove the license-shaped identifier/string. If this is legacy-option cleanup code (e.g. deleting a deprecated 'ipz_license_key' option), that call already lives at plugin root (international-press-zone.php), which is outside this rule's scanned roots (includes/, admin/) — it does not need an exception here.",
      "canary": "define('IPZ_LICENSE_KEY', get_option('license_key'));",
      "negativeCanary": [
        "// Author URI: https://press.zone"
      ]
    }
  ],
  "no-api-key-exposure": [
    {
      "id": "no-api-key-log-php",
      "title": "API key variable passed to a PHP logging/dump function",
      "category": "security",
      "severity": "critical",
      "pattern": "(?:error_log|var_dump|print_r|wp_die)\\s*\\([^)]*\\$\\w*[Aa]pi[_-]?[Kk]ey\\b",
      "description": "Product decision: the per-site API key must never be logged (.claude/skills/settings-management.md:207, api-integration.md:187).",
      "resolution": "Redact the key before logging (e.g. log only whether a key is present, or its last 4 characters) — never pass the raw key variable to error_log/var_dump/print_r/wp_die.",
      "canary": "error_log('Connect failed for key: ' . $api_key);"
    },
    {
      "id": "no-api-key-log-js",
      "title": "API key variable passed to a JS console function",
      "category": "security",
      "severity": "critical",
      "pattern": "console\\.(?:log|debug|info|warn|error)\\s*\\([^)]*\\b\\w*[Aa]pi[_-]?[Kk]ey\\b",
      "description": "Product decision: the per-site API key must never be logged (.claude/skills/settings-management.md:207, api-integration.md:187).",
      "resolution": "Remove the console call or log a redacted placeholder — never the raw key value.",
      "canary": "console.log('key', apiKey);"
    },
    {
      "id": "no-api-key-render-jsx",
      "title": "API key value interpolated into rendered JSX",
      "category": "security",
      "severity": "critical",
      "pattern": "\\{[^{}]*\\b\\w*[Aa]pi[_-]?[Kk]ey\\b[^{}]*\\}",
      "description": "Product decision: the per-site API key must never be rendered in the UI (.claude/skills/settings-management.md:56-68 — the connect flow never surfaces the raw key to the user).",
      "resolution": "Render connection status (connected/not connected) instead of the key value. If a masked/last-4 representation is genuinely needed, name the variable to make the masking obvious (e.g. apiKeyMasked) so this rule does not need weakening.",
      "excludeGlobs": [
        "**/*.md"
      ],
      "canary": "<span>{apiKey}</span>"
    }
  ],
  "no-handrolled-shared-ui": [
    {
      "id": "no-handrolled-button-classes",
      "title": "hand-rolled button/anchor with Button's classes",
      "category": "convention",
      "severity": "high",
      "pattern": "(?:presszone-international-btn|ipz-button)(?![\\w-])",
      "description": "UI-makeover convention (docs/plans/2026-08-21-ipz-ui-makeover.md): hand-rolled markup carrying the shared Button / LinkButton / IconButton component's base CSS class duplicates the component instead of calling it, and drifts out of sync with its accessibility and theming behavior. Modifier-only or longer class names (e.g. a component call passing an additive class) do not match — only the bare base class token does.",
      "resolution": "Import Button / LinkButton / IconButton from admin/src/components/index.js and let it emit its own classes. Extra classes go through the component's className/attrs options — never rebuild its markup by hand. If this file IS defining a shared component, it belongs under admin/src/components/ (which this rule does not scan).",
      "excludeGlobs": [
        "**/components/**",
        "**/*.scss",
        "**/*.md"
      ],
      "canary": "el('button', { class: 'ipz-button ipz-button--secondary' }, label)",
      "negativeCanary": [
        "className: 'ipz-button--custom-modifier'",
        "const row = 'ipz-buttons-row';"
      ]
    },
    {
      "id": "no-handrolled-badge-markup",
      "title": "hand-rolled badge with Badge's classes",
      "category": "convention",
      "severity": "high",
      "pattern": "ipz-badge(?![\\w-])",
      "description": "UI-makeover convention (docs/plans/2026-08-21-ipz-ui-makeover.md): hand-rolled markup carrying the shared Badge component's base CSS class duplicates the component instead of calling it, and drifts out of sync with its accessibility and theming behavior.",
      "resolution": "Import Badge from admin/src/components/index.js and let it emit its own classes. Extra classes go through the component's className/attrs options — never rebuild its markup by hand. If this file IS defining a shared component, it belongs under admin/src/components/ (which this rule does not scan).",
      "excludeGlobs": [
        "**/components/**",
        "**/*.scss",
        "**/*.md"
      ],
      "canary": "<span class=\"ipz-badge ipz-badge--success\">Done</span>",
      "negativeCanary": [
        "dot.className = 'ipz-badge__dot';"
      ]
    },
    {
      "id": "no-handrolled-spinner-markup",
      "title": "hand-rolled spinner with Spinner's classes",
      "category": "convention",
      "severity": "high",
      "pattern": "presszone-international-spinner(?:-with-text)?(?![\\w-])",
      "description": "UI-makeover convention (docs/plans/2026-08-21-ipz-ui-makeover.md): hand-rolled markup carrying the shared Spinner / SpinnerWithText component's base CSS class duplicates the component instead of calling it, and drifts out of sync with its accessibility and theming behavior.",
      "resolution": "Import Spinner / SpinnerWithText from admin/src/components/index.js and let it emit its own classes. Extra classes go through the component's className/attrs options — never rebuild its markup by hand. If this file IS defining a shared component, it belongs under admin/src/components/ (which this rule does not scan).",
      "excludeGlobs": [
        "**/components/**",
        "**/*.scss",
        "**/*.md"
      ],
      "canary": "el('div', { class: 'presszone-international-spinner-with-text' })"
    },
    {
      "id": "no-handrolled-notice-markup",
      "title": "hand-rolled notice with Notice's classes",
      "category": "convention",
      "severity": "high",
      "pattern": "ipz-notice(?![\\w-])",
      "description": "UI-makeover convention (docs/plans/2026-08-21-ipz-ui-makeover.md): hand-rolled markup carrying the shared Notice component's base CSS class duplicates the component instead of calling it, and drifts out of sync with its accessibility and theming behavior.",
      "resolution": "Import Notice from admin/src/components/index.js and let it emit its own classes. Extra classes go through the component's className/attrs options — never rebuild its markup by hand. If this file IS defining a shared component, it belongs under admin/src/components/ (which this rule does not scan).",
      "excludeGlobs": [
        "**/components/**",
        "**/*.scss",
        "**/*.md"
      ],
      "canary": "el('div', { class: 'ipz-notice ipz-notice--error', role: 'alert' })",
      "negativeCanary": [
        "el('p', { class: 'ipz-notice__hint' })"
      ]
    },
    {
      "id": "no-handrolled-empty-state-markup",
      "title": "hand-rolled empty state with EmptyState's classes",
      "category": "convention",
      "severity": "high",
      "pattern": "presszone-international-empty-state(?![\\w-])",
      "description": "UI-makeover convention (docs/plans/2026-08-21-ipz-ui-makeover.md): hand-rolled markup carrying the shared EmptyState component's base CSS class duplicates the component instead of calling it, and drifts out of sync with its accessibility and theming behavior.",
      "resolution": "Import EmptyState from admin/src/components/index.js and let it emit its own classes. Extra classes go through the component's className/attrs options — never rebuild its markup by hand. If this file IS defining a shared component, it belongs under admin/src/components/ (which this rule does not scan).",
      "excludeGlobs": [
        "**/components/**",
        "**/*.scss",
        "**/*.md"
      ],
      "canary": "el('div', { class: 'presszone-international-empty-state' })"
    },
    {
      "id": "no-handrolled-stat-card-markup",
      "title": "hand-rolled stat card with StatCard's classes",
      "category": "convention",
      "severity": "high",
      "pattern": "presszone-international-stat-card(?![\\w-])",
      "description": "UI-makeover convention (docs/plans/2026-08-21-ipz-ui-makeover.md): hand-rolled markup carrying the shared StatCard component's base CSS class duplicates the component instead of calling it, and drifts out of sync with its accessibility and theming behavior.",
      "resolution": "Import StatCard from admin/src/components/index.js and let it emit its own classes. Extra classes go through the component's className/attrs options — never rebuild its markup by hand. If this file IS defining a shared component, it belongs under admin/src/components/ (which this rule does not scan).",
      "excludeGlobs": [
        "**/components/**",
        "**/*.scss",
        "**/*.md"
      ],
      "canary": "el('div', { class: 'presszone-international-stat-card' })"
    },
    {
      "id": "no-handrolled-page-header-markup",
      "title": "hand-rolled page header with PageHeader's classes",
      "category": "convention",
      "severity": "high",
      "pattern": "ipz-page-header(?![\\w-])",
      "description": "UI-makeover convention (docs/plans/2026-08-21-ipz-ui-makeover.md): hand-rolled markup carrying the shared PageHeader component's base CSS class duplicates the component instead of calling it, and drifts out of sync with its accessibility and theming behavior.",
      "resolution": "Import PageHeader from admin/src/components/index.js and let it emit its own classes. Extra classes go through the component's className/attrs options — never rebuild its markup by hand. If this file IS defining a shared component, it belongs under admin/src/components/ (which this rule does not scan).",
      "excludeGlobs": [
        "**/components/**",
        "**/*.scss",
        "**/*.md"
      ],
      "canary": "el('header', { class: 'ipz-page-header' }, title)",
      "negativeCanary": [
        "className: 'ipz-page-header--analytics'",
        "el('div', { class: 'ipz-page-header__title' })"
      ]
    },
    {
      "id": "no-handrolled-progress-bar-markup",
      "title": "hand-rolled progress bar with ProgressBar's classes",
      "category": "convention",
      "severity": "high",
      "pattern": "ipz-progress-bar(?![\\w-])",
      "description": "UI-makeover convention (docs/plans/2026-08-21-ipz-ui-makeover.md): hand-rolled markup carrying the shared ProgressBar component's base CSS class duplicates the component instead of calling it, and drifts out of sync with its accessibility and theming behavior.",
      "resolution": "Import ProgressBar from admin/src/components/index.js and let it emit its own classes. Extra classes go through the component's className/attrs options — never rebuild its markup by hand. If this file IS defining a shared component, it belongs under admin/src/components/ (which this rule does not scan).",
      "excludeGlobs": [
        "**/components/**",
        "**/*.scss",
        "**/*.md"
      ],
      "canary": "el('div', { class: 'ipz-progress-bar' })"
    },
    {
      "id": "no-handrolled-pagination-markup",
      "title": "hand-rolled pagination with Pagination's classes",
      "category": "convention",
      "severity": "high",
      "pattern": "ipz-pagination(?![\\w-])",
      "description": "UI-makeover convention (docs/plans/2026-08-21-ipz-ui-makeover.md): hand-rolled markup carrying the shared Pagination component's base CSS class duplicates the component instead of calling it, and drifts out of sync with its accessibility and theming behavior.",
      "resolution": "Import Pagination from admin/src/components/index.js and let it emit its own classes. Extra classes go through the component's className/attrs options — never rebuild its markup by hand. If this file IS defining a shared component, it belongs under admin/src/components/ (which this rule does not scan).",
      "excludeGlobs": [
        "**/components/**",
        "**/*.scss",
        "**/*.md"
      ],
      "canary": "el('nav', { class: 'ipz-pagination' })",
      "negativeCanary": [
        "el('div', { class: 'ipz-pagination-container' })"
      ]
    }
  ]
}
