Skip to content

API reference

The top-level colorsense package is the canonical public API: everything below is re-exported there, so from colorsense import ... is the supported import path. Anything not listed here is internal and free to change between releases.

Entry point

colorsense.analyze async

analyze(
    url: str,
    *,
    config_path: str | Path | None = None,
    viewport: Viewport = DEFAULT_VIEWPORT,
    themes: tuple[Theme, ...] = DEFAULT_THEMES,
    politeness: PolitenessPolicy | None = None,
    max_total_seconds: float | None = None,
    browser_args: tuple[str, ...] = (),
    include_tokens: bool = False,
) -> AnalysisResult

Analyze url and return a typed AnalysisResult.

Async-native: the requested themes are rendered concurrently — sharing one lazily launched headless Chromium, each theme in its own browser context — gated by politeness; the rest of the pipeline is pure CPU work, offloaded to worker threads via asyncio.to_thread so the event loop stays responsive. Awaitable directly from an asyncio event loop (e.g. a FastAPI async def endpoint).

Parameters:

Name Type Description Default
url str

Page to analyze. Any http(s) fetch is gated by politeness. file:// URLs (used by the test suite) are an explicit opt-in via PolitenessPolicy(allow_file_urls=True); all other schemes are rejected.

required
config_path str | Path | None

Path to a palette config YAML to override the default. When None (the default) the configuration bundled with the package is used, so no file needs to exist on disk. Copy the bundled data/palette_config.yaml and pass its path here to tune.

None
viewport Viewport

Render viewport; defaults to 1280x800 at 1x scale.

DEFAULT_VIEWPORT
themes tuple[Theme, ...]

Themes to render, in priority order (the first is "primary": it is the theme kept when near-identical renders collapse). Duplicates are ignored. Defaults to light only — most sites have no dark mode and a second theme roughly doubles the work. Pass themes=(Theme.LIGHT, Theme.DARK) (or the exported LIGHT_AND_DARK) to also analyze dark mode; near-identical renders still collapse to a single reported theme.

DEFAULT_THEMES
politeness PolitenessPolicy | None

Fetch policy (robots gate, rate limit, render cache). A conservative default PolitenessPolicy is created when omitted. The consumer is responsible for authorization — see SECURITY.md.

None
max_total_seconds float | None

Optional overall deadline for the entire call — every theme render plus the CPU classification — enforced via asyncio.timeout (the SECURITY.md §2 deadline, shipped as a knob). None (default) imposes no deadline, the previous behavior. On expiry, all in-flight renders are cancelled, the shared browser is closed on the way out, and AnalysisTimeoutError is raised (a TimeoutError subclass carrying the url and budget). Must be positive when set (<= 0 raises ValueError).

None
browser_args tuple[str, ...]

Extra command-line arguments for the call's Chromium launch, appended to the library's own launch arguments and passed verbatim to Chromium (the library does not validate or allowlist the flags — mechanism, not policy). Every render of this call — all themes share one browser — launches with them. Canonical use case: browser_args=("--js-flags=--max-old-space-size=512",) caps each renderer process's V8 heap at 512 MB. Note this bounds the JS heap only, not total renderer memory; hard per-render memory/CPU caps are the container/cgroup layer's job (see SECURITY.md §2). Default (): no extra arguments, behavior unchanged. Non-string entries (or a bare string) raise TypeError before any render.

()
include_tokens bool

When True, each ThemePalette carries its declared design tokens as tokens (a tuple of DesignToken; () when no usable color tokens were found — none declared, or all declarations filtered as non-color/ignore/zero-weight). When False (the default) tokens is None. The flag gates only output assembly: token classification and reconciliation always run, so every other field is identical either way.

False

Returns:

Type Description
AnalysisResult

A typed AnalysisResult with the per-theme palettes,

AnalysisResult

the cross-theme third-party colors, and the run metadata.

Raises:

Type Description
UnsupportedSchemeError

If the URL scheme is not fetchable under the policy: only http(s) by default; file:// requires PolitenessPolicy(allow_file_urls=True); every other scheme is always rejected.

RobotsDisallowedError

If robots.txt disallows the fetch and the policy respects it.

RenderError

If the page fails to render or navigate (DNS, timeout, TLS, or navigation error).

AnalysisTimeoutError

If max_total_seconds is set and the whole analysis does not finish within it.

colorsense.LIGHT_AND_DARK module-attribute

LIGHT_AND_DARK: tuple[Theme, ...] = (LIGHT, DARK)

colorsense.DEFAULT_VIEWPORT module-attribute

DEFAULT_VIEWPORT = Viewport(
    width=1280, height=800, device_scale_factor=1.0
)

Result & contracts

colorsense.AnalysisResult pydantic-model

Bases: BaseModel

The top-level typed result returned by analyze.

Per-theme analysis (the color-keyed index, the role-keyed usage view, divergence, and opt-in tokens) lives on each ThemePalette in themes.

This aggregate is immutable: it is frozen=True and its sequence field (third_party_colors) is a tuple, so neither reassigning an attribute (result.url = ...) nor mutating a sequence in place is possible. The themes dict is protected from reassignment by frozen but its contents are not deep-frozen.

