feat: add evaluation evidence board
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { EvaluationEvidenceScene } from "./EvaluationEvidenceScene.js";
|
||||
|
||||
const scene = {
|
||||
id: "evaluation",
|
||||
number: 13,
|
||||
title: "Evaluation",
|
||||
claimClass: "evaluated" as const,
|
||||
evidencePointer: "Thesis Evaluation and Appendix C",
|
||||
stageTheme: "paper" as const,
|
||||
view: "evaluation" as const,
|
||||
beats: [],
|
||||
};
|
||||
|
||||
const beat = (id: string) => ({
|
||||
id,
|
||||
title: id,
|
||||
caption: `Caption for ${id}`,
|
||||
chatMode: "hidden" as const,
|
||||
chatTheme: "light" as const,
|
||||
evidencePresentation: "hidden" as const,
|
||||
figure: null,
|
||||
});
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
describe("EvaluationEvidenceScene", () => {
|
||||
it.each(["cohort", "validity", "findings"])("renders the persistent board for %s", (beatId) => {
|
||||
render(<EvaluationEvidenceScene scene={scene} beat={beat(beatId)} />);
|
||||
expect(screen.getByRole("group", { name: /evaluation evidence board/i })).toHaveAttribute(
|
||||
"data-evaluation-beat",
|
||||
beatId,
|
||||
);
|
||||
expect(screen.getByText("36")).toBeInTheDocument();
|
||||
expect(screen.getByText("27")).toBeInTheDocument();
|
||||
expect(screen.getByText("8")).toBeInTheDocument();
|
||||
expect(screen.getByText("1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the manual audit reconciliation and validity statement", () => {
|
||||
render(<EvaluationEvidenceScene scene={scene} beat={beat("validity")} />);
|
||||
expect(screen.getByText("7 automatic successes")).toBeInTheDocument();
|
||||
expect(screen.getByText("invalid as clean evidence")).toBeInTheDocument();
|
||||
expect(screen.getByText("3 automatic failures")).toBeInTheDocument();
|
||||
expect(screen.getByText("accepted from saved evidence")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Bounded longitudinal engineering evidence, not a controlled model comparison."),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders six labelled finding icons without ranking language", () => {
|
||||
render(<EvaluationEvidenceScene scene={scene} beat={beat("findings")} />);
|
||||
const findings = screen.getByRole("list", { name: /ux gaps exposed by trials/i });
|
||||
expect(findings.querySelectorAll("svg")).toHaveLength(6);
|
||||
for (const label of [
|
||||
"Schema discovery",
|
||||
"Repair hints",
|
||||
"Binding commands",
|
||||
"Output schemas",
|
||||
"Shell assumptions",
|
||||
"Source contamination",
|
||||
]) {
|
||||
expect(screen.getByText(label)).toBeInTheDocument();
|
||||
}
|
||||
expect(findings.textContent).not.toMatch(/%|success rate|leaderboard/i);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { FC } from "react";
|
||||
import { Cable, FileOutput, SearchCode, ShieldAlert, Terminal, Wrench, type LucideProps } from "lucide-react";
|
||||
import { StageCaption } from "../StageCaption.js";
|
||||
import type { SceneBeatDefinition, SceneDefinition } from "../storyboard.js";
|
||||
import { evaluationEvidence, isEvaluationBeatId, type EvaluationFindingIcon } from "./evaluation-evidence.js";
|
||||
|
||||
type EvaluationEvidenceSceneProps = {
|
||||
readonly scene: SceneDefinition;
|
||||
readonly beat: SceneBeatDefinition;
|
||||
};
|
||||
|
||||
const findingIcons: Record<EvaluationFindingIcon, FC<LucideProps>> = {
|
||||
schema: SearchCode,
|
||||
repair: Wrench,
|
||||
binding: Cable,
|
||||
output: FileOutput,
|
||||
shell: Terminal,
|
||||
contamination: ShieldAlert,
|
||||
};
|
||||
|
||||
export const EvaluationEvidenceScene: FC<EvaluationEvidenceSceneProps> = ({ scene, beat }) => {
|
||||
const beatId = isEvaluationBeatId(beat.id) ? beat.id : "cohort";
|
||||
|
||||
return (
|
||||
<>
|
||||
<StageCaption eyebrow={`Act III · ${scene.claimClass}`} title={scene.title}>
|
||||
<p>{beat.caption}</p>
|
||||
</StageCaption>
|
||||
<section
|
||||
className="evaluation-board"
|
||||
role="group"
|
||||
aria-label="evaluation evidence board"
|
||||
data-evaluation-beat={beatId}
|
||||
>
|
||||
<div className="evaluation-board__cohort" aria-label="campaign cohort equation">
|
||||
<span className="evaluation-board__cohort-total">{evaluationEvidence.totalTrials}</span>
|
||||
<span className="evaluation-board__cohort-label">audited trials</span>
|
||||
<span className="evaluation-board__cohort-equation" aria-label="cohort factors">
|
||||
{evaluationEvidence.cohortFactors.map((factor, index) => (
|
||||
<span className="evaluation-board__factor" key={factor.label}>
|
||||
{index > 0 ? " × " : ""}
|
||||
<strong>{factor.value}</strong> {factor.label}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ol className="evaluation-board__outcomes" aria-label="audited outcomes">
|
||||
{evaluationEvidence.outcomes.map((outcome) => (
|
||||
<li className={`evaluation-board__outcome evaluation-board__outcome--${outcome.kind}`} key={outcome.label}>
|
||||
<strong>{outcome.value}</strong>
|
||||
<span>{outcome.label}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
<div className="evaluation-board__audit" aria-label="automatic and manual audit reconciliation">
|
||||
<p className="evaluation-board__section-label">Automatic grading → manual audit</p>
|
||||
{evaluationEvidence.auditCorrections.map((correction) => (
|
||||
<div className="evaluation-board__audit-row" key={correction.automatic}>
|
||||
<span>{correction.automatic}</span>
|
||||
<span aria-hidden="true">→</span>
|
||||
<strong>{correction.audited}</strong>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<ol className="evaluation-board__findings" aria-label="UX gaps exposed by trials">
|
||||
{evaluationEvidence.findings.map((finding) => {
|
||||
const Icon = findingIcons[finding.icon];
|
||||
return (
|
||||
<li className="evaluation-board__finding" key={finding.icon}>
|
||||
<Icon aria-hidden="true" />
|
||||
<span>{finding.label}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
|
||||
<p className="evaluation-board__boundary">{evaluationEvidence.validityStatement}</p>
|
||||
</section>
|
||||
<p className="scene-body__evidence">{scene.evidencePointer}</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { evaluationEvidence, isEvaluationBeatId } from "./evaluation-evidence.js";
|
||||
|
||||
describe("evaluationEvidence", () => {
|
||||
it("preserves the exact audited trial projection", () => {
|
||||
expect(evaluationEvidence.totalTrials).toBe(36);
|
||||
expect(evaluationEvidence.outcomes).toEqual([
|
||||
{ value: 27, label: "clean product-path passes", kind: "pass" },
|
||||
{ value: 8, label: "invalid evaluation samples", kind: "invalid" },
|
||||
{ value: 1, label: "failure", kind: "fail" },
|
||||
]);
|
||||
expect(evaluationEvidence.auditCorrections).toEqual([
|
||||
{ automatic: "7 automatic successes", audited: "invalid as clean evidence" },
|
||||
{ automatic: "3 automatic failures", audited: "accepted from saved evidence" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves the validity boundary and six distinct findings", () => {
|
||||
expect(evaluationEvidence.validityStatement).toBe(
|
||||
"Bounded longitudinal engineering evidence, not a controlled model comparison.",
|
||||
);
|
||||
expect(evaluationEvidence.findings.map((finding) => finding.label)).toEqual([
|
||||
"Schema discovery",
|
||||
"Repair hints",
|
||||
"Binding commands",
|
||||
"Output schemas",
|
||||
"Shell assumptions",
|
||||
"Source contamination",
|
||||
]);
|
||||
expect(new Set(evaluationEvidence.findings.map((finding) => finding.icon)).size).toBe(6);
|
||||
expect(JSON.stringify(evaluationEvidence)).not.toMatch(/%|success rate|leaderboard|superior/i);
|
||||
});
|
||||
|
||||
it("recognizes only authored evaluation beats", () => {
|
||||
expect(["cohort", "validity", "findings"].every(isEvaluationBeatId)).toBe(true);
|
||||
expect(isEvaluationBeatId("unknown")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
export type EvaluationBeatId = "cohort" | "validity" | "findings";
|
||||
export type EvaluationFindingIcon = "schema" | "repair" | "binding" | "output" | "shell" | "contamination";
|
||||
|
||||
export type EvaluationEvidenceModel = {
|
||||
readonly cohortFactors: readonly { readonly value: string; readonly label: string }[];
|
||||
readonly totalTrials: 36;
|
||||
readonly outcomes: readonly { readonly value: number; readonly label: string; readonly kind: "pass" | "invalid" | "fail" }[];
|
||||
readonly auditCorrections: readonly { readonly automatic: string; readonly audited: string }[];
|
||||
readonly findings: readonly { readonly label: string; readonly icon: EvaluationFindingIcon }[];
|
||||
readonly validityStatement: string;
|
||||
};
|
||||
|
||||
export const evaluationEvidence: EvaluationEvidenceModel = {
|
||||
cohortFactors: [
|
||||
{ value: "2", label: "challenges" },
|
||||
{ value: "2", label: "hosted models" },
|
||||
{ value: "3", label: "profiles" },
|
||||
{ value: "3", label: "waves" },
|
||||
],
|
||||
totalTrials: 36,
|
||||
outcomes: [
|
||||
{ value: 27, label: "clean product-path passes", kind: "pass" },
|
||||
{ value: 8, label: "invalid evaluation samples", kind: "invalid" },
|
||||
{ value: 1, label: "failure", kind: "fail" },
|
||||
],
|
||||
// These are campaign-specific audit disagreements, not a general accuracy
|
||||
// measure for automatic grading.
|
||||
auditCorrections: [
|
||||
{ automatic: "7 automatic successes", audited: "invalid as clean evidence" },
|
||||
{ automatic: "3 automatic failures", audited: "accepted from saved evidence" },
|
||||
],
|
||||
findings: [
|
||||
{ label: "Schema discovery", icon: "schema" },
|
||||
{ label: "Repair hints", icon: "repair" },
|
||||
{ label: "Binding commands", icon: "binding" },
|
||||
{ label: "Output schemas", icon: "output" },
|
||||
{ label: "Shell assumptions", icon: "shell" },
|
||||
{ label: "Source contamination", icon: "contamination" },
|
||||
],
|
||||
validityStatement: "Bounded longitudinal engineering evidence, not a controlled model comparison.",
|
||||
};
|
||||
|
||||
export const isEvaluationBeatId = (value: string): value is EvaluationBeatId =>
|
||||
value === "cohort" || value === "validity" || value === "findings";
|
||||
@@ -80,6 +80,150 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.evaluation-board {
|
||||
--evaluation-ink: oklch(0.2 0.03 250);
|
||||
--evaluation-paper: oklch(0.96 0.018 85);
|
||||
--evaluation-rule: oklch(0.74 0.05 75);
|
||||
display: grid;
|
||||
grid-template-columns: minmax(14rem, 0.8fr) minmax(20rem, 1.2fr);
|
||||
gap: 0;
|
||||
margin: 0.25rem 0 0.5rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--evaluation-rule);
|
||||
border-radius: 0.35rem;
|
||||
background: var(--evaluation-paper);
|
||||
color: var(--evaluation-ink);
|
||||
box-shadow: 0 0.8rem 2rem rgb(20 25 35 / 12%);
|
||||
}
|
||||
|
||||
.evaluation-board__cohort,
|
||||
.evaluation-board__outcomes,
|
||||
.evaluation-board__audit,
|
||||
.evaluation-board__findings,
|
||||
.evaluation-board__boundary {
|
||||
transition: opacity 180ms ease, transform 180ms ease;
|
||||
}
|
||||
|
||||
.evaluation-board[data-evaluation-beat="cohort"] .evaluation-board__cohort,
|
||||
.evaluation-board[data-evaluation-beat="validity"] .evaluation-board__audit,
|
||||
.evaluation-board[data-evaluation-beat="findings"] .evaluation-board__findings {
|
||||
transform: translateY(-0.12rem);
|
||||
}
|
||||
|
||||
.evaluation-board__cohort {
|
||||
display: grid;
|
||||
align-content: center;
|
||||
gap: 0.2rem;
|
||||
padding: 1rem 1.15rem;
|
||||
border-right: 1px solid var(--evaluation-rule);
|
||||
}
|
||||
|
||||
.evaluation-board__cohort-total {
|
||||
font: 3.7rem/0.9 "Barlow Condensed", sans-serif;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.evaluation-board__cohort-label,
|
||||
.evaluation-board__section-label {
|
||||
color: oklch(0.4 0.04 250);
|
||||
font: 0.65rem/1.2 "IBM Plex Mono", monospace;
|
||||
letter-spacing: 0.09em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.evaluation-board__cohort-equation {
|
||||
margin-top: 0.7rem;
|
||||
font: 0.76rem/1.45 "IBM Plex Mono", monospace;
|
||||
}
|
||||
|
||||
.evaluation-board__factor strong {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.evaluation-board__outcomes {
|
||||
display: flex;
|
||||
grid-column: 2;
|
||||
gap: 0.5rem;
|
||||
margin: 0;
|
||||
padding: 1rem 1.15rem 0.8rem;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.evaluation-board__outcome {
|
||||
display: grid;
|
||||
flex: 1;
|
||||
gap: 0.3rem;
|
||||
min-width: 0;
|
||||
padding-left: 0.7rem;
|
||||
border-left: 3px solid oklch(0.55 0.03 250);
|
||||
}
|
||||
|
||||
.evaluation-board__outcome--pass { border-color: oklch(0.55 0.14 145); }
|
||||
.evaluation-board__outcome--invalid { border-color: oklch(0.66 0.14 75); }
|
||||
.evaluation-board__outcome--fail { border-color: oklch(0.58 0.16 25); }
|
||||
|
||||
.evaluation-board__outcome strong {
|
||||
font: 2.2rem/0.9 "Barlow Condensed", sans-serif;
|
||||
}
|
||||
|
||||
.evaluation-board__outcome span,
|
||||
.evaluation-board__finding span,
|
||||
.evaluation-board__audit-row {
|
||||
font: 0.72rem/1.3 "Source Sans 3", sans-serif;
|
||||
}
|
||||
|
||||
.evaluation-board__audit,
|
||||
.evaluation-board__findings {
|
||||
padding: 0.8rem 1.15rem;
|
||||
border-top: 1px solid var(--evaluation-rule);
|
||||
}
|
||||
|
||||
.evaluation-board__section-label { margin: 0 0 0.45rem; }
|
||||
|
||||
.evaluation-board__audit-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1.15fr;
|
||||
gap: 0.65rem;
|
||||
align-items: center;
|
||||
padding: 0.28rem 0;
|
||||
border-top: 1px solid oklch(0.84 0.035 75);
|
||||
}
|
||||
|
||||
.evaluation-board__audit-row strong { font-weight: 650; }
|
||||
|
||||
.evaluation-board__findings {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.45rem 0.8rem;
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.evaluation-board__finding {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.evaluation-board__finding svg { width: 1rem; height: 1rem; flex: 0 0 auto; }
|
||||
|
||||
.evaluation-board__boundary {
|
||||
grid-column: 1 / -1;
|
||||
margin: 0;
|
||||
padding: 0.7rem 1.15rem;
|
||||
border-top: 1px solid var(--evaluation-rule);
|
||||
background: oklch(0.91 0.025 85);
|
||||
font: italic 0.82rem/1.35 "Newsreader", serif;
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.evaluation-board { grid-template-columns: 1fr; }
|
||||
.evaluation-board__cohort { border-right: 0; }
|
||||
.evaluation-board__outcomes,
|
||||
.evaluation-board__findings { grid-column: auto; }
|
||||
.evaluation-board__audit-row { grid-template-columns: 1fr auto 1fr; }
|
||||
}
|
||||
|
||||
.stage-caption__eyebrow {
|
||||
color: oklch(0.78 0.035 250);
|
||||
font-size: 0.8rem;
|
||||
|
||||
Reference in New Issue
Block a user