docs(P06): complete final review + audit + approval-gate phase (#6)

Squash-merge of phase/06-final-review-approval-ship into milestone/v0.1-prototype. Review PASS-WITH-FIXES (deep-link P0 + 5 P1s, 75/75 tests). Audit PASS-WITH-NOTES (all notes applied). Tag v0.1.6 = extended-prototype release candidate. Milestone merge to main HALTS at the REQ-62 owner approval gate.
This commit was merged in pull request #6.
This commit is contained in:
2026-09-27 04:37:36 +00:00
parent a118f359a7
commit 600b49c2fa
27 changed files with 225 additions and 97 deletions
+2 -2
View File
@@ -47,7 +47,7 @@ FastAPI routes: POST /api/runs, GET /api/methodologies, GET /api/runs/{id}, appr
- **DependsOn:** engine, methodologies, config
### 7. Frontend (Next.js App Router, TypeScript strict)
Screens: Ask (question + optional context), Choose (methodology cards with ETA/cost/why-recommended; dials behind "advanced"), Plan & Run (approve/edit plan, live SSE trace, pause/cancel), Reading Room v0 (rendered report, hover-a-citation → exact quoted passage, ledger panel, Markdown/JSON export), plus v2 surface mockups (project graph, task inbox) in the prototype.
App shell (root layout, P5): persistent sidebar (primary nav — New research / Library / Settings; adapter connection-status dots as a v2 preview) + top bar (⌘K navigation palette — screens + stored reports; avatar button → user menu with the local single-user profile). Screens: Ask (question + optional context), Choose (methodology cards with ETA/cost/why-recommended; dials behind "advanced"), Plan & Run (approve/edit plan, live SSE trace, pause/cancel), Reading Room v0 (rendered report, hover-a-citation → exact quoted passage, ledger panel, Markdown/JSON export), Library (P5: folder-structure report storage — auto-categorized folders, recency/title sorting, report cards into the Reading Room; cross-report Connections panel — shared-sources ⇄ / related-topic → edges; project graph + task inbox as v2-preview panels), Settings (P5: masked API-key status, storage directory, default methodology + resolved 12-dial display, appearance preview, about/version).
- **Boundaries:** consumes the typed `ApiClient` interface only — components never import the mock client directly. In v0.1 the interface is bound to `MockApiClient` (fixture data + scripted event replay); in v0.2 phase 3 the binding swaps to the OpenAPI-generated client with no component changes.
- **DependsOn:** ApiClient contract (v0.1: mock; v0.2: API layer)
@@ -58,7 +58,7 @@ v0.1 (prototype): Ask (question + context) → Choose (methodology slug + dial o
v0.2 (MVP): Ask (question + context) → Choose (methodology slug + dial overrides) → POST /api/runs → engine PLANNING (planner model via model adapter, dial bundle resolved) → AWAITING_APPROVAL (plan persisted; SSE events; user approves/edits) → SEARCHING (search adapter, routing per dials) → EXTRACTING (fetch pages → evidence store, stable passage IDs) → REFLECTING (planner model; depth loop per dials) → SYNTHESIZING (writer model; context = evidence passages only, per-claim citation markers) → VERIFYING (liveness pass, quote anchoring, confidence labels) → DONE (report + ledger + checklist persisted to storage; Reading Room renders; export).
## Build Order
Milestone v0.1 (prototype): 1. Frontend scaffold + ApiClient/MockApiClient boundary → 2. Ask + Choose → 3. Plan & Run + Reading Room v0 → 4. v2 mockups + polish + 3-click validation → 5. review + owner approval gate + ship.
Milestone v0.1 (prototype): 1. Frontend scaffold + ApiClient/MockApiClient boundary → 2. Ask + Choose → 3. Plan & Run + Reading Room v0 → 4. v2 mockups + polish + 3-click validation → 5. review + owner approval gate (owner returned gate feedback → extension cycle: 5. app shell + research library + settings + avatar → 6. final review + approval re-presentation + ship).
Milestone v0.2 (MVP, gated on v0.1 approval):
1. Methodology framework (dial schema + 2 seed methodologies + registry)
+3 -3
View File
@@ -1,8 +1,8 @@
{
"phase": 6,
"stage": "execute",
"stage": "approval_pending",
"milestone": "v0.1",
"phase_role": "final",
"attempts": 0,
"updated_at": "2026-09-27T03:30:00Z"
}
"updated_at": "2026-09-27T04:40:00Z"
}
+33 -32
View File
@@ -1,53 +1,54 @@
# PLAN — Milestone v0.1 Phase 5: App Shell + Research Library + Settings + Avatar
# PLAN — Milestone v0.1 Phase 6 (FINAL): Review + Approval Re-presentation + Ship
---
phase: 5
phase: 6
milestone: v0.1
status: planned
status: executed
must_haves:
- "Persistent app shell (sidebar + top bar + avatar menu) on every screen, adding zero required clicks to the happy path (REQ-63, REQ-66)"
- "Research library with folder-structure storage view, auto-categorization, sorting, and cross-report connection drawing in the project graph (REQ-64)"
- "Settings screen — API keys (masked), storage endpoint, methodology defaults, appearance slot, about/version — all through the ApiClient boundary (REQ-65)"
- "All new data access extends the ApiClient interface + MockApiClient fixtures; zero throwaway for v0.2 (REQ-53/54 discipline)"
- "Full-milestone review: PASS-WITH-FIXES — deep-link P0 (stored-report cards resolved to the wrong run) + 5 P1s fixed and verified in-phase; 75/75 tests"
- "Audit: PASS-WITH-NOTES — reconstruction, tags, file/branch/commit discipline, secrets sweep all green; notes applied (version bump, ROADMAP status, ARCHITECTURE surfaces)"
- "Final phase patch v0.1.6 shipped — milestone release candidate, presented at the REQ-62 owner approval gate"
personas:
- frontend-engineer
- ci-lead-developer
requirements: [REQ-63, REQ-64, REQ-65, REQ-66]
requirements: [REQ-62]
---
## User-Facing Surface
Five routes gain a persistent shell; two new surfaces ship:
The extended prototype presented for owner approval: the full 5-screen click-path (Ask → Choose → Plan & Run → Reading Room → Library) now inside the persistent app shell (sidebar + top bar + avatar), the folder-structure research library with cross-report connections, the settings surface (API keys, storage, methodology defaults, appearance, about), and the ⌘K navigation palette. Releases v0.1.0–v0.1.5; v0.1.6 is the extended-prototype release candidate.
1. **App shell** (all screens): left sidebar — New research (Ask), Library, Settings nav + future slots (connection-status dots previewing v0.2 adapter health, methodology-defaults preview); top bar — search/⌘K slot (navigation-only command palette: screens + stored reports) + avatar button (right) opening a user menu (profile, Settings, About). Honest "preview" pills on non-functional slots, per the established library-banner pattern.
2. **Research library** (`/library`): folder-structure view of stored reports (auto-categorized by topic + methodology, sortable by recency/title), report cards linking to Reading Room, project graph extended with cross-report edges (shared-source ⇄ and topical → connections), task inbox retained.
3. **Settings** (`/settings`): API keys section (model: Ollama cloud/local; search: Tavily — masked values, save wired through the mock boundary), storage endpoint (local directory), methodology defaults (resolved 12-dial display), appearance (theme slot, marked preview — paper palette is light-only), about/version (v0.1.5, prototype notice).
## Happy Path (owner review script)
## Happy Path
1. `cd frontend && pnpm install && pnpm dev` → http://localhost:3000 → shell renders on /ask
2. Ask → question → Research this (click 1)
3. Choose → Deep Report card → Start research (click 2)
4. Plan & Run → Approve plan (click 3) → live trace → pause/resume/cancel
5. Read the report → hover [n] → exact quote; ledger; export Markdown/JSON
6. Sidebar → Library → folder tree → open a stored report → matches its card title
7. Library → Connections → shared-source ⇄ and related-topic → edges
8. ⌘K → type "lith" → jump to the stored report
9. Avatar → user menu → Settings → API keys masked, save round-trips
10. Owner decision: approve → v0.1.6 published + milestone merge to main; or request changes
1. `cd frontend && pnpm install && pnpm dev` → http://localhost:3000 → shell renders (sidebar + top bar) on /ask
2. New research → question → Research this (click 1) → Choose → Start research (click 2) → Approve plan (click 3) — ≤3 clicks, shell adds zero
3. Sidebar → Library → browse folder tree → open a stored report → Reading Room
4. Library → Connections panel → cross-report edges visible (⇄ shared sources, → related topic)
5. Top bar avatar → user menu → Settings → API keys + storage + methodology defaults render; save persists (mock)
6. ⌘K (top bar search slot) → type "lib" → navigate to Library
## What this phase did
1. **Review** (multi-persona, full milestone diff `main..HEAD`, P5-weighted): verdict PASS-WITH-FIXES. P0-1: stored-report deep links resolved to the wrong run (every library card + palette entry opened the demo report under a mismatched id) — fixed via the `#storedRun` resolver + card-title override in the scripted builders; probe + boundary tests prove run/report/ledger/export agree. P1s fixed in-phase: avatar-menu dismiss race (toggleRef guard, pinned by a new test), lost v2-preview pills on graph/inbox panels, stale root README, settings stale-after-save + non-live dial preview (now previews the live selection with formatted values), doc rot (run-page header, VERSIONING.md table). Suite 72 → 75.
2. **Audit**: PASS-WITH-NOTES — tags v0.1.0–v0.1.5 verified annotated + on-branch and mapped to ship records; REQ traceability consistent with ---ci--- blocks; secrets clean across full history; branch hygiene exact. Notes applied in this phase: settings About bumped to v0.1.6 (P1-watch), ROADMAP P6 → in_progress, ARCHITECTURE.md §7 + Build Order extended with P5/P6 surfaces, release README example bumped; `---/ci---` closer convention restored going forward.
3. **Ship (this commit sequence)**: PR #6 (phase/06 → milestone), squash-merge, tag v0.1.6, release with full milestone summary. Per REQ-62 and the prototype_approval escalation hook, the milestone merge to main does NOT proceed autonomously — the pipeline halts at the gate for the recorded owner decision.
## UX Acceptance Criteria
- Shell on every screen; sidebar/top bar never break the 68ch reading measure (Reading Room unaffected)
- 3-click happy path pinned green post-shell (three-click-path.test.tsx extended)
- No placeholder/stub text beyond honest "preview" pills; typography care per vision §4 on all new surfaces
- All new data through `api.*` — no component imports MockApiClient; v0.2 OpenAPI swap unaffected
- Avatar menu keyboard-accessible; focus ring in accent (global :focus-visible standard)
- Owner demo script runs clean on the shipped code (deep-link P0 fix verified by test)
- No placeholder/stub text in owner-visible surfaces; preview pills honest
- The 3-click criterion still pinned green post-shell
- Avatar menu opens/closes cleanly; keyboard accessible throughout
## Waves
## Verification
1. **T-5.1 Boundary first**: types (ReportSummary, ReportFolder, Settings, UserProfile, ConnectionStatus, SavedSettings) → ApiClient gains getReports, getSettings, saveSettings, getUserProfile, getConnectionStatus → MockApiClient + fixtures (stored-reports tree, masked keys, adapter health, profile) → co-located tests
2. **T-5.2 Shell chrome**: sidebar + top bar + palette + avatar components; layout restructure (all 5 routes under the shell); dedupe per-page `<main>` wrappers; regression tests
3. **T-5.3 Library**: folder tree + sortable cards + cross-report graph edges; extract reusable panels; tests
4. **T-5.4 Settings**: 5 sections, save flow through the boundary; tests
5. **T-5.5 Polish**: README screens table, typography pass, 3-click re-pin, gates green
- Gates: typecheck ✓ lint ✓ tests 75/75 ✓ build ✓ (re-run after review fixes, plus by audit)
- Deep-link boundary test: every stored report resolves to its own id/question/methodology/title
- Post-hoc v0.2 log: demo-run fallback 404 semantics, shared scripted report body disclosure, palette scroll-into-view, raw methodology slugs on cards, pt-[14vh] token
## Scope Guard
P6 (final review + approval re-presentation) follows P5 ship. Milestone merge to main remains gated on the owner's recorded approval (REQ-62, prototype_approval escalation hook). No backend work, no v0.2 scope, no auth/multi-tenancy (local single-user profile only).
Milestone merge to main, v0.1.6 publication, v0.2 unblocking, and all v0.2 P1 work wait on the owner approval gate (REQ-62, prototype_approval escalation hook). The pipeline does not proceed autonomously past this commit.
+1 -1
View File
@@ -122,4 +122,4 @@
| REQ-25, REQ-26, REQ-33 (API, client, tooling) | v0.2 | P3 | blocked |
| REQ-17, REQ-32, REQ-34 (adversarial, E2E, round-trip) | v0.2 | P4 | blocked |
| All REQ-01..34 (milestone gate) | v0.2 | P5 | blocked |
| REQ-35..51 | v2 backlog (post-MVP milestones) | — | pending |
| REQ-35..51 | v2 backlog (post-MVP milestones) | — | pending |
+3 -2
View File
@@ -52,7 +52,7 @@ Two milestones, owner-directed 2026-09-27. **Milestone v0.1 — Prototype**: the
- **Requirements covered**: REQ-62 (gate) + milestone approval of REQ-52..66
- **Success criteria**: review PASS (P0s fixed in-phase); audit PASS; v0.1.6 tagged + release; pipeline halts at the approval gate for the owner's recorded decision
- **Depends on**: 5
- **Status**: planned
- **Status**: in_progress (review PASS-WITH-FIXES — deep-link P0 + 5 P1s fixed; audit PASS-WITH-NOTES; shipping as v0.1.6, then the gate halts for the owner decision)
## Milestone v0.2 — MVP (feature) — BLOCKED until prototype approval
@@ -92,4 +92,5 @@ Phases renumbered from the original v1.0 plan (1-based within the milestone):
- **v0.1.6 = milestone release** (P6 final phase patch — the approved extended prototype release, pending owner decision)
- **v0.2.x = MVP release** (MAJOR still 0; ship only if all v0.2 phases complete)
- If MVP scope extends: additional 0.x minor milestones (v0.3, …), each a full milestone cycle
- **v1.0.0**: owner-approved release only — not created by the pipeline autonomously
- **v1.0.0**: owner-approved release only — not created by the pipeline autonomously
+1 -1
View File
@@ -28,7 +28,7 @@ bash .ciagent/release/deploy.sh <version>
Examples:
```bash
bash .ciagent/release/deploy.sh v0.1.4 # deploy tagged version
bash .ciagent/release/deploy.sh v0.1.5 # deploy tagged version
bash .ciagent/release/deploy.sh v0.2.1 # future releases
```
+3 -2
View File
@@ -8,7 +8,7 @@ as established by the CIAgent branch strategy and `config.json`.
| Milestone | Tags | Deployed by |
|---|---|---|
| v0.1 prototype | v0.1.0 … v0.1.4 | `bash .ciagent/release/deploy.sh v0.1.4` |
| v0.1 prototype | v0.1.0 … v0.1.5 | `bash .ciagent/release/deploy.sh v0.1.5` |
| v0.2 MVP | v0.2.0 … | `bash .ciagent/release/deploy.sh v0.2.x` |
## Rules
@@ -32,7 +32,8 @@ as established by the CIAgent branch strategy and `config.json`.
`deploy.sh`'s parameter block.
6. **Milestone gating applies to deployment too.** The v0.1 prototype
deploys for owner review. v0.2 releases deploy only after the
v0.1.4 owner-approval gate is cleared.
REQ-62 owner-approval gate is cleared (recorded on the extended
prototype, v0.1.6).
## Container conventions
+5 -2
View File
@@ -12,7 +12,7 @@ pnpm install
pnpm dev
```
Open http://localhost:3000 — you land on **Ask**. Type a question, click **Research this**, pick a methodology on **Choose** (Deep Report is the recommended default), and press **Start research**.
Open http://localhost:3000 — you land on **Ask**, inside the app shell (left sidebar, top bar with ⌘K search and your avatar). Type a question, click **Research this**, pick a methodology on **Choose** (Deep Report is the recommended default), and press **Start research**.
Three clicks from cold start to initiated research.
@@ -24,7 +24,10 @@ Three clicks from cold start to initiated research.
| `/choose` | All 8 locked methodology cards — mock ETA, cost estimate, why-recommended, and the 12 methodology dials behind each card's Advanced drawer. |
| `/run/[id]` | Plan & Run: approve the research plan (sub-questions, source universe, outline), watch the live trace stream, pause/resume or cancel. |
| `/run/[id]/report` | Reading Room: the report with page-anchored citations — hover or focus any `[n]` marker for the exact quoted passage — plus the citation ledger and Markdown/JSON exports. |
| `/library` | v2 preview: project graph and task inbox mockups rendered from fixture data. |
| `/library` | Stored reports filed into auto-categorized folders (sortable, links into the Reading Room), the Connections panel (shared sources ⇄, related topics →), plus v2-preview project graph and task inbox. |
| `/settings` | API-key status (masked, never raw), the reports storage directory, methodology defaults with the resolved 12 dials, appearance, and about/version. |
The persistent shell (every screen): sidebar nav + adapter connection-status dots, top bar with the ⌘K navigation palette (screens + stored reports) and the avatar menu (local profile, Settings, About).
All data flows through a typed `ApiClient` interface backed by `MockApiClient` — the same components will talk to the FastAPI backend via the OpenAPI-generated client in the MVP milestone, with zero rewrites.
+1 -1
View File
@@ -28,4 +28,4 @@ export default function AskPage() {
</p>
</div>
);
}
}
+1 -1
View File
@@ -11,4 +11,4 @@ import { api } from "@/lib/api/client";
export default async function ChoosePage() {
const methodologies = await api.getMethodologies();
return <ChooseView methodologies={methodologies} />;
}
}
+1 -1
View File
@@ -36,4 +36,4 @@ export default function RootLayout({
</body>
</html>
);
}
}
+1 -1
View File
@@ -11,4 +11,4 @@ export default function LibraryPage() {
<LibraryView />
</div>
);
}
}
+5 -6
View File
@@ -1,12 +1,11 @@
import { RunView } from "@/components/run-view";
/**
* /run/[id] (T-3.2b) — placeholder for the Plan & Run screen.
* /run/[id] (T-2.4, REQ-57) — the Plan & Run screen.
*
* Phase 2 ships the real screen (plan approval, live scripted trace,
* pause/cancel). For now: the run's question + methodology and a clear
* "ships in phase 2" notice, so the Start-research click-path lands
* somewhere honest.
* Server shell for the run stage: plan approval, the live scripted
* trace, and the terminal-state onward links. Run data flows through
* the ApiClient boundary inside RunView.
*/
export default async function RunPage({
params,
@@ -19,4 +18,4 @@ export default async function RunPage({
<RunView id={id} />
</div>
);
}
}
+1 -1
View File
@@ -11,4 +11,4 @@ export default async function ReportPage({
<ReadingRoom id={id} />
</div>
);
}
}
+2 -1
View File
@@ -11,4 +11,5 @@ export default function SettingsPage() {
<SettingsView />
</div>
);
}
}
@@ -115,6 +115,16 @@ describe("AppTopBar (REQ-63, REQ-66)", () => {
await user.keyboard("{Escape}");
expect(screen.queryByRole("menu", { name: "User menu" })).toBeNull();
});
it("clicking the avatar again closes the menu (toggle, not reopen)", async () => {
const user = userEvent.setup();
render(<AppTopBar />);
const avatar = screen.getByRole("button", { name: "User menu" });
await user.click(avatar);
await screen.findByRole("menu", { name: "User menu" });
await user.click(avatar);
expect(screen.queryByRole("menu", { name: "User menu" })).toBeNull();
});
});
describe("CommandPalette (REQ-63)", () => {
+12 -8
View File
@@ -11,7 +11,7 @@
*/
import { usePathname, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { forwardRef, useEffect, useRef, useState } from "react";
import {
api,
type ReportLibrary,
@@ -26,6 +26,7 @@ export function AppTopBar() {
const [paletteOpen, setPaletteOpen] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
const [library, setLibrary] = useState<ReportLibrary | null>(null);
const avatarRef = useRef<HTMLButtonElement | null>(null);
// ⌘K / Ctrl-K opens the palette from anywhere in the app.
useEffect(() => {
@@ -86,11 +87,16 @@ export function AppTopBar() {
</kbd>
</button>
<AvatarButton menuOpen={menuOpen} onToggle={() => setMenuOpen((v) => !v)} />
<AvatarButton
ref={avatarRef}
menuOpen={menuOpen}
onToggle={() => setMenuOpen((v) => !v)}
/>
</div>
{menuOpen && (
<UserMenu
toggleRef={avatarRef}
onNavigate={() => setMenuOpen(false)}
onDismiss={() => setMenuOpen(false)}
/>
@@ -111,13 +117,10 @@ export function AppTopBar() {
}
/** The avatar disc — initials from the profile fixture. */
function AvatarButton({
menuOpen,
onToggle,
}: {
const AvatarButton = forwardRef<HTMLButtonElement, {
menuOpen: boolean;
onToggle: () => void;
}) {
}>(function AvatarButton({ menuOpen, onToggle }, ref) {
const [profile, setProfile] = useState<UserProfile | null>(null);
useEffect(() => {
let cancelled = false;
@@ -136,6 +139,7 @@ function AvatarButton({
return (
<button
ref={ref}
type="button"
onClick={onToggle}
aria-haspopup="menu"
@@ -146,7 +150,7 @@ function AvatarButton({
{profile?.initials ?? "··"}
</button>
);
}
});
/** Screen label shown top-left — quiet wayfinding, not a breadcrumb. */
function pathLabel(pathname: string): string {
+22 -6
View File
@@ -345,12 +345,20 @@ function shortTitle(title: string | undefined): string {
function ProjectGraphPanel({ graph }: { graph: ProjectGraph }) {
return (
<section aria-label="Project graph" className="rounded-card border border-hairline bg-paper-raised px-5 py-5">
<h2 className="text-small font-semibold uppercase tracking-wide text-ink-faint">
Project graph
</h2>
<div className="flex items-baseline justify-between gap-2">
<h2 className="text-small font-semibold uppercase tracking-wide text-ink-faint">
Project graph
</h2>
<span className="rounded-full border border-hairline px-1.5 text-caption text-ink-faint">
preview
</span>
</div>
<p className="mt-1 text-caption text-ink-soft">
Topics you have researched, linked where they share sources.
</p>
<p className="mt-1 text-caption text-ink-faint">
Cross-project linking lands in v0.2 — preview mockup below.
</p>
<div className="mt-5 space-y-4">
{graph.nodes.map((node) => {
@@ -433,12 +441,20 @@ function TaskInboxPanel({ initialItems }: { initialItems: InboxItem[] }) {
return (
<section aria-label="Task inbox" className="rounded-card border border-hairline bg-paper-raised px-5 py-5">
<h2 className="text-small font-semibold uppercase tracking-wide text-ink-faint">
Task inbox
</h2>
<div className="flex items-baseline justify-between gap-2">
<h2 className="text-small font-semibold uppercase tracking-wide text-ink-faint">
Task inbox
</h2>
<span className="rounded-full border border-hairline px-1.5 text-caption text-ink-faint">
preview
</span>
</div>
<p className="mt-1 text-caption text-ink-soft">
Open questions worth following up · {openCount} open
</p>
<p className="mt-1 text-caption text-ink-faint">
Toggles are local-only — the inbox persists in v0.2.
</p>
<ul className="mt-4 space-y-2">
{items.map((item) => (
+1 -1
View File
@@ -75,7 +75,7 @@ export function ReadingRoom({ id }: { id: string }) {
</Link>
{" · "}
<Link href="/library" className="text-accent underline underline-offset-2">
Library (v2 preview)
Library
</Link>
</p>
<h1 className="measure-reading mt-3 text-display font-semibold text-ink">
+1 -1
View File
@@ -218,7 +218,7 @@ export function RunView({ id }: { id: string }) {
</p>
<p className="mt-3 text-caption">
<Link href="/library" className="text-accent underline underline-offset-2">
Library (v2 preview)
Library
</Link>
</p>
</div>
+19 -2
View File
@@ -55,10 +55,27 @@ describe("SettingsView (REQ-65)", () => {
expect(dialRow("Depth")).toBeInTheDocument();
expect(dialRow("Breadth")).toBeInTheDocument();
expect(dialRow("Citation rigor")).toBeInTheDocument();
expect(within(section).getByText("per-claim")).toBeInTheDocument();
expect(within(section).getByText("Per-Claim")).toBeInTheDocument();
expect(within(section).getByText("Depth")).toBeInTheDocument();
});
it("the resolved dial display follows the selected methodology live", async () => {
const user = userEvent.setup();
render(<SettingsView />);
const section = await screen.findByRole("region", { name: /Methodology defaults/i });
// deep-report resolves to hybrid routing; Rapid Answer is web-only.
expect(within(section).getByText("Hybrid")).toBeInTheDocument();
await user.selectOptions(
within(section).getByLabelText(/Default methodology/i),
"rapid-answer",
);
// The dial preview updates to Rapid Answer's bundle — memo output.
expect(within(section).getAllByText("Memo").length).toBeGreaterThanOrEqual(1);
expect(within(section).queryByText("Hybrid")).toBeNull();
});
it("save is disabled until dirty; saving round-trips through the boundary", async () => {
const user = userEvent.setup();
render(<SettingsView />);
@@ -90,6 +107,6 @@ describe("SettingsView (REQ-65)", () => {
expect(screen.getByText("preview", { exact: true })).toBeInTheDocument();
expect(screen.getByText(/About & version|About and version/i)).toBeInTheDocument();
expect(screen.getByText(/v0\.1\.5 · prototype/i)).toBeInTheDocument();
expect(screen.getByText(/v0\.1\.6 · prototype/i)).toBeInTheDocument();
});
});
+27 -12
View File
@@ -22,6 +22,7 @@ import {
type Methodology,
type Settings,
} from "@/lib/api/client";
import { formatDialValue } from "@/lib/dials";
type SettingsState =
| { kind: "loading" }
@@ -84,6 +85,9 @@ export function SettingsView() {
});
setStorage(saved.storageDirectory);
setDefaultSlug(saved.defaultMethodologySlug);
// Refresh the ready state so the resolved dial display and the
// dirty check reflect the SAVED payload, not the stale one.
setState({ kind: "ready", settings: saved, methodologies });
setSaveState("saved");
} catch {
setSaveState("error");
@@ -194,7 +198,11 @@ export function SettingsView() {
</select>
</label>
<DialDisplay settings={settings} methodologies={methodologies} />
<DialDisplay
selectedSlug={defaultSlug}
settings={settings}
methodologies={methodologies}
/>
</section>
{/* ---- Appearance -------------------------------------------- */}
@@ -236,7 +244,7 @@ export function SettingsView() {
visited-only citations, three clicks to a trustworthy report.
</p>
<p className="mt-2 text-caption text-ink-faint">
v0.1.5 · prototype — runs on scripted mock data; the engine,
v0.1.6 · prototype — runs on scripted mock data; the engine,
adapters, and trust loop arrive in v0.2.
</p>
</section>
@@ -267,32 +275,39 @@ export function SettingsView() {
);
}
/** The resolved default-dial display — labels from the DIALS registry. */
/**
* The resolved default-dial display — labels from the DIALS registry.
* Previews the currently selected methodology (live), falling back to
* the saved payload's resolved dials when the selection is unknown.
*/
function DialDisplay({
selectedSlug,
settings,
methodologies,
}: {
selectedSlug: string;
settings: Settings;
methodologies: Methodology[];
}) {
const chosen =
methodologies.find((m) => m.slug === selectedSlug) ??
methodologies.find((m) => m.slug === settings.defaultMethodologySlug) ??
methodologies[0];
const dials = chosen?.defaultDials ?? settings.defaultDials;
const rows: [string, string][] = [
["Source universe", dials.sourceUniverse],
["Search routing", dials.searchRouting],
["Query strategy", dials.queryStrategy],
["Source universe", formatDialValue(dials.sourceUniverse)],
["Search routing", formatDialValue(dials.searchRouting)],
["Query strategy", formatDialValue(dials.queryStrategy)],
["Breadth", String(dials.breadth)],
["Depth", String(dials.depth)],
["Quality gates", dials.qualityGates],
["Citation rigor", dials.citationRigor],
["Quality gates", formatDialValue(dials.qualityGates)],
["Citation rigor", formatDialValue(dials.citationRigor)],
["Verification passes", String(dials.verificationPasses)],
["Synthesis style", dials.synthesisStyle],
["Output structure", dials.outputStructure],
["Memory scope", dials.memoryScope],
["Checkpoints", dials.checkpoints],
["Synthesis style", formatDialValue(dials.synthesisStyle)],
["Output structure", formatDialValue(dials.outputStructure)],
["Memory scope", formatDialValue(dials.memoryScope)],
["Checkpoints", formatDialValue(dials.checkpoints)],
];
return (
+8 -3
View File
@@ -9,15 +9,18 @@
*/
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState, type RefObject } from "react";
import { api, type UserProfile } from "@/lib/api/client";
export function UserMenu({
onNavigate,
onDismiss,
toggleRef,
}: {
onNavigate: () => void;
onDismiss: () => void;
/** The avatar button — clicks on it toggle, not dismiss-then-reopen. */
toggleRef?: RefObject<HTMLButtonElement | null>;
}) {
const [profile, setProfile] = useState<UserProfile | null>(null);
const menuRef = useRef<HTMLDivElement>(null);
@@ -43,7 +46,9 @@ export function UserMenu({
if (event.key === "Escape") onDismiss();
};
const onClick = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
const target = event.target as Node;
if (toggleRef?.current?.contains(target)) return;
if (menuRef.current && !menuRef.current.contains(target)) {
onDismiss();
}
};
@@ -53,7 +58,7 @@ export function UserMenu({
window.removeEventListener("keydown", onKey);
window.removeEventListener("mousedown", onClick);
};
}, [onDismiss]);
}, [onDismiss, toggleRef]);
return (
<div
+33 -2
View File
@@ -102,6 +102,13 @@ export class MockApiClient implements ApiClient {
if (stored) {
return structuredClone(stored);
}
// A stored-library report's run: deep links from the library cards
// and ⌘K palette resolve to the card's own question and title —
// never the demo-run fallback (REQ-64).
const libraryRun = this.#storedRun(id);
if (libraryRun) {
return structuredClone(libraryRun);
}
// No created run this session — fall back to the scripted demo run so
// deep links and hard refreshes keep working (prototype behavior).
return structuredClone(this.#defaultRun());
@@ -132,6 +139,22 @@ export class MockApiClient implements ApiClient {
return id === this.#defaultRun().id;
}
/**
* A stored-library report's run (REQ-64). Built on demand from the
* library fixture — same question/title as the card, methodology
* slug from the summary, status done. Deep links from library
* cards and the ⌘K palette resolve through here.
*/
#storedRun(id: string): ResearchRun | undefined {
const stored = reportLibraryFixture.reports.find((r) => r.runId === id);
if (!stored) return undefined;
return buildScriptedRun({
question: stored.question,
methodologySlug: stored.methodologySlug,
id: stored.runId,
});
}
getRunEvents(
id: string,
onEvent: (event: TraceEvent) => void,
@@ -187,7 +210,14 @@ export class MockApiClient implements ApiClient {
async getReport(id: string): Promise<Report> {
const run = await this.getRun(id);
return buildScriptedReport(run, run.plan.question);
// Stored-library reports keep their card title — the library card,
// palette entry, and report headline always agree.
const stored = reportLibraryFixture.reports.find((r) => r.runId === id);
return buildScriptedReport(
run,
run.plan.question,
stored?.title,
);
}
async getLedger(id: string): Promise<CitationLedger> {
@@ -200,7 +230,8 @@ export class MockApiClient implements ApiClient {
format: "markdown" | "json",
): Promise<{ filename: string; content: string }> {
const run = await this.getRun(id);
const report = buildScriptedReport(run, run.plan.question);
const stored = reportLibraryFixture.reports.find((r) => r.runId === id);
const report = buildScriptedReport(run, run.plan.question, stored?.title);
const ledger = buildScriptedLedger(run);
if (format === "json") {
return {
+11 -4
View File
@@ -286,14 +286,16 @@ function hash(input: string): string {
/**
* Build the scripted Deep Report run. The question (and optional
* context) are embedded verbatim into the plan, so every screen in the
* click-path shows the user's own words.
* click-path shows the user's own words. An explicit id override keeps
* stored-library run ids stable for deep links.
*/
export function buildScriptedRun(input: {
question: string;
context?: string;
methodologySlug: string;
id?: string;
}): ResearchRun {
const runId = scriptedRunId(input.question);
const runId = input.id ?? scriptedRunId(input.question);
return {
id: runId,
status: "done",
@@ -327,15 +329,20 @@ export function buildScriptedRun(input: {
};
}
/** The scripted Deep Report — executive summary + 4 sections, [^pN] markers. */
/**
* The scripted Deep Report — executive summary + 4 sections, [^pN] markers.
* An explicit title override keeps stored-library reports consistent with
* their library-card titles (the card title IS the report title).
*/
export function buildScriptedReport(
run: ResearchRun,
question: string,
title?: string,
): Report {
return {
id: `${run.id}-report`,
runId: run.id,
title: `Deep Report — ${question}`,
title: title ?? `Deep Report — ${question}`,
sections: [
{
heading: "Executive summary",
@@ -34,6 +34,22 @@ describe("getReportLibrary (REQ-64)", () => {
expect(first.generatedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
expect(first.citationCount).toBeGreaterThanOrEqual(0);
});
it("every stored report's run deep-links resolve to its own title + question", async () => {
const library = await client.getReportLibrary();
for (const summary of library.reports) {
// The run behind the card: the card's own id, question, methodology.
const run = await client.getRun(summary.runId);
expect(run.id).toBe(summary.runId);
expect(run.plan.question).toBe(summary.question);
expect(run.plan.methodologySlug).toBe(summary.methodologySlug);
expect(run.status).toBe("done");
// The report headline matches the card title — no bait-and-switch.
const report = await client.getReport(summary.runId);
expect(report.title).toBe(summary.title);
expect(report.runId).toBe(summary.runId);
}
});
});
describe("getReportConnections (REQ-64)", () => {
+2 -1
View File
@@ -405,4 +405,5 @@ export interface UserProfile {
initials: string;
/** Role line under the name in the menu. */
tagline: string;
}
}