feat: gather defense discussion topics
This commit is contained in:
@@ -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)));
|
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 {
|
.operator-chat {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
@@ -23,6 +23,15 @@ describe("storyboard navigation", () => {
|
|||||||
.toBe("#discuss/where-is-ai-agent");
|
.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", () => {
|
it("falls back for unknown scene, beat, and branch hashes", () => {
|
||||||
expect(locationFromHash("#scene/missing/nope")).toEqual(defaultMainLocation);
|
expect(locationFromHash("#scene/missing/nope")).toEqual(defaultMainLocation);
|
||||||
expect(locationFromHash("#scene/lifecycle/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", () => {
|
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(0, 3).every((scene) => scene.stageTheme === "paper")).toBe(true);
|
||||||
expect(mainScenes.slice(3, 12).every((scene) => scene.stageTheme === "night")).toBe(true);
|
expect(mainScenes.slice(3, 12).every((scene) => scene.stageTheme === "night")).toBe(true);
|
||||||
|
|||||||
@@ -239,9 +239,9 @@ export const mainScenes = defineScenes([
|
|||||||
stageTheme: "paper",
|
stageTheme: "paper",
|
||||||
view: "evaluation",
|
view: "evaluation",
|
||||||
beats: [
|
beats: [
|
||||||
sceneBeat("cohort", "36-trial cohort", "Two challenges, two hosted models, three profiles, and three waves."),
|
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."),
|
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."),
|
sceneBeat("findings", "Longitudinal findings", "Trials exposed concrete authoring and diagnostic UX gaps.", { chatMode: "hidden" }),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -253,9 +253,10 @@ export const mainScenes = defineScenes([
|
|||||||
stageTheme: "paper",
|
stageTheme: "paper",
|
||||||
view: "conclusion",
|
view: "conclusion",
|
||||||
beats: [
|
beats: [
|
||||||
sceneBeat("limits", "Implemented boundary", "The prototype is not a production sandbox, scheduler, or broad agent benchmark."),
|
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."),
|
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: "dock", chatTheme: "light" }),
|
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" }),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
Reference in New Issue
Block a user