ui: ProblemCard/ConcludeCard redesigned — the solution is the hero

Owner round (2026-10-05), Slice C:

- ProblemCard: ALWAYS OPEN — the answered card's container + eyebrow
  discipline ('Problem' label, lh-prose statement), no chevron, no
  collapsed bar. The dock mirrors it in Slice D.
- ConcludeCard: the reveal is the modelSolution committed at POSE time,
  shown verbatim as the hero — never re-authored (the slim schema's
  law). The verdict rides below in the quiz-explanation register
  (small, muted 'Verdict:' row). Nothing collapsible. Legacy inputs
  with their own solutionReveal fall back to it.
- The reveal join: modelSolutionForConclude (native-messages.ts) — the
  newest cleanly-posed problem before the conclude call; refused poses
  reveal nothing. projectView enriches tool-conclude parts with
  part.reveal at projection; ArtifactPartProps carries the field
  (contracts/chat.ts).

Pinned: always-open problem card; hero-first conclude layout with the
verdict after it; legacy fallback; the join (clean pose, refused pose,
multi-problem records) and the view enrichment.
This commit is contained in:
amosblomqvist
2026-10-05 02:29:16 +03:00
parent d19df86b13
commit 5d253a933c
6 changed files with 142 additions and 65 deletions
+25 -10
View File
@@ -76,7 +76,7 @@ describe("PracticeDock", () => {
// ── the transcript cards ─────────────────────────────────────────────────────
describe("the practice transcript cards", () => {
it("ProblemCard: the statement renders — the commitment NEVER does", () => {
it("ProblemCard: ALWAYS OPEN, the 'Problem' eyebrow + the statement — the commitment NEVER renders", () => {
const html = renderToStaticMarkup(
<ProblemCard
part={part({
@@ -84,33 +84,48 @@ describe("the practice transcript cards", () => {
statement: "Prove the parity claim.",
mode: "recall",
modelSolution: "Pair them.",
decisionPoints: ["pairing"],
expectedWrongMoves: [{ move: "induction", resolution: "pairing is direct" }],
strategy: "near-twin",
rationale: "weakest target",
},
})}
/>,
);
assert.match(html, /data-testid="problem-card"/);
assert.match(html, /Problem/);
assert.match(html, /Prove the parity claim\./);
for (const hidden of ["Pair them.", "induction", "near-twin", "weakest target", "pairing"]) {
assert.doesNotMatch(html, new RegExp(hidden.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")), `commitment leaked: ${hidden}`);
}
assert.doesNotMatch(html, /Pair them\./, "the commitment stays hidden");
// No chevron, no collapsed bar (owner round, 2026-10-05).
assert.doesNotMatch(html, /lucide-chevron|<button/, "nothing collapsible");
});
it("ConcludeCard: outcome badge + verdict headline; the reveal expands", () => {
it("ConcludeCard: the SOLUTION is the hero (the pose's modelSolution via part.reveal); the verdict rides below, low-key", () => {
const html = renderToStaticMarkup(
<ConcludeCard
part={part({
type: "tool-conclude",
input: { outcome: "solvedWithHints", verdict: "The pairing came after R2.", solutionReveal: "Pair them; each pair is even." },
input: { outcome: "solvedWithHints", verdict: "The pairing came after R2." },
reveal: "Pair them; each pair is even.",
})}
/>,
);
assert.match(html, /data-testid="conclude-card"/);
assert.match(html, /Solved with hints/);
assert.match(html, /Pair them; each pair is even\./, "the committed solution, verbatim");
assert.match(html, /Verdict:/);
assert.match(html, /The pairing came after R2\./);
// The verdict row renders AFTER the solution (the hero first).
assert.ok(html.indexOf("Pair them; each pair is even.") < html.indexOf("The pairing came after R2."));
assert.doesNotMatch(html, /<button/, "nothing collapsible");
});
it("ConcludeCard: legacy records fall back to their own solutionReveal", () => {
const html = renderToStaticMarkup(
<ConcludeCard
part={part({
type: "tool-conclude",
input: { outcome: "solved", verdict: "Clean.", solutionReveal: "Pair them; each pair is even." },
})}
/>,
);
assert.match(html, /Pair them; each pair is even\./);
});
it("ConcludeCard: the dismissal synthesis is one muted line — no reveal card", () => {