HextorUI
lib

The five scoring axes (FROZEN)

Axis keys, order, maxima and colour slots. Structure only — display copy lives with the block that renders it.

npx shadcn@latest add @hextor/axes

Source

/**
 * The five axes, in FROZEN order.
 *
 * Order, keys, maxima and colour slots are the product's visual identity: a
 * radar silhouette is only comparable between two people, or between one person
 * and their own six months ago, if none of them ever changes. Reordering this
 * array retroactively invalidates every Skill Card that has been shared. Treat
 * it as append-only — if the rubric changes, add an axis and bump
 * RUBRIC_VERSION; never reorder, never recolour, never repurpose a key.
 *
 * This table is deliberately STRUCTURE ONLY. Display copy lives with the block
 * that renders it (see `labels.ts` next to skill-radar), so the registry stays
 * usable outside Thai without forking the frozen part.
 */

export type AxisKey =
  | "correctness"
  | "test_quality"
  | "complexity"
  | "performance"
  | "code_quality";

export interface AxisScore {
  axis: AxisKey;
  points: number;
  max: number;
  /** Withheld when an axis cannot be scored honestly — e.g. perf on a dev-grade node. */
  withheld?: boolean;
  withheldReason?: string;
}

export interface AxisMeta {
  key: AxisKey;
  labelEn: string;
  max: number;
  /**
   * A `var()` reference rather than a Tailwind class on purpose: Tailwind v4's
   * scanner cannot see a class assembled at runtime, so `bg-${slot}` would
   * silently render unstyled. Consumed through `style`, it always resolves.
   * Backed by @hextor/viz-tokens.
   */
  cssVar: string;
}

export const AXES: AxisMeta[] = [
  { key: "correctness", labelEn: "Correctness", max: 40, cssVar: "var(--axis-correctness)" },
  { key: "test_quality", labelEn: "Test quality", max: 25, cssVar: "var(--axis-test-quality)" },
  { key: "complexity", labelEn: "Complexity", max: 15, cssVar: "var(--axis-complexity)" },
  { key: "performance", labelEn: "Performance", max: 15, cssVar: "var(--axis-performance)" },
  { key: "code_quality", labelEn: "Code quality", max: 5, cssVar: "var(--axis-code-quality)" },
];

export const AXIS_TOTAL = AXES.reduce((sum, a) => sum + a.max, 0);

export const RUBRIC_VERSION = "v1.0.0";

/**
 * These maxima must sum to 100 and must match the server rubric that persists
 * the scores; if they drift, the number on the radar is not the number in the
 * user's row.
 *
 * That invariant is asserted in this package's test suite, NOT thrown at module
 * load. A registry item is copied into someone else's repo — a top-level throw
 * would take down a consumer's build for a mistake they did not make and cannot
 * fix, and would do it at import time, before any error boundary exists.
 */

export function axisMeta(key: AxisKey): AxisMeta {
  const found = AXES.find((a) => a.key === key);
  if (!found) throw new Error(`unknown axis: ${key}`);
  return found;
}

Docs

Each axis names a CSS custom property from @hextor/viz-tokens rather than a Tailwind class, because Tailwind v4's scanner cannot see a class assembled at runtime — bg-${slot} compiles to nothing. The dependency on viz-tokens is declared so the palette can neither arrive by accident nor be silently omitted.

Unlike the upstream copy this table does NOT throw at module load when the maxima fail to sum to 100. A registry item is copied into your repo; a top-level throw would take down your build at import time for a mistake you did not make. The invariant is asserted in HextorUI's own test suite instead.

Dependencies

@hextor/viz-tokens