Loading...
}
-
{data && !data.ready && (
diff --git a/src/client/routes/KnowledgeWorkspace.test.tsx b/src/client/routes/KnowledgeWorkspace.test.tsx
index 28c6a9a1..f13068ca 100644
--- a/src/client/routes/KnowledgeWorkspace.test.tsx
+++ b/src/client/routes/KnowledgeWorkspace.test.tsx
@@ -2,9 +2,23 @@
import { cleanup, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it } from 'vitest';
+import { vi } from 'vitest';
import type { EntitiesData } from '../../shared/api-types.js';
-import { KnowledgeWorkspaceView } from './KnowledgeWorkspace.js';
+import type { KnowledgeWorkspaceLoaderData } from '../workspace/workspace-loader.js';
+import { KnowledgeWorkspace, KnowledgeWorkspaceView } from './KnowledgeWorkspace.js';
+
+let currentLoaderData: KnowledgeWorkspaceLoaderData;
+
+vi.mock('@tanstack/react-router', () => ({
+ Link: ({ children, to, ...props }: React.AnchorHTMLAttributes & { to?: string }) => (
+
+ {children}
+
+ ),
+ useLoaderData: () => currentLoaderData,
+ useParams: () => ({ id: '1' }),
+}));
afterEach(() => {
cleanup();
@@ -22,6 +36,12 @@ const emptyEntities: EntitiesData = {
relationships: [],
};
+afterEach(() => {
+ currentLoaderData = {
+ entitySnapshot: emptyEntities,
+ };
+});
+
describe('KnowledgeWorkspaceView', () => {
it('renders kind-grouped sections in registry order with labels and counts', () => {
const entities: EntitiesData = {
@@ -115,3 +135,21 @@ describe('KnowledgeWorkspaceView', () => {
expect(screen.getAllByText('Single-user only').length).toBeGreaterThanOrEqual(2);
});
});
+
+describe('KnowledgeWorkspace', () => {
+ it('renders route-level heading, navigation, and loader-backed content', () => {
+ currentLoaderData = {
+ entitySnapshot: {
+ ...emptyEntities,
+ goals: [{ id: 1, project_id: 1, kind: 'goal', subtype: null, content: 'Ship MVP', rationale: null }],
+ },
+ };
+
+ render();
+
+ expect(screen.getByRole('heading', { name: 'Knowledge' })).toBeTruthy();
+ expect(screen.getByRole('link', { name: '← Back to interview' })).toBeTruthy();
+ expect(screen.getByText('Review captured knowledge items and relationships.')).toBeTruthy();
+ expect(screen.getByText('Ship MVP')).toBeTruthy();
+ });
+});
diff --git a/src/client/routes/KnowledgeWorkspace.tsx b/src/client/routes/KnowledgeWorkspace.tsx
index 89cdeafa..5e77565c 100644
--- a/src/client/routes/KnowledgeWorkspace.tsx
+++ b/src/client/routes/KnowledgeWorkspace.tsx
@@ -3,7 +3,8 @@ import { Link, useLoaderData, useParams } from '@tanstack/react-router';
import { Badge } from '@/components/ui/badge';
import type { EntitiesData } from '../../shared/api-types.js';
-import { knowledgeKindRegistry, type KnowledgeCollectionKey } from '../../shared/knowledge.js';
+import { knowledgeKindRegistry } from '../../shared/knowledge.js';
+import type { KnowledgeWorkspaceLoaderData } from '../workspace/workspace-loader.js';
function entityKey(collection: string, id: number) {
return `${collection}:${id}`;
@@ -107,7 +108,9 @@ export function KnowledgeWorkspaceView({ entities }: { entities: EntitiesData })
export function KnowledgeWorkspace() {
const { id } = useParams({ from: '/project/$id/knowledge' });
- const { entitySnapshot } = useLoaderData({ from: '/project/$id/knowledge' });
+ const { entitySnapshot } = useLoaderData({
+ from: '/project/$id/knowledge',
+ }) as KnowledgeWorkspaceLoaderData;
return (
diff --git a/src/client/routes/export-loader.test.ts b/src/client/routes/export-loader.test.ts
new file mode 100644
index 00000000..f6df2876
--- /dev/null
+++ b/src/client/routes/export-loader.test.ts
@@ -0,0 +1,33 @@
+// @vitest-environment happy-dom
+
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { fetchExportPreviewLoaderData } from './export-loader.js';
+
+const fetchMock = vi.fn
();
+
+beforeEach(() => {
+ fetchMock.mockReset();
+ vi.stubGlobal('fetch', fetchMock);
+});
+
+afterEach(() => {
+ vi.unstubAllGlobals();
+});
+
+describe('export route loader', () => {
+ it('loads export preview data from the export endpoint', async () => {
+ fetchMock.mockResolvedValueOnce(
+ new Response(JSON.stringify({ ready: true, markdown: '# Reviewed Spec' }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ }),
+ );
+
+ await expect(fetchExportPreviewLoaderData(7)).resolves.toEqual({
+ ready: true,
+ markdown: '# Reviewed Spec',
+ });
+ expect(fetchMock).toHaveBeenCalledWith('/api/projects/7/export');
+ });
+});
diff --git a/src/client/routes/export-loader.ts b/src/client/routes/export-loader.ts
new file mode 100644
index 00000000..abb64fc8
--- /dev/null
+++ b/src/client/routes/export-loader.ts
@@ -0,0 +1,14 @@
+export interface ExportLoaderData {
+ ready: boolean;
+ markdown?: string;
+}
+
+export async function fetchExportPreviewLoaderData(projectId: number | string): Promise {
+ const id = String(projectId);
+ const response = await fetch(`/api/projects/${id}/export`);
+ if (!response.ok) {
+ throw new Error('Failed to load export');
+ }
+
+ return response.json() as Promise;
+}
diff --git a/src/client/workspace/workspace-loader.test.ts b/src/client/workspace/workspace-loader.test.ts
new file mode 100644
index 00000000..c44b80b1
--- /dev/null
+++ b/src/client/workspace/workspace-loader.test.ts
@@ -0,0 +1,130 @@
+// @vitest-environment happy-dom
+
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+import type { EntitiesData, ProjectState } from '../../shared/api-types.js';
+import { fetchInterviewWorkspaceLoaderData, fetchKnowledgeWorkspaceLoaderData } from './workspace-loader.js';
+
+const fetchMock = vi.fn();
+
+const projectState: ProjectState = {
+ project: {
+ id: 7,
+ name: 'Project 7',
+ active_turn_id: null,
+ created_at: '2026-04-03 10:00:00',
+ updated_at: '2026-04-03 10:00:00',
+ },
+ workflow: {
+ phases: {
+ scope: {
+ status: 'unstarted',
+ closeability: false,
+ readiness: 'low',
+ closureBasis: null,
+ proposalPending: false,
+ turnId: null,
+ summary: null,
+ },
+ design: {
+ status: 'unstarted',
+ closeability: false,
+ readiness: 'low',
+ closureBasis: null,
+ proposalPending: false,
+ turnId: null,
+ summary: null,
+ },
+ requirements: {
+ status: 'unstarted',
+ closeability: false,
+ readiness: 'low',
+ closureBasis: null,
+ proposalPending: false,
+ turnId: null,
+ summary: null,
+ },
+ criteria: {
+ status: 'unstarted',
+ closeability: false,
+ readiness: 'low',
+ closureBasis: null,
+ proposalPending: false,
+ turnId: null,
+ summary: null,
+ },
+ },
+ },
+ turns: [],
+};
+
+const entitySnapshot: EntitiesData = {
+ goals: [],
+ terms: [],
+ contexts: [],
+ constraints: [],
+ requirements: [],
+ criteria: [],
+ decisions: [],
+ assumptions: [],
+ relationships: [],
+};
+
+beforeEach(() => {
+ fetchMock.mockReset();
+ vi.stubGlobal('fetch', fetchMock);
+});
+
+afterEach(() => {
+ vi.unstubAllGlobals();
+});
+
+describe('workspace route loaders', () => {
+ it('loads interview workspace route data from the project and entities endpoints', async () => {
+ fetchMock
+ .mockResolvedValueOnce(
+ new Response(JSON.stringify(projectState), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ }),
+ )
+ .mockResolvedValueOnce(
+ new Response(JSON.stringify(entitySnapshot), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ }),
+ );
+
+ await expect(fetchInterviewWorkspaceLoaderData(7)).resolves.toEqual({ projectState, entitySnapshot });
+ expect(fetchMock).toHaveBeenNthCalledWith(1, '/api/projects/7');
+ expect(fetchMock).toHaveBeenNthCalledWith(2, '/api/projects/7/entities');
+ });
+
+ it('loads knowledge workspace route data from the same current route contract through its own helper', async () => {
+ fetchMock
+ .mockResolvedValueOnce(
+ new Response(JSON.stringify(projectState), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ }),
+ )
+ .mockResolvedValueOnce(
+ new Response(JSON.stringify(entitySnapshot), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ }),
+ );
+
+ await expect(fetchKnowledgeWorkspaceLoaderData('7')).resolves.toEqual({ entitySnapshot });
+ expect(fetchMock).toHaveBeenNthCalledWith(1, '/api/projects/7');
+ expect(fetchMock).toHaveBeenNthCalledWith(2, '/api/projects/7/entities');
+ });
+
+ it('fails knowledge workspace loading when the project does not exist', async () => {
+ fetchMock.mockResolvedValueOnce(new Response('Not found', { status: 404 }));
+
+ await expect(fetchKnowledgeWorkspaceLoaderData('999')).rejects.toThrow('Failed to load project');
+ expect(fetchMock).toHaveBeenCalledOnce();
+ expect(fetchMock).toHaveBeenCalledWith('/api/projects/999');
+ });
+});
diff --git a/src/client/workspace/workspace-loader.ts b/src/client/workspace/workspace-loader.ts
index b5ded259..aa3318d2 100644
--- a/src/client/workspace/workspace-loader.ts
+++ b/src/client/workspace/workspace-loader.ts
@@ -5,6 +5,10 @@ export interface WorkspaceLoaderData {
entitySnapshot: EntitiesData;
}
+export interface KnowledgeWorkspaceLoaderData {
+ entitySnapshot: EntitiesData;
+}
+
async function fetchJson(url: string, errorMessage: string): Promise {
const response = await fetch(url);
if (!response.ok) {
@@ -14,7 +18,7 @@ async function fetchJson(url: string, errorMessage: string): Promise {
return response.json() as Promise;
}
-export async function fetchWorkspaceLoaderData(projectId: number | string): Promise {
+async function fetchWorkflowDetailLoaderData(projectId: number | string): Promise {
const id = String(projectId);
const [projectState, entitySnapshot] = await Promise.all([
fetchJson(`/api/projects/${id}`, 'Failed to load project'),
@@ -23,3 +27,22 @@ export async function fetchWorkspaceLoaderData(projectId: number | string): Prom
return { projectState, entitySnapshot };
}
+
+export async function fetchInterviewWorkspaceLoaderData(
+ projectId: number | string,
+): Promise {
+ return fetchWorkflowDetailLoaderData(projectId);
+}
+
+export async function fetchKnowledgeWorkspaceLoaderData(
+ projectId: number | string,
+): Promise {
+ const id = String(projectId);
+ await fetchJson(`/api/projects/${id}`, 'Failed to load project');
+ const entitySnapshot = await fetchJson(
+ `/api/projects/${id}/entities`,
+ 'Failed to load project entities',
+ );
+
+ return { entitySnapshot };
+}
diff --git a/src/server/app.test.ts b/src/server/app.test.ts
index 13f31a11..76f5c693 100644
--- a/src/server/app.test.ts
+++ b/src/server/app.test.ts
@@ -275,7 +275,9 @@ describe('GET /api/projects/:id/export', () => {
const res = await request(app).get(`/api/projects/${projectId}/export`).expect(200);
expect(res.body.ready).toBe(true);
expect(res.body.markdown).toContain('# Done');
+ expect(res.body.markdown).toContain('Resume the interview from SQLite after restart');
expect(res.body.markdown).toContain('Verify SQLite resume');
+ expect(res.body.markdown).not.toContain('Support exporting the spec as a PDF');
});
});
diff --git a/src/server/app.ts b/src/server/app.ts
index 5198acf9..091f9f9b 100644
--- a/src/server/app.ts
+++ b/src/server/app.ts
@@ -39,6 +39,7 @@ import {
getOptionsForTurn,
updateTurn,
getEntitiesForProject,
+ getEntitiesForProjectOnActivePath,
recordReviewFromTurnResponse,
} from './db.js';
import { isExportReady, renderExportMarkdown } from './export.js';
@@ -169,7 +170,7 @@ export function createApp(dbPath?: string) {
res.json({ ready: false });
return;
}
- const entities = getEntitiesForProject(db, id);
+ const entities = getEntitiesForProjectOnActivePath(db, id);
const markdown = renderExportMarkdown(projectState.project.name, entities, projectState.workflow);
res.json({ ready: true, markdown });
});
diff --git a/src/server/db.ts b/src/server/db.ts
index 2a136110..9624dba9 100644
--- a/src/server/db.ts
+++ b/src/server/db.ts
@@ -792,6 +792,27 @@ export function getScopeBundleForProject(db: DB, projectId: number) {
};
}
+function getKnowledgeItemIdsLinkedToActivePath(db: DB, projectId: number): Set {
+ const activeTurnIds = getActivePath(db, projectId).map((turn) => turn.id);
+ if (activeTurnIds.length === 0) {
+ return new Set();
+ }
+
+ const rows = db
+ .select({ itemId: schema.turnKnowledgeItem.item_id })
+ .from(schema.turnKnowledgeItem)
+ .innerJoin(schema.knowledgeItem, eq(schema.knowledgeItem.id, schema.turnKnowledgeItem.item_id))
+ .where(
+ and(
+ eq(schema.knowledgeItem.project_id, projectId),
+ inArray(schema.turnKnowledgeItem.turn_id, activeTurnIds),
+ ),
+ )
+ .all() as Array<{ itemId: number }>;
+
+ return new Set(rows.map((row) => row.itemId));
+}
+
export function getEntitiesForProject(db: DB, projectId: number): EntitiesForProject {
const genericKnowledgeCollections = Object.fromEntries(
genericKnowledgeKindRegistry.map((entry) => [
@@ -851,3 +872,23 @@ export function getEntitiesForProject(db: DB, projectId: number): EntitiesForPro
})),
};
}
+
+export function getEntitiesForProjectOnActivePath(db: DB, projectId: number): EntitiesForProject {
+ const entities = getEntitiesForProject(db, projectId);
+ const activeItemIds = getKnowledgeItemIdsLinkedToActivePath(db, projectId);
+
+ return {
+ goals: entities.goals.filter((item) => activeItemIds.has(item.id)),
+ terms: entities.terms.filter((item) => activeItemIds.has(item.id)),
+ contexts: entities.contexts.filter((item) => activeItemIds.has(item.id)),
+ constraints: entities.constraints.filter((item) => activeItemIds.has(item.id)),
+ requirements: entities.requirements.filter((item) => activeItemIds.has(item.id)),
+ criteria: entities.criteria.filter((item) => activeItemIds.has(item.id)),
+ decisions: entities.decisions.filter((item) => activeItemIds.has(item.id)),
+ assumptions: entities.assumptions.filter((item) => activeItemIds.has(item.id)),
+ relationships: entities.relationships.filter(
+ (relationship) =>
+ activeItemIds.has(relationship.source.id) && activeItemIds.has(relationship.target.id),
+ ),
+ };
+}
diff --git a/src/server/export.test.ts b/src/server/export.test.ts
index 0ee20943..93c4763a 100644
--- a/src/server/export.test.ts
+++ b/src/server/export.test.ts
@@ -1,14 +1,35 @@
-import { describe, expect, it } from 'vitest';
+import { afterEach, describe, expect, it } from 'vitest';
import type { EntitiesData } from '../shared/api-types.js';
-import type { WorkflowState } from './db.js';
-import { renderExportMarkdown } from './export.js';
+import { getProjectState } from './core.js';
+import {
+ advanceHead,
+ createDb,
+ createKnowledgeItem,
+ createProject,
+ createTurn,
+ getEntitiesForProject,
+ getEntitiesForProjectOnActivePath,
+ linkKnowledgeItemToTurn,
+ type WorkflowState,
+} from './db.js';
+import { buildReviewedExportProjection, renderExportMarkdown } from './export.js';
+import {
+ seedAllPhasesClosedWithForcedDesign,
+ seedAllPhasesClosedWithLowReadinessScope,
+} from './fixtures/scenarios.js';
-function createClosedPhase(basis: string = 'interviewer_recommended') {
+function createClosedPhase({
+ basis = 'interviewer_recommended',
+ readiness = 'high',
+}: {
+ basis?: string;
+ readiness?: 'low' | 'medium' | 'high';
+} = {}) {
return {
status: 'closed' as const,
closeability: true,
- readiness: 'high' as const,
+ readiness,
closureBasis: basis,
proposalPending: false,
turnId: 1,
@@ -41,6 +62,58 @@ const emptyEntities: EntitiesData = {
};
describe('renderExportMarkdown', () => {
+ const openDbs: Array> = [];
+
+ afterEach(() => {
+ while (openDbs.length > 0) {
+ openDbs.pop()?.$client.close();
+ }
+ });
+
+ it('projects reviewed export sections and caveats before markdown rendering', () => {
+ const entities: EntitiesData = {
+ ...emptyEntities,
+ requirements: [
+ {
+ id: 1,
+ project_id: 1,
+ kind: 'requirement',
+ subtype: null,
+ content: 'Export spec',
+ rationale: null,
+ reviewStatus: 'approved',
+ },
+ {
+ id: 2,
+ project_id: 1,
+ kind: 'requirement',
+ subtype: null,
+ content: 'PDF export',
+ rationale: null,
+ reviewStatus: 'rejected',
+ },
+ ],
+ decisions: [{ id: 3, project_id: 1, content: 'Use SQLite', rationale: 'Zero config' }],
+ };
+ const workflow = createAllClosedWorkflow({
+ design: createClosedPhase({ basis: 'user_forced', readiness: 'low' }),
+ });
+
+ expect(buildReviewedExportProjection(entities, workflow)).toEqual({
+ caveats: ['design was closed via user-forced closure', 'design was closed with low readiness'],
+ sections: [
+ {
+ heading: 'Requirements',
+ items: [{ content: 'Export spec', rationale: null }],
+ },
+ {
+ heading: 'Decisions',
+ items: [{ content: 'Use SQLite', rationale: 'Zero config' }],
+ },
+ ],
+ });
+ });
+
it('renders kind-grouped sections from entities', () => {
const entities: EntitiesData = {
...emptyEntities,
@@ -85,7 +158,7 @@ describe('renderExportMarkdown', () => {
it('includes closure caveats for forced-close phases', () => {
const workflow = createAllClosedWorkflow({
- design: createClosedPhase('user_forced'),
+ design: createClosedPhase({ basis: 'user_forced' }),
});
const md = renderExportMarkdown('Test', emptyEntities, workflow);
@@ -94,7 +167,7 @@ describe('renderExportMarkdown', () => {
expect(md).toContain('user-forced');
});
- it('includes review status for requirements and criteria', () => {
+ it('renders only approved reviewed items in the export body', () => {
const entities: EntitiesData = {
...emptyEntities,
requirements: [
@@ -116,12 +189,151 @@ describe('renderExportMarkdown', () => {
rationale: null,
reviewStatus: 'rejected',
},
+ {
+ id: 3,
+ project_id: 1,
+ kind: 'requirement',
+ subtype: null,
+ content: 'CSV export',
+ rationale: null,
+ reviewStatus: 'pending',
+ },
+ ],
+ criteria: [
+ {
+ id: 4,
+ project_id: 1,
+ kind: 'criterion',
+ subtype: null,
+ content: 'Reload shows the active interview state',
+ rationale: null,
+ reviewStatus: 'approved',
+ },
+ {
+ id: 5,
+ project_id: 1,
+ kind: 'criterion',
+ subtype: null,
+ content: 'PDF download works offline',
+ rationale: null,
+ reviewStatus: 'rejected',
+ },
],
};
const md = renderExportMarkdown('Test', entities, createAllClosedWorkflow());
- expect(md).toMatch(/Export spec.*approved/i);
- expect(md).toMatch(/PDF export.*rejected/i);
+ expect(md).toContain('Export spec');
+ expect(md).toContain('Reload shows the active interview state');
+ expect(md).not.toContain('PDF export');
+ expect(md).not.toContain('CSV export');
+ expect(md).not.toContain('PDF download works offline');
+ expect(md).not.toMatch(/\bapproved\b/i);
+ expect(md).not.toMatch(/\brejected\b/i);
+ });
+
+ it('includes closure caveats for low-readiness phases', () => {
+ const workflow = createAllClosedWorkflow({
+ design: createClosedPhase({ readiness: 'low' }),
+ });
+
+ const md = renderExportMarkdown('Test', emptyEntities, workflow);
+
+ expect(md).toContain('design');
+ expect(md).toContain('low readiness');
+ });
+
+ it('filters export content to knowledge linked on the active path', () => {
+ const db = createDb();
+ openDbs.push(db);
+ const project = createProject(db, 'Branching Project');
+ const rootTurn = createTurn(db, project.id, {
+ phase: 'scope',
+ question: 'What database?',
+ answer: 'We are still deciding.',
+ });
+ const abandonedBranchTurn = createTurn(db, project.id, {
+ phase: 'design',
+ parent_turn_id: rootTurn.id,
+ question: 'Which storage option?',
+ answer: 'Take the SQLite branch.',
+ });
+ const activeBranchTurn = createTurn(db, project.id, {
+ phase: 'design',
+ parent_turn_id: rootTurn.id,
+ question: 'Which storage option?',
+ answer: 'Take the Postgres branch.',
+ });
+ advanceHead(db, project.id, activeBranchTurn.id);
+
+ const abandonedDecision = createKnowledgeItem(db, project.id, 'decision', 'Use SQLite for persistence', {
+ rationale: 'This belonged to the abandoned branch.',
+ });
+ const activeDecision = createKnowledgeItem(db, project.id, 'decision', 'Use Postgres for persistence', {
+ rationale: 'This belongs to the active branch.',
+ });
+ linkKnowledgeItemToTurn(db, abandonedDecision.id, abandonedBranchTurn.id);
+ linkKnowledgeItemToTurn(db, activeDecision.id, activeBranchTurn.id);
+
+ expect(getEntitiesForProject(db, project.id).decisions).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ content: 'Use SQLite for persistence' }),
+ expect.objectContaining({ content: 'Use Postgres for persistence' }),
+ ]),
+ );
+
+ const markdown = renderExportMarkdown(
+ project.name,
+ getEntitiesForProjectOnActivePath(db, project.id),
+ createAllClosedWorkflow(),
+ );
+
+ expect(markdown).toContain('Use Postgres for persistence');
+ expect(markdown).not.toContain('Use SQLite for persistence');
+ });
+
+ it('renders the forced-close canonical fixture with the expected export caveat', () => {
+ const db = createDb();
+ openDbs.push(db);
+ const projectId = createProject(db, 'Forced-Close All Phases Closed').id;
+ seedAllPhasesClosedWithForcedDesign(db, projectId);
+
+ const projectState = getProjectState(db, projectId);
+ expect(projectState).not.toBeNull();
+ expect(projectState?.workflow.phases.design).toMatchObject({
+ status: 'closed',
+ closureBasis: 'user_forced',
+ });
+
+ const markdown = renderExportMarkdown(
+ projectState!.project.name,
+ getEntitiesForProject(db, projectId),
+ projectState!.workflow,
+ );
+ expect(markdown).toContain('__design__ was closed via user-forced closure');
+ expect(markdown).not.toContain('Support exporting the spec as a PDF');
+ });
+
+ it('renders the low-readiness canonical fixture with the expected export caveat', () => {
+ const db = createDb();
+ openDbs.push(db);
+ const projectId = createProject(db, 'Low-Readiness All Phases Closed').id;
+ seedAllPhasesClosedWithLowReadinessScope(db, projectId);
+
+ const projectState = getProjectState(db, projectId);
+ expect(projectState).not.toBeNull();
+ expect(projectState?.workflow.phases.scope).toMatchObject({
+ status: 'closed',
+ readiness: 'low',
+ closureBasis: 'interviewer_recommended',
+ });
+
+ const markdown = renderExportMarkdown(
+ projectState!.project.name,
+ getEntitiesForProject(db, projectId),
+ projectState!.workflow,
+ );
+ expect(markdown).toContain('__scope__ was closed with low readiness');
+ expect(markdown).not.toContain('Support exporting the spec as a PDF');
});
});
diff --git a/src/server/export.ts b/src/server/export.ts
index 8f0d8ac5..43c5aba2 100644
--- a/src/server/export.ts
+++ b/src/server/export.ts
@@ -4,26 +4,75 @@ import type { EntitiesData } from '../shared/api-types.js';
import { knowledgeKindRegistry } from '../shared/knowledge.js';
import type { WorkflowState } from './db.js';
-function renderItem(item: { content: string; rationale?: string | null; reviewStatus?: string }): string {
+export interface ReviewedExportItem {
+ content: string;
+ rationale?: string | null;
+}
+
+export interface ReviewedExportSection {
+ heading: string;
+ items: ReviewedExportItem[];
+}
+
+export interface ReviewedExportProjection {
+ caveats: string[];
+ sections: ReviewedExportSection[];
+}
+
+function renderItem(item: ReviewedExportItem): string {
const parts = [item.content];
- if (item.reviewStatus) {
- parts.push(`(${item.reviewStatus})`);
- }
if (item.rationale) {
parts.push(`— ${item.rationale}`);
}
return parts.join(' ');
}
-function renderCaveats(workflow: WorkflowState): string {
+function getReviewedExportItems(
+ items: Array<{ content: string; rationale?: string | null; reviewStatus?: string }>,
+) {
+ return items.filter((item) => !('reviewStatus' in item) || item.reviewStatus === 'approved');
+}
+
+function getReviewedExportCaveats(workflow: WorkflowState): string[] {
const caveats: string[] = [];
for (const [phase, state] of Object.entries(workflow.phases)) {
if (state.closureBasis && state.closureBasis !== 'interviewer_recommended') {
- caveats.push(`${bold(phase)} was closed via user-forced closure`);
+ caveats.push(`${phase} was closed via user-forced closure`);
+ }
+ if (state.readiness === 'low') {
+ caveats.push(`${phase} was closed with low readiness`);
}
}
+ return caveats;
+}
+
+function renderCaveats(caveats: string[]): string {
if (caveats.length === 0) return '';
- return `${h3('Closure Caveats')}\n\n${ul(caveats)}\n`;
+ return `${h3('Closure Caveats')}\n\n${ul(caveats.map((caveat) => bold(caveat.split(' ')[0]!) + caveat.slice(caveat.indexOf(' '))))}\n`;
+}
+
+export function buildReviewedExportProjection(
+ entities: EntitiesData,
+ workflow: WorkflowState,
+): ReviewedExportProjection {
+ return {
+ caveats: getReviewedExportCaveats(workflow),
+ sections: knowledgeKindRegistry.flatMap((entry) => {
+ const items = getReviewedExportItems(entities[entry.collectionKey]).map((item) => ({
+ content: item.content,
+ rationale: item.rationale,
+ }));
+
+ return items.length > 0
+ ? [
+ {
+ heading: entry.label,
+ items,
+ } satisfies ReviewedExportSection,
+ ]
+ : [];
+ }),
+ };
}
export function renderExportMarkdown(
@@ -32,19 +81,17 @@ export function renderExportMarkdown(
workflow: WorkflowState,
): string {
const sections: string[] = [h1(projectName), ''];
+ const projection = buildReviewedExportProjection(entities, workflow);
- const caveatSection = renderCaveats(workflow);
+ const caveatSection = renderCaveats(projection.caveats);
if (caveatSection) {
sections.push(caveatSection);
}
- for (const entry of knowledgeKindRegistry) {
- const items = entities[entry.collectionKey];
- if (items.length === 0) continue;
-
- sections.push(h2(entry.label));
+ for (const section of projection.sections) {
+ sections.push(h2(section.heading));
sections.push('');
- sections.push(ul(items.map(renderItem)));
+ sections.push(ul(section.items.map(renderItem)));
sections.push('');
}
diff --git a/src/server/fixtures/manifest.test.ts b/src/server/fixtures/manifest.test.ts
new file mode 100644
index 00000000..ecb8106e
--- /dev/null
+++ b/src/server/fixtures/manifest.test.ts
@@ -0,0 +1,51 @@
+import { afterEach, beforeEach, describe, expect, it } from 'vitest';
+
+import { loadActivePathWithOptions } from '../core.js';
+import { createDb, type DB } from '../db.js';
+import { formatProjectedTurnResponse, projectTurnResponse } from '../turn-response.js';
+import { seedFromManifest, type ManifestScenario } from './manifest.js';
+
+let db: DB;
+
+beforeEach(() => {
+ db = createDb();
+});
+
+afterEach(() => {
+ db.$client.close();
+});
+
+describe('seedFromManifest', () => {
+ it('persists selected option ids in user_parts so seeded turns rehydrate with option text', () => {
+ const scenario: ManifestScenario = {
+ turns: [
+ {
+ phase: 'scope',
+ question: 'Which launch surface should we prioritize?',
+ why: 'The fixture should preserve the selected option text after reload.',
+ impact: 'high',
+ answer: 'Start with the web workspace.',
+ options: [
+ { content: 'CLI-first workflow', is_recommended: false },
+ { content: 'Web workspace', is_recommended: true },
+ ],
+ selectedOptionPositions: [1],
+ },
+ ],
+ knowledgeItems: [],
+ edges: [],
+ };
+
+ const projectId = seedFromManifest(db, scenario, 'Manifest Seed');
+ const turn = loadActivePathWithOptions(db, projectId)[0]!;
+ const projectedResponse = projectTurnResponse(turn);
+
+ expect(projectedResponse).toEqual({
+ selectedOptionIds: [turn.options![1]!.id],
+ selectedOptionContents: ['Web workspace'],
+ freeText: undefined,
+ });
+ expect(formatProjectedTurnResponse(projectedResponse!)).toContain('Chosen options: Web workspace');
+ expect(formatProjectedTurnResponse(projectedResponse!)).not.toContain('Chosen options: 1');
+ });
+});
diff --git a/src/server/fixtures/manifest.ts b/src/server/fixtures/manifest.ts
new file mode 100644
index 00000000..69676c61
--- /dev/null
+++ b/src/server/fixtures/manifest.ts
@@ -0,0 +1,305 @@
+import { readFileSync } from 'node:fs';
+import { dirname, join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+import { and, eq } from 'drizzle-orm';
+
+import {
+ advanceHead,
+ confirmPhaseOutcome,
+ createKnowledgeItem,
+ createOption,
+ createPhaseOutcome,
+ createProject,
+ createTurn,
+ linkKnowledgeItemToTurn,
+ applyTurnResponseSelections,
+ type DB,
+} from '../db.js';
+import * as schema from '../schema.js';
+import type { ScenarioFn } from './scenarios.js';
+
+// ---------------------------------------------------------------------------
+// Manifest types
+// ---------------------------------------------------------------------------
+
+export interface ManifestOption {
+ content: string;
+ is_recommended: boolean;
+}
+
+export interface ManifestTurn {
+ phase: 'scope' | 'design' | 'requirements' | 'criteria';
+ question: string;
+ answer: string;
+ why?: string | null;
+ impact?: 'high' | 'medium' | 'low' | null;
+ options?: ManifestOption[];
+ selectedOptionPositions?: number[];
+ freeText?: string | null;
+ isProposal?: boolean;
+ isConfirmation?: boolean;
+}
+
+export interface ManifestKnowledgeItem {
+ kind: 'goal' | 'term' | 'context' | 'constraint' | 'decision' | 'assumption' | 'requirement' | 'criterion';
+ content: string;
+ rationale?: string | null;
+ capturedAtTurn: number;
+ reviewAction?: 'reviewed' | 'rejected';
+ reviewedAtTurn?: number;
+}
+
+export interface ManifestEdge {
+ fromItemIndex: number;
+ toItemIndex: number;
+ relation: 'depends_on' | 'derived_from' | 'constrains' | 'verifies' | 'refines';
+}
+
+export interface ManifestScenario {
+ turns: ManifestTurn[];
+ knowledgeItems: ManifestKnowledgeItem[];
+ edges: ManifestEdge[];
+}
+
+export interface Manifest {
+ name: string;
+ description: string;
+ scenarios: Record;
+}
+
+// ---------------------------------------------------------------------------
+// Seeder
+// ---------------------------------------------------------------------------
+
+export function seedFromManifest(db: DB, scenario: ManifestScenario, projectName: string): number {
+ const project = createProject(db, projectName);
+ const projectId = project.id;
+
+ // Track manifest turn index → actual turn ID
+ const turnIdMap = new Map();
+ let prevTurnId: number | null = null;
+
+ for (let i = 0; i < scenario.turns.length; i++) {
+ const mt = scenario.turns[i]!;
+
+ if (mt.isConfirmation) {
+ // Find the most recent proposal turn in the same phase
+ let proposalTurnId: number | null = null;
+ for (let j = i - 1; j >= 0; j--) {
+ if (scenario.turns[j]!.isProposal && scenario.turns[j]!.phase === mt.phase) {
+ proposalTurnId = turnIdMap.get(j) ?? null;
+ break;
+ }
+ }
+
+ const userParts = JSON.stringify([
+ { type: 'text', text: `Confirm ${mt.phase} closure` },
+ {
+ type: 'data-confirmation',
+ data: {
+ kind: 'confirm-proposed-phase-closure',
+ proposalTurnId,
+ phase: mt.phase,
+ },
+ },
+ ]);
+
+ const turn = createTurn(db, projectId, {
+ phase: mt.phase,
+ parent_turn_id: prevTurnId,
+ question: '',
+ answer: `Confirm ${mt.phase} closure`,
+ user_parts: userParts,
+ });
+ turnIdMap.set(i, turn.id);
+
+ // Find the phase outcome created by the proposal turn and confirm it
+ const outcome =
+ proposalTurnId != null
+ ? (db
+ .select()
+ .from(schema.phaseOutcome)
+ .where(
+ and(
+ eq(schema.phaseOutcome.project_id, projectId),
+ eq(schema.phaseOutcome.proposal_turn_id, proposalTurnId),
+ eq(schema.phaseOutcome.status, 'proposed'),
+ ),
+ )
+ .get() as { id: number } | undefined)
+ : undefined;
+ if (outcome) {
+ confirmPhaseOutcome(db, outcome.id, turn.id);
+ }
+
+ advanceHead(db, projectId, turn.id);
+ prevTurnId = turn.id;
+ continue;
+ }
+
+ if (mt.isProposal) {
+ const assistantParts = JSON.stringify([
+ { type: 'text', text: '' },
+ {
+ type: 'tool-propose_phase_closure',
+ toolCallId: `tc_proposal_${i}`,
+ state: 'output-available',
+ input: { phase: mt.phase, summary: mt.answer },
+ output: { ok: true, turnId: -1, phase: mt.phase }, // placeholder, updated below
+ },
+ ]);
+
+ const turn = createTurn(db, projectId, {
+ phase: mt.phase,
+ parent_turn_id: prevTurnId,
+ question: '',
+ answer: mt.answer,
+ assistant_parts: assistantParts,
+ });
+ turnIdMap.set(i, turn.id);
+
+ createPhaseOutcome(db, {
+ projectId,
+ phase: mt.phase,
+ proposal_turn_id: turn.id,
+ summary: mt.answer,
+ });
+
+ advanceHead(db, projectId, turn.id);
+ prevTurnId = turn.id;
+ continue;
+ }
+
+ // Regular turn
+ const options = mt.options ?? [];
+ const assistantParts = JSON.stringify([
+ { type: 'text', text: '' },
+ {
+ type: 'tool-ask_question',
+ toolCallId: `tc_${i}`,
+ state: 'output-available',
+ input: {
+ question: mt.question,
+ why: mt.why ?? null,
+ impact: mt.impact ?? null,
+ options,
+ },
+ output: { ok: true, turnId: -1, optionCount: options.length },
+ },
+ ]);
+
+ // We need the turn ID for user_parts, so create first then update user_parts
+ const turn = createTurn(db, projectId, {
+ phase: mt.phase,
+ parent_turn_id: prevTurnId,
+ question: mt.question,
+ why: mt.why ?? null,
+ impact: mt.impact ?? null,
+ answer: mt.answer,
+ assistant_parts: assistantParts,
+ });
+ turnIdMap.set(i, turn.id);
+
+ // Create options in DB and retain their row IDs for user_parts rehydration.
+ const optionIdsByPosition = new Map();
+ for (let p = 0; p < options.length; p++) {
+ const opt = options[p]!;
+ const createdOption = createOption(db, turn.id, {
+ position: p,
+ content: opt.content,
+ is_recommended: opt.is_recommended,
+ });
+ optionIdsByPosition.set(p, createdOption.id);
+ }
+
+ // Apply selections
+ if (mt.selectedOptionPositions && mt.selectedOptionPositions.length > 0) {
+ applyTurnResponseSelections(db, turn.id, mt.selectedOptionPositions);
+ }
+
+ // Set user_parts (needs actual turn.id, so done after creation)
+ const selectedIds = (mt.selectedOptionPositions ?? [])
+ .map((position) => optionIdsByPosition.get(position))
+ .filter((optionId): optionId is number => optionId != null);
+ const userParts = JSON.stringify([
+ { type: 'text', text: mt.answer },
+ {
+ type: 'data-turn-response',
+ data: {
+ turnId: turn.id,
+ selectedOptionIds: selectedIds,
+ freeText: mt.freeText ?? undefined,
+ },
+ },
+ ]);
+ db.update(schema.turn).set({ user_parts: userParts }).where(eq(schema.turn.id, turn.id)).run();
+
+ advanceHead(db, projectId, turn.id);
+ prevTurnId = turn.id;
+ }
+
+ // --- Knowledge items ---
+ const itemIdMap = new Map();
+
+ for (let k = 0; k < scenario.knowledgeItems.length; k++) {
+ const mi = scenario.knowledgeItems[k]!;
+ const item = createKnowledgeItem(db, projectId, mi.kind, mi.content, {
+ rationale: mi.rationale ?? null,
+ });
+ itemIdMap.set(k, item.id);
+
+ // Link to capturing turn
+ const captureTurnId = turnIdMap.get(mi.capturedAtTurn);
+ if (captureTurnId != null) {
+ linkKnowledgeItemToTurn(db, item.id, captureTurnId, 'captured');
+ }
+
+ // Link review action
+ if (mi.reviewAction && mi.reviewedAtTurn != null) {
+ const reviewTurnId = turnIdMap.get(mi.reviewedAtTurn);
+ if (reviewTurnId != null) {
+ linkKnowledgeItemToTurn(db, item.id, reviewTurnId, mi.reviewAction);
+ }
+ }
+ }
+
+ // --- Edges ---
+ for (const edge of scenario.edges) {
+ const fromId = itemIdMap.get(edge.fromItemIndex);
+ const toId = itemIdMap.get(edge.toItemIndex);
+ if (fromId != null && toId != null) {
+ db.insert(schema.knowledgeEdge)
+ .values({ from_item_id: fromId, to_item_id: toId, relation: edge.relation })
+ .run();
+ }
+ }
+
+ return projectId;
+}
+
+// ---------------------------------------------------------------------------
+// Manifest loader
+// ---------------------------------------------------------------------------
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+export function loadManifestScenarios(manifestName: string): Record {
+ const filePath = join(__dirname, 'manifests', `${manifestName}.json`);
+ const raw = readFileSync(filePath, 'utf-8');
+ const manifest: Manifest = JSON.parse(raw);
+
+ const result: Record = {};
+
+ for (const scenarioKey of Object.keys(manifest.scenarios)) {
+ const scenario = manifest.scenarios[scenarioKey]!;
+ const fullKey = `${manifestName}-${scenarioKey}`;
+ const defaultName = `${manifest.name} (${scenarioKey})`;
+
+ result[fullKey] = (db: DB, projectName?: string) => {
+ return seedFromManifest(db, scenario, projectName ?? defaultName);
+ };
+ }
+
+ return result;
+}
diff --git a/src/server/fixtures/manifests/issue-tracker.json b/src/server/fixtures/manifests/issue-tracker.json
new file mode 100644
index 00000000..50f07c2d
--- /dev/null
+++ b/src/server/fixtures/manifests/issue-tracker.json
@@ -0,0 +1,2218 @@
+{
+ "name": "Issue Tracker",
+ "description": "A tiny issue tracker for a small software team with tickets, comments, status changes, audit history, and role-based permissions.",
+ "scenarios": {
+ "scope-closed": {
+ "turns": [
+ {
+ "phase": "scope",
+ "question": "What is the primary goal of this issue tracker? Are you looking to replace an existing tool, or is this a new workflow for the team?",
+ "why": "Understanding whether this replaces an existing process helps scope the migration effort and identify must-have features vs nice-to-haves.",
+ "impact": "high",
+ "answer": "Replace our spreadsheet — we need proper tracking from creation to completion",
+ "options": [
+ {
+ "content": "Replace an existing tool (e.g., spreadsheet, email threads) with a purpose-built tracker",
+ "is_recommended": true
+ },
+ {
+ "content": "Introduce issue tracking as a new practice — the team currently has no formal process",
+ "is_recommended": false
+ },
+ {
+ "content": "Supplement an existing enterprise tool (e.g., Jira) with a lighter alternative for smaller projects",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "We're currently using a shared Google Sheet and it's painful — no history, no accountability"
+ },
+ {
+ "phase": "scope",
+ "question": "Who are the primary users of this system, and how large is the team?",
+ "why": "Team size and user roles directly influence the permission model, notification complexity, and performance requirements.",
+ "impact": "high",
+ "answer": "About 8 developers across 2-3 projects, plus a couple of managers who need visibility",
+ "options": [
+ { "content": "A small team (under 10 people) working on 1-3 projects", "is_recommended": true },
+ {
+ "content": "A medium team (10-30 people) with multiple squads and project leads",
+ "is_recommended": false
+ },
+ {
+ "content": "A cross-functional group including developers, QA, product managers, and stakeholders",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "About 8 developers across 2-3 projects, plus a couple of managers who want read-only dashboards"
+ },
+ {
+ "phase": "scope",
+ "question": "What are the key entities the system needs to track? Think about the core objects users will create and interact with daily.",
+ "why": "Defining the entity model early prevents scope creep — each entity adds CRUD, permissions, and relationship complexity.",
+ "impact": "high",
+ "answer": "Tickets with status, assignee, priority — plus comments for discussion and audit",
+ "options": [
+ {
+ "content": "Tickets (work items) with status, assignee, priority, and description",
+ "is_recommended": true
+ },
+ {
+ "content": "Tickets plus epics or parent groupings for organizing related work",
+ "is_recommended": false
+ },
+ { "content": "Tickets, sprints, and a backlog with capacity planning", "is_recommended": false }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "Each ticket should also support comments — timestamped notes for discussion and status updates"
+ },
+ {
+ "phase": "scope",
+ "question": "Are there compliance or audit requirements for this system? Some teams need to prove who changed what and when.",
+ "why": "Audit trail requirements affect the data model significantly — they may require immutable event logs rather than simple CRUD updates.",
+ "impact": "high",
+ "answer": "Yes, we need full audit history for compliance — every status change must be traceable",
+ "options": [
+ {
+ "content": "Full audit trail required — every status change must record who, what, and when",
+ "is_recommended": true
+ },
+ {
+ "content": "Basic change history is nice to have but not a compliance requirement",
+ "is_recommended": false
+ },
+ {
+ "content": "No audit requirements — simple current-state tracking is sufficient",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "scope",
+ "question": "What constraints should we establish for the v1 scope? Understanding what you explicitly want to exclude helps keep the first version focused.",
+ "why": "Explicit constraints prevent scope creep and help prioritize — knowing what's out helps clarify what's in.",
+ "impact": "medium",
+ "answer": "Keep it simpler than Jira, add role-based permissions with three roles",
+ "options": [
+ {
+ "content": "Keep it deliberately simpler than Jira — no custom workflows, no custom fields in v1",
+ "is_recommended": true
+ },
+ { "content": "Match Jira's core feature set but with a cleaner UX", "is_recommended": false },
+ {
+ "content": "Build a minimal viable tracker and expand based on user feedback",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "We need role-based permissions though — admin, developer, and viewer roles"
+ },
+ {
+ "phase": "scope",
+ "question": "",
+ "answer": "Scope context is well-defined: the team needs a simple issue tracker replacing their spreadsheet, with tickets, comments, audit history, and role-based permissions for ~8 developers.",
+ "isProposal": true
+ },
+ {
+ "phase": "scope",
+ "question": "",
+ "answer": "Confirm scope closure",
+ "isConfirmation": true
+ }
+ ],
+ "knowledgeItems": [
+ {
+ "kind": "goal",
+ "content": "Track work items from creation to completion with clear ownership and status visibility",
+ "capturedAtTurn": 0
+ },
+ {
+ "kind": "goal",
+ "content": "Provide a complete audit trail for all status changes to satisfy compliance requirements",
+ "capturedAtTurn": 3
+ },
+ {
+ "kind": "goal",
+ "content": "Enable team-wide visibility into work distribution and project progress",
+ "capturedAtTurn": 1
+ },
+ {
+ "kind": "term",
+ "content": "ticket — A trackable unit of work with status, assignee, priority, and description",
+ "capturedAtTurn": 2
+ },
+ {
+ "kind": "term",
+ "content": "assignee — Team member responsible for completing a ticket",
+ "capturedAtTurn": 2
+ },
+ {
+ "kind": "term",
+ "content": "status — Current lifecycle state of a ticket (e.g., open, in-progress, resolved, closed)",
+ "capturedAtTurn": 2
+ },
+ {
+ "kind": "term",
+ "content": "comment — Timestamped note attached to a ticket for discussion and status updates",
+ "capturedAtTurn": 2
+ },
+ {
+ "kind": "context",
+ "content": "Team of approximately 8 developers working across 2-3 projects, plus managers needing read-only visibility",
+ "capturedAtTurn": 1
+ },
+ {
+ "kind": "context",
+ "content": "Currently tracking work in a shared Google Sheet with no audit trail or accountability",
+ "capturedAtTurn": 0
+ },
+ {
+ "kind": "constraint",
+ "content": "Must be simpler than Jira — no custom workflows or custom fields in v1",
+ "capturedAtTurn": 4
+ },
+ {
+ "kind": "constraint",
+ "content": "Audit history required for compliance — every status change must be traceable",
+ "capturedAtTurn": 3
+ },
+ {
+ "kind": "constraint",
+ "content": "Role-based permissions with three roles: admin, developer, viewer",
+ "capturedAtTurn": 4
+ }
+ ],
+ "edges": [
+ {
+ "fromItemIndex": 10,
+ "toItemIndex": 1,
+ "relation": "constrains"
+ },
+ {
+ "fromItemIndex": 8,
+ "toItemIndex": 2,
+ "relation": "derived_from"
+ },
+ {
+ "fromItemIndex": 9,
+ "toItemIndex": 0,
+ "relation": "constrains"
+ }
+ ]
+ },
+ "design-active": {
+ "turns": [
+ {
+ "phase": "scope",
+ "question": "What is the primary goal of this issue tracker? Are you looking to replace an existing tool, or is this a new workflow for the team?",
+ "why": "Understanding whether this replaces an existing process helps scope the migration effort and identify must-have features vs nice-to-haves.",
+ "impact": "high",
+ "answer": "Replace our spreadsheet — we need proper tracking from creation to completion",
+ "options": [
+ {
+ "content": "Replace an existing tool (e.g., spreadsheet, email threads) with a purpose-built tracker",
+ "is_recommended": true
+ },
+ {
+ "content": "Introduce issue tracking as a new practice — the team currently has no formal process",
+ "is_recommended": false
+ },
+ {
+ "content": "Supplement an existing enterprise tool (e.g., Jira) with a lighter alternative for smaller projects",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "We're currently using a shared Google Sheet and it's painful — no history, no accountability"
+ },
+ {
+ "phase": "scope",
+ "question": "Who are the primary users of this system, and how large is the team?",
+ "why": "Team size and user roles directly influence the permission model, notification complexity, and performance requirements.",
+ "impact": "high",
+ "answer": "About 8 developers across 2-3 projects, plus a couple of managers who need visibility",
+ "options": [
+ { "content": "A small team (under 10 people) working on 1-3 projects", "is_recommended": true },
+ {
+ "content": "A medium team (10-30 people) with multiple squads and project leads",
+ "is_recommended": false
+ },
+ {
+ "content": "A cross-functional group including developers, QA, product managers, and stakeholders",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "About 8 developers across 2-3 projects, plus a couple of managers who want read-only dashboards"
+ },
+ {
+ "phase": "scope",
+ "question": "What are the key entities the system needs to track? Think about the core objects users will create and interact with daily.",
+ "why": "Defining the entity model early prevents scope creep — each entity adds CRUD, permissions, and relationship complexity.",
+ "impact": "high",
+ "answer": "Tickets with status, assignee, priority — plus comments for discussion and audit",
+ "options": [
+ {
+ "content": "Tickets (work items) with status, assignee, priority, and description",
+ "is_recommended": true
+ },
+ {
+ "content": "Tickets plus epics or parent groupings for organizing related work",
+ "is_recommended": false
+ },
+ { "content": "Tickets, sprints, and a backlog with capacity planning", "is_recommended": false }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "Each ticket should also support comments — timestamped notes for discussion and status updates"
+ },
+ {
+ "phase": "scope",
+ "question": "Are there compliance or audit requirements for this system? Some teams need to prove who changed what and when.",
+ "why": "Audit trail requirements affect the data model significantly — they may require immutable event logs rather than simple CRUD updates.",
+ "impact": "high",
+ "answer": "Yes, we need full audit history for compliance — every status change must be traceable",
+ "options": [
+ {
+ "content": "Full audit trail required — every status change must record who, what, and when",
+ "is_recommended": true
+ },
+ {
+ "content": "Basic change history is nice to have but not a compliance requirement",
+ "is_recommended": false
+ },
+ {
+ "content": "No audit requirements — simple current-state tracking is sufficient",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "scope",
+ "question": "What constraints should we establish for the v1 scope? Understanding what you explicitly want to exclude helps keep the first version focused.",
+ "why": "Explicit constraints prevent scope creep and help prioritize — knowing what's out helps clarify what's in.",
+ "impact": "medium",
+ "answer": "Keep it simpler than Jira, add role-based permissions with three roles",
+ "options": [
+ {
+ "content": "Keep it deliberately simpler than Jira — no custom workflows, no custom fields in v1",
+ "is_recommended": true
+ },
+ { "content": "Match Jira's core feature set but with a cleaner UX", "is_recommended": false },
+ {
+ "content": "Build a minimal viable tracker and expand based on user feedback",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "We need role-based permissions though — admin, developer, and viewer roles"
+ },
+ {
+ "phase": "scope",
+ "question": "",
+ "answer": "Scope context is well-defined: the team needs a simple issue tracker replacing their spreadsheet, with tickets, comments, audit history, and role-based permissions for ~8 developers.",
+ "isProposal": true
+ },
+ {
+ "phase": "scope",
+ "question": "",
+ "answer": "Confirm scope closure",
+ "isConfirmation": true
+ },
+ {
+ "phase": "design",
+ "question": "How should the primary interface be organized? The team needs to see ticket status at a glance — what layout best supports that?",
+ "why": "The primary view determines the mental model users build around the tool. A board-style layout emphasizes flow; a list-style layout emphasizes filtering and bulk operations.",
+ "impact": "high",
+ "answer": "Kanban board with swimlanes per assignee — that's how we think about work",
+ "options": [
+ {
+ "content": "Kanban board with columns per status and optional swimlanes per assignee",
+ "is_recommended": true
+ },
+ {
+ "content": "Sortable table view with inline editing and bulk actions",
+ "is_recommended": false
+ },
+ {
+ "content": "Combined view with a board as default and table as an alternative",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "design",
+ "question": "How should the audit trail be implemented? This affects both data model complexity and query performance.",
+ "why": "The audit implementation is a core architectural decision — an event-sourced log is more complete but harder to query, while comment-based logging is simpler but may miss some changes.",
+ "impact": "high",
+ "answer": "Comment-based activity log — simpler and keeps everything in one timeline per ticket",
+ "options": [
+ {
+ "content": "Comment-based activity log — status changes appear as system-generated comments in the ticket timeline",
+ "is_recommended": true
+ },
+ {
+ "content": "Separate audit table with structured event records for each field change",
+ "is_recommended": false
+ },
+ {
+ "content": "Event-sourced model where the ticket state is derived from an append-only event log",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "This also means the comment timeline becomes the single source of truth for what happened on a ticket"
+ }
+ ],
+ "knowledgeItems": [
+ {
+ "kind": "goal",
+ "content": "Track work items from creation to completion with clear ownership and status visibility",
+ "capturedAtTurn": 0
+ },
+ {
+ "kind": "goal",
+ "content": "Provide a complete audit trail for all status changes to satisfy compliance requirements",
+ "capturedAtTurn": 3
+ },
+ {
+ "kind": "goal",
+ "content": "Enable team-wide visibility into work distribution and project progress",
+ "capturedAtTurn": 1
+ },
+ {
+ "kind": "term",
+ "content": "ticket — A trackable unit of work with status, assignee, priority, and description",
+ "capturedAtTurn": 2
+ },
+ {
+ "kind": "term",
+ "content": "assignee — Team member responsible for completing a ticket",
+ "capturedAtTurn": 2
+ },
+ {
+ "kind": "term",
+ "content": "status — Current lifecycle state of a ticket (e.g., open, in-progress, resolved, closed)",
+ "capturedAtTurn": 2
+ },
+ {
+ "kind": "term",
+ "content": "comment — Timestamped note attached to a ticket for discussion and status updates",
+ "capturedAtTurn": 2
+ },
+ {
+ "kind": "context",
+ "content": "Team of approximately 8 developers working across 2-3 projects, plus managers needing read-only visibility",
+ "capturedAtTurn": 1
+ },
+ {
+ "kind": "context",
+ "content": "Currently tracking work in a shared Google Sheet with no audit trail or accountability",
+ "capturedAtTurn": 0
+ },
+ {
+ "kind": "constraint",
+ "content": "Must be simpler than Jira — no custom workflows or custom fields in v1",
+ "capturedAtTurn": 4
+ },
+ {
+ "kind": "constraint",
+ "content": "Audit history required for compliance — every status change must be traceable",
+ "capturedAtTurn": 3
+ },
+ {
+ "kind": "constraint",
+ "content": "Role-based permissions with three roles: admin, developer, viewer",
+ "capturedAtTurn": 4
+ },
+ {
+ "kind": "decision",
+ "content": "Kanban board as primary view with columns per status and optional swimlanes per assignee",
+ "rationale": "Matches how the team thinks about work flow; emphasizes status progression over list filtering",
+ "capturedAtTurn": 7
+ },
+ {
+ "kind": "decision",
+ "content": "Comment-based activity log rather than a separate audit table — status changes appear as system-generated comments in the ticket timeline",
+ "rationale": "Keeps everything in one timeline per ticket; simpler data model and the comment timeline becomes the single source of truth",
+ "capturedAtTurn": 8
+ },
+ {
+ "kind": "decision",
+ "content": "API-layer role checks with three roles: admin (full access), developer (create/edit assigned + unassigned), viewer (read-only)",
+ "rationale": "Simpler than row-level security; three roles cover the team's permission needs without over-engineering",
+ "capturedAtTurn": 8
+ },
+ {
+ "kind": "assumption",
+ "content": "Linear status workflow (open → in-progress → resolved → closed) is sufficient for v1 — no custom transitions needed",
+ "capturedAtTurn": 7
+ },
+ {
+ "kind": "assumption",
+ "content": "Three roles (admin, developer, viewer) cover all current and near-term permission needs",
+ "capturedAtTurn": 8
+ },
+ {
+ "kind": "assumption",
+ "content": "Team size will remain under 20 for the foreseeable future, so per-user performance is not a concern",
+ "capturedAtTurn": 8
+ }
+ ],
+ "edges": [
+ {
+ "fromItemIndex": 10,
+ "toItemIndex": 1,
+ "relation": "constrains"
+ },
+ {
+ "fromItemIndex": 8,
+ "toItemIndex": 2,
+ "relation": "derived_from"
+ },
+ {
+ "fromItemIndex": 9,
+ "toItemIndex": 0,
+ "relation": "constrains"
+ },
+ {
+ "fromItemIndex": 12,
+ "toItemIndex": 2,
+ "relation": "depends_on"
+ },
+ {
+ "fromItemIndex": 13,
+ "toItemIndex": 10,
+ "relation": "depends_on"
+ },
+ {
+ "fromItemIndex": 15,
+ "toItemIndex": 12,
+ "relation": "constrains"
+ },
+ {
+ "fromItemIndex": 14,
+ "toItemIndex": 11,
+ "relation": "depends_on"
+ }
+ ]
+ },
+ "requirements-ready": {
+ "turns": [
+ {
+ "phase": "scope",
+ "question": "What is the primary goal of this issue tracker? Are you looking to replace an existing tool, or is this a new workflow for the team?",
+ "why": "Understanding whether this replaces an existing process helps scope the migration effort and identify must-have features vs nice-to-haves.",
+ "impact": "high",
+ "answer": "Replace our spreadsheet — we need proper tracking from creation to completion",
+ "options": [
+ {
+ "content": "Replace an existing tool (e.g., spreadsheet, email threads) with a purpose-built tracker",
+ "is_recommended": true
+ },
+ {
+ "content": "Introduce issue tracking as a new practice — the team currently has no formal process",
+ "is_recommended": false
+ },
+ {
+ "content": "Supplement an existing enterprise tool (e.g., Jira) with a lighter alternative for smaller projects",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "We're currently using a shared Google Sheet and it's painful — no history, no accountability"
+ },
+ {
+ "phase": "scope",
+ "question": "Who are the primary users of this system, and how large is the team?",
+ "why": "Team size and user roles directly influence the permission model, notification complexity, and performance requirements.",
+ "impact": "high",
+ "answer": "About 8 developers across 2-3 projects, plus a couple of managers who need visibility",
+ "options": [
+ { "content": "A small team (under 10 people) working on 1-3 projects", "is_recommended": true },
+ {
+ "content": "A medium team (10-30 people) with multiple squads and project leads",
+ "is_recommended": false
+ },
+ {
+ "content": "A cross-functional group including developers, QA, product managers, and stakeholders",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "About 8 developers across 2-3 projects, plus a couple of managers who want read-only dashboards"
+ },
+ {
+ "phase": "scope",
+ "question": "What are the key entities the system needs to track? Think about the core objects users will create and interact with daily.",
+ "why": "Defining the entity model early prevents scope creep — each entity adds CRUD, permissions, and relationship complexity.",
+ "impact": "high",
+ "answer": "Tickets with status, assignee, priority — plus comments for discussion and audit",
+ "options": [
+ {
+ "content": "Tickets (work items) with status, assignee, priority, and description",
+ "is_recommended": true
+ },
+ {
+ "content": "Tickets plus epics or parent groupings for organizing related work",
+ "is_recommended": false
+ },
+ { "content": "Tickets, sprints, and a backlog with capacity planning", "is_recommended": false }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "Each ticket should also support comments — timestamped notes for discussion and status updates"
+ },
+ {
+ "phase": "scope",
+ "question": "Are there compliance or audit requirements for this system? Some teams need to prove who changed what and when.",
+ "why": "Audit trail requirements affect the data model significantly — they may require immutable event logs rather than simple CRUD updates.",
+ "impact": "high",
+ "answer": "Yes, we need full audit history for compliance — every status change must be traceable",
+ "options": [
+ {
+ "content": "Full audit trail required — every status change must record who, what, and when",
+ "is_recommended": true
+ },
+ {
+ "content": "Basic change history is nice to have but not a compliance requirement",
+ "is_recommended": false
+ },
+ {
+ "content": "No audit requirements — simple current-state tracking is sufficient",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "scope",
+ "question": "What constraints should we establish for the v1 scope? Understanding what you explicitly want to exclude helps keep the first version focused.",
+ "why": "Explicit constraints prevent scope creep and help prioritize — knowing what's out helps clarify what's in.",
+ "impact": "medium",
+ "answer": "Keep it simpler than Jira, add role-based permissions with three roles",
+ "options": [
+ {
+ "content": "Keep it deliberately simpler than Jira — no custom workflows, no custom fields in v1",
+ "is_recommended": true
+ },
+ { "content": "Match Jira's core feature set but with a cleaner UX", "is_recommended": false },
+ {
+ "content": "Build a minimal viable tracker and expand based on user feedback",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "We need role-based permissions though — admin, developer, and viewer roles"
+ },
+ {
+ "phase": "scope",
+ "question": "",
+ "answer": "Scope context is well-defined: the team needs a simple issue tracker replacing their spreadsheet, with tickets, comments, audit history, and role-based permissions for ~8 developers.",
+ "isProposal": true
+ },
+ {
+ "phase": "scope",
+ "question": "",
+ "answer": "Confirm scope closure",
+ "isConfirmation": true
+ },
+ {
+ "phase": "design",
+ "question": "How should the primary interface be organized? The team needs to see ticket status at a glance — what layout best supports that?",
+ "why": "The primary view determines the mental model users build around the tool. A board-style layout emphasizes flow; a list-style layout emphasizes filtering and bulk operations.",
+ "impact": "high",
+ "answer": "Kanban board with swimlanes per assignee — that's how we think about work",
+ "options": [
+ {
+ "content": "Kanban board with columns per status and optional swimlanes per assignee",
+ "is_recommended": true
+ },
+ {
+ "content": "Sortable table view with inline editing and bulk actions",
+ "is_recommended": false
+ },
+ {
+ "content": "Combined view with a board as default and table as an alternative",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "design",
+ "question": "How should the audit trail be implemented? This affects both data model complexity and query performance.",
+ "why": "The audit implementation is a core architectural decision — an event-sourced log is more complete but harder to query, while comment-based logging is simpler but may miss some changes.",
+ "impact": "high",
+ "answer": "Comment-based activity log — simpler and keeps everything in one timeline per ticket",
+ "options": [
+ {
+ "content": "Comment-based activity log — status changes appear as system-generated comments in the ticket timeline",
+ "is_recommended": true
+ },
+ {
+ "content": "Separate audit table with structured event records for each field change",
+ "is_recommended": false
+ },
+ {
+ "content": "Event-sourced model where the ticket state is derived from an append-only event log",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "This also means the comment timeline becomes the single source of truth for what happened on a ticket"
+ },
+ {
+ "phase": "design",
+ "question": "",
+ "answer": "Design commitments are captured: Kanban board as primary view, comment-based activity log for audit, and API-layer role checks with three roles.",
+ "isProposal": true
+ },
+ {
+ "phase": "design",
+ "question": "",
+ "answer": "Confirm design closure",
+ "isConfirmation": true
+ },
+ {
+ "phase": "requirements",
+ "question": "Based on the scope and design commitments, I've synthesized the following requirement. Does this accurately capture the ticket creation workflow you need?",
+ "why": "Validating synthesized requirements against user intent ensures completeness and catches assumptions before they become implementation bugs.",
+ "impact": "high",
+ "answer": "Yes, that captures it — approve this requirement",
+ "options": [
+ {
+ "content": "Approve — this requirement accurately captures the needed functionality",
+ "is_recommended": true
+ },
+ {
+ "content": "Needs revision — the requirement is close but missing important details",
+ "is_recommended": false
+ },
+ { "content": "Reject — this requirement doesn't belong in v1 scope", "is_recommended": false }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "requirements",
+ "question": "This requirement covers the audit trail implementation. Given your compliance needs and the comment-based design decision, does this requirement capture the right granularity?",
+ "why": "Audit requirements need precise specification — vague audit requirements lead to either over-engineering or compliance gaps.",
+ "impact": "high",
+ "answer": "Approve — status change audit logging is essential",
+ "options": [
+ {
+ "content": "Approve — this captures the audit trail requirement at the right level of detail",
+ "is_recommended": true
+ },
+ {
+ "content": "Needs more detail — specify exactly which fields trigger audit entries",
+ "is_recommended": false
+ },
+ { "content": "Too granular — simplify to just tracking status changes", "is_recommended": false }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "requirements",
+ "question": "This requirement covers the permission model. Given the three-role design decision, does this capture the visibility rules correctly?",
+ "why": "Permission requirements are security-critical — ambiguous permission specs are a common source of authorization vulnerabilities.",
+ "impact": "high",
+ "answer": "Approve — the three-tier visibility model is correct",
+ "options": [
+ {
+ "content": "Approve — the role-based visibility rules are correctly specified",
+ "is_recommended": true
+ },
+ {
+ "content": "Needs adjustment — developers should see all tickets, not just assigned ones",
+ "is_recommended": false
+ },
+ { "content": "Reject — we need a more granular permission model", "is_recommended": false }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "requirements",
+ "question": "This requirement covers ticket list filtering. Is this the right set of filter dimensions for v1?",
+ "why": "Filter capabilities directly affect usability — too few filters make the tool frustrating, too many add UI complexity.",
+ "impact": "medium",
+ "answer": "Approve — those four filter dimensions cover our daily needs",
+ "options": [
+ {
+ "content": "Approve — status, assignee, priority, and date filters are sufficient for v1",
+ "is_recommended": true
+ },
+ {
+ "content": "Add text search across title and description as a fifth filter",
+ "is_recommended": false
+ },
+ { "content": "Remove date filtering — we rarely need it", "is_recommended": false }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "requirements",
+ "question": "This requirement covers CSV export for reporting. Given your constraint to keep things simpler than Jira, should CSV export be included in v1?",
+ "why": "Export features are often requested but rarely critical for v1 — deferring non-essential features keeps the initial scope tight.",
+ "impact": "low",
+ "answer": "Reject this one — CSV export can wait for v2",
+ "options": [
+ {
+ "content": "Approve — CSV export is needed from day one for reporting workflows",
+ "is_recommended": false
+ },
+ {
+ "content": "Reject — defer CSV export to v2; the team can use the UI for now",
+ "is_recommended": true
+ },
+ {
+ "content": "Simplify — just support copy-paste of filtered views instead of full CSV export",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [1],
+ "freeText": "We can manage without export for now — the spreadsheet transition doesn't require it"
+ },
+ {
+ "phase": "requirements",
+ "question": "",
+ "answer": "Requirements are reviewed: 4 approved (ticket CRUD, audit logging, role-based visibility, filtering) and 1 rejected (CSV export deferred to v2). The requirement set has explicit review coverage.",
+ "isProposal": true
+ },
+ {
+ "phase": "requirements",
+ "question": "",
+ "answer": "Confirm requirements closure",
+ "isConfirmation": true
+ }
+ ],
+ "knowledgeItems": [
+ {
+ "kind": "goal",
+ "content": "Track work items from creation to completion with clear ownership and status visibility",
+ "capturedAtTurn": 0
+ },
+ {
+ "kind": "goal",
+ "content": "Provide a complete audit trail for all status changes to satisfy compliance requirements",
+ "capturedAtTurn": 3
+ },
+ {
+ "kind": "goal",
+ "content": "Enable team-wide visibility into work distribution and project progress",
+ "capturedAtTurn": 1
+ },
+ {
+ "kind": "term",
+ "content": "ticket — A trackable unit of work with status, assignee, priority, and description",
+ "capturedAtTurn": 2
+ },
+ {
+ "kind": "term",
+ "content": "assignee — Team member responsible for completing a ticket",
+ "capturedAtTurn": 2
+ },
+ {
+ "kind": "term",
+ "content": "status — Current lifecycle state of a ticket (e.g., open, in-progress, resolved, closed)",
+ "capturedAtTurn": 2
+ },
+ {
+ "kind": "term",
+ "content": "comment — Timestamped note attached to a ticket for discussion and status updates",
+ "capturedAtTurn": 2
+ },
+ {
+ "kind": "context",
+ "content": "Team of approximately 8 developers working across 2-3 projects, plus managers needing read-only visibility",
+ "capturedAtTurn": 1
+ },
+ {
+ "kind": "context",
+ "content": "Currently tracking work in a shared Google Sheet with no audit trail or accountability",
+ "capturedAtTurn": 0
+ },
+ {
+ "kind": "constraint",
+ "content": "Must be simpler than Jira — no custom workflows or custom fields in v1",
+ "capturedAtTurn": 4
+ },
+ {
+ "kind": "constraint",
+ "content": "Audit history required for compliance — every status change must be traceable",
+ "capturedAtTurn": 3
+ },
+ {
+ "kind": "constraint",
+ "content": "Role-based permissions with three roles: admin, developer, viewer",
+ "capturedAtTurn": 4
+ },
+ {
+ "kind": "decision",
+ "content": "Kanban board as primary view with columns per status and optional swimlanes per assignee",
+ "rationale": "Matches how the team thinks about work flow; emphasizes status progression over list filtering",
+ "capturedAtTurn": 7
+ },
+ {
+ "kind": "decision",
+ "content": "Comment-based activity log rather than a separate audit table — status changes appear as system-generated comments in the ticket timeline",
+ "rationale": "Keeps everything in one timeline per ticket; simpler data model and the comment timeline becomes the single source of truth",
+ "capturedAtTurn": 8
+ },
+ {
+ "kind": "decision",
+ "content": "API-layer role checks with three roles: admin (full access), developer (create/edit assigned + unassigned), viewer (read-only)",
+ "rationale": "Simpler than row-level security; three roles cover the team's permission needs without over-engineering",
+ "capturedAtTurn": 8
+ },
+ {
+ "kind": "assumption",
+ "content": "Linear status workflow (open → in-progress → resolved → closed) is sufficient for v1 — no custom transitions needed",
+ "capturedAtTurn": 7
+ },
+ {
+ "kind": "assumption",
+ "content": "Three roles (admin, developer, viewer) cover all current and near-term permission needs",
+ "capturedAtTurn": 8
+ },
+ {
+ "kind": "assumption",
+ "content": "Team size will remain under 20 for the foreseeable future, so per-user performance is not a concern",
+ "capturedAtTurn": 8
+ },
+ {
+ "kind": "requirement",
+ "content": "Create, edit, and close tickets with required fields: title, description, priority, and assignee",
+ "capturedAtTurn": 11,
+ "reviewAction": "reviewed",
+ "reviewedAtTurn": 11
+ },
+ {
+ "kind": "requirement",
+ "content": "Status change creates a timestamped audit log entry recording the actor identity, previous status, and new status",
+ "capturedAtTurn": 12,
+ "reviewAction": "reviewed",
+ "reviewedAtTurn": 12
+ },
+ {
+ "kind": "requirement",
+ "content": "Role-based visibility: admins see all tickets and settings, developers see assigned and unassigned tickets, viewers have read-only access",
+ "capturedAtTurn": 13,
+ "reviewAction": "reviewed",
+ "reviewedAtTurn": 13
+ },
+ {
+ "kind": "requirement",
+ "content": "Filter and sort ticket list by status, assignee, priority, and creation date",
+ "capturedAtTurn": 14,
+ "reviewAction": "reviewed",
+ "reviewedAtTurn": 14
+ },
+ {
+ "kind": "requirement",
+ "content": "Export ticket data as CSV for reporting",
+ "capturedAtTurn": 15,
+ "reviewAction": "rejected",
+ "reviewedAtTurn": 15
+ }
+ ],
+ "edges": [
+ {
+ "fromItemIndex": 10,
+ "toItemIndex": 1,
+ "relation": "constrains"
+ },
+ {
+ "fromItemIndex": 8,
+ "toItemIndex": 2,
+ "relation": "derived_from"
+ },
+ {
+ "fromItemIndex": 9,
+ "toItemIndex": 0,
+ "relation": "constrains"
+ },
+ {
+ "fromItemIndex": 12,
+ "toItemIndex": 2,
+ "relation": "depends_on"
+ },
+ {
+ "fromItemIndex": 13,
+ "toItemIndex": 10,
+ "relation": "depends_on"
+ },
+ {
+ "fromItemIndex": 15,
+ "toItemIndex": 12,
+ "relation": "constrains"
+ },
+ {
+ "fromItemIndex": 14,
+ "toItemIndex": 11,
+ "relation": "depends_on"
+ },
+ {
+ "fromItemIndex": 18,
+ "toItemIndex": 0,
+ "relation": "refines"
+ },
+ {
+ "fromItemIndex": 19,
+ "toItemIndex": 10,
+ "relation": "verifies"
+ },
+ {
+ "fromItemIndex": 20,
+ "toItemIndex": 11,
+ "relation": "derived_from"
+ }
+ ]
+ },
+ "criteria-ready": {
+ "turns": [
+ {
+ "phase": "scope",
+ "question": "What is the primary goal of this issue tracker? Are you looking to replace an existing tool, or is this a new workflow for the team?",
+ "why": "Understanding whether this replaces an existing process helps scope the migration effort and identify must-have features vs nice-to-haves.",
+ "impact": "high",
+ "answer": "Replace our spreadsheet — we need proper tracking from creation to completion",
+ "options": [
+ {
+ "content": "Replace an existing tool (e.g., spreadsheet, email threads) with a purpose-built tracker",
+ "is_recommended": true
+ },
+ {
+ "content": "Introduce issue tracking as a new practice — the team currently has no formal process",
+ "is_recommended": false
+ },
+ {
+ "content": "Supplement an existing enterprise tool (e.g., Jira) with a lighter alternative for smaller projects",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "We're currently using a shared Google Sheet and it's painful — no history, no accountability"
+ },
+ {
+ "phase": "scope",
+ "question": "Who are the primary users of this system, and how large is the team?",
+ "why": "Team size and user roles directly influence the permission model, notification complexity, and performance requirements.",
+ "impact": "high",
+ "answer": "About 8 developers across 2-3 projects, plus a couple of managers who need visibility",
+ "options": [
+ { "content": "A small team (under 10 people) working on 1-3 projects", "is_recommended": true },
+ {
+ "content": "A medium team (10-30 people) with multiple squads and project leads",
+ "is_recommended": false
+ },
+ {
+ "content": "A cross-functional group including developers, QA, product managers, and stakeholders",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "About 8 developers across 2-3 projects, plus a couple of managers who want read-only dashboards"
+ },
+ {
+ "phase": "scope",
+ "question": "What are the key entities the system needs to track? Think about the core objects users will create and interact with daily.",
+ "why": "Defining the entity model early prevents scope creep — each entity adds CRUD, permissions, and relationship complexity.",
+ "impact": "high",
+ "answer": "Tickets with status, assignee, priority — plus comments for discussion and audit",
+ "options": [
+ {
+ "content": "Tickets (work items) with status, assignee, priority, and description",
+ "is_recommended": true
+ },
+ {
+ "content": "Tickets plus epics or parent groupings for organizing related work",
+ "is_recommended": false
+ },
+ { "content": "Tickets, sprints, and a backlog with capacity planning", "is_recommended": false }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "Each ticket should also support comments — timestamped notes for discussion and status updates"
+ },
+ {
+ "phase": "scope",
+ "question": "Are there compliance or audit requirements for this system? Some teams need to prove who changed what and when.",
+ "why": "Audit trail requirements affect the data model significantly — they may require immutable event logs rather than simple CRUD updates.",
+ "impact": "high",
+ "answer": "Yes, we need full audit history for compliance — every status change must be traceable",
+ "options": [
+ {
+ "content": "Full audit trail required — every status change must record who, what, and when",
+ "is_recommended": true
+ },
+ {
+ "content": "Basic change history is nice to have but not a compliance requirement",
+ "is_recommended": false
+ },
+ {
+ "content": "No audit requirements — simple current-state tracking is sufficient",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "scope",
+ "question": "What constraints should we establish for the v1 scope? Understanding what you explicitly want to exclude helps keep the first version focused.",
+ "why": "Explicit constraints prevent scope creep and help prioritize — knowing what's out helps clarify what's in.",
+ "impact": "medium",
+ "answer": "Keep it simpler than Jira, add role-based permissions with three roles",
+ "options": [
+ {
+ "content": "Keep it deliberately simpler than Jira — no custom workflows, no custom fields in v1",
+ "is_recommended": true
+ },
+ { "content": "Match Jira's core feature set but with a cleaner UX", "is_recommended": false },
+ {
+ "content": "Build a minimal viable tracker and expand based on user feedback",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "We need role-based permissions though — admin, developer, and viewer roles"
+ },
+ {
+ "phase": "scope",
+ "question": "",
+ "answer": "Scope context is well-defined: the team needs a simple issue tracker replacing their spreadsheet, with tickets, comments, audit history, and role-based permissions for ~8 developers.",
+ "isProposal": true
+ },
+ {
+ "phase": "scope",
+ "question": "",
+ "answer": "Confirm scope closure",
+ "isConfirmation": true
+ },
+ {
+ "phase": "design",
+ "question": "How should the primary interface be organized? The team needs to see ticket status at a glance — what layout best supports that?",
+ "why": "The primary view determines the mental model users build around the tool. A board-style layout emphasizes flow; a list-style layout emphasizes filtering and bulk operations.",
+ "impact": "high",
+ "answer": "Kanban board with swimlanes per assignee — that's how we think about work",
+ "options": [
+ {
+ "content": "Kanban board with columns per status and optional swimlanes per assignee",
+ "is_recommended": true
+ },
+ {
+ "content": "Sortable table view with inline editing and bulk actions",
+ "is_recommended": false
+ },
+ {
+ "content": "Combined view with a board as default and table as an alternative",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "design",
+ "question": "How should the audit trail be implemented? This affects both data model complexity and query performance.",
+ "why": "The audit implementation is a core architectural decision — an event-sourced log is more complete but harder to query, while comment-based logging is simpler but may miss some changes.",
+ "impact": "high",
+ "answer": "Comment-based activity log — simpler and keeps everything in one timeline per ticket",
+ "options": [
+ {
+ "content": "Comment-based activity log — status changes appear as system-generated comments in the ticket timeline",
+ "is_recommended": true
+ },
+ {
+ "content": "Separate audit table with structured event records for each field change",
+ "is_recommended": false
+ },
+ {
+ "content": "Event-sourced model where the ticket state is derived from an append-only event log",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "This also means the comment timeline becomes the single source of truth for what happened on a ticket"
+ },
+ {
+ "phase": "design",
+ "question": "",
+ "answer": "Design commitments are captured: Kanban board as primary view, comment-based activity log for audit, and API-layer role checks with three roles.",
+ "isProposal": true
+ },
+ {
+ "phase": "design",
+ "question": "",
+ "answer": "Confirm design closure",
+ "isConfirmation": true
+ },
+ {
+ "phase": "requirements",
+ "question": "Based on the scope and design commitments, I've synthesized the following requirement. Does this accurately capture the ticket creation workflow you need?",
+ "why": "Validating synthesized requirements against user intent ensures completeness and catches assumptions before they become implementation bugs.",
+ "impact": "high",
+ "answer": "Yes, that captures it — approve this requirement",
+ "options": [
+ {
+ "content": "Approve — this requirement accurately captures the needed functionality",
+ "is_recommended": true
+ },
+ {
+ "content": "Needs revision — the requirement is close but missing important details",
+ "is_recommended": false
+ },
+ { "content": "Reject — this requirement doesn't belong in v1 scope", "is_recommended": false }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "requirements",
+ "question": "This requirement covers the audit trail implementation. Given your compliance needs and the comment-based design decision, does this requirement capture the right granularity?",
+ "why": "Audit requirements need precise specification — vague audit requirements lead to either over-engineering or compliance gaps.",
+ "impact": "high",
+ "answer": "Approve — status change audit logging is essential",
+ "options": [
+ {
+ "content": "Approve — this captures the audit trail requirement at the right level of detail",
+ "is_recommended": true
+ },
+ {
+ "content": "Needs more detail — specify exactly which fields trigger audit entries",
+ "is_recommended": false
+ },
+ { "content": "Too granular — simplify to just tracking status changes", "is_recommended": false }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "requirements",
+ "question": "This requirement covers the permission model. Given the three-role design decision, does this capture the visibility rules correctly?",
+ "why": "Permission requirements are security-critical — ambiguous permission specs are a common source of authorization vulnerabilities.",
+ "impact": "high",
+ "answer": "Approve — the three-tier visibility model is correct",
+ "options": [
+ {
+ "content": "Approve — the role-based visibility rules are correctly specified",
+ "is_recommended": true
+ },
+ {
+ "content": "Needs adjustment — developers should see all tickets, not just assigned ones",
+ "is_recommended": false
+ },
+ { "content": "Reject — we need a more granular permission model", "is_recommended": false }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "requirements",
+ "question": "This requirement covers ticket list filtering. Is this the right set of filter dimensions for v1?",
+ "why": "Filter capabilities directly affect usability — too few filters make the tool frustrating, too many add UI complexity.",
+ "impact": "medium",
+ "answer": "Approve — those four filter dimensions cover our daily needs",
+ "options": [
+ {
+ "content": "Approve — status, assignee, priority, and date filters are sufficient for v1",
+ "is_recommended": true
+ },
+ {
+ "content": "Add text search across title and description as a fifth filter",
+ "is_recommended": false
+ },
+ { "content": "Remove date filtering — we rarely need it", "is_recommended": false }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "requirements",
+ "question": "This requirement covers CSV export for reporting. Given your constraint to keep things simpler than Jira, should CSV export be included in v1?",
+ "why": "Export features are often requested but rarely critical for v1 — deferring non-essential features keeps the initial scope tight.",
+ "impact": "low",
+ "answer": "Reject this one — CSV export can wait for v2",
+ "options": [
+ {
+ "content": "Approve — CSV export is needed from day one for reporting workflows",
+ "is_recommended": false
+ },
+ {
+ "content": "Reject — defer CSV export to v2; the team can use the UI for now",
+ "is_recommended": true
+ },
+ {
+ "content": "Simplify — just support copy-paste of filtered views instead of full CSV export",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [1],
+ "freeText": "We can manage without export for now — the spreadsheet transition doesn't require it"
+ },
+ {
+ "phase": "requirements",
+ "question": "",
+ "answer": "Requirements are reviewed: 4 approved (ticket CRUD, audit logging, role-based visibility, filtering) and 1 rejected (CSV export deferred to v2). The requirement set has explicit review coverage.",
+ "isProposal": true
+ },
+ {
+ "phase": "requirements",
+ "question": "",
+ "answer": "Confirm requirements closure",
+ "isConfirmation": true
+ },
+ {
+ "phase": "criteria",
+ "question": "This criterion verifies the audit trail requirement. Does it capture the right level of verification precision for the status change audit log?",
+ "why": "Audit criteria must be objectively testable — vague criteria lead to ambiguous acceptance testing and compliance gaps.",
+ "impact": "high",
+ "answer": "Approve — actor identity and ISO 8601 timestamp are the right verification points",
+ "options": [
+ { "content": "Approve — this criterion is precise and testable", "is_recommended": true },
+ {
+ "content": "Needs refinement — add verification of the previous and new status values",
+ "is_recommended": false
+ },
+ { "content": "Too strict — ISO 8601 is overkill for an internal tool", "is_recommended": false }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "criteria",
+ "question": "This criterion verifies the permission model. Does the negative-case framing capture the security boundary correctly?",
+ "why": "Security criteria work best as negative constraints — verifying what cannot happen is often more important than what can.",
+ "impact": "high",
+ "answer": "Approve — the negative-case framing is exactly right for security boundaries",
+ "options": [
+ {
+ "content": "Approve — non-admin deletion and audit modification are the right security boundaries to verify",
+ "is_recommended": true
+ },
+ {
+ "content": "Expand — also verify that viewers cannot create or edit tickets",
+ "is_recommended": false
+ },
+ {
+ "content": "Simplify — just verify that role checks exist, not specific denied operations",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "criteria",
+ "question": "This criterion covers ticket list performance. Is 2 seconds with 500 tickets the right performance bar for v1?",
+ "why": "Performance criteria need concrete thresholds — without them, 'fast enough' is subjective and untestable.",
+ "impact": "medium",
+ "answer": "Approve — 2 seconds for 500 tickets is a reasonable v1 bar",
+ "options": [
+ {
+ "content": "Approve — 2 seconds for 500 tickets with any filter is a reasonable v1 target",
+ "is_recommended": true
+ },
+ {
+ "content": "Tighten — should be under 1 second for the team size we're targeting",
+ "is_recommended": false
+ },
+ {
+ "content": "Relax — 5 seconds is acceptable for v1 since the team is small",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "criteria",
+ "question": "This criterion covers CSV export verification. Since the CSV export requirement was rejected, should this criterion also be rejected?",
+ "why": "Criteria that verify rejected requirements should typically be rejected to maintain consistency between the requirement and criteria sets.",
+ "impact": "low",
+ "answer": "Reject — this is coupled to the rejected CSV export requirement",
+ "options": [
+ {
+ "content": "Reject — this criterion is moot since the underlying requirement was deferred to v2",
+ "is_recommended": true
+ },
+ {
+ "content": "Keep as deferred — mark it for v2 alongside the requirement",
+ "is_recommended": false
+ },
+ {
+ "content": "Approve anyway — we might add CSV export during implementation",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ }
+ ],
+ "knowledgeItems": [
+ {
+ "kind": "goal",
+ "content": "Track work items from creation to completion with clear ownership and status visibility",
+ "capturedAtTurn": 0
+ },
+ {
+ "kind": "goal",
+ "content": "Provide a complete audit trail for all status changes to satisfy compliance requirements",
+ "capturedAtTurn": 3
+ },
+ {
+ "kind": "goal",
+ "content": "Enable team-wide visibility into work distribution and project progress",
+ "capturedAtTurn": 1
+ },
+ {
+ "kind": "term",
+ "content": "ticket — A trackable unit of work with status, assignee, priority, and description",
+ "capturedAtTurn": 2
+ },
+ {
+ "kind": "term",
+ "content": "assignee — Team member responsible for completing a ticket",
+ "capturedAtTurn": 2
+ },
+ {
+ "kind": "term",
+ "content": "status — Current lifecycle state of a ticket (e.g., open, in-progress, resolved, closed)",
+ "capturedAtTurn": 2
+ },
+ {
+ "kind": "term",
+ "content": "comment — Timestamped note attached to a ticket for discussion and status updates",
+ "capturedAtTurn": 2
+ },
+ {
+ "kind": "context",
+ "content": "Team of approximately 8 developers working across 2-3 projects, plus managers needing read-only visibility",
+ "capturedAtTurn": 1
+ },
+ {
+ "kind": "context",
+ "content": "Currently tracking work in a shared Google Sheet with no audit trail or accountability",
+ "capturedAtTurn": 0
+ },
+ {
+ "kind": "constraint",
+ "content": "Must be simpler than Jira — no custom workflows or custom fields in v1",
+ "capturedAtTurn": 4
+ },
+ {
+ "kind": "constraint",
+ "content": "Audit history required for compliance — every status change must be traceable",
+ "capturedAtTurn": 3
+ },
+ {
+ "kind": "constraint",
+ "content": "Role-based permissions with three roles: admin, developer, viewer",
+ "capturedAtTurn": 4
+ },
+ {
+ "kind": "decision",
+ "content": "Kanban board as primary view with columns per status and optional swimlanes per assignee",
+ "rationale": "Matches how the team thinks about work flow; emphasizes status progression over list filtering",
+ "capturedAtTurn": 7
+ },
+ {
+ "kind": "decision",
+ "content": "Comment-based activity log rather than a separate audit table — status changes appear as system-generated comments in the ticket timeline",
+ "rationale": "Keeps everything in one timeline per ticket; simpler data model and the comment timeline becomes the single source of truth",
+ "capturedAtTurn": 8
+ },
+ {
+ "kind": "decision",
+ "content": "API-layer role checks with three roles: admin (full access), developer (create/edit assigned + unassigned), viewer (read-only)",
+ "rationale": "Simpler than row-level security; three roles cover the team's permission needs without over-engineering",
+ "capturedAtTurn": 8
+ },
+ {
+ "kind": "assumption",
+ "content": "Linear status workflow (open → in-progress → resolved → closed) is sufficient for v1 — no custom transitions needed",
+ "capturedAtTurn": 7
+ },
+ {
+ "kind": "assumption",
+ "content": "Three roles (admin, developer, viewer) cover all current and near-term permission needs",
+ "capturedAtTurn": 8
+ },
+ {
+ "kind": "assumption",
+ "content": "Team size will remain under 20 for the foreseeable future, so per-user performance is not a concern",
+ "capturedAtTurn": 8
+ },
+ {
+ "kind": "requirement",
+ "content": "Create, edit, and close tickets with required fields: title, description, priority, and assignee",
+ "capturedAtTurn": 11,
+ "reviewAction": "reviewed",
+ "reviewedAtTurn": 11
+ },
+ {
+ "kind": "requirement",
+ "content": "Status change creates a timestamped audit log entry recording the actor identity, previous status, and new status",
+ "capturedAtTurn": 12,
+ "reviewAction": "reviewed",
+ "reviewedAtTurn": 12
+ },
+ {
+ "kind": "requirement",
+ "content": "Role-based visibility: admins see all tickets and settings, developers see assigned and unassigned tickets, viewers have read-only access",
+ "capturedAtTurn": 13,
+ "reviewAction": "reviewed",
+ "reviewedAtTurn": 13
+ },
+ {
+ "kind": "requirement",
+ "content": "Filter and sort ticket list by status, assignee, priority, and creation date",
+ "capturedAtTurn": 14,
+ "reviewAction": "reviewed",
+ "reviewedAtTurn": 14
+ },
+ {
+ "kind": "requirement",
+ "content": "Export ticket data as CSV for reporting",
+ "capturedAtTurn": 15,
+ "reviewAction": "rejected",
+ "reviewedAtTurn": 15
+ },
+ {
+ "kind": "criterion",
+ "content": "Every status change records the actor identity and ISO 8601 timestamp in the audit log",
+ "capturedAtTurn": 19,
+ "reviewAction": "reviewed",
+ "reviewedAtTurn": 19
+ },
+ {
+ "kind": "criterion",
+ "content": "Non-admin users cannot delete tickets or modify audit entries",
+ "capturedAtTurn": 20,
+ "reviewAction": "reviewed",
+ "reviewedAtTurn": 20
+ },
+ {
+ "kind": "criterion",
+ "content": "Ticket list loads within 2 seconds for up to 500 tickets with any filter combination",
+ "capturedAtTurn": 21,
+ "reviewAction": "reviewed",
+ "reviewedAtTurn": 21
+ },
+ {
+ "kind": "criterion",
+ "content": "CSV export includes all visible fields and respects role-based visibility filters",
+ "capturedAtTurn": 22,
+ "reviewAction": "rejected",
+ "reviewedAtTurn": 22
+ }
+ ],
+ "edges": [
+ {
+ "fromItemIndex": 10,
+ "toItemIndex": 1,
+ "relation": "constrains"
+ },
+ {
+ "fromItemIndex": 8,
+ "toItemIndex": 2,
+ "relation": "derived_from"
+ },
+ {
+ "fromItemIndex": 9,
+ "toItemIndex": 0,
+ "relation": "constrains"
+ },
+ {
+ "fromItemIndex": 12,
+ "toItemIndex": 2,
+ "relation": "depends_on"
+ },
+ {
+ "fromItemIndex": 13,
+ "toItemIndex": 10,
+ "relation": "depends_on"
+ },
+ {
+ "fromItemIndex": 15,
+ "toItemIndex": 12,
+ "relation": "constrains"
+ },
+ {
+ "fromItemIndex": 14,
+ "toItemIndex": 11,
+ "relation": "depends_on"
+ },
+ {
+ "fromItemIndex": 18,
+ "toItemIndex": 0,
+ "relation": "refines"
+ },
+ {
+ "fromItemIndex": 19,
+ "toItemIndex": 10,
+ "relation": "verifies"
+ },
+ {
+ "fromItemIndex": 20,
+ "toItemIndex": 11,
+ "relation": "derived_from"
+ },
+ {
+ "fromItemIndex": 23,
+ "toItemIndex": 19,
+ "relation": "verifies"
+ },
+ {
+ "fromItemIndex": 24,
+ "toItemIndex": 20,
+ "relation": "verifies"
+ },
+ {
+ "fromItemIndex": 25,
+ "toItemIndex": 21,
+ "relation": "constrains"
+ },
+ {
+ "fromItemIndex": 26,
+ "toItemIndex": 22,
+ "relation": "verifies"
+ }
+ ]
+ },
+ "all-phases-closed": {
+ "turns": [
+ {
+ "phase": "scope",
+ "question": "What is the primary goal of this issue tracker? Are you looking to replace an existing tool, or is this a new workflow for the team?",
+ "why": "Understanding whether this replaces an existing process helps scope the migration effort and identify must-have features vs nice-to-haves.",
+ "impact": "high",
+ "answer": "Replace our spreadsheet — we need proper tracking from creation to completion",
+ "options": [
+ {
+ "content": "Replace an existing tool (e.g., spreadsheet, email threads) with a purpose-built tracker",
+ "is_recommended": true
+ },
+ {
+ "content": "Introduce issue tracking as a new practice — the team currently has no formal process",
+ "is_recommended": false
+ },
+ {
+ "content": "Supplement an existing enterprise tool (e.g., Jira) with a lighter alternative for smaller projects",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "We're currently using a shared Google Sheet and it's painful — no history, no accountability"
+ },
+ {
+ "phase": "scope",
+ "question": "Who are the primary users of this system, and how large is the team?",
+ "why": "Team size and user roles directly influence the permission model, notification complexity, and performance requirements.",
+ "impact": "high",
+ "answer": "About 8 developers across 2-3 projects, plus a couple of managers who need visibility",
+ "options": [
+ { "content": "A small team (under 10 people) working on 1-3 projects", "is_recommended": true },
+ {
+ "content": "A medium team (10-30 people) with multiple squads and project leads",
+ "is_recommended": false
+ },
+ {
+ "content": "A cross-functional group including developers, QA, product managers, and stakeholders",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "About 8 developers across 2-3 projects, plus a couple of managers who want read-only dashboards"
+ },
+ {
+ "phase": "scope",
+ "question": "What are the key entities the system needs to track? Think about the core objects users will create and interact with daily.",
+ "why": "Defining the entity model early prevents scope creep — each entity adds CRUD, permissions, and relationship complexity.",
+ "impact": "high",
+ "answer": "Tickets with status, assignee, priority — plus comments for discussion and audit",
+ "options": [
+ {
+ "content": "Tickets (work items) with status, assignee, priority, and description",
+ "is_recommended": true
+ },
+ {
+ "content": "Tickets plus epics or parent groupings for organizing related work",
+ "is_recommended": false
+ },
+ { "content": "Tickets, sprints, and a backlog with capacity planning", "is_recommended": false }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "Each ticket should also support comments — timestamped notes for discussion and status updates"
+ },
+ {
+ "phase": "scope",
+ "question": "Are there compliance or audit requirements for this system? Some teams need to prove who changed what and when.",
+ "why": "Audit trail requirements affect the data model significantly — they may require immutable event logs rather than simple CRUD updates.",
+ "impact": "high",
+ "answer": "Yes, we need full audit history for compliance — every status change must be traceable",
+ "options": [
+ {
+ "content": "Full audit trail required — every status change must record who, what, and when",
+ "is_recommended": true
+ },
+ {
+ "content": "Basic change history is nice to have but not a compliance requirement",
+ "is_recommended": false
+ },
+ {
+ "content": "No audit requirements — simple current-state tracking is sufficient",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "scope",
+ "question": "What constraints should we establish for the v1 scope? Understanding what you explicitly want to exclude helps keep the first version focused.",
+ "why": "Explicit constraints prevent scope creep and help prioritize — knowing what's out helps clarify what's in.",
+ "impact": "medium",
+ "answer": "Keep it simpler than Jira, add role-based permissions with three roles",
+ "options": [
+ {
+ "content": "Keep it deliberately simpler than Jira — no custom workflows, no custom fields in v1",
+ "is_recommended": true
+ },
+ { "content": "Match Jira's core feature set but with a cleaner UX", "is_recommended": false },
+ {
+ "content": "Build a minimal viable tracker and expand based on user feedback",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "We need role-based permissions though — admin, developer, and viewer roles"
+ },
+ {
+ "phase": "scope",
+ "question": "",
+ "answer": "Scope context is well-defined: the team needs a simple issue tracker replacing their spreadsheet, with tickets, comments, audit history, and role-based permissions for ~8 developers.",
+ "isProposal": true
+ },
+ {
+ "phase": "scope",
+ "question": "",
+ "answer": "Confirm scope closure",
+ "isConfirmation": true
+ },
+ {
+ "phase": "design",
+ "question": "How should the primary interface be organized? The team needs to see ticket status at a glance — what layout best supports that?",
+ "why": "The primary view determines the mental model users build around the tool. A board-style layout emphasizes flow; a list-style layout emphasizes filtering and bulk operations.",
+ "impact": "high",
+ "answer": "Kanban board with swimlanes per assignee — that's how we think about work",
+ "options": [
+ {
+ "content": "Kanban board with columns per status and optional swimlanes per assignee",
+ "is_recommended": true
+ },
+ {
+ "content": "Sortable table view with inline editing and bulk actions",
+ "is_recommended": false
+ },
+ {
+ "content": "Combined view with a board as default and table as an alternative",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "design",
+ "question": "How should the audit trail be implemented? This affects both data model complexity and query performance.",
+ "why": "The audit implementation is a core architectural decision — an event-sourced log is more complete but harder to query, while comment-based logging is simpler but may miss some changes.",
+ "impact": "high",
+ "answer": "Comment-based activity log — simpler and keeps everything in one timeline per ticket",
+ "options": [
+ {
+ "content": "Comment-based activity log — status changes appear as system-generated comments in the ticket timeline",
+ "is_recommended": true
+ },
+ {
+ "content": "Separate audit table with structured event records for each field change",
+ "is_recommended": false
+ },
+ {
+ "content": "Event-sourced model where the ticket state is derived from an append-only event log",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": "This also means the comment timeline becomes the single source of truth for what happened on a ticket"
+ },
+ {
+ "phase": "design",
+ "question": "",
+ "answer": "Design commitments are captured: Kanban board as primary view, comment-based activity log for audit, and API-layer role checks with three roles.",
+ "isProposal": true
+ },
+ {
+ "phase": "design",
+ "question": "",
+ "answer": "Confirm design closure",
+ "isConfirmation": true
+ },
+ {
+ "phase": "requirements",
+ "question": "Based on the scope and design commitments, I've synthesized the following requirement. Does this accurately capture the ticket creation workflow you need?",
+ "why": "Validating synthesized requirements against user intent ensures completeness and catches assumptions before they become implementation bugs.",
+ "impact": "high",
+ "answer": "Yes, that captures it — approve this requirement",
+ "options": [
+ {
+ "content": "Approve — this requirement accurately captures the needed functionality",
+ "is_recommended": true
+ },
+ {
+ "content": "Needs revision — the requirement is close but missing important details",
+ "is_recommended": false
+ },
+ { "content": "Reject — this requirement doesn't belong in v1 scope", "is_recommended": false }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "requirements",
+ "question": "This requirement covers the audit trail implementation. Given your compliance needs and the comment-based design decision, does this requirement capture the right granularity?",
+ "why": "Audit requirements need precise specification — vague audit requirements lead to either over-engineering or compliance gaps.",
+ "impact": "high",
+ "answer": "Approve — status change audit logging is essential",
+ "options": [
+ {
+ "content": "Approve — this captures the audit trail requirement at the right level of detail",
+ "is_recommended": true
+ },
+ {
+ "content": "Needs more detail — specify exactly which fields trigger audit entries",
+ "is_recommended": false
+ },
+ { "content": "Too granular — simplify to just tracking status changes", "is_recommended": false }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "requirements",
+ "question": "This requirement covers the permission model. Given the three-role design decision, does this capture the visibility rules correctly?",
+ "why": "Permission requirements are security-critical — ambiguous permission specs are a common source of authorization vulnerabilities.",
+ "impact": "high",
+ "answer": "Approve — the three-tier visibility model is correct",
+ "options": [
+ {
+ "content": "Approve — the role-based visibility rules are correctly specified",
+ "is_recommended": true
+ },
+ {
+ "content": "Needs adjustment — developers should see all tickets, not just assigned ones",
+ "is_recommended": false
+ },
+ { "content": "Reject — we need a more granular permission model", "is_recommended": false }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "requirements",
+ "question": "This requirement covers ticket list filtering. Is this the right set of filter dimensions for v1?",
+ "why": "Filter capabilities directly affect usability — too few filters make the tool frustrating, too many add UI complexity.",
+ "impact": "medium",
+ "answer": "Approve — those four filter dimensions cover our daily needs",
+ "options": [
+ {
+ "content": "Approve — status, assignee, priority, and date filters are sufficient for v1",
+ "is_recommended": true
+ },
+ {
+ "content": "Add text search across title and description as a fifth filter",
+ "is_recommended": false
+ },
+ { "content": "Remove date filtering — we rarely need it", "is_recommended": false }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "requirements",
+ "question": "This requirement covers CSV export for reporting. Given your constraint to keep things simpler than Jira, should CSV export be included in v1?",
+ "why": "Export features are often requested but rarely critical for v1 — deferring non-essential features keeps the initial scope tight.",
+ "impact": "low",
+ "answer": "Reject this one — CSV export can wait for v2",
+ "options": [
+ {
+ "content": "Approve — CSV export is needed from day one for reporting workflows",
+ "is_recommended": false
+ },
+ {
+ "content": "Reject — defer CSV export to v2; the team can use the UI for now",
+ "is_recommended": true
+ },
+ {
+ "content": "Simplify — just support copy-paste of filtered views instead of full CSV export",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [1],
+ "freeText": "We can manage without export for now — the spreadsheet transition doesn't require it"
+ },
+ {
+ "phase": "requirements",
+ "question": "",
+ "answer": "Requirements are reviewed: 4 approved (ticket CRUD, audit logging, role-based visibility, filtering) and 1 rejected (CSV export deferred to v2). The requirement set has explicit review coverage.",
+ "isProposal": true
+ },
+ {
+ "phase": "requirements",
+ "question": "",
+ "answer": "Confirm requirements closure",
+ "isConfirmation": true
+ },
+ {
+ "phase": "criteria",
+ "question": "This criterion verifies the audit trail requirement. Does it capture the right level of verification precision for the status change audit log?",
+ "why": "Audit criteria must be objectively testable — vague criteria lead to ambiguous acceptance testing and compliance gaps.",
+ "impact": "high",
+ "answer": "Approve — actor identity and ISO 8601 timestamp are the right verification points",
+ "options": [
+ { "content": "Approve — this criterion is precise and testable", "is_recommended": true },
+ {
+ "content": "Needs refinement — add verification of the previous and new status values",
+ "is_recommended": false
+ },
+ { "content": "Too strict — ISO 8601 is overkill for an internal tool", "is_recommended": false }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "criteria",
+ "question": "This criterion verifies the permission model. Does the negative-case framing capture the security boundary correctly?",
+ "why": "Security criteria work best as negative constraints — verifying what cannot happen is often more important than what can.",
+ "impact": "high",
+ "answer": "Approve — the negative-case framing is exactly right for security boundaries",
+ "options": [
+ {
+ "content": "Approve — non-admin deletion and audit modification are the right security boundaries to verify",
+ "is_recommended": true
+ },
+ {
+ "content": "Expand — also verify that viewers cannot create or edit tickets",
+ "is_recommended": false
+ },
+ {
+ "content": "Simplify — just verify that role checks exist, not specific denied operations",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "criteria",
+ "question": "This criterion covers ticket list performance. Is 2 seconds with 500 tickets the right performance bar for v1?",
+ "why": "Performance criteria need concrete thresholds — without them, 'fast enough' is subjective and untestable.",
+ "impact": "medium",
+ "answer": "Approve — 2 seconds for 500 tickets is a reasonable v1 bar",
+ "options": [
+ {
+ "content": "Approve — 2 seconds for 500 tickets with any filter is a reasonable v1 target",
+ "is_recommended": true
+ },
+ {
+ "content": "Tighten — should be under 1 second for the team size we're targeting",
+ "is_recommended": false
+ },
+ {
+ "content": "Relax — 5 seconds is acceptable for v1 since the team is small",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "criteria",
+ "question": "This criterion covers CSV export verification. Since the CSV export requirement was rejected, should this criterion also be rejected?",
+ "why": "Criteria that verify rejected requirements should typically be rejected to maintain consistency between the requirement and criteria sets.",
+ "impact": "low",
+ "answer": "Reject — this is coupled to the rejected CSV export requirement",
+ "options": [
+ {
+ "content": "Reject — this criterion is moot since the underlying requirement was deferred to v2",
+ "is_recommended": true
+ },
+ {
+ "content": "Keep as deferred — mark it for v2 alongside the requirement",
+ "is_recommended": false
+ },
+ {
+ "content": "Approve anyway — we might add CSV export during implementation",
+ "is_recommended": false
+ }
+ ],
+ "selectedOptionPositions": [0],
+ "freeText": null
+ },
+ {
+ "phase": "criteria",
+ "question": "",
+ "answer": "Criteria review is complete: 3 approved (audit log verification, permission boundaries, performance threshold) and 1 rejected (CSV export verification, coupled to rejected requirement).",
+ "isProposal": true
+ },
+ {
+ "phase": "criteria",
+ "question": "",
+ "answer": "Confirm criteria closure",
+ "isConfirmation": true
+ }
+ ],
+ "knowledgeItems": [
+ {
+ "kind": "goal",
+ "content": "Track work items from creation to completion with clear ownership and status visibility",
+ "capturedAtTurn": 0
+ },
+ {
+ "kind": "goal",
+ "content": "Provide a complete audit trail for all status changes to satisfy compliance requirements",
+ "capturedAtTurn": 3
+ },
+ {
+ "kind": "goal",
+ "content": "Enable team-wide visibility into work distribution and project progress",
+ "capturedAtTurn": 1
+ },
+ {
+ "kind": "term",
+ "content": "ticket — A trackable unit of work with status, assignee, priority, and description",
+ "capturedAtTurn": 2
+ },
+ {
+ "kind": "term",
+ "content": "assignee — Team member responsible for completing a ticket",
+ "capturedAtTurn": 2
+ },
+ {
+ "kind": "term",
+ "content": "status — Current lifecycle state of a ticket (e.g., open, in-progress, resolved, closed)",
+ "capturedAtTurn": 2
+ },
+ {
+ "kind": "term",
+ "content": "comment — Timestamped note attached to a ticket for discussion and status updates",
+ "capturedAtTurn": 2
+ },
+ {
+ "kind": "context",
+ "content": "Team of approximately 8 developers working across 2-3 projects, plus managers needing read-only visibility",
+ "capturedAtTurn": 1
+ },
+ {
+ "kind": "context",
+ "content": "Currently tracking work in a shared Google Sheet with no audit trail or accountability",
+ "capturedAtTurn": 0
+ },
+ {
+ "kind": "constraint",
+ "content": "Must be simpler than Jira — no custom workflows or custom fields in v1",
+ "capturedAtTurn": 4
+ },
+ {
+ "kind": "constraint",
+ "content": "Audit history required for compliance — every status change must be traceable",
+ "capturedAtTurn": 3
+ },
+ {
+ "kind": "constraint",
+ "content": "Role-based permissions with three roles: admin, developer, viewer",
+ "capturedAtTurn": 4
+ },
+ {
+ "kind": "decision",
+ "content": "Kanban board as primary view with columns per status and optional swimlanes per assignee",
+ "rationale": "Matches how the team thinks about work flow; emphasizes status progression over list filtering",
+ "capturedAtTurn": 7
+ },
+ {
+ "kind": "decision",
+ "content": "Comment-based activity log rather than a separate audit table — status changes appear as system-generated comments in the ticket timeline",
+ "rationale": "Keeps everything in one timeline per ticket; simpler data model and the comment timeline becomes the single source of truth",
+ "capturedAtTurn": 8
+ },
+ {
+ "kind": "decision",
+ "content": "API-layer role checks with three roles: admin (full access), developer (create/edit assigned + unassigned), viewer (read-only)",
+ "rationale": "Simpler than row-level security; three roles cover the team's permission needs without over-engineering",
+ "capturedAtTurn": 8
+ },
+ {
+ "kind": "assumption",
+ "content": "Linear status workflow (open → in-progress → resolved → closed) is sufficient for v1 — no custom transitions needed",
+ "capturedAtTurn": 7
+ },
+ {
+ "kind": "assumption",
+ "content": "Three roles (admin, developer, viewer) cover all current and near-term permission needs",
+ "capturedAtTurn": 8
+ },
+ {
+ "kind": "assumption",
+ "content": "Team size will remain under 20 for the foreseeable future, so per-user performance is not a concern",
+ "capturedAtTurn": 8
+ },
+ {
+ "kind": "requirement",
+ "content": "Create, edit, and close tickets with required fields: title, description, priority, and assignee",
+ "capturedAtTurn": 11,
+ "reviewAction": "reviewed",
+ "reviewedAtTurn": 11
+ },
+ {
+ "kind": "requirement",
+ "content": "Status change creates a timestamped audit log entry recording the actor identity, previous status, and new status",
+ "capturedAtTurn": 12,
+ "reviewAction": "reviewed",
+ "reviewedAtTurn": 12
+ },
+ {
+ "kind": "requirement",
+ "content": "Role-based visibility: admins see all tickets and settings, developers see assigned and unassigned tickets, viewers have read-only access",
+ "capturedAtTurn": 13,
+ "reviewAction": "reviewed",
+ "reviewedAtTurn": 13
+ },
+ {
+ "kind": "requirement",
+ "content": "Filter and sort ticket list by status, assignee, priority, and creation date",
+ "capturedAtTurn": 14,
+ "reviewAction": "reviewed",
+ "reviewedAtTurn": 14
+ },
+ {
+ "kind": "requirement",
+ "content": "Export ticket data as CSV for reporting",
+ "capturedAtTurn": 15,
+ "reviewAction": "rejected",
+ "reviewedAtTurn": 15
+ },
+ {
+ "kind": "criterion",
+ "content": "Every status change records the actor identity and ISO 8601 timestamp in the audit log",
+ "capturedAtTurn": 19,
+ "reviewAction": "reviewed",
+ "reviewedAtTurn": 19
+ },
+ {
+ "kind": "criterion",
+ "content": "Non-admin users cannot delete tickets or modify audit entries",
+ "capturedAtTurn": 20,
+ "reviewAction": "reviewed",
+ "reviewedAtTurn": 20
+ },
+ {
+ "kind": "criterion",
+ "content": "Ticket list loads within 2 seconds for up to 500 tickets with any filter combination",
+ "capturedAtTurn": 21,
+ "reviewAction": "reviewed",
+ "reviewedAtTurn": 21
+ },
+ {
+ "kind": "criterion",
+ "content": "CSV export includes all visible fields and respects role-based visibility filters",
+ "capturedAtTurn": 22,
+ "reviewAction": "rejected",
+ "reviewedAtTurn": 22
+ }
+ ],
+ "edges": [
+ {
+ "fromItemIndex": 10,
+ "toItemIndex": 1,
+ "relation": "constrains"
+ },
+ {
+ "fromItemIndex": 8,
+ "toItemIndex": 2,
+ "relation": "derived_from"
+ },
+ {
+ "fromItemIndex": 9,
+ "toItemIndex": 0,
+ "relation": "constrains"
+ },
+ {
+ "fromItemIndex": 12,
+ "toItemIndex": 2,
+ "relation": "depends_on"
+ },
+ {
+ "fromItemIndex": 13,
+ "toItemIndex": 10,
+ "relation": "depends_on"
+ },
+ {
+ "fromItemIndex": 15,
+ "toItemIndex": 12,
+ "relation": "constrains"
+ },
+ {
+ "fromItemIndex": 14,
+ "toItemIndex": 11,
+ "relation": "depends_on"
+ },
+ {
+ "fromItemIndex": 18,
+ "toItemIndex": 0,
+ "relation": "refines"
+ },
+ {
+ "fromItemIndex": 19,
+ "toItemIndex": 10,
+ "relation": "verifies"
+ },
+ {
+ "fromItemIndex": 20,
+ "toItemIndex": 11,
+ "relation": "derived_from"
+ },
+ {
+ "fromItemIndex": 23,
+ "toItemIndex": 19,
+ "relation": "verifies"
+ },
+ {
+ "fromItemIndex": 24,
+ "toItemIndex": 20,
+ "relation": "verifies"
+ },
+ {
+ "fromItemIndex": 25,
+ "toItemIndex": 21,
+ "relation": "constrains"
+ },
+ {
+ "fromItemIndex": 26,
+ "toItemIndex": 22,
+ "relation": "verifies"
+ }
+ ]
+ }
+ }
+}
diff --git a/src/server/fixtures/scenarios.ts b/src/server/fixtures/scenarios.ts
index d7953a82..9901dba4 100644
--- a/src/server/fixtures/scenarios.ts
+++ b/src/server/fixtures/scenarios.ts
@@ -3,12 +3,23 @@ import {
confirmPhaseOutcome,
createKnowledgeItem,
createPhaseOutcome,
+ createConfirmedPhaseOutcome,
createProject,
createTurn,
linkKnowledgeItemToTurn,
type DB,
} from '../db.js';
+function createConfirmationParts(text: string, data: object): string {
+ return JSON.stringify([
+ { type: 'text', text },
+ {
+ type: 'data-confirmation',
+ data,
+ },
+ ]);
+}
+
export function seedClosedScope(db: DB, projectId: number) {
const scopeTurn = createTurn(db, projectId, {
phase: 'scope',
@@ -37,17 +48,11 @@ export function seedClosedScope(db: DB, projectId: number) {
parent_turn_id: scopeProposalTurn.id,
question: '',
answer: 'Confirm scope closure',
- user_parts: JSON.stringify([
- { type: 'text', text: 'Confirm scope closure' },
- {
- type: 'data-confirmation',
- data: {
- kind: 'confirm-proposed-phase-closure',
- proposalTurnId: scopeProposalTurn.id,
- phase: 'scope',
- },
- },
- ]),
+ user_parts: createConfirmationParts('Confirm scope closure', {
+ kind: 'confirm-proposed-phase-closure',
+ proposalTurnId: scopeProposalTurn.id,
+ phase: 'scope',
+ }),
});
confirmPhaseOutcome(db, scopeOutcome.id, scopeConfirmationTurn.id);
advanceHead(db, projectId, scopeConfirmationTurn.id);
@@ -84,17 +89,11 @@ export function seedRequirementsReady(db: DB, projectId: number) {
parent_turn_id: seededDesign.designTurn.id,
question: '',
answer: 'Confirm design closure',
- user_parts: JSON.stringify([
- { type: 'text', text: 'Confirm design closure' },
- {
- type: 'data-confirmation',
- data: {
- kind: 'confirm-proposed-phase-closure',
- proposalTurnId: seededDesign.designTurn.id,
- phase: 'design',
- },
- },
- ]),
+ user_parts: createConfirmationParts('Confirm design closure', {
+ kind: 'confirm-proposed-phase-closure',
+ proposalTurnId: seededDesign.designTurn.id,
+ phase: 'design',
+ }),
});
confirmPhaseOutcome(db, designOutcome.id, designConfirmationTurn.id);
advanceHead(db, projectId, designConfirmationTurn.id);
@@ -102,9 +101,7 @@ export function seedRequirementsReady(db: DB, projectId: number) {
return { ...seededDesign, designConfirmationTurn };
}
-export function seedCriteriaReady(db: DB, projectId: number) {
- const seededRequirements = seedRequirementsReady(db, projectId);
-
+function seedClosedRequirementsReview(db: DB, projectId: number, parentTurnId: number) {
const approvedRequirement = createKnowledgeItem(
db,
projectId,
@@ -120,7 +117,7 @@ export function seedCriteriaReady(db: DB, projectId: number) {
const reviewTurn = createTurn(db, projectId, {
phase: 'requirements',
- parent_turn_id: seededRequirements.designConfirmationTurn.id,
+ parent_turn_id: parentTurnId,
question: 'Are these requirements all reviewed now?',
answer: 'Yes — approve resume and reject PDF export',
});
@@ -148,23 +145,16 @@ export function seedCriteriaReady(db: DB, projectId: number) {
parent_turn_id: requirementsProposalTurn.id,
question: '',
answer: 'Confirm requirements closure',
- user_parts: JSON.stringify([
- { type: 'text', text: 'Confirm requirements closure' },
- {
- type: 'data-confirmation',
- data: {
- kind: 'confirm-proposed-phase-closure',
- proposalTurnId: requirementsProposalTurn.id,
- phase: 'requirements',
- },
- },
- ]),
+ user_parts: createConfirmationParts('Confirm requirements closure', {
+ kind: 'confirm-proposed-phase-closure',
+ proposalTurnId: requirementsProposalTurn.id,
+ phase: 'requirements',
+ }),
});
confirmPhaseOutcome(db, requirementsOutcome.id, requirementsConfirmationTurn.id);
advanceHead(db, projectId, requirementsConfirmationTurn.id);
return {
- ...seededRequirements,
approvedRequirement,
rejectedRequirement,
reviewTurn,
@@ -173,13 +163,22 @@ export function seedCriteriaReady(db: DB, projectId: number) {
};
}
-export function seedAllPhasesClosed(db: DB, projectId: number) {
- const seededCriteria = seedCriteriaReady(db, projectId);
+export function seedCriteriaReady(db: DB, projectId: number) {
+ const seededRequirements = seedRequirementsReady(db, projectId);
+ const reviewedRequirements = seedClosedRequirementsReview(
+ db,
+ projectId,
+ seededRequirements.designConfirmationTurn.id,
+ );
+ return { ...seededRequirements, ...reviewedRequirements };
+}
+
+function seedClosedCriteriaReview(db: DB, projectId: number, parentTurnId: number) {
const criterion = createKnowledgeItem(db, projectId, 'criterion', 'Verify SQLite resume');
const criterionReviewTurn = createTurn(db, projectId, {
phase: 'criteria',
- parent_turn_id: seededCriteria.requirementsConfirmationTurn.id,
+ parent_turn_id: parentTurnId,
question: 'Are these criteria reviewed?',
answer: 'Yes — approve the criterion',
});
@@ -206,27 +205,149 @@ export function seedAllPhasesClosed(db: DB, projectId: number) {
parent_turn_id: criteriaProposalTurn.id,
question: '',
answer: 'Confirm criteria closure',
- user_parts: JSON.stringify([
- { type: 'text', text: 'Confirm criteria closure' },
- {
- type: 'data-confirmation',
- data: {
- kind: 'confirm-proposed-phase-closure',
- proposalTurnId: criteriaProposalTurn.id,
- phase: 'criteria',
- },
- },
- ]),
+ user_parts: createConfirmationParts('Confirm criteria closure', {
+ kind: 'confirm-proposed-phase-closure',
+ proposalTurnId: criteriaProposalTurn.id,
+ phase: 'criteria',
+ }),
});
confirmPhaseOutcome(db, criteriaOutcome.id, criteriaConfirmationTurn.id);
advanceHead(db, projectId, criteriaConfirmationTurn.id);
+ return { criterion, criterionReviewTurn, criteriaProposalTurn, criteriaConfirmationTurn };
+}
+
+export function seedAllPhasesClosed(db: DB, projectId: number) {
+ const seededCriteria = seedCriteriaReady(db, projectId);
+ const reviewedCriteria = seedClosedCriteriaReview(
+ db,
+ projectId,
+ seededCriteria.requirementsConfirmationTurn.id,
+ );
+
+ return { ...seededCriteria, ...reviewedCriteria };
+}
+
+export function seedAllPhasesClosedWithForcedDesign(db: DB, projectId: number) {
+ const seededScope = seedClosedScope(db, projectId);
+
+ const designTurn = createTurn(db, projectId, {
+ phase: 'design',
+ parent_turn_id: seededScope.scopeConfirmationTurn.id,
+ question: 'Which tradeoff matters most?',
+ answer: 'Keep the repository seam small',
+ });
+ advanceHead(db, projectId, designTurn.id);
+
+ const designForceCloseTurn = createTurn(db, projectId, {
+ phase: 'design',
+ parent_turn_id: designTurn.id,
+ question: '',
+ answer: 'Force design closure',
+ user_parts: createConfirmationParts('Force design closure', {
+ kind: 'force-close-active-phase',
+ phase: 'design',
+ }),
+ });
+ advanceHead(db, projectId, designForceCloseTurn.id);
+
+ const designOutcome = createPhaseOutcome(db, {
+ projectId,
+ phase: 'design',
+ proposal_turn_id: designForceCloseTurn.id,
+ summary: 'Design closed by user without an interviewer recommendation.',
+ });
+ confirmPhaseOutcome(db, designOutcome.id, designForceCloseTurn.id);
+
+ const reviewedRequirements = seedClosedRequirementsReview(db, projectId, designForceCloseTurn.id);
+ const reviewedCriteria = seedClosedCriteriaReview(
+ db,
+ projectId,
+ reviewedRequirements.requirementsConfirmationTurn.id,
+ );
+
return {
- ...seededCriteria,
- criterion,
- criterionReviewTurn,
- criteriaProposalTurn,
- criteriaConfirmationTurn,
+ ...seededScope,
+ designTurn,
+ designForceCloseTurn,
+ ...reviewedRequirements,
+ ...reviewedCriteria,
+ };
+}
+
+export function seedAllPhasesClosedWithLowReadinessScope(db: DB, projectId: number) {
+ const designTurn = createTurn(db, projectId, {
+ phase: 'design',
+ question: 'Which tradeoff matters most?',
+ answer: 'Keep the repository seam small',
+ });
+ advanceHead(db, projectId, designTurn.id);
+
+ const scopeClosureTurn = createTurn(db, projectId, {
+ phase: 'design',
+ parent_turn_id: designTurn.id,
+ question: '',
+ answer: 'Confirm scope closure',
+ user_parts: createConfirmationParts('Confirm scope closure', {
+ kind: 'confirm-proposed-phase-closure',
+ proposalTurnId: designTurn.id,
+ phase: 'scope',
+ }),
+ });
+ advanceHead(db, projectId, scopeClosureTurn.id);
+
+ createConfirmedPhaseOutcome(db, {
+ projectId,
+ phase: 'scope',
+ proposal_turn_id: scopeClosureTurn.id,
+ confirmation_turn_id: scopeClosureTurn.id,
+ summary:
+ 'Scope was closed from a minimal downstream checkpoint to exercise low-readiness export caveats.',
+ });
+
+ const designProposalTurn = createTurn(db, projectId, {
+ phase: 'design',
+ parent_turn_id: scopeClosureTurn.id,
+ question: '',
+ answer: 'The main architectural commitments are captured well enough to review requirements.',
+ });
+ advanceHead(db, projectId, designProposalTurn.id);
+
+ const designConfirmationTurn = createTurn(db, projectId, {
+ phase: 'design',
+ parent_turn_id: designProposalTurn.id,
+ question: '',
+ answer: 'Confirm design closure',
+ user_parts: createConfirmationParts('Confirm design closure', {
+ kind: 'confirm-proposed-phase-closure',
+ proposalTurnId: designProposalTurn.id,
+ phase: 'design',
+ }),
+ });
+ advanceHead(db, projectId, designConfirmationTurn.id);
+
+ const designOutcome = createPhaseOutcome(db, {
+ projectId,
+ phase: 'design',
+ proposal_turn_id: designProposalTurn.id,
+ summary: 'The main architectural commitments are captured well enough to review requirements.',
+ });
+ confirmPhaseOutcome(db, designOutcome.id, designConfirmationTurn.id);
+
+ const reviewedRequirements = seedClosedRequirementsReview(db, projectId, designConfirmationTurn.id);
+ const reviewedCriteria = seedClosedCriteriaReview(
+ db,
+ projectId,
+ reviewedRequirements.requirementsConfirmationTurn.id,
+ );
+
+ return {
+ designTurn,
+ scopeClosureTurn,
+ designProposalTurn,
+ designConfirmationTurn,
+ ...reviewedRequirements,
+ ...reviewedCriteria,
};
}
@@ -258,6 +379,26 @@ export const scenarios: Record = {
seedAllPhasesClosed(db, project.id);
return project.id;
},
+ 'forced-close-all-phases-closed': (db, name = 'Forced-Close All Phases Closed') => {
+ const project = createProject(db, name);
+ seedAllPhasesClosedWithForcedDesign(db, project.id);
+ return project.id;
+ },
+ 'low-readiness-all-phases-closed': (db, name = 'Low-Readiness All Phases Closed') => {
+ const project = createProject(db, name);
+ seedAllPhasesClosedWithLowReadinessScope(db, project.id);
+ return project.id;
+ },
};
-export const scenarioNames = Object.keys(scenarios);
+// Manifest-based scenarios (additive — issue-tracker domain with rich parts + knowledge)
+let manifestScenarios: Record = {};
+try {
+ const { loadManifestScenarios } = await import('./manifest.js');
+ manifestScenarios = loadManifestScenarios('issue-tracker');
+} catch {
+ // Manifest files may not exist in all environments (e.g., CI without fixtures)
+}
+
+export const allScenarios: Record = { ...scenarios, ...manifestScenarios };
+export const scenarioNames = Object.keys(allScenarios);
diff --git a/src/server/fixtures/seed.ts b/src/server/fixtures/seed.ts
index 4c9bed52..1a6d51e8 100644
--- a/src/server/fixtures/seed.ts
+++ b/src/server/fixtures/seed.ts
@@ -1,16 +1,16 @@
import { createDb } from '../db.js';
-import { scenarioNames, scenarios } from './scenarios.js';
+import { allScenarios, scenarioNames } from './scenarios.js';
const args = process.argv.slice(2);
const scenarioName = args[0];
const dbPath = args[1] ?? './brunch.db';
-if (!scenarioName || !scenarios[scenarioName]) {
+if (!scenarioName || !allScenarios[scenarioName]) {
console.error(scenarioName ? `Unknown scenario: ${scenarioName}` : 'Usage: seed [db-path]');
console.error(`\nAvailable scenarios:\n${scenarioNames.map((n) => ` - ${n}`).join('\n')}`);
process.exit(1);
}
const db = createDb(dbPath);
-const projectId = scenarios[scenarioName](db);
+const projectId = allScenarios[scenarioName](db);
console.log(`Seeded "${scenarioName}" → project ${projectId} in ${dbPath}`);
diff --git a/src/server/interview.test.ts b/src/server/interview.test.ts
index 429e9b2e..8b27688a 100644
--- a/src/server/interview.test.ts
+++ b/src/server/interview.test.ts
@@ -44,6 +44,38 @@ describe('structuredQuestionSchema', () => {
}),
).toThrow();
});
+
+ it('accepts the legacy review field and normalizes it to requirementReview', () => {
+ expect(
+ structuredQuestionSchema.parse({
+ question: 'Should we approve this requirement?',
+ why: 'Requirement review coverage should continue to work during the prompt transition.',
+ impact: 'high',
+ options: [
+ { content: 'Approve', is_recommended: true },
+ { content: 'Reject', is_recommended: false },
+ ],
+ review: {
+ kind: 'requirement-approval',
+ requirementId: 42,
+ approveOptionPosition: 0,
+ },
+ }),
+ ).toEqual({
+ question: 'Should we approve this requirement?',
+ why: 'Requirement review coverage should continue to work during the prompt transition.',
+ impact: 'high',
+ options: [
+ { content: 'Approve', is_recommended: true },
+ { content: 'Reject', is_recommended: false },
+ ],
+ requirementReview: {
+ kind: 'requirement-approval',
+ requirementId: 42,
+ approveOptionPosition: 0,
+ },
+ });
+ });
});
describe('getSystemPrompt', () => {
@@ -64,6 +96,7 @@ describe('getSystemPrompt', () => {
expect(getSystemPrompt('requirements')).toContain('current requirement inventory');
expect(getSystemPrompt('requirements')).toContain('requirement-approval');
expect(getSystemPrompt('requirements')).toContain('requirement-rejection');
+ expect(getSystemPrompt('requirements')).toContain('requirementReview');
expect(getSystemPrompt('requirements')).toContain('propose_phase_closure');
});
});
diff --git a/src/server/interview.ts b/src/server/interview.ts
index 93f3fceb..808f0fc5 100644
--- a/src/server/interview.ts
+++ b/src/server/interview.ts
@@ -55,9 +55,9 @@ When the main architectural commitments are sufficiently captured for now, use t
Your job is to walk the accumulated requirements, check for gaps, suggest additions, and confirm completeness. Ground each review turn in the current requirement inventory provided in context. Present requirements for the user to confirm, modify, or flag as missing.
-When asking the user to approve one specific requirement, review one requirement at a time and include \`review: { kind: 'requirement-approval', requirementId, approveOptionPosition }\` in the ask_question input so the approval target is explicit.
+When asking the user to approve one specific requirement, review one requirement at a time and include \`requirementReview: { kind: 'requirement-approval', requirementId, approveOptionPosition }\` in the ask_question input so the approval target is explicit.
-When asking the user to reject one specific requirement, include \`review: { kind: 'requirement-rejection', requirementId, rejectOptionPosition }\` so the rejection target is explicit.
+When asking the user to reject one specific requirement, include \`requirementReview: { kind: 'requirement-rejection', requirementId, rejectOptionPosition }\` so the rejection target is explicit.
When every current requirement has explicit review coverage and the set appears complete for now, use the \`propose_phase_closure\` tool instead of another question. The summary should explain why requirements can close and criteria review can begin.
diff --git a/src/shared/chat.ts b/src/shared/chat.ts
index 8c3c9d88..cc6609eb 100644
--- a/src/shared/chat.ts
+++ b/src/shared/chat.ts
@@ -68,16 +68,35 @@ export const structuredQuestionSchema = z
)
.min(2),
requirementReview: requirementReviewSchema.optional(),
+ review: requirementReviewSchema.optional(),
criterionReview: criterionReviewSchema.optional(),
})
.superRefine((value, ctx) => {
- if (value.requirementReview) {
- validateReviewOptionPosition(value.requirementReview, 'requirementReview', value.options.length, ctx);
+ if (value.requirementReview && value.review) {
+ ctx.addIssue({
+ code: 'custom',
+ message: 'Use requirementReview instead of review when both are present',
+ path: ['review'],
+ });
+ }
+
+ const requirementReview = value.requirementReview ?? value.review;
+ if (requirementReview) {
+ validateReviewOptionPosition(
+ requirementReview,
+ value.requirementReview ? 'requirementReview' : 'review',
+ value.options.length,
+ ctx,
+ );
}
if (value.criterionReview) {
validateReviewOptionPosition(value.criterionReview, 'criterionReview', value.options.length, ctx);
}
- });
+ })
+ .transform(({ review, requirementReview, ...value }) => ({
+ ...value,
+ ...((requirementReview ?? review) ? { requirementReview: requirementReview ?? review } : {}),
+ }));
export const askQuestionToolOutputSchema = z.object({
ok: z.literal(true),
diff --git a/tsconfig.json b/tsconfig.json
index 38cd59f6..faf4c1a8 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -11,5 +11,5 @@
"@/*": ["./src/client/*"]
}
},
- "include": ["src", "node_modules/vite/client.d.ts"]
+ "include": ["src", ".ladle", "node_modules/vite/client.d.ts"]
}