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;
|
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 {
|
.stage-caption__eyebrow {
|
||||||
color: oklch(0.78 0.035 250);
|
color: oklch(0.78 0.035 250);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user