feat: gather defense discussion topics

This commit is contained in:
lda
2026-07-10 18:57:33 +07:00 Verified
parent 6b78ce819b
commit 0a02c23be2
9 changed files with 309 additions and 6 deletions
+67
View File
@@ -0,0 +1,67 @@
# Task 3 Report
## Files
- `web/apps/console/src/presentation/discussion/defense-discussion-index.ts`
- `web/apps/console/src/presentation/discussion/defense-discussion-index.test.ts`
- `web/apps/console/src/presentation/discussion/DefenseDiscussionIndex.tsx`
- `web/apps/console/src/presentation/discussion/DefenseDiscussionIndex.test.tsx`
- `web/apps/console/src/presentation/storyboard.ts`
- `web/apps/console/src/presentation/storyboard.test.ts`
- `web/apps/console/src/presentation/storyboard-navigation.test.ts`
- `web/apps/console/src/presentation/presentation.css`
- `.superpowers/sdd/task-3-report.md`
## TDD Evidence
### RED
Projection command:
```text
pnpm --dir web --filter @lda/console test -- src/presentation/discussion/defense-discussion-index.test.ts
```
Result: failed before tests ran because `./defense-discussion-index.js` did not exist.
Component/storyboard command:
```text
pnpm --dir web --filter @lda/console test -- src/presentation/discussion/DefenseDiscussionIndex.test.tsx src/presentation/storyboard.test.ts src/presentation/storyboard-navigation.test.ts
```
Result: failed with the missing component module, one missing Questions beat assertion, and one missing Questions navigation assertion. Existing storyboard/navigation coverage had 17 passing tests.
### GREEN
Focused combined run passed: 4 test files and 23 tests.
## Verification
- `pnpm --dir web --filter @lda/console test -- src/presentation/discussion/defense-discussion-index.test.ts src/presentation/discussion/DefenseDiscussionIndex.test.tsx src/presentation/storyboard.test.ts src/presentation/storyboard-navigation.test.ts` passed: 23 tests.
- `pnpm --dir web --filter @lda/console test -- src/presentation/discussion/defense-discussion-index.test.ts` passed: 2 tests.
- `pnpm --dir web --filter @lda/console typecheck` passed.
- `pnpm --dir web --filter @lda/console build` passed; Vite emitted the existing chunk-size warning for the 788 kB JavaScript bundle.
- `git diff --check` passed.
## Deviations
- The report file is included because the task explicitly requires it.
- The index remains a standalone component because the task file list excludes `SceneBody.tsx` and route integration files; no files outside the Task 3 surface were changed.
- The canonical branch title `Live demo reliability` is used in the component test; its question and answer remain sourced from the canonical branch object.
## Concerns
- `DefenseDiscussionIndex` is not wired into the scene renderer by this task. A later integration task must render it for the Questions beat.
- The CSS intentionally uses a two-column ledger and one-column narrow layout; it does not use pill styling.
- The production build retains the existing Vite warning about a JavaScript chunk larger than 500 kB.
## Self-review
- Confirmed all 22 canonical branch IDs occur exactly once in the seven groups and all mapping keys match `discussionBranches`.
- Confirmed group branch objects are derived by filtering canonical `discussionBranches`, without duplicated titles or answer content.
- Confirmed the required Lucide icons appear beside visible group labels.
- Confirmed branch buttons pass canonical IDs to `openDiscussion`.
- Confirmed Evaluation and every Conclusion beat, including Questions, explicitly use hidden chat.
- Confirmed Questions navigation resolves to `#scene/conclusion/questions` with an empty focus path.
- Confirmed only Task 3 files and this required report were changed.
@@ -0,0 +1,32 @@
import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { discussionBranches } from "../storyboard.js";
import { defenseDiscussionGroups } from "./defense-discussion-index.js";
import { DefenseDiscussionIndex } from "./DefenseDiscussionIndex.js";
describe("DefenseDiscussionIndex", () => {
afterEach(cleanup);
it("renders seven labelled topic sections and every canonical branch title", () => {
render(<DefenseDiscussionIndex openDiscussion={vi.fn()} />);
const nav = screen.getByRole("navigation", { name: "defense discussion index" });
expect(within(nav).getAllByRole("heading", { level: 2 })).toHaveLength(7);
for (const group of defenseDiscussionGroups) {
const heading = within(nav).getByRole("heading", { name: group.label, level: 2 });
expect(heading.querySelector("svg")).not.toBeNull();
}
for (const branch of discussionBranches) {
expect(within(nav).getByRole("button", { name: branch.title })).toBeInTheDocument();
}
});
it("opens the canonical branch selected from the index", () => {
const openDiscussion = vi.fn();
render(<DefenseDiscussionIndex openDiscussion={openDiscussion} />);
fireEvent.click(screen.getByRole("button", { name: "Live demo reliability" }));
expect(openDiscussion).toHaveBeenCalledWith("demo-reliability");
});
});
@@ -0,0 +1,40 @@
import type { FC } from "react";
import { BadgeHelp, Boxes, ChartNoAxesCombined, FileCode2, Map, PlaySquare, Rocket } from "lucide-react";
import { defenseDiscussionGroups, type DefenseDiscussionTopicId } from "./defense-discussion-index.js";
const topicIcons = {
contribution: BadgeHelp,
positioning: Map,
runtime: Boxes,
authoring: FileCode2,
demo: PlaySquare,
evaluation: ChartNoAxesCombined,
production: Rocket,
} as const satisfies Record<DefenseDiscussionTopicId, typeof BadgeHelp>;
export const DefenseDiscussionIndex: FC<{ readonly openDiscussion: (branchId: string) => void }> = ({
openDiscussion,
}) => (
<nav className="defense-discussion-index" aria-label="defense discussion index">
{defenseDiscussionGroups.map((group) => {
const Icon = topicIcons[group.id];
return (
<section className="defense-discussion-index__group" key={group.id}>
<h2 className="defense-discussion-index__heading">
<Icon aria-hidden="true" focusable="false" />
<span>{group.label}</span>
</h2>
<ul className="defense-discussion-index__list">
{group.branches.map((branch) => (
<li key={branch.id}>
<button type="button" onClick={() => openDiscussion(branch.id)}>
{branch.title}
</button>
</li>
))}
</ul>
</section>
);
})}
</nav>
);
@@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";
import { discussionBranches } from "../storyboard.js";
import { defenseDiscussionGroups, discussionTopicByBranchId } from "./defense-discussion-index.js";
describe("defense discussion index", () => {
it("exhaustively projects every canonical discussion branch exactly once", () => {
const indexedIds = defenseDiscussionGroups.flatMap((group) => group.branches.map((branch) => branch.id));
expect(indexedIds).toHaveLength(discussionBranches.length);
expect(new Set(indexedIds)).toEqual(new Set(discussionBranches.map((branch) => branch.id)));
expect(Object.keys(discussionTopicByBranchId).sort()).toEqual(
discussionBranches.map((branch) => branch.id).sort(),
);
});
it("derives indexed branch objects from the canonical definitions", () => {
for (const branch of discussionBranches) {
const indexed = defenseDiscussionGroups.flatMap((group) => group.branches).find(({ id }) => id === branch.id);
expect(indexed).toBe(branch);
}
});
});
@@ -0,0 +1,58 @@
import { discussionBranches, type DiscussionBranchDefinition, type DiscussionBranchId } from "../storyboard.js";
export type DefenseDiscussionTopicId =
| "contribution"
| "positioning"
| "runtime"
| "authoring"
| "demo"
| "evaluation"
| "production";
export type DefenseDiscussionGroup = {
readonly id: DefenseDiscussionTopicId;
readonly label: string;
readonly branches: readonly DiscussionBranchDefinition[];
};
// This explicit record is intentionally exhaustive: adding a Q&A branch must
// also place it in the end-of-defense index instead of silently hiding it.
export const discussionTopicByBranchId: Record<DiscussionBranchId, DefenseDiscussionTopicId> = {
"where-is-ai-agent": "contribution",
"title-ai-agent-wording": "contribution",
"direct-orchestration": "positioning",
"generated-scripts": "positioning",
"hosted-automation": "positioning",
"durable-agent-graphs": "positioning",
"mcp-agent-scale": "positioning",
"not-just-scripts": "positioning",
"not-just-cli": "runtime",
"lifecycle-states": "runtime",
"run-persistence": "runtime",
"raw-plan-import": "authoring",
"validation-diagnostics": "authoring",
"why-schemas": "authoring",
"typed-interrupts": "authoring",
"replay-provenance": "demo",
"demo-reliability": "demo",
"prepared-replay-boundary": "demo",
"evaluation-validity": "evaluation",
"provider-security": "production",
"security-production-boundary": "production",
"production-readiness": "production",
};
const discussionTopicGroups = [
{ id: "contribution", label: "Contribution" },
{ id: "positioning", label: "Positioning" },
{ id: "runtime", label: "Runtime" },
{ id: "authoring", label: "Authoring" },
{ id: "demo", label: "Demo" },
{ id: "evaluation", label: "Evaluation" },
{ id: "production", label: "Production" },
] as const satisfies readonly { id: DefenseDiscussionTopicId; label: string }[];
export const defenseDiscussionGroups: readonly DefenseDiscussionGroup[] = discussionTopicGroups.map((topic) => ({
...topic,
branches: discussionBranches.filter((branch) => discussionTopicByBranchId[branch.id] === topic.id),
}));
@@ -461,6 +461,73 @@
background: color-mix(in oklch, var(--color-intent, oklch(0.53 0.17 250)) 8%, var(--color-editorial-paper, oklch(0.975 0.012 82)));
}
.defense-discussion-index {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1.15rem 1.5rem;
margin-top: 1.1rem;
max-height: min(30rem, 56vh);
overflow: auto;
padding: 0.15rem 0.2rem 0.35rem;
}
.defense-discussion-index__group {
min-width: 0;
border-top: 2px solid color-mix(in oklch, var(--color-editorial-ink, oklch(0.19 0.015 65)) 68%, transparent);
padding-top: 0.65rem;
}
.defense-discussion-index__heading {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0 0 0.55rem;
color: var(--color-editorial-ink, oklch(0.19 0.015 65));
font: 750 0.82rem/1 var(--font-interface);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.defense-discussion-index__heading svg {
width: 1.05rem;
height: 1.05rem;
color: var(--color-intent, oklch(0.53 0.17 250));
}
.defense-discussion-index__list {
display: grid;
gap: 0.2rem;
margin: 0;
padding: 0;
list-style: none;
}
.defense-discussion-index__list button {
width: 100%;
border: 0;
border-bottom: 1px solid color-mix(in oklch, var(--color-editorial-muted, oklch(0.48 0.025 65)) 22%, transparent);
background: transparent;
color: var(--color-editorial-ink, oklch(0.19 0.015 65));
padding: 0.4rem 0.15rem;
text-align: left;
font: 600 0.86rem/1.25 var(--font-interface);
}
.defense-discussion-index__list button:hover,
.defense-discussion-index__list button:focus-visible {
background: color-mix(in oklch, var(--color-intent, oklch(0.53 0.17 250)) 8%, transparent);
color: var(--color-intent, oklch(0.53 0.17 250));
outline: 2px solid var(--color-intent, oklch(0.53 0.17 250));
outline-offset: 2px;
}
@media (max-width: 640px) {
.defense-discussion-index {
grid-template-columns: 1fr;
max-height: 50vh;
}
}
.operator-chat {
height: 100%;
overflow-y: auto;
@@ -23,6 +23,15 @@ describe("storyboard navigation", () => {
.toBe("#discuss/where-is-ai-agent");
});
it("parses the Questions beat location", () => {
expect(locationFromHash("#scene/conclusion/questions")).toEqual({
kind: "main",
sceneId: "conclusion",
beatId: "questions",
focusPath: [],
});
});
it("falls back for unknown scene, beat, and branch hashes", () => {
expect(locationFromHash("#scene/missing/nope")).toEqual(defaultMainLocation);
expect(locationFromHash("#scene/lifecycle/nope")).toEqual(defaultMainLocation);
@@ -35,6 +35,13 @@ describe("defense storyboard catalog", () => {
}
});
it("adds a hidden-chat questions beat after the conclusion beats", () => {
expect(findBeat("conclusion", "questions")).toBeDefined();
expect(findBeat("evaluation", "cohort")?.chatMode).toBe("hidden");
expect(findBeat("conclusion", "conclusion")?.chatMode).toBe("hidden");
expect(findBeat("conclusion", "questions")?.chatMode).toBe("hidden");
});
it("uses act-level stage themes and independent chat composition", () => {
expect(mainScenes.slice(0, 3).every((scene) => scene.stageTheme === "paper")).toBe(true);
expect(mainScenes.slice(3, 12).every((scene) => scene.stageTheme === "night")).toBe(true);
@@ -239,9 +239,9 @@ export const mainScenes = defineScenes([
stageTheme: "paper",
view: "evaluation",
beats: [
sceneBeat("cohort", "36-trial cohort", "Two challenges, two hosted models, three profiles, and three waves."),
sceneBeat("validity", "Bounded validity", "Manual audit separates task completion from valid product-surface evidence."),
sceneBeat("findings", "Longitudinal findings", "Trials exposed concrete authoring and diagnostic UX gaps."),
sceneBeat("cohort", "36-trial cohort", "Two challenges, two hosted models, three profiles, and three waves.", { chatMode: "hidden" }),
sceneBeat("validity", "Bounded validity", "Manual audit separates task completion from valid product-surface evidence.", { chatMode: "hidden" }),
sceneBeat("findings", "Longitudinal findings", "Trials exposed concrete authoring and diagnostic UX gaps.", { chatMode: "hidden" }),
],
},
{
@@ -253,9 +253,10 @@ export const mainScenes = defineScenes([
stageTheme: "paper",
view: "conclusion",
beats: [
sceneBeat("limits", "Implemented boundary", "The prototype is not a production sandbox, scheduler, or broad agent benchmark."),
sceneBeat("future", "Surrounding layers", "A live LLM interface, scheduling, and broader evaluation remain future work."),
sceneBeat("conclusion", "Planner proposes; runtime executes", "The typed substrate makes reusable agent-operated automation inspectable.", { chatMode: "dock", chatTheme: "light" }),
sceneBeat("limits", "Implemented boundary", "The prototype is not a production sandbox, scheduler, or broad agent benchmark.", { chatMode: "hidden" }),
sceneBeat("future", "Surrounding layers", "A live LLM interface, scheduling, and broader evaluation remain future work.", { chatMode: "hidden" }),
sceneBeat("conclusion", "Planner proposes; runtime executes", "The typed substrate makes reusable agent-operated automation inspectable.", { chatMode: "hidden", chatTheme: "light" }),
sceneBeat("questions", "Questions", "Discussion topics gathered for examiner questions.", { chatMode: "hidden", chatTheme: "light" }),
],
},
]);