validateCyberpunk function

List<GuardrailViolation> validateCyberpunk(
  1. ThemeSpec spec,
  2. GuardrailLimits lim,
  3. List<int> bgs
)

The family-specific guardrails (called from validateTheme for ThemeFamily.cyberpunk specs only):

  • cyberOperatorCue: the recorded fill-versus-outline cue is real. Digit and function faces are opaque plates (L ≤ cyberPlateMaxL) with labels lighter than the face; operator and = faces are opaque slabs (L ≥ cyberSlabMinL) with darker labels and ≥ 3:1 against the digit plate; operators are ≥ 200 heavier.
  • cyberEdge: the plate edge lines that carry the cue are opaque and ≥ 3:1 against their faces, 1–1.5 dp wide.
  • cyberKeyBoundary: every key group's face or opaque edge reaches 3:1 against every scene sample (WCAG 1.4.11).
  • cyberScene: the scene is a static cyberGrid with five colours, opaque stops, inks under their alpha ceilings and a tilt within ±20°; the panel scanline alpha is ≤ cyberPanelScanMaxAlpha (0 under the Accessible lock); keys are cut with chamfer 12–24 % and no generic state layer.

Implementation

List<GuardrailViolation> validateCyberpunk(
  ThemeSpec spec,
  GuardrailLimits lim,
  List<int> bgs,
) {
  final out = <GuardrailViolation>[];
  final p = spec.palette;
  final plates = {'digit': p.digit, 'function': p.function};
  final slabs = {'operator': p.operator, 'equals': p.equals};

  // cyberOperatorCue.
  for (final MapEntry(key: name, value: k) in plates.entries) {
    final l = argbToOklch(k.face | 0xFF000000).l;
    if (!_opaque(k.face) ||
        l > cyberPlateMaxL ||
        luminanceArgb(k.label | 0xFF000000) <= luminanceArgb(k.face)) {
      out.add(
        GuardrailViolation(
          GuardrailRule.cyberOperatorCue,
          l,
          cyberPlateMaxL,
          '$name plate (opaque, L ≤ $cyberPlateMaxL, lighter label)',
        ),
      );
    }
  }
  for (final MapEntry(key: name, value: k) in slabs.entries) {
    final l = argbToOklch(k.face | 0xFF000000).l;
    if (!_opaque(k.face) ||
        l < cyberSlabMinL ||
        luminanceArgb(k.label | 0xFF000000) >= luminanceArgb(k.face)) {
      out.add(
        GuardrailViolation(
          GuardrailRule.cyberOperatorCue,
          l,
          cyberSlabMinL,
          '$name slab (opaque, L ≥ $cyberSlabMinL, darker label)',
        ),
      );
    }
    final cr = contrastRatio(k.face | 0xFF000000, p.digit.face | 0xFF000000);
    if (cr < cyberSlabVsPlateRatio) {
      out.add(
        GuardrailViolation(
          GuardrailRule.cyberOperatorCue,
          cr,
          cyberSlabVsPlateRatio,
          '$name slab vs digit plate',
        ),
      );
    }
  }
  final bonus = spec.type.operatorWeight - spec.type.keyWeight;
  if (bonus < 200) {
    out.add(
      GuardrailViolation(
        GuardrailRule.cyberOperatorCue,
        bonus.toDouble(),
        200,
        'operator weight bonus',
      ),
    );
  }

  // cyberEdge.
  for (final MapEntry(key: name, value: k) in plates.entries) {
    final ratio = _opaque(k.edge) ? contrastRatio(k.edge, k.face) : 1.0;
    if (ratio < ratioLarge) {
      out.add(
        GuardrailViolation(GuardrailRule.cyberEdge, ratio, ratioLarge, name),
      );
    }
  }
  final width = spec.keyStyle.outlineWidthCdp;
  if (!cyberEdgeWidthCdp.contains(width)) {
    out.add(
      GuardrailViolation(
        GuardrailRule.cyberEdge,
        width / 100,
        cyberEdgeWidthCdp.lo / 100,
        'edge width (dp)',
      ),
    );
  }

  // cyberKeyBoundary.
  for (final MapEntry(key: name, value: k) in {...plates, ...slabs}.entries) {
    final face = _opaque(k.face) ? _minContrastOver(k.face, bgs) : 1.0;
    final edge = _opaque(k.edge) ? _minContrastOver(k.edge, bgs) : 1.0;
    if (face < ratioLarge && edge < ratioLarge) {
      out.add(
        GuardrailViolation(
          GuardrailRule.cyberKeyBoundary,
          face > edge ? face : edge,
          ratioLarge,
          name,
        ),
      );
    }
  }

  // cyberScene.
  void scene(String detail, [double measured = 0, double required = 0]) =>
      out.add(
        GuardrailViolation(
          GuardrailRule.cyberScene,
          measured,
          required,
          detail,
        ),
      );
  final bg = spec.background;
  if (bg.kind != BackgroundKind.cyberGrid) scene('kind ${bg.kind.name}');
  if (bg.colors.length != 5) {
    scene('colours', bg.colors.length.toDouble(), 5);
  } else {
    if (!_opaque(bg.colors[0]) || !_opaque(bg.colors[1])) scene('stops');
    for (final (i, cap) in [
      (2, cyberGridMaxAlpha),
      (3, cyberTraceMaxAlpha),
      (4, cyberScanMaxAlpha),
    ]) {
      final a = _alpha(bg.colors[i]);
      if (a > cap) scene('ink $i alpha', a.toDouble(), cap.toDouble());
    }
  }
  final angle = bg.angleTenths % 3600;
  if (angle > cyberMaxAngleTenths && angle < 3600 - cyberMaxAngleTenths) {
    scene('angle', angle.toDouble(), cyberMaxAngleTenths.toDouble());
  }
  if (bg.driftPeriodMs != 0) scene('drift', bg.driftPeriodMs.toDouble());
  final scanAlpha = _alpha(p.shadow);
  if (scanAlpha > cyberPanelScanMaxAlpha ||
      (spec.accessible && scanAlpha != 0)) {
    scene(
      'panel scanline alpha',
      scanAlpha.toDouble(),
      spec.accessible ? 0 : cyberPanelScanMaxAlpha.toDouble(),
    );
  }
  if (spec.keyShape.kind != KeyShapeKind.cut ||
      !cyberCornerPermille.contains(spec.keyShape.cornerPermille)) {
    scene('shape', spec.keyShape.cornerPermille.toDouble());
  }
  if (spec.keyStyle.stateLayerPermille != 0) scene('state layer');
  return out;
}