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:
@@ -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)
|
||||
|
||||
@@ -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
@@ -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.
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -28,4 +28,4 @@ export default function AskPage() {
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,4 @@ import { api } from "@/lib/api/client";
|
||||
export default async function ChoosePage() {
|
||||
const methodologies = await api.getMethodologies();
|
||||
return <ChooseView methodologies={methodologies} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,4 +36,4 @@ export default function RootLayout({
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,4 @@ export default function LibraryPage() {
|
||||
<LibraryView />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,4 @@ export default async function ReportPage({
|
||||
<ReadingRoom id={id} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)", () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)", () => {
|
||||
|
||||
@@ -405,4 +405,5 @@ export interface UserProfile {
|
||||
initials: string;
|
||||
/** Role line under the name in the menu. */
|
||||
tagline: string;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user