Show JSON schema:
{
  "$defs": {
    "Color": {
      "description": "An sRGB color with cached OKLCH coordinates.\n\n``hex`` is always the *opaque* normalized lowercase 7-char sRGB hex string\n(``#rrggbb``) \u2014 alpha is carried separately in ``alpha`` and never encoded in the\nhex (the invariant ``color/primitives.py`` establishes; fixed-length hexes are also\nwhat keeps lexicographic tie-breaks well-defined). ``lightness``/``chroma``/``hue``\nare the OKLCH coordinates of the (composited) color; ``alpha`` is the source alpha.",
      "properties": {
        "hex": {
          "description": "Opaque normalized lowercase 7-char sRGB hex (``#rrggbb``); alpha is never encoded here (it lives in ``alpha``).",
          "examples": [
            "#1f6feb",
            "#ffffff",
            "#0d1117"
          ],
          "pattern": "^#[0-9a-f]{6}$",
          "title": "Hex",
          "type": "string"
        },
        "lightness": {
          "description": "OKLCH lightness of the (composited) color; 0.0 is black, 1.0 is white.",
          "examples": [
            0.62
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Lightness",
          "type": "number"
        },
        "chroma": {
          "description": "OKLCH chroma (saturation) of the (composited) color; 0.0 is achromatic. Unbounded above, though sRGB colors rarely exceed ~0.4.",
          "examples": [
            0.18
          ],
          "minimum": 0.0,
          "title": "Chroma",
          "type": "number"
        },
        "hue": {
          "description": "OKLCH hue angle in degrees; achromatic colors are normalized to 0.0 (never NaN).",
          "examples": [
            256.3
          ],
          "exclusiveMaximum": 360.0,
          "minimum": 0.0,
          "title": "Hue",
          "type": "number"
        },
        "alpha": {
          "default": 1.0,
          "description": "Source alpha: 0.0 is fully transparent, 1.0 is fully opaque.",
          "examples": [
            1.0
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Alpha",
          "type": "number"
        }
      },
      "required": [
        "hex",
        "lightness",
        "chroma",
        "hue"
      ],
      "title": "Color",
      "type": "object"
    },
    "ColorUsage": {
      "description": "A measured color and where it is used \u2014 one entry of the color-keyed canonical inventory.\n\n``prominence`` is the overall ranking signal blending area-truth (primary) with routed\nvote mass (secondary), so dominant backgrounds rank high while zero-area brand accents\n(CTA/link colors) are not buried; the ``colors`` tuple is sorted by it, descending,\nwith a ``hex`` tiebreak. ``area`` is the raw screenshot area fraction the color's\ncluster covers (an auditable signal, not a probability). ``usages`` lists every\n[`UsageRole`][colorsense.UsageRole] the color appears in, most-used first\n(``weight`` descending, ``hex`` tiebreak).",
      "properties": {
        "color": {
          "$ref": "#/$defs/Color",
          "description": "The measured color this inventory entry describes."
        },
        "prominence": {
          "description": "Overall ranking signal blending area-truth (primary) with routed vote mass (secondary); the ``colors`` tuple is sorted by it, descending.",
          "examples": [
            0.83
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Prominence",
          "type": "number"
        },
        "area": {
          "description": "Raw screenshot area fraction the color's cluster covers (auditable, not a probability).",
          "examples": [
            0.31
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Area",
          "type": "number"
        },
        "usages": {
          "description": "Every usage role the color appears in, most-used first (``weight`` descending).",
          "items": {
            "$ref": "#/$defs/Usage"
          },
          "title": "Usages",
          "type": "array"
        }
      },
      "required": [
        "color",
        "prominence",
        "area"
      ],
      "title": "ColorUsage",
      "type": "object"
    },
    "ComponentType": {
      "description": "Visual component a rendered element belongs to (source of a measured color).\n\nPublic: keys the fine-grained component evidence in the result tree \u2014 both\n[`Usage`][colorsense.Usage]``.components`` on the color-keyed index and\n[`UsageEntry`][colorsense.UsageEntry]``.components`` on the role-keyed projection \u2014\nnaming which component types contributed a color to a usage role.\n\nAttributes:\n    PAGE_BG: Page background.\n    PAGE_TEXT: Page body text.\n    HEADER_BG: Header background.\n    HEADER_TEXT: Header text.\n    NAV_BG: Nav background.\n    NAV_TEXT: Nav text.\n    FOOTER_BG: Footer background.\n    FOOTER_TEXT: Footer text.\n    HERO_BG: Hero background.\n    HERO_TEXT: Hero text.\n    CARD_BG: Card background.\n    CARD_TEXT: Card text.\n    CTA_BG: Primary call-to-action button background.\n    CTA_TEXT: Primary call-to-action button text/label.\n    LINK: Hyperlink text color.\n    BUTTON_SECONDARY: Secondary button background.\n    MODAL_BG: Modal/dialog background.\n    INPUT_BG: Form input background.\n    BORDER: Border/divider color.\n    BADGE: Badge/chip/pill background.\n    THIRD_PARTY: Color from an embedded third-party widget.",
      "enum": [
        "page_bg",
        "page_text",
        "header_bg",
        "header_text",
        "nav_bg",
        "nav_text",
        "footer_bg",
        "footer_text",
        "hero_bg",
        "hero_text",
        "card_bg",
        "card_text",
        "cta_bg",
        "cta_text",
        "link",
        "button_secondary",
        "modal_bg",
        "input_bg",
        "border",
        "badge",
        "third_party"
      ],
      "title": "ComponentType",
      "type": "string"
    },
    "DesignToken": {
      "description": "A declared design token (CSS custom property) in the public result.\n\n``name`` is the declared property name (e.g. ``--fgColor-default``); ``color`` is its\nvalue resolved in the rendered theme; ``semantic_role`` is the inferred semantic role.",
      "properties": {
        "name": {
          "description": "Declared property name, including the leading ``--``.",
          "examples": [
            "--fgColor-default"
          ],
          "title": "Name",
          "type": "string"
        },
        "color": {
          "$ref": "#/$defs/Color",
          "description": "The token's value resolved in the rendered theme."
        },
        "semantic_role": {
          "$ref": "#/$defs/TokenSemanticRole",
          "description": "The inferred semantic role for the token."
        }
      },
      "required": [
        "name",
        "color",
        "semantic_role"
      ],
      "title": "DesignToken",
      "type": "object"
    },
    "DivergenceItem": {
      "description": "A declared-but-unused or used-but-undeclared palette discrepancy.\n\nKeyed by [`UsageRole`][colorsense.UsageRole] \u2014 the role-keyed usage view is where\ndeclared token intent is reconciled against measured usage.",
      "properties": {
        "role": {
          "$ref": "#/$defs/UsageRole",
          "description": "The usage role the discrepancy was found under."
        },
        "color": {
          "$ref": "#/$defs/Color",
          "description": "The declared-but-unused or used-but-undeclared color."
        },
        "note": {
          "description": "Human-readable explanation of the discrepancy.",
          "examples": [
            "declared as brand_primary but not measured in use"
          ],
          "title": "Note",
          "type": "string"
        }
      },
      "required": [
        "role",
        "color",
        "note"
      ],
      "title": "DivergenceItem",
      "type": "object"
    },
    "PropertyFamily": {
      "description": "Which family of CSS properties paints the color \u2014 the rollup axis over roles.\n\nCoarser than [`UsageRole`][colorsense.UsageRole]: every role belongs to exactly one\nfamily (the mapping is [`UsageRole.property_family`][colorsense.UsageRole.property_family]).\n\nAttributes:\n    BACKGROUND: ``background-color`` / ``background-image`` fills.\n    TEXT: The ``color`` property (typography).\n    BORDER: The ``border-color`` property.",
      "enum": [
        "background",
        "text",
        "border"
      ],
      "title": "PropertyFamily",
      "type": "string"
    },
    "RunMetadata": {
      "description": "Provenance for a single ``analyze`` run.\n\nRecords which themes were requested versus actually analyzed (later themes whose\nrender is perceptually identical to the primary are collapsed away) and the fetch\npolicy in effect. A run reduced to a single theme iff ``len(themes_analyzed) == 1``.",
      "properties": {
        "themes_requested": {
          "description": "Themes the caller requested.",
          "examples": [
            [
              "light",
              "dark"
            ]
          ],
          "items": {
            "$ref": "#/$defs/Theme"
          },
          "title": "Themes Requested",
          "type": "array"
        },
        "themes_analyzed": {
          "description": "Themes actually analyzed; a run reduced to a single theme iff this has length 1 (perceptually-identical later themes are collapsed away).",
          "examples": [
            [
              "light"
            ]
          ],
          "items": {
            "$ref": "#/$defs/Theme"
          },
          "title": "Themes Analyzed",
          "type": "array"
        },
        "user_agent": {
          "default": "",
          "description": "User-Agent string used for fetches.",
          "title": "User Agent",
          "type": "string"
        },
        "respect_robots": {
          "default": true,
          "description": "Whether robots.txt was honored for this run.",
          "title": "Respect Robots",
          "type": "boolean"
        }
      },
      "title": "RunMetadata",
      "type": "object"
    },
    "Theme": {
      "description": "Color scheme a site is rendered under.\n\nAttributes:\n    LIGHT: Light color scheme (the default).\n    DARK: Dark color scheme (``prefers-color-scheme: dark``).",
      "enum": [
        "light",
        "dark"
      ],
      "title": "Theme",
      "type": "string"
    },
    "ThemePalette": {
      "description": "Everything derived for a single rendered theme.\n\n* ``colors`` \u2014 the **canonical, color-keyed index**: every measured color\n  ([`ColorUsage`][colorsense.ColorUsage]) with where it is used and an overall\n  ``prominence`` ranking. Answers \"how is each color used?\". Third-party-dominated\n  colors are excluded (they live on ``AnalysisResult.third_party_colors``).\n* ``usage`` \u2014 the **role-keyed projection** ([`UsagePalette`][colorsense.UsagePalette]):\n  the reconciled posterior over usage roles (measured usage pooled with declared token\n  intent). Answers \"which colors paint each role?\".\n* ``divergence`` \u2014 declared-vs-measured discrepancies, keyed by usage role.\n* ``tokens`` \u2014 declared design tokens, opt-in: ``None`` means tokens were **not\n  requested** (``include_tokens=False``, the default); ``()`` means tokens were\n  requested but no usable color tokens were found \u2014 the page declares no custom\n  properties at all, or every declaration was filtered as non-meaningful (no\n  resolvable color, ``ignore`` semantic role, or zero classification weight).",
      "properties": {
        "theme": {
          "$ref": "#/$defs/Theme",
          "description": "The theme this palette was derived for."
        },
        "colors": {
          "description": "Canonical color-keyed index: every measured color with where it is used, ranked by ``prominence`` (third-party-dominated colors excluded).",
          "items": {
            "$ref": "#/$defs/ColorUsage"
          },
          "title": "Colors",
          "type": "array"
        },
        "usage": {
          "$ref": "#/$defs/UsagePalette",
          "description": "Role-keyed projection: which colors paint each usage role (measured pooled with token intent)."
        },
        "divergence": {
          "description": "Declared-vs-measured discrepancies, keyed by usage role.",
          "items": {
            "$ref": "#/$defs/DivergenceItem"
          },
          "title": "Divergence",
          "type": "array"
        },
        "tokens": {
          "anyOf": [
            {
              "items": {
                "$ref": "#/$defs/DesignToken"
              },
              "type": "array"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "description": "Declared design tokens, opt-in: ``None`` means tokens were not requested; ``()`` means requested but none were usable.",
          "title": "Tokens"
        }
      },
      "required": [
        "theme",
        "usage"
      ],
      "title": "ThemePalette",
      "type": "object"
    },
    "TokenSemanticRole": {
      "description": "Semantic role inferred for a declared design token (CSS custom property).\n\nAttributes:\n    BRAND_PRIMARY: Primary brand color.\n    BRAND_SECONDARY: Secondary brand color.\n    BRAND_ACCENT: Accent brand color.\n    INTERACTIVE: Interactive-element color (links, controls).\n    SURFACE_BASE: Base page/surface background.\n    SURFACE_RAISED: Raised surface background (cards, modals).\n    TEXT_BODY: Body text color.\n    NEUTRAL: Neutral/gray with no specific role.\n    BORDER: Border/divider color.\n    TEXT_ON: Foreground meant to sit on a colored fill (an \"on\" color).\n    STATUS: Status color (success/warning/error/info).\n    IGNORE: Not a meaningful palette token (filtered out).",
      "enum": [
        "brand_primary",
        "brand_secondary",
        "brand_accent",
        "interactive",
        "surface_base",
        "surface_raised",
        "text_body",
        "neutral",
        "border",
        "text_on",
        "status",
        "ignore"
      ],
      "title": "TokenSemanticRole",
      "type": "string"
    },
    "Usage": {
      "description": "One usage slot of a color in the color-keyed index ([`ColorUsage`][colorsense.ColorUsage]).\n\nA color may be used in several roles (e.g. the same gray as ``text`` *and* ``border``);\neach gets its own ``Usage``. ``role`` is the [`UsageRole`][colorsense.UsageRole] this\nslot describes and ``property_family`` is its [`PropertyFamily`][colorsense.PropertyFamily]\nrollup (denormalized \u2014 always ``role.property_family`` \u2014 so consumers can group by family\nwithout recomputing). ``weight`` is this color's share of *its own* usages \u2014 the role's\nrouted mass over the color's total routed mass, so a color's ``weight`` values sum to\n~1.0. ``components`` is normalized evidence within this slot: which component types\ncontributed the color to this role (e.g. ``{card_bg: 0.7, modal_bg: 0.3}``), summing to\n~1.0 when non-empty.",
      "properties": {
        "role": {
          "$ref": "#/$defs/UsageRole",
          "description": "The usage role this slot describes."
        },
        "property_family": {
          "$ref": "#/$defs/PropertyFamily",
          "description": "Rollup family for ``role`` (denormalized \u2014 always ``role.property_family``)."
        },
        "weight": {
          "description": "This role's share of the color's total routed mass; a color's ``weight`` values sum to ~1.0.",
          "examples": [
            0.7
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Weight",
          "type": "number"
        },
        "components": {
          "additionalProperties": {
            "type": "number"
          },
          "description": "Normalized evidence: which component types contributed the color to this role (each value in ``[0, 1]``, summing to ~1.0 when non-empty).",
          "examples": [
            {
              "card_bg": 0.7,
              "modal_bg": 0.3
            }
          ],
          "propertyNames": {
            "$ref": "#/$defs/ComponentType"
          },
          "title": "Components",
          "type": "object"
        }
      },
      "required": [
        "role",
        "property_family",
        "weight"
      ],
      "title": "Usage",
      "type": "object"
    },
    "UsageEntry": {
      "description": "One color's standing within a usage role (role-keyed projection).\n\n``probability`` is the posterior prominence of this color *within its role*\n(entries of one role sum to ~1.0). ``area`` is the raw screenshot area fraction\nthe color's cluster covers \u2014 an auditable signal, not a probability. ``components``\nis normalized evidence: which component types contributed this color to this\nrole (e.g. ``{card_bg: 0.7, modal_bg: 0.3}``), summing to ~1.0 when non-empty.",
      "properties": {
        "color": {
          "$ref": "#/$defs/Color",
          "description": "The color this role entry describes."
        },
        "probability": {
          "description": "Posterior prominence of this color within its role; entries of one role sum to ~1.0.",
          "examples": [
            0.55
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Probability",
          "type": "number"
        },
        "area": {
          "description": "Raw screenshot area fraction the color's cluster covers (auditable, not a probability).",
          "examples": [
            0.31
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Area",
          "type": "number"
        },
        "components": {
          "additionalProperties": {
            "type": "number"
          },
          "description": "Normalized evidence: which component types contributed this color to this role (each value in ``[0, 1]``, summing to ~1.0 when non-empty).",
          "examples": [
            {
              "card_bg": 0.7,
              "modal_bg": 0.3
            }
          ],
          "propertyNames": {
            "$ref": "#/$defs/ComponentType"
          },
          "title": "Components",
          "type": "object"
        }
      },
      "required": [
        "color",
        "probability",
        "area"
      ],
      "title": "UsageEntry",
      "type": "object"
    },
    "UsagePalette": {
      "description": "The role-keyed palette projection: which colors paint each usage role.\n\n``mapping`` is guaranteed to contain every [`UsageRole`][colorsense.UsageRole]; a\nrole with no detected entries maps to an empty tuple. This invariant is enforced by an\nafter-validator that backfills any missing roles, so even ``UsagePalette()`` and\nthe empty-input path expose all eight keys.",
      "properties": {
        "mapping": {
          "additionalProperties": {
            "items": {
              "$ref": "#/$defs/UsageEntry"
            },
            "type": "array"
          },
          "description": "Colors painting each [`UsageRole`][colorsense.UsageRole]; guaranteed to contain every role (an unused role maps to ``()``).",
          "propertyNames": {
            "$ref": "#/$defs/UsageRole"
          },
          "title": "Mapping",
          "type": "object"
        }
      },
      "title": "UsagePalette",
      "type": "object"
    },
    "UsageRole": {
      "description": "Developer-facing usage role \u2014 how a rendered color is used on the page.\n\nThe role taxonomy keys the role-keyed palette projection\n([`UsagePalette`][colorsense.UsagePalette]) and labels each\n[`Usage`][colorsense.Usage] slot of the color-keyed index. It splits the two axes the\nold 4-value usage taxonomy conflated \u2014 *which CSS property paints the color* (the\n[`PropertyFamily`][colorsense.PropertyFamily] rollup) versus *what kind of element it\nis* \u2014 so that, e.g., link text and CTA button backgrounds no longer share one slot.\n\nAttributes:\n    PAGE: The base canvas (the page background).\n    SURFACE: Raised content backgrounds: cards, modals, hero, inputs.\n    BANNER: Chrome-bar backgrounds: header, nav, footer.\n    CTA: The primary action background (CTA buttons).\n    ACTION: Secondary action backgrounds: secondary buttons, badges.\n    TEXT: Body/heading typography at every layer.\n    LINK: Link color (typography of anchors).\n    BORDER: Borders and dividers.",
      "enum": [
        "page",
        "surface",
        "banner",
        "cta",
        "action",
        "text",
        "link",
        "border"
      ],
      "title": "UsageRole",
      "type": "string"
    },
    "Viewport": {
      "description": "Rendering viewport.",
      "properties": {
        "width": {
          "description": "Viewport width in CSS pixels.",
          "examples": [
            1280
          ],
          "minimum": 1,
          "title": "Width",
          "type": "integer"
        },
        "height": {
          "description": "Viewport height in CSS pixels.",
          "examples": [
            800
          ],
          "minimum": 1,
          "title": "Height",
          "type": "integer"
        },
        "device_scale_factor": {
          "description": "Device pixel ratio (DIP\u2192raster multiplier); 1.0 is non-retina, 2.0 is retina.",
          "examples": [
            1.0
          ],
          "exclusiveMinimum": 0.0,
          "title": "Device Scale Factor",
          "type": "number"
        }
      },
      "required": [
        "width",
        "height",
        "device_scale_factor"
      ],
      "title": "Viewport",
      "type": "object"
    }
  },
  "description": "The top-level typed result returned by ``analyze``.\n\nPer-theme analysis (the color-keyed index, the role-keyed usage view, divergence, and\nopt-in tokens) lives on each [`ThemePalette`][colorsense.ThemePalette] in ``themes``.\n\nThis aggregate is **immutable**: it is ``frozen=True`` and its sequence field\n(``third_party_colors``) is a tuple, so neither reassigning an attribute\n(``result.url = ...``) nor mutating a sequence in place is possible. The ``themes``\ndict is protected from reassignment by ``frozen`` but its contents are not\ndeep-frozen.",
  "properties": {
    "url": {
      "description": "The analyzed page URL.",
      "examples": [
        "https://example.com/"
      ],
      "title": "Url",
      "type": "string"
    },
    "viewport": {
      "$ref": "#/$defs/Viewport",
      "description": "Viewport the page was rendered at."
    },
    "themes": {
      "additionalProperties": {
        "$ref": "#/$defs/ThemePalette"
      },
      "description": "Per-theme analysis, keyed by [`Theme`][colorsense.Theme].",
      "propertyNames": {
        "$ref": "#/$defs/Theme"
      },
      "title": "Themes",
      "type": "object"
    },
    "third_party_colors": {
      "description": "Colors attributed to third-party/vendor widgets, held out of the per-theme index.",
      "items": {
        "$ref": "#/$defs/Color"
      },
      "title": "Third Party Colors",
      "type": "array"
    },
    "metadata": {
      "$ref": "#/$defs/RunMetadata",
      "description": "Provenance for the run (themes requested/analyzed, fetch policy)."
    }
  },
  "required": [
    "url",
    "viewport"
  ],
  "title": "AnalysisResult",
  "type": "object"
}

Config:

  • frozen: True

Fields:

url pydantic-field

url: str

The analyzed page URL.

viewport pydantic-field

viewport: Viewport

Viewport the page was rendered at.

themes pydantic-field

themes: dict[Theme, ThemePalette]

Per-theme analysis, keyed by Theme.

third_party_colors pydantic-field

third_party_colors: tuple[Color, ...]

Colors attributed to third-party/vendor widgets, held out of the per-theme index.

metadata pydantic-field

metadata: RunMetadata

Provenance for the run (themes requested/analyzed, fetch policy).

colorsense.RunMetadata pydantic-model

Bases: BaseModel

Provenance for a single analyze run.

Records which themes were requested versus actually analyzed (later themes whose render is perceptually identical to the primary are collapsed away) and the fetch policy in effect. A run reduced to a single theme iff len(themes_analyzed) == 1.

Show JSON schema:
{
  "$defs": {
    "Theme": {
      "description": "Color scheme a site is rendered under.\n\nAttributes:\n    LIGHT: Light color scheme (the default).\n    DARK: Dark color scheme (``prefers-color-scheme: dark``).",
      "enum": [
        "light",
        "dark"
      ],
      "title": "Theme",
      "type": "string"
    }
  },
  "description": "Provenance for a single ``analyze`` run.\n\nRecords which themes were requested versus actually analyzed (later themes whose\nrender is perceptually identical to the primary are collapsed away) and the fetch\npolicy in effect. A run reduced to a single theme iff ``len(themes_analyzed) == 1``.",
  "properties": {
    "themes_requested": {
      "description": "Themes the caller requested.",
      "examples": [
        [
          "light",
          "dark"
        ]
      ],
      "items": {
        "$ref": "#/$defs/Theme"
      },
      "title": "Themes Requested",
      "type": "array"
    },
    "themes_analyzed": {
      "description": "Themes actually analyzed; a run reduced to a single theme iff this has length 1 (perceptually-identical later themes are collapsed away).",
      "examples": [
        [
          "light"
        ]
      ],
      "items": {
        "$ref": "#/$defs/Theme"
      },
      "title": "Themes Analyzed",
      "type": "array"
    },
    "user_agent": {
      "default": "",
      "description": "User-Agent string used for fetches.",
      "title": "User Agent",
      "type": "string"
    },
    "respect_robots": {
      "default": true,
      "description": "Whether robots.txt was honored for this run.",
      "title": "Respect Robots",
      "type": "boolean"
    }
  },
  "title": "RunMetadata",
  "type": "object"
}

Config:

  • frozen: True

Fields:

themes_requested pydantic-field

themes_requested: tuple[Theme, ...]

Themes the caller requested.

themes_analyzed pydantic-field

themes_analyzed: tuple[Theme, ...]

Themes actually analyzed; a run reduced to a single theme iff this has length 1 (perceptually-identical later themes are collapsed away).

user_agent pydantic-field

user_agent: str = ''

User-Agent string used for fetches.

respect_robots pydantic-field

respect_robots: bool = True

Whether robots.txt was honored for this run.

colorsense.ThemePalette pydantic-model

Bases: BaseModel

Everything derived for a single rendered theme.

  • colors — the canonical, color-keyed index: every measured color (ColorUsage) with where it is used and an overall prominence ranking. Answers "how is each color used?". Third-party-dominated colors are excluded (they live on AnalysisResult.third_party_colors).
  • usage — the role-keyed projection (UsagePalette): the reconciled posterior over usage roles (measured usage pooled with declared token intent). Answers "which colors paint each role?".
  • divergence — declared-vs-measured discrepancies, keyed by usage role.
  • tokens — declared design tokens, opt-in: None means tokens were not requested (include_tokens=False, the default); () means tokens were requested but no usable color tokens were found — the page declares no custom properties at all, or every declaration was filtered as non-meaningful (no resolvable color, ignore semantic role, or zero classification weight).
Show JSON schema:
{
  "$defs": {
    "Color": {
      "description": "An sRGB color with cached OKLCH coordinates.\n\n``hex`` is always the *opaque* normalized lowercase 7-char sRGB hex string\n(``#rrggbb``) \u2014 alpha is carried separately in ``alpha`` and never encoded in the\nhex (the invariant ``color/primitives.py`` establishes; fixed-length hexes are also\nwhat keeps lexicographic tie-breaks well-defined). ``lightness``/``chroma``/``hue``\nare the OKLCH coordinates of the (composited) color; ``alpha`` is the source alpha.",
      "properties": {
        "hex": {
          "description": "Opaque normalized lowercase 7-char sRGB hex (``#rrggbb``); alpha is never encoded here (it lives in ``alpha``).",
          "examples": [
            "#1f6feb",
            "#ffffff",
            "#0d1117"
          ],
          "pattern": "^#[0-9a-f]{6}$",
          "title": "Hex",
          "type": "string"
        },
        "lightness": {
          "description": "OKLCH lightness of the (composited) color; 0.0 is black, 1.0 is white.",
          "examples": [
            0.62
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Lightness",
          "type": "number"
        },
        "chroma": {
          "description": "OKLCH chroma (saturation) of the (composited) color; 0.0 is achromatic. Unbounded above, though sRGB colors rarely exceed ~0.4.",
          "examples": [
            0.18
          ],
          "minimum": 0.0,
          "title": "Chroma",
          "type": "number"
        },
        "hue": {
          "description": "OKLCH hue angle in degrees; achromatic colors are normalized to 0.0 (never NaN).",
          "examples": [
            256.3
          ],
          "exclusiveMaximum": 360.0,
          "minimum": 0.0,
          "title": "Hue",
          "type": "number"
        },
        "alpha": {
          "default": 1.0,
          "description": "Source alpha: 0.0 is fully transparent, 1.0 is fully opaque.",
          "examples": [
            1.0
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Alpha",
          "type": "number"
        }
      },
      "required": [
        "hex",
        "lightness",
        "chroma",
        "hue"
      ],
      "title": "Color",
      "type": "object"
    },
    "ColorUsage": {
      "description": "A measured color and where it is used \u2014 one entry of the color-keyed canonical inventory.\n\n``prominence`` is the overall ranking signal blending area-truth (primary) with routed\nvote mass (secondary), so dominant backgrounds rank high while zero-area brand accents\n(CTA/link colors) are not buried; the ``colors`` tuple is sorted by it, descending,\nwith a ``hex`` tiebreak. ``area`` is the raw screenshot area fraction the color's\ncluster covers (an auditable signal, not a probability). ``usages`` lists every\n[`UsageRole`][colorsense.UsageRole] the color appears in, most-used first\n(``weight`` descending, ``hex`` tiebreak).",
      "properties": {
        "color": {
          "$ref": "#/$defs/Color",
          "description": "The measured color this inventory entry describes."
        },
        "prominence": {
          "description": "Overall ranking signal blending area-truth (primary) with routed vote mass (secondary); the ``colors`` tuple is sorted by it, descending.",
          "examples": [
            0.83
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Prominence",
          "type": "number"
        },
        "area": {
          "description": "Raw screenshot area fraction the color's cluster covers (auditable, not a probability).",
          "examples": [
            0.31
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Area",
          "type": "number"
        },
        "usages": {
          "description": "Every usage role the color appears in, most-used first (``weight`` descending).",
          "items": {
            "$ref": "#/$defs/Usage"
          },
          "title": "Usages",
          "type": "array"
        }
      },
      "required": [
        "color",
        "prominence",
        "area"
      ],
      "title": "ColorUsage",
      "type": "object"
    },
    "ComponentType": {
      "description": "Visual component a rendered element belongs to (source of a measured color).\n\nPublic: keys the fine-grained component evidence in the result tree \u2014 both\n[`Usage`][colorsense.Usage]``.components`` on the color-keyed index and\n[`UsageEntry`][colorsense.UsageEntry]``.components`` on the role-keyed projection \u2014\nnaming which component types contributed a color to a usage role.\n\nAttributes:\n    PAGE_BG: Page background.\n    PAGE_TEXT: Page body text.\n    HEADER_BG: Header background.\n    HEADER_TEXT: Header text.\n    NAV_BG: Nav background.\n    NAV_TEXT: Nav text.\n    FOOTER_BG: Footer background.\n    FOOTER_TEXT: Footer text.\n    HERO_BG: Hero background.\n    HERO_TEXT: Hero text.\n    CARD_BG: Card background.\n    CARD_TEXT: Card text.\n    CTA_BG: Primary call-to-action button background.\n    CTA_TEXT: Primary call-to-action button text/label.\n    LINK: Hyperlink text color.\n    BUTTON_SECONDARY: Secondary button background.\n    MODAL_BG: Modal/dialog background.\n    INPUT_BG: Form input background.\n    BORDER: Border/divider color.\n    BADGE: Badge/chip/pill background.\n    THIRD_PARTY: Color from an embedded third-party widget.",
      "enum": [
        "page_bg",
        "page_text",
        "header_bg",
        "header_text",
        "nav_bg",
        "nav_text",
        "footer_bg",
        "footer_text",
        "hero_bg",
        "hero_text",
        "card_bg",
        "card_text",
        "cta_bg",
        "cta_text",
        "link",
        "button_secondary",
        "modal_bg",
        "input_bg",
        "border",
        "badge",
        "third_party"
      ],
      "title": "ComponentType",
      "type": "string"
    },
    "DesignToken": {
      "description": "A declared design token (CSS custom property) in the public result.\n\n``name`` is the declared property name (e.g. ``--fgColor-default``); ``color`` is its\nvalue resolved in the rendered theme; ``semantic_role`` is the inferred semantic role.",
      "properties": {
        "name": {
          "description": "Declared property name, including the leading ``--``.",
          "examples": [
            "--fgColor-default"
          ],
          "title": "Name",
          "type": "string"
        },
        "color": {
          "$ref": "#/$defs/Color",
          "description": "The token's value resolved in the rendered theme."
        },
        "semantic_role": {
          "$ref": "#/$defs/TokenSemanticRole",
          "description": "The inferred semantic role for the token."
        }
      },
      "required": [
        "name",
        "color",
        "semantic_role"
      ],
      "title": "DesignToken",
      "type": "object"
    },
    "DivergenceItem": {
      "description": "A declared-but-unused or used-but-undeclared palette discrepancy.\n\nKeyed by [`UsageRole`][colorsense.UsageRole] \u2014 the role-keyed usage view is where\ndeclared token intent is reconciled against measured usage.",
      "properties": {
        "role": {
          "$ref": "#/$defs/UsageRole",
          "description": "The usage role the discrepancy was found under."
        },
        "color": {
          "$ref": "#/$defs/Color",
          "description": "The declared-but-unused or used-but-undeclared color."
        },
        "note": {
          "description": "Human-readable explanation of the discrepancy.",
          "examples": [
            "declared as brand_primary but not measured in use"
          ],
          "title": "Note",
          "type": "string"
        }
      },
      "required": [
        "role",
        "color",
        "note"
      ],
      "title": "DivergenceItem",
      "type": "object"
    },
    "PropertyFamily": {
      "description": "Which family of CSS properties paints the color \u2014 the rollup axis over roles.\n\nCoarser than [`UsageRole`][colorsense.UsageRole]: every role belongs to exactly one\nfamily (the mapping is [`UsageRole.property_family`][colorsense.UsageRole.property_family]).\n\nAttributes:\n    BACKGROUND: ``background-color`` / ``background-image`` fills.\n    TEXT: The ``color`` property (typography).\n    BORDER: The ``border-color`` property.",
      "enum": [
        "background",
        "text",
        "border"
      ],
      "title": "PropertyFamily",
      "type": "string"
    },
    "Theme": {
      "description": "Color scheme a site is rendered under.\n\nAttributes:\n    LIGHT: Light color scheme (the default).\n    DARK: Dark color scheme (``prefers-color-scheme: dark``).",
      "enum": [
        "light",
        "dark"
      ],
      "title": "Theme",
      "type": "string"
    },
    "TokenSemanticRole": {
      "description": "Semantic role inferred for a declared design token (CSS custom property).\n\nAttributes:\n    BRAND_PRIMARY: Primary brand color.\n    BRAND_SECONDARY: Secondary brand color.\n    BRAND_ACCENT: Accent brand color.\n    INTERACTIVE: Interactive-element color (links, controls).\n    SURFACE_BASE: Base page/surface background.\n    SURFACE_RAISED: Raised surface background (cards, modals).\n    TEXT_BODY: Body text color.\n    NEUTRAL: Neutral/gray with no specific role.\n    BORDER: Border/divider color.\n    TEXT_ON: Foreground meant to sit on a colored fill (an \"on\" color).\n    STATUS: Status color (success/warning/error/info).\n    IGNORE: Not a meaningful palette token (filtered out).",
      "enum": [
        "brand_primary",
        "brand_secondary",
        "brand_accent",
        "interactive",
        "surface_base",
        "surface_raised",
        "text_body",
        "neutral",
        "border",
        "text_on",
        "status",
        "ignore"
      ],
      "title": "TokenSemanticRole",
      "type": "string"
    },
    "Usage": {
      "description": "One usage slot of a color in the color-keyed index ([`ColorUsage`][colorsense.ColorUsage]).\n\nA color may be used in several roles (e.g. the same gray as ``text`` *and* ``border``);\neach gets its own ``Usage``. ``role`` is the [`UsageRole`][colorsense.UsageRole] this\nslot describes and ``property_family`` is its [`PropertyFamily`][colorsense.PropertyFamily]\nrollup (denormalized \u2014 always ``role.property_family`` \u2014 so consumers can group by family\nwithout recomputing). ``weight`` is this color's share of *its own* usages \u2014 the role's\nrouted mass over the color's total routed mass, so a color's ``weight`` values sum to\n~1.0. ``components`` is normalized evidence within this slot: which component types\ncontributed the color to this role (e.g. ``{card_bg: 0.7, modal_bg: 0.3}``), summing to\n~1.0 when non-empty.",
      "properties": {
        "role": {
          "$ref": "#/$defs/UsageRole",
          "description": "The usage role this slot describes."
        },
        "property_family": {
          "$ref": "#/$defs/PropertyFamily",
          "description": "Rollup family for ``role`` (denormalized \u2014 always ``role.property_family``)."
        },
        "weight": {
          "description": "This role's share of the color's total routed mass; a color's ``weight`` values sum to ~1.0.",
          "examples": [
            0.7
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Weight",
          "type": "number"
        },
        "components": {
          "additionalProperties": {
            "type": "number"
          },
          "description": "Normalized evidence: which component types contributed the color to this role (each value in ``[0, 1]``, summing to ~1.0 when non-empty).",
          "examples": [
            {
              "card_bg": 0.7,
              "modal_bg": 0.3
            }
          ],
          "propertyNames": {
            "$ref": "#/$defs/ComponentType"
          },
          "title": "Components",
          "type": "object"
        }
      },
      "required": [
        "role",
        "property_family",
        "weight"
      ],
      "title": "Usage",
      "type": "object"
    },
    "UsageEntry": {
      "description": "One color's standing within a usage role (role-keyed projection).\n\n``probability`` is the posterior prominence of this color *within its role*\n(entries of one role sum to ~1.0). ``area`` is the raw screenshot area fraction\nthe color's cluster covers \u2014 an auditable signal, not a probability. ``components``\nis normalized evidence: which component types contributed this color to this\nrole (e.g. ``{card_bg: 0.7, modal_bg: 0.3}``), summing to ~1.0 when non-empty.",
      "properties": {
        "color": {
          "$ref": "#/$defs/Color",
          "description": "The color this role entry describes."
        },
        "probability": {
          "description": "Posterior prominence of this color within its role; entries of one role sum to ~1.0.",
          "examples": [
            0.55
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Probability",
          "type": "number"
        },
        "area": {
          "description": "Raw screenshot area fraction the color's cluster covers (auditable, not a probability).",
          "examples": [
            0.31
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Area",
          "type": "number"
        },
        "components": {
          "additionalProperties": {
            "type": "number"
          },
          "description": "Normalized evidence: which component types contributed this color to this role (each value in ``[0, 1]``, summing to ~1.0 when non-empty).",
          "examples": [
            {
              "card_bg": 0.7,
              "modal_bg": 0.3
            }
          ],
          "propertyNames": {
            "$ref": "#/$defs/ComponentType"
          },
          "title": "Components",
          "type": "object"
        }
      },
      "required": [
        "color",
        "probability",
        "area"
      ],
      "title": "UsageEntry",
      "type": "object"
    },
    "UsagePalette": {
      "description": "The role-keyed palette projection: which colors paint each usage role.\n\n``mapping`` is guaranteed to contain every [`UsageRole`][colorsense.UsageRole]; a\nrole with no detected entries maps to an empty tuple. This invariant is enforced by an\nafter-validator that backfills any missing roles, so even ``UsagePalette()`` and\nthe empty-input path expose all eight keys.",
      "properties": {
        "mapping": {
          "additionalProperties": {
            "items": {
              "$ref": "#/$defs/UsageEntry"
            },
            "type": "array"
          },
          "description": "Colors painting each [`UsageRole`][colorsense.UsageRole]; guaranteed to contain every role (an unused role maps to ``()``).",
          "propertyNames": {
            "$ref": "#/$defs/UsageRole"
          },
          "title": "Mapping",
          "type": "object"
        }
      },
      "title": "UsagePalette",
      "type": "object"
    },
    "UsageRole": {
      "description": "Developer-facing usage role \u2014 how a rendered color is used on the page.\n\nThe role taxonomy keys the role-keyed palette projection\n([`UsagePalette`][colorsense.UsagePalette]) and labels each\n[`Usage`][colorsense.Usage] slot of the color-keyed index. It splits the two axes the\nold 4-value usage taxonomy conflated \u2014 *which CSS property paints the color* (the\n[`PropertyFamily`][colorsense.PropertyFamily] rollup) versus *what kind of element it\nis* \u2014 so that, e.g., link text and CTA button backgrounds no longer share one slot.\n\nAttributes:\n    PAGE: The base canvas (the page background).\n    SURFACE: Raised content backgrounds: cards, modals, hero, inputs.\n    BANNER: Chrome-bar backgrounds: header, nav, footer.\n    CTA: The primary action background (CTA buttons).\n    ACTION: Secondary action backgrounds: secondary buttons, badges.\n    TEXT: Body/heading typography at every layer.\n    LINK: Link color (typography of anchors).\n    BORDER: Borders and dividers.",
      "enum": [
        "page",
        "surface",
        "banner",
        "cta",
        "action",
        "text",
        "link",
        "border"
      ],
      "title": "UsageRole",
      "type": "string"
    }
  },
  "description": "Everything derived for a single rendered theme.\n\n* ``colors`` \u2014 the **canonical, color-keyed index**: every measured color\n  ([`ColorUsage`][colorsense.ColorUsage]) with where it is used and an overall\n  ``prominence`` ranking. Answers \"how is each color used?\". Third-party-dominated\n  colors are excluded (they live on ``AnalysisResult.third_party_colors``).\n* ``usage`` \u2014 the **role-keyed projection** ([`UsagePalette`][colorsense.UsagePalette]):\n  the reconciled posterior over usage roles (measured usage pooled with declared token\n  intent). Answers \"which colors paint each role?\".\n* ``divergence`` \u2014 declared-vs-measured discrepancies, keyed by usage role.\n* ``tokens`` \u2014 declared design tokens, opt-in: ``None`` means tokens were **not\n  requested** (``include_tokens=False``, the default); ``()`` means tokens were\n  requested but no usable color tokens were found \u2014 the page declares no custom\n  properties at all, or every declaration was filtered as non-meaningful (no\n  resolvable color, ``ignore`` semantic role, or zero classification weight).",
  "properties": {
    "theme": {
      "$ref": "#/$defs/Theme",
      "description": "The theme this palette was derived for."
    },
    "colors": {
      "description": "Canonical color-keyed index: every measured color with where it is used, ranked by ``prominence`` (third-party-dominated colors excluded).",
      "items": {
        "$ref": "#/$defs/ColorUsage"
      },
      "title": "Colors",
      "type": "array"
    },
    "usage": {
      "$ref": "#/$defs/UsagePalette",
      "description": "Role-keyed projection: which colors paint each usage role (measured pooled with token intent)."
    },
    "divergence": {
      "description": "Declared-vs-measured discrepancies, keyed by usage role.",
      "items": {
        "$ref": "#/$defs/DivergenceItem"
      },
      "title": "Divergence",
      "type": "array"
    },
    "tokens": {
      "anyOf": [
        {
          "items": {
            "$ref": "#/$defs/DesignToken"
          },
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Declared design tokens, opt-in: ``None`` means tokens were not requested; ``()`` means requested but none were usable.",
      "title": "Tokens"
    }
  },
  "required": [
    "theme",
    "usage"
  ],
  "title": "ThemePalette",
  "type": "object"
}

Config:

  • frozen: True

Fields:

theme pydantic-field

theme: Theme

The theme this palette was derived for.

colors pydantic-field

colors: tuple[ColorUsage, ...]

Canonical color-keyed index: every measured color with where it is used, ranked by prominence (third-party-dominated colors excluded).

usage pydantic-field

usage: UsagePalette

Role-keyed projection: which colors paint each usage role (measured pooled with token intent).

divergence pydantic-field

divergence: tuple[DivergenceItem, ...]

Declared-vs-measured discrepancies, keyed by usage role.

tokens pydantic-field

tokens: tuple[DesignToken, ...] | None = None

Declared design tokens, opt-in: None means tokens were not requested; () means requested but none were usable.

colorsense.ColorUsage pydantic-model

Bases: BaseModel

A measured color and where it is used — one entry of the color-keyed canonical inventory.

prominence is the overall ranking signal blending area-truth (primary) with routed vote mass (secondary), so dominant backgrounds rank high while zero-area brand accents (CTA/link colors) are not buried; the colors tuple is sorted by it, descending, with a hex tiebreak. area is the raw screenshot area fraction the color's cluster covers (an auditable signal, not a probability). usages lists every UsageRole the color appears in, most-used first (weight descending, hex tiebreak).

Show JSON schema:
{
  "$defs": {
    "Color": {
      "description": "An sRGB color with cached OKLCH coordinates.\n\n``hex`` is always the *opaque* normalized lowercase 7-char sRGB hex string\n(``#rrggbb``) \u2014 alpha is carried separately in ``alpha`` and never encoded in the\nhex (the invariant ``color/primitives.py`` establishes; fixed-length hexes are also\nwhat keeps lexicographic tie-breaks well-defined). ``lightness``/``chroma``/``hue``\nare the OKLCH coordinates of the (composited) color; ``alpha`` is the source alpha.",
      "properties": {
        "hex": {
          "description": "Opaque normalized lowercase 7-char sRGB hex (``#rrggbb``); alpha is never encoded here (it lives in ``alpha``).",
          "examples": [
            "#1f6feb",
            "#ffffff",
            "#0d1117"
          ],
          "pattern": "^#[0-9a-f]{6}$",
          "title": "Hex",
          "type": "string"
        },
        "lightness": {
          "description": "OKLCH lightness of the (composited) color; 0.0 is black, 1.0 is white.",
          "examples": [
            0.62
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Lightness",
          "type": "number"
        },
        "chroma": {
          "description": "OKLCH chroma (saturation) of the (composited) color; 0.0 is achromatic. Unbounded above, though sRGB colors rarely exceed ~0.4.",
          "examples": [
            0.18
          ],
          "minimum": 0.0,
          "title": "Chroma",
          "type": "number"
        },
        "hue": {
          "description": "OKLCH hue angle in degrees; achromatic colors are normalized to 0.0 (never NaN).",
          "examples": [
            256.3
          ],
          "exclusiveMaximum": 360.0,
          "minimum": 0.0,
          "title": "Hue",
          "type": "number"
        },
        "alpha": {
          "default": 1.0,
          "description": "Source alpha: 0.0 is fully transparent, 1.0 is fully opaque.",
          "examples": [
            1.0
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Alpha",
          "type": "number"
        }
      },
      "required": [
        "hex",
        "lightness",
        "chroma",
        "hue"
      ],
      "title": "Color",
      "type": "object"
    },
    "ComponentType": {
      "description": "Visual component a rendered element belongs to (source of a measured color).\n\nPublic: keys the fine-grained component evidence in the result tree \u2014 both\n[`Usage`][colorsense.Usage]``.components`` on the color-keyed index and\n[`UsageEntry`][colorsense.UsageEntry]``.components`` on the role-keyed projection \u2014\nnaming which component types contributed a color to a usage role.\n\nAttributes:\n    PAGE_BG: Page background.\n    PAGE_TEXT: Page body text.\n    HEADER_BG: Header background.\n    HEADER_TEXT: Header text.\n    NAV_BG: Nav background.\n    NAV_TEXT: Nav text.\n    FOOTER_BG: Footer background.\n    FOOTER_TEXT: Footer text.\n    HERO_BG: Hero background.\n    HERO_TEXT: Hero text.\n    CARD_BG: Card background.\n    CARD_TEXT: Card text.\n    CTA_BG: Primary call-to-action button background.\n    CTA_TEXT: Primary call-to-action button text/label.\n    LINK: Hyperlink text color.\n    BUTTON_SECONDARY: Secondary button background.\n    MODAL_BG: Modal/dialog background.\n    INPUT_BG: Form input background.\n    BORDER: Border/divider color.\n    BADGE: Badge/chip/pill background.\n    THIRD_PARTY: Color from an embedded third-party widget.",
      "enum": [
        "page_bg",
        "page_text",
        "header_bg",
        "header_text",
        "nav_bg",
        "nav_text",
        "footer_bg",
        "footer_text",
        "hero_bg",
        "hero_text",
        "card_bg",
        "card_text",
        "cta_bg",
        "cta_text",
        "link",
        "button_secondary",
        "modal_bg",
        "input_bg",
        "border",
        "badge",
        "third_party"
      ],
      "title": "ComponentType",
      "type": "string"
    },
    "PropertyFamily": {
      "description": "Which family of CSS properties paints the color \u2014 the rollup axis over roles.\n\nCoarser than [`UsageRole`][colorsense.UsageRole]: every role belongs to exactly one\nfamily (the mapping is [`UsageRole.property_family`][colorsense.UsageRole.property_family]).\n\nAttributes:\n    BACKGROUND: ``background-color`` / ``background-image`` fills.\n    TEXT: The ``color`` property (typography).\n    BORDER: The ``border-color`` property.",
      "enum": [
        "background",
        "text",
        "border"
      ],
      "title": "PropertyFamily",
      "type": "string"
    },
    "Usage": {
      "description": "One usage slot of a color in the color-keyed index ([`ColorUsage`][colorsense.ColorUsage]).\n\nA color may be used in several roles (e.g. the same gray as ``text`` *and* ``border``);\neach gets its own ``Usage``. ``role`` is the [`UsageRole`][colorsense.UsageRole] this\nslot describes and ``property_family`` is its [`PropertyFamily`][colorsense.PropertyFamily]\nrollup (denormalized \u2014 always ``role.property_family`` \u2014 so consumers can group by family\nwithout recomputing). ``weight`` is this color's share of *its own* usages \u2014 the role's\nrouted mass over the color's total routed mass, so a color's ``weight`` values sum to\n~1.0. ``components`` is normalized evidence within this slot: which component types\ncontributed the color to this role (e.g. ``{card_bg: 0.7, modal_bg: 0.3}``), summing to\n~1.0 when non-empty.",
      "properties": {
        "role": {
          "$ref": "#/$defs/UsageRole",
          "description": "The usage role this slot describes."
        },
        "property_family": {
          "$ref": "#/$defs/PropertyFamily",
          "description": "Rollup family for ``role`` (denormalized \u2014 always ``role.property_family``)."
        },
        "weight": {
          "description": "This role's share of the color's total routed mass; a color's ``weight`` values sum to ~1.0.",
          "examples": [
            0.7
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Weight",
          "type": "number"
        },
        "components": {
          "additionalProperties": {
            "type": "number"
          },
          "description": "Normalized evidence: which component types contributed the color to this role (each value in ``[0, 1]``, summing to ~1.0 when non-empty).",
          "examples": [
            {
              "card_bg": 0.7,
              "modal_bg": 0.3
            }
          ],
          "propertyNames": {
            "$ref": "#/$defs/ComponentType"
          },
          "title": "Components",
          "type": "object"
        }
      },
      "required": [
        "role",
        "property_family",
        "weight"
      ],
      "title": "Usage",
      "type": "object"
    },
    "UsageRole": {
      "description": "Developer-facing usage role \u2014 how a rendered color is used on the page.\n\nThe role taxonomy keys the role-keyed palette projection\n([`UsagePalette`][colorsense.UsagePalette]) and labels each\n[`Usage`][colorsense.Usage] slot of the color-keyed index. It splits the two axes the\nold 4-value usage taxonomy conflated \u2014 *which CSS property paints the color* (the\n[`PropertyFamily`][colorsense.PropertyFamily] rollup) versus *what kind of element it\nis* \u2014 so that, e.g., link text and CTA button backgrounds no longer share one slot.\n\nAttributes:\n    PAGE: The base canvas (the page background).\n    SURFACE: Raised content backgrounds: cards, modals, hero, inputs.\n    BANNER: Chrome-bar backgrounds: header, nav, footer.\n    CTA: The primary action background (CTA buttons).\n    ACTION: Secondary action backgrounds: secondary buttons, badges.\n    TEXT: Body/heading typography at every layer.\n    LINK: Link color (typography of anchors).\n    BORDER: Borders and dividers.",
      "enum": [
        "page",
        "surface",
        "banner",
        "cta",
        "action",
        "text",
        "link",
        "border"
      ],
      "title": "UsageRole",
      "type": "string"
    }
  },
  "description": "A measured color and where it is used \u2014 one entry of the color-keyed canonical inventory.\n\n``prominence`` is the overall ranking signal blending area-truth (primary) with routed\nvote mass (secondary), so dominant backgrounds rank high while zero-area brand accents\n(CTA/link colors) are not buried; the ``colors`` tuple is sorted by it, descending,\nwith a ``hex`` tiebreak. ``area`` is the raw screenshot area fraction the color's\ncluster covers (an auditable signal, not a probability). ``usages`` lists every\n[`UsageRole`][colorsense.UsageRole] the color appears in, most-used first\n(``weight`` descending, ``hex`` tiebreak).",
  "properties": {
    "color": {
      "$ref": "#/$defs/Color",
      "description": "The measured color this inventory entry describes."
    },
    "prominence": {
      "description": "Overall ranking signal blending area-truth (primary) with routed vote mass (secondary); the ``colors`` tuple is sorted by it, descending.",
      "examples": [
        0.83
      ],
      "maximum": 1.0,
      "minimum": 0.0,
      "title": "Prominence",
      "type": "number"
    },
    "area": {
      "description": "Raw screenshot area fraction the color's cluster covers (auditable, not a probability).",
      "examples": [
        0.31
      ],
      "maximum": 1.0,
      "minimum": 0.0,
      "title": "Area",
      "type": "number"
    },
    "usages": {
      "description": "Every usage role the color appears in, most-used first (``weight`` descending).",
      "items": {
        "$ref": "#/$defs/Usage"
      },
      "title": "Usages",
      "type": "array"
    }
  },
  "required": [
    "color",
    "prominence",
    "area"
  ],
  "title": "ColorUsage",
  "type": "object"
}

Config:

  • frozen: True

Fields:

color pydantic-field

color: Color

The measured color this inventory entry describes.

prominence pydantic-field

prominence: float

Overall ranking signal blending area-truth (primary) with routed vote mass (secondary); the colors tuple is sorted by it, descending.

area pydantic-field

area: float

Raw screenshot area fraction the color's cluster covers (auditable, not a probability).

usages pydantic-field

usages: tuple[Usage, ...]

Every usage role the color appears in, most-used first (weight descending).

colorsense.Usage pydantic-model

Bases: BaseModel

One usage slot of a color in the color-keyed index (ColorUsage).

A color may be used in several roles (e.g. the same gray as text and border); each gets its own Usage. role is the UsageRole this slot describes and property_family is its PropertyFamily rollup (denormalized — always role.property_family — so consumers can group by family without recomputing). weight is this color's share of its own usages — the role's routed mass over the color's total routed mass, so a color's weight values sum to ~1.0. components is normalized evidence within this slot: which component types contributed the color to this role (e.g. {card_bg: 0.7, modal_bg: 0.3}), summing to ~1.0 when non-empty.

Show JSON schema:
{
  "$defs": {
    "ComponentType": {
      "description": "Visual component a rendered element belongs to (source of a measured color).\n\nPublic: keys the fine-grained component evidence in the result tree \u2014 both\n[`Usage`][colorsense.Usage]``.components`` on the color-keyed index and\n[`UsageEntry`][colorsense.UsageEntry]``.components`` on the role-keyed projection \u2014\nnaming which component types contributed a color to a usage role.\n\nAttributes:\n    PAGE_BG: Page background.\n    PAGE_TEXT: Page body text.\n    HEADER_BG: Header background.\n    HEADER_TEXT: Header text.\n    NAV_BG: Nav background.\n    NAV_TEXT: Nav text.\n    FOOTER_BG: Footer background.\n    FOOTER_TEXT: Footer text.\n    HERO_BG: Hero background.\n    HERO_TEXT: Hero text.\n    CARD_BG: Card background.\n    CARD_TEXT: Card text.\n    CTA_BG: Primary call-to-action button background.\n    CTA_TEXT: Primary call-to-action button text/label.\n    LINK: Hyperlink text color.\n    BUTTON_SECONDARY: Secondary button background.\n    MODAL_BG: Modal/dialog background.\n    INPUT_BG: Form input background.\n    BORDER: Border/divider color.\n    BADGE: Badge/chip/pill background.\n    THIRD_PARTY: Color from an embedded third-party widget.",
      "enum": [
        "page_bg",
        "page_text",
        "header_bg",
        "header_text",
        "nav_bg",
        "nav_text",
        "footer_bg",
        "footer_text",
        "hero_bg",
        "hero_text",
        "card_bg",
        "card_text",
        "cta_bg",
        "cta_text",
        "link",
        "button_secondary",
        "modal_bg",
        "input_bg",
        "border",
        "badge",
        "third_party"
      ],
      "title": "ComponentType",
      "type": "string"
    },
    "PropertyFamily": {
      "description": "Which family of CSS properties paints the color \u2014 the rollup axis over roles.\n\nCoarser than [`UsageRole`][colorsense.UsageRole]: every role belongs to exactly one\nfamily (the mapping is [`UsageRole.property_family`][colorsense.UsageRole.property_family]).\n\nAttributes:\n    BACKGROUND: ``background-color`` / ``background-image`` fills.\n    TEXT: The ``color`` property (typography).\n    BORDER: The ``border-color`` property.",
      "enum": [
        "background",
        "text",
        "border"
      ],
      "title": "PropertyFamily",
      "type": "string"
    },
    "UsageRole": {
      "description": "Developer-facing usage role \u2014 how a rendered color is used on the page.\n\nThe role taxonomy keys the role-keyed palette projection\n([`UsagePalette`][colorsense.UsagePalette]) and labels each\n[`Usage`][colorsense.Usage] slot of the color-keyed index. It splits the two axes the\nold 4-value usage taxonomy conflated \u2014 *which CSS property paints the color* (the\n[`PropertyFamily`][colorsense.PropertyFamily] rollup) versus *what kind of element it\nis* \u2014 so that, e.g., link text and CTA button backgrounds no longer share one slot.\n\nAttributes:\n    PAGE: The base canvas (the page background).\n    SURFACE: Raised content backgrounds: cards, modals, hero, inputs.\n    BANNER: Chrome-bar backgrounds: header, nav, footer.\n    CTA: The primary action background (CTA buttons).\n    ACTION: Secondary action backgrounds: secondary buttons, badges.\n    TEXT: Body/heading typography at every layer.\n    LINK: Link color (typography of anchors).\n    BORDER: Borders and dividers.",
      "enum": [
        "page",
        "surface",
        "banner",
        "cta",
        "action",
        "text",
        "link",
        "border"
      ],
      "title": "UsageRole",
      "type": "string"
    }
  },
  "description": "One usage slot of a color in the color-keyed index ([`ColorUsage`][colorsense.ColorUsage]).\n\nA color may be used in several roles (e.g. the same gray as ``text`` *and* ``border``);\neach gets its own ``Usage``. ``role`` is the [`UsageRole`][colorsense.UsageRole] this\nslot describes and ``property_family`` is its [`PropertyFamily`][colorsense.PropertyFamily]\nrollup (denormalized \u2014 always ``role.property_family`` \u2014 so consumers can group by family\nwithout recomputing). ``weight`` is this color's share of *its own* usages \u2014 the role's\nrouted mass over the color's total routed mass, so a color's ``weight`` values sum to\n~1.0. ``components`` is normalized evidence within this slot: which component types\ncontributed the color to this role (e.g. ``{card_bg: 0.7, modal_bg: 0.3}``), summing to\n~1.0 when non-empty.",
  "properties": {
    "role": {
      "$ref": "#/$defs/UsageRole",
      "description": "The usage role this slot describes."
    },
    "property_family": {
      "$ref": "#/$defs/PropertyFamily",
      "description": "Rollup family for ``role`` (denormalized \u2014 always ``role.property_family``)."
    },
    "weight": {
      "description": "This role's share of the color's total routed mass; a color's ``weight`` values sum to ~1.0.",
      "examples": [
        0.7
      ],
      "maximum": 1.0,
      "minimum": 0.0,
      "title": "Weight",
      "type": "number"
    },
    "components": {
      "additionalProperties": {
        "type": "number"
      },
      "description": "Normalized evidence: which component types contributed the color to this role (each value in ``[0, 1]``, summing to ~1.0 when non-empty).",
      "examples": [
        {
          "card_bg": 0.7,
          "modal_bg": 0.3
        }
      ],
      "propertyNames": {
        "$ref": "#/$defs/ComponentType"
      },
      "title": "Components",
      "type": "object"
    }
  },
  "required": [
    "role",
    "property_family",
    "weight"
  ],
  "title": "Usage",
  "type": "object"
}

Config:

  • frozen: True

Fields:

role pydantic-field

role: UsageRole

The usage role this slot describes.

property_family pydantic-field

property_family: PropertyFamily

Rollup family for role (denormalized — always role.property_family).

weight pydantic-field

weight: float

This role's share of the color's total routed mass; a color's weight values sum to ~1.0.

components pydantic-field

components: dict[ComponentType, float]

Normalized evidence: which component types contributed the color to this role (each value in [0, 1], summing to ~1.0 when non-empty).

colorsense.UsagePalette pydantic-model

Bases: BaseModel

The role-keyed palette projection: which colors paint each usage role.

mapping is guaranteed to contain every UsageRole; a role with no detected entries maps to an empty tuple. This invariant is enforced by an after-validator that backfills any missing roles, so even UsagePalette() and the empty-input path expose all eight keys.

Show JSON schema:
{
  "$defs": {
    "Color": {
      "description": "An sRGB color with cached OKLCH coordinates.\n\n``hex`` is always the *opaque* normalized lowercase 7-char sRGB hex string\n(``#rrggbb``) \u2014 alpha is carried separately in ``alpha`` and never encoded in the\nhex (the invariant ``color/primitives.py`` establishes; fixed-length hexes are also\nwhat keeps lexicographic tie-breaks well-defined). ``lightness``/``chroma``/``hue``\nare the OKLCH coordinates of the (composited) color; ``alpha`` is the source alpha.",
      "properties": {
        "hex": {
          "description": "Opaque normalized lowercase 7-char sRGB hex (``#rrggbb``); alpha is never encoded here (it lives in ``alpha``).",
          "examples": [
            "#1f6feb",
            "#ffffff",
            "#0d1117"
          ],
          "pattern": "^#[0-9a-f]{6}$",
          "title": "Hex",
          "type": "string"
        },
        "lightness": {
          "description": "OKLCH lightness of the (composited) color; 0.0 is black, 1.0 is white.",
          "examples": [
            0.62
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Lightness",
          "type": "number"
        },
        "chroma": {
          "description": "OKLCH chroma (saturation) of the (composited) color; 0.0 is achromatic. Unbounded above, though sRGB colors rarely exceed ~0.4.",
          "examples": [
            0.18
          ],
          "minimum": 0.0,
          "title": "Chroma",
          "type": "number"
        },
        "hue": {
          "description": "OKLCH hue angle in degrees; achromatic colors are normalized to 0.0 (never NaN).",
          "examples": [
            256.3
          ],
          "exclusiveMaximum": 360.0,
          "minimum": 0.0,
          "title": "Hue",
          "type": "number"
        },
        "alpha": {
          "default": 1.0,
          "description": "Source alpha: 0.0 is fully transparent, 1.0 is fully opaque.",
          "examples": [
            1.0
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Alpha",
          "type": "number"
        }
      },
      "required": [
        "hex",
        "lightness",
        "chroma",
        "hue"
      ],
      "title": "Color",
      "type": "object"
    },
    "ComponentType": {
      "description": "Visual component a rendered element belongs to (source of a measured color).\n\nPublic: keys the fine-grained component evidence in the result tree \u2014 both\n[`Usage`][colorsense.Usage]``.components`` on the color-keyed index and\n[`UsageEntry`][colorsense.UsageEntry]``.components`` on the role-keyed projection \u2014\nnaming which component types contributed a color to a usage role.\n\nAttributes:\n    PAGE_BG: Page background.\n    PAGE_TEXT: Page body text.\n    HEADER_BG: Header background.\n    HEADER_TEXT: Header text.\n    NAV_BG: Nav background.\n    NAV_TEXT: Nav text.\n    FOOTER_BG: Footer background.\n    FOOTER_TEXT: Footer text.\n    HERO_BG: Hero background.\n    HERO_TEXT: Hero text.\n    CARD_BG: Card background.\n    CARD_TEXT: Card text.\n    CTA_BG: Primary call-to-action button background.\n    CTA_TEXT: Primary call-to-action button text/label.\n    LINK: Hyperlink text color.\n    BUTTON_SECONDARY: Secondary button background.\n    MODAL_BG: Modal/dialog background.\n    INPUT_BG: Form input background.\n    BORDER: Border/divider color.\n    BADGE: Badge/chip/pill background.\n    THIRD_PARTY: Color from an embedded third-party widget.",
      "enum": [
        "page_bg",
        "page_text",
        "header_bg",
        "header_text",
        "nav_bg",
        "nav_text",
        "footer_bg",
        "footer_text",
        "hero_bg",
        "hero_text",
        "card_bg",
        "card_text",
        "cta_bg",
        "cta_text",
        "link",
        "button_secondary",
        "modal_bg",
        "input_bg",
        "border",
        "badge",
        "third_party"
      ],
      "title": "ComponentType",
      "type": "string"
    },
    "UsageEntry": {
      "description": "One color's standing within a usage role (role-keyed projection).\n\n``probability`` is the posterior prominence of this color *within its role*\n(entries of one role sum to ~1.0). ``area`` is the raw screenshot area fraction\nthe color's cluster covers \u2014 an auditable signal, not a probability. ``components``\nis normalized evidence: which component types contributed this color to this\nrole (e.g. ``{card_bg: 0.7, modal_bg: 0.3}``), summing to ~1.0 when non-empty.",
      "properties": {
        "color": {
          "$ref": "#/$defs/Color",
          "description": "The color this role entry describes."
        },
        "probability": {
          "description": "Posterior prominence of this color within its role; entries of one role sum to ~1.0.",
          "examples": [
            0.55
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Probability",
          "type": "number"
        },
        "area": {
          "description": "Raw screenshot area fraction the color's cluster covers (auditable, not a probability).",
          "examples": [
            0.31
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Area",
          "type": "number"
        },
        "components": {
          "additionalProperties": {
            "type": "number"
          },
          "description": "Normalized evidence: which component types contributed this color to this role (each value in ``[0, 1]``, summing to ~1.0 when non-empty).",
          "examples": [
            {
              "card_bg": 0.7,
              "modal_bg": 0.3
            }
          ],
          "propertyNames": {
            "$ref": "#/$defs/ComponentType"
          },
          "title": "Components",
          "type": "object"
        }
      },
      "required": [
        "color",
        "probability",
        "area"
      ],
      "title": "UsageEntry",
      "type": "object"
    },
    "UsageRole": {
      "description": "Developer-facing usage role \u2014 how a rendered color is used on the page.\n\nThe role taxonomy keys the role-keyed palette projection\n([`UsagePalette`][colorsense.UsagePalette]) and labels each\n[`Usage`][colorsense.Usage] slot of the color-keyed index. It splits the two axes the\nold 4-value usage taxonomy conflated \u2014 *which CSS property paints the color* (the\n[`PropertyFamily`][colorsense.PropertyFamily] rollup) versus *what kind of element it\nis* \u2014 so that, e.g., link text and CTA button backgrounds no longer share one slot.\n\nAttributes:\n    PAGE: The base canvas (the page background).\n    SURFACE: Raised content backgrounds: cards, modals, hero, inputs.\n    BANNER: Chrome-bar backgrounds: header, nav, footer.\n    CTA: The primary action background (CTA buttons).\n    ACTION: Secondary action backgrounds: secondary buttons, badges.\n    TEXT: Body/heading typography at every layer.\n    LINK: Link color (typography of anchors).\n    BORDER: Borders and dividers.",
      "enum": [
        "page",
        "surface",
        "banner",
        "cta",
        "action",
        "text",
        "link",
        "border"
      ],
      "title": "UsageRole",
      "type": "string"
    }
  },
  "description": "The role-keyed palette projection: which colors paint each usage role.\n\n``mapping`` is guaranteed to contain every [`UsageRole`][colorsense.UsageRole]; a\nrole with no detected entries maps to an empty tuple. This invariant is enforced by an\nafter-validator that backfills any missing roles, so even ``UsagePalette()`` and\nthe empty-input path expose all eight keys.",
  "properties": {
    "mapping": {
      "additionalProperties": {
        "items": {
          "$ref": "#/$defs/UsageEntry"
        },
        "type": "array"
      },
      "description": "Colors painting each [`UsageRole`][colorsense.UsageRole]; guaranteed to contain every role (an unused role maps to ``()``).",
      "propertyNames": {
        "$ref": "#/$defs/UsageRole"
      },
      "title": "Mapping",
      "type": "object"
    }
  },
  "title": "UsagePalette",
  "type": "object"
}

Config:

  • frozen: True

Fields:

Validators:

  • _backfill_roles

mapping pydantic-field

mapping: dict[UsageRole, tuple[UsageEntry, ...]]

Colors painting each UsageRole; guaranteed to contain every role (an unused role maps to ()).

colorsense.UsageEntry pydantic-model

Bases: BaseModel

One color's standing within a usage role (role-keyed projection).

probability is the posterior prominence of this color within its role (entries of one role sum to ~1.0). area is the raw screenshot area fraction the color's cluster covers — an auditable signal, not a probability. components is normalized evidence: which component types contributed this color to this role (e.g. {card_bg: 0.7, modal_bg: 0.3}), summing to ~1.0 when non-empty.

Show JSON schema:
{
  "$defs": {
    "Color": {
      "description": "An sRGB color with cached OKLCH coordinates.\n\n``hex`` is always the *opaque* normalized lowercase 7-char sRGB hex string\n(``#rrggbb``) \u2014 alpha is carried separately in ``alpha`` and never encoded in the\nhex (the invariant ``color/primitives.py`` establishes; fixed-length hexes are also\nwhat keeps lexicographic tie-breaks well-defined). ``lightness``/``chroma``/``hue``\nare the OKLCH coordinates of the (composited) color; ``alpha`` is the source alpha.",
      "properties": {
        "hex": {
          "description": "Opaque normalized lowercase 7-char sRGB hex (``#rrggbb``); alpha is never encoded here (it lives in ``alpha``).",
          "examples": [
            "#1f6feb",
            "#ffffff",
            "#0d1117"
          ],
          "pattern": "^#[0-9a-f]{6}$",
          "title": "Hex",
          "type": "string"
        },
        "lightness": {
          "description": "OKLCH lightness of the (composited) color; 0.0 is black, 1.0 is white.",
          "examples": [
            0.62
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Lightness",
          "type": "number"
        },
        "chroma": {
          "description": "OKLCH chroma (saturation) of the (composited) color; 0.0 is achromatic. Unbounded above, though sRGB colors rarely exceed ~0.4.",
          "examples": [
            0.18
          ],
          "minimum": 0.0,
          "title": "Chroma",
          "type": "number"
        },
        "hue": {
          "description": "OKLCH hue angle in degrees; achromatic colors are normalized to 0.0 (never NaN).",
          "examples": [
            256.3
          ],
          "exclusiveMaximum": 360.0,
          "minimum": 0.0,
          "title": "Hue",
          "type": "number"
        },
        "alpha": {
          "default": 1.0,
          "description": "Source alpha: 0.0 is fully transparent, 1.0 is fully opaque.",
          "examples": [
            1.0
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Alpha",
          "type": "number"
        }
      },
      "required": [
        "hex",
        "lightness",
        "chroma",
        "hue"
      ],
      "title": "Color",
      "type": "object"
    },
    "ComponentType": {
      "description": "Visual component a rendered element belongs to (source of a measured color).\n\nPublic: keys the fine-grained component evidence in the result tree \u2014 both\n[`Usage`][colorsense.Usage]``.components`` on the color-keyed index and\n[`UsageEntry`][colorsense.UsageEntry]``.components`` on the role-keyed projection \u2014\nnaming which component types contributed a color to a usage role.\n\nAttributes:\n    PAGE_BG: Page background.\n    PAGE_TEXT: Page body text.\n    HEADER_BG: Header background.\n    HEADER_TEXT: Header text.\n    NAV_BG: Nav background.\n    NAV_TEXT: Nav text.\n    FOOTER_BG: Footer background.\n    FOOTER_TEXT: Footer text.\n    HERO_BG: Hero background.\n    HERO_TEXT: Hero text.\n    CARD_BG: Card background.\n    CARD_TEXT: Card text.\n    CTA_BG: Primary call-to-action button background.\n    CTA_TEXT: Primary call-to-action button text/label.\n    LINK: Hyperlink text color.\n    BUTTON_SECONDARY: Secondary button background.\n    MODAL_BG: Modal/dialog background.\n    INPUT_BG: Form input background.\n    BORDER: Border/divider color.\n    BADGE: Badge/chip/pill background.\n    THIRD_PARTY: Color from an embedded third-party widget.",
      "enum": [
        "page_bg",
        "page_text",
        "header_bg",
        "header_text",
        "nav_bg",
        "nav_text",
        "footer_bg",
        "footer_text",
        "hero_bg",
        "hero_text",
        "card_bg",
        "card_text",
        "cta_bg",
        "cta_text",
        "link",
        "button_secondary",
        "modal_bg",
        "input_bg",
        "border",
        "badge",
        "third_party"
      ],
      "title": "ComponentType",
      "type": "string"
    }
  },
  "description": "One color's standing within a usage role (role-keyed projection).\n\n``probability`` is the posterior prominence of this color *within its role*\n(entries of one role sum to ~1.0). ``area`` is the raw screenshot area fraction\nthe color's cluster covers \u2014 an auditable signal, not a probability. ``components``\nis normalized evidence: which component types contributed this color to this\nrole (e.g. ``{card_bg: 0.7, modal_bg: 0.3}``), summing to ~1.0 when non-empty.",
  "properties": {
    "color": {
      "$ref": "#/$defs/Color",
      "description": "The color this role entry describes."
    },
    "probability": {
      "description": "Posterior prominence of this color within its role; entries of one role sum to ~1.0.",
      "examples": [
        0.55
      ],
      "maximum": 1.0,
      "minimum": 0.0,
      "title": "Probability",
      "type": "number"
    },
    "area": {
      "description": "Raw screenshot area fraction the color's cluster covers (auditable, not a probability).",
      "examples": [
        0.31
      ],
      "maximum": 1.0,
      "minimum": 0.0,
      "title": "Area",
      "type": "number"
    },
    "components": {
      "additionalProperties": {
        "type": "number"
      },
      "description": "Normalized evidence: which component types contributed this color to this role (each value in ``[0, 1]``, summing to ~1.0 when non-empty).",
      "examples": [
        {
          "card_bg": 0.7,
          "modal_bg": 0.3
        }
      ],
      "propertyNames": {
        "$ref": "#/$defs/ComponentType"
      },
      "title": "Components",
      "type": "object"
    }
  },
  "required": [
    "color",
    "probability",
    "area"
  ],
  "title": "UsageEntry",
  "type": "object"
}

Config:

  • frozen: True

Fields:

color pydantic-field

color: Color

The color this role entry describes.

probability pydantic-field

probability: float

Posterior prominence of this color within its role; entries of one role sum to ~1.0.

area pydantic-field

area: float

Raw screenshot area fraction the color's cluster covers (auditable, not a probability).

components pydantic-field

components: dict[ComponentType, float]

Normalized evidence: which component types contributed this color to this role (each value in [0, 1], summing to ~1.0 when non-empty).

colorsense.DivergenceItem pydantic-model

Bases: BaseModel

A declared-but-unused or used-but-undeclared palette discrepancy.

Keyed by UsageRole — the role-keyed usage view is where declared token intent is reconciled against measured usage.

Show JSON schema:
{
  "$defs": {
    "Color": {
      "description": "An sRGB color with cached OKLCH coordinates.\n\n``hex`` is always the *opaque* normalized lowercase 7-char sRGB hex string\n(``#rrggbb``) \u2014 alpha is carried separately in ``alpha`` and never encoded in the\nhex (the invariant ``color/primitives.py`` establishes; fixed-length hexes are also\nwhat keeps lexicographic tie-breaks well-defined). ``lightness``/``chroma``/``hue``\nare the OKLCH coordinates of the (composited) color; ``alpha`` is the source alpha.",
      "properties": {
        "hex": {
          "description": "Opaque normalized lowercase 7-char sRGB hex (``#rrggbb``); alpha is never encoded here (it lives in ``alpha``).",
          "examples": [
            "#1f6feb",
            "#ffffff",
            "#0d1117"
          ],
          "pattern": "^#[0-9a-f]{6}$",
          "title": "Hex",
          "type": "string"
        },
        "lightness": {
          "description": "OKLCH lightness of the (composited) color; 0.0 is black, 1.0 is white.",
          "examples": [
            0.62
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Lightness",
          "type": "number"
        },
        "chroma": {
          "description": "OKLCH chroma (saturation) of the (composited) color; 0.0 is achromatic. Unbounded above, though sRGB colors rarely exceed ~0.4.",
          "examples": [
            0.18
          ],
          "minimum": 0.0,
          "title": "Chroma",
          "type": "number"
        },
        "hue": {
          "description": "OKLCH hue angle in degrees; achromatic colors are normalized to 0.0 (never NaN).",
          "examples": [
            256.3
          ],
          "exclusiveMaximum": 360.0,
          "minimum": 0.0,
          "title": "Hue",
          "type": "number"
        },
        "alpha": {
          "default": 1.0,
          "description": "Source alpha: 0.0 is fully transparent, 1.0 is fully opaque.",
          "examples": [
            1.0
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Alpha",
          "type": "number"
        }
      },
      "required": [
        "hex",
        "lightness",
        "chroma",
        "hue"
      ],
      "title": "Color",
      "type": "object"
    },
    "UsageRole": {
      "description": "Developer-facing usage role \u2014 how a rendered color is used on the page.\n\nThe role taxonomy keys the role-keyed palette projection\n([`UsagePalette`][colorsense.UsagePalette]) and labels each\n[`Usage`][colorsense.Usage] slot of the color-keyed index. It splits the two axes the\nold 4-value usage taxonomy conflated \u2014 *which CSS property paints the color* (the\n[`PropertyFamily`][colorsense.PropertyFamily] rollup) versus *what kind of element it\nis* \u2014 so that, e.g., link text and CTA button backgrounds no longer share one slot.\n\nAttributes:\n    PAGE: The base canvas (the page background).\n    SURFACE: Raised content backgrounds: cards, modals, hero, inputs.\n    BANNER: Chrome-bar backgrounds: header, nav, footer.\n    CTA: The primary action background (CTA buttons).\n    ACTION: Secondary action backgrounds: secondary buttons, badges.\n    TEXT: Body/heading typography at every layer.\n    LINK: Link color (typography of anchors).\n    BORDER: Borders and dividers.",
      "enum": [
        "page",
        "surface",
        "banner",
        "cta",
        "action",
        "text",
        "link",
        "border"
      ],
      "title": "UsageRole",
      "type": "string"
    }
  },
  "description": "A declared-but-unused or used-but-undeclared palette discrepancy.\n\nKeyed by [`UsageRole`][colorsense.UsageRole] \u2014 the role-keyed usage view is where\ndeclared token intent is reconciled against measured usage.",
  "properties": {
    "role": {
      "$ref": "#/$defs/UsageRole",
      "description": "The usage role the discrepancy was found under."
    },
    "color": {
      "$ref": "#/$defs/Color",
      "description": "The declared-but-unused or used-but-undeclared color."
    },
    "note": {
      "description": "Human-readable explanation of the discrepancy.",
      "examples": [
        "declared as brand_primary but not measured in use"
      ],
      "title": "Note",
      "type": "string"
    }
  },
  "required": [
    "role",
    "color",
    "note"
  ],
  "title": "DivergenceItem",
  "type": "object"
}

Config:

  • frozen: True

Fields:

role pydantic-field

role: UsageRole

The usage role the discrepancy was found under.

color pydantic-field

color: Color

The declared-but-unused or used-but-undeclared color.

note pydantic-field

note: str

Human-readable explanation of the discrepancy.

colorsense.DesignToken pydantic-model

Bases: BaseModel

A declared design token (CSS custom property) in the public result.

name is the declared property name (e.g. --fgColor-default); color is its value resolved in the rendered theme; semantic_role is the inferred semantic role.

Show JSON schema:
{
  "$defs": {
    "Color": {
      "description": "An sRGB color with cached OKLCH coordinates.\n\n``hex`` is always the *opaque* normalized lowercase 7-char sRGB hex string\n(``#rrggbb``) \u2014 alpha is carried separately in ``alpha`` and never encoded in the\nhex (the invariant ``color/primitives.py`` establishes; fixed-length hexes are also\nwhat keeps lexicographic tie-breaks well-defined). ``lightness``/``chroma``/``hue``\nare the OKLCH coordinates of the (composited) color; ``alpha`` is the source alpha.",
      "properties": {
        "hex": {
          "description": "Opaque normalized lowercase 7-char sRGB hex (``#rrggbb``); alpha is never encoded here (it lives in ``alpha``).",
          "examples": [
            "#1f6feb",
            "#ffffff",
            "#0d1117"
          ],
          "pattern": "^#[0-9a-f]{6}$",
          "title": "Hex",
          "type": "string"
        },
        "lightness": {
          "description": "OKLCH lightness of the (composited) color; 0.0 is black, 1.0 is white.",
          "examples": [
            0.62
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Lightness",
          "type": "number"
        },
        "chroma": {
          "description": "OKLCH chroma (saturation) of the (composited) color; 0.0 is achromatic. Unbounded above, though sRGB colors rarely exceed ~0.4.",
          "examples": [
            0.18
          ],
          "minimum": 0.0,
          "title": "Chroma",
          "type": "number"
        },
        "hue": {
          "description": "OKLCH hue angle in degrees; achromatic colors are normalized to 0.0 (never NaN).",
          "examples": [
            256.3
          ],
          "exclusiveMaximum": 360.0,
          "minimum": 0.0,
          "title": "Hue",
          "type": "number"
        },
        "alpha": {
          "default": 1.0,
          "description": "Source alpha: 0.0 is fully transparent, 1.0 is fully opaque.",
          "examples": [
            1.0
          ],
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Alpha",
          "type": "number"
        }
      },
      "required": [
        "hex",
        "lightness",
        "chroma",
        "hue"
      ],
      "title": "Color",
      "type": "object"
    },
    "TokenSemanticRole": {
      "description": "Semantic role inferred for a declared design token (CSS custom property).\n\nAttributes:\n    BRAND_PRIMARY: Primary brand color.\n    BRAND_SECONDARY: Secondary brand color.\n    BRAND_ACCENT: Accent brand color.\n    INTERACTIVE: Interactive-element color (links, controls).\n    SURFACE_BASE: Base page/surface background.\n    SURFACE_RAISED: Raised surface background (cards, modals).\n    TEXT_BODY: Body text color.\n    NEUTRAL: Neutral/gray with no specific role.\n    BORDER: Border/divider color.\n    TEXT_ON: Foreground meant to sit on a colored fill (an \"on\" color).\n    STATUS: Status color (success/warning/error/info).\n    IGNORE: Not a meaningful palette token (filtered out).",
      "enum": [
        "brand_primary",
        "brand_secondary",
        "brand_accent",
        "interactive",
        "surface_base",
        "surface_raised",
        "text_body",
        "neutral",
        "border",
        "text_on",
        "status",
        "ignore"
      ],
      "title": "TokenSemanticRole",
      "type": "string"
    }
  },
  "description": "A declared design token (CSS custom property) in the public result.\n\n``name`` is the declared property name (e.g. ``--fgColor-default``); ``color`` is its\nvalue resolved in the rendered theme; ``semantic_role`` is the inferred semantic role.",
  "properties": {
    "name": {
      "description": "Declared property name, including the leading ``--``.",
      "examples": [
        "--fgColor-default"
      ],
      "title": "Name",
      "type": "string"
    },
    "color": {
      "$ref": "#/$defs/Color",
      "description": "The token's value resolved in the rendered theme."
    },
    "semantic_role": {
      "$ref": "#/$defs/TokenSemanticRole",
      "description": "The inferred semantic role for the token."
    }
  },
  "required": [
    "name",
    "color",
    "semantic_role"
  ],
  "title": "DesignToken",
  "type": "object"
}

Config:

  • frozen: True

Fields:

name pydantic-field

name: str

Declared property name, including the leading --.

color pydantic-field

color: Color

The token's value resolved in the rendered theme.

semantic_role pydantic-field

semantic_role: TokenSemanticRole

The inferred semantic role for the token.

colorsense.Color pydantic-model

Bases: BaseModel

An sRGB color with cached OKLCH coordinates.

hex is always the opaque normalized lowercase 7-char sRGB hex string (#rrggbb) — alpha is carried separately in alpha and never encoded in the hex (the invariant color/primitives.py establishes; fixed-length hexes are also what keeps lexicographic tie-breaks well-defined). lightness/chroma/hue are the OKLCH coordinates of the (composited) color; alpha is the source alpha.

Show JSON schema:
{
  "description": "An sRGB color with cached OKLCH coordinates.\n\n``hex`` is always the *opaque* normalized lowercase 7-char sRGB hex string\n(``#rrggbb``) \u2014 alpha is carried separately in ``alpha`` and never encoded in the\nhex (the invariant ``color/primitives.py`` establishes; fixed-length hexes are also\nwhat keeps lexicographic tie-breaks well-defined). ``lightness``/``chroma``/``hue``\nare the OKLCH coordinates of the (composited) color; ``alpha`` is the source alpha.",
  "properties": {
    "hex": {
      "description": "Opaque normalized lowercase 7-char sRGB hex (``#rrggbb``); alpha is never encoded here (it lives in ``alpha``).",
      "examples": [
        "#1f6feb",
        "#ffffff",
        "#0d1117"
      ],
      "pattern": "^#[0-9a-f]{6}$",
      "title": "Hex",
      "type": "string"
    },
    "lightness": {
      "description": "OKLCH lightness of the (composited) color; 0.0 is black, 1.0 is white.",
      "examples": [
        0.62
      ],
      "maximum": 1.0,
      "minimum": 0.0,
      "title": "Lightness",
      "type": "number"
    },
    "chroma": {
      "description": "OKLCH chroma (saturation) of the (composited) color; 0.0 is achromatic. Unbounded above, though sRGB colors rarely exceed ~0.4.",
      "examples": [
        0.18
      ],
      "minimum": 0.0,
      "title": "Chroma",
      "type": "number"
    },
    "hue": {
      "description": "OKLCH hue angle in degrees; achromatic colors are normalized to 0.0 (never NaN).",
      "examples": [
        256.3
      ],
      "exclusiveMaximum": 360.0,
      "minimum": 0.0,
      "title": "Hue",
      "type": "number"
    },
    "alpha": {
      "default": 1.0,
      "description": "Source alpha: 0.0 is fully transparent, 1.0 is fully opaque.",
      "examples": [
        1.0
      ],
      "maximum": 1.0,
      "minimum": 0.0,
      "title": "Alpha",
      "type": "number"
    }
  },
  "required": [
    "hex",
    "lightness",
    "chroma",
    "hue"
  ],
  "title": "Color",
  "type": "object"
}

Config:

  • frozen: True

Fields:

hex pydantic-field

hex: str

Opaque normalized lowercase 7-char sRGB hex (#rrggbb); alpha is never encoded here (it lives in alpha).

lightness pydantic-field

lightness: float

OKLCH lightness of the (composited) color; 0.0 is black, 1.0 is white.

chroma pydantic-field

chroma: float

OKLCH chroma (saturation) of the (composited) color; 0.0 is achromatic. Unbounded above, though sRGB colors rarely exceed ~0.4.

hue pydantic-field

hue: float

OKLCH hue angle in degrees; achromatic colors are normalized to 0.0 (never NaN).

alpha pydantic-field

alpha: float = 1.0

Source alpha: 0.0 is fully transparent, 1.0 is fully opaque.

colorsense.Viewport pydantic-model

Bases: BaseModel

Rendering viewport.

Show JSON schema:
{
  "description": "Rendering viewport.",
  "properties": {
    "width": {
      "description": "Viewport width in CSS pixels.",
      "examples": [
        1280
      ],
      "minimum": 1,
      "title": "Width",
      "type": "integer"
    },
    "height": {
      "description": "Viewport height in CSS pixels.",
      "examples": [
        800
      ],
      "minimum": 1,
      "title": "Height",
      "type": "integer"
    },
    "device_scale_factor": {
      "description": "Device pixel ratio (DIP\u2192raster multiplier); 1.0 is non-retina, 2.0 is retina.",
      "examples": [
        1.0
      ],
      "exclusiveMinimum": 0.0,
      "title": "Device Scale Factor",
      "type": "number"
    }
  },
  "required": [
    "width",
    "height",
    "device_scale_factor"
  ],
  "title": "Viewport",
  "type": "object"
}

Config:

  • frozen: True

Fields:

width pydantic-field

width: int

Viewport width in CSS pixels.

height pydantic-field

height: int

Viewport height in CSS pixels.

device_scale_factor pydantic-field

device_scale_factor: float

Device pixel ratio (DIP→raster multiplier); 1.0 is non-retina, 2.0 is retina.

Enums

colorsense.Theme

Bases: StrEnum

Color scheme a site is rendered under.

Attributes:

Name Type Description
LIGHT

Light color scheme (the default).

DARK

Dark color scheme (prefers-color-scheme: dark).

colorsense.UsageRole

Bases: StrEnum

Developer-facing usage role — how a rendered color is used on the page.

The role taxonomy keys the role-keyed palette projection (UsagePalette) and labels each Usage slot of the color-keyed index. It splits the two axes the old 4-value usage taxonomy conflated — which CSS property paints the color (the PropertyFamily rollup) versus what kind of element it is — so that, e.g., link text and CTA button backgrounds no longer share one slot.

Attributes:

Name Type Description
PAGE

The base canvas (the page background).

SURFACE

Raised content backgrounds: cards, modals, hero, inputs.

BANNER

Chrome-bar backgrounds: header, nav, footer.

CTA

The primary action background (CTA buttons).

ACTION

Secondary action backgrounds: secondary buttons, badges.

TEXT

Body/heading typography at every layer.

LINK

Link color (typography of anchors).

BORDER

Borders and dividers.

property_family property

property_family: PropertyFamily

The PropertyFamily this usage role rolls up to.

A fixed code-level convention, the role-side twin of ComponentType.property_family: text and link are painted by the element's color (the text family), border by its border-color, and every other role (page/surface/banner/cta/action) by a background property. link is a text role even though it names an interactive element, because a link's painted color is its typography color, not its (usually transparent) background.

Returns:

Type Description
PropertyFamily

PropertyFamily.TEXT for text/link,

PropertyFamily

PropertyFamily.BORDER for border, and

PropertyFamily

PropertyFamily.BACKGROUND for every other role.

colorsense.PropertyFamily

Bases: StrEnum

Which family of CSS properties paints the color — the rollup axis over roles.

Coarser than UsageRole: every role belongs to exactly one family (the mapping is UsageRole.property_family).

Attributes:

Name Type Description
BACKGROUND

background-color / background-image fills.

TEXT

The color property (typography).

BORDER

The border-color property.

colorsense.ComponentType

Bases: StrEnum

Visual component a rendered element belongs to (source of a measured color).

Public: keys the fine-grained component evidence in the result tree — both Usage.components on the color-keyed index and UsageEntry.components on the role-keyed projection — naming which component types contributed a color to a usage role.

Attributes:

Name Type Description
PAGE_BG

Page background.

PAGE_TEXT

Page body text.

HEADER_BG

Header background.

HEADER_TEXT

Header text.

NAV_BG

Nav background.

NAV_TEXT

Nav text.

FOOTER_BG

Footer background.

FOOTER_TEXT

Footer text.

HERO_BG

Hero background.

HERO_TEXT

Hero text.

CARD_BG

Card background.

CARD_TEXT

Card text.

CTA_BG

Primary call-to-action button background.

CTA_TEXT

Primary call-to-action button text/label.

LINK

Hyperlink text color.

BUTTON_SECONDARY

Secondary button background.

MODAL_BG

Modal/dialog background.

INPUT_BG

Form input background.

BORDER

Border/divider color.

BADGE

Badge/chip/pill background.

THIRD_PARTY

Color from an embedded third-party widget.

property_family property

property_family: PropertyFamily

The PropertyFamily this component's vote mass routes to.

The routing convention is fixed in code, the component-side twin of UsageRole.property_family: components whose value ends with _text — plus link, whose painted color is its typography color, not its (usually transparent) background — are painted by the element's color (the text family), border by its border-color, and everything else (including badge, third_party and button_secondary) by its background-color (the background family).

This is the single source of truth for component routing, shared by the component classifier's per-family normalization (classify/components.py) and the inventory's per-family attribution (palette/inventory.py). The two partitions MUST stay identical, so both read this one property — it lives here in the shared-contracts module so neither importer creates a cross-layer dependency.

Returns:

Type Description
PropertyFamily

PropertyFamily.TEXT for *_text components and

PropertyFamily

link, PropertyFamily.BORDER for border, and

PropertyFamily

PropertyFamily.BACKGROUND for every other component.

colorsense.TokenSemanticRole

Bases: StrEnum

Semantic role inferred for a declared design token (CSS custom property).

Attributes:

Name Type Description
BRAND_PRIMARY

Primary brand color.

BRAND_SECONDARY

Secondary brand color.

BRAND_ACCENT

Accent brand color.

INTERACTIVE

Interactive-element color (links, controls).

SURFACE_BASE

Base page/surface background.

SURFACE_RAISED

Raised surface background (cards, modals).

TEXT_BODY

Body text color.

NEUTRAL

Neutral/gray with no specific role.

BORDER

Border/divider color.

TEXT_ON

Foreground meant to sit on a colored fill (an "on" color).

STATUS

Status color (success/warning/error/info).

IGNORE

Not a meaningful palette token (filtered out).

Configuration

colorsense.Config pydantic-model

Bases: BaseModel

The fully-loaded palette configuration plus token helper methods.

Show JSON schema:
{
  "$defs": {
    "AnchorRange": {
      "description": "Inclusive ``[low, high]`` anchor-step range for a scale convention.",
      "properties": {
        "low": {
          "title": "Low",
          "type": "integer"
        },
        "high": {
          "title": "High",
          "type": "integer"
        }
      },
      "required": [
        "low",
        "high"
      ],
      "title": "AnchorRange",
      "type": "object"
    },
    "ChannelRoute": {
      "description": "One arm of ``UsageIntentOrChannel``: routes a token to a non-palette channel.\n\nChannel routes (``text_on`` / ``status`` / ``ignore``) carry no usage\ndistribution: the token is reported on its side-channel, not ranked by usage.",
      "properties": {
        "channel": {
          "title": "Channel",
          "type": "string"
        }
      },
      "required": [
        "channel"
      ],
      "title": "ChannelRoute",
      "type": "object"
    },
    "CircleBadgeConfig": {
      "description": "Recognizes small recurring clickable circular chips as badges.\n\nA perfect circle (``rounded-full`` with ``width == height``) is NOT a pill \u2014 the pill\ntest demands ``width > height`` \u2014 so the badge geometry rule never matches it, and a\nsmall circular chip with no text node (an icon-only corner badge) falls through to the\ncard detector and leaks its accent into ``surface``. This family promotes a small circle\nto ``badge`` ONLY when it is clickable, paints a fill, and RECURS as a group of at least\n``min_siblings`` structurally-similar siblings \u2014 the signature of a repeated UI chip\n(supabase's 54 black corner badges), as opposed to a lone decorative dot (a single\n``rounded-full`` divot, kept out of every role) or a one-off clickable status dot (whose\ncolor already wins ``link`` from its text channel \u2014 the recurrence gate is what protects\nthat lone dot from being wrongly promoted to ``action``). No ``has_text`` gate, because\nthese chips carry an icon, not a text node.",
      "properties": {
        "max_h_px": {
          "exclusiveMinimum": 0.0,
          "title": "Max H Px",
          "type": "number"
        },
        "min_siblings": {
          "minimum": 1,
          "title": "Min Siblings",
          "type": "integer"
        },
        "votes": {
          "additionalProperties": {
            "type": "number"
          },
          "title": "Votes",
          "type": "object"
        }
      },
      "required": [
        "max_h_px",
        "min_siblings",
        "votes"
      ],
      "title": "CircleBadgeConfig",
      "type": "object"
    },
    "ComponentClassifierConfig": {
      "description": "The ``component_classifier`` config domain.",
      "properties": {
        "softmax_temperature": {
          "exclusiveMinimum": 0.0,
          "title": "Softmax Temperature",
          "type": "number"
        },
        "min_component_prob": {
          "maximum": 1.0,
          "minimum": 0.0,
          "title": "Min Component Prob",
          "type": "number"
        },
        "semantic_tags": {
          "items": {
            "$ref": "#/$defs/VoteRule"
          },
          "title": "Semantic Tags",
          "type": "array"
        },
        "geometry": {
          "$ref": "#/$defs/GeometryConfig"
        },
        "class_tokens": {
          "items": {
            "$ref": "#/$defs/VoteRule"
          },
          "title": "Class Tokens",
          "type": "array"
        },
        "interactivity": {
          "items": {
            "$ref": "#/$defs/WhenRule"
          },
          "title": "Interactivity",
          "type": "array"
        },
        "border_presence": {
          "$ref": "#/$defs/PresenceRule"
        },
        "text_presence": {
          "$ref": "#/$defs/PresenceRule"
        },
        "repetition": {
          "$ref": "#/$defs/RepetitionConfig"
        },
        "circle_badge": {
          "$ref": "#/$defs/CircleBadgeConfig"
        },
        "page_canvas_fallback": {
          "$ref": "#/$defs/PageCanvasFallbackConfig"
        },
        "third_party": {
          "$ref": "#/$defs/ThirdPartyConfig"
        },
        "suppressors": {
          "additionalProperties": {
            "$ref": "#/$defs/Suppressor"
          },
          "title": "Suppressors",
          "type": "object"
        },
        "brand_components": {
          "items": {
            "type": "string"
          },
          "title": "Brand Components",
          "type": "array"
        },
        "contrast_relabel": {
          "$ref": "#/$defs/ContrastRelabelConfig"
        }
      },
      "required": [
        "softmax_temperature",
        "min_component_prob",
        "semantic_tags",
        "geometry",
        "class_tokens",
        "interactivity",
        "border_presence",
        "text_presence",
        "repetition",
        "circle_badge",
        "page_canvas_fallback",
        "third_party",
        "suppressors",
        "brand_components",
        "contrast_relabel"
      ],
      "title": "ComponentClassifierConfig",
      "type": "object"
    },
    "ContrastRelabelConfig": {
      "description": "Thresholds for the CTA-label contrast relabel (see ``classify.components``).\n\nA non-anchor clickable element whose label text sits on its own distinct interactive\nfill \u2014 legible against that fill but illegible on the page canvas \u2014 is a CTA *label*,\nnot an inline link: its text-channel ``link`` vote is relabeled to ``cta_text``\n(channel-preserving). The predicate compares the element's composited ``effective_bg``\nagainst the derived page canvas with ``ciede2000`` and tests text legibility with the\nWCAG ``contrast_ratio``. Both thresholds are principled constants, not panel-fitted:\n``wcag_min_contrast`` is the WCAG 2.x legibility floor and ``canvas_delta_e`` is the\nperceptual just-noticeable-difference floor that defines color identity.",
      "properties": {
        "wcag_min_contrast": {
          "exclusiveMinimum": 1.0,
          "maximum": 21.0,
          "title": "Wcag Min Contrast",
          "type": "number"
        },
        "canvas_delta_e": {
          "exclusiveMinimum": 0.0,
          "title": "Canvas Delta E",
          "type": "number"
        }
      },
      "required": [
        "wcag_min_contrast",
        "canvas_delta_e"
      ],
      "title": "ContrastRelabelConfig",
      "type": "object"
    },
    "GeometryConfig": {
      "description": "The geometry feature family: thresholds plus positional rules.",
      "properties": {
        "thresholds": {
          "$ref": "#/$defs/GeometryThresholds"
        },
        "rules": {
          "items": {
            "$ref": "#/$defs/WhenRule"
          },
          "title": "Rules",
          "type": "array"
        }
      },
      "required": [
        "thresholds",
        "rules"
      ],
      "title": "GeometryConfig",
      "type": "object"
    },
    "GeometryThresholds": {
      "description": "Geometry thresholds (fractions of the viewport, or pixels).",
      "properties": {
        "top_band": {
          "title": "Top Band",
          "type": "number"
        },
        "bottom_band": {
          "title": "Bottom Band",
          "type": "number"
        },
        "full_width": {
          "title": "Full Width",
          "type": "number"
        },
        "short_h": {
          "title": "Short H",
          "type": "number"
        },
        "hero_min_h": {
          "title": "Hero Min H",
          "type": "number"
        },
        "sticky_top_px": {
          "title": "Sticky Top Px",
          "type": "number"
        },
        "small_area": {
          "title": "Small Area",
          "type": "number"
        },
        "badge_max_h_px": {
          "title": "Badge Max H Px",
          "type": "number"
        }
      },
      "required": [
        "top_band",
        "bottom_band",
        "full_width",
        "short_h",
        "hero_min_h",
        "sticky_top_px",
        "small_area",
        "badge_max_h_px"
      ],
      "title": "GeometryThresholds",
      "type": "object"
    },
    "MatchType": {
      "description": "How a name rule's ``match`` string is compared against a token name.",
      "enum": [
        "substring",
        "exact",
        "regex"
      ],
      "title": "MatchType",
      "type": "string"
    },
    "NameRule": {
      "description": "A single token-name -> semantic-role rule.",
      "properties": {
        "match": {
          "title": "Match",
          "type": "string"
        },
        "role": {
          "$ref": "#/$defs/TokenSemanticRole"
        },
        "weight": {
          "minimum": 0.0,
          "title": "Weight",
          "type": "number"
        },
        "match_type": {
          "$ref": "#/$defs/MatchType",
          "default": "substring"
        }
      },
      "required": [
        "match",
        "role",
        "weight"
      ],
      "title": "NameRule",
      "type": "object"
    },
    "PageCanvasFallbackConfig": {
      "description": "Fallback for sites whose canonical canvas (``html``/``body``/``main``) is transparent.\n\nModern utility-CSS sites (e.g. shadcn) leave ``html``/``body``/``main`` with\n``background: transparent`` and paint the white page surface on a single\nfull-viewport ``<div>`` instead. That div's only ``page_bg`` signal is a few weak\nclass-token votes, which the geometry ``hero_bg`` vote and the repetition ``card_bg``\nvote bury in the bg-channel softmax \u2014 so the real page color never reaches the ``page``\nrole. When (and only when) the canonical canvas paints no opaque background, the\nclassifier treats the largest viewport-spanning opaque-bg element near the top of the\npage as the page canvas: it injects a ``page_bg`` prior and removes the ``suppress``\ncomponents (the competing hero/card votes) from that one element. Opaque-body sites\nnever trigger this, so they are untouched. The full-width and top-band gates reuse the\ngeometry thresholds (``full_width``, ``top_band``), so this is a fraction-of-viewport\ntest, not absolute pixels.",
      "properties": {
        "page_bg_vote": {
          "minimum": 0.0,
          "title": "Page Bg Vote",
          "type": "number"
        },
        "color_match_delta_e": {
          "minimum": 0.0,
          "title": "Color Match Delta E",
          "type": "number"
        },
        "suppress": {
          "items": {
            "type": "string"
          },
          "title": "Suppress",
          "type": "array"
        }
      },
      "required": [
        "page_bg_vote",
        "color_match_delta_e",
        "suppress"
      ],
      "title": "PageCanvasFallbackConfig",
      "type": "object"
    },
    "PresenceRule": {
      "description": "A presence-gated feature family: ``{votes: {component: weight}}``.\n\nApplied when a structural fact holds for an element (e.g. it paints a border, or it\nhas direct text content) \u2014 there is no ``match``/``when`` string because the gating\npredicate is the feature family itself (fixed in ``classify.components``).",
      "properties": {
        "votes": {
          "additionalProperties": {
            "type": "number"
          },
          "title": "Votes",
          "type": "object"
        }
      },
      "required": [
        "votes"
      ],
      "title": "PresenceRule",
      "type": "object"
    },
    "RelationalModifier": {
      "description": "A regex modifier that reroutes a token to ``text-on-<base>``.\n\nThe ``pattern`` must compile and contain a named capture group ``base`` \u2014 enforced at\nload so a typo'd pattern fails loudly instead of becoming a rule that silently never\nreroutes anything (``match_relational`` skips matches without a ``base`` group).",
      "properties": {
        "pattern": {
          "title": "Pattern",
          "type": "string"
        },
        "type": {
          "title": "Type",
          "type": "string"
        },
        "weight": {
          "minimum": 0.0,
          "title": "Weight",
          "type": "number"
        }
      },
      "required": [
        "pattern",
        "type",
        "weight"
      ],
      "title": "RelationalModifier",
      "type": "object"
    },
    "RepetitionConfig": {
      "description": "The repetition (card detector) feature family.",
      "properties": {
        "min_siblings": {
          "title": "Min Siblings",
          "type": "integer"
        },
        "requires_any": {
          "items": {
            "type": "string"
          },
          "title": "Requires Any",
          "type": "array"
        },
        "votes": {
          "additionalProperties": {
            "type": "number"
          },
          "title": "Votes",
          "type": "object"
        }
      },
      "required": [
        "min_siblings",
        "requires_any",
        "votes"
      ],
      "title": "RepetitionConfig",
      "type": "object"
    },
    "ScaleDetectionConfig": {
      "description": "Numbered-scale detection settings.",
      "properties": {
        "enabled": {
          "title": "Enabled",
          "type": "boolean"
        },
        "number_pattern": {
          "title": "Number Pattern",
          "type": "string"
        },
        "chromatic_families": {
          "items": {
            "type": "string"
          },
          "title": "Chromatic Families",
          "type": "array"
        },
        "neutral_families": {
          "items": {
            "type": "string"
          },
          "title": "Neutral Families",
          "type": "array"
        },
        "anchor_ranges": {
          "additionalProperties": {
            "$ref": "#/$defs/AnchorRange"
          },
          "title": "Anchor Ranges",
          "type": "object"
        },
        "base_weight": {
          "exclusiveMinimum": 0.0,
          "title": "Base Weight",
          "type": "number"
        },
        "scale_present_confidence_boost": {
          "minimum": 0.0,
          "title": "Scale Present Confidence Boost",
          "type": "number"
        }
      },
      "required": [
        "enabled",
        "number_pattern",
        "chromatic_families",
        "neutral_families",
        "anchor_ranges",
        "base_weight",
        "scale_present_confidence_boost"
      ],
      "title": "ScaleDetectionConfig",
      "type": "object"
    },
    "Suppressor": {
      "description": "A multiplicative veto applied after vote summation.\n\n``applies_to`` is restricted to the two scopes the classifier implements:\n``\"all\"`` (every accumulated vote) or ``\"brand_components\"`` (only the\nconfigured brand components, on third-party elements).",
      "properties": {
        "factor": {
          "minimum": 0.0,
          "title": "Factor",
          "type": "number"
        },
        "applies_to": {
          "enum": [
            "all",
            "brand_components"
          ],
          "title": "Applies To",
          "type": "string"
        }
      },
      "required": [
        "factor",
        "applies_to"
      ],
      "title": "Suppressor",
      "type": "object"
    },
    "ThirdPartyConfig": {
      "description": "The origin / third-party feature family.",
      "properties": {
        "votes_iframe": {
          "additionalProperties": {
            "type": "number"
          },
          "title": "Votes Iframe",
          "type": "object"
        },
        "votes_cross_origin": {
          "additionalProperties": {
            "type": "number"
          },
          "title": "Votes Cross Origin",
          "type": "object"
        },
        "votes_shadow_host": {
          "additionalProperties": {
            "type": "number"
          },
          "title": "Votes Shadow Host",
          "type": "object"
        },
        "votes_vendor_match": {
          "additionalProperties": {
            "type": "number"
          },
          "title": "Votes Vendor Match",
          "type": "object"
        },
        "vendor_prefixes": {
          "items": {
            "type": "string"
          },
          "title": "Vendor Prefixes",
          "type": "array"
        }
      },
      "required": [
        "votes_iframe",
        "votes_cross_origin",
        "votes_shadow_host",
        "votes_vendor_match",
        "vendor_prefixes"
      ],
      "title": "ThirdPartyConfig",
      "type": "object"
    },
    "TokenSemanticRole": {
      "description": "Semantic role inferred for a declared design token (CSS custom property).\n\nAttributes:\n    BRAND_PRIMARY: Primary brand color.\n    BRAND_SECONDARY: Secondary brand color.\n    BRAND_ACCENT: Accent brand color.\n    INTERACTIVE: Interactive-element color (links, controls).\n    SURFACE_BASE: Base page/surface background.\n    SURFACE_RAISED: Raised surface background (cards, modals).\n    TEXT_BODY: Body text color.\n    NEUTRAL: Neutral/gray with no specific role.\n    BORDER: Border/divider color.\n    TEXT_ON: Foreground meant to sit on a colored fill (an \"on\" color).\n    STATUS: Status color (success/warning/error/info).\n    IGNORE: Not a meaningful palette token (filtered out).",
      "enum": [
        "brand_primary",
        "brand_secondary",
        "brand_accent",
        "interactive",
        "surface_base",
        "surface_raised",
        "text_body",
        "neutral",
        "border",
        "text_on",
        "status",
        "ignore"
      ],
      "title": "TokenSemanticRole",
      "type": "string"
    },
    "TokenVocabularyConfig": {
      "description": "The ``token_vocabulary`` config domain.",
      "properties": {
        "namespace_prefixes": {
          "items": {
            "type": "string"
          },
          "title": "Namespace Prefixes",
          "type": "array"
        },
        "strip_trailing": {
          "items": {
            "type": "string"
          },
          "title": "Strip Trailing",
          "type": "array"
        },
        "known_system_confidence_boost": {
          "minimum": 0.0,
          "title": "Known System Confidence Boost",
          "type": "number"
        },
        "name_rules": {
          "items": {
            "$ref": "#/$defs/NameRule"
          },
          "title": "Name Rules",
          "type": "array"
        },
        "relational_modifiers": {
          "items": {
            "$ref": "#/$defs/RelationalModifier"
          },
          "title": "Relational Modifiers",
          "type": "array"
        },
        "scale_detection": {
          "$ref": "#/$defs/ScaleDetectionConfig"
        },
        "semantic_role_to_usage_intent_or_channel": {
          "additionalProperties": {
            "anyOf": [
              {
                "$ref": "#/$defs/UsageIntent"
              },
              {
                "$ref": "#/$defs/ChannelRoute"
              }
            ]
          },
          "propertyNames": {
            "$ref": "#/$defs/TokenSemanticRole"
          },
          "title": "Semantic Role To Usage Intent Or Channel",
          "type": "object"
        },
        "status_excluded_from_palette": {
          "title": "Status Excluded From Palette",
          "type": "boolean"
        }
      },
      "required": [
        "namespace_prefixes",
        "strip_trailing",
        "known_system_confidence_boost",
        "name_rules",
        "relational_modifiers",
        "scale_detection",
        "semantic_role_to_usage_intent_or_channel",
        "status_excluded_from_palette"
      ],
      "title": "TokenVocabularyConfig",
      "type": "object"
    },
    "UsageIntent": {
      "description": "One arm of ``UsageIntentOrChannel``: a normalized distribution over usage roles.\n\nExpresses where a semantic role's color is expected to be used, inferred\nfrom the token's name before the page is measured.",
      "properties": {
        "distribution": {
          "additionalProperties": {
            "type": "number"
          },
          "propertyNames": {
            "$ref": "#/$defs/UsageRole"
          },
          "title": "Distribution",
          "type": "object"
        }
      },
      "required": [
        "distribution"
      ],
      "title": "UsageIntent",
      "type": "object"
    },
    "UsageRole": {
      "description": "Developer-facing usage role \u2014 how a rendered color is used on the page.\n\nThe role taxonomy keys the role-keyed palette projection\n([`UsagePalette`][colorsense.UsagePalette]) and labels each\n[`Usage`][colorsense.Usage] slot of the color-keyed index. It splits the two axes the\nold 4-value usage taxonomy conflated \u2014 *which CSS property paints the color* (the\n[`PropertyFamily`][colorsense.PropertyFamily] rollup) versus *what kind of element it\nis* \u2014 so that, e.g., link text and CTA button backgrounds no longer share one slot.\n\nAttributes:\n    PAGE: The base canvas (the page background).\n    SURFACE: Raised content backgrounds: cards, modals, hero, inputs.\n    BANNER: Chrome-bar backgrounds: header, nav, footer.\n    CTA: The primary action background (CTA buttons).\n    ACTION: Secondary action backgrounds: secondary buttons, badges.\n    TEXT: Body/heading typography at every layer.\n    LINK: Link color (typography of anchors).\n    BORDER: Borders and dividers.",
      "enum": [
        "page",
        "surface",
        "banner",
        "cta",
        "action",
        "text",
        "link",
        "border"
      ],
      "title": "UsageRole",
      "type": "string"
    },
    "VoteRule": {
      "description": "A ``{match: <token>, votes: {component: weight}}`` rule.",
      "properties": {
        "match": {
          "title": "Match",
          "type": "string"
        },
        "votes": {
          "additionalProperties": {
            "type": "number"
          },
          "title": "Votes",
          "type": "object"
        }
      },
      "required": [
        "match",
        "votes"
      ],
      "title": "VoteRule",
      "type": "object"
    },
    "WhenRule": {
      "description": "A ``{when: <predicate>, votes: {component: weight}}`` rule.",
      "properties": {
        "when": {
          "title": "When",
          "type": "string"
        },
        "votes": {
          "additionalProperties": {
            "type": "number"
          },
          "title": "Votes",
          "type": "object"
        }
      },
      "required": [
        "when",
        "votes"
      ],
      "title": "WhenRule",
      "type": "object"
    }
  },
  "description": "The fully-loaded palette configuration plus token helper methods.",
  "properties": {
    "token_vocabulary": {
      "$ref": "#/$defs/TokenVocabularyConfig"
    },
    "component_classifier": {
      "$ref": "#/$defs/ComponentClassifierConfig"
    }
  },
  "required": [
    "token_vocabulary",
    "component_classifier"
  ],
  "title": "Config",
  "type": "object"
}

Config:

  • frozen: True

Fields:

  • token_vocabulary (TokenVocabularyConfig)
  • component_classifier (ComponentClassifierConfig)

match_name_rule

match_name_rule(
    name: str,
) -> tuple[TokenSemanticRole, float] | None

Match name against name_rules with exact > regex > substring precedence.

If a known system namespace prefix was present, the returned weight is multiplied by known_system_confidence_boost.

Parameters:

Name Type Description Default
name str

The token name to match (a leading -- is stripped first).

required

Returns:

Type Description
tuple[TokenSemanticRole, float] | None

The matched (role, weight) pair, or None when no rule matches.

detect_scale

detect_scale(name: str) -> ScaleInfo | None

Detect a trailing scale number and identify its family.

family is the remainder once the namespace and the trailing number are stripped.

Parameters:

Name Type Description Default
name str

The token name to inspect (a leading -- is stripped first).

required

Returns:

Type Description
ScaleInfo | None

A ScaleInfo describing the family and number, or None when scale

ScaleInfo | None

detection is disabled or no scale number is present.

match_relational

match_relational(name: str) -> RelationalInfo | None

Match relational_modifiers patterns against the post-strip name.

Parameters:

Name Type Description Default
name str

The token name to match (a leading -- is stripped first).

required

Returns:

Type Description
RelationalInfo | None

A RelationalInfo carrying the captured base token, the modifier type,

RelationalInfo | None

and its weight, or None when nothing matches.

colorsense.load_default_config

load_default_config() -> Config

Load the palette configuration bundled with the installed package.

Resolves data/palette_config.yaml from the package itself (via importlib.resources), so it works regardless of the current working directory and whether the package is installed editable, as a wheel, or zipped. This is what colorsense.analyze uses when no config_path is given.

Returns:

Type Description
Config

The validated Config loaded from the bundled YAML.

colorsense.load_config

load_config(path: str | Path) -> Config

Read, validate, and normalize the palette config YAML at path.

For the configuration shipped with the package, prefer load_default_config.

Parameters:

Name Type Description Default
path str | Path

Filesystem path to the palette config YAML.

required

Returns:

Type Description
Config

The validated Config parsed from path.

Raises:

Type Description
ValueError

If the file cannot be read, or wrapping malformed YAML / a top-level non-mapping.

ValidationError

On schema violations — never a bare KeyError.

Fetch policy & networking

colorsense.PolitenessPolicy

Gate, pace, and cache page renders on behalf of a consumer.

Parameters:

Name Type Description Default
user_agent str

Identifiable User-Agent sent on the wire for both the robots.txt GET and the page render itself (forwarded to the harvester, which sets it on the browser context). Defaults to DEFAULT_USER_AGENT.

DEFAULT_USER_AGENT
robots_agent str

The product token matched against robots.txt User-agent: groups by can_fetch. Kept separate from user_agent so site-specific robots groups still match — see DEFAULT_ROBOTS_AGENT (the default, "colorsense") for the prefix-matching rationale.

DEFAULT_ROBOTS_AGENT
respect_robots bool

When True (default), can_fetch consults robots.txt and fetch raises RobotsDisallowedError on a disallow. Set False to bypass the check entirely — the consumer then owns authorization. Disabling robots also disables Crawl-delay honoring (no robots.txt is ever fetched).

True
allow_file_urls bool

Whether fetch may render file:// URLs. False by default — file:// reads arbitrary local files, so it must be an explicit opt-in (the test suite opts in to render its local fixtures). When allowed, file:// still bypasses the robots gate and the rate limiter: it has no host and no robots concept. Schemes other than http/https/file are always rejected; rejections raise UnsupportedSchemeError.

False
request_filter RequestFilter | None

Optional synchronous or asynchronous predicate (RequestFilter) over every HTTP(S) request URL the browser makes while rendering — the navigation itself and all sub-resources (scripts, images, XHR/fetch issued by the page's own JS) — and over the policy's own robots.txt GET (the initial robots URL and each redirect hop it follows; a rejected hop aborts the fetch, which fails open as "no rules" while the navigation stays gated by this same filter browser-side). Returning False aborts that request. A sync predicate is invoked inline on the event loop's request path and must not block (cheap string checks only); an async predicate is awaited — the shipped colorsense.block_private_networks guard is async and resolves hostnames off-loop. This is the in-library mechanism against sub-resource SSRF: validating the navigation URL alone cannot stop the rendered page from requesting internal endpoints (e.g. 169.254.169.254). A predicate that raises fails closed (the request is aborted). What route interception cannot see is closed rather than filtered: configuring a filter also refuses every WebSocket connection outright (the handshake never goes out), and service workers are always blocked at context creation. None (default) installs no interception at all — zero overhead.

None
max_concurrent_renders int | None

Optional cap on simultaneous renders through this policy — the SECURITY.md §2 concurrency bound, shipped as a knob. None (default) is unbounded, the previous behavior. When set, an asyncio.Semaphore bounds concurrent harvester calls. Composition with the other gates: cache hits never take a slot (they return before the limiter), single-flight followers never take a slot (they await the leader's future), and the throttle/robots wait happens outside the slot — the semaphore wraps strictly the render itself, so a leader parked in a long Crawl-delay sleep does not starve renders to other hosts. The semaphore is created lazily inside the running event loop (and re-created if the policy is later used from a different loop), so one policy can serve sequential asyncio.run calls and two policies never share a limiter. Must be >= 1 when set.

None
min_interval float

Minimum seconds between same-host fetches (per-host rate limiter). When the host's robots.txt declares a Crawl-delay for this policy's robots_agent, the effective per-host interval is max(min_interval, crawl_delay) with the crawl delay capped at max_crawl_delay. The delay is learned from the robots.txt fetch itself, which is the host's first throttled request — so it applies from the second fetch to that host onward.

DEFAULT_MIN_INTERVAL
max_crawl_delay float

Upper bound (seconds) on an honored robots.txt Crawl-delay. Defaults to DEFAULT_MAX_CRAWL_DELAY (30.0) so a hostile or typo'd robots.txt cannot stall a pipeline arbitrarily; raise it to honor longer delays.

DEFAULT_MAX_CRAWL_DELAY
max_cache_entries int | None

Upper bound on the render cache (_cache), which holds full Harvest objects — the largest things this policy retains. When the cache would exceed this, the least-recently-used entry is evicted (the cache is an OrderedDict; a hit moves its key to the most-recently-used end). Defaults to DEFAULT_MAX_CACHE_ENTRIES. Pass 0 or None for an unbounded cache (the legacy grow-forever behavior — only sensible for short-lived runs).

DEFAULT_MAX_CACHE_ENTRIES
harvester Harvester

The render function, injectable for testing. Defaults to colorsense.harvest.harvest_page.

harvest_page
robots_loader RobotsLoader

Injectable async robots.txt retrieval seam — swapped out by the test suite so no real network is incurred.

_default_robots_loader
clock Clock

Synchronous time source, injectable for tests.

monotonic
sleeper Sleeper

Injectable async sleep seam — swapped out by the test suite so no real wall-clock delay is incurred.

sleep

can_fetch async

can_fetch(url: str) -> bool

Whether url may be fetched under this policy.

Non-network URLs (e.g. file:// fixtures) and a disabled robots check always return True. A missing/unreachable robots.txt permits fetching.

Parameters:

Name Type Description Default
url str

The URL whose robots.txt permission is checked.

required

Returns:

Type Description
bool

True if url may be fetched under this policy, else False.

fetch async

fetch(
    url: str,
    theme: Theme,
    config: Config,
    viewport: Viewport,
    *,
    browser: SharedBrowser | None = None,
) -> Harvest

Return a Harvest for url/theme/viewport, politely.

browser is an optional shared-browser handle forwarded to the harvester verbatim — the policy itself knows nothing about browser lifecycle (neither launching nor closing). colorsense.analyze passes one handle to all of a call's theme fetches so they share a single Chromium launch; the handle is lazy, so a fetch served from the cache (or coalesced onto an in-flight leader) never triggers a launch. The cache key is unchanged (URL + theme + viewport): sharing a browser does not change what is rendered, only where the context lives.

The URL scheme is validated first — before even the cache lookup, so a previously cached file:// harvest can never be served by a policy that forbids file URLs (raises UnsupportedSchemeError; see allow_file_urls).

Cache hits return immediately (no robots check, no throttle, no render) and mark the entry most-recently-used. Otherwise the per-host rate limiter is applied, the robots gate is enforced (its robots.txt GET is the first throttled request to the host), and the harvester awaited; the result is cached (LRU-evicting the least-recently-used entry if the cache is bounded and now full) before return.

Concurrent misses for the same key are coalesced (single-flight): the first caller becomes the leader and runs exactly one throttle → robots gate → render; any caller that arrives while that render is in flight becomes a follower, awaiting the leader's result instead of launching a redundant headless render. All followers receive the leader's Harvest — or, if the leader's gate/render fails, the same exception (the failure is not cached, and the next fetch re-renders). Distinct keys never share, so unrelated renders still run in parallel.

Cancellation does not fan out like a failure: if the leader's task is cancelled (e.g. its caller's analyze(max_total_seconds=...) deadline expires, or its HTTP client disconnects in a server), followers never inherit the CancelledError. They re-elect instead — exactly one follower becomes the new leader (re-running throttle → robots gate → render) and the rest follow it; if the render somehow completed first, the re-checking loop serves it from the cache. A follower whose own task is cancelled still raises CancelledError normally, and the cancelled leader still propagates its own CancelledError to its caller.

Parameters:

Name Type Description Default
url str

Page to fetch. Its scheme is validated before anything else.

required
theme Theme

The theme to render under (part of the cache key).

required
config Config

The loaded palette configuration forwarded to the harvester.

required
viewport Viewport

The viewport to render at (part of the cache key).

required
browser SharedBrowser | None

Optional shared-browser handle forwarded to the harvester verbatim; the policy never launches or closes it.

None

Returns:

Type Description
Harvest

The Harvest for url/theme/viewport — freshly rendered, served from

Harvest

the cache, or coalesced onto an in-flight render of the same key.

Raises:

Type Description
UnsupportedSchemeError

When url's scheme is not fetchable under this policy.

RobotsDisallowedError

When robots.txt disallows url and respect_robots is set.

colorsense.block_private_networks

block_private_networks(
    *,
    allowed_hosts: Iterable[str] | None = None,
    resolver: Resolver = _default_resolver,
    ttl: float = DEFAULT_GUARD_TTL_SECONDS,
    max_entries: int = DEFAULT_GUARD_MAX_ENTRIES,
    clock: Clock = monotonic,
    resolve_timeout: float = DEFAULT_GUARD_RESOLVE_TIMEOUT_SECONDS,
) -> Callable[[str], Awaitable[bool]]

Build a request_filter predicate that rejects non-public destinations.

The returned async predicate (await guard(url) -> bool; True permits, False aborts; only usable under a running event loop, as the request_filter seams are) is meant for PolitenessPolicy(request_filter=...), where the library applies it to every http(s) URL the browser requests while rendering — the navigation, every redirect hop, and all sub-resources, including the page's own fetch/XHR calls — and to the policy's own robots.txt GET, whose initial URL and every redirect Location are vetted before each request goes out. The two browser network paths Playwright's context.route cannot intercept are closed rather than filtered: WebSocket connections are refused outright whenever a request_filter is configured (the handshake never reaches the network), and service workers are always blocked at context creation — strictly stronger than per-URL vetting for both. It implements the SECURITY.md §1 egress-filter item:

  • only http(s) URLs pass; URLs carrying userinfo (user:pass@host) are rejected;
  • the hostname is resolved (stdlib getaddrinfo; IP literals pass through without a network round trip) and the URL is rejected if any resolved address is non-public: loopback, RFC 1918/private, link-local (including the 169.254.169.254 cloud metadata endpoint), CGNAT 100.64.0.0/10, unspecified, multicast, reserved, and their IPv6 equivalents (IPv6 zone suffixes are stripped before classification);
  • malformed URLs, resolution failures, and empty resolutions all fail closed.

The library treats a raising predicate as fail-closed, but this guard never relies on that — it catches its own failure modes and returns False explicitly.

Honest residual gap: a URL-string predicate cannot fully defeat DNS rebinding — Chromium resolves hostnames independently when it connects, so a hostname can flip from public to internal between this check and the connection. Network isolation of the browser environment remains the primary control per SECURITY.md; this filter is defense in depth. Resolution runs off the event loop on a small thread pool the predicate itself owns (GUARD_RESOLVER_MAX_WORKERS threads, created lazily on the first cache miss and kept for the predicate's lifetime) — never the loop's shared default to_thread executor, so guard lookups cannot starve the pipeline's CPU phase or an embedding application's own thread-pool work. Concurrent misses for one host coalesce into a single lookup; fan-out to distinct novel hostnames beyond the pool size queues inside the guard's own pool (bounded threads, not one pinned thread per hostile hostname); each lookup is capped at resolve_timeout seconds, after which the URL fails closed and the negative verdict is cached. Verdicts land in a per-hostname TTL+LRU cache (negative verdicts cached too) — so a slow resolver costs bounded guard-pool time plus latency for that host only, never a loop stall. Honest limit: a timed-out lookup's thread still runs the resolver to completion inside the pool — the timeout bounds the caller's wait, not the thread's occupancy.

Single-event-loop-at-a-time contract: the coalescing machinery uses loop-bound asyncio.Future\ s, so each returned predicate must only be used from one event loop at a time. Reusing one predicate sequentially across loops (e.g. back-to-back asyncio.run calls) is supported — when idle it re-binds to the new loop and keeps its verdict cache (the lookup pool itself is loop-independent and carries over unchanged). Concurrent use from multiple event loops raises RuntimeError (detected best-effort). Direct callers see that error; through request_filter it fails closed instead, so misuse there manifests as requests from the other loop being aborted. Create a separate predicate per loop for concurrent use.

Parameters:

Name Type Description Default
allowed_hosts Iterable[str] | None

Optional exact (lowercase-compared) hostname allowlist applied before resolution: a host not on the list is rejected, and a host on the list must still resolve to only-public addresses. The allowlist narrows the filter, never widens it.

None
resolver Resolver

host -> [addresses] seam, injectable for tests. Stays synchronous — the guard runs it on its own bounded lookup pool on a cache miss. Defaults to a blocking socket.getaddrinfo lookup; raising OSError fails closed.

_default_resolver
ttl float

Seconds a hostname's verdict is reused before re-resolving. Defaults to DEFAULT_GUARD_TTL_SECONDS (60).

DEFAULT_GUARD_TTL_SECONDS
max_entries int

LRU bound on the verdict cache. Defaults to DEFAULT_GUARD_MAX_ENTRIES (1024).

DEFAULT_GUARD_MAX_ENTRIES
clock Clock

Monotonic time source for the TTL, injectable for tests.

monotonic
resolve_timeout float

Seconds a single lookup may take before the URL fails closed (and the negative verdict is cached). Defaults to DEFAULT_GUARD_RESOLVE_TIMEOUT_SECONDS (10).

DEFAULT_GUARD_RESOLVE_TIMEOUT_SECONDS

Returns:

Type Description
Callable[[str], Awaitable[bool]]

An async request_filter predicate (await guard(url) -> bool).

colorsense.RequestFilter module-attribute

RequestFilter = (
    Callable[[str], bool] | Callable[[str], Awaitable[bool]]
)

An egress predicate over a request URL: True permits, False aborts.

Either a plain synchronous callable or an async one. A sync predicate is invoked inline on the event loop's request path, so it must not block (cheap string checks only); an async predicate is awaited, free to move slow work (e.g. DNS resolution) off the loop — the shipped colorsense.block_private_networks guard is async for exactly that reason. Both seams that apply a filter (the browser route handler and the robots loader) evaluate it through evaluate_request_filter, so raising — sync or async — always fails closed.

Errors

colorsense.RenderError

Bases: Exception

A page failed to render or to harvest cleanly.

Raised when the underlying browser engine cannot load the target URL — e.g. DNS resolution failure, connection refused, TLS error, navigation timeout, or any other Playwright navigation/render failure. The version-private Playwright exception is wrapped so consumers have a single, stable, documented type to catch instead of reaching into playwright._impl.

Also raised when the harvest itself fails on a hostile or degenerate page: a page that tampers with DOM APIs can make the in-page harvest payloads come back malformed (surfacing as KeyError/TypeError/ValueError/pydantic ValidationError), and a capture whose decoded dimensions exceed the decompression-bomb cap is rejected. All of these are wrapped into this one public type.

The original error is chained via __cause__ (raise ... from err).

Attributes:

Name Type Description
url

The offending URL that failed to render or harvest.

colorsense.RobotsDisallowedError

Bases: RuntimeError

Raised when robots.txt disallows a URL and the active policy respects it.

Attributes:

Name Type Description
url

The disallowed URL.

colorsense.UnsupportedSchemeError

Bases: ValueError

Raised when a URL's scheme is not one the policy will render.

Raised by PolitenessPolicy.fetch. Only http/https URLs are fetchable by default. file:// (a local-file-read primitive) is an explicit opt-in via PolitenessPolicy(allow_file_urls=True); every other scheme (ftp, data, javascript, scheme-less, ...) is always rejected.

Attributes:

Name Type Description
url

The offending URL whose scheme was rejected.

colorsense.AnalysisTimeoutError

Bases: TimeoutError

Raised by analyze when max_total_seconds expires.

Subclasses the builtin TimeoutError, so a generic except TimeoutError still catches it.

Attributes:

Name Type Description
url

The URL whose analysis exceeded the deadline.

max_total_seconds

The configured overall deadline, in seconds.