testsAndMisc/python_pkg/artgate/codes.py
Krzysztof kuhy Rudnicki da8ca6d827
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
artgate: deterministic exit-code gates for generated game art
Adds the route-independent spine of the planned art-generation bake-off: a
harness that decides whether generated art is well-formed, so "non-sloppy"
becomes machine-checkable instead of a judgement call.

Findings that shaped the design, all measured rather than assumed:

- Integer-scale invariance separates true pixel art (impurity 0.0000) from
  interpolated fakes (1.0000) exactly -- but passes trivially at scale 1, so
  it must never be trusted alone on native-resolution output.
- Gate ORDER is load-bearing. A soft-alpha image has no fully-opaque pixels,
  so an opaque-only colour count returns 0 and silently passes. ALPHA_NOT_BINARY
  now runs first and COLOR_COUNT fails closed on an empty opaque set.
- Counting must be over opaque RGB only: transparent pixels retain arbitrary
  RGB and inflated a naive RGBA count, rejecting correct art at 18/16.
- Declaring a gate was not enough to run it. hand_painted.toml shipped
  declaring SILHOUETTE_LOW_CONTRAST with no implementation, so it parsed, ran
  nothing, and reported a pass. load_spec now fails closed on unimplemented
  gates, result-only codes, and gates missing their config.

Thresholds are calibrated against 250 genuine 32x32 CC0 icons, not guessed:
p50=7 p90=13 p99=26 opaque colours; cap 32 rejects 0.0% of real art while raw
generator output measured 34403 colours. An initial cap of 16 wrongly rejected
a real DCSS icon -- re-derived from the corpus rather than nudged.

Verified: 57 tests, 100% branch coverage, zero suppressions, all hooks pass.
58/60 (96.7%) of real CC0 icons pass the full stack; both failures confirmed
true positives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9rQ12yZ7sJxiPQTCS5dUF
2026-08-15 22:20:36 +02:00

83 lines
2.7 KiB
Python

"""Reason-code vocabulary for art gates.
Single source of truth. Every gate failure, every rejects-store record and
every contact-sheet annotation keys off these constants, so that "feed
rejections back as negative examples" is a queryable fact next session rather
than a good intention.
Exit codes are deliberately three-valued: conflating a harness crash with an
art rejection would silently convert our own bugs into art failures and
poison both the rejects store and the bake-off metrics.
"""
from __future__ import annotations
from enum import Enum
from typing import Final
# All applicable gates passed.
EXIT_PASS: Final = 0
# The asset is well-formed but failed at least one gate.
EXIT_GATE_FAILURE: Final = 1
# Spec malformed or image unreadable -- a harness fault, never an art verdict.
EXIT_HARNESS_ERROR: Final = 2
class Code(str, Enum):
"""Machine-readable gate failure codes.
Values are SCREAMING_SNAKE so they survive a round trip through JSONL and
remain greppable with ``jq`` without a lookup table.
"""
OFF_PALETTE = "OFF_PALETTE"
TOO_MANY_COLORS = "TOO_MANY_COLORS"
NO_OPAQUE_PIXELS = "NO_OPAQUE_PIXELS"
ALPHA_NOT_BINARY = "ALPHA_NOT_BINARY"
CANVAS_SIZE = "CANVAS_SIZE"
MARGIN_TOO_SMALL = "MARGIN_TOO_SMALL"
EMPTY_CANVAS = "EMPTY_CANVAS"
SCALE_NOT_INVARIANT = "SCALE_NOT_INVARIANT"
OUTLINE_INCONSISTENT = "OUTLINE_INCONSISTENT"
SILHOUETTE_LOW_CONTRAST = "SILHOUETTE_LOW_CONTRAST"
SEAM_ENERGY = "SEAM_ENERGY"
QUADRANT_WEIGHT = "QUADRANT_WEIGHT"
LOOP_POPS = "LOOP_POPS"
STATIC_ANIMATION = "STATIC_ANIMATION"
LICENSE_NOT_PERMISSIVE = "LICENSE_NOT_PERMISSIVE"
LICENSE_AMBIGUOUS = "LICENSE_AMBIGUOUS"
def __str__(self) -> str:
"""Return the bare code, so f-strings do not leak ``Code.`` prefixes."""
return self.value
# Codes a spec may DECLARE. A code outside this set is either a result-only
# code (emitted by a gate, never requested) or not yet implemented.
#
# This set exists because declaring a gate must never be enough to make it run:
# a spec that names an unimplemented gate would parse cleanly, run nothing, and
# report a pass -- indistinguishable from art that genuinely passed. Keeping the
# declarable set explicit forces `load_spec` to fail closed instead.
DECLARABLE: Final = frozenset(
{
Code.ALPHA_NOT_BINARY,
Code.TOO_MANY_COLORS,
Code.OFF_PALETTE,
Code.CANVAS_SIZE,
Code.SCALE_NOT_INVARIANT,
Code.SILHOUETTE_LOW_CONTRAST,
}
)
# Codes a gate may EMIT but a spec may not request. ``NO_OPAQUE_PIXELS`` is the
# fail-closed result of the colour and palette gates, not a gate in its own right.
RESULT_ONLY: Final = frozenset(
{
Code.NO_OPAQUE_PIXELS,
Code.EMPTY_CANVAS,
}
)