Implement the 031-quality-gates-hygiene feature that strengthens automated quality gates by adding pnpm audit to the check script, v8 coverage thresholds with per-directory auto-ratchet (domain 96%, adapters 71%, persistence 87%), Biome cognitive complexity enforcement (max 15), and keyboard accessibility for the combatant row, while cleaning up all blanket biome-ignore comments, refactoring 5 overly complex functions into smaller helpers, and codifying the early-enforcement principle in the constitution and CLAUDE.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-11 00:52:29 +01:00
parent 47da942b73
commit 69363d4f7d
19 changed files with 1265 additions and 163 deletions

View File

@@ -254,25 +254,36 @@ function formatConditionImmunities(
.join(", ");
}
function renderListItem(item: string | RawEntryObject): string | undefined {
if (typeof item === "string") {
return `${stripTags(item)}`;
}
if (item.name && item.entries) {
return `${stripTags(item.name)}: ${renderEntries(item.entries)}`;
}
return undefined;
}
function renderEntryObject(entry: RawEntryObject, parts: string[]): void {
if (entry.type === "list") {
for (const item of entry.items ?? []) {
const rendered = renderListItem(item);
if (rendered) parts.push(rendered);
}
} else if (entry.type === "item" && entry.name && entry.entries) {
parts.push(`${stripTags(entry.name)}: ${renderEntries(entry.entries)}`);
} else if (entry.entries) {
parts.push(renderEntries(entry.entries));
}
}
function renderEntries(entries: (string | RawEntryObject)[]): string {
const parts: string[] = [];
for (const entry of entries) {
if (typeof entry === "string") {
parts.push(stripTags(entry));
} else if (entry.type === "list") {
for (const item of entry.items ?? []) {
if (typeof item === "string") {
parts.push(`${stripTags(item)}`);
} else if (item.name && item.entries) {
parts.push(
`${stripTags(item.name)}: ${renderEntries(item.entries)}`,
);
}
}
} else if (entry.type === "item" && entry.name && entry.entries) {
parts.push(`${stripTags(entry.name)}: ${renderEntries(entry.entries)}`);
} else if (entry.entries) {
parts.push(renderEntries(entry.entries));
} else {
renderEntryObject(entry, parts);
}
}
return parts.join(" ");

View File

@@ -354,6 +354,35 @@ function InitiativeDisplay({
);
}
function rowBorderClass(
isActive: boolean,
isConcentrating: boolean | undefined,
): string {
if (isActive) return "border-l-2 border-l-accent bg-accent/10";
if (isConcentrating) return "border-l-2 border-l-purple-400";
return "border-l-2 border-l-transparent";
}
function concentrationIconClass(
isConcentrating: boolean | undefined,
dimmed: boolean,
): string {
if (!isConcentrating)
return "opacity-0 group-hover:opacity-50 text-muted-foreground";
return dimmed ? "opacity-50 text-purple-400" : "opacity-100 text-purple-400";
}
function activateOnKeyDown(
handler: () => void,
): (e: { key: string; preventDefault: () => void }) => void {
return (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handler();
}
};
}
export function CombatantRow({
ref,
combatant,
@@ -402,21 +431,21 @@ export function CombatantRow({
}, [combatant.isConcentrating]);
return (
/* biome-ignore lint/a11y/useKeyWithClickEvents: row click opens stat block */
/* biome-ignore lint/a11y/noStaticElementInteractions: row click opens stat block */
/* biome-ignore lint/a11y/noStaticElementInteractions: role="button" is set conditionally when onShowStatBlock exists */
<div
ref={ref}
role={onShowStatBlock ? "button" : undefined}
tabIndex={onShowStatBlock ? 0 : undefined}
className={cn(
"group rounded-md pr-3 transition-colors",
isActive
? "border-l-2 border-l-accent bg-accent/10"
: combatant.isConcentrating
? "border-l-2 border-l-purple-400"
: "border-l-2 border-l-transparent",
rowBorderClass(isActive, combatant.isConcentrating),
isPulsing && "animate-concentration-pulse",
onShowStatBlock && "cursor-pointer",
)}
onClick={onShowStatBlock}
onKeyDown={
onShowStatBlock ? activateOnKeyDown(onShowStatBlock) : undefined
}
>
<div className="grid grid-cols-[2rem_3rem_1fr_auto_auto_2rem] items-center gap-3 py-2">
{/* Concentration */}
@@ -430,20 +459,18 @@ export function CombatantRow({
aria-label="Toggle concentration"
className={cn(
"flex w-full items-center justify-center self-stretch -my-2 -ml-[2px] pl-[14px] transition-opacity hover:text-hover-neutral hover:opacity-100",
combatant.isConcentrating
? dimmed
? "opacity-50 text-purple-400"
: "opacity-100 text-purple-400"
: "opacity-0 group-hover:opacity-50 text-muted-foreground",
concentrationIconClass(combatant.isConcentrating, dimmed),
)}
>
<Brain size={16} />
</button>
{/* Initiative */}
{/* biome-ignore lint/a11y/useKeyWithClickEvents: stopPropagation wrapper */}
{/* biome-ignore lint/a11y/noStaticElementInteractions: stopPropagation wrapper */}
<div onClick={(e) => e.stopPropagation()}>
{/* biome-ignore lint/a11y/noStaticElementInteractions: stopPropagation wrapper for interactive children */}
<div
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<InitiativeDisplay
initiative={initiative}
combatantId={id}
@@ -478,21 +505,21 @@ export function CombatantRow({
</div>
{/* AC */}
{/* biome-ignore lint/a11y/useKeyWithClickEvents: stopPropagation wrapper */}
{/* biome-ignore lint/a11y/noStaticElementInteractions: stopPropagation wrapper */}
{/* biome-ignore lint/a11y/noStaticElementInteractions: stopPropagation wrapper for interactive children */}
<div
className={cn(dimmed && "opacity-50")}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<AcDisplay ac={combatant.ac} onCommit={(v) => onSetAc(id, v)} />
</div>
{/* HP */}
{/* biome-ignore lint/a11y/useKeyWithClickEvents: stopPropagation wrapper */}
{/* biome-ignore lint/a11y/noStaticElementInteractions: stopPropagation wrapper */}
{/* biome-ignore lint/a11y/noStaticElementInteractions: stopPropagation wrapper for interactive children */}
<div
className="flex items-center gap-1"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<ClickableHp
currentHp={currentHp}

View File

@@ -18,6 +18,76 @@ export function saveEncounter(encounter: Encounter): void {
}
}
function validateAc(value: unknown): number | undefined {
return typeof value === "number" && Number.isInteger(value) && value >= 0
? value
: undefined;
}
function validateConditions(value: unknown): ConditionId[] | undefined {
if (!Array.isArray(value)) return undefined;
const valid = value.filter(
(v): v is ConditionId =>
typeof v === "string" && VALID_CONDITION_IDS.has(v),
);
return valid.length > 0 ? valid : undefined;
}
function validateCreatureId(value: unknown) {
return typeof value === "string" && value.length > 0
? creatureId(value)
: undefined;
}
function validateHp(
rawMaxHp: unknown,
rawCurrentHp: unknown,
): { maxHp: number; currentHp: number } | undefined {
if (
typeof rawMaxHp !== "number" ||
!Number.isInteger(rawMaxHp) ||
rawMaxHp < 1
) {
return undefined;
}
const validCurrentHp =
typeof rawCurrentHp === "number" &&
Number.isInteger(rawCurrentHp) &&
rawCurrentHp >= 0 &&
rawCurrentHp <= rawMaxHp;
return {
maxHp: rawMaxHp,
currentHp: validCurrentHp ? rawCurrentHp : rawMaxHp,
};
}
function rehydrateCombatant(c: unknown) {
const entry = c as Record<string, unknown>;
const base = {
id: combatantId(entry.id as string),
name: entry.name as string,
initiative:
typeof entry.initiative === "number" ? entry.initiative : undefined,
};
const shared = {
...base,
ac: validateAc(entry.ac),
conditions: validateConditions(entry.conditions),
isConcentrating: entry.isConcentrating === true ? true : undefined,
creatureId: validateCreatureId(entry.creatureId),
};
const hp = validateHp(entry.maxHp, entry.currentHp);
return hp ? { ...shared, ...hp } : shared;
}
function isValidCombatantEntry(c: unknown): boolean {
if (typeof c !== "object" || c === null || Array.isArray(c)) return false;
const entry = c as Record<string, unknown>;
return typeof entry.id === "string" && typeof entry.name === "string";
}
export function loadEncounter(): Encounter | null {
try {
const raw = localStorage.getItem(STORAGE_KEY);
@@ -45,82 +115,9 @@ export function loadEncounter(): Encounter | null {
};
}
for (const c of combatants) {
if (typeof c !== "object" || c === null || Array.isArray(c)) return null;
const entry = c as Record<string, unknown>;
if (typeof entry.id !== "string") return null;
if (typeof entry.name !== "string") return null;
}
if (!combatants.every(isValidCombatantEntry)) return null;
const rehydrated = combatants.map((c) => {
const entry = c as Record<string, unknown>;
const base = {
id: combatantId(entry.id as string),
name: entry.name as string,
initiative:
typeof entry.initiative === "number" ? entry.initiative : undefined,
};
// Validate AC field
const ac = entry.ac;
const validAc =
typeof ac === "number" && Number.isInteger(ac) && ac >= 0
? ac
: undefined;
// Validate conditions field
const rawConditions = entry.conditions;
const validConditions: ConditionId[] | undefined = Array.isArray(
rawConditions,
)
? (rawConditions.filter(
(v): v is ConditionId =>
typeof v === "string" && VALID_CONDITION_IDS.has(v),
) as ConditionId[])
: undefined;
const conditions =
validConditions && validConditions.length > 0
? validConditions
: undefined;
// Validate isConcentrating field
const isConcentrating = entry.isConcentrating === true ? true : undefined;
// Validate creatureId field
const rawCreatureId = entry.creatureId;
const validCreatureId =
typeof rawCreatureId === "string" && rawCreatureId.length > 0
? creatureId(rawCreatureId)
: undefined;
// Validate and attach HP fields if valid
const maxHp = entry.maxHp;
const currentHp = entry.currentHp;
if (typeof maxHp === "number" && Number.isInteger(maxHp) && maxHp >= 1) {
const validCurrentHp =
typeof currentHp === "number" &&
Number.isInteger(currentHp) &&
currentHp >= 0 &&
currentHp <= maxHp;
return {
...base,
ac: validAc,
conditions,
isConcentrating,
creatureId: validCreatureId,
maxHp,
currentHp: validCurrentHp ? currentHp : maxHp,
};
}
return {
...base,
ac: validAc,
conditions,
isConcentrating,
creatureId: validCreatureId,
};
});
const rehydrated = combatants.map(rehydrateCombatant);
const result = createEncounter(
rehydrated,