feat: US-001 - Reverse timeline direction to top = most recent
This commit is contained in:
+160
-299
@@ -1,376 +1,237 @@
|
||||
{
|
||||
"project": "Portfolio — LLM CV Knowledge Accuracy",
|
||||
"branchName": "ralph/llm-cv-knowledge",
|
||||
"description": "Migrate from Gemini to OpenRouter (z-ai/glm-5), enrich LLM context with full CV detail, and benchmark accuracy against 10 verifiable questions until 90%+ pass rate.",
|
||||
"project": "Portfolio — Career Constellation Clinical Pathway Overhaul",
|
||||
"branchName": "ralph/constellation-overhaul",
|
||||
"description": "Transform the CareerConstellation D3 force graph from a prototype-quality visualisation into a polished clinical patient pathway diagram — reversed timeline, dynamic height sync, refined node styling, bidirectional hover highlighting, and muted skill nodes that reveal on interaction.",
|
||||
"userStories": [
|
||||
{
|
||||
"id": "US-001",
|
||||
"title": "Install @xenova/transformers and add generate-embeddings script skeleton",
|
||||
"description": "As a developer, I need the Transformers.js dependency installed and a runnable script scaffold so subsequent stories can generate and use embeddings.",
|
||||
"title": "Reverse timeline direction to top = most recent",
|
||||
"description": "As a visitor, I want the graph's vertical timeline to run top-to-bottom from 2025→2017 so it visually aligns with the reverse-chronological work experience cards in the adjacent column.",
|
||||
"acceptanceCriteria": [
|
||||
"npm install @xenova/transformers",
|
||||
"Create scripts/generate-embeddings.ts with a main() function that imports the pipeline from @xenova/transformers",
|
||||
"Script loads the all-MiniLM-L6-v2 model and embeds a single test string, logging the vector length to confirm it works",
|
||||
"Add npm script: \"generate-embeddings\": \"npx tsx scripts/generate-embeddings.ts\"",
|
||||
"Running npm run generate-embeddings prints the vector length (384) and exits cleanly",
|
||||
"Typecheck passes"
|
||||
"yScale domain reversed: [maxYear, minYear] maps to [topPadding, height - bottomPadding] so 2025 is near the top and 2017 near the bottom",
|
||||
"Role nodes appear at correct reversed year positions",
|
||||
"Year labels along the timeline axis read top-to-bottom: 2025, 2024, ..., 2017",
|
||||
"Skill nodes cluster around their linked roles at the correct vertical positions",
|
||||
"Timeline vertical line, year dots, and horizontal guide lines all reflect the reversed scale",
|
||||
"Screen reader description (srDescription) updated to mention reverse-chronological order",
|
||||
"Typecheck passes (npm run typecheck)"
|
||||
],
|
||||
"priority": 1,
|
||||
"passes": true,
|
||||
"notes": "Use @xenova/transformers (not @huggingface/transformers — the Xenova fork has better Node.js ONNX support). The model ID is 'Xenova/all-MiniLM-L6-v2'. Pipeline type is 'feature-extraction'. tsx is already available via npx for running TypeScript scripts."
|
||||
"notes": "In CareerConstellation.tsx, the yScale is defined at line ~153. Change .domain([minYear, maxYear]) to .domain([maxYear, minYear]). This reversal flows through all elements that use yScale. The buildScreenReaderDescription() function at line ~63 should also mention 'reverse-chronological order' in its output. Use the d3-viz skill for implementation."
|
||||
},
|
||||
{
|
||||
"id": "US-002",
|
||||
"title": "Build rich text representations for each palette item",
|
||||
"description": "As a developer, I want each palette item to have a natural-language paragraph for embedding that captures deep context, not just the title.",
|
||||
"title": "Dynamic height matching with work experience column",
|
||||
"description": "As a visitor, I want the constellation graph to fill the same vertical space as the work experience column so both columns appear balanced.",
|
||||
"acceptanceCriteria": [
|
||||
"New function buildEmbeddingTexts() in src/lib/search.ts that returns Array<{ id: string, text: string }> for all palette items",
|
||||
"Consultation items include: role, org, duration, history narrative, examination bullets, coded entry descriptions",
|
||||
"Skill items include: name, category, frequency, proficiency percentage, years of experience",
|
||||
"KPI items include: value, label, explanation, story context and outcomes",
|
||||
"Investigation items include: name, methodology, tech stack list, results",
|
||||
"Education items include: title, institution, type, research detail",
|
||||
"Quick Action items include: title and subtitle (short text is fine)",
|
||||
"Achievement items include: title, subtitle, and linked KPI story context if available",
|
||||
"Each text is a readable natural-language paragraph, not a keyword dump",
|
||||
"Typecheck passes"
|
||||
"Remove fixed DESKTOP_HEIGHT, TABLET_HEIGHT, MOBILE_HEIGHT constants from CareerConstellation.tsx",
|
||||
"CareerConstellation accepts an optional containerHeight prop (number) for the target height",
|
||||
"DashboardLayout measures the rendered height of the .chronology-stream element using a ref and ResizeObserver",
|
||||
"DashboardLayout passes the measured height (or a sensible fallback) to CareerConstellation as containerHeight",
|
||||
"Graph container uses containerHeight when available, with a minimum of 400px",
|
||||
"On mobile (single-column layout where .pathway-columns is 1fr), the graph uses a standalone fallback height of 360px",
|
||||
"The viewBox and all D3 scales update correctly when height changes",
|
||||
"Typecheck passes (npm run typecheck)",
|
||||
"Verify in browser: expand/collapse work experience cards and confirm graph height adjusts"
|
||||
],
|
||||
"priority": 2,
|
||||
"passes": true,
|
||||
"notes": "This function will be used by both the build script (to generate embeddings) and potentially by the chat widget (for context). Import the raw data files (consultations, skills, kpis, investigations, documents) to access the full data beyond what buildPaletteData() surfaces. The id must match the PaletteItem id so embeddings can be correlated."
|
||||
"passes": false,
|
||||
"notes": "Add a ref to the .chronology-stream div in DashboardLayout. Use ResizeObserver to measure its offsetHeight. Pass it as a prop to CareerConstellation. Inside the constellation, use this prop in the dimensions state instead of the fixed getHeight() function. The getHeight() function can become the fallback for when no containerHeight is provided. CSS class .pathway-graph-sticky already has position:sticky — the height change should work within that. Use the d3-viz skill for implementation."
|
||||
},
|
||||
{
|
||||
"id": "US-003",
|
||||
"title": "Generate and commit embeddings.json",
|
||||
"description": "As a developer, I want the generate-embeddings script to produce a complete embeddings.json file using the rich text representations.",
|
||||
"title": "Clinical pathway background and timeline structure",
|
||||
"description": "As a visitor, I want the graph to look like a clinical patient pathway diagram — clean, precise, and institutional — matching the GP system dashboard aesthetic.",
|
||||
"acceptanceCriteria": [
|
||||
"scripts/generate-embeddings.ts imports buildEmbeddingTexts() from src/lib/search.ts",
|
||||
"Script embeds each item's text using the all-MiniLM-L6-v2 model via @xenova/transformers pipeline",
|
||||
"Outputs src/data/embeddings.json as an array of { id: string, embedding: number[] }",
|
||||
"Each embedding is a 384-dimensional float array",
|
||||
"Running npm run generate-embeddings regenerates the file successfully",
|
||||
"The JSON file is valid and parseable",
|
||||
"Typecheck passes"
|
||||
"Background: remove the radial gradient, use a clean fill matching var(--surface) (#FFFFFF) or very subtle var(--bg-dashboard) (#F0F5F4)",
|
||||
"Add a subtle 1px border to the SVG container via the wrapping div: border 1px solid var(--border-light), border-radius var(--radius-sm)",
|
||||
"Timeline axis: refined 1px vertical rule using var(--border) colour (#D4E0DE), not the current thick teal line",
|
||||
"Year markers: small horizontal ticks (6-8px wide) extending right from the timeline, not floating dots",
|
||||
"Year labels: font-family var(--font-geist-mono), font-size 10px, fill var(--text-tertiary) (#8DA8A5)",
|
||||
"Horizontal guide lines: very subtle — stroke-opacity 0.25, stroke-dasharray '3 4' (dotted), using var(--border-light)",
|
||||
"Remove the existing legend box from inside the SVG entirely (replacement comes in US-008)",
|
||||
"All colours use CSS custom property values from the design system",
|
||||
"Typecheck passes (npm run typecheck)",
|
||||
"Verify in browser — the graph background and structure should feel consistent with the rest of the dashboard tiles"
|
||||
],
|
||||
"priority": 3,
|
||||
"passes": true,
|
||||
"notes": "The pipeline returns a Tensor — use .tolist() or .data to extract the raw float array. Mean-pool across the token dimension (dim 1) to get a single 384-d vector per input. Process items sequentially to avoid OOM in Node. The output file will be ~200KB for ~40 items with 384 floats each."
|
||||
"passes": false,
|
||||
"notes": "Most changes are in the main useEffect that builds the SVG (starting around line 132). Remove the radialGradient defs and the rect that uses it. Replace with a simple rect fill. The legendGroup creation (lines ~221-265) should be removed entirely. The timeline vertical line (lines ~189-196) should change from stroke #A8C4BF / width 2 to the border token colour / width 1. Year dots (circle.year-dot) should become short horizontal ticks (line elements). Year guide lines should become dashed. Use the d3-viz skill for implementation."
|
||||
},
|
||||
{
|
||||
"id": "US-004",
|
||||
"title": "Preload ONNX model during boot sequence",
|
||||
"description": "As a visitor, I want the semantic search model to download in the background during the boot/ECG/login phases so it's ready when I reach the dashboard.",
|
||||
"title": "Role node redesign — clinical record pill badges",
|
||||
"description": "As a visitor, I want role nodes to look like refined clinical record entries — rounded rectangle badges anchored to their timeline position.",
|
||||
"acceptanceCriteria": [
|
||||
"New src/lib/embedding-model.ts module that exports: initModel(), embedQuery(text: string), and isModelReady()",
|
||||
"initModel() loads the all-MiniLM-L6-v2 pipeline from @xenova/transformers and stores it in a module-level variable",
|
||||
"embedQuery() returns a Promise<number[]> (384-d vector) for a given text string",
|
||||
"isModelReady() returns boolean indicating if the model has finished loading",
|
||||
"initModel() is called in App.tsx useEffect on mount (during boot phase) — fire and forget, no await",
|
||||
"If initModel() fails (network error, etc.), isModelReady() remains false — no error thrown or shown",
|
||||
"Model is cached by @xenova/transformers in IndexedDB — subsequent page loads are near-instant",
|
||||
"Boot/ECG/login animations are not affected by model loading",
|
||||
"Typecheck passes"
|
||||
"Role nodes rendered as rounded rectangles (pills): approximately 100px wide x 32px tall, with rx/ry 16px for pill shape",
|
||||
"Each role node displays shortLabel text centred inside, using font-family var(--font-ui), weight 600, size 11px",
|
||||
"Role node fill uses orgColor at 0.12 opacity, with a 1px border of orgColor at 0.4 opacity, and text in orgColor at full strength",
|
||||
"A thin connector line (1px, var(--border) colour) links each role node horizontally back to the timeline axis at its year position",
|
||||
"Role node hover state: border opacity increases to 0.7, shadow appears (approximate var(--shadow-sm))",
|
||||
"Active/pinned role node: border becomes solid at full orgColor opacity, slightly stronger shadow",
|
||||
"ROLE_RADIUS constant replaced with ROLE_WIDTH and ROLE_HEIGHT constants for the pill dimensions",
|
||||
"Force simulation collision detection updated to use the pill dimensions (not circular radius)",
|
||||
"Focus ring styling updated to surround the pill shape instead of the old circle",
|
||||
"Typecheck passes (npm run typecheck)",
|
||||
"Verify in browser — role nodes appear as labelled pill badges along the timeline"
|
||||
],
|
||||
"priority": 4,
|
||||
"passes": true,
|
||||
"notes": "Use pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2') which auto-downloads and caches the ONNX model. The module-level pattern (let pipelineInstance = null) avoids React re-render issues. embedQuery should mean-pool the tensor output the same way as the build script. Wrap initModel() in a try/catch that silently swallows errors."
|
||||
"passes": false,
|
||||
"notes": "This changes role nodes from <circle> to <rect> with rounded corners. The nodeSelection code that filters d.type === 'role' (lines ~354-380) needs to append 'rect' instead of 'circle'. Position with x = -ROLE_WIDTH/2 and y = -ROLE_HEIGHT/2 so they centre on the force simulation position. The focus-ring can also become a rect. The text element stays largely the same but needs its positioning adjusted (no more dy offset needed if dominant-baseline is middle). The collision force for roles should use a radius roughly equal to Math.max(ROLE_WIDTH, ROLE_HEIGHT)/2 + padding. The connector line should go from the timeline X position to the left edge of the pill node. Use the d3-viz skill for implementation."
|
||||
},
|
||||
{
|
||||
"id": "US-005",
|
||||
"title": "Implement cosine similarity search module",
|
||||
"description": "As a developer, I need a semantic search function that compares a query embedding against pre-computed item embeddings and returns ranked results.",
|
||||
"title": "Skill node redesign — muted default with reveal on interaction",
|
||||
"description": "As a visitor, I want skill nodes to be visually subdued by default, becoming prominent only when a connected role or skill is hovered or clicked.",
|
||||
"acceptanceCriteria": [
|
||||
"New src/lib/semantic-search.ts module",
|
||||
"Exports semanticSearch(queryEmbedding: number[], embeddings: Array<{ id: string, embedding: number[] }>, threshold?: number): Array<{ id: string, score: number }>",
|
||||
"Uses cosine similarity: dot(a,b) / (magnitude(a) * magnitude(b))",
|
||||
"Results sorted by score descending",
|
||||
"Optional threshold parameter filters out low-relevance results (default 0.3)",
|
||||
"Exports loadEmbeddings() that imports embeddings.json and returns the parsed array",
|
||||
"Typecheck passes"
|
||||
"Default (resting) state: small circles radius 7px, fill-opacity 0.2, no visible label (label opacity 0)",
|
||||
"Skill node fill colours by domain: technical uses var(--accent) #0D6E6E, clinical uses var(--success) #059669, leadership uses var(--amber) #D97706",
|
||||
"When a connected role is hovered/pinned: connected skill nodes transition to radius 11px, fill-opacity 0.85, labels fade in (opacity 0 → 1)",
|
||||
"Skill labels: font-family var(--font-geist-mono), font-size 10px, fill var(--text-secondary) (#5B7A78)",
|
||||
"When a skill node itself is hovered: that skill and all connected roles highlight, skill grows to full size with label visible",
|
||||
"Link lines default state: opacity 0.08, colour var(--border-light) — barely visible",
|
||||
"Link lines highlighted state: opacity 0.55, colour matching the skill's domain colour, stroke-width 1.5px",
|
||||
"Unconnected nodes (not in the active highlight group) reduce to opacity 0.06 — nearly invisible",
|
||||
"All transitions 150-200ms and respect prefers-reduced-motion (skip to final state)",
|
||||
"Typecheck passes (npm run typecheck)",
|
||||
"Verify in browser — graph looks clean and quiet at rest, informative on hover"
|
||||
],
|
||||
"priority": 5,
|
||||
"passes": true,
|
||||
"notes": "Keep the cosine similarity implementation simple — no libraries needed for 384-d vectors over ~40 items. The loadEmbeddings function can use a dynamic import or direct import of the JSON file (Vite handles JSON imports natively)."
|
||||
"passes": false,
|
||||
"notes": "This modifies the applyGraphHighlight() function (line ~439) and the initial skill node rendering (lines ~382-403). The resting state setup happens when nodes are first created and in the 'no activeNodeId' branch of applyGraphHighlight. The highlighted state logic is in the activeNodeId branch. Key change: skill labels default to opacity 0 (not the current collision-based visibility), and only become visible via applyGraphHighlight when connected. The updateSkillLabelVisibility() function can be simplified or merged into applyGraphHighlight. The SKILL_RADIUS constant should be split into SKILL_RADIUS_DEFAULT (7) and SKILL_RADIUS_ACTIVE (11). Link line styling in the resting branch should use much lower opacity than current 0.45. Use the d3-viz skill for implementation."
|
||||
},
|
||||
{
|
||||
"id": "US-006",
|
||||
"title": "Integrate semantic search into command palette",
|
||||
"description": "As a visitor, I want the command palette to use semantic search when available, falling back to Fuse.js otherwise.",
|
||||
"title": "Bidirectional hover — graph node highlights timeline card",
|
||||
"description": "As a visitor, I want hovering a role node in the graph to highlight the corresponding work experience card in the timeline, creating a clear bidirectional link.",
|
||||
"acceptanceCriteria": [
|
||||
"CommandPalette.tsx checks isModelReady() from embedding-model.ts",
|
||||
"When model is ready and query is non-empty: call embedQuery(query), then semanticSearch() against loaded embeddings, then map result IDs back to PaletteItem objects",
|
||||
"When model is NOT ready: use existing Fuse.js search (current behavior preserved exactly)",
|
||||
"Search is debounced by ~200ms to avoid calling embedQuery on every keystroke",
|
||||
"Results maintain existing groupBySection() grouping and section ordering",
|
||||
"Existing keyboard navigation, action routing, and UI unchanged",
|
||||
"Typecheck passes",
|
||||
"Verify in browser: search 'data analysis' surfaces analytics-related roles/skills not just items with 'data' in title"
|
||||
"CareerConstellation gains a new prop: onNodeHover?: (id: string | null) => void",
|
||||
"Role node mouseenter fires onNodeHover(d.id), mouseleave fires onNodeHover(null)",
|
||||
"DashboardLayout passes onNodeHover callback to CareerConstellation and stores result as highlightedRoleId state",
|
||||
"WorkExperienceSubsection gains a new prop: highlightedRoleId?: string | null",
|
||||
"When highlightedRoleId matches a RoleItem's consultation.id, that card shows a subtle highlight: border-color var(--accent-border), background rgba(10,128,128,0.03)",
|
||||
"LastConsultationSubsection also gains highlightedRoleId prop and participates in the highlight system for the most recent role (consultations[0].id)",
|
||||
"Highlight clears when mouse leaves both the card and graph node",
|
||||
"On touch devices, tap-to-pin works: tapping a role pins the highlight in both graph and timeline",
|
||||
"Existing onNodeHighlight (timeline → graph) continues to work alongside the new reverse direction",
|
||||
"Typecheck passes (npm run typecheck)",
|
||||
"Verify in browser — hover graph nodes and confirm timeline cards highlight; hover timeline cards and confirm graph highlights"
|
||||
],
|
||||
"priority": 6,
|
||||
"passes": true,
|
||||
"notes": "The debounce is important — embedQuery takes ~20-50ms per call. Use a useRef + setTimeout pattern or a simple debounce hook. The mapping from semantic search results (id + score) back to PaletteItems should use a Map for O(1) lookup. Keep the Fuse.js imports and buildSearchIndex — they're the fallback path."
|
||||
"passes": false,
|
||||
"notes": "This adds the reverse direction to the existing partial bidirectional system. Currently DashboardLayout has handleNodeHighlight which sets highlightedNodeId (timeline → graph). The new onNodeHover adds graph → timeline. Both pieces of state coexist. In WorkExperienceSubsection, add a style to the RoleItem wrapper div that applies when highlightedRoleId matches — a subtle border and background change. For LastConsultationSubsection, apply the same highlight logic to its outer wrapper. The touch/pin logic in CareerConstellation already handles pinnedNodeId — the new onNodeHover should also fire for pinned nodes so timeline cards stay highlighted."
|
||||
},
|
||||
{
|
||||
"id": "US-007",
|
||||
"title": "Chat widget — floating button component",
|
||||
"description": "As a visitor, I see a floating chat button at the bottom-right of the dashboard that I can click to open a chat panel.",
|
||||
"title": "Curved link lines between roles and skills",
|
||||
"description": "As a visitor, I want the connection lines between roles and skills to be smooth curves rather than basic straight lines, matching a clinical pathway aesthetic.",
|
||||
"acceptanceCriteria": [
|
||||
"New src/components/ChatWidget.tsx component",
|
||||
"Renders a 48px circular button, fixed position, bottom: 24px, right: 24px",
|
||||
"Uses teal accent background (var(--accent)), white MessageCircle icon from lucide-react",
|
||||
"Shadow: var(--shadow-md). Hover: var(--shadow-lg) + scale(1.05) transition",
|
||||
"Button has a subtle entrance animation: fade + translateY(8px) → translateY(0), delayed ~1s after mount",
|
||||
"Respects prefers-reduced-motion (no animation, just visible)",
|
||||
"z-index above dashboard content but below command palette overlay (z-index 90)",
|
||||
"onClick toggles an isOpen state (panel rendering comes in next story)",
|
||||
"Mounted in DashboardLayout.tsx",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
"Replace <line> elements with <path> elements for links",
|
||||
"Use D3 curve generators (d3.curveBasis or d3.line().curve(d3.curveBasis)) to create smooth curves between source and target",
|
||||
"Default link styling: 1px stroke, colour var(--border-light), opacity 0.08 — barely visible at rest",
|
||||
"Highlighted link styling: 1.5px stroke, domain colour of the skill end, opacity proportional to link strength value (range 0.35-0.65)",
|
||||
"The tick handler updates path d attributes instead of line x1/y1/x2/y2",
|
||||
"Links animate smoothly between default and highlighted states (CSS transition on stroke, stroke-opacity, stroke-width)",
|
||||
"Respect prefers-reduced-motion — skip transitions",
|
||||
"Typecheck passes (npm run typecheck)",
|
||||
"Verify in browser — links are nearly invisible at rest and clearly trace pathways on hover"
|
||||
],
|
||||
"priority": 7,
|
||||
"passes": true,
|
||||
"notes": "Use framer-motion for the entrance animation to match the rest of the app's motion patterns. The button should use font-ui for any text. On mobile (<640px), button is 40px and positioned bottom: 16px, right: 16px. The VITE_GEMINI_API_KEY env var check can wait until the Gemini integration story — for now just render the button unconditionally."
|
||||
"passes": false,
|
||||
"notes": "The linkSelection is created at lines ~340-345. Change from .join('line') to .join('path'). For the curve, generate a simple quadratic or cubic bezier path string in the tick handler: given source (sx,sy) and target (tx,ty), create a path like M sx,sy Q cx,cy tx,ty where cx,cy is a control point offset to create a gentle arc. A simple approach: control point at ((sx+tx)/2, sy) or ((sx+tx)/2, (sy+ty)/2 + offset). Alternatively use d3.linkHorizontal() or d3.linkVertical() which generate smooth curves between two points. The applyGraphHighlight function's link styling (lines ~465-482) needs updating from line attributes to path attributes — but stroke/stroke-opacity/stroke-width work the same on paths. Use the d3-viz skill for implementation."
|
||||
},
|
||||
{
|
||||
"id": "US-008",
|
||||
"title": "Chat widget — panel UI with message display",
|
||||
"description": "As a visitor, I want a chat panel that opens above the floating button where I can type questions and see responses.",
|
||||
"title": "Compact domain legend as HTML below SVG",
|
||||
"description": "As a visitor, I want a small unobtrusive legend explaining the domain colour coding, rendered as HTML below the graph.",
|
||||
"acceptanceCriteria": [
|
||||
"Chat panel renders when isOpen is true, positioned above the floating button (bottom: 88px, right: 24px)",
|
||||
"Panel dimensions: 380px wide, max-height 480px, with overflow-y auto for messages",
|
||||
"Header: title text ('Ask about Andy'), close button (X icon)",
|
||||
"Message area: user messages right-aligned in teal-tinted bubbles, assistant messages left-aligned in light gray bubbles",
|
||||
"Input area at bottom: text field with placeholder 'Ask me anything...', send button (Send icon)",
|
||||
"Enter key submits message, Shift+Enter for newline",
|
||||
"Panel entrance animation: scale(0.95) + opacity(0) → scale(1) + opacity(1), 200ms ease-out",
|
||||
"Panel exit animation: reverse of entrance",
|
||||
"Respects prefers-reduced-motion",
|
||||
"Responsive: on mobile (<640px), panel is full-width (left: 0, right: 0, bottom: 0) with rounded top corners only",
|
||||
"Messages are stored in component state as Array<{ role: 'user' | 'assistant', content: string }>",
|
||||
"Submitting a message adds it to state and shows it in the UI (no API call yet — assistant response is a placeholder)",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
"A compact single-line legend rendered as a React div below the SVG element, inside the CareerConstellation container",
|
||||
"Legend shows three small coloured dots (6px circles) with labels: 'Technical', 'Clinical', 'Leadership' using the domain colours (var(--accent), var(--success), var(--amber))",
|
||||
"Legend text: font-family var(--font-geist-mono), font-size 10px, colour var(--text-tertiary)",
|
||||
"Items separated by subtle dot or pipe separators",
|
||||
"Include hint text: 'Hover to explore connections' — same style, slightly more muted",
|
||||
"Legend takes minimal vertical space (~24px total height)",
|
||||
"Legend wraps gracefully on narrow screens (flex-wrap)",
|
||||
"Typecheck passes (npm run typecheck)",
|
||||
"Verify in browser"
|
||||
],
|
||||
"priority": 8,
|
||||
"passes": true,
|
||||
"notes": "Use the design system tokens: var(--surface) for panel bg, var(--border-light) for borders, var(--text-primary) for text, var(--accent) for user bubble bg at 10% opacity, font-ui for body text, font-geist for timestamps. The placeholder assistant response can be a static string like 'AI chat coming soon — this is a preview of the chat interface.' This lets us verify the full UI before wiring up Gemini."
|
||||
"passes": false,
|
||||
"notes": "This is pure React JSX added to the return block of CareerConstellation (after the SVG and before the closing container div). No D3 involved. Use inline styles consistent with the rest of the component, or simple Tailwind classes. The legend replaces the SVG-based legend that was removed in US-003. Position it as a flex row with gap: 12px, items centred vertically, padding: 6px 12px."
|
||||
},
|
||||
{
|
||||
"id": "US-009",
|
||||
"title": "Chat widget — Gemini Flash integration",
|
||||
"description": "As a visitor, I can ask natural language questions and get intelligent, streamed answers about Andy's experience.",
|
||||
"title": "Force simulation tuning for clinical layout",
|
||||
"description": "As a developer, I want the D3 force simulation tuned so role nodes stay firmly anchored to timeline positions while skill nodes distribute cleanly to the right.",
|
||||
"acceptanceCriteria": [
|
||||
"New src/lib/gemini.ts module that exports sendChatMessage(messages: ChatMessage[], cvContext: string): AsyncGenerator<string>",
|
||||
"Calls Google Gemini Flash API (gemini-2.0-flash) using the REST API with fetch (no SDK needed)",
|
||||
"API key sourced from import.meta.env.VITE_GEMINI_API_KEY",
|
||||
"System prompt includes structured CV context built from buildEmbeddingTexts() output",
|
||||
"System prompt instructs the model to answer questions about Andy's professional experience accurately and concisely",
|
||||
"System prompt instructs the model to include relevant palette item IDs in its response as a JSON array at the end",
|
||||
"Responses are streamed using the Gemini streaming endpoint",
|
||||
"ChatWidget.tsx wires up real messages: on submit, calls sendChatMessage and streams tokens into the assistant message bubble",
|
||||
"Loading state shown (typing indicator) while waiting for first token",
|
||||
"If VITE_GEMINI_API_KEY is not set, chat button is still visible but panel shows 'Chat is currently unavailable' message",
|
||||
"If API call fails, show error message in chat: 'Sorry, I couldn't process that. Please try again.'",
|
||||
"Conversation history (last 10 messages) passed to API for multi-turn context",
|
||||
"Typecheck passes"
|
||||
"Role nodes have very high forceY strength (0.95-1.0) and consistent forceX strength anchoring them at a fixed horizontal offset from the timeline",
|
||||
"Skill nodes distribute in the space to the right of the role column, clustered near connected roles",
|
||||
"Increase collision radius to prevent label overlap when skills are revealed on hover (account for SKILL_RADIUS_ACTIVE + label height)",
|
||||
"Simulation alphaDecay tuned so graph stabilises within 1-2 seconds (or immediately for prefers-reduced-motion)",
|
||||
"Boundary clamping keeps all nodes within the SVG viewport with adequate padding — role pill labels don't clip, skill labels don't overflow",
|
||||
"On height changes (from US-002), simulation re-initialises without jarring jumps — preserve approximate positions",
|
||||
"The charge force strength balanced to avoid nodes clustering too tightly or spreading too far",
|
||||
"Typecheck passes (npm run typecheck)",
|
||||
"Verify in browser — nodes appear organised and intentional, not randomly scattered"
|
||||
],
|
||||
"priority": 9,
|
||||
"passes": true,
|
||||
"notes": "Gemini REST streaming endpoint: POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent?alt=sse&key=API_KEY. The response is SSE (server-sent events) — parse each 'data:' line as JSON and extract candidates[0].content.parts[0].text. The system prompt with CV context will be ~2-3K tokens — well within Gemini Flash limits. For the palette item IDs, instruct the model to end its response with a line like [ITEMS: id1, id2, id3] which can be parsed client-side."
|
||||
"passes": false,
|
||||
"notes": "The simulation is configured at lines ~515-532. Key parameters to tune: forceX/forceY strengths for roles (increase to ~1.0), forceX/forceY for skills (keep at 0.15-0.25 for organic clustering), charge strength (currently -85, may need adjustment with new pill-shaped roles), collide radius (needs to account for pill width for roles, and active radius + label for skills), link distance (currently 56, may need increase with larger role nodes). The alphaDecay is currently 0.06 for animated mode — could increase to 0.08-0.1 for faster settling. For the reduced-motion path, the 220 ticks (line 580) may need adjustment. Use the d3-viz skill for implementation."
|
||||
},
|
||||
{
|
||||
"id": "US-010",
|
||||
"title": "Chat widget — clickable portfolio item cards in responses",
|
||||
"description": "As a visitor, I want AI chat responses to include clickable portfolio items so I can drill into relevant sections.",
|
||||
"title": "Content audit — verify role data against CV source",
|
||||
"description": "As the portfolio owner, I want all role titles, organisations, dates, and achievement bullets verified against the source CV documents.",
|
||||
"acceptanceCriteria": [
|
||||
"After parsing the assistant response, extract referenced palette item IDs from the [ITEMS: ...] suffix",
|
||||
"Render matched items as compact clickable cards below the answer text in the assistant bubble",
|
||||
"Cards reuse icon/color mapping from CommandPalette (iconByType, iconColorStyles)",
|
||||
"Cards show item title and subtitle in a compact horizontal layout",
|
||||
"Clicking a card triggers the same action routing as command palette via handlePaletteAction in DashboardLayout",
|
||||
"If no items are referenced or IDs don't match, no cards are shown (just the text answer)",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
"Cross-reference src/data/consultations.ts against References/CV_v4.md and References/Andy_Charlwood_CV_ATS_Optimised.pdf",
|
||||
"All role titles match the CV exactly",
|
||||
"All organisation names are consistent (e.g., 'NHS Norfolk & Waveney ICB' everywhere, 'Tesco PLC' not 'Tesco')",
|
||||
"All date ranges are correct (start/end for each role matching CV)",
|
||||
"Achievement bullets (examination arrays) are accurate — numbers, percentages, claims match CV source",
|
||||
"constellation.ts role node data (labels, shortLabels, orgColors, years) is consistent with consultations.ts",
|
||||
"Any discrepancies found are fixed",
|
||||
"Intentional abbreviations (e.g., shortened bullet text) are documented in code comments only where truly necessary",
|
||||
"Typecheck passes (npm run typecheck)"
|
||||
],
|
||||
"priority": 10,
|
||||
"passes": true,
|
||||
"notes": "The action routing needs to flow from ChatWidget up to DashboardLayout. Add an onAction prop to ChatWidget (same pattern as CommandPalette). DashboardLayout passes handlePaletteAction to ChatWidget. Export iconByType and iconColorStyles from CommandPalette (or extract to a shared module) so ChatWidget can reuse them."
|
||||
"passes": false,
|
||||
"notes": "Read src/data/consultations.ts and compare field-by-field against References/CV_v4.md. The CV has 4 roles: Interim Head (May-Nov 2025), Deputy Head (Jul 2024-Present), High-Cost Drugs (May 2022-Jul 2024), Pharmacy Manager (Nov 2017-May 2022). Check that consultations.ts has the same number of entries with matching data. Also verify constellation.ts nodes match — particularly startYear/endYear values and organization names. Fix any mismatches in the data files."
|
||||
},
|
||||
{
|
||||
"id": "US-011",
|
||||
"title": "Mobile full-screen chat panel",
|
||||
"description": "As a mobile visitor, I want the chat panel to be a full-screen overlay so it's easy to use on small screens.",
|
||||
"title": "Accessibility — fix focusable buttons and tab order",
|
||||
"description": "As a visitor using assistive technology, I want the constellation graph to be keyboard navigable with proper focus rings and screen reader support.",
|
||||
"acceptanceCriteria": [
|
||||
"Below md breakpoint (768px), chat panel renders as full-screen overlay using position: fixed; inset: 0 with 100dvh height",
|
||||
"Full-screen mode has the existing header with close button (no visual change needed, just full-width)",
|
||||
"Floating chat button is hidden (display: none or opacity: 0) while panel is open on mobile (<768px)",
|
||||
"Above 768px, existing panel behavior is unchanged (380px wide, anchored bottom-right, max-height 480px)",
|
||||
"Panel open/close animation still respects prefers-reduced-motion",
|
||||
"Safe area insets applied via env(safe-area-inset-*) for notched devices",
|
||||
"Input area stays pinned to bottom of screen on mobile",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
"Hidden accessibility buttons have pointerEvents: 'auto' (not 'none') so they are actually focusable and clickable",
|
||||
"Tab order follows reverse-chronological sequence: role nodes from most recent to oldest, then skill nodes grouped by domain (technical → clinical → leadership)",
|
||||
"Focus ring styling is visible: 2px solid var(--accent) with 2px offset, matching design system",
|
||||
"aria-label on the SVG updated to mention 'clinical pathway' metaphor",
|
||||
"All interactive states (hover highlight, pin) are achievable via keyboard (Enter/Space to activate)",
|
||||
"prefers-reduced-motion is respected — all animations skip to final state",
|
||||
"Typecheck passes (npm run typecheck)"
|
||||
],
|
||||
"priority": 11,
|
||||
"passes": true,
|
||||
"notes": "The current ChatWidget already has some mobile handling (bottom-sheet style at <640px). This story changes the breakpoint to 768px (md) and makes it truly full-screen instead of 85vh. Use 100dvh (dynamic viewport height) to account for mobile browser chrome. The floating button visibility can be controlled by combining isOpen state with a CSS media query or a useMediaQuery hook. The <style> block with data-chat-panel attribute is the place to update responsive rules."
|
||||
"passes": false,
|
||||
"notes": "The accessibility buttons are at lines ~661-705 in the JSX. The critical bug is pointerEvents: 'none' on line 688 — change to 'auto'. Also check the containing div at line 658 which also has pointerEvents: 'none' — the buttons inside should override with 'auto'. The constellationNodes.map ordering determines tab order — consider sorting the nodes array for this specific rendering (roles first sorted by startYear desc, then skills grouped by domain). The focus/blur handlers at lines 692-693 already exist and work with the D3 focus ring. The SVG aria-label at line 629 should be updated."
|
||||
},
|
||||
{
|
||||
"id": "US-012",
|
||||
"title": "Welcome message with suggested question chips",
|
||||
"description": "As a visitor opening the chat, I see a friendly welcome message and clickable suggested questions so I know what to ask.",
|
||||
"title": "Responsive behaviour — mobile and tablet fallback",
|
||||
"description": "As a visitor on a smaller screen, I want the constellation graph to display appropriately when the columns stack vertically.",
|
||||
"acceptanceCriteria": [
|
||||
"When chat panel is open and conversation is empty, display welcome text: 'Hey! I'm here to help you learn more about Andy. What would you like to know?'",
|
||||
"Welcome text is styled as an AI message bubble (left-aligned, light background, same styling as assistant messages)",
|
||||
"Below the welcome bubble, show 2-3 clickable pill/chip buttons with suggested questions",
|
||||
"Suggested questions: 'What's his NHS experience?', 'Tell me about his data skills', 'What projects has he built?'",
|
||||
"Chips styled with: teal accent border, rounded-full, font-ui 12-13px, hover state (teal background tint)",
|
||||
"Clicking a chip sends that question as a user message (same codepath as typing + Enter)",
|
||||
"Welcome message and chips always visible when conversation is empty (persist across panel open/close)",
|
||||
"Once any message is sent, the welcome/chips area is replaced by the conversation messages",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
"On mobile/tablet (single-column .pathway-columns layout), the graph renders at a fixed height of 360-400px since no column to match",
|
||||
"The graph simplifies on small screens: role pill labels may use shorter text, skill node default radius decreases slightly (6px)",
|
||||
"Touch interactions work correctly: tap to pin a node, tap elsewhere to unpin",
|
||||
"Graph content is not cropped or overflowing on narrow viewports (min-width handling via boundary clamping)",
|
||||
"The HTML legend from US-008 wraps gracefully on narrow screens",
|
||||
"Timeline axis position adjusts for narrower viewports (closer to left edge)",
|
||||
"Typecheck passes (npm run typecheck)",
|
||||
"Verify in browser at mobile viewport widths (375px, 430px)"
|
||||
],
|
||||
"priority": 12,
|
||||
"passes": true,
|
||||
"notes": "Replace the current empty-state text ('Ask me anything about Andy's experience, skills, or projects.') with the new welcome bubble + chips. The chips should call handleSubmit (or equivalent) with the chip text pre-filled — simplest approach is setInputValue(chipText) then immediately trigger submit. Check that the welcome state reappears if the user hasn't sent a message (messages.length === 0). The suggested questions could live in a const array at the top of ChatWidget for easy future editing."
|
||||
},
|
||||
{
|
||||
"id": "US-013",
|
||||
"title": "Self-host ONNX embedding model",
|
||||
"description": "As a developer, I want the ONNX model files served from the same host as the site to eliminate dependency on Hugging Face CDN.",
|
||||
"acceptanceCriteria": [
|
||||
"Model files for Xenova/all-MiniLM-L6-v2 downloaded and placed in public/models/all-MiniLM-L6-v2/onnx/ (matching HF repo structure)",
|
||||
"Required files present: model_quantized.onnx, tokenizer.json, tokenizer_config.json, config.json, and any other files the pipeline expects",
|
||||
"src/lib/embedding-model.ts updated: configure @xenova/transformers env to use local model path (e.g., env.localModelPath or custom model URL pointing to /models/)",
|
||||
"scripts/generate-embeddings.ts also updated to use the same local model path for consistency",
|
||||
"Model files are NOT in .gitignore — they are committed as static assets",
|
||||
"No network requests to huggingface.co in the browser network tab when semantic search is used",
|
||||
"Semantic search still works correctly in the command palette after the change",
|
||||
"Typecheck passes"
|
||||
],
|
||||
"priority": 13,
|
||||
"passes": true,
|
||||
"notes": "Transformers.js uses env.localModelPath or env.remoteHost to control where models are fetched from. Setting env.localModelPath = '/models/' should make it look for files at /models/Xenova/all-MiniLM-L6-v2/onnx/model_quantized.onnx etc. The Vite public/ directory serves files at the root — so public/models/ becomes /models/ at runtime. For the build script (Node.js), use a file:// path or the local filesystem path instead. Download model files from https://huggingface.co/Xenova/all-MiniLM-L6-v2/tree/main — the quantized ONNX model is ~23MB. Check what files the pipeline actually requests by watching network tab before making this change."
|
||||
},
|
||||
{
|
||||
"id": "US-014",
|
||||
"title": "Migrate production chat from Gemini to OpenRouter",
|
||||
"description": "As a developer, I need to replace the Gemini API integration with OpenRouter so the chat uses z-ai/glm-5.",
|
||||
"acceptanceCriteria": [
|
||||
"Rename src/lib/gemini.ts to src/lib/llm.ts",
|
||||
"Update all imports across the codebase (ChatWidget.tsx, search.ts, any other files importing from gemini.ts)",
|
||||
"Replace Gemini API calls with OpenRouter's OpenAI-compatible API (POST https://openrouter.ai/api/v1/chat/completions)",
|
||||
"Model set to z-ai/glm-5 in request body",
|
||||
"API key read from import.meta.env.VITE_OPEN_ROUTER_API_KEY via Authorization: Bearer header",
|
||||
"Include HTTP-Referer and X-Title headers as recommended by OpenRouter docs",
|
||||
"SSE streaming works using OpenRouter's stream: true option (parse choices[0].delta.content from each SSE data line)",
|
||||
"System prompt sent as first message with role: 'system' (OpenAI chat completions format)",
|
||||
"Message history uses role: 'user' | 'assistant' (no 'model' mapping needed — already correct)",
|
||||
"Export updated constant: LLM_DISPLAY_NAME = 'GLM-5' and update ChatWidget model indicator text",
|
||||
"Rename isGeminiAvailable() to isLLMAvailable() and update all call sites",
|
||||
"Typecheck passes",
|
||||
"Verify in browser: chat opens, sends a message, streams a response correctly"
|
||||
],
|
||||
"priority": 14,
|
||||
"passes": true,
|
||||
"notes": "OpenRouter uses the OpenAI-compatible format. Key differences from Gemini: (1) Auth via Bearer token header, not URL param. (2) System prompt is a message with role:'system', not a separate system_instruction field. (3) Streaming SSE data lines contain {choices:[{delta:{content:'...'}}]}, not candidates[0].content.parts[0].text. (4) The [DONE] sentinel is the same. (5) Add headers: 'HTTP-Referer': window.location.origin, 'X-Title': 'Andy Charlwood Portfolio'. The buildSystemPrompt() function and its content stay the same — only the API transport changes. The buildRequestBody() function needs the most changes."
|
||||
},
|
||||
{
|
||||
"id": "US-015",
|
||||
"title": "Migrate benchmark script to OpenRouter",
|
||||
"description": "As a developer, I need the benchmark harness to use OpenRouter so it tests the same model and prompt path as production.",
|
||||
"acceptanceCriteria": [
|
||||
"scripts/benchmark.ts uses OpenRouter API (POST https://openrouter.ai/api/v1/chat/completions) instead of Gemini",
|
||||
"API key read from process.env.VITE_OPEN_ROUTER_API_KEY (loaded from .env file)",
|
||||
"Request body uses OpenAI chat completions format: messages array with system/user roles",
|
||||
"Model set to z-ai/glm-5 in request body",
|
||||
"Auth via Authorization: Bearer header (not URL param)",
|
||||
"Rate limit retry logic updated for OpenRouter error responses (429 status)",
|
||||
"Response parsing updated: extract choices[0].message.content (non-streaming endpoint)",
|
||||
"Scoring calls also use OpenRouter with same model",
|
||||
"Model name in results output updated to z-ai/glm-5",
|
||||
"npm run benchmark runs end-to-end without errors",
|
||||
"Typecheck passes"
|
||||
],
|
||||
"priority": 15,
|
||||
"passes": true,
|
||||
"notes": "The benchmark uses the non-streaming endpoint (no stream:true needed). OpenRouter non-streaming response format: { choices: [{ message: { content: '...' } }] }. The buildSystemPrompt() function should be imported from the renamed llm.ts (or duplicated if the import path alias doesn't work in tsx scripts — check if @/ alias resolves). Keep the same retry logic structure but update status code handling for OpenRouter. The scoring prompt and question flow are unchanged — only the API transport layer changes."
|
||||
},
|
||||
{
|
||||
"id": "US-016",
|
||||
"title": "Enrich system prompt with full CV context",
|
||||
"description": "As a portfolio visitor, I want the AI to have comprehensive knowledge of Andy's background so it can answer detailed questions accurately.",
|
||||
"acceptanceCriteria": [
|
||||
"buildSystemPrompt() in llm.ts includes full professional profile narrative from CV_v4.md",
|
||||
"Each role includes full achievement bullets, not just the summary text from buildEmbeddingTexts()",
|
||||
"Clear section headers in the prompt: Professional Profile, Career History (per role with dates/employer), Education, Skills, Projects",
|
||||
"NHS employment (May 2022+) explicitly distinguished from private sector (Tesco PLC)",
|
||||
"Clinical specialties listed under the relevant role (rheumatology, ophthalmology, dermatology, etc.)",
|
||||
"Methodology details included (e.g., how the switching algorithm worked, what dm+d integration involved)",
|
||||
"Education includes specific grades, subjects, research topics, classifications",
|
||||
"Leadership training (Mary Seacole Programme) included with year and result",
|
||||
"No invented or extrapolated content — everything sourced from CV_v4.md and data files",
|
||||
"System prompt remains under 8KB total",
|
||||
"Typecheck passes"
|
||||
],
|
||||
"priority": 16,
|
||||
"passes": true,
|
||||
"notes": "The current system prompt uses buildEmbeddingTexts() which gives one paragraph per palette item — good for embeddings but too compressed for detailed Q&A. The enriched prompt should read more like a structured CV with full bullet points. Source content from References/CV_v4.md — read the file to extract all detail. Consider structuring as: ## Profile (personal statement), ## Career History (each role as ### with bullets), ## Education (each qualification), ## Projects (each project with tech and outcomes). Keep it well-structured with markdown headers — LLMs parse this better than flat text."
|
||||
},
|
||||
{
|
||||
"id": "US-017",
|
||||
"title": "Improve system prompt instructions and LLM parameters",
|
||||
"description": "As a portfolio visitor, I want the AI to cite specifics, distinguish between employers, and aggregate across roles when asked.",
|
||||
"acceptanceCriteria": [
|
||||
"Prompt instructs LLM to distinguish NHS employment (ICB, May 2022+) from private sector (Tesco PLC, community pharmacy)",
|
||||
"Prompt instructs LLM to aggregate across roles when asked broad questions (e.g., 'what tools has Andy built?' should list tools from ALL roles)",
|
||||
"Prompt instructs LLM to cite specific metrics, dates, and outcomes when available rather than being vague",
|
||||
"Prompt instructs LLM to answer from the provided context only and say so when information isn't available",
|
||||
"Temperature lowered from 0.7 to 0.3-0.5 for more consistent factual responses",
|
||||
"maxOutputTokens increased from 512 to at least 768 to avoid truncating detailed answers",
|
||||
"The [ITEMS: ...] suffix instruction is preserved and clear",
|
||||
"Typecheck passes"
|
||||
],
|
||||
"priority": 17,
|
||||
"passes": true,
|
||||
"notes": "These are behavioral instructions that go in the Rules section of the system prompt. Keep them concise — LLMs follow shorter, clearer rules better than long paragraphs. Consider: '1. Distinguish NHS employment (May 2022–present, ICB) from private sector (Tesco PLC). 2. When asked about tools/skills across career, aggregate from ALL roles. 3. Cite specific numbers, dates, and outcomes — never say approximate when exact figures are available. 4. If the answer isn't in the context, say so clearly.' Temperature and maxTokens are set in the API request config, not the prompt."
|
||||
},
|
||||
{
|
||||
"id": "US-018",
|
||||
"title": "Enrich embedding texts and regenerate embeddings",
|
||||
"description": "As a portfolio visitor, I want semantic search to surface relevant results even for nuanced queries by having richer embedding texts.",
|
||||
"acceptanceCriteria": [
|
||||
"buildEmbeddingTexts() in search.ts generates richer text per item with full achievement narratives, methodology detail, and clinical specialties",
|
||||
"Role history narratives are included (currently only examination bullets and codedEntries may be used)",
|
||||
"Cross-references included where items relate (e.g., CD monitoring project links to controlled drugs skill)",
|
||||
"Embedding texts remain well-formed natural language (not keyword soup)",
|
||||
"Embeddings regenerated by running npm run generate-embeddings",
|
||||
"Output written to src/data/embeddings.json",
|
||||
"Number of embeddings matches number of palette items (currently 42)",
|
||||
"Typecheck passes"
|
||||
],
|
||||
"priority": 18,
|
||||
"passes": true,
|
||||
"notes": "This combines the PRD's US-005 (enrich texts) and US-006 (regenerate embeddings) since they must happen together. Review what buildEmbeddingTexts() currently produces and identify gaps — the benchmark questions highlight what's missing (e.g., clinical specialties, methodology detail, dm+d context, employer classification). After modifying the texts, run npm run generate-embeddings to regenerate. Verify the embedding count matches before and after."
|
||||
},
|
||||
{
|
||||
"id": "US-019",
|
||||
"title": "Run benchmark and validate accuracy",
|
||||
"description": "As a developer, I want to run the benchmark against the enriched prompt and verify the pass threshold is met.",
|
||||
"acceptanceCriteria": [
|
||||
"Run npm run benchmark successfully against OpenRouter with enriched system prompt",
|
||||
"Score 18/20 or higher (90%+ accuracy) on the 10 benchmark questions",
|
||||
"No question scores 0 (no factual errors)",
|
||||
"Results saved to scripts/benchmark-results/ as a timestamped iteration file",
|
||||
"Additionally test 5 general questions manually or via script: 'Tell me about Andy', 'What does Andy do?', 'How can I contact Andy?', 'What is this website?', 'What are Andy's strongest skills?'",
|
||||
"General questions produce sensible, accurate responses without degradation",
|
||||
"If benchmark fails threshold, identify failing questions and make structural improvements to the prompt (not question-specific hacks), then re-run",
|
||||
"Final passing results saved as evidence"
|
||||
],
|
||||
"priority": 19,
|
||||
"passes": true,
|
||||
"notes": "This is the iterative loop. In a single Ralph iteration, run the benchmark, review results, and if needed make targeted improvements to the system prompt in llm.ts. Focus on structural fixes: if Q7 (clinical specialties) fails, ensure the system prompt lists specialties under the relevant role — this helps ALL specialty questions, not just Q7. If the benchmark takes too many iterations, focus on getting the most impactful improvements in and document remaining gaps. The anti-benchmaxing rules apply: no hardcoded answers, no question-specific prompt clauses."
|
||||
"passes": false,
|
||||
"notes": "The current getHeight() function handles mobile with MOBILE_HEIGHT = 310. After US-002, the containerHeight prop drives the height on desktop. On mobile, detect that containerHeight is not being passed (or is invalid) and fall back to a fixed 360px. The CSS media query in index.css (line ~403) switches .pathway-columns to two-column at a certain breakpoint — below that, the graph is in a single-column stacked layout. The timelineX calculation (line 151) should account for narrow widths — Math.max(80, ...) to keep it accessible. Use the d3-viz skill for implementation."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user