mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-19 21:11:45 +10:00
docs: remove agent docs
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
# Structured Style Rules for React PDF
|
||||
|
||||
Reactive Resume no longer renders final PDFs through browser HTML/CSS, so end-user custom CSS would create a misleading contract: React PDF accepts style objects on renderer components, not arbitrary browser selectors. Resume appearance customization is therefore modeled as structured Style Rules in resume metadata, targeting semantic section and rich-text slots that the PDF package translates into safe React PDF styles.
|
||||
|
||||
This preserves user-controlled section styling while keeping PDF rendering portable across preview, export, public resume views, templates, and server generation. Raw CSS, raw React PDF style objects, section-item targeting, and direct PDF-preview selection are intentionally out of v1; they can be revisited only if the product needs that power enough to accept the additional validation and layout-breakage risk.
|
||||
@@ -1,241 +0,0 @@
|
||||
# Monorepo Architecture Reorg Handoff
|
||||
|
||||
This handoff is the coordination entrypoint for the Reactive Resume architecture reorganization. It intentionally references the implementation plan instead of duplicating it.
|
||||
|
||||
## Primary Plan
|
||||
|
||||
- Plan: `docs/superpowers/plans/2026-05-14-monorepo-architecture-reorg.md`
|
||||
- Worklog: `docs/superpowers/worklogs/2026-05-14-monorepo-architecture-reorg.md`
|
||||
- Branch at start: `feat/explore-hono-orpc-migration`
|
||||
|
||||
## Current Operating Rules
|
||||
|
||||
- Preserve existing unrelated local changes:
|
||||
- `apps/server/package.json`
|
||||
- `package.json`
|
||||
- `packages/api/package.json`
|
||||
- `packages/env/package.json`
|
||||
- `packages/utils/package.json`
|
||||
- `pnpm-lock.yaml`
|
||||
- Do not use `git reset --hard` or destructive checkout commands.
|
||||
- Use green internal commits if committing is requested/appropriate.
|
||||
- Use root `AGENTS.md` as the final normative architecture source of truth.
|
||||
- Do not create local package READMEs for architecture rules unless a package has unavoidable operational constraints.
|
||||
|
||||
## Suggested Skills for Future Agents
|
||||
|
||||
- `subagent-driven-development` for executing one task slice with review gates.
|
||||
- `executing-plans` for sequential execution from the plan.
|
||||
- `documentation-writer` for `AGENTS.md`, ADR, and public architecture docs.
|
||||
- `handoff` when pausing or delegating a task outside Codex.
|
||||
- `turborepo` when editing `turbo.json`, package tags, or task graph rules.
|
||||
- `context7-mcp` if checking current docs for library/framework behavior.
|
||||
|
||||
## Delegation Guidance
|
||||
|
||||
External agents should be handed exactly one task from the plan plus this handoff path. They should report:
|
||||
|
||||
- Status: `DONE`, `DONE_WITH_CONCERNS`, `BLOCKED`, or `NEEDS_CONTEXT`
|
||||
- Files changed
|
||||
- Tests/commands run
|
||||
- Any plan deviations
|
||||
- Follow-up tasks needed
|
||||
|
||||
## Task Ownership Status
|
||||
|
||||
- Task 0 Coordination Artifacts: done
|
||||
- Task 1 Pure Resume Domain Package: done
|
||||
- Task 2 DOCX Package: done
|
||||
- Task 3 PDF Package Browser/Server Generation Surface: done
|
||||
- Task 4 MCP Package and Shared Tool Contracts: done
|
||||
- Task 5 API Feature Reorganization: done
|
||||
- Task 6 Server Adapter Reorganization: done
|
||||
- Task 7 Domain-First Web Reorganization: done
|
||||
- Task 8 Dialog Registry Rework: done
|
||||
- Task 9 Boundary Enforcement: done
|
||||
- Task 10 Documentation and Source of Truth: done
|
||||
- Task 11 Final Validation and Handoff: done
|
||||
|
||||
## Architecture Decisions Already Locked
|
||||
|
||||
- `@reactive-resume/schema` is Zod/types only.
|
||||
- `@reactive-resume/resume` owns pure resume-domain behavior.
|
||||
- `@reactive-resume/ai` owns model contracts, not DB-backed agent runtime.
|
||||
- Agent runtime remains in `packages/api/features/agent`.
|
||||
- MCP uses an in-process oRPC `RouterClient`.
|
||||
- MCP tools move to canonical unprefixed snake_case names, no old aliases.
|
||||
- `@reactive-resume/pdf` owns PDF generation helpers but not PDF.js viewer UI.
|
||||
- `apps/web` uses domain-first features and generic-only `src/components`.
|
||||
- `packages/db` remains centralized.
|
||||
|
||||
## Latest Task 1 Result
|
||||
|
||||
- Created `@reactive-resume/resume` with source-consumed exports:
|
||||
- `@reactive-resume/resume/patch`
|
||||
- `@reactive-resume/resume/icons`
|
||||
- Moved JSON Patch behavior/tests and social network icon mapping/tests out of `@reactive-resume/utils`.
|
||||
- Updated consumers in `packages/ai`, `packages/api`, `packages/db`, `packages/import`, and `apps/web`.
|
||||
- Removed old utils exports for `./resume/patch` and `./network-icons`.
|
||||
- Removed `fast-json-patch` from `@reactive-resume/utils` and `@reactive-resume/ai`; it now belongs to `@reactive-resume/resume`.
|
||||
- Task 2 later removed `@reactive-resume/schema` from `@reactive-resume/utils` after moving DOCX code into `@reactive-resume/docx`.
|
||||
|
||||
## Latest Task 2 Result
|
||||
|
||||
- Created `@reactive-resume/docx` with source-consumed root export `@reactive-resume/docx`.
|
||||
- Moved the DOCX builder, HTML conversion, link utilities, section renderers, and colocated tests from `packages/utils/src/resume/docx` to `packages/docx/src`.
|
||||
- Updated web DOCX export callers and the export-section test mock to import from `@reactive-resume/docx`.
|
||||
- Removed the old `@reactive-resume/utils/resume/docx` export and removed `docx` plus `@reactive-resume/schema` from `@reactive-resume/utils`.
|
||||
- Validation passed for `@reactive-resume/docx` tests/typecheck, `@reactive-resume/utils` typecheck, `web` typecheck, focused web export test command, and focused Biome check.
|
||||
|
||||
## Latest Task 3 Notes
|
||||
|
||||
- Added `@reactive-resume/pdf/browser` and `@reactive-resume/pdf/server` exports.
|
||||
- `createResumePdfBlob({ data, template, resolveSectionTitle })` delegates to `pdf(<ResumeDocument ... />).toBlob()`.
|
||||
- `createResumePdfFile({ data, filename, template, resolveSectionTitle })` delegates to `renderToBuffer(<ResumeDocument ... />)` and preserves the existing `File` response body shape.
|
||||
- Lingui locale loading stays in the web-local wrappers under `apps/web/src/features/resume/export`; those wrappers resolve section titles and pass `resolveSectionTitle` into the package helpers.
|
||||
- The PDF.js viewer/canvas components remain in web. `apps/web/src/features/resume/preview/preview.browser.tsx` now calls the web-local blob wrapper instead of importing `@react-pdf/renderer` directly.
|
||||
- The authenticated PDF export path now reaches `@reactive-resume/pdf/server` through `packages/api/src/features/resume/export.ts`; `apps/server` consumes the explicit API feature export instead of owning PDF rendering logic directly.
|
||||
- Validation passed for `@reactive-resume/pdf` tests/typecheck, `web` typecheck, `server` typecheck, focused web PDF export/viewer tests, and focused Biome check.
|
||||
|
||||
## Latest Task 4 Notes
|
||||
|
||||
- Created `@reactive-resume/mcp` with source-consumed exports for the compact public surface plus direct subpaths for server card, tool names, tools, prompts, and resources.
|
||||
- Moved MCP tools, prompts, resources, server-card generation, tool annotations, and colocated tests from `apps/web/src/routes/mcp/-helpers` to `packages/mcp/src`.
|
||||
- `apps/server/src/mcp/handler.ts` and `apps/server/src/openapi/metadata.ts` now import from `@reactive-resume/mcp`; server-side MCP execution still uses the injected in-process oRPC `RouterClient`.
|
||||
- Canonical MCP tool names are unprefixed snake_case. Key renames: `read_resume` replaces the old get-resume tool name, and `apply_resume_patch` replaces the old patch tool name. No `reactive_resume_*` aliases remain.
|
||||
- Added `@reactive-resume/ai/tools/resume-tool-contracts` and reused its JSON Patch operations schema from MCP while keeping MCP-specific `id` context in the MCP input schema.
|
||||
- Validation passed for `@reactive-resume/mcp` tests/typecheck, `server` typecheck, `@reactive-resume/ai` typecheck, focused Biome check, and the app-to-app import scan.
|
||||
|
||||
## Latest Task 5 Notes
|
||||
|
||||
- `packages/api/src` is now organized by feature/capability under `packages/api/src/features`.
|
||||
- The root router still exports the same top-level API contract from `packages/api/src/routers/index.ts`, but it imports feature routers from `features/*/router`.
|
||||
- Agent modules now live under `features/agent`, with separate procedure modules for threads, messages, attachments, and actions; run-state lives in `runs.ts`, tool construction in `tools.ts`, and the remaining shared orchestration stays in `service.ts`.
|
||||
- Resume modules now live under `features/resume`, with capability procedure modules for CRUD, tags, statistics, analysis, events, sharing, and export. Access helpers and resume update events are feature-owned.
|
||||
- The authenticated PDF download procedure moved to `packages/api/src/features/resume/export.ts` and calls `@reactive-resume/pdf/server`.
|
||||
- `@reactive-resume/api` no longer exports `./services/*` or `./helpers/*`; explicit exports now cover routers, context, flags type, resume runtime/export, and storage runtime.
|
||||
- `apps/server` imports storage and the PDF procedure from explicit API feature exports. `apps/web` API imports remain type-only.
|
||||
- Follow-up risk: `features/agent/service.ts` and `features/resume/service.ts` are still large DB-backed facades. They are feature-owned now, but further splitting should be handled as behavior-preserving follow-up work with targeted tests around run lifecycle, patch transactions, notifications, and storage cleanup.
|
||||
- Validation passed for API tests/typecheck, server typecheck, web typecheck, MCP typecheck, focused Biome, and old service/helper import/export scans.
|
||||
|
||||
## Latest Task 6 Notes
|
||||
|
||||
- `apps/server/src` now reads as a runtime adapter app:
|
||||
- `http`: route composition, auth/health HTTP handlers, common header/cookie helpers
|
||||
- `rpc`: oRPC fetch handler and request-locale extraction
|
||||
- `mcp`: MCP auth, per-request server setup, and streamable HTTP transport handler
|
||||
- `openapi`: OpenAPI handler plus OAuth/OpenID/MCP well-known metadata
|
||||
- `static`: upload serving, `/schema.json`, and `apps/web/dist` static/SPA fallback serving
|
||||
- `startup`: database migrations and local-storage path checks
|
||||
- `apps/server/src/index.ts` now only re-exports `createApp`, runs startup checks, computes the port, and starts the Hono server.
|
||||
- The route order and public paths from the previous `index.ts` were preserved, including `/api/rpc`, `/api/openapi`, `/api/auth/*`, `/api/health`, `/uploads/*`, `/schema.json`, `/auth/oauth`, `/mcp`, `/.well-known/*`, and the web-dist fallback.
|
||||
- Server-side API imports remain on explicit runtime exports only; no `@reactive-resume/api/services/*` or `apps/web/src` imports were introduced.
|
||||
- Validation passed for server test/typecheck during implementation. Final Task 6 validation commands should still be listed in the worklog/final response after the executing agent's last run.
|
||||
|
||||
## Latest Task 7 Slice 1 Notes
|
||||
|
||||
- Resume-owned web code now starts under `apps/web/src/features/resume`:
|
||||
- `builder/draft.ts` owns the builder resume draft store/hooks.
|
||||
- `preview/*` owns the builder preview shell, PDF.js canvas renderer, shared preview helpers/tests, and dashboard thumbnail PDF rendering helpers.
|
||||
- `export/pdf-document*.tsx` owns web-local PDF document/blob/file wrappers around `@reactive-resume/pdf`.
|
||||
- `public/*` owns the public resume view, public PDF.js viewer, CSS, and tests.
|
||||
- The old generic resume component folder and public-route private component folder were removed after their files moved.
|
||||
- Direct `pdfjs-dist` imports are expected only under `apps/web/src/features/resume`; do not move them into `packages/pdf`.
|
||||
- The public resume route remains responsible for loader/error/head composition, keeps `ssr: "data-only"`, and lazy-loads `@/features/resume/public/public-resume`.
|
||||
- The builder preview route keeps `ssr: false` and lazy-loads the preview page composition.
|
||||
- Remaining Task 7 work should continue moving non-resume web domains out of generic `components`/routes. Task 8 still owns dialog registry decomposition beyond import updates caused by the draft-store move.
|
||||
|
||||
## Latest Task 7 Slice 2 Notes
|
||||
|
||||
- App-shell code now starts under feature folders:
|
||||
- `apps/web/src/features/command-palette` owns the command palette implementation and tests.
|
||||
- `apps/web/src/features/theme` owns the theme provider, combobox, toggle button, and tests.
|
||||
- `apps/web/src/features/locale` owns the locale combobox and tests.
|
||||
- `apps/web/src/features/user` owns the user dropdown.
|
||||
- Auth route UI now lives under `apps/web/src/features/auth`; route files under `apps/web/src/routes/auth` keep redirects/search validation and compose the feature pages.
|
||||
- Settings page UI now lives under `apps/web/src/features/settings`; route files under `apps/web/src/routes/dashboard/settings` keep dashboard header composition and redirects. `job-search.tsx` remains route-only because it is already just a redirect shim.
|
||||
- The old `@/components/{command-palette,theme,locale,user}` import paths should not be reintroduced; current consumers import from `@/features/*`.
|
||||
- `apps/web/src/components` now contains remaining generic primitive/screen folders only.
|
||||
- Task 8 still owns dialog registry decomposition; this slice did not decompose dialog definitions.
|
||||
|
||||
## Latest Task 8 Notes
|
||||
|
||||
- `apps/web/src/dialogs/store.ts` remains the single global dialog runtime and still exports `useDialogStore` plus `DialogProps<T>` for existing callers.
|
||||
- Dialog schemas now compose through `apps/web/src/dialogs/schemas.ts` from domain-owned schema entries in `dialogs/auth/schema.ts`, `dialogs/api-key/schema.ts`, and `dialogs/resume/schema.ts`.
|
||||
- Dialog rendering now composes through `apps/web/src/dialogs/renderers.tsx` from domain-owned renderer registries in `dialogs/auth/registry.tsx`, `dialogs/api-key/registry.tsx`, and `dialogs/resume/registry.tsx`.
|
||||
- `apps/web/src/dialogs/manager.tsx` only imports the dialog shell, `renderDialog`, and the global store; it no longer imports every dialog implementation directly.
|
||||
- No settings/auth-specific dialog tests existed beyond the shared dialog store and resume template tests.
|
||||
|
||||
## Latest Task 9 Notes
|
||||
|
||||
- `turbo.json` now has executable `boundaries` config:
|
||||
- Global dependencies deny `web` and `server`, preventing package-to-app and app-to-app dependency edges.
|
||||
- Root test tools are explicit `implicitDependencies`: `vitest`, `@testing-library/jest-dom`, `@testing-library/react`, and `@testing-library/user-event`.
|
||||
- Tag rules cover app, server, browser, universal, domain, and UI layers.
|
||||
- Workspace `turbo.json` files now exist for both apps and every package. Each extends root config with `extends: ["//"]` and declares tags used by the root boundaries.
|
||||
- Every workspace `vitest.config.ts` keeps its legitimate root shared config import with `// @boundaries-ignore root shared Vitest config` immediately above `import { createVitestProjectConfig } from "../../vitest.shared";`.
|
||||
- `biome.json` now enables `style.noRestrictedImports` for forbidden cross-workspace source/path imports:
|
||||
- `@reactive-resume/*/src/**`
|
||||
- `apps/**`
|
||||
- `packages/**`
|
||||
- `biome.json` also registers `tooling/grit/no-cross-workspace-src-imports.grit`, which flags import/export/dynamic import sources that reach into another workspace's `src` tree.
|
||||
- `apps/web/tsconfig.json` no longer maps `@reactive-resume/ui/*` directly to `../../packages/ui/src/*`; the web app uses `@reactive-resume/ui` package exports instead.
|
||||
- Files changed by Task 9:
|
||||
- `turbo.json`
|
||||
- `apps/*/turbo.json`
|
||||
- `packages/*/turbo.json`
|
||||
- `biome.json`
|
||||
- `tooling/grit/no-cross-workspace-src-imports.grit`
|
||||
- `apps/web/tsconfig.json`
|
||||
- `apps/server/vitest.config.ts`
|
||||
- `apps/web/vitest.config.ts`
|
||||
- `packages/*/vitest.config.ts`
|
||||
- `docs/superpowers/worklogs/2026-05-14-monorepo-architecture-reorg.md`
|
||||
- `docs/superpowers/handoffs/2026-05-14-monorepo-architecture-reorg.md`
|
||||
- Validation passed:
|
||||
- `pnpm exec turbo boundaries`
|
||||
- `pnpm exec biome check biome.json turbo.json tooling/grit/no-cross-workspace-src-imports.grit apps/web/tsconfig.json apps/server/turbo.json apps/web/turbo.json packages/*/turbo.json`
|
||||
- `pnpm --filter web typecheck`
|
||||
- `pnpm --filter @reactive-resume/api typecheck`
|
||||
- `pnpm --filter server typecheck`
|
||||
- Remaining risks:
|
||||
- The Turbo tag set is intentionally coarse and may need finer per-package rules as future browser/server package classes settle.
|
||||
- The GritQL plugin only inspects code import/export sources; keep the package export-map and tsconfig scans in the final validation list.
|
||||
- The root shared Vitest config remains an intentional boundary ignore; moving it behind a package export would be a separate test-infra cleanup.
|
||||
|
||||
## Latest Task 10 Notes
|
||||
|
||||
- `AGENTS.md` is now the operational source of truth for package roles, runtime tags, import rules, placement decisions, and validation commands.
|
||||
- `docs/contributing/architecture.mdx` is now a public contributor overview of the current monorepo layout instead of the removed single-`src` architecture.
|
||||
- `docs/adr/0001-workspace-boundaries.md` records the rationale for domain-first packages, explicit export maps, `turbo boundaries`, and Biome/Grit import enforcement.
|
||||
- MCP user docs already list canonical unprefixed tool names from Task 4. AI Agent tool docs already list `read_resume` and `apply_resume_patch`; no Task 10 changes were needed there.
|
||||
- Follow-up documentation audit on 2026-05-15 reconciled AGENTS and public docs with the final `apps/server` Hono adapter shape, current Node/pnpm prerequisites, Base UI wording, provider-native web research behavior, and the current env schema.
|
||||
- Docs validation available in-repo is limited:
|
||||
- Stale-text scan passed except expected `ORPCClient` diagram labels.
|
||||
- Biome ignored Markdown/MDX and reported that no files were processed for the docs paths.
|
||||
- No dedicated docs build/check script exists in `package.json`.
|
||||
|
||||
## Latest Task 11 Notes
|
||||
|
||||
- Final cleanup after `pnpm knip`:
|
||||
- Removed stale app dependencies from `apps/server/package.json` and `apps/web/package.json`.
|
||||
- Deleted the unused web server PDF wrapper at `apps/web/src/features/resume/export/pdf-document.server.tsx`.
|
||||
- Removed unnecessary exported markers from internal helper functions/constants.
|
||||
- Added `pdfExportRateLimit` to `packages/api/src/features/resume/export.ts`.
|
||||
- Removed unused jobs rate-limit middleware entries that no current router uses.
|
||||
- Final validation commands passed:
|
||||
- `pnpm install --lockfile-only`
|
||||
- `pnpm install`
|
||||
- `pnpm exec biome check .`
|
||||
- `pnpm exec turbo boundaries`
|
||||
- `pnpm knip`
|
||||
- `pnpm typecheck`
|
||||
- `pnpm test`
|
||||
- `pnpm build`
|
||||
- `pnpm knip` still prints a non-failing configuration hint: `src/server.ts apps/web knip.json Refine entry pattern (no matches)`.
|
||||
- Useful review anchors:
|
||||
- Architecture rules: `AGENTS.md`, `docs/contributing/architecture.mdx`, `docs/adr/0001-workspace-boundaries.md`.
|
||||
- Boundary enforcement: `turbo.json`, workspace `turbo.json` files, `biome.json`, `tooling/grit/no-cross-workspace-src-imports.grit`.
|
||||
- API feature tree: `packages/api/src/features`.
|
||||
- Server adapter tree: `apps/server/src/{http,rpc,mcp,openapi,static,startup}`.
|
||||
- Web feature tree: `apps/web/src/features`.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,277 +0,0 @@
|
||||
# Monorepo Architecture Reorganization Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use `subagent-driven-development` or `executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
> **Status note:** This implementation plan has been executed and may contain historical intermediate paths. Use `AGENTS.md`, `docs/contributing/architecture.mdx`, `docs/adr/0001-workspace-boundaries.md`, and `docs/superpowers/handoffs/2026-05-14-monorepo-architecture-reorg.md` for current architecture guidance.
|
||||
|
||||
**Goal:** Reorganize Reactive Resume into clear domain/package boundaries so server, web, API, MCP, PDF, resume-domain logic, and documentation are easier to debug and enforce.
|
||||
|
||||
**Architecture:** This is one PR with green internal commits. Packages expose role-based explicit public surfaces, web routes become thin shells over domain features, API code is colocated by feature/capability, and boundary rules prevent app-to-app source imports and private package source imports.
|
||||
|
||||
**Tech Stack:** pnpm 11, Turborepo 2, TypeScript/tsgo, Vite, React 19, TanStack Router, oRPC, Drizzle, React PDF, PDF.js, MCP SDK, Biome/GritQL, Vitest.
|
||||
|
||||
---
|
||||
|
||||
## Non-Negotiable Decisions
|
||||
|
||||
- Root `AGENTS.md` is the single normative architecture source of truth.
|
||||
- `docs/adr/0001-workspace-boundaries.md` records rationale only; it must not duplicate the full operational rule set.
|
||||
- `docs/contributing/architecture.mdx` is a descriptive overview, not a second source of truth.
|
||||
- Keep one PR and green internal commits; no compatibility wrappers or old import-path shims.
|
||||
- Preserve existing unrelated user changes in manifests and lockfile. Do not reset or revert them.
|
||||
- Keep `apps/server` and `apps/web` split. `apps/server` may serve `apps/web/dist`; app-to-app `src` imports are banned.
|
||||
- `apps/web` may import `packages/api` types only. Runtime imports from `packages/api` in web are banned.
|
||||
- `apps/server` may import explicit runtime exports from `packages/api`.
|
||||
- Tests remain colocated with moved code.
|
||||
|
||||
## Task 0: Coordination Artifacts
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/superpowers/plans/2026-05-14-monorepo-architecture-reorg.md`
|
||||
- Modify: `docs/superpowers/handoffs/2026-05-14-monorepo-architecture-reorg.md`
|
||||
- Modify: `docs/superpowers/worklogs/2026-05-14-monorepo-architecture-reorg.md`
|
||||
|
||||
- [ ] Keep this plan updated when implementation discoveries force refinements.
|
||||
- [ ] Keep the handoff file focused on current task ownership and next-agent startup context.
|
||||
- [ ] Keep the worklog append-only with commands, validation results, blockers, and changed ownership decisions.
|
||||
|
||||
## Task 1: Pure Resume Domain Package
|
||||
|
||||
**Goal:** Create `@reactive-resume/resume` for pure resume-domain behavior, keeping `@reactive-resume/schema` validation-only and `@reactive-resume/utils` generic.
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/resume/package.json`
|
||||
- Create: `packages/resume/tsconfig.json`
|
||||
- Create: `packages/resume/vitest.config.ts`
|
||||
- Move/create: `packages/resume/src/patch.ts`
|
||||
- Move/create: `packages/resume/src/icons.ts`
|
||||
- Move tests from `packages/utils/src/resume/patch.test.ts`
|
||||
- Move tests for `packages/utils/src/network-icons.test.ts`
|
||||
- Modify importers/callers currently using `@reactive-resume/utils/resume/patch`
|
||||
- Modify importers/callers currently using `@reactive-resume/utils/network-icons`
|
||||
- Modify `packages/utils/package.json`
|
||||
- Modify root workspace manifests/lockfile as needed
|
||||
|
||||
- [x] Create the new package using the repo's source-consumed package pattern.
|
||||
- [x] Move JSON Patch schema/types/application/comparison/error into `@reactive-resume/resume/patch`.
|
||||
- [x] Move social network to icon-name mapping into `@reactive-resume/resume/icons`.
|
||||
- [x] Update all runtime and test imports.
|
||||
- [x] Remove `@reactive-resume/schema` dependency from `@reactive-resume/utils`.
|
||||
- [x] Run `pnpm --filter @reactive-resume/resume test`.
|
||||
- [x] Run `pnpm --filter @reactive-resume/resume typecheck`.
|
||||
- [x] Run targeted typechecks for known consumers: `@reactive-resume/api`, `@reactive-resume/ai`, `web`.
|
||||
|
||||
## Task 2: DOCX Package
|
||||
|
||||
**Goal:** Create `@reactive-resume/docx` as the dedicated DOCX export package.
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/docx/package.json`
|
||||
- Create: `packages/docx/tsconfig.json`
|
||||
- Create: `packages/docx/vitest.config.ts`
|
||||
- Move: `packages/utils/src/resume/docx/*` to `packages/docx/src/*`
|
||||
- Modify web export callers currently using `@reactive-resume/utils/resume/docx`
|
||||
- Modify package dependencies/lockfile
|
||||
|
||||
- [x] Create `@reactive-resume/docx` with explicit export `"."` or `"./builder"` as appropriate.
|
||||
- [x] Move DOCX implementation and tests unchanged except import paths.
|
||||
- [x] Update web callers to import DOCX export from `@reactive-resume/docx`.
|
||||
- [x] Remove DOCX dependencies from `@reactive-resume/utils` if no longer used there.
|
||||
- [x] Run `pnpm --filter @reactive-resume/docx test`.
|
||||
- [x] Run `pnpm --filter @reactive-resume/docx typecheck`.
|
||||
- [x] Run focused web export tests/typecheck.
|
||||
|
||||
## Task 3: PDF Package Browser/Server Generation Surface
|
||||
|
||||
**Goal:** Keep `@reactive-resume/pdf` focused on document/template/font rendering and pure generation adapters. Do not move PDF.js viewer UI into the PDF package.
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/pdf/src/browser.tsx`
|
||||
- Create: `packages/pdf/src/server.tsx`
|
||||
- Modify: `packages/pdf/package.json`
|
||||
- Modify: web PDF generation callers currently using `apps/web/src/libs/resume/pdf-document.tsx`
|
||||
- Modify: `apps/server`/API PDF download code after Task 5
|
||||
|
||||
- [x] Add data-plus-options generation APIs:
|
||||
- `createResumePdfBlob({ data, template, resolveSectionTitle })`
|
||||
- `createResumePdfFile({ data, filename, template, resolveSectionTitle })`
|
||||
- [x] Keep Lingui locale loading in `apps/web`; pass `resolveSectionTitle` into PDF helpers.
|
||||
- [x] Keep `ResumeDocument` as the underlying render surface.
|
||||
- [x] Add/update tests for the browser/server helpers where practical.
|
||||
- [x] Run `pnpm --filter @reactive-resume/pdf test`.
|
||||
- [x] Run `pnpm --filter @reactive-resume/pdf typecheck`.
|
||||
|
||||
## Task 4: MCP Package and Shared Tool Contracts
|
||||
|
||||
**Goal:** Extract MCP implementation from web route helpers into `@reactive-resume/mcp`; share model-facing tool contracts from `@reactive-resume/ai`.
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/mcp/package.json`
|
||||
- Create: `packages/mcp/tsconfig.json`
|
||||
- Create: `packages/mcp/vitest.config.ts`
|
||||
- Move: `apps/web/src/routes/mcp/-helpers/*` to `packages/mcp/src/*`
|
||||
- Create/modify: `packages/ai/src/tools/resume-tool-contracts.ts`
|
||||
- Modify: `packages/ai/package.json`
|
||||
- Modify: `apps/server/src/handlers/mcp.ts`
|
||||
- Modify: `apps/server/src/handlers/metadata.ts`
|
||||
- Remove old web helper imports
|
||||
|
||||
- [x] Move MCP tools/prompts/resources/metadata card generation and tests into `@reactive-resume/mcp`.
|
||||
- [x] Keep MCP execution through an injected/in-process oRPC `RouterClient`.
|
||||
- [x] Rename MCP tools to canonical unprefixed snake_case names such as `list_resumes`, `read_resume`, `apply_resume_patch`.
|
||||
- [x] Do not keep old `reactive_resume_*` aliases.
|
||||
- [x] Use shared base tool contracts from `@reactive-resume/ai`, with MCP-specific schema extensions for explicit context fields such as `resumeId`.
|
||||
- [ ] Update MCP docs/card version and cache-refresh guidance later in documentation tasks.
|
||||
- [x] Run `pnpm --filter @reactive-resume/mcp test`.
|
||||
- [x] Run `pnpm --filter @reactive-resume/mcp typecheck`.
|
||||
- [x] Run `pnpm --filter server typecheck`.
|
||||
|
||||
## Task 5: API Feature Reorganization
|
||||
|
||||
**Goal:** Move `packages/api/src` from technical layers into feature/capability modules with explicit public exports.
|
||||
|
||||
**Files:**
|
||||
- Reorganize under `packages/api/src/features/*`
|
||||
- Modify: `packages/api/src/routers/index.ts`
|
||||
- Modify: `packages/api/package.json`
|
||||
- Modify consumers in `apps/server`, `apps/web` type imports, and packages
|
||||
|
||||
- [x] Split `features/agent` into `threads`, `messages`, `attachments`, `actions`, `runs`, and `tools`.
|
||||
- [x] Split `features/resume` by capability: `crud`, `tags`, `statistics`, `analysis`, `access`, `events`, `sharing`, `export`.
|
||||
- [x] Move authenticated PDF download procedure into `features/resume/export`; it calls `@reactive-resume/pdf/server`.
|
||||
- [x] Keep Drizzle schema centralized in `packages/db`; API features consume schema but do not own table definitions.
|
||||
- [x] Remove `./services/*` and `./helpers/*` wildcard exports.
|
||||
- [x] Add explicit runtime exports required by `apps/server`.
|
||||
- [x] Preserve `apps/web` API imports as type-only.
|
||||
- [x] Run `pnpm --filter @reactive-resume/api test`.
|
||||
- [x] Run `pnpm --filter @reactive-resume/api typecheck`.
|
||||
- [x] Run `pnpm --filter server typecheck`.
|
||||
- [x] Run `pnpm --filter web typecheck`.
|
||||
|
||||
## Task 6: Server Adapter Reorganization
|
||||
|
||||
**Goal:** Make `apps/server` read as a runtime adapter app.
|
||||
|
||||
**Files:**
|
||||
- Reorganize `apps/server/src` into `http`, `rpc`, `mcp`, `openapi`, `static`, `startup`
|
||||
- Modify: `apps/server/src/index.ts`
|
||||
- Modify: `apps/server/package.json` if imports/dependencies change
|
||||
|
||||
- [x] Move handlers into adapter folders.
|
||||
- [x] Keep server logic thin: auth/session/HTTP transport/static serving/startup.
|
||||
- [x] Use explicit API runtime exports only.
|
||||
- [x] Keep serving `apps/web/dist` allowed.
|
||||
- [x] Run `pnpm --filter server test`.
|
||||
- [x] Run `pnpm --filter server typecheck`.
|
||||
|
||||
## Task 7: Domain-First Web Reorganization
|
||||
|
||||
**Goal:** Move web code into domain/workflow feature trees so routes become thin shells.
|
||||
|
||||
**Files:**
|
||||
- Create/reorganize under `apps/web/src/features/resume/*`
|
||||
- Create/reorganize under `apps/web/src/features/{command-palette,theme,locale,user,auth,settings,dialogs}/*`
|
||||
- Modify route imports and tests
|
||||
|
||||
- [x] Move resume domain code into workflow folders:
|
||||
- `builder`
|
||||
- `preview`
|
||||
- `public`
|
||||
- `dialogs`
|
||||
- `sections`
|
||||
- `templates`
|
||||
- `export`
|
||||
- `pdf-viewer`
|
||||
- [x] Move PDF.js viewer/canvas UI into web resume feature, not `@reactive-resume/pdf`.
|
||||
- [x] Keep route files responsible for URL params, loaders, redirects, SSR settings, and composition.
|
||||
- [x] Leave `apps/web/src/components` with generic app-level primitives/screens only.
|
||||
- [x] Move app shell concerns into separate features: command palette, theme, locale, user.
|
||||
- [x] Move auth workflows into `features/auth` and settings sections into `features/settings`.
|
||||
- [x] Run focused moved tests.
|
||||
- [x] Run `pnpm --filter web typecheck`.
|
||||
|
||||
Slice 1 notes:
|
||||
|
||||
- Moved resume builder draft state, builder preview, PDF.js canvas preview, dashboard thumbnail PDF rendering helpers, public resume PDF viewer, and web-local PDF document wrappers under `apps/web/src/features/resume/*`.
|
||||
- Removed `apps/web/src/components/resume` and `apps/web/src/routes/$username/-components`; the public resume route now lazy-loads from `features/resume/public`.
|
||||
- Kept PDF.js viewer/canvas code in `apps/web/src/features/resume` and left `@reactive-resume/pdf` limited to PDF generation helpers.
|
||||
- Broader Task 7 remains open for non-resume web feature moves and deeper dialog registry work owned by Task 8.
|
||||
|
||||
Slice 2 notes:
|
||||
|
||||
- Moved command palette, theme, locale, and user shell components/tests from `apps/web/src/components/*` to `apps/web/src/features/{command-palette,theme,locale,user}` and updated consumers.
|
||||
- Moved auth route UI, layout, and social auth component into `apps/web/src/features/auth`, leaving auth route files as guard/search/composition wrappers.
|
||||
- Moved settings page UI and authentication/integration subcomponents into `apps/web/src/features/settings`, leaving settings route files as dashboard-header/composition wrappers; `job-search` stayed route-only because it is already a redirect shim.
|
||||
- `apps/web/src/components` now contains only the remaining generic primitive/screen folders.
|
||||
- Task 8 still owns dialog registry decomposition; this slice only updated imports around existing dialog usage.
|
||||
|
||||
## Task 8: Dialog Registry Rework
|
||||
|
||||
**Goal:** Keep one central dialog runtime but make domain modules own their dialog definitions/renderers.
|
||||
|
||||
**Files:**
|
||||
- Modify/create under `apps/web/src/features/dialogs`
|
||||
- Modify/create domain dialog registry modules under relevant features
|
||||
- Modify root dialog manager import
|
||||
|
||||
- [x] Replace the single giant discriminated union/manager import hub with composable domain registries.
|
||||
- [x] Keep one global dialog store/runtime.
|
||||
- [x] Each domain exports its schema entries and renderers.
|
||||
- [x] Preserve existing dialog behavior and tests.
|
||||
- [x] Run dialog-focused tests.
|
||||
- [x] Run `pnpm --filter web typecheck`.
|
||||
|
||||
## Task 9: Boundary Enforcement
|
||||
|
||||
**Goal:** Make architectural rules executable.
|
||||
|
||||
**Files:**
|
||||
- Modify: `turbo.json`
|
||||
- Modify: `biome.json`
|
||||
- Create: `tooling/grit/no-cross-workspace-src-imports.grit`
|
||||
- Modify package manifests/tsconfigs to remove forbidden direct source path aliases
|
||||
|
||||
- [x] Add `turbo boundaries` package/runtime rules.
|
||||
- [x] Add a Biome GritQL plugin/rule for forbidden cross-workspace `src` imports and path aliases.
|
||||
- [x] Enforce subpath runtime tags: root exports environment-neutral; browser/server code behind explicit subpaths.
|
||||
- [x] Allow wildcard exports only for role-approved leaf libraries such as UI components/hooks.
|
||||
- [x] Run `pnpm exec turbo boundaries`.
|
||||
- [x] Run a focused Biome boundary check.
|
||||
|
||||
## Task 10: Documentation and Source of Truth
|
||||
|
||||
**Goal:** Document the final architecture without duplicating rules in multiple places.
|
||||
|
||||
**Files:**
|
||||
- Modify: `AGENTS.md`
|
||||
- Create: `docs/adr/0001-workspace-boundaries.md`
|
||||
- Modify: `docs/contributing/architecture.mdx`
|
||||
- Modify: `docs/guides/using-the-mcp-server.mdx` if MCP tool names change
|
||||
- Modify: `docs/guides/ai-agent-tools.mdx` if shared tool naming changes
|
||||
|
||||
- [x] Update root `AGENTS.md` with the normative workspace map, import rules, package roles, runtime tags, validation commands, and placement decision tree.
|
||||
- [x] Add ADR rationale for boundaries and rejected alternatives.
|
||||
- [x] Refresh public architecture docs as overview only.
|
||||
- [x] Update MCP docs to mention canonical unprefixed tool names and client refresh/cache-clear guidance.
|
||||
- [x] Avoid local package READMEs for architecture rules unless unavoidable.
|
||||
- [x] Run docs-related checks available in the repo.
|
||||
|
||||
## Task 11: Final Validation and Handoff
|
||||
|
||||
**Goal:** Finish with a validated branch and external-agent-ready handoff.
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/superpowers/handoffs/2026-05-14-monorepo-architecture-reorg.md`
|
||||
- Modify: `docs/superpowers/worklogs/2026-05-14-monorepo-architecture-reorg.md`
|
||||
|
||||
- [x] Run focused checks listed in earlier tasks.
|
||||
- [ ] Run final repo checks:
|
||||
- `pnpm install --lockfile-only`
|
||||
- `pnpm exec biome check .`
|
||||
- `pnpm exec turbo boundaries`
|
||||
- `pnpm knip`
|
||||
- `pnpm typecheck`
|
||||
- `pnpm test`
|
||||
- `pnpm build`
|
||||
- [x] Update the handoff with completed tasks, remaining risks, commands run, and useful file paths.
|
||||
- [x] Dispatch final review if subagents are available.
|
||||
@@ -1,214 +0,0 @@
|
||||
# Docker Nightly and Release Tags Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking.
|
||||
|
||||
**Goal:** Update the Docker image workflow so pushes to `main` publish amd64-only nightly tags, while manual runs and pushed version tags publish release tags with the existing multi-arch behavior.
|
||||
|
||||
**Architecture:** Keep the existing digest-build and manifest-merge pipeline in `.github/workflows/docker-build.yml`. Add event-based mode expressions so nightly and release publishing share setup, registry auth, digest upload/download, signing, and inspection while differing only in triggers, platform inclusion, final tags, and redeploy behavior.
|
||||
|
||||
**Tech Stack:** GitHub Actions, Docker Buildx, `docker/metadata-action@v6`, `docker/build-push-action@v7`, `docker buildx imagetools`, Cosign.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add Workflow Triggers and Mode Outputs
|
||||
|
||||
**Files:**
|
||||
- Modify: `.github/workflows/docker-build.yml`
|
||||
|
||||
- [x] **Step 1: Update workflow triggers**
|
||||
|
||||
Change the workflow `on` block to include:
|
||||
|
||||
```yaml
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "v*"
|
||||
```
|
||||
|
||||
Expected behavior:
|
||||
- `push` to `main` runs the workflow.
|
||||
- `push` to tags matching `v*` runs the workflow.
|
||||
- PR events do not run the workflow.
|
||||
- Manual runs still run the workflow.
|
||||
|
||||
- [x] **Step 2: Add a mode job**
|
||||
|
||||
Add a `mode` job before `build`:
|
||||
|
||||
```yaml
|
||||
mode:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
nightly: ${{ steps.mode.outputs.nightly }}
|
||||
release: ${{ steps.mode.outputs.release }}
|
||||
matrix: ${{ steps.mode.outputs.matrix }}
|
||||
steps:
|
||||
- name: Determine publishing mode
|
||||
id: mode
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/main" ]]; then
|
||||
echo "nightly=true" >> "$GITHUB_OUTPUT"
|
||||
echo "release=false" >> "$GITHUB_OUTPUT"
|
||||
echo 'matrix={"include":[{"platform":"linux/amd64","runner":"ubuntu-latest","arch":"amd64"}]}' >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "nightly=false" >> "$GITHUB_OUTPUT"
|
||||
echo "release=true" >> "$GITHUB_OUTPUT"
|
||||
echo 'matrix={"include":[{"platform":"linux/amd64","runner":"ubuntu-latest","arch":"amd64"},{"platform":"linux/arm64","runner":"ubuntu-24.04-arm","arch":"arm64"}]}' >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
```
|
||||
|
||||
This job centralizes the event decision so later jobs do not duplicate the full expression.
|
||||
|
||||
### Task 2: Generate the Platform Matrix from Mode
|
||||
|
||||
**Files:**
|
||||
- Modify: `.github/workflows/docker-build.yml`
|
||||
|
||||
- [x] **Step 1: Make `build` depend on `mode`**
|
||||
|
||||
Set:
|
||||
|
||||
```yaml
|
||||
needs: mode
|
||||
```
|
||||
|
||||
on the `build` job.
|
||||
|
||||
- [x] **Step 2: Replace the static build matrix with the mode output**
|
||||
|
||||
Set the build strategy matrix to the JSON emitted by the `mode` job:
|
||||
|
||||
```yaml
|
||||
matrix: ${{ fromJSON(needs.mode.outputs.matrix) }}
|
||||
```
|
||||
|
||||
Expected behavior:
|
||||
- Nightly runs create only the `amd64` matrix entry.
|
||||
- Release/manual/tag runs create both `amd64` and `arm64` matrix entries.
|
||||
|
||||
### Task 3: Generate Conditional Final Tags
|
||||
|
||||
**Files:**
|
||||
- Modify: `.github/workflows/docker-build.yml`
|
||||
|
||||
- [x] **Step 1: Make `merge` depend on `mode` and `build`**
|
||||
|
||||
Set:
|
||||
|
||||
```yaml
|
||||
needs:
|
||||
- mode
|
||||
- build
|
||||
```
|
||||
|
||||
on the `merge` job.
|
||||
|
||||
- [x] **Step 2: Update final Docker metadata tags**
|
||||
|
||||
In the merge job's Docker metadata step, replace the unconditional release tag list with conditional tags:
|
||||
|
||||
```yaml
|
||||
tags: |
|
||||
type=sha,prefix=sha-
|
||||
type=raw,value=nightly,enable=${{ needs.mode.outputs.nightly == 'true' }}
|
||||
type=raw,value=nightly-{{date 'YYYYMMDDHHmmss' tz='UTC'}},enable=${{ needs.mode.outputs.nightly == 'true' }}
|
||||
type=raw,value=latest,enable=${{ needs.mode.outputs.release == 'true' }}
|
||||
type=raw,value=v${{ steps.version.outputs.version }},enable=${{ needs.mode.outputs.release == 'true' }}
|
||||
type=raw,value=v${{ steps.semver.outputs.major }}.${{ steps.semver.outputs.minor }},enable=${{ needs.mode.outputs.release == 'true' }}
|
||||
type=raw,value=v${{ steps.semver.outputs.major }},enable=${{ needs.mode.outputs.release == 'true' }}
|
||||
```
|
||||
|
||||
Expected behavior:
|
||||
- Nightly runs publish `nightly`, `nightly-{UTC timestamp}`, and SHA tags.
|
||||
- Release/manual/tag runs publish `latest`, version aliases, and SHA tags.
|
||||
|
||||
### Task 4: Make Post-Publish Steps Mode-Aware
|
||||
|
||||
**Files:**
|
||||
- Modify: `.github/workflows/docker-build.yml`
|
||||
|
||||
- [x] **Step 1: Emit canonical image references from the manifest step**
|
||||
|
||||
In `Create manifest list and push`, compute the canonical final tag from mode:
|
||||
|
||||
```bash
|
||||
if [[ "${{ needs.mode.outputs.nightly }}" == "true" ]]; then
|
||||
FINAL_TAG="nightly"
|
||||
else
|
||||
FINAL_TAG="v${{ steps.version.outputs.version }}"
|
||||
fi
|
||||
```
|
||||
|
||||
Use `$FINAL_TAG` for both GHCR and Docker Hub digest lookup.
|
||||
|
||||
- [x] **Step 2: Inspect the canonical final tag**
|
||||
|
||||
Update `Inspect image` to inspect:
|
||||
|
||||
```bash
|
||||
docker buildx imagetools inspect ghcr.io/${{ env.IMAGE }}:${{ steps.manifest.outputs.final_tag }}
|
||||
docker buildx imagetools inspect docker.io/${{ env.IMAGE }}:${{ steps.manifest.outputs.final_tag }}
|
||||
```
|
||||
|
||||
- [x] **Step 3: Keep redeploy release-only**
|
||||
|
||||
Add this condition to `Redeploy Stack`:
|
||||
|
||||
```yaml
|
||||
if: ${{ needs.mode.outputs.release == 'true' }}
|
||||
```
|
||||
|
||||
Expected behavior:
|
||||
- Nightly publishes images, signs them, and inspects them.
|
||||
- Nightly does not redeploy the stack.
|
||||
- Release/manual/tag publishes images, signs them, inspects them, and redeploys.
|
||||
|
||||
### Task 5: Validate Workflow Logic
|
||||
|
||||
**Files:**
|
||||
- Test: `.github/workflows/docker-build.yml`
|
||||
|
||||
- [x] **Step 1: Parse YAML**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
ruby -e 'require "yaml"; YAML.load_file(".github/workflows/docker-build.yml"); puts "yaml ok"'
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```text
|
||||
yaml ok
|
||||
```
|
||||
|
||||
- [x] **Step 2: Review the workflow diff**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git diff -- .github/workflows/docker-build.yml docs/superpowers/plans/2026-05-15-docker-nightly-release-tags.md
|
||||
```
|
||||
|
||||
Confirm these requirements in the diff:
|
||||
- `push` to `main` is enabled.
|
||||
- pushed tags matching `v*` are enabled.
|
||||
- no `pull_request` trigger exists.
|
||||
- the matrix output contains only amd64 when `nightly == 'true'`.
|
||||
- nightly tags are enabled only for nightly mode.
|
||||
- release tags are enabled only for release mode.
|
||||
- redeploy is release-only.
|
||||
|
||||
- [x] **Step 3: Check working tree scope**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git status --short .github/workflows/docker-build.yml docs/superpowers/plans/2026-05-15-docker-nightly-release-tags.md
|
||||
```
|
||||
|
||||
Expected output includes only the workflow and this plan among files changed by this implementation.
|
||||
@@ -1,117 +0,0 @@
|
||||
# Manifest-Only PWA Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Remove Reactive Resume's service-worker and Workbox PWA behavior while keeping install metadata.
|
||||
|
||||
**Architecture:** Keep the manifest link and install meta tags in the web root route. Keep manifest data in
|
||||
`apps/web/public/manifest.webmanifest`, keep head meta tags in `apps/web/src/libs/pwa.ts`, and remove the
|
||||
service-worker registration export plus the `vite-plugin-pwa` build plugin.
|
||||
|
||||
**Tech Stack:** TanStack Start, Vite, TypeScript, pnpm, Vitest, Workbox removal.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Remove service-worker wiring
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/web/index.html`
|
||||
- Modify: `apps/web/vite.config.ts`
|
||||
- Modify: `apps/web/src/libs/pwa.ts`
|
||||
- Modify: `apps/web/src/routes/__root.tsx`
|
||||
- Delete: `apps/web/src/libs/pwa.test.ts`
|
||||
|
||||
- [x] **Step 1: Remove `vite-plugin-pwa` imports and plugin usage**
|
||||
|
||||
In `apps/web/vite.config.ts`, remove:
|
||||
|
||||
```ts
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
import { pwaManifest } from "./src/libs/pwa";
|
||||
```
|
||||
|
||||
Delete the local `pwa()` helper and remove `pwa()` from the `plugins` array.
|
||||
|
||||
- [x] **Step 2: Add install metadata to static HTML**
|
||||
|
||||
In `apps/web/index.html`, add the manifest link, icon links, theme color, and Apple/mobile install meta tags inside
|
||||
`<head>` so install metadata is present without plugin HTML injection.
|
||||
|
||||
- [x] **Step 3: Remove dead manifest export and runtime service-worker registration**
|
||||
|
||||
In `apps/web/src/libs/pwa.ts`, remove the `pwaManifest` and `pwaServiceWorkerRegistrationScript` exports so the
|
||||
module only owns head meta tags.
|
||||
|
||||
In `apps/web/src/routes/__root.tsx`, change the PWA import to:
|
||||
|
||||
```ts
|
||||
import { pwaHeadMetaTags } from "@/libs/pwa";
|
||||
```
|
||||
|
||||
Remove the `scripts` entry that injects `pwaServiceWorkerRegistrationScript` in production.
|
||||
|
||||
- [x] **Step 4: Delete obsolete PWA unit test**
|
||||
|
||||
Delete `apps/web/src/libs/pwa.test.ts`, because the remaining PWA surface is static manifest/head metadata.
|
||||
|
||||
### Task 2: Remove unused dependency graph
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/web/package.json`
|
||||
- Modify: `pnpm-lock.yaml`
|
||||
|
||||
- [x] **Step 1: Remove direct web dependency**
|
||||
|
||||
Remove this dependency from `apps/web/package.json`:
|
||||
|
||||
```json
|
||||
"vite-plugin-pwa": "^1.3.0"
|
||||
```
|
||||
|
||||
- [x] **Step 2: Refresh lockfile narrowly**
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
pnpm install --lockfile-only --offline --ignore-scripts
|
||||
```
|
||||
|
||||
Expected: the lockfile no longer contains `vite-plugin-pwa` or unused Workbox packages required only by that
|
||||
plugin. If the offline lockfile refresh is unavailable, edit the lockfile narrowly and verify with git diff.
|
||||
|
||||
### Task 3: Validate manifest-only behavior
|
||||
|
||||
**Files:**
|
||||
- Inspect: `apps/web/.output` or `apps/web/dist`
|
||||
|
||||
- [x] **Step 1: Run focused checks**
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
pnpm --filter web typecheck
|
||||
pnpm --filter web build
|
||||
```
|
||||
|
||||
Expected: both commands complete successfully.
|
||||
|
||||
- [x] **Step 2: Inspect build output for service-worker artifacts**
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
find apps/web/.output apps/web/dist -name 'sw.js' -o -name 'workbox-*' -o -name 'registerSW.js'
|
||||
```
|
||||
|
||||
Expected: no service-worker or Workbox files are printed. If one output directory does not exist, the command may
|
||||
print a find warning for that path; inspect the directory that exists.
|
||||
|
||||
- [x] **Step 3: Inspect source for removed service-worker registration**
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
rg -n "serviceWorker|register\\(\"/sw\\.js\"|VitePWA|vite-plugin-pwa|workbox" apps/web/src apps/web/vite.config.ts apps/web/package.json
|
||||
```
|
||||
|
||||
Expected: no matches for the removed PWA service-worker path.
|
||||
@@ -1,222 +0,0 @@
|
||||
# Unsafe OAuth Redirect URI Flag Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Replace `OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS` with `FLAG_ALLOW_UNSAFE_OAUTH_REDIRECT_URI`, preserving safe defaults and allowing any parseable redirect URI only when the flag is enabled.
|
||||
|
||||
**Architecture:** Keep OAuth redirect URI policy centralized in `packages/utils/src/url-security.node.ts`. Pass the new env flag from both Better Auth hook validation and the server auth preflight so both paths make identical decisions. Update env/docs references and tests in the same slice.
|
||||
|
||||
**Tech Stack:** TypeScript, Zod env schema, Better Auth hook middleware, Vitest, Turborepo env filtering, MDX docs.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify `packages/utils/src/url-security.node.ts`: Change the OAuth redirect validator from allowlist-based to mode-based.
|
||||
- Modify `packages/utils/src/url-security.node.test.ts`: Update safe-mode tests and add unsafe-mode coverage.
|
||||
- Modify `packages/env/src/server.ts`: Remove the old allowlist env var and add `FLAG_ALLOW_UNSAFE_OAUTH_REDIRECT_URI`.
|
||||
- Modify `packages/auth/src/config.ts`: Remove host-list parsing and pass `{ allowUnsafe: env.FLAG_ALLOW_UNSAFE_OAUTH_REDIRECT_URI }`.
|
||||
- Modify `apps/server/src/http/auth.ts`: Remove host-list parsing and pass the same flag to the validator.
|
||||
- Modify `apps/server/src/http/auth.test.ts`: Update env mock shape and preserve existing local edits.
|
||||
- Modify `turbo.json`, `.env.example`, and MDX docs: Replace old env references with the new flag and warnings.
|
||||
|
||||
### Task 1: URL Policy Tests And Validator
|
||||
|
||||
**Files:**
|
||||
- Modify: `packages/utils/src/url-security.node.test.ts`
|
||||
- Modify: `packages/utils/src/url-security.node.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing safe/unsafe OAuth redirect tests**
|
||||
|
||||
Use this shape in `packages/utils/src/url-security.node.test.ts`:
|
||||
|
||||
```ts
|
||||
describe("isAllowedOAuthRedirectUri", () => {
|
||||
const trustedOrigins = ["https://app.example.com"];
|
||||
|
||||
it("returns false for malformed URI", () => {
|
||||
expect(isAllowedOAuthRedirectUri("nope", trustedOrigins)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for any parseable URI when unsafe mode is enabled", () => {
|
||||
const options = { allowUnsafe: true };
|
||||
|
||||
expect(isAllowedOAuthRedirectUri("myapp://callback", trustedOrigins, options)).toBe(true);
|
||||
expect(isAllowedOAuthRedirectUri("http://example.com/cb", trustedOrigins, options)).toBe(true);
|
||||
expect(isAllowedOAuthRedirectUri("https://192.168.1.1/cb", trustedOrigins, options)).toBe(true);
|
||||
expect(isAllowedOAuthRedirectUri("https://u:p@app.example.com/cb#x", trustedOrigins, options)).toBe(true);
|
||||
expect(isAllowedOAuthRedirectUri("not a url", trustedOrigins, options)).toBe(false);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the focused utils test and verify it fails**
|
||||
|
||||
Run: `pnpm --filter @reactive-resume/utils test -- src/url-security.node.test.ts`
|
||||
|
||||
Expected before implementation: TypeScript/test failure because `isAllowedOAuthRedirectUri` still requires the removed allowlist argument.
|
||||
|
||||
- [ ] **Step 3: Implement mode-based OAuth redirect validation**
|
||||
|
||||
Use this signature in `packages/utils/src/url-security.node.ts`:
|
||||
|
||||
```ts
|
||||
type OAuthRedirectUriOptions = {
|
||||
allowUnsafe?: boolean;
|
||||
};
|
||||
|
||||
export function isAllowedOAuthRedirectUri(
|
||||
input: string,
|
||||
trustedOrigins: string[],
|
||||
options?: OAuthRedirectUriOptions,
|
||||
) {
|
||||
const parsed = parseUrl(input);
|
||||
if (!parsed) return false;
|
||||
if (options?.allowUnsafe) return true;
|
||||
if (parsed.username || parsed.password) return false;
|
||||
if (parsed.hash) return false;
|
||||
|
||||
const origin = parsed.origin.toLowerCase();
|
||||
const hostname = normalizeHostname(parsed.hostname);
|
||||
|
||||
if (parsed.protocol === "http:") return isOAuthLoopbackRedirectHost(hostname);
|
||||
if (parsed.protocol !== "https:") return false;
|
||||
if (isPrivateOrLoopbackHost(hostname)) return false;
|
||||
|
||||
return trustedOrigins.includes(origin);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the focused utils test and verify it passes**
|
||||
|
||||
Run: `pnpm --filter @reactive-resume/utils test -- src/url-security.node.test.ts`
|
||||
|
||||
Expected after implementation: all tests in `url-security.node.test.ts` pass.
|
||||
|
||||
### Task 2: Env And Runtime Wiring
|
||||
|
||||
**Files:**
|
||||
- Modify: `packages/env/src/server.ts`
|
||||
- Modify: `packages/auth/src/config.ts`
|
||||
- Modify: `apps/server/src/http/auth.ts`
|
||||
- Modify: `apps/server/src/http/auth.test.ts`
|
||||
- Modify: `turbo.json`
|
||||
|
||||
- [ ] **Step 1: Update env schema and Turbo env list**
|
||||
|
||||
In `packages/env/src/server.ts`, remove:
|
||||
|
||||
```ts
|
||||
OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS: z.string().optional(),
|
||||
```
|
||||
|
||||
Add with feature flags:
|
||||
|
||||
```ts
|
||||
FLAG_ALLOW_UNSAFE_OAUTH_REDIRECT_URI: z.stringbool().default(false),
|
||||
```
|
||||
|
||||
In `turbo.json`, remove `"OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS"` and add `"FLAG_ALLOW_UNSAFE_OAUTH_REDIRECT_URI"` beside the other flags.
|
||||
|
||||
- [ ] **Step 2: Wire the flag into Better Auth config**
|
||||
|
||||
In `packages/auth/src/config.ts`, remove `parseAllowedHostList` usage and call:
|
||||
|
||||
```ts
|
||||
if (
|
||||
!isAllowedOAuthRedirectUri(uri, TRUSTED_ORIGINS, {
|
||||
allowUnsafe: env.FLAG_ALLOW_UNSAFE_OAUTH_REDIRECT_URI,
|
||||
})
|
||||
) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "redirect_uri is not allowed for dynamic client registration",
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Wire the flag into server preflight**
|
||||
|
||||
In `apps/server/src/http/auth.ts`, remove `parseAllowedHostList` usage and call:
|
||||
|
||||
```ts
|
||||
!isAllowedOAuthRedirectUri(redirectUri, oauthTrustedOrigins, {
|
||||
allowUnsafe: env.FLAG_ALLOW_UNSAFE_OAUTH_REDIRECT_URI,
|
||||
})
|
||||
```
|
||||
|
||||
Update the test env mock in `apps/server/src/http/auth.test.ts`:
|
||||
|
||||
```ts
|
||||
env: {
|
||||
SERVER_PORT: 3001,
|
||||
APP_URL: "http://localhost:3000",
|
||||
FLAG_ALLOW_UNSAFE_OAUTH_REDIRECT_URI: false,
|
||||
},
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run focused typechecks**
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
pnpm --filter @reactive-resume/auth typecheck
|
||||
pnpm --filter server typecheck
|
||||
```
|
||||
|
||||
Expected: both commands exit 0.
|
||||
|
||||
### Task 3: Docs And Env Examples
|
||||
|
||||
**Files:**
|
||||
- Modify: `.env.example`
|
||||
- Modify: `docs/self-hosting/docker.mdx`
|
||||
- Modify: `docs/self-hosting/sso.mdx`
|
||||
- Modify: `docs/getting-started/quickstart.mdx`
|
||||
|
||||
- [ ] **Step 1: Replace old env docs with the new flag**
|
||||
|
||||
Remove all `OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS` references.
|
||||
|
||||
Add this warning wherever feature flags are documented:
|
||||
|
||||
```md
|
||||
`FLAG_ALLOW_UNSAFE_OAUTH_REDIRECT_URI`: Allows dynamic OAuth client registration to use any parseable redirect URI, including custom schemes, private hosts, and non-loopback `http://` URLs. Keep disabled unless this is a trusted self-hosted deployment. Enabling it on public or multi-tenant instances can enable phishing or token exfiltration.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the removed env is gone from product code and docs**
|
||||
|
||||
Run: `rg -n "OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS" . --glob "!docs/superpowers/**"`
|
||||
|
||||
Expected: no matches outside the approved design and implementation plan documents.
|
||||
|
||||
Run: `rg -n "FLAG_ALLOW_UNSAFE_OAUTH_REDIRECT_URI" .`
|
||||
|
||||
Expected: matches in env schema, Turbo config, docs, tests, and runtime validation paths.
|
||||
|
||||
### Task 4: Final Verification
|
||||
|
||||
**Files:**
|
||||
- Verify all modified files.
|
||||
|
||||
- [ ] **Step 1: Run focused tests**
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
pnpm --filter @reactive-resume/utils test -- src/url-security.node.test.ts
|
||||
pnpm --filter server test -- src/http/auth.test.ts
|
||||
```
|
||||
|
||||
Expected: both commands exit 0.
|
||||
|
||||
- [ ] **Step 2: Run focused typechecks and boundaries**
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
pnpm --filter @reactive-resume/auth typecheck
|
||||
pnpm --filter server typecheck
|
||||
pnpm exec turbo boundaries
|
||||
```
|
||||
|
||||
Expected: all commands exit 0.
|
||||
@@ -1,670 +0,0 @@
|
||||
# E2E Tests Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build a Playwright E2E test setup that runs deterministic core Reactive Resume flows with ephemeral accounts/data locally and on GitHub Actions for every PR.
|
||||
|
||||
**Architecture:** Add a root-level Playwright harness that targets the production server after `pnpm build`. Use hybrid fixtures: one browser-driven auth smoke spec, and helper-created authenticated browser state plus ephemeral database cleanup for resume flows. Keep initial PR-gated coverage to auth, sample resume creation, builder autosave, JSON export/import, and public sharing.
|
||||
|
||||
**Tech Stack:** Playwright, TypeScript, pnpm, Turbo, GitHub Actions, PostgreSQL service container, Better Auth HTTP endpoints, Drizzle/Postgres cleanup helpers.
|
||||
|
||||
---
|
||||
|
||||
## File structure
|
||||
|
||||
- Create `playwright.config.ts`: Playwright projects, reporters, artifact policy, base URL, and production `webServer`.
|
||||
- Create `tests/e2e/README.md`: local setup and CI behavior.
|
||||
- Create `tests/e2e/fixtures/data.ts`: unique test identity and resume value generation.
|
||||
- Create `tests/e2e/fixtures/auth.ts`: browser UI auth helpers and API-backed authenticated storage state helpers.
|
||||
- Create `tests/e2e/fixtures/db.ts`: user cleanup by email/username prefix.
|
||||
- Create `tests/e2e/fixtures/resume.ts`: UI helpers for creating sample resumes and accessing builder sections.
|
||||
- Create `tests/e2e/fixtures/test.ts`: typed Playwright fixture composition.
|
||||
- Create `tests/e2e/specs/auth.spec.ts`: browser registration/login smoke flow.
|
||||
- Create `tests/e2e/specs/resume-lifecycle.spec.ts`: dashboard create sample resume and builder autosave flow.
|
||||
- Create `tests/e2e/specs/json-export-import.spec.ts`: deterministic JSON backup/restore flow.
|
||||
- Create `tests/e2e/specs/public-sharing.spec.ts`: public sharing flow with anonymous browser context.
|
||||
- Create `.github/workflows/e2e.yml`: PR/push workflow with Postgres, build, Playwright install, E2E run, and report uploads.
|
||||
- Modify `package.json`: add Playwright dependency and E2E scripts.
|
||||
- Modify `turbo.json`: register E2E task if routed through package scripts.
|
||||
- Modify targeted UI files only if accessible locators are not sufficient.
|
||||
|
||||
## Task 1: Add Playwright dependency and scripts
|
||||
|
||||
**Files:**
|
||||
- Modify: `package.json`
|
||||
- Modify: `pnpm-lock.yaml`
|
||||
- Modify: `turbo.json`
|
||||
|
||||
- [ ] **Step 1: Add Playwright with the package manager**
|
||||
|
||||
Run: `pnpm add -D @playwright/test`
|
||||
|
||||
Expected: `package.json` and `pnpm-lock.yaml` include `@playwright/test`.
|
||||
|
||||
- [ ] **Step 2: Add root scripts**
|
||||
|
||||
Change root `package.json` scripts to include:
|
||||
|
||||
```json
|
||||
{
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:ci": "playwright test"
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Register the Turbo task**
|
||||
|
||||
Add this task entry to `turbo.json`:
|
||||
|
||||
```json
|
||||
"test:e2e": {
|
||||
"cache": false
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify script discovery**
|
||||
|
||||
Run: `pnpm exec playwright --version`
|
||||
|
||||
Expected: Playwright prints a version and exits successfully.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add package.json pnpm-lock.yaml turbo.json
|
||||
git commit -m "test: add playwright e2e scripts"
|
||||
git push -u origin feat/e2e-test-plan-2b10
|
||||
```
|
||||
|
||||
## Task 2: Add Playwright configuration
|
||||
|
||||
**Files:**
|
||||
- Create: `playwright.config.ts`
|
||||
|
||||
- [ ] **Step 1: Create the config**
|
||||
|
||||
Create `playwright.config.ts`:
|
||||
|
||||
```ts
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
const port = Number.parseInt(process.env.PORT ?? "3000", 10);
|
||||
const baseURL = process.env.APP_URL ?? `http://127.0.0.1:${port}`;
|
||||
const isCI = process.env.CI === "true" || process.env.CI === "1";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests/e2e/specs",
|
||||
fullyParallel: true,
|
||||
forbidOnly: isCI,
|
||||
retries: isCI ? 2 : 0,
|
||||
workers: isCI ? 2 : undefined,
|
||||
reporter: isCI
|
||||
? [
|
||||
["list"],
|
||||
["github"],
|
||||
["junit", { outputFile: "test-results/e2e-junit.xml" }],
|
||||
]
|
||||
: [["list"], ["html", { open: "never" }]],
|
||||
use: {
|
||||
baseURL,
|
||||
trace: "retain-on-failure",
|
||||
screenshot: "only-on-failure",
|
||||
video: "retain-on-failure",
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command: "pnpm start",
|
||||
url: `${baseURL}/api/health`,
|
||||
reuseExistingServer: !isCI,
|
||||
timeout: 120_000,
|
||||
env: {
|
||||
...process.env,
|
||||
PORT: String(port),
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify config loads**
|
||||
|
||||
Run: `pnpm exec playwright test --list`
|
||||
|
||||
Expected: Playwright lists zero tests at this point without config errors.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add playwright.config.ts
|
||||
git commit -m "test: configure playwright"
|
||||
git push -u origin feat/e2e-test-plan-2b10
|
||||
```
|
||||
|
||||
## Task 3: Add E2E fixtures
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/e2e/fixtures/data.ts`
|
||||
- Create: `tests/e2e/fixtures/db.ts`
|
||||
- Create: `tests/e2e/fixtures/auth.ts`
|
||||
- Create: `tests/e2e/fixtures/resume.ts`
|
||||
- Create: `tests/e2e/fixtures/test.ts`
|
||||
|
||||
- [ ] **Step 1: Add test data helpers**
|
||||
|
||||
Create `tests/e2e/fixtures/data.ts`:
|
||||
|
||||
```ts
|
||||
import type { TestInfo } from "@playwright/test";
|
||||
|
||||
const sanitize = (value: string) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
||||
|
||||
export type E2EAccount = {
|
||||
name: string;
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export function createRunSlug(testInfo: TestInfo) {
|
||||
const worker = testInfo.workerIndex;
|
||||
const title = sanitize(testInfo.titlePath.join("-")).slice(0, 40);
|
||||
const suffix = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
return `e2e-${worker}-${title}-${suffix}`;
|
||||
}
|
||||
|
||||
export function createAccount(testInfo: TestInfo): E2EAccount {
|
||||
const slug = createRunSlug(testInfo).replaceAll("-", "_").slice(0, 48);
|
||||
return {
|
||||
name: "E2E Test User",
|
||||
username: slug,
|
||||
email: `${slug}@example.test`,
|
||||
password: "Password123!",
|
||||
};
|
||||
}
|
||||
|
||||
export function createResumeName(testInfo: TestInfo) {
|
||||
return `E2E Resume ${createRunSlug(testInfo)}`;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add database cleanup**
|
||||
|
||||
Create `tests/e2e/fixtures/db.ts`:
|
||||
|
||||
```ts
|
||||
import { eq, or } from "drizzle-orm";
|
||||
import { db, getPool } from "@reactive-resume/db/client";
|
||||
import { user } from "@reactive-resume/db/schema";
|
||||
|
||||
export async function deleteE2EUser(account: { email: string; username: string }) {
|
||||
await db.delete(user).where(or(eq(user.email, account.email), eq(user.username, account.username)));
|
||||
}
|
||||
|
||||
export async function closeE2EDatabase() {
|
||||
await getPool().end();
|
||||
globalThis.__pool = undefined;
|
||||
globalThis.__drizzle = undefined;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add auth helpers**
|
||||
|
||||
Create `tests/e2e/fixtures/auth.ts`:
|
||||
|
||||
```ts
|
||||
import type { Browser, Page } from "@playwright/test";
|
||||
import type { E2EAccount } from "./data";
|
||||
|
||||
export async function registerViaUi(page: Page, account: E2EAccount) {
|
||||
await page.goto("/auth/register");
|
||||
await page.getByLabel("Name").fill(account.name);
|
||||
await page.getByLabel("Username").fill(account.username);
|
||||
await page.getByLabel("Email Address").fill(account.email);
|
||||
await page.getByLabel("Password").fill(account.password);
|
||||
await page.getByRole("button", { name: "Sign up" }).click();
|
||||
await page.getByRole("link", { name: /continue/i }).click();
|
||||
await page.waitForURL(/\/dashboard/);
|
||||
}
|
||||
|
||||
export async function loginViaUi(page: Page, account: E2EAccount) {
|
||||
await page.goto("/auth/login");
|
||||
await page.getByLabel(/email|username/i).fill(account.email);
|
||||
await page.getByLabel("Password").fill(account.password);
|
||||
await page.getByRole("button", { name: "Sign in" }).click();
|
||||
await page.waitForURL(/\/dashboard/);
|
||||
}
|
||||
|
||||
export async function createAuthenticatedPage(browser: Browser, account: E2EAccount) {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
await registerViaUi(page, account);
|
||||
return page;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add resume UI helpers**
|
||||
|
||||
Create `tests/e2e/fixtures/resume.ts`:
|
||||
|
||||
```ts
|
||||
import type { Page, TestInfo } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
import { createResumeName } from "./data";
|
||||
|
||||
export async function createSampleResumeFromDashboard(page: Page, testInfo: TestInfo) {
|
||||
const resumeName = createResumeName(testInfo);
|
||||
await page.goto("/dashboard/resumes");
|
||||
await page.getByText("Create a new resume").click();
|
||||
await page.getByRole("dialog", { name: "Create a new resume" }).getByLabel("Name").fill(resumeName);
|
||||
await page.getByRole("button", { name: "Create resume with options" }).click();
|
||||
await page.getByRole("menuitem", { name: "Create a Sample Resume" }).click();
|
||||
await expect(page.getByText(resumeName)).toBeVisible();
|
||||
await page.getByText(resumeName).click();
|
||||
await page.waitForURL(/\/builder\/.+/);
|
||||
return resumeName;
|
||||
}
|
||||
|
||||
export async function openRightSidebarSection(page: Page, name: string) {
|
||||
await page.getByRole("button", { name }).click();
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Add fixture composition**
|
||||
|
||||
Create `tests/e2e/fixtures/test.ts`:
|
||||
|
||||
```ts
|
||||
import { test as base, expect } from "@playwright/test";
|
||||
import type { E2EAccount } from "./data";
|
||||
import { createAccount } from "./data";
|
||||
import { deleteE2EUser } from "./db";
|
||||
import { registerViaUi } from "./auth";
|
||||
|
||||
type Fixtures = {
|
||||
account: E2EAccount;
|
||||
authPage: import("@playwright/test").Page;
|
||||
};
|
||||
|
||||
export const test = base.extend<Fixtures>({
|
||||
account: async ({}, use, testInfo) => {
|
||||
const account = createAccount(testInfo);
|
||||
await use(account);
|
||||
await deleteE2EUser(account);
|
||||
},
|
||||
authPage: async ({ browser, account }, use) => {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
await registerViaUi(page, account);
|
||||
await use(page);
|
||||
await context.close();
|
||||
},
|
||||
});
|
||||
|
||||
export { expect };
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run type-aware feedback**
|
||||
|
||||
Run: `pnpm exec tsc --noEmit --allowImportingTsExtensions false --moduleResolution bundler --target es2022 --module esnext tests/e2e/fixtures/*.ts`
|
||||
|
||||
Expected: If direct `tsc` is too narrow for workspace exports, use `pnpm typecheck` after Task 6 instead.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add tests/e2e/fixtures
|
||||
git commit -m "test: add e2e fixtures"
|
||||
git push -u origin feat/e2e-test-plan-2b10
|
||||
```
|
||||
|
||||
## Task 4: Add core E2E specs
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/e2e/specs/auth.spec.ts`
|
||||
- Create: `tests/e2e/specs/resume-lifecycle.spec.ts`
|
||||
- Create: `tests/e2e/specs/json-export-import.spec.ts`
|
||||
- Create: `tests/e2e/specs/public-sharing.spec.ts`
|
||||
|
||||
- [ ] **Step 1: Add auth smoke spec**
|
||||
|
||||
Create `tests/e2e/specs/auth.spec.ts`:
|
||||
|
||||
```ts
|
||||
import { test, expect } from "../fixtures/test";
|
||||
import { loginViaUi, registerViaUi } from "../fixtures/auth";
|
||||
|
||||
test("registers and logs in with email credentials", async ({ page, account }) => {
|
||||
await registerViaUi(page, account);
|
||||
await expect(page.getByRole("heading", { name: "Resumes" })).toBeVisible();
|
||||
await page.getByRole("button", { name: /user menu|account|profile/i }).click();
|
||||
await page.getByRole("menuitem", { name: /logout|sign out/i }).click();
|
||||
await loginViaUi(page, account);
|
||||
await expect(page.getByRole("heading", { name: "Resumes" })).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add resume lifecycle spec**
|
||||
|
||||
Create `tests/e2e/specs/resume-lifecycle.spec.ts`:
|
||||
|
||||
```ts
|
||||
import { test, expect } from "../fixtures/test";
|
||||
import { createSampleResumeFromDashboard } from "../fixtures/resume";
|
||||
|
||||
test("creates a sample resume and persists a basics edit", async ({ authPage: page }, testInfo) => {
|
||||
await createSampleResumeFromDashboard(page, testInfo);
|
||||
const updatedName = `E2E Edited ${Date.now()}`;
|
||||
await page.getByRole("button", { name: "Basics" }).click();
|
||||
await page.getByLabel("Name").fill(updatedName);
|
||||
await page.reload();
|
||||
await page.getByRole("button", { name: "Basics" }).click();
|
||||
await expect(page.getByLabel("Name")).toHaveValue(updatedName);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add JSON export/import spec**
|
||||
|
||||
Create `tests/e2e/specs/json-export-import.spec.ts`:
|
||||
|
||||
```ts
|
||||
import { test, expect } from "../fixtures/test";
|
||||
import { createSampleResumeFromDashboard } from "../fixtures/resume";
|
||||
|
||||
test("exports and imports a resume JSON backup", async ({ authPage: page }, testInfo) => {
|
||||
const resumeName = await createSampleResumeFromDashboard(page, testInfo);
|
||||
await page.getByRole("button", { name: "Export" }).click();
|
||||
const downloadPromise = page.waitForEvent("download");
|
||||
await page.getByRole("button", { name: /^JSON$/ }).click();
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toMatch(/\.json$/);
|
||||
const path = await download.path();
|
||||
expect(path).toBeTruthy();
|
||||
if (!path) throw new Error("Expected Playwright to provide a downloaded JSON path.");
|
||||
await page.goto("/dashboard/resumes");
|
||||
await page.getByText("Import an existing resume").click();
|
||||
await page.getByRole("combobox").click();
|
||||
await page.getByRole("option", { name: "Reactive Resume (JSON)" }).click();
|
||||
await page.getByText("Click here to select a file to import").setInputFiles(path);
|
||||
await page.getByRole("button", { name: "Import" }).click();
|
||||
await page.waitForURL(/\/builder\/.+/);
|
||||
await expect(page.getByText(resumeName)).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add public sharing spec**
|
||||
|
||||
Create `tests/e2e/specs/public-sharing.spec.ts`:
|
||||
|
||||
```ts
|
||||
import { test, expect } from "../fixtures/test";
|
||||
import { createSampleResumeFromDashboard } from "../fixtures/resume";
|
||||
|
||||
test("publishes a resume and renders it for an anonymous visitor", async ({ browser, authPage: page }, testInfo) => {
|
||||
await createSampleResumeFromDashboard(page, testInfo);
|
||||
await page.getByRole("button", { name: "Sharing" }).click();
|
||||
await page.getByLabel("Allow Public Access").click();
|
||||
const publicUrl = await page.getByLabel("URL").inputValue();
|
||||
const anonymous = await browser.newPage();
|
||||
await anonymous.goto(publicUrl);
|
||||
await expect(anonymous.getByRole("button", { name: /download/i })).toBeVisible();
|
||||
await anonymous.close();
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run list mode**
|
||||
|
||||
Run: `pnpm test:e2e -- --list`
|
||||
|
||||
Expected: Four Chromium tests are listed.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add tests/e2e/specs
|
||||
git commit -m "test: add core e2e specs"
|
||||
git push -u origin feat/e2e-test-plan-2b10
|
||||
```
|
||||
|
||||
## Task 5: Add documentation
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/e2e/README.md`
|
||||
|
||||
- [ ] **Step 1: Add E2E README**
|
||||
|
||||
Create `tests/e2e/README.md`:
|
||||
|
||||
```md
|
||||
# E2E Tests
|
||||
|
||||
Reactive Resume uses Playwright for PR-gated browser coverage of deterministic core flows.
|
||||
|
||||
## Local setup
|
||||
|
||||
Start PostgreSQL:
|
||||
|
||||
`sudo docker compose -f compose.dev.yml up -d postgres`
|
||||
|
||||
Generate local test secrets:
|
||||
|
||||
`export AUTH_SECRET=$(openssl rand -hex 32)`
|
||||
|
||||
`export ENCRYPTION_SECRET=$(openssl rand -hex 32)`
|
||||
|
||||
Run database migrations:
|
||||
|
||||
`APP_URL=http://localhost:3000 PORT=3000 DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres FLAG_DISABLE_SIGNUPS=false FLAG_DISABLE_EMAIL_AUTH=false FLAG_DISABLE_API_RATE_LIMIT=true LOCAL_STORAGE_PATH=/workspace/data/e2e pnpm db:migrate`
|
||||
|
||||
Build the production app:
|
||||
|
||||
`APP_URL=http://localhost:3000 PORT=3000 DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres FLAG_DISABLE_SIGNUPS=false FLAG_DISABLE_EMAIL_AUTH=false FLAG_DISABLE_API_RATE_LIMIT=true LOCAL_STORAGE_PATH=/workspace/data/e2e pnpm build`
|
||||
|
||||
Run tests:
|
||||
|
||||
`APP_URL=http://localhost:3000 PORT=3000 DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres FLAG_DISABLE_SIGNUPS=false FLAG_DISABLE_EMAIL_AUTH=false FLAG_DISABLE_API_RATE_LIMIT=true LOCAL_STORAGE_PATH=/workspace/data/e2e pnpm test:e2e`
|
||||
|
||||
## Coverage
|
||||
|
||||
- Email/password auth smoke.
|
||||
- Dashboard sample resume creation.
|
||||
- Builder basics edit and autosave persistence.
|
||||
- JSON export/import.
|
||||
- Public sharing for anonymous visitors.
|
||||
|
||||
PDF, DOCX, OAuth, passkeys, 2FA, password reset, and AI flows are intentionally outside the initial PR gate.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add tests/e2e/README.md
|
||||
git commit -m "docs: document e2e workflow"
|
||||
git push -u origin feat/e2e-test-plan-2b10
|
||||
```
|
||||
|
||||
## Task 6: Add GitHub Actions workflow
|
||||
|
||||
**Files:**
|
||||
- Create: `.github/workflows/e2e.yml`
|
||||
|
||||
- [ ] **Step 1: Add workflow**
|
||||
|
||||
Create `.github/workflows/e2e.yml`:
|
||||
|
||||
```yaml
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: ["main"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
APP_URL: http://localhost:3000
|
||||
PORT: "3000"
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
||||
FLAG_DISABLE_SIGNUPS: "false"
|
||||
FLAG_DISABLE_EMAIL_AUTH: "false"
|
||||
FLAG_DISABLE_API_RATE_LIMIT: "true"
|
||||
LOCAL_STORAGE_PATH: /tmp/reactive-resume-e2e-storage
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U postgres -d postgres"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Install Playwright Browser
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
|
||||
- name: Generate Test Secrets
|
||||
run: |
|
||||
echo "AUTH_SECRET=$(openssl rand -hex 32)" >> "$GITHUB_ENV"
|
||||
echo "ENCRYPTION_SECRET=$(openssl rand -hex 32)" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Prepare Storage
|
||||
run: mkdir -p "$LOCAL_STORAGE_PATH"
|
||||
|
||||
- name: Run Database Migrations
|
||||
run: pnpm db:migrate
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Run E2E Tests
|
||||
run: pnpm test:e2e:ci
|
||||
|
||||
- name: Upload Playwright Report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: playwright-report
|
||||
path: |
|
||||
playwright-report
|
||||
test-results
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add .github/workflows/e2e.yml
|
||||
git commit -m "ci: run e2e tests on pull requests"
|
||||
git push -u origin feat/e2e-test-plan-2b10
|
||||
```
|
||||
|
||||
## Task 7: Verify and stabilize
|
||||
|
||||
**Files:**
|
||||
- Modify any E2E files that fail due to actual labels or app behavior.
|
||||
- Modify targeted UI files only if no stable accessible locator exists.
|
||||
|
||||
- [ ] **Step 1: Run non-mutating checks**
|
||||
|
||||
Run: `pnpm exec biome check package.json turbo.json playwright.config.ts tests/e2e .github/workflows/e2e.yml`
|
||||
|
||||
Expected: No Biome errors.
|
||||
|
||||
- [ ] **Step 2: Run typecheck**
|
||||
|
||||
Run: `pnpm typecheck`
|
||||
|
||||
Expected: Typecheck passes for all workspaces.
|
||||
|
||||
- [ ] **Step 3: Start PostgreSQL**
|
||||
|
||||
Run: `sudo docker compose -f compose.dev.yml up -d postgres`
|
||||
|
||||
Expected: Postgres container is healthy.
|
||||
|
||||
- [ ] **Step 4: Build production app**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
export AUTH_SECRET=$(openssl rand -hex 32)
|
||||
export ENCRYPTION_SECRET=$(openssl rand -hex 32)
|
||||
APP_URL=http://localhost:3000 PORT=3000 DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres FLAG_DISABLE_SIGNUPS=false FLAG_DISABLE_EMAIL_AUTH=false FLAG_DISABLE_API_RATE_LIMIT=true LOCAL_STORAGE_PATH=/workspace/data/e2e pnpm build
|
||||
```
|
||||
|
||||
Expected: web and server production builds complete.
|
||||
|
||||
- [ ] **Step 5: Run E2E suite**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
APP_URL=http://localhost:3000 PORT=3000 DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres FLAG_DISABLE_SIGNUPS=false FLAG_DISABLE_EMAIL_AUTH=false FLAG_DISABLE_API_RATE_LIMIT=true LOCAL_STORAGE_PATH=/workspace/data/e2e pnpm test:e2e
|
||||
```
|
||||
|
||||
Expected: All Chromium specs pass.
|
||||
|
||||
- [ ] **Step 6: Commit stabilization changes**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "test: stabilize e2e suite"
|
||||
git push -u origin feat/e2e-test-plan-2b10
|
||||
```
|
||||
|
||||
## Self-review
|
||||
|
||||
- Spec coverage: Tasks cover Playwright setup, production-build target, hybrid fixtures, ephemeral data cleanup, core deterministic flows, CI workflow, and documentation.
|
||||
- Placeholder scan: No placeholder steps remain; each task names exact files, commands, and expected outcomes.
|
||||
- Type consistency: Fixture types are introduced before specs use them, and all helper names match across tasks.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,55 +0,0 @@
|
||||
# React PDF HTML Prose Spacing Design
|
||||
|
||||
## Context
|
||||
|
||||
Resume rich-text fields are rendered in PDF through `packages/pdf/src/templates/shared/rich-text.tsx`, which wraps normalized HTML with `react-pdf-html`. Each template currently supplies body-derived styles such as `richParagraph`, `richListItemRow`, `richListItemMarker`, and `richListItemContent`. Those styles intentionally keep rich text aligned with the template typography.
|
||||
|
||||
The current PDF rich-text spacing differs from what users see in Tailwind Typography prose. Paragraphs and list items are effectively flattened by zero margins, while browser prose gives paragraphs and list items vertical rhythm and trims the outside edges of the rich-text flow.
|
||||
|
||||
## Goals
|
||||
|
||||
- Add vertical spacing to `react-pdf-html` paragraphs and list items in the shared `RichText` path.
|
||||
- Match Tailwind Typography `prose-sm` spacing proportions without importing Tailwind's browser font size into PDF output.
|
||||
- Preserve each resume template's body font size and line height for rich text and adjacent non-rich-text PDF elements.
|
||||
- Remove the outer top margin from the first renderable rich-text block and the outer bottom margin from the last renderable rich-text block.
|
||||
- Keep the change shared and template-compatible unless a future template needs an explicit exception.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not change web editor `.wysiwyg` styles.
|
||||
- Do not change template body or heading font sizes.
|
||||
- Do not add a new PDF typography system.
|
||||
- Do not restyle headings, blockquotes, tables, or other HTML tags beyond paragraph and list item spacing.
|
||||
|
||||
## Recommended Approach
|
||||
|
||||
Use Tailwind Typography `prose-sm` spacing ratios and derive the final PDF spacing from the current template body font size. Tailwind Typography's `prose-sm` paragraph spacing is `16 / 14` of the body size, and list item spacing is `4 / 14` of the body size. The PDF renderer should use those ratios against the resolved body font size already present in the template styles.
|
||||
|
||||
This keeps rich-text content visually consistent with Tailwind Typography while respecting the resume template's PDF typography scale.
|
||||
|
||||
## Implementation Shape
|
||||
|
||||
The shared `RichText` component should:
|
||||
|
||||
1. Continue normalizing input through `normalizeRichTextHtml`.
|
||||
2. Inspect the normalized HTML to identify renderable top-level prose blocks relevant to this change: paragraphs and list items.
|
||||
3. Apply derived paragraph spacing to `p` through the `react-pdf-html` stylesheet.
|
||||
4. Apply derived list item spacing to the custom `li` renderer row container.
|
||||
5. Trim `marginTop` on the first relevant rich-text element and `marginBottom` on the last relevant rich-text element.
|
||||
6. Merge spacing after template styles where needed so old `margin: 0` template defaults do not suppress the new shared prose rhythm.
|
||||
|
||||
For list items, the spacing belongs on the custom `li` row container. Marker and content styles should continue coming from template slots so bullets, numbers, font size, and line height remain template-owned.
|
||||
|
||||
## Testing
|
||||
|
||||
Add focused tests around the shared rich-text spacing behavior. Coverage should include:
|
||||
|
||||
- A single paragraph has both outer margins trimmed.
|
||||
- Multiple paragraphs keep internal paragraph spacing and trim only the first top and last bottom edge.
|
||||
- Multiple list items get list-item vertical spacing with first top and last bottom trimmed.
|
||||
- Mixed paragraph/list content trims the first and last relevant elements across element types.
|
||||
- Rich-text font sizing remains template-derived rather than hard-coded to Tailwind's browser `14px`.
|
||||
|
||||
## Open Decisions
|
||||
|
||||
No open decisions remain. The approved behavior is to use Tailwind Typography spacing ratios derived from the resume template body font size.
|
||||
@@ -1,94 +0,0 @@
|
||||
# Docker Nightly and Release Tagging Design
|
||||
|
||||
## Goal
|
||||
|
||||
Update `.github/workflows/docker-build.yml` so the existing Docker build pipeline supports two publishing modes without duplicating the workflow:
|
||||
|
||||
- Nightly publishing on pushes to the default branch, `main`.
|
||||
- Release publishing only on manual workflow runs or pushed Git tags.
|
||||
|
||||
## Trigger Rules
|
||||
|
||||
The workflow should run for:
|
||||
|
||||
- `workflow_dispatch`, which publishes release tags.
|
||||
- `push` to `main`, which publishes nightly tags only.
|
||||
- `push` to Git tags matching `v*`, which publishes release tags.
|
||||
|
||||
Pull request events should not publish Docker images. This avoids exposing registry credentials to PR contexts and keeps nightly publishing tied to commits that have actually landed on `main`.
|
||||
|
||||
## Publishing Modes
|
||||
|
||||
### Nightly Mode
|
||||
|
||||
Nightly mode applies when:
|
||||
|
||||
- `github.event_name == "push"`
|
||||
- `github.ref == "refs/heads/main"`
|
||||
|
||||
Nightly mode should push these tags to both GHCR and Docker Hub:
|
||||
|
||||
- `nightly`
|
||||
- `nightly-{timestamp}`
|
||||
|
||||
The timestamp should be generated by the workflow at publish time in UTC using `YYYYMMDDHHmmss`, for example `nightly-20260515143000`.
|
||||
|
||||
Nightly builds should use only the `linux/amd64` platform. They should not build or publish `linux/arm64`.
|
||||
|
||||
### Release Mode
|
||||
|
||||
Release mode applies when:
|
||||
|
||||
- The workflow is run manually with `workflow_dispatch`.
|
||||
- A Git tag is pushed.
|
||||
|
||||
Release mode should preserve the existing release tag behavior:
|
||||
|
||||
- `latest`
|
||||
- `v{VERSION}` from root `package.json`
|
||||
- `v{MAJOR}.{MINOR}`
|
||||
- `v{MAJOR}`
|
||||
|
||||
Release builds should preserve the current multi-platform behavior:
|
||||
|
||||
- `linux/amd64`
|
||||
- `linux/arm64`
|
||||
|
||||
## Workflow Shape
|
||||
|
||||
Keep a single `.github/workflows/docker-build.yml` file. The current workflow already has the useful structure:
|
||||
|
||||
1. Build per-platform images and push them by digest.
|
||||
2. Upload digest artifacts.
|
||||
3. Merge digests into final manifest tags.
|
||||
4. Sign and inspect final images.
|
||||
5. Redeploy the stack.
|
||||
|
||||
The implementation should add a small mode-detection step or expression-based conditions rather than creating a second near-identical workflow.
|
||||
|
||||
The build matrix should include both architectures, but the arm64 build should be skipped during nightly mode. This keeps release behavior unchanged while making nightly pushes cheaper and faster.
|
||||
|
||||
The merge job should generate final tags conditionally:
|
||||
|
||||
- Nightly mode emits `nightly` and `nightly-{timestamp}`.
|
||||
- Release mode emits `latest` and version aliases.
|
||||
|
||||
The merge job should inspect and sign whichever final manifest tag is authoritative for the current mode:
|
||||
|
||||
- Nightly mode can inspect/sign `nightly`.
|
||||
- Release mode can inspect/sign `v{VERSION}`.
|
||||
|
||||
## Deployment
|
||||
|
||||
The existing `Redeploy Stack` step should remain release-only. Nightly pushes should publish images but should not redeploy the running stack.
|
||||
|
||||
## Validation
|
||||
|
||||
Validate the change by reviewing the workflow syntax and checking that:
|
||||
|
||||
- A `push` to `main` resolves to a single `linux/amd64` build and nightly tags.
|
||||
- A `workflow_dispatch` run resolves to both architectures and release tags.
|
||||
- A pushed Git tag resolves to both architectures and release tags.
|
||||
- No PR event can publish images.
|
||||
|
||||
If local workflow lint tooling is unavailable, inspect the YAML and relevant GitHub Actions expressions directly.
|
||||
@@ -1,99 +0,0 @@
|
||||
# Manifest-Only PWA Design
|
||||
|
||||
Date: 2026-05-15
|
||||
|
||||
## Goal
|
||||
|
||||
Reduce Reactive Resume's PWA implementation to install metadata only.
|
||||
|
||||
The site should remain installable on supported phones and desktops through the web app manifest, icons,
|
||||
screenshots, and mobile app meta tags. The PWA implementation should not generate, register, or rely on a
|
||||
service worker, and it should not provide offline support, app-shell precaching, runtime caching, or fallback
|
||||
navigation.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not add offline support.
|
||||
- Do not add a no-op service worker.
|
||||
- Do not add runtime caching rules for app assets, API responses, uploaded files, generated PDFs, or routes.
|
||||
- Do not change product UI, routing, auth, resume editing, public resume pages, or PDF generation behavior.
|
||||
- Do not change normal browser or HTTP caching behavior owned by the browser or server.
|
||||
|
||||
## Architecture
|
||||
|
||||
Keep install metadata in `apps/web/public/manifest.webmanifest`. That static manifest remains the source of truth
|
||||
for app name, description, theme color, background color, icons, screenshots, categories, scope, and start URL.
|
||||
|
||||
Keep mobile install/open-as-app meta tags in `apps/web/src/libs/pwa.ts`. That module continues to own:
|
||||
|
||||
- `pwaHeadMetaTags`
|
||||
- app name and theme color used by those head meta tags
|
||||
|
||||
Add the same manifest, icon, and mobile install hints directly to `apps/web/index.html` so install metadata is
|
||||
present in the initial HTML without relying on `vite-plugin-pwa` HTML injection or client-side route head updates.
|
||||
|
||||
Remove the service-worker part of the current architecture:
|
||||
|
||||
- Remove `VitePWA` usage from `apps/web/vite.config.ts`.
|
||||
- Remove the `vite-plugin-pwa` dependency from `apps/web/package.json` and the lockfile.
|
||||
- Remove the now-unused `pwaManifest` and `pwaServiceWorkerRegistrationScript` exports from `apps/web/src/libs/pwa.ts`.
|
||||
- Stop injecting the production `navigator.serviceWorker.register("/sw.js", { scope: "/" })` script from
|
||||
`apps/web/src/routes/__root.tsx`.
|
||||
|
||||
The root route keeps the existing manifest link:
|
||||
|
||||
```tsx
|
||||
{ rel: "manifest", href: "/manifest.webmanifest", crossOrigin: "use-credentials" }
|
||||
```
|
||||
|
||||
The root route also keeps the PWA-related head meta tags, including `theme-color` and Apple mobile web app tags,
|
||||
because those are part of the install/open-as-app experience rather than offline behavior.
|
||||
|
||||
## Data Flow
|
||||
|
||||
Browsers discover install metadata by reading the root document head and fetching `/manifest.webmanifest`.
|
||||
|
||||
Manifest-linked assets continue to be served as static public assets from `apps/web/public`, including:
|
||||
|
||||
- favicon assets
|
||||
- PWA icons
|
||||
- maskable icon
|
||||
- Apple touch icon
|
||||
- installation screenshots
|
||||
|
||||
No service worker is emitted by the web build, no service worker is registered at runtime, and no Workbox
|
||||
precache manifest is produced.
|
||||
|
||||
## Error Handling
|
||||
|
||||
There is no PWA-controlled offline fallback.
|
||||
|
||||
When the network is unavailable, the app behaves like a normal website: navigations, API calls, uploaded assets,
|
||||
and generated downloads fail according to the browser and server behavior already in place. This is intentional
|
||||
because installability is the only remaining PWA goal.
|
||||
|
||||
## Testing
|
||||
|
||||
Remove `apps/web/src/libs/pwa.test.ts`.
|
||||
|
||||
The file currently tests both install metadata and service-worker registration behavior. After this change, the
|
||||
remaining PWA surface is static manifest/head metadata with no behavior-specific module contract worth preserving
|
||||
as a dedicated unit test.
|
||||
|
||||
Implementation should still verify by inspection and build output that:
|
||||
|
||||
- `apps/web/dist/index.html` includes the manifest link and mobile install meta tags
|
||||
- the root route keeps the manifest link
|
||||
- PWA head meta tags remain present
|
||||
- no service-worker registration script is exported or injected by the root route
|
||||
- no `sw.js` or Workbox precache output is emitted by the web build
|
||||
|
||||
Focused validation after implementation:
|
||||
|
||||
```sh
|
||||
pnpm --filter web typecheck
|
||||
pnpm --filter web build
|
||||
```
|
||||
|
||||
If the package dependency removal changes the lockfile, verify the lockfile remains scoped to removing
|
||||
`vite-plugin-pwa` and its now-unused Workbox dependency graph.
|
||||
@@ -1,95 +0,0 @@
|
||||
# Unsafe OAuth Redirect URI Flag Design
|
||||
|
||||
Date: 2026-05-15
|
||||
|
||||
## Goal
|
||||
|
||||
Replace `OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS` with a single explicit escape hatch:
|
||||
`FLAG_ALLOW_UNSAFE_OAUTH_REDIRECT_URI`.
|
||||
|
||||
By default, dynamic OAuth client registration keeps the existing safe behavior: redirect URIs are allowed only when they target the app origin or local loopback callback hosts. When the flag is enabled, trusted self-hosted deployments may register any parseable redirect URI, including private network URLs, non-loopback `http://` URLs, and custom schemes such as `myapp://callback`.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not change whether dynamic OAuth client registration is enabled.
|
||||
- Do not change MCP OAuth audience handling, login, consent, token issuance, or public client registration defaults.
|
||||
- Do not reuse `FLAG_ALLOW_UNSAFE_AI_BASE_URL`; OAuth redirect handling is a separate trust boundary.
|
||||
- Do not keep a curated redirect-host allowlist after this change.
|
||||
|
||||
## Architecture
|
||||
|
||||
The existing redirect URI policy remains centralized in `@reactive-resume/utils/url-security.node`.
|
||||
|
||||
Introduce a mode-based OAuth redirect validator API, for example:
|
||||
|
||||
```ts
|
||||
isAllowedOAuthRedirectUri(input, trustedOrigins, { allowUnsafe })
|
||||
```
|
||||
|
||||
Safe mode keeps the current rules:
|
||||
|
||||
- Reject malformed URIs.
|
||||
- Reject embedded credentials.
|
||||
- Reject URI fragments.
|
||||
- Allow `http://localhost`, `http://127.0.0.1`, and `http://[::1]` callbacks.
|
||||
- Allow `https://` callbacks whose origin matches `APP_URL`.
|
||||
- Reject other public HTTPS hosts, private HTTPS hosts, non-loopback HTTP hosts, and non-HTTP schemes.
|
||||
|
||||
Unsafe mode deliberately weakens the redirect URI trust check:
|
||||
|
||||
- Allow any URI accepted by the platform URL parser.
|
||||
- Allow custom schemes such as `myapp://callback`.
|
||||
- Allow private and loopback hosts on any supported URL scheme.
|
||||
- Allow non-loopback `http://` URLs.
|
||||
|
||||
Unsafe mode should still reject strings that are not parseable as URLs. It does not preserve the current credentials or fragment restrictions, because the requested behavior is absolute unsafe: any parseable URI scheme is accepted.
|
||||
|
||||
## Data Flow
|
||||
|
||||
`packages/env/src/server.ts` defines `FLAG_ALLOW_UNSAFE_OAUTH_REDIRECT_URI` as a `z.stringbool().default(false)` feature flag and removes `OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS`.
|
||||
|
||||
`packages/auth/src/config.ts` uses the flag in the Better Auth `hooks.before` validation for `/oauth2/register`.
|
||||
|
||||
`apps/server/src/http/auth.ts` uses the same flag in the server-level registration preflight before forwarding the request to Better Auth.
|
||||
|
||||
Both validation paths must make the same allow or reject decision for the same redirect URI. The server preflight remains useful because it returns OAuth-shaped registration errors before the request enters Better Auth, while the Better Auth hook remains a defense-in-depth check.
|
||||
|
||||
## Documentation
|
||||
|
||||
Remove `OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS` from:
|
||||
|
||||
- `packages/env/src/server.ts`
|
||||
- `turbo.json`
|
||||
- `.env.example`
|
||||
- Docker and self-hosting docs
|
||||
- Quickstart docs
|
||||
|
||||
Add `FLAG_ALLOW_UNSAFE_OAUTH_REDIRECT_URI` to the feature flag sections and env examples with a warning that it is only appropriate for trusted self-hosted deployments. The warning should call out that enabling it permits arbitrary OAuth redirect URIs and can enable phishing or token exfiltration if used on public or multi-tenant instances.
|
||||
|
||||
## Error Handling
|
||||
|
||||
Rejected safe-mode redirects continue to return the existing error shape:
|
||||
|
||||
- Better Auth hook: `BAD_REQUEST` with `redirect_uri is not allowed for dynamic client registration`.
|
||||
- Server preflight: `400` with `invalid_redirect_uri`.
|
||||
|
||||
Unsafe mode only returns those errors when the redirect URI cannot be parsed as a URL or a non-string entry is supplied in `redirect_uris`.
|
||||
|
||||
## Testing
|
||||
|
||||
Update focused tests in the relevant packages:
|
||||
|
||||
- URL policy tests cover safe mode preserving current allowed and rejected cases.
|
||||
- URL policy tests cover unsafe mode allowing custom schemes, private hosts, non-loopback `http://`, credentials, and fragments when the URI is parseable.
|
||||
- Auth/server tests cover the env mock shape after removing `OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS`.
|
||||
- Documentation and env references no longer mention the removed variable.
|
||||
|
||||
Run focused validation after implementation:
|
||||
|
||||
```sh
|
||||
pnpm --filter @reactive-resume/utils test -- src/url-security.node.test.ts
|
||||
pnpm --filter server test -- src/http/auth.test.ts
|
||||
pnpm --filter @reactive-resume/auth typecheck
|
||||
pnpm --filter server typecheck
|
||||
pnpm exec turbo boundaries
|
||||
```
|
||||
@@ -1,29 +0,0 @@
|
||||
# Agent Snapshot Rollback Design
|
||||
|
||||
## Context
|
||||
|
||||
The AI Agent currently stores inverse JSON Patch operations for every applied resume patch. That lets the UI offer a revert action, but it rejects some valid JSON Patch operations because the server must be able to invert them before applying the patch.
|
||||
|
||||
## Goal
|
||||
|
||||
Use persisted resume snapshots for agent rollback so valid JSON Patch operations can be applied without first generating inverse operations.
|
||||
|
||||
## Design
|
||||
|
||||
Agent patch actions store the original JSON Patch operations for audit/debugging and a `snapshotData` copy of the resume data from immediately before the patch was applied. The database column is `snapshot_data`.
|
||||
|
||||
When `apply_resume_patch` runs, the server reads the current working resume, stores `resume.data` as `snapshotData`, applies the model-generated JSON Patch through the existing resume patch validator, and records the resulting `appliedUpdatedAt`.
|
||||
|
||||
The existing action revert endpoint becomes snapshot rollback. Rolling back an action restores that action's `snapshotData`, which means the resume returns to the state before that patch. If the selected action is older than the latest applied action, every applied patch action at or after the selected action is marked `rolled_back` so the chat remains auditable and the UI can show which actions were undone.
|
||||
|
||||
Rollback keeps the version guard. The restore is allowed only when the current resume version still matches the latest applied patch action for that resume/thread. If the builder or another process changed the resume after the latest agent patch, rollback marks the selected action `conflicted` and does not replace the resume JSON.
|
||||
|
||||
Existing rows that only have `inverse_operations` are legacy non-revertible actions after the migration. The migration removes `inverse_operations` and adds nullable `snapshot_data`.
|
||||
|
||||
## UI
|
||||
|
||||
The latest applied patch can be undone with the existing action button. Older applied patches can also be restored, but the label should make the destructive effect clear: rolling back to an older action discards that action and later applied agent patches. Actions with status `rolled_back` display as rolled back and cannot be rolled back again.
|
||||
|
||||
## Testing
|
||||
|
||||
Service tests cover storing `snapshotData`, restoring a snapshot, marking later applied actions as `rolled_back`, conflict handling, and rejecting legacy actions without `snapshotData`. Schema tests cover the `snapshotData` column replacing `inverseOperations`.
|
||||
@@ -1,129 +0,0 @@
|
||||
# Custom Styles Header Targets Design
|
||||
|
||||
## Context
|
||||
|
||||
Reactive Resume now stores user-defined custom styling as structured style rules in `metadata.styleRules`. The current rule system targets semantic section content and rich-text slots, then resolves those rules in `packages/pdf` through the shared section-style context.
|
||||
|
||||
The resume header is not currently part of that section-style context. Profile pictures, `basics.name`, `basics.headline`, and contact items are rendered directly inside each template header. That means users can style normal sections but cannot target the highest-visibility identity and contact content.
|
||||
|
||||
## Goals
|
||||
|
||||
- Make the profile picture targetable by Custom Styles.
|
||||
- Make the resume title and subtitle targetable by Custom Styles.
|
||||
- Make contact items targetable as a grouped v1 surface.
|
||||
- Keep the feature inside the existing `metadata.styleRules` model.
|
||||
- Preserve existing template header layouts and template-owned base styles.
|
||||
- Keep contact styling grouped in v1 instead of targeting email, phone, location, website, or custom fields separately.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not add per-contact-type targeting in v1.
|
||||
- Do not expose picture source, upload, crop, or visibility settings through style rules.
|
||||
- Do not redesign template headers or normalize every header into one layout.
|
||||
- Do not add a second metadata object for header-specific custom styles.
|
||||
- Do not make `sectionItem` targeting part of this work.
|
||||
|
||||
## Approved Approach
|
||||
|
||||
Extend the existing structured Style Rules system with a new header target and header-specific slots.
|
||||
|
||||
Add a new target scope:
|
||||
|
||||
- `header`
|
||||
|
||||
Add header slots:
|
||||
|
||||
- `picture`
|
||||
- `title`
|
||||
- `subtitle`
|
||||
- `contactList`
|
||||
- `contactItem`
|
||||
- `contactText`
|
||||
- `contactLink`
|
||||
- `contactIcon`
|
||||
|
||||
The header target applies only to the resume header/basic-info area. Global rules can also apply to matching header slots, which lets users define broad defaults such as "all contact text red." Header-targeted rules override global header-slot rules.
|
||||
|
||||
## UI Behavior
|
||||
|
||||
The Custom Styles panel should add `Header` to the target scope selector.
|
||||
|
||||
Slot availability should depend on target scope:
|
||||
|
||||
- `Header`: show only header slots.
|
||||
- `Section type` and `Specific section`: show section and rich-text slots only.
|
||||
- `All sections`: show all semantic slot groups, including the header group, because global rules apply anywhere the selected slot exists.
|
||||
|
||||
Header labels should be user-facing and concrete:
|
||||
|
||||
- `Picture`
|
||||
- `Title`
|
||||
- `Subtitle`
|
||||
- `Contact list`
|
||||
- `Contact item`
|
||||
- `Contact text`
|
||||
- `Contact link`
|
||||
- `Contact icon`
|
||||
|
||||
Applied rule labels should remain readable, for example `Header: Contact text` or `All sections: Contact text`.
|
||||
|
||||
## Rendering Design
|
||||
|
||||
Keep section rendering as-is with `SectionStyleProvider`.
|
||||
|
||||
Add a header/basic-info resolver path next to the section-style path. The resolver should use the same style intent translation and safety constraints as section slots, but it should resolve against header context instead of section context.
|
||||
|
||||
Template header elements should compose styles in this order:
|
||||
|
||||
1. Template defaults.
|
||||
2. Global rules for the selected header slot.
|
||||
3. Header-targeted rules for the selected header slot.
|
||||
4. Renderer safety styles where applicable.
|
||||
|
||||
Each template keeps its current layout and structure. The integration only layers resolved custom styles onto existing elements:
|
||||
|
||||
- `picture`: the profile `Image`.
|
||||
- `title`: `basics.name`.
|
||||
- `subtitle`: `basics.headline`.
|
||||
- `contactList`: the contact list container.
|
||||
- `contactItem`: each contact item wrapper or row.
|
||||
- `contactText`: text inside contact items.
|
||||
- `contactLink`: linked contact wrappers such as email, phone, website, and linked custom fields.
|
||||
- `contactIcon`: icons inside contact items.
|
||||
|
||||
Grouped contact styling means email, phone, location, website, and custom fields share the same contact slots. Templates with special wrapper structures, such as Rhyhorn separators, should keep those wrappers and apply the closest matching grouped slot.
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. The web UI writes header rules into `resume.data.metadata.styleRules`.
|
||||
2. The schema validates `header` targets and header slots.
|
||||
3. PDF rendering resolves header slots through the shared style-rule resolver.
|
||||
4. Template headers compose resolved styles with their existing styles.
|
||||
5. Preview, export, and public PDF paths receive the behavior automatically because they all render from resume metadata.
|
||||
|
||||
## Testing
|
||||
|
||||
Add focused coverage in three layers:
|
||||
|
||||
- Schema tests:
|
||||
- Accept `header` targets.
|
||||
- Accept header slots.
|
||||
- Reject unsupported slots and unsupported raw layout properties.
|
||||
- Preserve grouped contact style rules in `metadata.styleRules`.
|
||||
- PDF resolver tests:
|
||||
- Resolve global header-slot rules.
|
||||
- Resolve header-targeted rules.
|
||||
- Let header-targeted rules override global header-slot rules.
|
||||
- Ignore disabled header rules.
|
||||
- Keep section rules from applying to header slots.
|
||||
- Web UI tests:
|
||||
- `Header` appears as a target scope.
|
||||
- Header target limits slot options to header slots.
|
||||
- Header rules can be created, updated, toggled, displayed, managed, and deleted.
|
||||
- Applied rules display readable labels such as `Header: Contact text`.
|
||||
|
||||
Template rendering tests should focus on the common resolver/contact behavior and one or two representative templates rather than snapshotting every template header. The implementation still needs to wire the new slots into each template header.
|
||||
|
||||
## Open Decisions
|
||||
|
||||
No open decisions remain. Contact styling is grouped for v1, and the approved target model is a new `header` scope plus header-specific slots inside the existing `metadata.styleRules` system.
|
||||
@@ -1,262 +0,0 @@
|
||||
# E2E Tests Design
|
||||
|
||||
## Context
|
||||
|
||||
Reactive Resume is a pnpm/Turborepo monorepo with two deployable apps:
|
||||
|
||||
- `apps/web`: TanStack Start / React / Vite user interface.
|
||||
- `apps/server`: Hono / Node.js server that owns API, auth, MCP, static uploads, OpenAPI, and production web serving.
|
||||
|
||||
The product is a resume builder. The highest-value browser journeys are:
|
||||
|
||||
1. Create an account and authenticate.
|
||||
2. Create a resume from the dashboard.
|
||||
3. Open the builder and edit resume basics.
|
||||
4. Persist builder edits through autosave.
|
||||
5. Export and import structured resume JSON.
|
||||
6. Enable public sharing and view the resume as an anonymous visitor.
|
||||
|
||||
The repository currently has broad Vitest coverage but no Playwright or Cypress harness. GitHub Actions currently runs autofix/lint behavior on PRs and Docker image publishing on pushes/tags, but no PR-gated E2E workflow.
|
||||
|
||||
Context7 was requested for current framework guidance, but the configured Context7 quota was exhausted in this environment. The implementation plan should re-check current Playwright docs when Context7 access is available.
|
||||
|
||||
## Goals
|
||||
|
||||
- Add a working Playwright E2E setup.
|
||||
- Run E2E tests against the production build path, not only Vite dev mode.
|
||||
- Use ephemeral accounts and data for each run.
|
||||
- Cover deterministic core UX flows on every PR.
|
||||
- Keep CI runtime and flake risk low enough for a default PR gate.
|
||||
- Leave room to add PDF, DOCX, OAuth, passkey, 2FA, and AI flows later without bloating the initial gate.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Do not add PDF or DOCX download validation to the initial PR gate.
|
||||
- Do not automate OAuth, passkeys, 2FA, or password reset in the first suite.
|
||||
- Do not include AI Agent, AI import, or resume analysis flows in the first suite.
|
||||
- Do not add a cross-browser matrix initially.
|
||||
- Do not assert pixel-perfect PDF/canvas rendering.
|
||||
|
||||
## Recommended approach
|
||||
|
||||
Use a root-level Playwright harness that runs Chromium against a production build:
|
||||
|
||||
1. CI installs dependencies and Playwright Chromium.
|
||||
2. CI starts PostgreSQL.
|
||||
3. CI sets test environment variables.
|
||||
4. CI builds the app with `pnpm build`.
|
||||
5. Playwright starts the production server with `pnpm start`.
|
||||
6. E2E specs exercise browser flows against `APP_URL`.
|
||||
|
||||
Use hybrid fixtures:
|
||||
|
||||
- The auth smoke test registers and logs in through the browser UI.
|
||||
- Non-auth specs create ephemeral users and authenticated browser state through test helpers.
|
||||
- Resume setup can use helper APIs or direct database helpers when the setup itself is not the behavior under test.
|
||||
- Each test uses unique data and cleans up its own ephemeral user.
|
||||
|
||||
This keeps the suite close to user reality while avoiding repeated slow setup in every test.
|
||||
|
||||
## File layout
|
||||
|
||||
Add:
|
||||
|
||||
- `playwright.config.ts`
|
||||
- `tests/e2e/README.md`
|
||||
- `tests/e2e/fixtures/test.ts`
|
||||
- `tests/e2e/fixtures/auth.ts`
|
||||
- `tests/e2e/fixtures/db.ts`
|
||||
- `tests/e2e/fixtures/resume.ts`
|
||||
- `tests/e2e/specs/auth.spec.ts`
|
||||
- `tests/e2e/specs/resume-lifecycle.spec.ts`
|
||||
- `tests/e2e/specs/json-export-import.spec.ts`
|
||||
- `tests/e2e/specs/public-sharing.spec.ts`
|
||||
- `.github/workflows/e2e.yml`
|
||||
|
||||
Update:
|
||||
|
||||
- root `package.json` scripts
|
||||
- `turbo.json` tasks, if root scripts are routed through Turbo
|
||||
- lockfile after adding Playwright
|
||||
- contributing docs if a short root README is not enough
|
||||
|
||||
## Playwright configuration
|
||||
|
||||
The initial config should:
|
||||
|
||||
- Use Chromium only.
|
||||
- Read `baseURL` from `APP_URL`, defaulting to `http://localhost:3000`.
|
||||
- Use traces, screenshots, and videos on failure.
|
||||
- Use a local-friendly reporter plus a CI reporter/artifact format.
|
||||
- Start `pnpm start` through `webServer`.
|
||||
- Reuse an existing local server outside CI when possible.
|
||||
- Prefer role, label, and accessible-name locators.
|
||||
- Add targeted `data-testid` attributes only when accessibility locators are not stable enough.
|
||||
|
||||
Recommended root scripts:
|
||||
|
||||
- `test:e2e`: run Playwright.
|
||||
- `test:e2e:ui`: run Playwright UI mode.
|
||||
- `test:e2e:ci`: run Playwright with CI reporter settings.
|
||||
|
||||
## Environment and infrastructure
|
||||
|
||||
Minimum E2E environment:
|
||||
|
||||
- `APP_URL=http://localhost:3000`
|
||||
- `PORT=3000`
|
||||
- `DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres`
|
||||
- `AUTH_SECRET=<non-empty test secret>`
|
||||
- `FLAG_DISABLE_SIGNUPS=false`
|
||||
- `FLAG_DISABLE_EMAIL_AUTH=false`
|
||||
- `FLAG_DISABLE_API_RATE_LIMIT=true`
|
||||
- `LOCAL_STORAGE_PATH=<absolute temp path>`
|
||||
|
||||
S3, Redis, SMTP, and AI provider variables are not required for the initial deterministic suite.
|
||||
|
||||
PostgreSQL should be isolated for CI runs. The simplest PR workflow can use a GitHub Actions Postgres service container and a clean database per job. Local development can use `compose.dev.yml` to start Postgres.
|
||||
|
||||
## Ephemeral data strategy
|
||||
|
||||
Each worker/test should generate unique identities:
|
||||
|
||||
- Email: `e2e-<runId>-<worker>-<suffix>@example.test`
|
||||
- Username: `e2e_<runId>_<worker>_<suffix>`
|
||||
- Resume names/slugs prefixed with `e2e-`
|
||||
|
||||
Cleanup should delete the ephemeral user and rely on database cascade relationships for related resumes, sessions, accounts, API keys, and related records.
|
||||
|
||||
Use helper setup where it improves speed and stability:
|
||||
|
||||
- Create authenticated storage state for non-auth specs.
|
||||
- Create seeded resumes for tests that are about builder/export/sharing behavior rather than resume creation.
|
||||
- Keep one browser-driven auth spec to ensure registration/login still works end to end.
|
||||
|
||||
## Initial PR-gated specs
|
||||
|
||||
### Auth smoke
|
||||
|
||||
Flow:
|
||||
|
||||
1. Visit `/auth/register`.
|
||||
2. Register a unique user through the UI.
|
||||
3. Continue to `/dashboard`.
|
||||
4. Log out.
|
||||
5. Log in with the same credentials.
|
||||
6. Assert the dashboard resumes page loads.
|
||||
|
||||
Purpose:
|
||||
|
||||
- Proves email/password auth, session cookies, redirects, and dashboard access work.
|
||||
|
||||
### Resume lifecycle
|
||||
|
||||
Flow:
|
||||
|
||||
1. Start authenticated as an ephemeral user.
|
||||
2. Visit `/dashboard/resumes`.
|
||||
3. Create a sample resume through the UI.
|
||||
4. Open the resume in the builder.
|
||||
5. Edit the basics name field.
|
||||
6. Wait for autosave.
|
||||
7. Reload the builder.
|
||||
8. Assert the edited value persisted.
|
||||
|
||||
Purpose:
|
||||
|
||||
- Proves dashboard create, builder load, form editing, API update, and autosave persistence.
|
||||
|
||||
### JSON export and import
|
||||
|
||||
Flow:
|
||||
|
||||
1. Start authenticated with a sample resume.
|
||||
2. Open the builder.
|
||||
3. Export JSON.
|
||||
4. Assert a JSON download is created and parseable.
|
||||
5. Return to dashboard.
|
||||
6. Import the downloaded JSON through the import dialog.
|
||||
7. Assert the imported resume opens in the builder.
|
||||
|
||||
Purpose:
|
||||
|
||||
- Proves deterministic backup/restore without AI or binary rendering dependencies.
|
||||
|
||||
### Public sharing
|
||||
|
||||
Flow:
|
||||
|
||||
1. Start authenticated with a sample resume.
|
||||
2. Open builder sharing settings.
|
||||
3. Enable public access.
|
||||
4. Read the generated public URL.
|
||||
5. Open a fresh unauthenticated browser context.
|
||||
6. Visit the public URL.
|
||||
7. Assert the public resume viewer renders expected resume content.
|
||||
|
||||
Purpose:
|
||||
|
||||
- Proves sharing state updates, public routing, anonymous access, and public resume rendering.
|
||||
|
||||
## CI workflow
|
||||
|
||||
Add `.github/workflows/e2e.yml`:
|
||||
|
||||
- Trigger on `pull_request` and pushes to `main`.
|
||||
- Use Node 24.
|
||||
- Use pnpm via Corepack or `pnpm/action-setup`.
|
||||
- Cache pnpm dependencies through `actions/setup-node`.
|
||||
- Start PostgreSQL service.
|
||||
- Install dependencies with `pnpm install --frozen-lockfile`.
|
||||
- Install Playwright Chromium.
|
||||
- Run `pnpm build`.
|
||||
- Run `pnpm test:e2e:ci`.
|
||||
- Upload Playwright reports, traces, screenshots, and videos on failure.
|
||||
|
||||
The workflow should avoid secrets. All test credentials should be generated during the run.
|
||||
|
||||
## Selector strategy
|
||||
|
||||
Prefer stable user-facing locators:
|
||||
|
||||
- Roles: buttons, links, dialogs, textboxes, comboboxes.
|
||||
- Labels: form inputs and switches.
|
||||
- URLs: route assertions after navigation.
|
||||
|
||||
Add `data-testid` only where necessary:
|
||||
|
||||
- Builder canvas/PDF viewer surfaces that lack accessible names.
|
||||
- Download/import controls if file input access is otherwise too implementation-dependent.
|
||||
- Toast-independent save-state assertions if no visible stable status exists.
|
||||
|
||||
Keep test IDs narrow and product-neutral.
|
||||
|
||||
## Reliability considerations
|
||||
|
||||
- Autosave currently debounces changes by 500ms. Tests should wait for persisted state through reload or API-visible state, not arbitrary sleeps.
|
||||
- Public resume viewer uses browser PDF/canvas behavior. Assert meaningful content or viewer presence, not pixels.
|
||||
- JSON export/import is deterministic and should be the first export/import loop.
|
||||
- Disable API rate limiting in test env to avoid worker-order flakes.
|
||||
- Pin locale to the default English path or use role/label selectors robust to translations where practical.
|
||||
- Keep the initial suite serial only where shared worker state forces it; otherwise allow Playwright worker isolation.
|
||||
|
||||
## Future expansion
|
||||
|
||||
After the initial suite is stable:
|
||||
|
||||
1. Add PDF download validation as a separate optional or nightly job.
|
||||
2. Add DOCX download validation if the PDF job proves stable.
|
||||
3. Add password-protected public sharing.
|
||||
4. Add dashboard management flows: duplicate, lock, delete, tag filtering, list/grid view.
|
||||
5. Add settings flows: profile update, API key creation/deletion.
|
||||
6. Add AI flows only with mocked providers or a deterministic local provider.
|
||||
7. Add browser matrix only after the Chromium gate has low flake rates.
|
||||
|
||||
## Success criteria
|
||||
|
||||
- `pnpm test:e2e` runs locally against a built app and Postgres.
|
||||
- `pnpm test:e2e:ci` runs in GitHub Actions on every PR.
|
||||
- Tests create no persistent shared seed data.
|
||||
- Tests clean up ephemeral users and related data.
|
||||
- The initial suite covers auth, dashboard creation, builder autosave, JSON export/import, and public sharing.
|
||||
- CI uploads useful artifacts for diagnosing failures.
|
||||
@@ -1,94 +0,0 @@
|
||||
# Applications Tracker — Roadmap & Status
|
||||
|
||||
**Status legend:** ✅ done · 🟡 in progress · ⬜ not started · 🧊 deferred (intentional, revisit when needed)
|
||||
|
||||
A job-application tracker built inside Reactive Resume. Each application points at the live
|
||||
resume the user sent (`resumeId`), which is why it lives in-product rather than a generic
|
||||
tracker. Built from the claude.ai/design prototype "Applications Tracker.dc.html".
|
||||
|
||||
Owner dirs: `packages/db/src/schema/applications.ts`, `packages/schema/src/applications/`,
|
||||
`packages/api/src/features/applications/`, `apps/web/src/features/applications/`,
|
||||
`apps/web/src/routes/dashboard/applications/`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — Core slice ✅ (shipped)
|
||||
|
||||
The working vertical slice: data model, CRUD, board, add/detail panels. Zero new deps.
|
||||
|
||||
- ✅ DB: `application` table (user FK cascade, `resumeId` FK set-null, JSONB `contacts`/`activity`, follow-up + AI-reservation columns). Migration `20260705090711_third_hercules`.
|
||||
- ✅ Schema: `applicationStatusSchema`, `STAGES` (value/label/color), `contactSchema`, `activityEventSchema`, `aiMetadataSchema`.
|
||||
- ✅ API: oRPC `applications.{list,getById,create,update,addNote,delete}` — `protectedProcedure`, `userId`-scoped via one `requireOwned`; `update` auto-logs stage-change activity.
|
||||
- ✅ Web: sidebar nav item; `/dashboard/applications` route (empty state ↔ board).
|
||||
- ✅ Board: dnd-kit drag across 6 fixed stages, optimistic move + rollback toast.
|
||||
- ✅ Add slide-over: manual entry + link a live Reactive Resume; native date follow-up.
|
||||
- ✅ Detail slide-over: key facts, stage stepper, linked resume, contacts, follow-up, activity timeline + add-note, archive/delete.
|
||||
- ✅ Unit test: activity-logging path (`service.test.ts`). Typecheck / boundaries / biome clean.
|
||||
|
||||
**Known small gaps — now closed (✅):**
|
||||
- ✅ Lingui messages extracted (`pnpm --filter web lingui:extract`) so the new `<Trans>`/`t` strings are translatable.
|
||||
- ✅ Board caps rendered cards per column (`COLUMN_PAGE_SIZE = 50`) with a "Show N more" button, so a stage with hundreds of applications doesn't mount hundreds of draggable nodes. Server-side paging still unneeded (list payload is small).
|
||||
- ✅ Contacts are editable from the Detail panel (`ContactsEditor`): add name/role/label, remove, persisted via API `update`'s existing `contacts` field.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Table view + Insights ✅ (shipped)
|
||||
|
||||
- ✅ **Table view** — paginated table (25/page), row selection + bulk actions (move stage, add tag, archive, delete). Added a wrapped `Checkbox` in `packages/ui`; hand-rolled table (no `@tanstack/react-table` dep). Board/Table/Insights toggle via URL `view` search param.
|
||||
- ✅ **Insights view** — stat tiles, pipeline funnel (conversion per stage), source bars, and a shareable **funnel-flow SVG** with PNG export (canvas, no chart lib — kept the zero-dep property). Server aggregation `applications.stats` (per-stage/per-source counts); funnel/tiles derived client-side via `computeInsights()` (unit-tested).
|
||||
- ✅ Tags: `tags text[]` column + filter UI + bulk "add tag" + `applications.tags` distinct list.
|
||||
- ✅ Archived-toggle so archived rows can be reached and unarchived (closes the archive loop).
|
||||
|
||||
## Phase 2 — Campaigns + import + uploads ✅ (shipped)
|
||||
|
||||
- ✅ **Campaigns** — the `campaign` text field is now first-class: set on create (with a native `<datalist>` autocomplete of existing campaigns), filter on board/table, per-campaign Insights scoping, and an `applications.campaigns` distinct-with-counts endpoint. (Kept it a text field, not a table — grouping/filtering/insights work without the join-table overhead.)
|
||||
- ✅ **CSV import** — `Import CSV` opens a slide-over: paste rows or upload a `.csv`, a zero-dep parser (`csv.ts`, unit-tested) maps aliased headers (Company/Role/Stage/Salary/Source/Tags/…), previews recognized columns + ready/skipped counts, then bulk-creates via `applications.import` (max 500, each row gets a `created` activity event). Verified end-to-end in the browser.
|
||||
- ✅ **Cover-letter upload** — `coverLetterUrl`/`coverLetterName` columns; the detail panel's Documents section attaches a PDF via the existing `orpc.storage.uploadFile`, shows a download link, and supports removal.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — AI agent integration ✅ (shipped, verified live)
|
||||
|
||||
All four `applications.ai.*` procedures are implemented (`packages/api/src/features/applications/ai.ts`), resolving the user's default provider via `aiProvidersService.getDefaultRunnable` → `getModel` → `generateText`, with tolerant JSON parsing. Rate-limited via `aiRequestRateLimit`.
|
||||
|
||||
- ✅ **Job-posting autofill** — paste a URL → server fetches + strips the page → LLM extracts company/role/location/salary/jobDescription → prefills the Add form. (`jobDescription` is now a user-editable field so it persists for match/tailor.)
|
||||
- ✅ **Resume↔job match score** — scores the linked resume vs the job description, persists `matchScore` + gaps in `aiMetadata`. Rendered as a **fit ring** in the redesigned copilot.
|
||||
- ✅ **Tailor resume** — one-shot: duplicates the linked resume, regenerates a job-tailored `summary`, links the copy to the application, logs a timeline event. (No agent/REDIS dependency.)
|
||||
- ✅ **Draft cover letter / follow-up** — generates from application + resume context; shown inline with copy/dismiss.
|
||||
- ✅ **UX**: the AI section was redesigned into an **"Application Copilot"** module — a resume-fit ring (band-colored) as the signature element + a capability menu (icon + title + description) instead of plain buttons.
|
||||
|
||||
Verified live against a real OpenAI provider (autofill, match score, tailor resume).
|
||||
|
||||
### Extra polish shipped alongside (from review + user requests)
|
||||
- ✅ **Edit an application** — the Add sheet became `ApplicationFormSheet`, edit-capable; reachable from the detail-panel "Edit" button and the context menu.
|
||||
- ✅ **Context menus** — kebab "⋯" on board cards (hover) and table rows: Edit / Move to ▸ / Archive / Delete (`application-actions-menu.tsx`).
|
||||
- ✅ Review fixes: bulk stage-moves log activity; single-statement note append; "Mark rejected" + "no results" states; CSV BOM strip + 500-row cap + file-input reset.
|
||||
|
||||
### ✅ Cover-letter upload (re-enabled)
|
||||
Un-deferred. The blocker was `storage/service.ts` hardcoding a `.jpeg` key for every upload, so a PDF was read back as an image. Fixed by deriving the key extension from the content type the router already passes (`buildFileKey` + `EXTENSION_BY_CONTENT_TYPE`) — images stay `.jpeg` (unchanged for avatars), PDFs get `.pdf` and the static handler's existing `.pdf` force-download path serves them correctly. Re-added `coverLetterUrl`/`coverLetterName` columns (migration `20260705125353_yielding_star_brand`), DTO editable fields, and a Documents-section upload/download/remove in the detail sheet (PDF-only, best-effort `storage.deleteFile` on remove). Link-to-Reactive-Resume remains the primary document feature; the cover letter sits alongside it.
|
||||
|
||||
---
|
||||
|
||||
## (historical) Phase 3 plan — reservations in place, no model wired yet
|
||||
|
||||
Reservations already shipped: schema fields (`sourceUrl`, `jobDescription`, `matchScore`,
|
||||
`aiMetadata`) and stubbed `applications.ai.*` procedures that throw `NOT_IMPLEMENTED`
|
||||
(`packages/api/src/features/applications/ai.ts`). Each stub below just needs its handler
|
||||
implemented against `@reactive-resume/ai` and the existing agent/thread tables
|
||||
(`agentThread`, `agentAction` in `packages/db/src/schema/agent.ts`).
|
||||
|
||||
- ⬜ **Job-posting autofill** (`applications.ai.autofill`) — fetch `sourceUrl` (or accept pasted text) → LLM extracts company/role/location/salary/jobDescription → return a prefill object for the Add form; persist raw extraction in `aiMetadata`. UI: enable the disabled "Auto-fill" button in `add-application-sheet.tsx`.
|
||||
- ⬜ **Resume ↔ job match score** (`applications.ai.matchScore`) — compare the linked resume's data against `jobDescription` → write `matchScore` (0–100) + a gap list into `aiMetadata`. UI: surface a score badge on the card + a gaps section in the Detail panel.
|
||||
- ⬜ **Tailor resume for this job** (`applications.ai.tailorResume`) — spawn an agent thread that duplicates the linked resume and edits it for the posting (reuse the agent JSON-patch pipeline); link the new `resumeId` back to the application and log a timeline event.
|
||||
- ⬜ **Cover-letter / follow-up drafting** (`applications.ai.draftMessage`) — generate a cover letter or recruiter follow-up from application + resume + contact context. UI: the disabled "AI actions" block in `application-detail-sheet.tsx`.
|
||||
- ⬜ **Rate-limiting & provider check** — reuse `aiRequestRateLimit` (already exists) and gate on a configured AI provider (`aiProviders` table) before calling any of the above.
|
||||
|
||||
**Suggested order:** autofill (highest user value, self-contained) → match score → draft
|
||||
message → tailor resume (most complex, depends on the agent pipeline).
|
||||
|
||||
---
|
||||
|
||||
## Verification (each phase)
|
||||
- `pnpm --filter @reactive-resume/api test` + `typecheck`, `pnpm --filter web typecheck`, `pnpm exec turbo boundaries`, `pnpm check`.
|
||||
- DB changes: `dotenvx run -f .env.local -- pnpm db:generate` → review SQL → `pnpm db:migrate`.
|
||||
- Manual: `dotenvx run -f .env.local -- pnpm dev` (:3000) → exercise the new surface end-to-end.
|
||||
@@ -1,215 +0,0 @@
|
||||
# Application Tracker REST and MCP Parity
|
||||
|
||||
## Status
|
||||
|
||||
Approved design for implementation planning. Campaign tracking is out of scope because the current app, database schema, changelog, guides, and tests do not ship a campaign field or campaign endpoint.
|
||||
|
||||
## Goal
|
||||
|
||||
Make every shipped Application Tracker capability available outside the web UI through:
|
||||
|
||||
- the public REST/OpenAPI surface under `/api/openapi`;
|
||||
- Reactive Resume MCP tools for agent-only workflows;
|
||||
- the bundled `resume-builder` skill, so agents know the application tracker tools exist.
|
||||
|
||||
The result should let a user track applications through an MCP client alone: create records, import existing rows, move stages, log notes, manage contacts and follow-ups, attach sent documents, run AI copilot actions, and inspect pipeline health.
|
||||
|
||||
## Current Shipped Capabilities
|
||||
|
||||
The current application tracker supports:
|
||||
|
||||
- application records with company, role, location, salary, source, source URL, job description, notes, tags, status, archive state, linked resume, sent resume PDF, cover-letter PDF, contacts, follow-up date/note, applied date, and activity timeline;
|
||||
- fixed stages: `saved`, `applied`, `screening`, `interview`, `offer`, `rejected`;
|
||||
- create, edit, archive/unarchive, mark rejected, move stage, delete;
|
||||
- board, table, search, sort, tag filter, archived toggle, and command-palette open/create shortcuts;
|
||||
- bulk table actions: move stage, add tag, archive, delete;
|
||||
- CSV import with header mapping and a 500-row cap;
|
||||
- insights from aggregate stage/source counts plus client-derived weekly application velocity;
|
||||
- timeline notes and automatic created/stage activity entries;
|
||||
- contacts embedded on each application;
|
||||
- Application Copilot actions: job-posting autofill, resume match score, tailored resume copy, cover-letter draft, follow-up draft;
|
||||
- PDF upload/remove for resume file and cover letter through the generic storage route.
|
||||
|
||||
## Existing API Coverage
|
||||
|
||||
The app already routes most behavior through `packages/api/src/features/applications`:
|
||||
|
||||
- `list`, `getById`, `create`, `import`, `update`, `addNote`, `delete`;
|
||||
- `bulkUpdate`, `bulkDelete`, `stats`, `tags`;
|
||||
- `ai.autofill`, `ai.matchScore`, `ai.draftMessage`, `ai.tailorResume`.
|
||||
|
||||
These procedures are already authenticated and user-scoped through `protectedProcedure` and `applicationService`. The implementation should reuse them instead of adding a parallel controller or new business layer.
|
||||
|
||||
## REST Design
|
||||
|
||||
Keep the existing application procedures and improve parity with the public OpenAPI surface.
|
||||
|
||||
### Public Application Endpoints
|
||||
|
||||
Existing endpoints remain:
|
||||
|
||||
- `GET /applications`
|
||||
- `GET /applications/{id}`
|
||||
- `POST /applications`
|
||||
- `POST /applications/import`
|
||||
- `PUT /applications/{id}`
|
||||
- `POST /applications/{id}/notes`
|
||||
- `DELETE /applications/{id}`
|
||||
- `POST /applications/bulk-update`
|
||||
- `POST /applications/bulk-delete`
|
||||
- `GET /applications/stats`
|
||||
- `GET /applications/tags`
|
||||
- `POST /applications/ai/autofill`
|
||||
- `POST /applications/{id}/ai/match-score`
|
||||
- `POST /applications/{id}/ai/draft-message`
|
||||
- `POST /applications/{id}/ai/tailor-resume`
|
||||
|
||||
Add public document endpoints under the Applications tag:
|
||||
|
||||
- `POST /applications/{id}/documents/{kind}` where `kind` is `resume` or `cover-letter`.
|
||||
- Accept a PDF file via OpenAPI file upload.
|
||||
- Store through the existing storage service.
|
||||
- Update only the matching application fields:
|
||||
- resume: `resumeFileUrl`, `resumeFileName`;
|
||||
- cover letter: `coverLetterUrl`, `coverLetterName`.
|
||||
- Return the updated application.
|
||||
- `DELETE /applications/{id}/documents/{kind}`.
|
||||
- Delete the owned storage object best-effort.
|
||||
- Clear only the matching application fields.
|
||||
- Return the updated application.
|
||||
|
||||
This makes the document upload/remove UI available through public REST without exposing the generic storage route, which remains tagged `Internal` because it also serves profile-picture use cases.
|
||||
|
||||
### REST Behavior Details
|
||||
|
||||
- Use existing ownership checks: every application mutation must be scoped by `userId`.
|
||||
- Reject non-PDF document uploads, matching the UI.
|
||||
- Keep file size at the existing storage limit: 10MB.
|
||||
- Preserve existing delete cleanup for uploaded documents when an application is deleted.
|
||||
- Do not add server-side search/sort endpoints for board/table parity. The UI derives search and sort from `list`, and external clients can do the same. `list` already exposes status, tags, and archive filtering.
|
||||
- Do not add campaign fields or endpoints.
|
||||
- Regenerate or update `docs/spec.json` so the public API reference includes all application endpoints.
|
||||
|
||||
## MCP Design
|
||||
|
||||
Add application tools to `packages/mcp` using the existing metadata pattern:
|
||||
|
||||
- add names to `MCP_TOOL_NAME`;
|
||||
- add schemas/descriptions to `TOOL_META`;
|
||||
- add behavior hints to `TOOL_ANNOTATIONS`;
|
||||
- register handlers in `registerTools`;
|
||||
- let `buildMcpServerCard` derive tool entries from `TOOL_META`;
|
||||
- add focused tests so tool registration, annotations, and server card stay synchronized.
|
||||
|
||||
All MCP handlers should call the injected in-process oRPC `RouterClient`. They should not import database code or reimplement ownership checks.
|
||||
|
||||
### MCP Tools
|
||||
|
||||
Read/discovery:
|
||||
|
||||
- `list_applications`: list applications, with optional `status`, `tags`, and `includeArchived`.
|
||||
- `read_application`: read one full application by ID.
|
||||
- `list_application_tags`: list distinct tags.
|
||||
- `get_application_stats`: return aggregate stage/source counts.
|
||||
|
||||
Write:
|
||||
|
||||
- `create_application`: create an application.
|
||||
- `update_application`: update editable fields, including stage, archive state, contacts, follow-up, tags, linked resume, source URL, job description, notes, and document URLs.
|
||||
- `add_application_note`: append a timeline note.
|
||||
- `delete_application`: permanently delete one application.
|
||||
- `bulk_update_applications`: move, archive/unarchive, or add tags to multiple applications.
|
||||
- `bulk_delete_applications`: permanently delete multiple applications.
|
||||
- `import_applications`: bulk-create parsed application rows, matching the REST `import` shape.
|
||||
- `attach_application_document`: attach a sent resume or cover letter PDF.
|
||||
- `remove_application_document`: remove a sent resume or cover letter PDF.
|
||||
|
||||
AI:
|
||||
|
||||
- `autofill_application_from_job`: extract company, role, location, salary, and job description from a job posting URL or pasted job description.
|
||||
- `score_application_match`: score the linked resume against the job description and persist match metadata.
|
||||
- `tailor_resume_for_application`: create and link a tailored resume copy.
|
||||
- `draft_application_message`: draft either a cover letter or follow-up message.
|
||||
|
||||
### MCP Attachment Input
|
||||
|
||||
`attach_application_document` should support agent-only use. The tool should accept:
|
||||
|
||||
- `id`;
|
||||
- `kind`: `resume` or `cover-letter`;
|
||||
- `fileName`;
|
||||
- `contentType`: `application/pdf`;
|
||||
- `dataBase64`: PDF bytes encoded as base64.
|
||||
|
||||
The handler can decode the base64 and call the same shared application-document service used by the public REST file upload. This avoids forcing MCP clients to provide browser `File` objects while still giving agents the same document capability as the UI.
|
||||
|
||||
### MCP Output Shape
|
||||
|
||||
Prefer JSON text outputs for data-bearing tools. Human-readable success strings are fine for destructive/simple tools only when no structured data is useful. For create/update/read/list/stats/AI tools, return JSON so agents can chain calls reliably.
|
||||
|
||||
### Tool Annotations
|
||||
|
||||
- Read tools: read-only, idempotent, non-destructive.
|
||||
- AI generation tools:
|
||||
- `autofill_application_from_job` is read-only but non-idempotent if it fetches a URL or calls AI.
|
||||
- `draft_application_message` is read-only but non-idempotent.
|
||||
- `score_application_match` and `tailor_resume_for_application` are write, non-idempotent.
|
||||
- Delete tools are destructive.
|
||||
- `update_application`, `bulk_update_applications`, and document remove are write/idempotent when the same input repeats, except tag append remains effectively idempotent because the service de-duplicates tags.
|
||||
- `create_application`, `import_applications`, note append, and document attach are write/non-idempotent.
|
||||
- `openWorldHint` should be `false` for closed account data tools and `true` only for job-posting autofill because it can fetch an external URL.
|
||||
|
||||
## Skill Update
|
||||
|
||||
Update `skills/resume-builder/SKILL.md` in place.
|
||||
|
||||
Add concise guidance that Reactive Resume MCP can manage applications as well as resumes. The skill should tell agents:
|
||||
|
||||
- use application tools when the user wants to track job applications, import a spreadsheet, move an opportunity through stages, add notes/contacts/follow-ups, attach sent documents, or use Application Copilot;
|
||||
- use `list_applications` before acting on existing records;
|
||||
- use `create_application` or `import_applications` for new records;
|
||||
- use `tailor_resume_for_application` after a linked resume and job description exist;
|
||||
- review AI-generated cover letters and follow-ups before sending.
|
||||
|
||||
Do not turn the skill into a full API reference. Keep it short and point agents at MCP tools by name.
|
||||
|
||||
## Documentation Updates
|
||||
|
||||
Update:
|
||||
|
||||
- `docs/guides/using-the-mcp-server.mdx`: add Application Tracker tools, examples, and troubleshooting notes.
|
||||
- `docs/use-cases/api-mcp-resume-automation.mdx`: expand scope from resume-only automation to resume plus application tracking.
|
||||
- `docs/spec.json`: refresh the public OpenAPI spec so docs show application routes.
|
||||
|
||||
The existing job application and CSV guides already describe the UI behavior accurately and should not need broad rewrites.
|
||||
|
||||
## Testing Plan
|
||||
|
||||
Focused tests:
|
||||
|
||||
- API/service tests for attaching/removing resume and cover-letter documents, including ownership and storage cleanup.
|
||||
- DTO/router tests for application document upload constraints if the route logic is not already covered through service tests.
|
||||
- MCP tests that:
|
||||
- every `MCP_TOOL_NAME` has metadata and annotations;
|
||||
- server card includes the new tools;
|
||||
- `registerTools` registers application tools;
|
||||
- one read handler and one write handler call the expected oRPC client procedure;
|
||||
- base64 document attachment rejects non-PDF input.
|
||||
|
||||
Validation commands:
|
||||
|
||||
- `pnpm --filter @reactive-resume/api test`
|
||||
- `pnpm --filter @reactive-resume/mcp test`
|
||||
- `pnpm --filter @reactive-resume/api typecheck`
|
||||
- `pnpm --filter @reactive-resume/mcp typecheck`
|
||||
- `pnpm exec turbo boundaries`
|
||||
- a focused Biome check on touched files; use a non-mutating check first unless formatting edits are intentionally being applied.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Custom stages.
|
||||
- Campaign tracking.
|
||||
- Server-side board/search/sort endpoints.
|
||||
- A standalone application schema resource in MCP.
|
||||
- New dependencies.
|
||||
- Refactoring the application tracker UI.
|
||||
@@ -1,323 +0,0 @@
|
||||
# Monorepo Architecture Reorg Worklog
|
||||
|
||||
Append-only log for implementation, validation, and delegation notes.
|
||||
|
||||
> **Status note:** This file is chronological. Earlier entries may mention paths that were moved by later tasks. Use the latest handoff plus `AGENTS.md` and `docs/contributing/architecture.mdx` for current-state guidance.
|
||||
|
||||
## 2026-05-14
|
||||
|
||||
- Started implementation on branch `feat/explore-hono-orpc-migration`.
|
||||
- Initial dirty files existed before architecture implementation:
|
||||
- `apps/server/package.json`
|
||||
- `package.json`
|
||||
- `packages/api/package.json`
|
||||
- `packages/env/package.json`
|
||||
- `packages/utils/package.json`
|
||||
- `pnpm-lock.yaml`
|
||||
- Created coordination artifacts:
|
||||
- `docs/superpowers/plans/2026-05-14-monorepo-architecture-reorg.md`
|
||||
- `docs/superpowers/handoffs/2026-05-14-monorepo-architecture-reorg.md`
|
||||
- `docs/superpowers/worklogs/2026-05-14-monorepo-architecture-reorg.md`
|
||||
- Task 1 completed:
|
||||
- Created `packages/resume` as `@reactive-resume/resume`.
|
||||
- Moved `packages/utils/src/resume/patch.ts` to `packages/resume/src/patch.ts`.
|
||||
- Moved `packages/utils/src/network-icons.ts` to `packages/resume/src/icons.ts`.
|
||||
- Moved the patch and network icon tests into `packages/resume/src`.
|
||||
- Updated all old `@reactive-resume/utils/resume/patch` and `@reactive-resume/utils/network-icons` imports to `@reactive-resume/resume/patch` and `@reactive-resume/resume/icons`.
|
||||
- Updated package manifests and `pnpm-lock.yaml` for the new package and consumer dependency edges.
|
||||
- Removed `fast-json-patch` from `@reactive-resume/utils` and `@reactive-resume/ai`; it now belongs to `@reactive-resume/resume`.
|
||||
- Did not remove `@reactive-resume/schema` from `@reactive-resume/utils` because DOCX files still import schema and Task 2 owns that extraction.
|
||||
- Validation for Task 1:
|
||||
- Initial `pnpm --filter @reactive-resume/resume test` failed before Vitest because pnpm wanted to refresh modules in a non-TTY.
|
||||
- Retried with `CI=true`; it failed because the new workspace package was not yet represented in the frozen lockfile.
|
||||
- Ran `pnpm install --lockfile-only --no-frozen-lockfile`; completed with no downloads.
|
||||
- A later test run triggered pnpm dependency status install, but `node_modules` had been purged and registry access was blocked by sandbox DNS (`ENOTFOUND` / `fetch failed`).
|
||||
- Ran `pnpm install --no-frozen-lockfile` with network approval to restore `node_modules`; completed.
|
||||
- `pnpm --filter @reactive-resume/resume test` passed: 2 files, 43 tests.
|
||||
- `pnpm --filter @reactive-resume/resume typecheck` passed.
|
||||
- `pnpm --filter @reactive-resume/ai typecheck` passed.
|
||||
- `pnpm --filter @reactive-resume/api typecheck` passed.
|
||||
- `pnpm --filter web typecheck` passed.
|
||||
- Additional direct-consumer checks passed: `pnpm --filter @reactive-resume/db typecheck`, `pnpm --filter @reactive-resume/import typecheck`, and `pnpm --filter @reactive-resume/utils typecheck`.
|
||||
- Focused `pnpm exec biome check ...` initially found import-order/type-import issues; `pnpm exec biome check --write ...` fixed 10 touched files.
|
||||
- Focused `pnpm exec biome check ...` passed afterward on the touched package/source/manifests.
|
||||
- Task 1 review correction:
|
||||
- Spec review found `packages/db` had a type-only manifest dependency on `@reactive-resume/resume`.
|
||||
- Replaced the DB import with a local structural `StoredJsonPatchOperation` type for JSONB column annotations in `packages/db/src/schema/agent.ts`.
|
||||
- Removed `@reactive-resume/resume` from `packages/db/package.json` and `pnpm-lock.yaml`.
|
||||
- Task 1 review validation:
|
||||
- `pnpm --filter @reactive-resume/db typecheck` passed after the DB boundary correction.
|
||||
- `pnpm --filter @reactive-resume/api typecheck` passed.
|
||||
- `pnpm --filter web typecheck` passed.
|
||||
- `pnpm --filter @reactive-resume/resume test` passed: 2 files, 43 tests.
|
||||
- `pnpm --filter @reactive-resume/resume typecheck` passed.
|
||||
- Code-quality review noted unrelated dependency bumps in package manifests; those files were already dirty before this architecture work and were not treated as part of Task 1.
|
||||
- Task 2 completed:
|
||||
- Created `packages/docx` as `@reactive-resume/docx` with source-consumed root export.
|
||||
- Moved DOCX implementation/tests from `packages/utils/src/resume/docx` into `packages/docx/src`.
|
||||
- Updated web DOCX callers to import `buildDocx` from `@reactive-resume/docx`.
|
||||
- Removed the old `@reactive-resume/utils/resume/docx` export.
|
||||
- Removed `docx` and `@reactive-resume/schema` from `@reactive-resume/utils`.
|
||||
- Added `@reactive-resume/docx` as a web dependency.
|
||||
- Task 2 validation:
|
||||
- `pnpm --filter @reactive-resume/docx test` passed: 5 files, 46 tests.
|
||||
- `pnpm --filter @reactive-resume/docx typecheck` passed.
|
||||
- `pnpm --filter @reactive-resume/utils typecheck` passed.
|
||||
- `pnpm --filter web typecheck` passed.
|
||||
- Spec review found only stale coordination text; plan and handoff were corrected.
|
||||
- Task 2 completed:
|
||||
- Created `packages/docx` as `@reactive-resume/docx` with source-consumed root export `"." -> "./src/index.ts"`.
|
||||
- Moved `packages/utils/src/resume/docx/*` to `packages/docx/src/*`, keeping the implementation and tests colocated.
|
||||
- Updated builder DOCX export callers and the export-section test mock from `@reactive-resume/utils/resume/docx` to `@reactive-resume/docx`.
|
||||
- Removed the old `@reactive-resume/utils` `./resume/docx` export.
|
||||
- Removed `docx` and `@reactive-resume/schema` from `@reactive-resume/utils`; the new DOCX package owns those dependencies and depends on `@reactive-resume/utils` only for shared color parsing.
|
||||
- Added `@reactive-resume/docx` as a web dependency and refreshed `pnpm-lock.yaml`.
|
||||
- Validation for Task 2:
|
||||
- Initial `pnpm --filter @reactive-resume/docx test` failed on pnpm's non-TTY dependency-status install guard.
|
||||
- `CI=true pnpm --filter @reactive-resume/docx test` then failed because sandbox DNS blocked registry fetches while pnpm recreated `node_modules`.
|
||||
- Retried `CI=true pnpm --filter @reactive-resume/docx test` with network approval; passed: 5 files, 46 tests.
|
||||
- `pnpm --filter @reactive-resume/docx typecheck` passed.
|
||||
- `pnpm --filter @reactive-resume/utils typecheck` passed.
|
||||
- `pnpm --filter web typecheck` passed.
|
||||
- `pnpm --filter web test -- 'src/routes/builder/$resumeId/-sidebar/right/sections/export.test.tsx'` passed; Vitest reported 78 files and 430 tests.
|
||||
- Focused `pnpm exec biome check --write ...` passed on the moved DOCX package, touched web callers, and package manifests; no fixes were applied.
|
||||
- Task 3 in progress:
|
||||
- Added `packages/pdf/src/browser.tsx` with `createResumePdfBlob({ data, template, resolveSectionTitle })`.
|
||||
- Added `packages/pdf/src/server.tsx` with `createResumePdfFile({ data, filename, template, resolveSectionTitle })`.
|
||||
- Added `@reactive-resume/pdf/browser` and `@reactive-resume/pdf/server` package exports.
|
||||
- Kept Lingui locale loading in `apps/web/src/libs/resume/pdf-document.tsx`; the web wrapper now resolves localized section titles and delegates blob generation to `@reactive-resume/pdf/browser`.
|
||||
- Updated `apps/web/src/libs/resume/pdf-document.server.tsx` to delegate file generation to `@reactive-resume/pdf/server` after resolving localized section titles.
|
||||
- Updated `apps/web/src/components/resume/preview.browser.tsx` so the PDF.js canvas viewer remains in web but PDF blob generation goes through the web-local wrapper.
|
||||
- Updated `apps/server/src/handlers/resume-pdf.tsx` to reuse `@reactive-resume/pdf/server` and preserve the existing `File` response body flow.
|
||||
- Added package helper tests for browser/server generation adapters.
|
||||
- Task 3 validation so far:
|
||||
- Red test: `pnpm --filter @reactive-resume/pdf test -- src/browser.test.tsx src/server.test.tsx` failed because `packages/pdf/src/browser.tsx` and `packages/pdf/src/server.tsx` did not exist.
|
||||
- Interim implementation test exposed Vitest/Rolldown JSX transform limits for newly imported package TSX helpers; helpers now use `createElement` while keeping requested `.tsx` filenames.
|
||||
- `pnpm --filter @reactive-resume/pdf test -- src/browser.test.tsx src/server.test.tsx` passed; Vitest reported 17 files and 139 tests.
|
||||
- Initial focused web preview test failed because the test still mocked the old `useLocalizedResumeDocument` generation path.
|
||||
- Updated `apps/web/src/components/resume/preview.browser.test.tsx` to mock/assert `createResumePdfBlob`.
|
||||
- `pnpm --filter web test -- src/components/resume/preview.browser.test.tsx` passed; Vitest reported 78 files and 430 tests.
|
||||
- `pnpm --filter web test -- 'src/routes/builder/$resumeId/-sidebar/right/sections/export.test.tsx' 'src/routes/$username/-components/public-resume.test.tsx' 'src/routes/$username/-components/pdf-viewer.test.tsx'` passed; Vitest reported 78 files and 430 tests.
|
||||
- Task 4 completed:
|
||||
- Created `packages/mcp` as `@reactive-resume/mcp` with source-consumed exports for the compact public surface and direct server-card/tool/prompt/resource subpaths.
|
||||
- Moved MCP helper implementation and tests from `apps/web/src/routes/mcp/-helpers` into `packages/mcp/src`, then removed the empty web MCP helper route directory.
|
||||
- Updated `apps/server/src/handlers/mcp.ts` and `apps/server/src/handlers/metadata.ts` to import from `@reactive-resume/mcp`, removing the app-to-app source imports from `apps/server` into `apps/web`.
|
||||
- Preserved in-process MCP execution through the injected oRPC `RouterClient`; no HTTP RPC calls were introduced inside the server process.
|
||||
- Renamed MCP tool values to canonical unprefixed snake_case names: `list_resumes`, `list_resume_tags`, `read_resume`, `get_resume_analysis`, `create_resume`, `import_resume`, `duplicate_resume`, `apply_resume_patch`, `update_resume`, `delete_resume`, `lock_resume`, `unlock_resume`, and `get_resume_statistics`.
|
||||
- Updated prompts, server instructions, server-card metadata, and tests to use the canonical names and removed `reactive_resume_*` aliases/instructions.
|
||||
- Added `packages/ai/src/tools/resume-tool-contracts.ts` with the shared JSON Patch operations contract and reused it from `packages/ai/src/tools/patch-resume.ts` and MCP patch schemas.
|
||||
- Updated package manifests and `pnpm-lock.yaml`; `web` no longer owns `@modelcontextprotocol/sdk`, and `server` now depends on `@reactive-resume/mcp`.
|
||||
- Validation for Task 4:
|
||||
- Initial `pnpm --filter @reactive-resume/mcp test` reported no matching package before creation.
|
||||
- After package creation, `pnpm --filter @reactive-resume/mcp test` hit pnpm's non-TTY dependency-status install guard.
|
||||
- `CI=true pnpm --filter @reactive-resume/mcp test` then failed with a frozen-lockfile mismatch after moving dependencies.
|
||||
- Ran `pnpm install --lockfile-only --no-frozen-lockfile`; completed.
|
||||
- A later `CI=true pnpm --filter @reactive-resume/mcp test` failed because sandbox DNS blocked registry fetches while pnpm restored `node_modules`.
|
||||
- Retried `CI=true pnpm --filter @reactive-resume/mcp test` with network approval; passed: 4 files, 29 tests.
|
||||
- `pnpm --filter @reactive-resume/mcp typecheck` initially failed because the package type graph reaches API/auth TSX and tests had optional text access; fixed by enabling JSX in the package tsconfig and narrowing test content access.
|
||||
- Final validation commands passed: `pnpm --filter @reactive-resume/mcp test`, `pnpm --filter @reactive-resume/mcp typecheck`, `pnpm --filter server typecheck`, `pnpm --filter @reactive-resume/ai typecheck`, focused `pnpm exec biome check ...`, and `rg -n "\\.\\./\\.\\./\\.\\./web/src|apps/web/src|web/src/routes/mcp" apps/server/src packages/mcp/src`.
|
||||
- Initial final validation found `pnpm exec biome check ...` import/format issues in touched PDF/web files and type errors around the React element type passed into `pdf(...)` and `renderToBuffer(...)`.
|
||||
- Added narrow render-boundary casts using `Parameters<typeof pdf>[0]` and `Parameters<typeof renderToBuffer>[0]`.
|
||||
- Ran `pnpm exec biome check --write ...`; Biome fixed 4 touched files.
|
||||
- `pnpm --filter @reactive-resume/pdf test` passed: 17 files, 139 tests.
|
||||
- `pnpm --filter @reactive-resume/pdf typecheck` passed.
|
||||
- `pnpm --filter web typecheck` passed.
|
||||
- `pnpm --filter server typecheck` passed.
|
||||
- `pnpm exec biome check packages/pdf/src/browser.tsx packages/pdf/src/server.tsx packages/pdf/src/browser.test.tsx packages/pdf/src/server.test.tsx packages/pdf/package.json apps/web/src/libs/resume/pdf-document.tsx apps/web/src/libs/resume/pdf-document.server.tsx apps/web/src/components/resume/preview.browser.tsx apps/web/src/components/resume/preview.browser.test.tsx apps/server/src/handlers/resume-pdf.tsx docs/superpowers/plans/2026-05-14-monorepo-architecture-reorg.md docs/superpowers/worklogs/2026-05-14-monorepo-architecture-reorg.md docs/superpowers/handoffs/2026-05-14-monorepo-architecture-reorg.md` passed.
|
||||
- `pnpm --filter web test -- src/components/resume/preview.browser.test.tsx` passed; Vitest reported 78 files and 430 tests.
|
||||
- `pnpm --filter web test -- 'src/routes/builder/$resumeId/-sidebar/right/sections/export.test.tsx' 'src/routes/$username/-components/public-resume.test.tsx' 'src/routes/$username/-components/pdf-viewer.test.tsx'` passed; Vitest reported 78 files and 430 tests.
|
||||
- Task 4 review correction:
|
||||
- Spec review found stale MCP guide references to removed prefixed tool names and the unavailable `tailor_resume` prompt.
|
||||
- Updated `docs/guides/using-the-mcp-server.mdx` to document canonical tool names such as `list_resumes`, `read_resume`, and `apply_resume_patch`, and removed `tailor_resume`.
|
||||
- Removed the last code-side literal reference to the old prefixed tool-name family from `packages/mcp/src/tool-annotations.test.ts`.
|
||||
- Re-ran `rg "reactive_resume_|tailor_resume|web/src/routes/mcp|apps/web/src|\\.\\./\\.\\./\\.\\./web" apps/server packages/mcp packages/ai/src docs/guides/using-the-mcp-server.mdx -n`; no matches.
|
||||
- Re-ran `pnpm --filter @reactive-resume/mcp test`, `pnpm --filter @reactive-resume/mcp typecheck`, `pnpm --filter server typecheck`, `pnpm --filter @reactive-resume/ai typecheck`, and focused `pnpm exec biome check ...`; all passed.
|
||||
- Task 5 review correction:
|
||||
- Spec review found stale root project guidance that still pointed API work at `packages/api/src/routers/*` and `packages/api/src/services/*`.
|
||||
- Updated `AGENTS.md` so the normative guidance now points API work at `packages/api/src/features/*` and documents the new `resume`, `docx`, `pdf`, and `mcp` package ownership boundaries.
|
||||
- Re-ran `pnpm --filter @reactive-resume/api test`: 19 files, 155 tests passed.
|
||||
- Re-ran `pnpm --filter @reactive-resume/api typecheck`, `pnpm --filter server typecheck`, and `pnpm --filter web typecheck`; all passed.
|
||||
- Re-ran focused `pnpm exec biome check ...` on `AGENTS.md`, API/server/web touchpoints, and coordination docs; passed.
|
||||
- Task 7 slice 1 review correction:
|
||||
- Spec review found the moved public resume route missing its expected `ssr: "data-only"` setting and the builder preview route missing `ssr: false`.
|
||||
- Restored `ssr: "data-only"` in `apps/web/src/routes/$username/$slug.tsx`.
|
||||
- Restored `ssr: false` in `apps/web/src/routes/builder/$resumeId/index.tsx`.
|
||||
- Updated `AGENTS.md` and the handoff notes to point browser-only resume preview/public viewer code at `apps/web/src/features/resume/*` instead of the removed `components/resume` and `libs/resume` paths.
|
||||
- Task 5 completed:
|
||||
- Moved API routers and service/helpers from the old technical-layer folders into `packages/api/src/features/*`.
|
||||
- Agent is now feature-owned under `features/agent`, with procedure modules for `threads`, `messages`, `attachments`, and `actions`, run-state in `runs`, tool construction in `tools`, and remaining shared runtime orchestration in `service.ts`.
|
||||
- Finer-grained follow-up: `features/agent/service.ts` still contains shared thread/message/action orchestration because the run lifecycle, message persistence, attachment linking, and patch transaction helpers are tightly coupled; splitting that service body further should be a dedicated follow-up with behavior-specific tests.
|
||||
- Resume is now feature-owned under `features/resume`, with procedure modules for `crud`, `tags`, `statistics`, `analysis`, `event-router`, `sharing`, and `export`; access helpers and event publication moved under the same feature.
|
||||
- Finer-grained follow-up: `features/resume/service.ts` remains the DB-backed facade for shared transaction helpers, update notifications, access/statistics coupling, and storage cleanup. The public procedure surface is capability-split, and further DB-service extraction should preserve the existing transaction and event behavior.
|
||||
- Moved the authenticated PDF download procedure from `apps/server/src/handlers/resume-pdf.tsx` into `packages/api/src/features/resume/export.ts`; it now calls `@reactive-resume/pdf/server`.
|
||||
- Removed the old API package `./services/*` and `./helpers/*` wildcard exports.
|
||||
- Added explicit API runtime/type exports for `./features/storage`, `./features/resume`, `./features/resume/export`, and `./features/flags`.
|
||||
- Updated `apps/server` to import storage/PDF runtime surfaces from explicit API feature exports.
|
||||
- Updated `apps/web` to keep API imports type-only, including the `FeatureFlags` type from `@reactive-resume/api/features/flags`.
|
||||
- Added `@reactive-resume/pdf` as an API package dependency and refreshed `pnpm-lock.yaml`.
|
||||
- Validation for Task 5:
|
||||
- Initial `pnpm --filter @reactive-resume/api typecheck` failed because pnpm attempted a non-TTY dependency-status install after package metadata changes.
|
||||
- Retried with `CI=true`; sandbox DNS blocked registry fetches while rebuilding `node_modules`.
|
||||
- Retried `CI=true pnpm --filter @reactive-resume/api typecheck` with network approval; dependency restoration completed and the first compiler pass found moved import paths that needed correction.
|
||||
- `pnpm --filter @reactive-resume/api test` initially failed because the moved agent service test mocked the new `./service` module instead of its relocated AI/resume dependencies; fixed the mocks.
|
||||
- Final focused validations passed: `pnpm --filter @reactive-resume/api test`, `pnpm --filter @reactive-resume/api typecheck`, `pnpm --filter server typecheck`, `pnpm --filter web typecheck`, and `pnpm --filter @reactive-resume/mcp typecheck`.
|
||||
- Focused Biome check initially reported import-order/format issues; `pnpm exec biome check --write ...` fixed 7 touched API files.
|
||||
- Re-ran focused Biome check after the write pass; it passed.
|
||||
- Repo scans passed for no remaining `@reactive-resume/api/services/*` or `@reactive-resume/api/helpers/*` imports outside `packages/api`, no internal `../services` or `../helpers` imports, and no `./services/*` or `./helpers/*` wildcard exports in `packages/api/package.json`.
|
||||
- Task 6 completed:
|
||||
- Reorganized `apps/server/src` into runtime adapter areas: `http`, `rpc`, `mcp`, `openapi`, `static`, and `startup`.
|
||||
- Moved route registration into `apps/server/src/http/app.ts` and kept `apps/server/src/index.ts` as the process entrypoint that runs startup checks and starts Hono.
|
||||
- Moved common response/cookie helpers to `http/headers.ts`, auth and health HTTP handlers to `http`, oRPC request handling and locale extraction to `rpc`, MCP auth/server setup/transport handling to `mcp`, OpenAPI and well-known metadata to `openapi`, uploads/schema/web-dist serving to `static`, and migrations/local-storage lifecycle checks to `startup`.
|
||||
- Kept the public route order and paths from the previous `index.ts`, including serving `apps/web/dist`.
|
||||
- Preserved explicit API runtime imports only: `@reactive-resume/api/routers`, `@reactive-resume/api/features/storage`, and `@reactive-resume/api/features/resume/export`.
|
||||
- Validation for Task 6:
|
||||
- Baseline before moving files: `pnpm --filter server test` passed: 1 file, 1 test.
|
||||
- Interim validation after moving files: `pnpm --filter server typecheck` passed.
|
||||
- Interim validation after moving files: `pnpm --filter server test` passed: 1 file, 1 test.
|
||||
- Final `pnpm --filter server test` passed: 1 file, 1 test.
|
||||
- Final `pnpm --filter server typecheck` passed.
|
||||
- Final `pnpm --filter @reactive-resume/api typecheck` passed.
|
||||
- Focused `pnpm exec biome check apps/server/src docs/superpowers/plans/2026-05-14-monorepo-architecture-reorg.md docs/superpowers/worklogs/2026-05-14-monorepo-architecture-reorg.md docs/superpowers/handoffs/2026-05-14-monorepo-architecture-reorg.md` initially found import-order issues in `apps/server/src/http/app.ts` and `apps/server/src/mcp/handler.ts`; after manual import ordering fixes, the same command passed.
|
||||
- `rg -n "apps/web/src|from ['\"][^'\"]*web/src|@reactive-resume/api/services/" apps/server/src` returned no matches.
|
||||
- `rg -n "@reactive-resume/api/services/" apps/server packages/api apps/web packages` returned no matches.
|
||||
- `rg -n "from ['\"][^'\"]*apps/web/src|from ['\"][^'\"]*web/src|apps/web/src" apps/server` returned no matches.
|
||||
- Task 7 slice 1 completed:
|
||||
- Moved `apps/web/src/components/resume/builder-resume-draft.ts` to `apps/web/src/features/resume/builder/draft.ts` and updated builder/dialog consumers to import the feature-owned draft store directly.
|
||||
- Moved builder preview files and colocated tests from `apps/web/src/components/resume` to `apps/web/src/features/resume/preview`, including `preview.tsx`, `preview.browser.tsx`, `preview.shared.tsx`, `pdf-canvas.tsx`, and preview shared tests.
|
||||
- Moved dashboard resume thumbnail sizing helpers to `features/resume/preview/resume-thumbnail.shared.ts` and moved PDF.js thumbnail rendering into `features/resume/preview/pdf-thumbnail.ts`, so direct `pdfjs-dist` usage stays under `features/resume`.
|
||||
- Moved web-local PDF document wrappers from `apps/web/src/libs/resume/pdf-document*.tsx` to `apps/web/src/features/resume/export/pdf-document*.tsx`; the wrappers still resolve localized section titles in web and call `@reactive-resume/pdf/browser` or `@reactive-resume/pdf/server`.
|
||||
- Moved public resume route components and tests from `apps/web/src/routes/$username/-components` to `apps/web/src/features/resume/public`, including `public-resume.tsx`, `pdf-viewer.tsx`, `pdf-viewer.css`, and their tests.
|
||||
- Updated the public resume route to lazy-load `features/resume/public/public-resume` while preserving its loader, redirects, metadata, and route settings.
|
||||
- Removed the now-empty `apps/web/src/components/resume` and `apps/web/src/routes/$username/-components` directories.
|
||||
- Broader Task 7 remains open for command palette, theme, locale, user, auth, settings, and Task 8 dialog-registry work.
|
||||
- Task 7 slice 2 completed:
|
||||
- Moved command palette implementation/tests to `apps/web/src/features/command-palette`.
|
||||
- Moved theme provider, combobox, toggle button, and tests to `apps/web/src/features/theme`.
|
||||
- Moved locale combobox implementation/tests to `apps/web/src/features/locale`.
|
||||
- Moved user dropdown implementation to `apps/web/src/features/user`.
|
||||
- Moved auth layout, page UI, and social auth component into `apps/web/src/features/auth`; auth route files now keep route guards/search validation and compose feature pages.
|
||||
- Moved settings page UI and authentication/integration subcomponents into `apps/web/src/features/settings`; settings route files now keep dashboard headers and compose feature pages.
|
||||
- Left `apps/web/src/routes/dashboard/settings/job-search.tsx` route-only because it is already a redirect shim.
|
||||
- `pnpm --filter web test -- src/features/command-palette src/features/theme src/features/locale` passed; Vitest reported 74 files and 402 tests.
|
||||
- `pnpm --filter web typecheck` passed.
|
||||
- Focused Biome passed on the moved shell/auth/settings files and their route/import consumers; it checked 64 files with no fixes applied after a formatting write pass.
|
||||
- `rg -n "@/components/(command-palette|theme|locale|user)" apps/web/src` and `rg -n "components/(command-palette|theme|locale|user)" apps/web/src` returned no matches.
|
||||
- Task 8 completed:
|
||||
- Added dialog schema registries under `apps/web/src/dialogs/{auth,api-key,resume}/schema.ts` and composed them through `apps/web/src/dialogs/schemas.ts`.
|
||||
- Added domain renderer registries under `apps/web/src/dialogs/{auth,api-key,resume}/registry.tsx`, with shared renderer helper types in `apps/web/src/dialogs/renderer-registry.ts`.
|
||||
- Reduced `apps/web/src/dialogs/store.ts` to the single global dialog runtime/store while preserving typed `openDialog(type, data)` and `DialogProps<T>` exports.
|
||||
- Reduced `apps/web/src/dialogs/manager.tsx` to render via composed registries instead of directly importing all auth/API-key/resume dialog components.
|
||||
- Added a store test assertion that the central schema union is built from the domain schema registries.
|
||||
- Validation for Task 8 so far:
|
||||
- Red check: `pnpm --filter web test -- src/dialogs/store.test.ts` failed because `./schemas` did not exist.
|
||||
- `pnpm --filter web test -- src/dialogs/store.test.ts` passed after the registry implementation.
|
||||
- Initial `pnpm --filter web typecheck` found renderer-entry variance errors; fixed by typing renderer entries as an existential union of concrete dialog renderers.
|
||||
- `pnpm --filter web typecheck` passed after the renderer type fix.
|
||||
- `pnpm --filter web test -- src/dialogs/store.test.ts src/dialogs/resume/template/data.test.ts src/dialogs/resume/template/gallery.test.tsx` passed; Vitest reported 74 files and 403 tests.
|
||||
- Final `pnpm --filter web typecheck` passed.
|
||||
- Final `pnpm --filter web test -- src/dialogs/store.test.ts src/dialogs/resume/template/data.test.ts src/dialogs/resume/template/gallery.test.tsx` passed; Vitest reported 74 files and 403 tests.
|
||||
- Focused `pnpm exec biome check ...` passed on the touched dialog registry/store/manager files and Task 8 coordination docs; Biome checked 12 TypeScript files and ignored Markdown.
|
||||
- `rg -n "ts-pattern|Create[A-Za-z]+Dialog|Update[A-Za-z]+Dialog|TemplateGalleryDialog|from \"\\./(api-key|auth|resume)" apps/web/src/dialogs/manager.tsx` returned no matches.
|
||||
- `rg -n "@reactive-resume/schema/resume/data|awardItemSchema|certificationItemSchema|coverLetterItemSchema|customSectionSchema|educationItemSchema|experienceItemSchema|interestItemSchema|languageItemSchema|profileItemSchema|projectItemSchema|publicationItemSchema|referenceItemSchema|skillItemSchema|summaryItemSchema|volunteerItemSchema|z\\.object\\(\\{ type" apps/web/src/dialogs/store.ts` returned no matches.
|
||||
- Task 9 completed:
|
||||
- Inspected installed Turbo and Biome versions and local schemas/docs:
|
||||
- `pnpm exec turbo --version`: `2.9.12`
|
||||
- `pnpm exec biome --version`: `2.4.15`
|
||||
- `node_modules/.pnpm/turbo@2.9.12/node_modules/turbo/schema.json` includes root `boundaries`, `dependencies`, `dependents`, `implicitDependencies`, and workspace `tags`.
|
||||
- Context7/Biome docs and `node_modules/@biomejs/biome/configuration_schema.json` confirm local `.grit` plugins can be loaded through `biome.json` `plugins`.
|
||||
- Updated `turbo.json` with executable boundaries:
|
||||
- Deny dependencies on app workspaces `web` and `server`, preventing package-to-app and app-to-app imports.
|
||||
- Add root test-tool implicit dependencies for `vitest`, `@testing-library/jest-dom`, `@testing-library/react`, and `@testing-library/user-event` so test imports do not require duplicating root test devDependencies in every workspace package.
|
||||
- Add tag rules for app, server, browser, universal, domain, and UI layers.
|
||||
- Added workspace `turbo.json` files with `extends: ["//"]` and boundary tags for both apps and all packages.
|
||||
- Added `@boundaries-ignore root shared Vitest config` to every workspace `vitest.config.ts` import of `../../vitest.shared`; this is the only allowed cross-package source import left for the shared root test config.
|
||||
- Updated `biome.json` with `style.noRestrictedImports` patterns for:
|
||||
- `@reactive-resume/*/src/**`
|
||||
- `apps/**`
|
||||
- `packages/**`
|
||||
- Added `tooling/grit/no-cross-workspace-src-imports.grit` and registered it in `biome.json` as a second layer for import/export/dynamic import sources that reference another workspace's `src` tree.
|
||||
- Removed the cross-workspace `@reactive-resume/ui/* -> ../../packages/ui/src/*` path alias from `apps/web/tsconfig.json`; web now relies on the UI package export map.
|
||||
- Files changed for Task 9:
|
||||
- `turbo.json`
|
||||
- `apps/server/turbo.json`
|
||||
- `apps/web/turbo.json`
|
||||
- `apps/web/tsconfig.json`
|
||||
- `biome.json`
|
||||
- `tooling/grit/no-cross-workspace-src-imports.grit`
|
||||
- `apps/server/vitest.config.ts`
|
||||
- `apps/web/vitest.config.ts`
|
||||
- `packages/ai/turbo.json`
|
||||
- `packages/ai/vitest.config.ts`
|
||||
- `packages/api/turbo.json`
|
||||
- `packages/api/vitest.config.ts`
|
||||
- `packages/auth/turbo.json`
|
||||
- `packages/auth/vitest.config.ts`
|
||||
- `packages/config/turbo.json`
|
||||
- `packages/config/vitest.config.ts`
|
||||
- `packages/db/turbo.json`
|
||||
- `packages/db/vitest.config.ts`
|
||||
- `packages/docx/turbo.json`
|
||||
- `packages/docx/vitest.config.ts`
|
||||
- `packages/email/turbo.json`
|
||||
- `packages/email/vitest.config.ts`
|
||||
- `packages/env/turbo.json`
|
||||
- `packages/env/vitest.config.ts`
|
||||
- `packages/fonts/turbo.json`
|
||||
- `packages/fonts/vitest.config.ts`
|
||||
- `packages/import/turbo.json`
|
||||
- `packages/import/vitest.config.ts`
|
||||
- `packages/mcp/turbo.json`
|
||||
- `packages/mcp/vitest.config.ts`
|
||||
- `packages/pdf/turbo.json`
|
||||
- `packages/pdf/vitest.config.ts`
|
||||
- `packages/resume/turbo.json`
|
||||
- `packages/resume/vitest.config.ts`
|
||||
- `packages/runtime-externals/turbo.json`
|
||||
- `packages/schema/turbo.json`
|
||||
- `packages/schema/vitest.config.ts`
|
||||
- `packages/scripts/turbo.json`
|
||||
- `packages/ui/turbo.json`
|
||||
- `packages/ui/vitest.config.ts`
|
||||
- `packages/utils/turbo.json`
|
||||
- `packages/utils/vitest.config.ts`
|
||||
- `docs/superpowers/worklogs/2026-05-14-monorepo-architecture-reorg.md`
|
||||
- `docs/superpowers/handoffs/2026-05-14-monorepo-architecture-reorg.md`
|
||||
- Validation for Task 9:
|
||||
- Baseline `pnpm exec turbo boundaries` failed with 321 issues before configuration, mostly undeclared root test-tool imports and shared root Vitest config imports.
|
||||
- Intermediate `pnpm exec turbo boundaries` passed after adding implicit dependencies and the Vitest config boundary-ignore comments.
|
||||
- Final `pnpm exec turbo boundaries` passed: checked 660 files in 20 packages, no issues found.
|
||||
- `pnpm exec biome check biome.json turbo.json tooling/grit/no-cross-workspace-src-imports.grit apps/web/tsconfig.json apps/server/turbo.json apps/web/turbo.json packages/*/turbo.json` passed: checked 24 files, no fixes applied.
|
||||
- `pnpm --filter web typecheck` passed.
|
||||
- `pnpm --filter @reactive-resume/api typecheck` passed.
|
||||
- `pnpm --filter server typecheck` passed.
|
||||
- Remaining risks for Task 9:
|
||||
- The current Turbo tags are coarse layer tags. They enforce the current obvious app/browser/server/domain/UI direction, but future package splits may need more granular tags or per-package dependency/dependent rules.
|
||||
- The GritQL plugin is intentionally narrow: it only covers code import/export sources. JSON/tsconfig source-path aliases are covered separately by `noRestrictedImports`, manual scans, and the removal of the web-to-UI source alias.
|
||||
- The shared root `vitest.shared` import remains intentionally ignored in workspace Vitest configs. Moving that helper into a package would remove the ignore comments but would be a broader test-infra reorg.
|
||||
- Task 10 completed:
|
||||
- Updated `AGENTS.md` with executable boundary rules, package-role/runtime tags, a placement decision tree, and the `pnpm exec turbo boundaries` validation command.
|
||||
- Replaced the stale public architecture guide at `docs/contributing/architecture.mdx`, which still described an old single-`src` layout, with the current monorepo runtime map, workspace ownership table, boundary rules, feature placement guide, API layout, PDF/DOCX boundaries, and MCP boundary.
|
||||
- Added `docs/adr/0001-workspace-boundaries.md` with the accepted decision, context, consequences, and rejected alternatives.
|
||||
- Left MCP and AI Agent user guides unchanged in this task because Task 4 already updated MCP tool names, and `docs/guides/ai-agent-tools.mdx` already documents the canonical `read_resume` and `apply_resume_patch` tool names.
|
||||
- Did not add local package READMEs; `AGENTS.md`, the architecture guide, the ADR, `turbo.json`, workspace `turbo.json` tags, and `biome.json` are the source of truth.
|
||||
- Validation for Task 10:
|
||||
- `rg -n "src/integrations|src/components/resume|packages/api/src/services|packages/api/src/helpers|reactive_resume_|tailor_resume|Radix UI|ORPC" AGENTS.md docs/contributing/architecture.mdx docs/adr/0001-workspace-boundaries.md docs/guides/using-the-mcp-server.mdx docs/guides/ai-agent-tools.mdx` returned only the expected `ORPCClient` labels in the architecture mermaid diagram.
|
||||
- `pnpm exec biome check AGENTS.md docs/contributing/architecture.mdx docs/adr/0001-workspace-boundaries.md docs/superpowers/plans/2026-05-14-monorepo-architecture-reorg.md docs/superpowers/worklogs/2026-05-14-monorepo-architecture-reorg.md docs/superpowers/handoffs/2026-05-14-monorepo-architecture-reorg.md` did not process files because this Biome config ignores Markdown/MDX; there is no dedicated docs check script in `package.json`.
|
||||
- Task 11 completed:
|
||||
- Ran `pnpm install --lockfile-only`; first pass was already up to date, and a later pass updated `pnpm-lock.yaml` after removing stale app dependencies.
|
||||
- Ran `pnpm install` after manifest cleanup so pnpm's dependency-status check would stop trying to purge modules from non-TTY subcommands.
|
||||
- Cleaned up final `pnpm knip` findings:
|
||||
- Deleted unused `apps/web/src/features/resume/export/pdf-document.server.tsx`.
|
||||
- Removed unused app dependencies from `apps/server/package.json` and `apps/web/package.json`.
|
||||
- Removed unused internal exports in dialog registries, startup checks, API AI helpers, and agent tool helpers.
|
||||
- Reattached `pdfExportRateLimit` to `downloadResumePdfProcedure`.
|
||||
- Removed stale jobs rate-limit middleware exports that no current router uses.
|
||||
- `pnpm knip` now exits successfully with only the existing configuration hint: `src/server.ts apps/web knip.json Refine entry pattern (no matches)`.
|
||||
- Final validation for Task 11:
|
||||
- `pnpm install --lockfile-only` passed.
|
||||
- `pnpm exec biome check .` passed: 756 files checked, no fixes applied.
|
||||
- `pnpm exec turbo boundaries` passed: 659 files checked in 20 packages, no issues found.
|
||||
- `pnpm knip` passed with one configuration hint and no unused files/dependencies/exports.
|
||||
- `pnpm typecheck` passed: 18 successful tasks.
|
||||
- `pnpm test` passed: 18 successful tasks; notable totals include web 74 files/403 tests, UI 42 files/421 tests, API 19 files/155 tests, PDF 17 files/139 tests.
|
||||
- `pnpm build` passed: web and server builds completed.
|
||||
@@ -1,55 +0,0 @@
|
||||
# MCP and Directory Submissions Worklog
|
||||
|
||||
Date: 2026-07-07
|
||||
|
||||
## MCP registries
|
||||
|
||||
Submitted:
|
||||
|
||||
- [mcpservers.org](https://mcpservers.org/submit) - submitted free listing for `Reactive Resume`.
|
||||
- [mcp.so](https://mcp.so/) - submitted via GitHub issue: [chatmcp/mcpso#3058](https://github.com/chatmcp/mcpso/issues/3058). The issue was reformatted to match recent submissions.
|
||||
- [mcp.directory](https://mcp.directory/submit) - submitted the GitHub repository and MCP summary.
|
||||
- [Influzer MCP Directory](https://www.influzer.ai/mcp/submit) - submitted via form. The form reset after submission but did not show a confirmation message.
|
||||
- [Official MCP Registry](https://registry.modelcontextprotocol.io/) - published `io.github.amruthpillai/reactive-resume` version `5.2.2`; verified active/latest at [`/v0.1/servers/io.github.amruthpillai%2Freactive-resume/versions/latest`](https://registry.modelcontextprotocol.io/v0.1/servers/io.github.amruthpillai%2Freactive-resume/versions/latest).
|
||||
|
||||
Worth doing next:
|
||||
|
||||
- [Smithery](https://smithery.ai/docs/build/publish) - supports direct URL publishing for remote Streamable HTTP MCP servers. Needs account/auth flow.
|
||||
- [McpMux](https://mcpmux.com/) - registry is open source and accepts PRs; see [server definitions](https://mcpmux.com/docs/server-definitions/).
|
||||
- [AgenticSkills](https://agenticskills.io/mcp) - has a `Submit MCP` flow, likely manual/account gated.
|
||||
- [Glama](https://glama.ai/mcp/servers) - has an `Add Server` button, but the automated session could not open the flow.
|
||||
|
||||
Skip for now:
|
||||
|
||||
- [PulseMCP](https://www.pulsemcp.com/submit) - blocked by Cloudflare in automation and says it ingests the Official MCP Registry.
|
||||
- [MCP Market](https://mcpmarket.com/submit) - already has an unofficial RxResume listing at [mcpmarket.com/server/rxresume](https://mcpmarket.com/server/rxresume); handle as a claim/update, not a duplicate.
|
||||
- [MCP Server Hub](https://mcpserverhub.net/submit) - free path appears backlink-gated.
|
||||
- [Cline MCP Marketplace](https://github.com/cline/mcp-marketplace) - asks submitters to verify installation with Cline first.
|
||||
|
||||
## SaaS, free, and open-source directories
|
||||
|
||||
Already listed:
|
||||
|
||||
- [Product Hunt](https://www.producthunt.com/products/reactive-resume)
|
||||
- [AlternativeTo](https://alternativeto.net/software/reactive-resume/)
|
||||
- [SaaSHub](https://www.saashub.com/reactive-resume)
|
||||
- [LibHunt](https://www.libhunt.com/r/Reactive-Resume/Reactive-Resume)
|
||||
- [SourceForge](https://sourceforge.net/projects/reactive-resume.mirror/)
|
||||
- [Docker Hub](https://hub.docker.com/r/amruthpillai/reactive-resume)
|
||||
|
||||
Worth doing next:
|
||||
|
||||
- [Awesome-Selfhosted](https://github.com/awesome-selfhosted/awesome-selfhosted) - PR-based; add data in [awesome-selfhosted-data](https://github.com/awesome-selfhosted/awesome-selfhosted-data/blob/master/CONTRIBUTING.md).
|
||||
- [selfh.st Apps](https://selfh.st/apps/) - self-hosted app directory with a submit-content link.
|
||||
- [OpenAlternative](https://openalternative.co/) - open-source alternative directory; Reactive Resume was not found in the page search.
|
||||
- [Free Software Directory](https://directory.fsf.org/wiki/Main_Page) - good fit if FSF-free-software expectations are met.
|
||||
- [Open Source Software Directory](https://opensourcesoftwaredirectory.com/) - broad free/open-source software directory.
|
||||
- [Uneed](https://www.uneed.best/submit-a-tool) - account required after preview; broad product launch directory.
|
||||
- [Fazier](https://fazier.com/submit) - free startup/product submission flow.
|
||||
- [StartupBase](https://startupbase.io/) - account-gated startup/product launch directory.
|
||||
|
||||
Low priority:
|
||||
|
||||
- [BetaList](https://betalist.com/criteria) - early-stage startup positioning is weaker for an already-established open-source app.
|
||||
- G2/Capterra/GetApp-style review sites - useful only if there is a review-generation plan.
|
||||
- AI-only directories - submit only if positioning is specifically around the AI Agent/MCP workflow.
|
||||
@@ -1,259 +0,0 @@
|
||||
# Plan 001: Characterization tests for the resume service
|
||||
|
||||
> **Executor instructions**: Follow this plan step by step. Run every
|
||||
> verification command and confirm the expected result before moving to the
|
||||
> next step. If anything in the "STOP conditions" section occurs, stop and
|
||||
> report — do not improvise. When done, update the status row for this plan
|
||||
> in `plans/README.md`.
|
||||
>
|
||||
> **Drift check (run first)**: `git diff --stat 73daf22b2..HEAD -- packages/api/src/features/resume/service.ts`
|
||||
> If `service.ts` changed since this plan was written, compare the "Current
|
||||
> state" excerpts against the live code before proceeding; on a mismatch,
|
||||
> treat it as a STOP condition.
|
||||
|
||||
## Status
|
||||
|
||||
- **Priority**: P1
|
||||
- **Effort**: M
|
||||
- **Risk**: LOW
|
||||
- **Depends on**: none
|
||||
- **Category**: tests
|
||||
- **Planned at**: commit `73daf22b2`, 2026-07-08
|
||||
|
||||
## Why this matters
|
||||
|
||||
`packages/api/src/features/resume/service.ts` is 772 lines and owns every
|
||||
resume mutation — `create`, `update`, `patch` (JSON Patch application),
|
||||
`delete`, `duplicate`, `setLocked`, `setPassword`, `removePassword`,
|
||||
`verifyPassword`, `getBySlug`, plus version-history snapshotting. It has **no
|
||||
`service.test.ts`**. The only coverage is Playwright e2e, which exercises the
|
||||
happy path through the UI and cannot assert error codes, lock enforcement, or
|
||||
snapshot throttling in isolation. This service also has high churn (v5.2.0
|
||||
added undo/redo + version history). Characterization tests here (a) catch
|
||||
regressions in CRUD/patch/lock/password behavior before users do, and (b) are
|
||||
a prerequisite for Plan 003 (which changes not-found behavior in this file)
|
||||
and Plan 007 (template refactor) — you cannot safely refactor code that has no
|
||||
behavioral net under it.
|
||||
|
||||
The goal is **characterization tests**: capture what the code does today,
|
||||
locking in current behavior so later changes are deliberate, not accidental.
|
||||
|
||||
## Current state
|
||||
|
||||
- `packages/api/src/features/resume/service.ts` — the service under test.
|
||||
Exports a `resumeService` object literal whose methods each take an `input`
|
||||
object and call the mocked `db`. Key behaviors to pin down:
|
||||
- `update` (lines ~555–635): reads `isLocked`; throws `ORPCError("RESUME_LOCKED")`
|
||||
if locked; on a successful `UPDATE ... RETURNING`, if `!resume` throws
|
||||
`ORPCError("NOT_FOUND")`; maps a unique-constraint violation on
|
||||
`resume_slug_user_id_unique` to `ORPCError("RESUME_SLUG_ALREADY_EXISTS")`.
|
||||
- `setLocked` (lines ~663–679), `setPassword` (~681–699), `removePassword`
|
||||
(~726–742): each runs an `UPDATE ... RETURNING`, then `if (!resume) return;`
|
||||
(silent no-op when no row matches — **this is today's behavior; capture it
|
||||
as-is. Plan 003 will change it.**), else calls `notifyResumeUpdated`.
|
||||
- `delete` (lines ~744–769): transaction that throws `NOT_FOUND` when the row
|
||||
is missing and `RESUME_LOCKED` when locked, then deletes and cleans storage.
|
||||
- `statistics.increment` (lines ~199–237): two `INSERT ... ON CONFLICT DO
|
||||
UPDATE` writes inside a transaction.
|
||||
|
||||
- **Test convention to follow** — model the new test after the existing
|
||||
sibling `packages/api/src/features/applications/service.test.ts`. It mocks
|
||||
the DB layer with `vi.hoisted` + `vi.mock`, then dynamically imports the
|
||||
service. The exact shape to copy (from that file, lines 1–68):
|
||||
|
||||
```ts
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const dbMock = vi.hoisted(() => ({
|
||||
select: vi.fn(), insert: vi.fn(), update: vi.fn(),
|
||||
delete: vi.fn(), transaction: vi.fn(),
|
||||
}));
|
||||
vi.mock("@reactive-resume/db/client", () => ({ db: dbMock }));
|
||||
vi.mock("@reactive-resume/db/schema", () => ({ /* stub the tables used */ }));
|
||||
vi.mock("drizzle-orm", () => ({
|
||||
and: (...a: unknown[]) => a, eq: (...a: unknown[]) => a,
|
||||
isNotNull: (...a: unknown[]) => a, sql: Object.assign(
|
||||
(s: TemplateStringsArray, ...v: unknown[]) => ({ s, v }),
|
||||
{ join: (v: unknown[]) => v },
|
||||
),
|
||||
}));
|
||||
|
||||
const { resumeService } = await import("./service");
|
||||
```
|
||||
|
||||
Note the `applications/service.test.ts` helpers `createSelectChain(rows)` and
|
||||
`setSelectResults(...)` — reuse that pattern to script what each `db.select`
|
||||
/ `db.update().returning()` call resolves to.
|
||||
|
||||
- **Mocks this service needs beyond the applications example** (grep the
|
||||
imports at the top of `service.ts` and mock each):
|
||||
- `bcrypt` (`hash`, `compare`) — used by `setPassword`/`verifyPassword`.
|
||||
(The real import specifier in `service.ts` is `bcrypt`, not `bcryptjs` —
|
||||
mock that exact module path.) Mock `hash` to return a fixed string and
|
||||
`compare` to return a boolean you control.
|
||||
- The snapshot/patch helpers imported into `service.ts` (e.g.
|
||||
`applyResumePatchTx`, `maybeSnapshotOnSave`, `writeResumeVersion`,
|
||||
`notifyResumeUpdated`, `getStorageService`) — mock them so the service's
|
||||
own branching is what's under test, not their internals. Read the top of
|
||||
`service.ts` to get the exact import specifiers and mock each module path
|
||||
the same way the applications test mocks `../storage/service`.
|
||||
|
||||
- **ORPCError assertions** — errors are `ORPCError` instances from
|
||||
`@orpc/server` with a `.code` (e.g. `"NOT_FOUND"`, `"RESUME_LOCKED"`). Assert
|
||||
with `await expect(fn()).rejects.toThrow()` and, where you can, check the
|
||||
code: `await fn().catch((e) => expect(e.code).toBe("RESUME_LOCKED"))`, or
|
||||
assert on `.message`. Confirm the real shape by reading how
|
||||
`access-policy.test.ts` or `access.test.ts` in the same folder assert on
|
||||
`ORPCError` and copy that style.
|
||||
|
||||
## Commands you will need
|
||||
|
||||
| Purpose | Command | Expected on success |
|
||||
|-----------|---------------------------------------------------------------------|---------------------|
|
||||
| Typecheck | `pnpm --filter @reactive-resume/api typecheck` | exit 0, no errors |
|
||||
| Run test | `pnpm --filter @reactive-resume/api test -- resume/service.test.ts` | all pass |
|
||||
| All api tests | `pnpm --filter @reactive-resume/api test` | all pass |
|
||||
|
||||
(These are the repo's real commands — package-scoped Vitest via Turborepo.
|
||||
Do NOT run `pnpm check`; it rewrites files.)
|
||||
|
||||
## Scope
|
||||
|
||||
**In scope** (the only files you should create/modify):
|
||||
- `packages/api/src/features/resume/service.test.ts` (create)
|
||||
- `plans/README.md` (status row only)
|
||||
|
||||
**Out of scope** (do NOT touch):
|
||||
- `packages/api/src/features/resume/service.ts` — this plan adds tests that
|
||||
characterize its *current* behavior. Do not "fix" anything you find here,
|
||||
even the silent `if (!resume) return;` no-ops in `setLocked`/`setPassword`/
|
||||
`removePassword` — those are Plan 003's job, and this test must assert the
|
||||
current silent-return behavior so Plan 003's change is visible as a test diff.
|
||||
- Any router file (`crud.ts`, `sharing.ts`) — router tests are a separate
|
||||
future effort.
|
||||
- The real database or migrations — this is a pure unit test with a mocked db.
|
||||
|
||||
## Git workflow
|
||||
|
||||
- Branch: `advisor/001-resume-service-tests`
|
||||
- Commit style: conventional commits (repo uses them — e.g.
|
||||
`test(api): add characterization tests for resume service`).
|
||||
- Do NOT push or open a PR unless the operator instructed it.
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Scaffold the test file and mocks
|
||||
|
||||
Create `packages/api/src/features/resume/service.test.ts`. Copy the mock
|
||||
scaffolding pattern from `applications/service.test.ts` (lines 1–68). Read the
|
||||
top-of-file imports in `service.ts` and add a `vi.mock(...)` for every module
|
||||
it imports that touches I/O (db, schema, drizzle-orm, bcryptjs, storage
|
||||
service, and the patch/snapshot/notify helpers). Stub `@reactive-resume/db/schema`
|
||||
with the table/column objects the service references (`resume`,
|
||||
`resumeStatistics`, `resumeStatisticsDaily`, `user`).
|
||||
|
||||
**Verify**: `pnpm --filter @reactive-resume/api test -- resume/service.test.ts`
|
||||
→ the file is picked up (even with zero real tests it should not error on
|
||||
import; add one `it("imports", () => expect(resumeService).toBeDefined())` to
|
||||
confirm wiring). Expected: 1 passing test.
|
||||
|
||||
### Step 2: Characterize `update`
|
||||
|
||||
Add a `describe("update")` block with these cases, asserting **current**
|
||||
behavior:
|
||||
- Throws `RESUME_LOCKED` when the pre-read returns `{ isLocked: true }`.
|
||||
- Returns the updated row on success (script `db.update().set().where().returning()`
|
||||
to resolve `[{ id, name, slug, ... }]`).
|
||||
- Throws `NOT_FOUND` when the `RETURNING` resolves to `[]` (no row matched).
|
||||
- Maps a thrown error whose `cause.constraint === "resume_slug_user_id_unique"`
|
||||
to `RESUME_SLUG_ALREADY_EXISTS`.
|
||||
|
||||
**Verify**: `pnpm --filter @reactive-resume/api test -- resume/service.test.ts`
|
||||
→ all cases pass.
|
||||
|
||||
### Step 3: Characterize `setLocked`, `setPassword`, `removePassword`
|
||||
|
||||
Add a `describe` for each. For every method assert **both** paths:
|
||||
- Success path: `RETURNING` resolves to `[{ id, updatedAt }]` → the method
|
||||
resolves (returns `undefined`) and `notifyResumeUpdated` was called once with
|
||||
the expected `mutation` value (`"lock"` / `"password"`).
|
||||
- **Not-found path: `RETURNING` resolves to `[]` → the method resolves
|
||||
`undefined` and `notifyResumeUpdated` is NOT called** (this pins the current
|
||||
silent no-op; Plan 003 will flip this to throwing `NOT_FOUND`).
|
||||
- For `setPassword`, assert `hash` (mocked bcrypt) was called with the input
|
||||
password before the update.
|
||||
|
||||
**Verify**: `pnpm --filter @reactive-resume/api test -- resume/service.test.ts`
|
||||
→ all pass.
|
||||
|
||||
### Step 4: Characterize `verifyPassword` and `delete`
|
||||
|
||||
- `verifyPassword`: throws `INVALID_PASSWORD` when no matching row; throws
|
||||
`INVALID_PASSWORD` when `compare` (mocked) returns `false`; returns `true`
|
||||
and calls `grantResumeAccess` when `compare` returns `true`.
|
||||
- `delete`: script the transaction mock (see how `applications/service.test.ts`
|
||||
handles `db.transaction` — if it doesn't, make `dbMock.transaction` invoke
|
||||
its callback with a `tx` object exposing the same `select`/`delete` chain).
|
||||
Assert `NOT_FOUND` when the row is missing, `RESUME_LOCKED` when locked, and
|
||||
storage `delete` called for both screenshot and pdf keys on success.
|
||||
|
||||
**Verify**: `pnpm --filter @reactive-resume/api test -- resume/service.test.ts`
|
||||
→ all pass.
|
||||
|
||||
### Step 5: Characterize `statistics.increment`
|
||||
|
||||
Assert that a `views: true` call runs `db.transaction`, and inside it inserts
|
||||
into both `resumeStatistics` and `resumeStatisticsDaily` with an
|
||||
`onConflictDoUpdate`. You do not need to assert SQL text — assert that both
|
||||
`tx.insert(...)` calls happen (spy on the tx insert). This case is the safety
|
||||
net for Plan 005, which changes when `increment` is *called* (not its body).
|
||||
|
||||
**Verify**: `pnpm --filter @reactive-resume/api test` → the whole api package
|
||||
test suite passes (no regressions in sibling tests from your new mocks).
|
||||
|
||||
## Test plan
|
||||
|
||||
- New file: `packages/api/src/features/resume/service.test.ts`, structured as
|
||||
one `describe` per method, following `applications/service.test.ts` as the
|
||||
structural pattern.
|
||||
- Cases per method are listed in Steps 2–5 (happy path + each error/edge branch
|
||||
the code contains today).
|
||||
- Verification: `pnpm --filter @reactive-resume/api test` → all pass, including
|
||||
the new tests. Count the new tests in the output; expect ≥ 14 new cases.
|
||||
|
||||
## Done criteria
|
||||
|
||||
Machine-checkable. ALL must hold:
|
||||
|
||||
- [ ] `packages/api/src/features/resume/service.test.ts` exists
|
||||
- [ ] `pnpm --filter @reactive-resume/api test -- resume/service.test.ts` exits 0 with ≥ 14 passing cases
|
||||
- [ ] `pnpm --filter @reactive-resume/api test` exits 0 (no sibling regressions)
|
||||
- [ ] `pnpm --filter @reactive-resume/api typecheck` exits 0
|
||||
- [ ] `git status --porcelain` shows only `service.test.ts` (new) and `plans/README.md` modified
|
||||
- [ ] `plans/README.md` status row for 001 updated to DONE
|
||||
|
||||
## STOP conditions
|
||||
|
||||
Stop and report back (do not improvise) if:
|
||||
|
||||
- `service.ts` has drifted from the excerpts above (method line ranges or error
|
||||
codes differ materially) — the codebase changed since this plan was written.
|
||||
- The mocking approach fights the service: e.g. the service imports something
|
||||
that runs real I/O at module load and can't be cleanly mocked. Report what
|
||||
and where; do not weaken the test into a no-op.
|
||||
- You find a genuine bug while characterizing (behavior that looks wrong). Do
|
||||
NOT fix it here — write the test to capture current behavior, add a
|
||||
`// NOTE: characterizes current behavior; see finding` comment, and report it.
|
||||
|
||||
## Maintenance notes
|
||||
|
||||
- Plan 003 changes `setLocked`/`setPassword`/`removePassword` to throw
|
||||
`NOT_FOUND` instead of silently returning. When that lands, the "not-found
|
||||
path" assertions from Step 3 must be updated in the same PR — that test diff
|
||||
is the intended signal that behavior changed on purpose.
|
||||
- Plan 005 changes the *caller* of `statistics.increment` (dedup), not its body,
|
||||
so Step 5's test should keep passing; if it breaks, 005 changed more than
|
||||
intended.
|
||||
- A reviewer should check the mocks assert real branching, not tautologies
|
||||
(e.g. that `NOT_FOUND` comes from an empty `RETURNING`, not from a mock that
|
||||
always throws).
|
||||
@@ -1,233 +0,0 @@
|
||||
# Plan 002: Add CSP + framing headers to web pages; gate the uploads CORS header
|
||||
|
||||
> **Executor instructions**: Follow this plan step by step. Run every
|
||||
> verification command and confirm the expected result before moving on. If
|
||||
> anything in "STOP conditions" occurs, stop and report. When done, update the
|
||||
> status row for this plan in `plans/README.md`.
|
||||
>
|
||||
> **Drift check (run first)**:
|
||||
> `git diff --stat 73daf22b2..HEAD -- apps/server/src/static/web.ts apps/server/src/static/uploads.ts`
|
||||
> If either file changed since this plan was written, compare the "Current
|
||||
> state" excerpts against the live code before proceeding; on mismatch, STOP.
|
||||
|
||||
## Status
|
||||
|
||||
- **Priority**: P1
|
||||
- **Effort**: S
|
||||
- **Risk**: LOW
|
||||
- **Depends on**: none
|
||||
- **Category**: security
|
||||
- **Planned at**: commit `73daf22b2`, 2026-07-08
|
||||
|
||||
## Why this matters
|
||||
|
||||
The Hono server serves every HTML page (public resumes, the auth/dashboard/
|
||||
builder shells) through `handleWebApp` in `apps/server/src/static/web.ts`, and
|
||||
that response sets only `Content-Type` and (sometimes) `X-Robots-Tag`. There is
|
||||
**no `Content-Security-Policy` and no `X-Frame-Options`**. Consequences:
|
||||
- Any page can be framed by an attacker's site → clickjacking against
|
||||
authenticated actions and public resume views.
|
||||
- No script-source restriction → a single injected-content bug becomes a much
|
||||
larger XSS blast radius than it needs to be.
|
||||
|
||||
Separately, the file-serving endpoint in `apps/server/src/static/uploads.ts`
|
||||
sets `Access-Control-Allow-Origin: env.APP_URL` **unconditionally** on a GET
|
||||
endpoint whose only real consumers are same-origin. `Cross-Origin-Resource-Policy:
|
||||
same-site` is already set, so the ACAO header adds cross-origin exposure for no
|
||||
functional benefit.
|
||||
|
||||
This plan adds the missing headers to web responses and removes the
|
||||
unnecessary ACAO header. It is deliberately conservative: **CSP ships in
|
||||
report-only mode first** so it cannot break the app on rollout.
|
||||
|
||||
## Current state
|
||||
|
||||
- `apps/server/src/static/web.ts:55-65` — `getFallbackResponseHeaders` returns
|
||||
a plain object of headers per path (or `null` for a 404):
|
||||
|
||||
```ts
|
||||
function getFallbackResponseHeaders(pathname: string) {
|
||||
if (pathname === "/") return { "Content-Type": "text/html; charset=UTF-8" };
|
||||
if (isNoindexShellPath(pathname) || isPublicResumePath(pathname)) {
|
||||
return {
|
||||
"Content-Type": "text/html; charset=UTF-8",
|
||||
"X-Robots-Tag": "noindex, follow",
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
These objects are spread into the `Response` at `web.ts:86-92` (both the HEAD
|
||||
`new Response(null, { status: 200, headers })` path and the GET
|
||||
`new Response(html, { headers })` path).
|
||||
|
||||
- `apps/server/src/static/uploads.ts:28-46` — the file response already sets a
|
||||
strong header set as a model to follow, and ends with the ACAO line to remove:
|
||||
|
||||
```ts
|
||||
headers.set("Cache-Control", "public, max-age=31536000, immutable");
|
||||
headers.set("ETag", etag);
|
||||
headers.set("X-Content-Type-Options", "nosniff");
|
||||
headers.set("X-Robots-Tag", "noindex, nofollow");
|
||||
headers.set("Cross-Origin-Resource-Policy", "same-site");
|
||||
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
||||
headers.set("X-Frame-Options", "DENY");
|
||||
headers.set("X-Download-Options", "noopen");
|
||||
headers.set("Access-Control-Allow-Origin", env.APP_URL); // <-- remove this line
|
||||
```
|
||||
|
||||
- **Existing test files that assert headers** (follow their style, they are
|
||||
your regression net):
|
||||
- `apps/server/src/static/web.test.ts`
|
||||
- `apps/server/src/static/uploads.test.ts`
|
||||
|
||||
- **Design constraint** — the app has no known legitimate need to be iframed,
|
||||
and the builder preview is a same-origin pdf.js canvas (not a cross-origin
|
||||
frame). So `X-Frame-Options: DENY` is safe. The app does load web fonts and
|
||||
images from same origin and inline styles/scripts from the Vite bundle, which
|
||||
is why CSP starts **report-only**: do not enforce a policy you have not
|
||||
observed the app satisfy.
|
||||
|
||||
## Commands you will need
|
||||
|
||||
| Purpose | Command | Expected on success |
|
||||
|-----------|----------------------------------------|---------------------|
|
||||
| Typecheck | `pnpm --filter server typecheck` | exit 0 |
|
||||
| Test | `pnpm --filter server test -- static` | all pass |
|
||||
|
||||
(The server package is named `server` in `apps/server/package.json`, not
|
||||
`@reactive-resume/server`.)
|
||||
|
||||
(Do NOT run `pnpm check`.)
|
||||
|
||||
## Scope
|
||||
|
||||
**In scope**:
|
||||
- `apps/server/src/static/web.ts`
|
||||
- `apps/server/src/static/web.test.ts`
|
||||
- `apps/server/src/static/uploads.ts`
|
||||
- `apps/server/src/static/uploads.test.ts`
|
||||
- `plans/README.md` (status row)
|
||||
|
||||
**Out of scope** (do NOT touch):
|
||||
- The oRPC / auth / MCP / OpenAPI handlers — they return API responses, not
|
||||
HTML pages; header policy for those is a separate concern.
|
||||
- Enforcing (non-report-only) CSP — an enforced policy requires collecting
|
||||
violation reports first; that is explicit follow-up, not this plan.
|
||||
- Any web-app (`apps/web`) source — headers are set at the server layer.
|
||||
|
||||
## Git workflow
|
||||
|
||||
- Branch: `advisor/002-security-headers`
|
||||
- Commit style: conventional commits, e.g.
|
||||
`feat(server): add CSP report-only and framing headers to web responses`.
|
||||
- Do NOT push or open a PR unless instructed.
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Add framing + hardening headers to web responses
|
||||
|
||||
In `apps/server/src/static/web.ts`, extend the header objects returned by
|
||||
`getFallbackResponseHeaders` so that every non-null branch (the `/` branch and
|
||||
the noindex/public branch) also includes:
|
||||
|
||||
```
|
||||
"X-Frame-Options": "DENY",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||
```
|
||||
|
||||
Keep the existing `Content-Type` and `X-Robots-Tag` values unchanged. Prefer
|
||||
adding a small shared constant (e.g. `const BASE_SECURITY_HEADERS = { ... }`)
|
||||
and spreading it into both branches, so the two paths cannot drift.
|
||||
|
||||
**Verify**: `pnpm --filter @reactive-resume/server typecheck` → exit 0.
|
||||
|
||||
### Step 2: Add a report-only CSP header to web responses
|
||||
|
||||
Add to the same shared header set:
|
||||
|
||||
```
|
||||
"Content-Security-Policy-Report-Only":
|
||||
"default-src 'self'; img-src 'self' data: blob:; font-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; object-src 'none'",
|
||||
```
|
||||
|
||||
Use `Report-Only` (not the enforcing header) so nothing breaks on rollout.
|
||||
`'unsafe-inline'` for style/script is intentional for the first pass — the Vite
|
||||
bundle and inline theme script need it; tightening to nonces is future work.
|
||||
`frame-ancestors 'none'` is the CSP-level equivalent of `X-Frame-Options: DENY`.
|
||||
|
||||
**Verify**: `pnpm --filter @reactive-resume/server typecheck` → exit 0.
|
||||
|
||||
### Step 3: Remove the unconditional CORS header from uploads
|
||||
|
||||
In `apps/server/src/static/uploads.ts`, delete the line:
|
||||
|
||||
```ts
|
||||
headers.set("Access-Control-Allow-Origin", env.APP_URL);
|
||||
```
|
||||
|
||||
Leave every other header untouched. If, after removal, `env` is no longer
|
||||
referenced anywhere in the file, remove its now-unused import (check with the
|
||||
grep in Done criteria); if `env` is still used, keep the import.
|
||||
|
||||
**Verify**: `pnpm --filter @reactive-resume/server typecheck` → exit 0
|
||||
(no unused-import or undefined-symbol errors).
|
||||
|
||||
### Step 4: Update/extend tests
|
||||
|
||||
- In `web.test.ts`: add assertions that a GET to `/` and to a public resume
|
||||
path returns `X-Frame-Options: DENY` and a `Content-Security-Policy-Report-Only`
|
||||
header. Follow the existing test's request/response style in that file.
|
||||
- In `uploads.test.ts`: if an existing test asserts the presence of
|
||||
`Access-Control-Allow-Origin`, change it to assert the header is **absent**
|
||||
(`response.headers.get("Access-Control-Allow-Origin")` is `null`). Keep the
|
||||
assertions for `Cross-Origin-Resource-Policy` and the others.
|
||||
|
||||
**Verify**: `pnpm --filter @reactive-resume/server test -- static` → all pass.
|
||||
|
||||
## Test plan
|
||||
|
||||
- Extend `web.test.ts` with two cases: `/` and a public-resume path each carry
|
||||
the new framing + CSP-report-only headers.
|
||||
- Update `uploads.test.ts` so the ACAO header is asserted absent (and the other
|
||||
security headers still present).
|
||||
- Verification: `pnpm --filter @reactive-resume/server test -- static` → all
|
||||
pass, including the new/updated assertions.
|
||||
|
||||
## Done criteria
|
||||
|
||||
Machine-checkable. ALL must hold:
|
||||
|
||||
- [ ] `pnpm --filter @reactive-resume/server typecheck` exits 0
|
||||
- [ ] `pnpm --filter @reactive-resume/server test -- static` exits 0
|
||||
- [ ] `grep -n "X-Frame-Options" apps/server/src/static/web.ts` returns a match
|
||||
- [ ] `grep -n "Content-Security-Policy-Report-Only" apps/server/src/static/web.ts` returns a match
|
||||
- [ ] `grep -n "Access-Control-Allow-Origin" apps/server/src/static/uploads.ts` returns **no** matches
|
||||
- [ ] `git status --porcelain` lists only the four in-scope source/test files and `plans/README.md`
|
||||
- [ ] `plans/README.md` status row for 002 updated
|
||||
|
||||
## STOP conditions
|
||||
|
||||
Stop and report back if:
|
||||
|
||||
- `web.ts`/`uploads.ts` have drifted from the excerpts above.
|
||||
- Removing the ACAO header breaks an existing test that documents a *legitimate*
|
||||
cross-origin consumer of `/uploads/*` you were unaware of (read the test's
|
||||
intent before assuming it's stale) — report it instead of forcing the change.
|
||||
- You are tempted to ship an **enforcing** CSP (not report-only) — that is out
|
||||
of scope and can break the app; stop and confirm.
|
||||
|
||||
## Maintenance notes
|
||||
|
||||
- The CSP is report-only. Follow-up (separate plan): wire a report endpoint or
|
||||
read browser console CSP reports, confirm the app fully satisfies the policy,
|
||||
then promote `Content-Security-Policy-Report-Only` → `Content-Security-Policy`
|
||||
and drop `'unsafe-inline'` in favor of nonces where feasible.
|
||||
- If a future feature legitimately needs the app embeddable (e.g. an official
|
||||
embed widget), `frame-ancestors`/`X-Frame-Options` must be relaxed for that
|
||||
route only, not globally.
|
||||
- Reviewer should confirm the header set is shared between both `web.ts`
|
||||
branches (no drift) and that no API/JSON responses accidentally inherit the
|
||||
HTML CSP.
|
||||
@@ -1,238 +0,0 @@
|
||||
# Plan 003: Fix silent-success mutations and bound bulk-operation inputs
|
||||
|
||||
> **Executor instructions**: Follow step by step. Run every verification
|
||||
> command and confirm the expected result before moving on. Honor "STOP
|
||||
> conditions". When done, update the status row in `plans/README.md`.
|
||||
>
|
||||
> **Drift check (run first)**:
|
||||
> `git diff --stat 73daf22b2..HEAD -- packages/api/src/features/resume/service.ts packages/api/src/dto/application.ts packages/api/src/features/applications/service.ts`
|
||||
> If any changed since this plan was written, compare "Current state" excerpts
|
||||
> against live code; on mismatch, STOP.
|
||||
|
||||
## Status
|
||||
|
||||
- **Priority**: P1
|
||||
- **Effort**: S
|
||||
- **Risk**: LOW
|
||||
- **Depends on**: 001 (its Step 3 tests characterize the current silent-return
|
||||
behavior this plan changes — update them here). Not a hard blocker, but if
|
||||
001 is DONE you must update its assertions in the same PR.
|
||||
- **Category**: bug
|
||||
- **Planned at**: commit `73daf22b2`, 2026-07-08
|
||||
|
||||
## Why this matters
|
||||
|
||||
Two small, independent correctness/robustness fixes on the API layer:
|
||||
|
||||
1. **Silent-success mutations.** `setLocked`, `setPassword`, and
|
||||
`removePassword` in `packages/api/src/features/resume/service.ts` run an
|
||||
`UPDATE ... WHERE id = ? AND userId = ? RETURNING`, and when no row matches
|
||||
they do `if (!resume) return;` — returning HTTP 200 success. Every other
|
||||
mutation in this file (`update`, `delete`) throws `ORPCError("NOT_FOUND")` in
|
||||
the same situation. The inconsistency means a client calling these on a
|
||||
resume that doesn't exist (or isn't theirs) is told it succeeded, so the UI
|
||||
updates its state as if the lock/password change took effect. Ownership is
|
||||
still enforced (the `WHERE` includes `userId`), so this is not an auth
|
||||
bypass — it's a misleading false-success that hides not-found/not-owned.
|
||||
|
||||
2. **Unbounded bulk inputs.** `bulkUpdate` and `bulkDelete` DTOs in
|
||||
`packages/api/src/dto/application.ts` accept `ids: z.array(z.string()).min(1)`
|
||||
with **no `.max()`**. `bulkDelete`'s service loads all target rows into
|
||||
memory before deleting. A single API call with a very large `ids` array is
|
||||
unbounded memory/DB work. A `.max()` cap is a one-line defensive fix.
|
||||
|
||||
## Current state
|
||||
|
||||
### Part A — silent-success (resume/service.ts)
|
||||
|
||||
`setLocked` (~663–679), `setPassword` (~681–699), `removePassword` (~726–742)
|
||||
each look like this (setLocked shown; the other two differ only in the `.set`
|
||||
and the `mutation` label):
|
||||
|
||||
```ts
|
||||
setLocked: async (input: { id: string; userId: string; isLocked: boolean }) => {
|
||||
const [resume] = await db
|
||||
.update(schema.resume)
|
||||
.set({ isLocked: input.isLocked })
|
||||
.where(and(eq(schema.resume.id, input.id), eq(schema.resume.userId, input.userId)))
|
||||
.returning({ id: schema.resume.id, updatedAt: schema.resume.updatedAt });
|
||||
|
||||
if (!resume) return; // <-- silent no-op; should throw NOT_FOUND
|
||||
|
||||
await notifyResumeUpdated({ /* ... mutation: "lock" ... */ });
|
||||
},
|
||||
```
|
||||
|
||||
The reference behavior to match is `update` (~603) and `delete` (~751):
|
||||
`if (!resume) throw new ORPCError("NOT_FOUND");`. `ORPCError` is already imported
|
||||
at the top of `service.ts` (used throughout).
|
||||
|
||||
The routers that call these (for context; **do not change them**):
|
||||
- `crud.ts:187` `setLocked` — `protectedProcedure`, does not currently declare a
|
||||
`NOT_FOUND` error in `.errors({...})`.
|
||||
- `sharing.ts:29` `setPassword`, `sharing.ts:80` `removePassword` — likewise.
|
||||
|
||||
oRPC will surface a thrown `ORPCError("NOT_FOUND")` as a 404 regardless of
|
||||
whether it's declared in `.errors()`, matching how `update`/`delete` already
|
||||
behave (they throw the same without special router declarations). So no router
|
||||
change is required.
|
||||
|
||||
### Part B — unbounded bulk inputs (dto/application.ts)
|
||||
|
||||
`packages/api/src/dto/application.ts:163-176`:
|
||||
|
||||
```ts
|
||||
bulkUpdate: {
|
||||
input: z.object({
|
||||
ids: z.array(z.string()).min(1),
|
||||
status: applicationStatusSchema.optional(),
|
||||
archived: z.boolean().optional(),
|
||||
addTags: z.array(z.string()).optional(),
|
||||
}),
|
||||
output: z.object({ updated: z.number() }),
|
||||
},
|
||||
|
||||
bulkDelete: {
|
||||
input: z.object({ ids: z.array(z.string()).min(1) }),
|
||||
output: z.object({ deleted: z.number() }),
|
||||
},
|
||||
```
|
||||
|
||||
The consuming service is `packages/api/src/features/applications/service.ts`
|
||||
(`bulkUpdate` ~340–381, `bulkDelete` ~383–396; `bulkDelete` fetches all target
|
||||
rows into memory before deleting).
|
||||
|
||||
## Commands you will need
|
||||
|
||||
| Purpose | Command | Expected |
|
||||
|-----------|-------------------------------------------------------------------------|--------------------|
|
||||
| Typecheck | `pnpm --filter @reactive-resume/api typecheck` | exit 0 |
|
||||
| Test A | `pnpm --filter @reactive-resume/api test -- resume/service.test.ts` | all pass |
|
||||
| Test B | `pnpm --filter @reactive-resume/api test -- applications` | all pass |
|
||||
| All api | `pnpm --filter @reactive-resume/api test` | all pass |
|
||||
|
||||
(Do NOT run `pnpm check`.)
|
||||
|
||||
## Scope
|
||||
|
||||
**In scope**:
|
||||
- `packages/api/src/features/resume/service.ts` (Part A)
|
||||
- `packages/api/src/features/resume/service.test.ts` (update if 001 is DONE; else
|
||||
the tests may not exist yet — see Step 3)
|
||||
- `packages/api/src/dto/application.ts` (Part B)
|
||||
- `packages/api/src/dto/application.test.ts` if one exists, else add a focused
|
||||
test near the applications service tests (Step 4)
|
||||
- `plans/README.md` (status row)
|
||||
|
||||
**Out of scope**:
|
||||
- `crud.ts` / `sharing.ts` routers — no change needed (see Current state).
|
||||
- The applications service body — a `.max()` on input is sufficient; do not
|
||||
also rewrite the in-memory `bulkDelete` scan in this plan (that's a separate
|
||||
perf concern tracked elsewhere).
|
||||
- Any other service method.
|
||||
|
||||
## Git workflow
|
||||
|
||||
- Branch: `advisor/003-api-quick-fixes`
|
||||
- Commit style: conventional commits, e.g.
|
||||
`fix(api): throw NOT_FOUND on lock/password mutations for missing resume` and
|
||||
`fix(api): cap bulk application operation id arrays`.
|
||||
- Do NOT push or open a PR unless instructed.
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Throw NOT_FOUND in the three mutations
|
||||
|
||||
In `resume/service.ts`, change `if (!resume) return;` to
|
||||
`if (!resume) throw new ORPCError("NOT_FOUND");` in **all three** of `setLocked`,
|
||||
`setPassword`, `removePassword`. Match the exact form used by `update`/`delete`
|
||||
in the same file.
|
||||
|
||||
**Verify**: `pnpm --filter @reactive-resume/api typecheck` → exit 0.
|
||||
|
||||
### Step 2: Cap the bulk id arrays
|
||||
|
||||
In `dto/application.ts`, add `.max(200, "Too many items in a single bulk
|
||||
operation")` to the `ids` array in **both** `bulkUpdate` and `bulkDelete`
|
||||
inputs (i.e. `z.array(z.string()).min(1).max(200)`). 200 comfortably exceeds
|
||||
the UI's page size (~25) while bounding abuse; if a comment nearby documents a
|
||||
different intended cap, use that number and note it.
|
||||
|
||||
**Verify**: `pnpm --filter @reactive-resume/api typecheck` → exit 0.
|
||||
|
||||
### Step 3: Update / add tests for Part A
|
||||
|
||||
- If Plan 001 is DONE (`resume/service.test.ts` exists): update its `setLocked`
|
||||
/ `setPassword` / `removePassword` "not-found path" cases to now assert the
|
||||
method **rejects with `NOT_FOUND`** (previously they asserted a silent
|
||||
resolve). This test diff is the intended proof the behavior changed.
|
||||
- If Plan 001 is NOT done (no test file): add a minimal
|
||||
`resume/service.test.ts` covering just these three methods' not-found →
|
||||
`NOT_FOUND` behavior and success path, using
|
||||
`packages/api/src/features/applications/service.test.ts` as the mock pattern.
|
||||
|
||||
**Verify**: `pnpm --filter @reactive-resume/api test -- resume/service.test.ts`
|
||||
→ all pass; the three not-found cases assert `NOT_FOUND`.
|
||||
|
||||
### Step 4: Test for Part B
|
||||
|
||||
Add a small test that `bulkUpdate`/`bulkDelete` input schemas reject an `ids`
|
||||
array longer than the cap and accept one at the cap. If
|
||||
`packages/api/src/dto/application.test.ts` exists, add there; otherwise create
|
||||
it. Example shape:
|
||||
|
||||
```ts
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applicationDto } from "./application"; // confirm the real export name
|
||||
|
||||
it("rejects oversized bulk id arrays", () => {
|
||||
const ids = Array.from({ length: 201 }, (_, i) => String(i));
|
||||
expect(applicationDto.bulkDelete.input.safeParse({ ids }).success).toBe(false);
|
||||
});
|
||||
```
|
||||
|
||||
Confirm the actual export name/shape by reading the top and bottom of
|
||||
`dto/application.ts` before writing the import.
|
||||
|
||||
**Verify**: `pnpm --filter @reactive-resume/api test -- applications` and
|
||||
`pnpm --filter @reactive-resume/api test` → all pass.
|
||||
|
||||
## Test plan
|
||||
|
||||
- Part A: three methods now reject with `NOT_FOUND` on no-match (updated or new
|
||||
cases in `resume/service.test.ts`).
|
||||
- Part B: bulk input schemas reject arrays over the cap, accept at the cap.
|
||||
- Verification: `pnpm --filter @reactive-resume/api test` → all pass.
|
||||
|
||||
## Done criteria
|
||||
|
||||
Machine-checkable. ALL must hold:
|
||||
|
||||
- [ ] `grep -n "if (!resume) return;" packages/api/src/features/resume/service.ts` returns **no** matches
|
||||
- [ ] `grep -c "throw new ORPCError(\"NOT_FOUND\")" packages/api/src/features/resume/service.ts` is ≥ 5 (update, delete, + the 3 new)
|
||||
- [ ] `grep -n ".max(" packages/api/src/dto/application.ts` shows the cap on both bulk inputs
|
||||
- [ ] `pnpm --filter @reactive-resume/api typecheck` exits 0
|
||||
- [ ] `pnpm --filter @reactive-resume/api test` exits 0
|
||||
- [ ] `git status --porcelain` lists only in-scope files + `plans/README.md`
|
||||
- [ ] `plans/README.md` status row for 003 updated
|
||||
|
||||
## STOP conditions
|
||||
|
||||
Stop and report if:
|
||||
|
||||
- The three methods no longer contain `if (!resume) return;` (already fixed by
|
||||
someone else, or drifted) — reconcile against live code before editing.
|
||||
- Throwing `NOT_FOUND` breaks an e2e or unit test that *depended on* the silent
|
||||
success (a client that fires these against unknown ids and expects 200) —
|
||||
report the caller; do not revert the fix without confirming intent.
|
||||
- The applications DTO export name differs from `applicationDto` — read the file
|
||||
and use the real name; do not guess.
|
||||
|
||||
## Maintenance notes
|
||||
|
||||
- The `.max(200)` cap is an input guard, not a fix for `bulkDelete`'s O(n)
|
||||
in-memory scan — that remains a known, separate perf item. If bulk operations
|
||||
ever need to exceed the cap, revisit both the cap and the scan together.
|
||||
- If the routers later add explicit `.errors({ NOT_FOUND: ... })` declarations
|
||||
for these procedures (for nicer OpenAPI docs), that's compatible with this
|
||||
change but not required by it.
|
||||
@@ -1,116 +0,0 @@
|
||||
# Plan 004: Correct the README "Custom CSS" feature claim
|
||||
|
||||
> **Executor instructions**: Follow step by step. Confirm each verification.
|
||||
> Honor "STOP conditions". When done, update the status row in `plans/README.md`.
|
||||
>
|
||||
> **Drift check (run first)**: `git diff --stat 73daf22b2..HEAD -- README.md`
|
||||
> If `README.md` changed, re-locate the line described below before editing.
|
||||
|
||||
## Status
|
||||
|
||||
- **Priority**: P3
|
||||
- **Effort**: S
|
||||
- **Risk**: LOW
|
||||
- **Depends on**: none
|
||||
- **Category**: docs
|
||||
- **Planned at**: commit `73daf22b2`, 2026-07-08
|
||||
|
||||
## Why this matters
|
||||
|
||||
The README advertises a feature that no longer exists. Raw custom CSS was
|
||||
removed and replaced by a structured Style Rules system — a decision recorded
|
||||
in `docs/adr/0002-structured-style-rules-for-react-pdf.md` (React PDF accepts
|
||||
style objects, not arbitrary browser selectors, so user CSS would be a
|
||||
misleading contract). A user reading the README expects a CSS code editor and
|
||||
instead finds structured form controls. Fixing the line keeps the public
|
||||
feature list truthful and consistent with the ADR.
|
||||
|
||||
## Current state
|
||||
|
||||
- `README.md:63` — under the **Templates** feature list:
|
||||
|
||||
```
|
||||
- Custom CSS for advanced styling
|
||||
```
|
||||
|
||||
- `docs/adr/0002-structured-style-rules-for-react-pdf.md` — states raw CSS is
|
||||
intentionally out; appearance customization is modeled as structured Style
|
||||
Rules targeting semantic section/rich-text slots.
|
||||
|
||||
- The actual UI lives at
|
||||
`apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/custom-styles.tsx`
|
||||
(structured controls: color pickers, number inputs, dropdowns — not a CSS
|
||||
editor). You do not need to modify it; it is cited only to confirm the fix
|
||||
wording matches reality.
|
||||
|
||||
## Commands you will need
|
||||
|
||||
| Purpose | Command | Expected |
|
||||
|----------------|--------------------------------------|---------------------|
|
||||
| Locate line | `grep -n "Custom CSS" README.md` | one match (~line 63)|
|
||||
| Docs lint (opt)| `pnpm lint:docs` | exit 0 |
|
||||
|
||||
(Do NOT run `pnpm check` — it rewrites files. This is a one-line docs edit.)
|
||||
|
||||
## Scope
|
||||
|
||||
**In scope**:
|
||||
- `README.md` (the single feature-list line)
|
||||
- `plans/README.md` (status row)
|
||||
|
||||
**Out of scope**:
|
||||
- The Style Rules implementation or ADR.
|
||||
- Any other README line, unless the drift check shows the feature list moved.
|
||||
- The Mintlify docs under `docs/` — a separate concern.
|
||||
|
||||
## Git workflow
|
||||
|
||||
- Branch: `advisor/004-readme-custom-css`
|
||||
- Commit: `docs: correct README custom-CSS claim to Style Rules`
|
||||
- Do NOT push or open a PR unless instructed.
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Replace the line
|
||||
|
||||
Change `README.md:63` from:
|
||||
|
||||
```
|
||||
- Custom CSS for advanced styling
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```
|
||||
- Structured Style Rules for section and text styling
|
||||
```
|
||||
|
||||
Match the surrounding list's capitalization and punctuation (no trailing
|
||||
period — the sibling bullets have none).
|
||||
|
||||
**Verify**: `grep -n "Custom CSS" README.md` → **no** matches;
|
||||
`grep -n "Structured Style Rules" README.md` → one match.
|
||||
|
||||
## Test plan
|
||||
|
||||
No code tests. Optional: `pnpm lint:docs` → exit 0 (markdownlint clean).
|
||||
|
||||
## Done criteria
|
||||
|
||||
- [ ] `grep -n "Custom CSS" README.md` returns no matches
|
||||
- [ ] `grep -n "Style Rules" README.md` returns the new line
|
||||
- [ ] `git status --porcelain` lists only `README.md` and `plans/README.md`
|
||||
- [ ] `plans/README.md` status row for 004 updated
|
||||
|
||||
## STOP conditions
|
||||
|
||||
Stop and report if:
|
||||
|
||||
- The "Custom CSS" line is already gone or already reworded (someone fixed it).
|
||||
- The drift check shows the Templates feature list was substantially
|
||||
restructured — re-locate the correct line before editing.
|
||||
|
||||
## Maintenance notes
|
||||
|
||||
- If Style Rules ever gains a raw-CSS escape hatch (ADR 0002 lists it as
|
||||
possible future work), revisit this line.
|
||||
@@ -1,224 +0,0 @@
|
||||
# Plan 005: Deduplicate public-resume view-count writes
|
||||
|
||||
> **Executor instructions**: Follow step by step. Run every verification and
|
||||
> confirm before moving on. Honor "STOP conditions". When done, update the
|
||||
> status row in `plans/README.md`.
|
||||
>
|
||||
> **Drift check (run first)**:
|
||||
> `git diff --stat 73daf22b2..HEAD -- packages/api/src/features/resume/service.ts packages/api/src/features/resume/access-policy.ts`
|
||||
> If either changed since this plan, compare "Current state" excerpts against
|
||||
> live code; on mismatch, STOP.
|
||||
|
||||
## Status
|
||||
|
||||
- **Priority**: P2
|
||||
- **Effort**: M
|
||||
- **Risk**: MED
|
||||
- **Depends on**: 001 recommended (its Step 5 test characterizes
|
||||
`statistics.increment`, giving you a safety net that this plan changes the
|
||||
*caller*, not the write body).
|
||||
- **Category**: perf
|
||||
- **Planned at**: commit `73daf22b2`, 2026-07-08
|
||||
|
||||
## Why this matters
|
||||
|
||||
Every public-resume view triggers a database write. `getBySlug`
|
||||
(`packages/api/src/features/resume/service.ts:475`) calls
|
||||
`statistics.increment(...)` whenever `shouldCountForStatistics` is true, and
|
||||
`increment` (lines 199–237) runs a transaction with **two** `INSERT ... ON
|
||||
CONFLICT DO UPDATE` statements (`resumeStatistics` + `resumeStatisticsDaily`).
|
||||
This fires on every non-owner load — bots, crawlers, and refreshes included.
|
||||
TanStack Query's 60s `staleTime` only suppresses within-session refetches; it
|
||||
does nothing for distinct sessions or server-side crawlers. A popular public
|
||||
resume therefore drives continuous write traffic to two tables for what is
|
||||
functionally the same view counted many times.
|
||||
|
||||
This plan adds a short-window per-viewer dedup so a burst of views from the
|
||||
same client within a window counts once, cutting write volume without changing
|
||||
what the counters mean to users (roughly "unique-ish views per window").
|
||||
|
||||
## Current state
|
||||
|
||||
- `packages/api/src/features/resume/service.ts:475` — `getBySlug` receives
|
||||
`input.requestHeaders: Headers`, resolves the resume, and at lines 505–507:
|
||||
|
||||
```ts
|
||||
if (shouldCountForStatistics(resume, viewer)) {
|
||||
await resumeService.statistics.increment({ id: resume.id, views: true });
|
||||
}
|
||||
```
|
||||
|
||||
- `packages/api/src/features/resume/access-policy.ts:76` —
|
||||
`shouldCountForStatistics(resume, viewer)` decides *whether* a view counts
|
||||
(e.g. skip the owner). This plan adds an orthogonal *"have we already counted
|
||||
this viewer recently?"* gate; it does **not** change `shouldCountForStatistics`.
|
||||
|
||||
- `increment` (`service.ts:199-237`) — the two-table transactional write. **Do
|
||||
not change its body**; this plan changes only whether it is called.
|
||||
|
||||
- **Reusable client-identity helper**: `packages/utils/src/rate-limit.ts`
|
||||
exports `TRUSTED_IP_HEADERS` (the ordered list of proxy IP headers the app
|
||||
trusts). The rate-limit middleware already derives a client key from these
|
||||
headers the same way. Reuse `TRUSTED_IP_HEADERS` to read the viewer IP from
|
||||
`input.requestHeaders`; fall back to a `user-agent`-based key when no trusted
|
||||
IP header is present (mirror the middleware's fallback so behavior is
|
||||
consistent).
|
||||
|
||||
- **Environment**: `REDIS_URL` exists in `packages/env/src/server.ts` and
|
||||
`turbo.json` globalEnv, but the app runs as a single Node process by default
|
||||
and the existing rate limiter uses an **in-memory** store
|
||||
(`@orpc/experimental-ratelimit/memory`). Match that: an in-memory TTL cache
|
||||
is the right default here.
|
||||
|
||||
## Commands you will need
|
||||
|
||||
| Purpose | Command | Expected |
|
||||
|-----------|--------------------------------------------------------------------|-----------|
|
||||
| Typecheck | `pnpm --filter @reactive-resume/api typecheck` | exit 0 |
|
||||
| Test | `pnpm --filter @reactive-resume/api test -- resume` | all pass |
|
||||
| All api | `pnpm --filter @reactive-resume/api test` | all pass |
|
||||
|
||||
(Do NOT run `pnpm check`.)
|
||||
|
||||
## Scope
|
||||
|
||||
**In scope**:
|
||||
- A new small helper module, e.g.
|
||||
`packages/api/src/features/resume/view-dedup.ts` (create) — an in-memory
|
||||
TTL set keyed by `${resumeId}:${clientKey}` with a `shouldCountView(...)`
|
||||
function that returns `true` at most once per key per window.
|
||||
- `packages/api/src/features/resume/view-dedup.test.ts` (create)
|
||||
- `packages/api/src/features/resume/service.ts` — gate the `increment` call
|
||||
(lines 505–507) on the new helper.
|
||||
- `plans/README.md` (status row)
|
||||
|
||||
**Out of scope**:
|
||||
- `increment`'s write body and the DB schema — unchanged.
|
||||
- `shouldCountForStatistics` in `access-policy.ts` — unchanged.
|
||||
- Any Redis / distributed-cache implementation — an in-memory window is the
|
||||
agreed default; a distributed store is a documented future upgrade, not this
|
||||
plan.
|
||||
- Download counting (`downloads: true`) — this plan is about view writes only;
|
||||
do not alter download increments.
|
||||
|
||||
## Git workflow
|
||||
|
||||
- Branch: `advisor/005-stats-view-dedup`
|
||||
- Commit: `perf(api): dedup public-resume view increments within a short window`
|
||||
- Do NOT push or open a PR unless instructed.
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Write the dedup helper
|
||||
|
||||
Create `view-dedup.ts` exporting a pure-ish, testable function. Suggested
|
||||
shape (keep it small — this is not a cache library):
|
||||
|
||||
```ts
|
||||
// ponytail: in-memory per-process dedup window. Single-instance is the default
|
||||
// deploy; for multi-instance, swap the Map for a Redis SETNX+EXPIRE keyed the
|
||||
// same way (REDIS_URL already exists in env). Upgrade only if you scale out.
|
||||
const WINDOW_MS = 60 * 60 * 1000; // 1 hour
|
||||
const seen = new Map<string, number>(); // key -> expiry timestamp
|
||||
|
||||
export function shouldCountView(key: string, now: number): boolean {
|
||||
const expiry = seen.get(key);
|
||||
if (expiry !== undefined && expiry > now) return false;
|
||||
seen.set(key, now + WINDOW_MS);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function clientKeyFromHeaders(headers: Headers): string { /* uses TRUSTED_IP_HEADERS, UA fallback */ }
|
||||
```
|
||||
|
||||
Take `now` as a parameter (don't call `Date.now()` inside the predicate) so the
|
||||
test can drive the clock deterministically. Add a lightweight size guard: if
|
||||
`seen.size` exceeds a cap (e.g. 50_000), prune entries whose expiry has passed
|
||||
before inserting, so the Map can't grow unbounded. Reuse `TRUSTED_IP_HEADERS`
|
||||
from `@reactive-resume/utils/rate-limit` inside `clientKeyFromHeaders`.
|
||||
|
||||
**Verify**: `pnpm --filter @reactive-resume/api typecheck` → exit 0.
|
||||
|
||||
### Step 2: Test the helper
|
||||
|
||||
Create `view-dedup.test.ts`:
|
||||
- `shouldCountView(key, t)` returns `true` the first time, `false` for the same
|
||||
key within the window, and `true` again once `now` is past the window.
|
||||
- Two different keys are independent.
|
||||
- `clientKeyFromHeaders` derives distinct keys for distinct trusted-IP headers
|
||||
and a stable key for the UA fallback when no IP header is present.
|
||||
|
||||
**Verify**: `pnpm --filter @reactive-resume/api test -- view-dedup` → all pass.
|
||||
|
||||
### Step 3: Gate the increment call
|
||||
|
||||
In `service.ts` `getBySlug`, wrap the existing count so it fires only when both
|
||||
the policy allows it AND the viewer hasn't been counted this window:
|
||||
|
||||
```ts
|
||||
if (shouldCountForStatistics(resume, viewer)) {
|
||||
const key = `${resume.id}:${clientKeyFromHeaders(input.requestHeaders)}`;
|
||||
if (shouldCountView(key, Date.now())) {
|
||||
await resumeService.statistics.increment({ id: resume.id, views: true });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Do not change anything else in `getBySlug`.
|
||||
|
||||
**Verify**: `pnpm --filter @reactive-resume/api typecheck` → exit 0.
|
||||
|
||||
### Step 4: Confirm no regression in existing resume tests
|
||||
|
||||
**Verify**: `pnpm --filter @reactive-resume/api test -- resume` → all pass. In
|
||||
particular, if Plan 001's `statistics.increment` characterization test exists,
|
||||
it should still pass (this plan didn't touch `increment`'s body). If a
|
||||
`getBySlug` test asserts increment-on-view, it may now need the test to pass a
|
||||
fresh header/clock so the first view still counts — update it to reflect the
|
||||
dedup (first view counts, immediate repeat does not).
|
||||
|
||||
## Test plan
|
||||
|
||||
- `view-dedup.test.ts`: window behavior + key derivation (Step 2 cases).
|
||||
- If an existing `getBySlug` test asserts view counting, extend it: first call
|
||||
with a given client key increments; an immediate second call with the same
|
||||
key does not; a call with a different key does.
|
||||
- Verification: `pnpm --filter @reactive-resume/api test` → all pass.
|
||||
|
||||
## Done criteria
|
||||
|
||||
Machine-checkable. ALL must hold:
|
||||
|
||||
- [ ] `packages/api/src/features/resume/view-dedup.ts` and its `.test.ts` exist
|
||||
- [ ] `pnpm --filter @reactive-resume/api test -- view-dedup` passes with ≥ 4 cases
|
||||
- [ ] `grep -n "shouldCountView" packages/api/src/features/resume/service.ts` shows the gate around the increment call
|
||||
- [ ] `pnpm --filter @reactive-resume/api typecheck` exits 0
|
||||
- [ ] `pnpm --filter @reactive-resume/api test` exits 0
|
||||
- [ ] `git status --porcelain` lists only in-scope files + `plans/README.md`
|
||||
- [ ] `plans/README.md` status row for 005 updated
|
||||
|
||||
## STOP conditions
|
||||
|
||||
Stop and report if:
|
||||
|
||||
- `getBySlug` no longer calls `statistics.increment` at lines ~505–507, or its
|
||||
signature no longer exposes `requestHeaders` — the code drifted.
|
||||
- You cannot derive a client key from `input.requestHeaders` because headers
|
||||
aren't actually populated at this layer in practice (check a real request
|
||||
path / existing rate-limit middleware usage) — report it; a dedup keyed on an
|
||||
empty header is worthless.
|
||||
- You find that view counting must remain exact (product decision that every
|
||||
raw hit counts) — the dedup changes counter semantics slightly; if unsure,
|
||||
stop and confirm before shipping.
|
||||
|
||||
## Maintenance notes
|
||||
|
||||
- **Semantics change**: counters become "unique-ish views per 1h window per
|
||||
client" rather than "raw hits". Document this near the helper. If the product
|
||||
wants raw hit counts back, this gate is the single place to remove.
|
||||
- **Multi-instance ceiling**: the in-memory Map is per-process. If the app is
|
||||
ever horizontally scaled, each instance dedups independently (still a large
|
||||
reduction, but not global). The ponytail comment names Redis as the upgrade
|
||||
path; do it only if scale-out happens.
|
||||
- Reviewer should confirm `Date.now()` is only called at the call site (Step 3),
|
||||
not inside the predicate, so the helper stays testable.
|
||||
@@ -1,191 +0,0 @@
|
||||
# Plan 006: Stop re-mapping the font list per combobox instance; defer the font-metadata payload
|
||||
|
||||
> **Executor instructions**: Follow step by step. Confirm each verification.
|
||||
> Honor "STOP conditions". When done, update the status row in `plans/README.md`.
|
||||
>
|
||||
> **Drift check (run first)**:
|
||||
> `git diff --stat 73daf22b2..HEAD -- apps/web/src/components/typography/combobox.tsx apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/typography.tsx packages/fonts/src/index.ts`
|
||||
> If any changed, compare "Current state" excerpts against live code; on
|
||||
> mismatch, STOP.
|
||||
|
||||
## Status
|
||||
|
||||
- **Priority**: P2
|
||||
- **Effort**: S
|
||||
- **Risk**: LOW
|
||||
- **Depends on**: none
|
||||
- **Category**: perf
|
||||
- **Planned at**: commit `73daf22b2`, 2026-07-08
|
||||
|
||||
## Why this matters
|
||||
|
||||
`packages/fonts/src/webfontlist.json` is a **476 KB** metadata file (7,526
|
||||
lines). `packages/fonts/src/index.ts:7` imports it statically and derives
|
||||
`fontList` (~506 entries). The typography combobox
|
||||
(`apps/web/src/components/typography/combobox.tsx:11-24`) maps that full list
|
||||
into option objects inside a `useMemo(..., [])` — which memoizes **per component
|
||||
instance**, not globally. Two instances render (body + heading font pickers), so
|
||||
the 506-entry map runs twice at mount for identical output.
|
||||
|
||||
Two honest, bounded improvements:
|
||||
1. **Guaranteed win**: hoist the options mapping to module scope so it runs
|
||||
once per process regardless of instance count. Small but free and correct.
|
||||
2. **Payload win (conditional)**: the font metadata only matters where the font
|
||||
picker renders (the builder typography panel). Deferring it keeps it out of
|
||||
any route/panel that never opens the picker — *if* the bundler isn't already
|
||||
forced to load it there for another reason (PDF font registration also
|
||||
consumes `@reactive-resume/fonts`). Step 3 measures before changing, and
|
||||
reports back rather than forcing a change that yields nothing.
|
||||
|
||||
Scope this realistically: #1 is certain; #2 is worth doing only if the build
|
||||
shows the payload actually lands on a route that doesn't need it.
|
||||
|
||||
## Current state
|
||||
|
||||
- `apps/web/src/components/typography/combobox.tsx:1-27` — `FontFamilyCombobox`:
|
||||
|
||||
```ts
|
||||
import { fontList, getFont, getFontDisplayName, getFontSearchKeywords, sortFontWeights } from "@reactive-resume/fonts";
|
||||
// ...
|
||||
export function FontFamilyCombobox({ className, ...props }: FontFamilyComboboxProps) {
|
||||
const options = useMemo(() => {
|
||||
return fontList.map((font) => ({
|
||||
value: font.family,
|
||||
keywords: getFontSearchKeywords(font.family),
|
||||
label: <FontDisplay family={font.family} label={getFontDisplayName(font.family)} type={font.type}
|
||||
url={"preview" in font ? font.preview : undefined} />,
|
||||
}));
|
||||
}, []);
|
||||
return <Combobox {...props} options={options} className={cn("w-full", className)} />;
|
||||
}
|
||||
```
|
||||
|
||||
The `label` is JSX (`<FontDisplay .../>`), so the mapped array holds React
|
||||
elements — hoisting must keep the elements' props static (they are: derived
|
||||
purely from `font`). Nothing in the map depends on component props.
|
||||
|
||||
- **Single usage site**:
|
||||
`apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/typography.tsx:14`
|
||||
imports and renders `FontFamilyCombobox` (line ~106) and `FontWeightCombobox`
|
||||
(line ~133). No other file uses these components.
|
||||
|
||||
- `packages/fonts/src/index.ts` — statically imports `webfontlist.json` (line 7)
|
||||
and exports `fontList`, `getFont`, `getFontDisplayName`,
|
||||
`getFontSearchKeywords`, `sortFontWeights` (used by the combobox) plus
|
||||
registration helpers used by `packages/pdf`
|
||||
(`packages/pdf/src/hooks/use-register-fonts.ts`).
|
||||
|
||||
- Bundler note: TanStack Router has `autoCodeSplitting`; per prior analysis the
|
||||
font metadata resolves into the `pdf-document` chunk, not the main entry.
|
||||
This is exactly why Step 3 measures before assuming a payload win exists.
|
||||
|
||||
## Commands you will need
|
||||
|
||||
| Purpose | Command | Expected |
|
||||
|-----------------|---------------------------------------------------|---------------------|
|
||||
| Typecheck (web) | `pnpm --filter web typecheck` | exit 0 |
|
||||
| Web tests | `pnpm --filter web test -- typography` | all pass (or none) |
|
||||
| Build (Step 3) | `pnpm --filter web build` | exit 0; chunk sizes printed |
|
||||
|
||||
(Do NOT run `pnpm check`.)
|
||||
|
||||
## Scope
|
||||
|
||||
**In scope**:
|
||||
- `apps/web/src/components/typography/combobox.tsx`
|
||||
- `apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/typography.tsx`
|
||||
(only if Step 3 justifies lazy-loading)
|
||||
- `plans/README.md` (status row)
|
||||
|
||||
**Out of scope**:
|
||||
- `packages/fonts/*` — do not restructure the fonts package or change what
|
||||
`@reactive-resume/fonts` exports. The win here is on the web-app consumer side.
|
||||
- `packages/pdf` font registration — unrelated consumer.
|
||||
- The 476 KB JSON content itself.
|
||||
|
||||
## Git workflow
|
||||
|
||||
- Branch: `advisor/006-font-combobox`
|
||||
- Commit: `perf(web): compute font-picker options once at module scope`
|
||||
- Do NOT push or open a PR unless instructed.
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Hoist the font options to module scope
|
||||
|
||||
In `combobox.tsx`, move the `fontList.map(...)` computation out of the component
|
||||
into a module-level constant (e.g. `const FONT_FAMILY_OPTIONS = fontList.map(...)`)
|
||||
and pass that constant to `<Combobox options={FONT_FAMILY_OPTIONS} />`. Remove
|
||||
the now-unnecessary `useMemo` for `FontFamilyCombobox` (its dep array is empty
|
||||
and the value is now module-constant). Leave `FontWeightCombobox` unchanged —
|
||||
its options depend on the `fontFamily` prop and must stay per-instance.
|
||||
|
||||
**Verify**: `pnpm --filter web typecheck` → exit 0.
|
||||
|
||||
### Step 2: Confirm the picker still renders
|
||||
|
||||
If a typography test exists, run it; otherwise this is verified by typecheck +
|
||||
the build in Step 3. Ensure `FontDisplay` still receives the same props.
|
||||
|
||||
**Verify**: `pnpm --filter web test -- typography` → passes or reports no tests.
|
||||
|
||||
### Step 3: Measure, then decide on deferral (investigate — may end in a report)
|
||||
|
||||
Run `pnpm --filter web build` and note the chunk that contains the font
|
||||
metadata (search the build output / `apps/web/dist/assets` for the large
|
||||
chunk; the 476 KB JSON shows up as a ~400–500 KB contribution). Determine
|
||||
whether that chunk loads on a route that does **not** need the font picker
|
||||
(e.g. the dashboard or a public resume page).
|
||||
|
||||
- **If the font metadata already only loads with the PDF/preview chunk that the
|
||||
builder needs anyway** → deferring the combobox yields ~nothing. STOP and
|
||||
report this finding in `plans/README.md`'s status note; do not add lazy
|
||||
loading. Step 1 stands on its own as the deliverable.
|
||||
- **If the font metadata loads on a route with no font picker** → wrap
|
||||
`FontFamilyCombobox`/`FontWeightCombobox` at the `typography.tsx` usage site in
|
||||
`React.lazy` + `Suspense` (import the combobox module via dynamic `import()`),
|
||||
so `@reactive-resume/fonts` is fetched only when the typography panel mounts.
|
||||
Keep the fallback minimal (the existing field skeleton or a small spinner).
|
||||
|
||||
**Verify**: `pnpm --filter web build` → exit 0; record the before/after chunk
|
||||
observation in the PR description.
|
||||
|
||||
## Test plan
|
||||
|
||||
- Step 1 is behavior-preserving; typecheck + build are the gates.
|
||||
- If Step 3 adds lazy loading, manually confirm (or via an existing e2e that
|
||||
opens the typography panel) that the font picker still populates.
|
||||
- No new unit test is required for a pure hoist; if `typography.tsx` gains a
|
||||
Suspense boundary, ensure any existing builder e2e still passes.
|
||||
|
||||
## Done criteria
|
||||
|
||||
Machine-checkable. ALL must hold:
|
||||
|
||||
- [ ] `grep -n "FONT_FAMILY_OPTIONS\|const .*= fontList.map" apps/web/src/components/typography/combobox.tsx` shows a module-scope constant
|
||||
- [ ] `FontFamilyCombobox` no longer wraps the family options in `useMemo`
|
||||
- [ ] `pnpm --filter web typecheck` exits 0
|
||||
- [ ] `pnpm --filter web build` exits 0
|
||||
- [ ] `git status --porcelain` lists only in-scope files + `plans/README.md`
|
||||
- [ ] `plans/README.md` status row for 006 updated (note whether Step 3 added
|
||||
lazy loading or reported it unnecessary)
|
||||
|
||||
## STOP conditions
|
||||
|
||||
Stop and report if:
|
||||
|
||||
- Hoisting breaks the `FontDisplay` label rendering (elements need per-instance
|
||||
data) — re-check; the current props are purely `font`-derived, so this should
|
||||
not happen. If it does, the component drifted.
|
||||
- Step 3's measurement is ambiguous or the build doesn't surface chunk sizes —
|
||||
report what you observed rather than guessing; do not add lazy loading on a
|
||||
hunch.
|
||||
|
||||
## Maintenance notes
|
||||
|
||||
- This is the lowest-leverage item in the current batch; Step 1 is the sure
|
||||
thing. Do not over-engineer a fonts-package split — that has broad blast
|
||||
radius (PDF registration depends on the same exports) and isn't justified by
|
||||
the payload analysis.
|
||||
- If the fonts list ever grows substantially or becomes user-configurable,
|
||||
revisit a proper lazy/virtualized font source.
|
||||
@@ -1,279 +0,0 @@
|
||||
# Plan 007: Extract a shared template page-shell — spike + one-template pilot
|
||||
|
||||
> **Executor instructions**: This is a **spike + pilot**, not a 15-file
|
||||
> rewrite. You will build a parity net, refactor exactly ONE template behind it,
|
||||
> and then STOP and report. Do NOT migrate the other templates in this plan.
|
||||
> Run every verification. Honor "STOP conditions". When done, update the status
|
||||
> row in `plans/README.md`.
|
||||
>
|
||||
> **Drift check (run first)**:
|
||||
> `git diff --stat 73daf22b2..HEAD -- packages/pdf/src/templates/`
|
||||
> If the templates changed since this plan, compare "Current state" against
|
||||
> live code before proceeding; on mismatch, STOP.
|
||||
|
||||
## Status
|
||||
|
||||
- **Priority**: P2
|
||||
- **Effort**: L
|
||||
- **Risk**: HIGH
|
||||
- **Depends on**: 001 recommended as the characterization-testing exemplar/
|
||||
discipline (not a technical blocker — the PDF layer needs its own parity net,
|
||||
which is Step 1 here).
|
||||
- **Category**: tech-debt
|
||||
- **Planned at**: commit `73daf22b2`, 2026-07-08
|
||||
|
||||
## Why this matters
|
||||
|
||||
All 15 resume templates
|
||||
(`packages/pdf/src/templates/<name>/<Name>Page.tsx`) independently reimplement
|
||||
the **same page-shell orchestration**: compute `getTemplateMetrics`,
|
||||
`getTemplatePageSize`, `getTemplatePageMinHeightStyle`, `shouldShowResumeHeader`,
|
||||
`hasTemplatePicture`, `filterSections(page.main/sidebar)`, then render
|
||||
`<Page>` → `<TemplateProvider>` → a two-column `layout` → sidebar/main columns
|
||||
that `.map` over `<Section>`. Git history proves the cost: a single header
|
||||
line-height change touched 14 template files (commit `1b0bb067b`); the free-form
|
||||
layout feature touched 14 (`2cd774dab`). Every new template copies this again,
|
||||
and drift between copies is invisible until someone diffs all 15.
|
||||
|
||||
The goal is a shared, prop-/callback-driven `TemplatePageShell` that owns the
|
||||
orchestration while each template keeps its own styles and decorative choices.
|
||||
**Because the visual risk is high and no render-parity test net exists for
|
||||
templates today, this plan proves the abstraction on ONE template (Pikachu)
|
||||
behind a characterization snapshot, then stops for review before any rollout.**
|
||||
|
||||
## Current state
|
||||
|
||||
- `packages/pdf/src/templates/pikachu/PikachuPage.tsx` — the pilot. Structure
|
||||
(lines 58–110): the `PikachuPage` component computes the shared metrics/flags
|
||||
and renders the shell; a local `Header` component (lines 112–161) renders
|
||||
`basics.name`/`headline` + the shared contact-item components; a
|
||||
`usePikachuTemplate` hook (lines 163–292) builds the per-template `StyleSheet`.
|
||||
The orchestration block, verbatim:
|
||||
|
||||
```tsx
|
||||
export const PikachuPage = ({ page, pageIndex }: TemplatePageProps) => {
|
||||
const data = useRender();
|
||||
const { metadata, picture } = data;
|
||||
const { colors, styles } = usePikachuTemplate();
|
||||
const metrics = getTemplateMetrics(metadata.page);
|
||||
const pageSize = getTemplatePageSize(metadata.page.format);
|
||||
const pageMinHeightStyle = getTemplatePageMinHeightStyle(metadata.page.format);
|
||||
const showHeader = shouldShowResumeHeader(data, pageIndex);
|
||||
const showSidebar = !page.fullWidth;
|
||||
const hasPicture = hasTemplatePicture(picture);
|
||||
const mainSections = filterSections(page.main, data);
|
||||
const sidebarSections = filterSections(page.sidebar, data);
|
||||
|
||||
return (
|
||||
<Page size={pageSize} style={composeStyles(styles.page, pageMinHeightStyle)}>
|
||||
<TemplateProvider styles={styles} colors={colors} features={pikachuFeatures}>
|
||||
<View style={styles.layout}>
|
||||
{showSidebar && (
|
||||
<View style={composeStyles(styles.sidebarColumn, { width: `${metadata.layout.sidebarWidth}%`, rowGap: metrics.sectionGap })}>
|
||||
{showHeader && showSidebar && hasPicture && <Image src={picture.url} style={styles.picture} />}
|
||||
<View style={composeStyles(styles.sidebarContent, { rowGap: metrics.sectionGap })}>
|
||||
{sidebarSections.map((s) => <Section key={s} section={s} placement="sidebar" />)}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<View style={composeStyles(styles.mainColumn, { rowGap: metrics.sectionGap })}>
|
||||
{showHeader && (
|
||||
<View style={styles.headerRow}>
|
||||
{showHeader && !showSidebar && hasPicture && <Image src={picture.url} style={styles.picture} />}
|
||||
<Header styles={styles} colors={colors} />
|
||||
</View>
|
||||
)}
|
||||
<View style={{ rowGap: metrics.sectionGap }}>
|
||||
{mainSections.map((s) => <Section key={s} section={s} placement="main" />)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</TemplateProvider>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**Template-specific variation you must preserve** (this is why extraction is
|
||||
risky — the shell cannot assume one layout): Pikachu wraps its `Header` in a
|
||||
colored box and places the picture *beside* the header in the main column when
|
||||
there's no sidebar, but *above* the sidebar sections when there is. Other
|
||||
templates differ: Azurill (`AzurillPage.tsx`) uses `flexBasis` for sidebar
|
||||
width instead of a `width` percentage; Onyx (`OnyxPage.tsx`) is single-column
|
||||
header-on-top; Gengar (`GengarPage.tsx`) calls `getFeaturedSummaryLayout`.
|
||||
The shared shell must expose enough seams (render callbacks / slots) that each
|
||||
template keeps these differences — do not flatten them into one hardcoded
|
||||
layout.
|
||||
|
||||
- Shared helpers already exist under `packages/pdf/src/templates/shared/`:
|
||||
`metrics.ts`, `page-size.ts`, `cover-letter.ts` (`shouldShowResumeHeader`),
|
||||
`filtering.ts`, `picture.ts`, `contact-item.tsx`, `context.tsx`
|
||||
(`TemplateProvider`), `sections.tsx` (`Section`), `styles.ts` (`composeStyles`).
|
||||
The extraction consolidates the *orchestration that wires these together*, not
|
||||
the helpers themselves.
|
||||
|
||||
- **No render-parity test exists per template.** The only per-template test is
|
||||
`templates/scizor/ScizorPage.test.ts`, which greps source text — not a
|
||||
structural snapshot. You will build a real parity net in Step 1.
|
||||
|
||||
- `packages/pdf/src/document.ts` — defines `TemplatePageProps` and
|
||||
`TemplatePage`; `templates/index.ts` maps template name → page component.
|
||||
|
||||
## Commands you will need
|
||||
|
||||
| Purpose | Command | Expected |
|
||||
|----------------|---------------------------------------------------------------|--------------------|
|
||||
| Typecheck | `pnpm --filter @reactive-resume/pdf typecheck` | exit 0 |
|
||||
| PDF tests | `pnpm --filter @reactive-resume/pdf test` | all pass |
|
||||
| Pilot snapshot | `pnpm --filter @reactive-resume/pdf test -- pikachu` | all pass |
|
||||
|
||||
(Do NOT run `pnpm check`.)
|
||||
|
||||
## Scope
|
||||
|
||||
**In scope**:
|
||||
- A new shared shell, e.g. `packages/pdf/src/templates/shared/page-shell.tsx`
|
||||
(create) and its type additions in `shared/types.ts` if needed.
|
||||
- `packages/pdf/src/templates/pikachu/PikachuPage.tsx` (the ONE pilot migration)
|
||||
- A new parity snapshot test:
|
||||
`packages/pdf/src/templates/pikachu/PikachuPage.test.tsx` (create)
|
||||
- `plans/README.md` (status row + a note on the pilot outcome)
|
||||
|
||||
**Out of scope (hard stop)**:
|
||||
- The other 14 `*Page.tsx` templates — **do not touch them in this plan.** Their
|
||||
migration is explicit follow-up, gated on this pilot's review.
|
||||
- The shared helpers' internals (`metrics.ts`, `filtering.ts`, etc.).
|
||||
- `templates/index.ts` mapping — Pikachu's export name/signature must not change.
|
||||
- Any visual/styling change — this is a pure structural extraction; the pilot's
|
||||
rendered output must be byte-identical to before.
|
||||
|
||||
## Git workflow
|
||||
|
||||
- Branch: `advisor/007-template-shell-spike`
|
||||
- Commit style: conventional commits, e.g.
|
||||
`refactor(pdf): extract shared template page-shell; pilot on pikachu`.
|
||||
- Do NOT push or open a PR unless instructed.
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Build the parity net for Pikachu (BEFORE refactoring)
|
||||
|
||||
Create `packages/pdf/src/templates/pikachu/PikachuPage.test.tsx` that renders
|
||||
`PikachuPage` with a fixed sample resume and metadata and snapshots the produced
|
||||
element tree. Approach:
|
||||
|
||||
- Use `react-test-renderer` (already available transitively; if not, use the
|
||||
approach the existing `packages/pdf/src/browser.test.tsx` / `server.test.tsx`
|
||||
use to exercise components) to render the template wrapped in whatever context
|
||||
`useRender()` needs. Read `packages/pdf/src/context.tsx` to see how to provide
|
||||
the render context in a test (there may be a provider/helper the existing
|
||||
tests use — reuse it).
|
||||
- Feed it `sampleResumeData` from `@reactive-resume/schema/resume/sample` (used
|
||||
by `server.test.tsx`) and a representative `page` prop (one with a sidebar,
|
||||
one full-width — two snapshots).
|
||||
- Snapshot the tree with `expect(tree.toJSON()).toMatchSnapshot()`.
|
||||
|
||||
Run it to generate the baseline snapshot **against the current, un-refactored
|
||||
PikachuPage**.
|
||||
|
||||
**Verify**: `pnpm --filter @reactive-resume/pdf test -- pikachu` → passes and
|
||||
writes a `__snapshots__` file. Commit this snapshot as the baseline.
|
||||
|
||||
> If you cannot render the template in a test without excessive/fragile mocking
|
||||
> (react-pdf host primitives don't cooperate with the test renderer), STOP and
|
||||
> report — a structural refactor with no parity net is exactly what this plan
|
||||
> refuses to ship. Do not proceed to Step 2 without a working baseline.
|
||||
|
||||
### Step 2: Extract the shared shell
|
||||
|
||||
Create `shared/page-shell.tsx` exporting a `TemplatePageShell` component (or a
|
||||
hook + component pair) that encapsulates the orchestration from the excerpt:
|
||||
computing `metrics`/`pageSize`/`pageMinHeightStyle`/`showHeader`/`showSidebar`/
|
||||
`hasPicture`/`mainSections`/`sidebarSections`, and rendering the
|
||||
`<Page><TemplateProvider>...<View layout>` scaffold. Expose the per-template
|
||||
variation through props/render-callbacks, at minimum:
|
||||
- `styles`, `colors`, `features` (passed through to `TemplateProvider` and used
|
||||
by the scaffold).
|
||||
- `renderHeader` / `renderPicture` callbacks (or slot props) so Pikachu can keep
|
||||
its colored-box header and beside/above picture placement.
|
||||
- The sidebar-width strategy as data (Pikachu passes a `width` %; Azurill will
|
||||
later pass `flexBasis`) — model it so both fit without the shell hardcoding one.
|
||||
|
||||
Keep the shell's public surface minimal and documented with a short comment.
|
||||
|
||||
**Verify**: `pnpm --filter @reactive-resume/pdf typecheck` → exit 0.
|
||||
|
||||
### Step 3: Migrate Pikachu onto the shell
|
||||
|
||||
Rewrite `PikachuPage.tsx` so `PikachuPage` delegates its orchestration to
|
||||
`TemplatePageShell`, passing `usePikachuTemplate()`'s styles/colors, the
|
||||
`pikachuFeatures`, and its `Header`/picture rendering via the callbacks. Keep
|
||||
`usePikachuTemplate` and the `Header` component in the Pikachu file (styles stay
|
||||
template-owned). The export name and `TemplatePageProps` signature must not
|
||||
change.
|
||||
|
||||
**Verify**:
|
||||
- `pnpm --filter @reactive-resume/pdf test -- pikachu` → the Step 1 snapshot
|
||||
**still matches** (identical tree). If the snapshot changed, the refactor
|
||||
altered output — investigate and reconcile; do NOT blindly update the
|
||||
snapshot.
|
||||
- `pnpm --filter @reactive-resume/pdf test` → the full PDF suite passes.
|
||||
- `pnpm --filter @reactive-resume/pdf typecheck` → exit 0.
|
||||
|
||||
### Step 4: STOP and report — do not roll out
|
||||
|
||||
Write a short note in `plans/README.md`'s status area (or the PR description)
|
||||
covering: whether the shell's seams were sufficient for Pikachu without style
|
||||
regressions, what the shell's public API ended up being, and which of the other
|
||||
14 templates look like clean fits vs. which have layouts (e.g. Onyx single
|
||||
column, Gengar featured-summary) that will need extra seams. This note is the
|
||||
input for the reviewed rollout plans. **Do not migrate any other template.**
|
||||
|
||||
## Test plan
|
||||
|
||||
- New `PikachuPage.test.tsx` with two snapshots (with-sidebar, full-width),
|
||||
established as a baseline in Step 1 and asserted unchanged after Step 3.
|
||||
- Full `packages/pdf` suite must remain green.
|
||||
- Verification: `pnpm --filter @reactive-resume/pdf test` → all pass; the
|
||||
Pikachu snapshot is unchanged between baseline and post-refactor.
|
||||
|
||||
## Done criteria
|
||||
|
||||
Machine-checkable. ALL must hold:
|
||||
|
||||
- [ ] `packages/pdf/src/templates/shared/page-shell.tsx` exists and is imported by `PikachuPage.tsx`
|
||||
- [ ] `packages/pdf/src/templates/pikachu/PikachuPage.test.tsx` + its `__snapshots__` exist
|
||||
- [ ] `pnpm --filter @reactive-resume/pdf test` exits 0 (snapshot unchanged post-refactor)
|
||||
- [ ] `pnpm --filter @reactive-resume/pdf typecheck` exits 0
|
||||
- [ ] `git diff --name-only` shows **exactly one** `*Page.tsx` changed (pikachu); no other template file modified
|
||||
- [ ] `plans/README.md` status row for 007 updated with the pilot-outcome note
|
||||
- [ ] `grep -rL page-shell packages/pdf/src/templates/*/[A-Z]*Page.tsx` still lists 14 templates (i.e. only Pikachu adopted it)
|
||||
|
||||
## STOP conditions
|
||||
|
||||
Stop and report back (do not improvise) if:
|
||||
|
||||
- You cannot build a working render-parity snapshot in Step 1 (Step 1's own
|
||||
stop clause) — no net, no refactor.
|
||||
- The Pikachu snapshot changes after Step 3 and you cannot make it identical —
|
||||
the extraction is not behavior-preserving; report the diff instead of
|
||||
updating the snapshot to match.
|
||||
- Making the shell fit Pikachu forces the abstraction to also encode a second
|
||||
template's layout (you find yourself designing for Onyx/Gengar mid-pilot) —
|
||||
stop; the pilot's job is to prove one clean seam set, not to pre-solve all 15.
|
||||
- Any change would touch a template other than Pikachu — that's the rollout,
|
||||
which is out of scope.
|
||||
|
||||
## Maintenance notes
|
||||
|
||||
- Rollout is deliberately deferred. After this pilot is reviewed and merged,
|
||||
create follow-up plans that migrate the remaining templates in small reviewed
|
||||
batches (group by layout family: full-width/single-column like Onyx; sidebar-%
|
||||
like Pikachu; flexBasis like Azurill; featured-summary like Gengar). Each
|
||||
batch gets its own baseline snapshot first.
|
||||
- The shell's public API is load-bearing for 14 future migrations — a reviewer
|
||||
should scrutinize its seam design (are the render callbacks expressive enough?)
|
||||
more than the Pikachu diff itself.
|
||||
- Prior maintainer guidance: template rewrites should be spike-gated and must
|
||||
not break the react-pdf/DOCX/JSON export pipeline. This plan honors that by
|
||||
keeping styles template-owned and asserting identical render output.
|
||||
-171
@@ -1,171 +0,0 @@
|
||||
# Implementation Plans
|
||||
|
||||
Generated by the `improve` skill (deep audit) on 2026-07-08, against commit
|
||||
`73daf22b2`. A deep 8-category read-only audit produced 53 raw findings; after
|
||||
adversarial verification and manual vetting, the user selected the items below
|
||||
to turn into executor plans.
|
||||
|
||||
Each executor: read your plan fully before starting, honor its STOP conditions,
|
||||
and update your row when done. Plans are self-contained — you do not need the
|
||||
audit or this repo's history to execute one.
|
||||
|
||||
## Execution order & status
|
||||
|
||||
| Plan | Title | Priority | Effort | Risk | Depends on | Status |
|
||||
|------|-------|----------|--------|------|------------|--------|
|
||||
| 001 | Characterization tests for the resume service | P1 | M | LOW | — | DONE (branch `advisor/resume-service-improvements`) |
|
||||
| 002 | CSP + framing headers on web pages; gate uploads CORS | P1 | S | LOW | — | DONE (branch `advisor/security-and-docs`) |
|
||||
| 003 | Fix silent-success mutations; bound bulk-op inputs | P1 | S | LOW | 001 (soft) | DONE (branch `advisor/resume-service-improvements`) |
|
||||
| 004 | Correct README "Custom CSS" claim | P3 | S | LOW | — | DONE (branch `advisor/security-and-docs`) |
|
||||
| 005 | Deduplicate public-resume view-count writes | P2 | M | MED | 001 (soft) | DONE (branch `advisor/resume-service-improvements`) |
|
||||
| 006 | Font-picker options once; defer font payload | P2 | S | LOW | — | DONE (branch `advisor/font-payload`; Step-1 hoist only — build measurement confirmed the payload already loads only with the PDF chunk, so lazy-loading was correctly skipped) |
|
||||
| 007 | Extract shared template page-shell — spike + pilot | P2 | L | HIGH | 001 (discipline) | REVERTED from integration (user decision) — spike executed & proved the recipe, but full rollout judged not worth it; Pikachu reverted for template uniformity. Work preserved on branch `advisor/template-shell-spike`. |
|
||||
|
||||
Status values: TODO | IN PROGRESS | DONE | BLOCKED (one-line reason) | REJECTED (one-line rationale)
|
||||
|
||||
### Recommended order
|
||||
|
||||
Independent quick wins first (002, 004), then the tests-first foundation (001)
|
||||
before the behavior change that depends on it (003) and the caller change with
|
||||
a safety net (005). 006 is independent and low-leverage — do it anytime. 007
|
||||
(template refactor) is last and is a **spike + one-template pilot that stops for
|
||||
review**, not a full rollout.
|
||||
|
||||
Fastest safe sequence: **002 → 004 → 001 → 003 → 005 → 006 → 007**.
|
||||
|
||||
## Execution results (2026-07-08)
|
||||
|
||||
All 7 plans were executed by isolated-worktree executor subagents and reviewed
|
||||
by the advisor (re-ran every done-criterion, scope-checked, read diffs, audited
|
||||
tests). All **APPROVED**. Four branches, all based on `73daf22b2`:
|
||||
|
||||
| Branch | Plans | Package(s) | Verify (re-run by reviewer) |
|
||||
|--------|-------|------------|------------------------------|
|
||||
| `advisor/security-and-docs` | 002, 004 | apps/server, README | typecheck 0; `server` static tests 36/36 |
|
||||
| `advisor/font-payload` | 006 | apps/web | typecheck 0; web suite 457/457; build 0 |
|
||||
| `advisor/resume-service-improvements` | 001, 003, 005 | packages/api | typecheck 0; api suite 178/178; boundaries clean |
|
||||
| `advisor/template-shell-spike` | 007 | packages/pdf | typecheck 0; pdf suite 264/264; boundaries clean; parity snapshot byte-identical pre/post refactor |
|
||||
|
||||
**Post-execution decision (user, 2026-07-08):** Local integration branch
|
||||
`improve-integration` created off `main` with plans **001–006 merged**. Plan
|
||||
**007 was reverted** from it (the spike concluded a full 14-template rollout
|
||||
isn't worth it — the shell only centralizes layout orchestration, not the
|
||||
per-template style hooks where most recurring edits land; 6 templates are
|
||||
single-column and don't fit; consistency argues against a partial rollout). The
|
||||
`advisor/template-shell-spike` branch is kept if 007 is ever revisited.
|
||||
Known unrelated red: `packages/api/.../storage/service.test.ts` fails on the
|
||||
pinned **vitest 4.1.9** (`.rejects.toThrow` matcher `TypeError`, fixed in 4.1.10)
|
||||
— present on base `main` too, in a file no plan touches; the vitest suite isn't
|
||||
in CI, so it's latent. Bumping vitest to 4.1.10 clears it (a separate base-repo
|
||||
concern, left untouched).
|
||||
|
||||
**Integration notes for the user (merging is your call — the advisor never merges):**
|
||||
- The four branches touch **disjoint packages**, so they can be merged in any
|
||||
order with no cross-branch source conflicts. The only shared file is
|
||||
`pnpm-lock.yaml`, touched **only** by `advisor/template-shell-spike` — no
|
||||
lockfile conflict between branches.
|
||||
- **All commits are unsigned** (`--no-gpg-sign`): the executors ran headless and
|
||||
the repo's 1Password SSH signer needs interactive auth. Re-sign on merge if
|
||||
branch protection requires signed commits.
|
||||
- **007 adds test-only devDeps** (`react-dom`, `@types/react-dom`) to
|
||||
`packages/pdf` + a `vitest.config.ts` transform tweak — both were required to
|
||||
build the render-parity snapshot the spike depends on (react-test-renderer,
|
||||
which the plan suggested, is React-19-incompatible). Accept these consciously.
|
||||
- **007 is a pilot**: only Pikachu is migrated. The executor's recommended
|
||||
rollout batches for the other 14 templates (each snapshotted first):
|
||||
1. sidebar-% family (bronzor, ditto, glalie, leafish; likely kakuna, lapras,
|
||||
meowth, rhyhorn, scizor — verify two-column shape) — trivial, pass
|
||||
`sidebarColumnStyle={{ width }}`.
|
||||
2. flexBasis family (azurill, chikorita) — pass `sidebarColumnStyle={{ flexBasis }}`;
|
||||
the existing seam already covers it.
|
||||
3. featured-summary family (ditgar, gengar) — needs a new section-rendering
|
||||
seam; do NOT force into the current shell.
|
||||
4. single-column/header-on-top (onyx) — needs a separate shell/layout variant;
|
||||
the current shell hardcodes the two-column scaffold.
|
||||
|
||||
## Dependency notes
|
||||
|
||||
- **003 depends softly on 001**: 003 flips the three `setLocked`/`setPassword`/
|
||||
`removePassword` methods from silent-return to throwing `NOT_FOUND`. If 001 is
|
||||
DONE, its Step 3 assertions must be updated in 003's PR (that test diff is the
|
||||
intended proof of the behavior change). If 001 is not done, 003 adds a minimal
|
||||
test itself.
|
||||
- **005 depends softly on 001**: 001's Step 5 characterizes
|
||||
`statistics.increment`; 005 changes the *caller* (dedup), not the write body,
|
||||
so that test should keep passing — a useful regression signal.
|
||||
- **007 is not technically blocked** by 001 (the PDF layer needs its own render-
|
||||
parity net, which 007 builds in Step 1), but 001 should be done first as the
|
||||
characterization-testing exemplar. 007 explicitly does NOT roll out to all 15
|
||||
templates; rollout is deferred to reviewed follow-up plans.
|
||||
|
||||
## Findings considered and rejected
|
||||
|
||||
Recorded so they aren't re-audited next run. (Refuted by adversarial
|
||||
verification or downgraded during manual vetting against the code.)
|
||||
|
||||
- **"Production secrets committed to repository"** — FALSE. `.env.production` /
|
||||
`.env.local` exist in the working tree but are gitignored (`.env*` with an
|
||||
`.env.example` exception); 0 commits touch them; not in history.
|
||||
- **"Vulnerable @better-auth/oauth-provider (unbound resource indicators)"** —
|
||||
the claimed advisory (GHSA-p2fr-6hmx-4528) could not be verified as real, and
|
||||
the code already configures `validAudiences`. Treat any `@better-auth/*` bump
|
||||
as routine, not a security fix.
|
||||
- **Unauthenticated OAuth2 dynamic client registration bypass** — refuted;
|
||||
three validation layers exist (registration-time, server preflight,
|
||||
authorization endpoint).
|
||||
- **bulkUpdate tag-addition race** — refuted; Postgres row-locking serializes
|
||||
the `UPDATE ... SET tags = <subquery>` correctly.
|
||||
- **agent `getThread` missing ownership check** — refuted; the `WHERE` clause
|
||||
filters `userId` and throws `NOT_FOUND` otherwise.
|
||||
- **Snapshot throttle "bypassed" by AI patches** — deliberate, documented
|
||||
design; pruning is atomic and bounded at 30 versions, not a reactive overflow.
|
||||
Not a bug.
|
||||
- **Missing index on `(user_id, isPublic)`** — no code path filters resumes by
|
||||
public status; speculative (YAGNI).
|
||||
- **Stats daily-series "re-queries every 30 days after cache"** — refuted;
|
||||
TanStack Query 60s staleTime already dedups.
|
||||
- **Large composite components / sections.tsx / rich-input / custom-styles
|
||||
"need splitting"** (ARCH-02/05/06/07) — refuted; each is a single-responsibility
|
||||
internal-only component with no external reuse; a July-2026 audit already chose
|
||||
not to split `sections.tsx`.
|
||||
- **Export paths duplicate filtering (ARCH-03)** — refuted; PDF/DOCX/Markdown
|
||||
have genuinely different filtering needs, not a shared abstraction being
|
||||
reinvented.
|
||||
- **`node-html-parser` major-version lag** — refuted; only stable DOM APIs used,
|
||||
no breaking-change exposure.
|
||||
- **PORT not validated in server.ts (DX-03)** — refuted; the server *does* read
|
||||
`process.env.PORT` in production (`apps/server/src/index.ts:13`).
|
||||
|
||||
Downgraded to notes (real but low-leverage; not planned unless revisited):
|
||||
|
||||
- **Rich-text HTML not sanitized before react-pdf-html** (SEC-07) — PDF-only
|
||||
surface (browser path uses `stripHtml`; PDFs don't execute scripts), on the
|
||||
user's own content. Defense-in-depth only.
|
||||
- **MCP tools don't validate API-key scopes** (SEC-05) — latent; scoped keys
|
||||
can't be created yet. Revisit if/when scoped API keys ship.
|
||||
- **Applications attachment cleanup: concurrent-write orphans + swallowed
|
||||
`Promise.allSettled` failures** (CORRECT-02/03) — real but low severity for
|
||||
single-user; storage hygiene + observability. Candidate for a future plan.
|
||||
- **Agent service is 1397 LOC mixing concerns** (ARCH-04) — real tech debt;
|
||||
large L-effort refactor, deferred.
|
||||
- **Core deps pinned to prerelease/snapshot** (drizzle-orm/kit rc, drizzle-zod
|
||||
snapshot hash, TS native-preview dev build) — watch item; de-risk when stable
|
||||
releases land. Not urgent (not on the CI/production critical path).
|
||||
- **Vite `chunkSizeWarningLimit: 10MB`** masks bundle bloat — trivial DX nit.
|
||||
- **Dev-setup docs vs AGENTS.md `dotenvx` drift** (DX-02) — minor docs friction.
|
||||
- **Applications bulk O(n) in-memory scan** — the input-cap in Plan 003 bounds
|
||||
abuse; the scan rewrite is deferred.
|
||||
|
||||
## Not audited
|
||||
|
||||
Honest coverage gaps from this run:
|
||||
|
||||
- E2E/Playwright test behavior and flakiness (relied on CI).
|
||||
- PDF/DOCX template *rendering* correctness beyond structure (no visual-
|
||||
regression harness exists).
|
||||
- Email/notification delivery reliability.
|
||||
- S3 backup/restore and multi-instance storage behavior.
|
||||
- The AI provider integration layer's retry/streaming internals.
|
||||
- Lingui extraction/locale-catalog completeness (separate track).
|
||||
- Deep dependency transitive-tree audit (worked from manifests + lockfile;
|
||||
`pnpm audit`/`pnpm outdated` network access was not assumed available).
|
||||
Reference in New Issue
Block a user