Skip to content

feat: conversational create-resume wizard (/create) - #845

Open
srbhr wants to merge 16 commits into
mainfrom
feat/create-resume-wizard
Open

feat: conversational create-resume wizard (/create)#845
srbhr wants to merge 16 commits into
mainfrom
feat/create-resume-wizard

Conversation

@srbhr

@srbhr srbhr commented Jun 3, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a conversational Create Resume wizard at /create — the missing on-ramp for users who don't have a resume to upload. A guided chat interviews the user, the LLM authors polished resume content from their plain answers, the resume builds live beside the chat, and the result is saved as the master resume, then opened in the Builder.

This closes the "I have nothing → a tailored, downloadable resume" gap (previously the only way in was uploading a PDF/DOCX).

The flow

greet → name → "what do you do" → pick a section [Work] [Education] [Projects] [Skills] → you talk, the AI drafts that section (it appears in chat and the live preview) → contact details → AI summary (accept / rewrite) → Save → opens in the Builder as your master resume.

  • Minimum to finish: a name + at least one content section.
  • Personal/contact fields are typed, never AI-authored (they're facts).
  • The AI shapes and lightly polishes only what the user says — anti-fabrication rules forbid inventing employers, dates, metrics, tools, or technologies.

What's included

Backend (stateless, mirrors the existing enrichment analyze/enhance pattern):

  • POST /api/v1/resumes/draft-section — author one validated ResumeData fragment per section (LLM)
  • POST /api/v1/resumes — persist the assembled resume (becomes master iff none exists, via create_resume_atomic_master)
  • services/creation.py, prompts/creation.py (anti-fabrication), schemas/creation.py, routers/creation.py
  • Reuses the existing prompt-injection sanitizer on all user-supplied text (answers, name, role, and the resume context fed to the summary call)

Frontend (Swiss International Style):

  • /create route + creation-wizard.tsx orchestrator (responsive split: chat ↔ live preview; preview becomes a drawer on mobile)
  • wizard-script.ts pure state machine (assemble / append / finish gate / summarize), 5 presentational components, lib/api/create.ts
  • Live preview reuses the Builder/print renderer; running resume autosaves to localStorage
  • "Create from scratch" entry on the dashboard in both the no-master and master-exists states
  • create.* i18n across all 5 locales (en/es/zh/ja/pt-BR)

Tests & verification

  • Backend: 463 passed — new schema, prompt, service (mocked-LLM per section + injection sanitization + thin-answer non-fabrication), and integration tests (draft-section fragment, LLM-not-configured guard, master-iff-none invariant)
  • Frontend: 165 passed — wizard state machine (branching, skip, finish gate, merge/dedupe, assemble), API client
  • next build green — /create is a registered route, locale parity holds, tsc clean

Design docs

Spec + bite-sized TDD plan committed under docs/superpowers/ (2026-06-03-create-resume-wizard-*). Feature doc at docs/agent/features/create-resume.md; context files updated (root + backend + frontend CLAUDE.md, backend-guide/architecture endpoint tables, front-end-apis contract).

Notes

  • Repo-wide npm run lint (eslint .) is red on pre-existing prettier/prettier violations in unrelated files (components/resume/resume-*.tsx) — present on main, not introduced here; next build does not gate on ESLint. Every file in this PR is eslint + prettier clean.
  • Recommend a manual end-to-end smoke with the server running and an LLM key configured: visit /create, talk through a section, confirm it lands in the Builder prefilled.

Summary by cubic

Adds a conversational “Create Resume” wizard at /create that interviews the user, drafts clean sections from their answers with a live preview, and saves the result as the master resume for editing in the Builder. This gives new users a from‑scratch path without uploading a file.

  • New Features

    • Frontend: /create page with chat + live preview, section picker, typed contact fields, autosave to localStorage, summary accept/regenerate, opens in Builder on save.
    • Backend: POST /api/v1/resumes/draft-section (authors one validated fragment per section) and POST /api/v1/resumes (persists; master if none). Stateless service with anti‑fabrication rules and prompt‑injection sanitization.
    • Dashboard: “Create from scratch” card shown whether or not a master resume exists.
    • i18n: Wizard strings in en, es, zh, ja, pt‑BR.
    • Docs: Feature spec, plan, and API/architecture docs updated.
  • Bug Fixes

    • Sanitized resume_context before the summary prompt.
    • Skills drafts now merge and dedupe; chat confirms the added fragment.

Written for commit 85c7b6a. Summary will update on new commits.

Review in cubic

srbhr added 16 commits June 3, 2026 23:32
Approved brainstorming output for the /create Q&A wizard: guided-section
engine with AI authoring, live responsive preview side panel, save-as-master
then open in Builder. Stateless backend draft-section endpoints + a
create-from-JSON endpoint; frontend-orchestrated fixed script. Anti-fabrication
guardrails, deterministic test plan, and file map included.
…ext files

New feature doc (docs/agent/features/create-resume.md) plus context updates for
the /create route across .claude/CLAUDE.md, apps/{backend,frontend}/CLAUDE.md,
backend-guide/backend-architecture endpoint tables, and the front-end-apis
contract (draft-section + create-from-wizard).
resume_context carries user-entered name/title/contact and prior drafted text;
it was embedded into the summary LLM call without the injection sanitizer that
already guards answers/name/role. Sanitize the serialized JSON too.
- appendDraft('skills') now merges and dedupes (case-insensitive) instead of
  overwriting, so re-picking Skills accumulates rather than replacing.
- After a section drafts, the chat now confirms the drafted entry
  ('Added: <summary>...') via a new summarizeFragment helper + create.added
  string (5 locales), matching the spec's 'appears in chat and preview'.
…oard too

The /create entry was only in the no-master branch. Per the spec it should also
be reachable when a master exists (to make a normal non-master resume); add a
grid card gated on masterResumeId.
Copilot AI review requested due to automatic review settings June 3, 2026 19:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new “Create Resume” conversational wizard at /create to generate a structured resume from guided Q&A, draft section content via LLM (with anti-fabrication + injection sanitization), show a live preview, and persist the final resume (master iff none exists) before opening it in the Builder.

Changes:

  • Backend: added stateless creation endpoints (POST /resumes/draft-section, POST /resumes) plus creation schemas/service/prompts and tests.
  • Frontend: added /create route with wizard orchestrator, pure state helpers, API client, live preview, and dashboard entry points.
  • Docs/i18n: updated backend/frontend architecture/API docs and added create.* translations across 5 locales.

Reviewed changes

Copilot reviewed 39 out of 39 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
docs/superpowers/specs/2026-06-03-create-resume-wizard-design.md Design spec for the create wizard flow and constraints
docs/superpowers/plans/2026-06-03-create-resume-wizard.md Implementation plan detailing backend/frontend workstreams
docs/agent/features/create-resume.md Feature documentation for /create and related endpoints
docs/agent/architecture/backend-guide.md Backend endpoint quick reference updated with wizard endpoints
docs/agent/architecture/backend-architecture.md Backend endpoint table updated with wizard endpoints
docs/agent/apis/front-end-apis.md Frontend API contract updated with lib/api/create.ts
apps/frontend/tests/wizard-script.test.ts Unit tests for wizard pure state logic
apps/frontend/tests/api-create.test.ts Unit tests for create-wizard API client
apps/frontend/messages/en.json Added create.* i18n strings
apps/frontend/messages/es.json Added create.* i18n strings
apps/frontend/messages/zh.json Added create.* i18n strings
apps/frontend/messages/ja.json Added create.* i18n strings
apps/frontend/messages/pt-BR.json Added create.* i18n strings
apps/frontend/lib/api/resume.ts Exported ProcessedResume type for reuse
apps/frontend/lib/api/create.ts New typed API client for wizard draft + persist calls
apps/frontend/components/create/wizard-script.ts Pure wizard data model + append/assemble helpers
apps/frontend/components/create/wizard-preview.tsx Live preview wrapper reusing existing resume renderer
apps/frontend/components/create/section-picker.tsx Section picker UI component
apps/frontend/components/create/creation-wizard.tsx Wizard orchestrator (chat + preview + persistence)
apps/frontend/components/create/contact-fields.tsx Typed contact details form step
apps/frontend/components/create/chat-message.tsx Chat bubble component
apps/frontend/components/create/chat-input.tsx Chat input component with Enter/Shift+Enter handling
apps/frontend/CLAUDE.md Documented /create route and new API client
apps/frontend/app/(default)/dashboard/page.tsx Added “Create from scratch” entry points (no-master + master states)
apps/frontend/app/(default)/create/page.tsx Registered /create route wrapper
apps/backend/tests/unit/test_creation_schemas.py Unit tests for creation schemas
apps/backend/tests/unit/test_creation_prompts.py Unit tests ensuring prompts format correctly
apps/backend/tests/service/test_creation.py Service tests for section drafting (mocked LLM + sanitization)
apps/backend/tests/integration/test_create_api.py Integration tests for new wizard endpoints
apps/backend/tests/conftest.py Ensured isolated DB fixture patches the new router module
apps/backend/CLAUDE.md Documented the create-wizard router/service/prompts
apps/backend/app/services/creation.py New service to draft validated ResumeData fragments per section
apps/backend/app/schemas/creation.py New request/response schemas for wizard endpoints
apps/backend/app/schemas/init.py Exported creation schemas
apps/backend/app/routers/creation.py New router for draft-section + persist endpoints
apps/backend/app/routers/init.py Exported creation_router
apps/backend/app/prompts/creation.py New anti-fabrication prompts per section
apps/backend/app/main.py Mounted the new creation router under /api/v1
.claude/CLAUDE.md Added create-resume wizard to feature documentation index

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +33 to +36
if not _llm_configured():
raise HTTPException(
status_code=400, detail="LLM not configured. Please set an API key in Settings."
)
Comment on lines +54 to +60
async def create_resume_from_wizard(request: WizardResumeCreate) -> ResumeUploadResponse:
"""Persist the assembled resume; becomes master iff none exists."""
try:
normalized = normalize_resume_data(
ResumeData.model_validate(request.processed_data).model_dump()
)
created = await db.create_resume_atomic_master(
Comment on lines +2 to +6
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useTranslations } from '@/lib/i18n';
import { draftSection, createResumeFromWizard, type SectionKind } from '@/lib/api/create';
import {
Comment on lines +30 to +38
export function CreationWizard() {
const { t } = useTranslations();
const router = useRouter();
const [data, setData] = useState<WizardData>(emptyWizardData);
const [phase, setPhase] = useState<Phase>('name');
const [section, setSection] = useState<Pickable | null>(null);
const [turns, setTurns] = useState<Turn[]>([]);
const [busy, setBusy] = useState(false);
const [showPreview, setShowPreview] = useState(false);
"""Turn a user's plain answers into one validated ResumeData fragment."""
if not _llm_configured():
raise HTTPException(
status_code=400, detail="LLM not configured. Please set an API key in Settings."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Status code 400 for unconfigured LLM (design spec says 503)

The spec states "503 with a clear, generic message if not configured." While 400 works in practice (the frontend gates before reaching this), 503 (Service Unavailable) is semantically more accurate for a missing dependency. Consider aligning with the spec for consistency.

"""Persist the assembled resume; becomes master iff none exists."""
try:
normalized = normalize_resume_data(
ResumeData.model_validate(request.processed_data).model_dump()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: ResumeData.model_validate ValidationError returns HTTP 500 instead of 400

If request.processed_data contains invalid resume data (missing required fields, wrong types), Pydantic raises ValidationError. This is caught by the blanket except Exception and re-raised as HTTP 500 ("Failed to save your resume").

A 500 suggests a server bug, but the error is actually client-side bad input. Should catch ValidationError separately and return HTTP 400 with a generic "Invalid resume data" message (per the repo's "log details, return generic" pattern).

processing_status="ready",
)
if request.title:
await db.update_resume(created["resume_id"], {"title": request.title})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Non-atomic title update after resume creation

update_resume is called after create_resume_atomic_master has already succeeded. If update_resume fails (DB error, connection lost), the resume is persisted without a title but the client receives a 500 error. The user sees a failure message, may retry, and creates a duplicate resume.

Consider passing the title into create_resume_atomic_master directly, or wrapping both operations in a single transaction.

}
}}
/>
<Button onClick={send} disabled={disabled}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Button missing type="button"

The Send button has no explicit type attribute. If this component is ever wrapped inside a <form>, the button will default to type="submit" and trigger form submission. Add type="button" to prevent accidental submissions.

const next = { ...data, contact: c };
setData(next);
setPhase('summary');
void generateSummary(next);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Race condition: double-click on ContactFieldsForm submit triggers duplicate summary LLM calls

ContactFieldsForm's submit button has no disabled guard. If the user double-clicks "Continue", submitContact fires twice, each calling void generateSummary(next) in parallel. Both make unnecessary LLM API calls.

The first setBusy(true) blocks the UI briefly, but the second fire-and-forget call still executes before busy state takes effect. Add a disabled prop to ContactFieldsForm or guard with a ref to prevent duplicate calls.

@kilo-code-bot

kilo-code-bot Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 5 Issues Found (1 CRITICAL, 2 WARNING, 2 SUGGESTION) | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 2
SUGGESTION 2
Issue Details (click to expand)

CRITICAL

File Line Issue
apps/frontend/components/create/creation-wizard.tsx 143 Race condition: double-click on ContactFieldsForm submit triggers duplicate summary LLM calls

WARNING

File Line Issue
apps/backend/app/routers/creation.py 67 Non-atomic title update after resume creation - resume persists without title on failure, client gets 500, user may retry and duplicate
apps/backend/app/routers/creation.py 58 ResumeData.model_validate ValidationError returns HTTP 500 instead of 400 - should catch ValidationError separately

SUGGESTION

File Line Issue
apps/frontend/components/create/chat-input.tsx 40 Button missing type="button" - defaults to submit if wrapped in form
apps/backend/app/routers/creation.py 35 Status code 400 for unconfigured LLM (design spec says 503)
Other Observations (not in diff)
File Line Issue
apps/backend/app/routers/creation.py 73-79 created variable used outside try block - safe today but fragile to future changes
apps/frontend/components/create/wizard-preview.tsx 9 as ResumeData cast bypasses type safety - ProcessedResume allows null, ResumeData expects undefined
apps/backend/app/services/creation.py 52 complete_json retries=2 could mask LLM failures - consider logging raw LLM response
apps/frontend/components/create/creation-wizard.tsx 164 h-[100dvh] may cause layout shifts on iOS Safari
Files Reviewed (38 files)
  • apps/backend/app/routers/creation.py - 3 issues
  • apps/backend/app/routers/__init__.py - clean
  • apps/backend/app/main.py - clean
  • apps/backend/app/prompts/creation.py - clean
  • apps/backend/app/schemas/creation.py - clean
  • apps/backend/app/schemas/__init__.py - clean
  • apps/backend/app/services/creation.py - 1 observation
  • apps/backend/tests/conftest.py - clean
  • apps/backend/tests/integration/test_create_api.py - clean
  • apps/backend/tests/service/test_creation.py - clean
  • apps/backend/tests/unit/test_creation_prompts.py - clean
  • apps/backend/tests/unit/test_creation_schemas.py - clean
  • apps/frontend/app/(default)/create/page.tsx - clean
  • apps/frontend/app/(default)/dashboard/page.tsx - clean
  • apps/frontend/components/create/chat-input.tsx - 1 issue
  • apps/frontend/components/create/chat-message.tsx - clean
  • apps/frontend/components/create/contact-fields.tsx - clean
  • apps/frontend/components/create/creation-wizard.tsx - 1 critical + 1 observation
  • apps/frontend/components/create/section-picker.tsx - clean
  • apps/frontend/components/create/wizard-preview.tsx - 1 observation
  • apps/frontend/components/create/wizard-script.ts - clean
  • apps/frontend/lib/api/create.ts - clean
  • apps/frontend/lib/api/resume.ts - clean
  • apps/frontend/messages/en.json - clean
  • apps/frontend/messages/es.json - clean
  • apps/frontend/messages/ja.json - clean
  • apps/frontend/messages/pt-BR.json - clean
  • apps/frontend/messages/zh.json - clean
  • apps/frontend/tests/api-create.test.ts - clean
  • apps/frontend/tests/wizard-script.test.ts - clean
  • .claude/CLAUDE.md - clean
  • apps/backend/CLAUDE.md - clean
  • apps/frontend/CLAUDE.md - clean
  • docs/agent/apis/front-end-apis.md - clean
  • docs/agent/architecture/backend-architecture.md - clean
  • docs/agent/architecture/backend-guide.md - clean
  • docs/agent/features/create-resume.md - clean
  • docs/superpowers/plans/2026-06-03-create-resume-wizard.md - clean

Reviewed by qwen3.6-plus · 1,266,848 tokens

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7 issues found across 39 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/backend/app/services/creation.py">

<violation number="1" location="apps/backend/app/services/creation.py:86">
P2: Summary section silently coerces non-string LLM output via str(), risking data corruption. Unlike work/education/project/skills which validate against Pydantic models, the summary branch falls back to `str(summary)` when the LLM returns a non-string value (e.g., a dict or list). This produces a stringified representation like `"{'text': '...'}"` instead of rejecting the malformed output or extracting the intended text.</violation>
</file>

<file name="apps/frontend/components/create/contact-fields.tsx">

<violation number="1" location="apps/frontend/components/create/contact-fields.tsx:28">
P2: Missing `<form>` element wrapper — pressing Enter in any input field won't trigger submission, and there is no form landmark for screen readers. Users filling out the contact form must reach for the mouse/tab to the button instead of submitting via keyboard.</violation>
</file>

<file name="apps/frontend/app/(default)/dashboard/page.tsx">

<violation number="1" location="apps/frontend/app/(default)/dashboard/page.tsx:381">
P3: "Create from scratch" card is identically duplicated across both the "no master" branch and the "master exists" branch, creating a maintainability issue where changes to one must be manually synced to the other.</violation>

<violation number="2" location="apps/frontend/app/(default)/dashboard/page.tsx:381">
P3: Filler count calculation doesn't account for the new "Create from scratch" card when a master resume exists, causing an off-by-one in the grid layout's filler cards.</violation>
</file>

<file name="apps/frontend/components/create/creation-wizard.tsx">

<violation number="1" location="apps/frontend/components/create/creation-wizard.tsx:46">
P2: Restored draft data is cast to `WizardData` without shape validation, so malformed/legacy localStorage can crash the wizard on `.length` access.</violation>
</file>

<file name="apps/backend/app/routers/creation.py">

<violation number="1" location="apps/backend/app/routers/creation.py:57">
P1: Invalid `processed_data` is returned as HTTP 500 because `ValidationError` is swallowed by a broad exception handler in the create endpoint.</violation>

<violation number="2" location="apps/backend/app/routers/creation.py:67">
P2: Title update failure turns a successful resume creation into HTTP 500, causing partial success and retry duplication risk.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

async def create_resume_from_wizard(request: WizardResumeCreate) -> ResumeUploadResponse:
"""Persist the assembled resume; becomes master iff none exists."""
try:
normalized = normalize_resume_data(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Invalid processed_data is returned as HTTP 500 because ValidationError is swallowed by a broad exception handler in the create endpoint.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/app/routers/creation.py, line 57:

<comment>Invalid `processed_data` is returned as HTTP 500 because `ValidationError` is swallowed by a broad exception handler in the create endpoint.</comment>

<file context>
@@ -0,0 +1,79 @@
+async def create_resume_from_wizard(request: WizardResumeCreate) -> ResumeUploadResponse:
+    """Persist the assembled resume; becomes master iff none exists."""
+    try:
+        normalized = normalize_resume_data(
+            ResumeData.model_validate(request.processed_data).model_dump()
+        )
</file context>

return {"technicalSkills": validated.technicalSkills}
# summary
summary = raw.get("summary", "")
return {"summary": summary if isinstance(summary, str) else str(summary)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Summary section silently coerces non-string LLM output via str(), risking data corruption. Unlike work/education/project/skills which validate against Pydantic models, the summary branch falls back to str(summary) when the LLM returns a non-string value (e.g., a dict or list). This produces a stringified representation like "{'text': '...'}" instead of rejecting the malformed output or extracting the intended text.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/app/services/creation.py, line 86:

<comment>Summary section silently coerces non-string LLM output via str(), risking data corruption. Unlike work/education/project/skills which validate against Pydantic models, the summary branch falls back to `str(summary)` when the LLM returns a non-string value (e.g., a dict or list). This produces a stringified representation like `"{'text': '...'}"` instead of rejecting the malformed output or extracting the intended text.</comment>

<file context>
@@ -0,0 +1,86 @@
+        return {"technicalSkills": validated.technicalSkills}
+    # summary
+    summary = raw.get("summary", "")
+    return {"summary": summary if isinstance(summary, str) else str(summary)}
</file context>

const { t } = useTranslations();
const [values, setValues] = useState<ContactFields>(initial);
return (
<div className="border border-black bg-canvas p-4 shadow-sw-default">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Missing <form> element wrapper — pressing Enter in any input field won't trigger submission, and there is no form landmark for screen readers. Users filling out the contact form must reach for the mouse/tab to the button instead of submitting via keyboard.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/frontend/components/create/contact-fields.tsx, line 28:

<comment>Missing `<form>` element wrapper — pressing Enter in any input field won't trigger submission, and there is no form landmark for screen readers. Users filling out the contact form must reach for the mouse/tab to the button instead of submitting via keyboard.</comment>

<file context>
@@ -0,0 +1,47 @@
+  const { t } = useTranslations();
+  const [values, setValues] = useState<ContactFields>(initial);
+  return (
+    <div className="border border-black bg-canvas p-4 shadow-sw-default">
+      <p className="mb-3 font-serif text-lg">{t('create.contactTitle')}</p>
+      <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
</file context>

const saved = localStorage.getItem(DRAFT_KEY);
if (saved) {
try {
const parsed = JSON.parse(saved) as WizardData;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Restored draft data is cast to WizardData without shape validation, so malformed/legacy localStorage can crash the wizard on .length access.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/frontend/components/create/creation-wizard.tsx, line 46:

<comment>Restored draft data is cast to `WizardData` without shape validation, so malformed/legacy localStorage can crash the wizard on `.length` access.</comment>

<file context>
@@ -0,0 +1,228 @@
+    const saved = localStorage.getItem(DRAFT_KEY);
+    if (saved) {
+      try {
+        const parsed = JSON.parse(saved) as WizardData;
+        if (parsed && parsed.name) {
+          setData(parsed);
</file context>

processing_status="ready",
)
if request.title:
await db.update_resume(created["resume_id"], {"title": request.title})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Title update failure turns a successful resume creation into HTTP 500, causing partial success and retry duplication risk.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/app/routers/creation.py, line 67:

<comment>Title update failure turns a successful resume creation into HTTP 500, causing partial success and retry duplication risk.</comment>

<file context>
@@ -0,0 +1,79 @@
+            processing_status="ready",
+        )
+        if request.title:
+            await db.update_resume(created["resume_id"], {"title": request.title})
+    except Exception as e:
+        logger.error("create_resume_from_wizard failed: %s", e)
</file context>

@@ -17,6 +17,7 @@ import RefreshCw from 'lucide-react/dist/esm/icons/refresh-cw';
import Plus from 'lucide-react/dist/esm/icons/plus';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: "Create from scratch" card is identically duplicated across both the "no master" branch and the "master exists" branch, creating a maintainability issue where changes to one must be manually synced to the other.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/frontend/app/(default)/dashboard/page.tsx, line 381:

<comment>"Create from scratch" card is identically duplicated across both the "no master" branch and the "master exists" branch, creating a maintainability issue where changes to one must be manually synced to the other.</comment>

<file context>
@@ -350,32 +351,53 @@ export default function DashboardPage() {
+                  </Card>
+                }
+              />
+              <Link href="/create" className="block h-full">
                 <Card
                   variant="interactive"
</file context>

</Card>
}
/>
<Link href="/create" className="block h-full">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Filler count calculation doesn't account for the new "Create from scratch" card when a master resume exists, causing an off-by-one in the grid layout's filler cards.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/frontend/app/(default)/dashboard/page.tsx, line 381:

<comment>Filler count calculation doesn't account for the new "Create from scratch" card when a master resume exists, causing an off-by-one in the grid layout's filler cards.</comment>

<file context>
@@ -350,32 +351,53 @@ export default function DashboardPage() {
+                  </Card>
+                }
+              />
+              <Link href="/create" className="block h-full">
                 <Card
                   variant="interactive"
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants