Tasks: 13 — Personalization & Continuous-Learning Loop (two-layer data contract)
Status legend:
[ ]pending •[~]in-progress •[x]done •[-]dropped
Phase 1 — Two-layer data model + reconciliation core
-
T01 — Create
user_feedbacktable (append-only event log)- Files:
packages/db/src/schema/user-feedback.ts; export frompackages/db/src/schema/index.ts - Acceptance:
- Table
user_feedbackuses house style:integer("id").primaryKey().generatedAlwaysAsIdentity();text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }); optionaljobId integer references jobs.id;kindviatext("kind", { enum: ["score_dispute","suggestion_accepted","suggestion_rejected","artifact_edited","outcome_recorded"] }).notNull();targetRef text(artifact/machine-summary id);value jsonb("value").$type<...>();timestamp("created_at").notNull().defaultNow(). - Index
user_feedback_user_created_idxon(userId, createdAt). - Exported and importable as
import { userFeedback } from "@ever-hust/db".
- Table
- Estimate: 0.5 day
- Files:
-
T02 — Create
user_overridestable (durable per-user preferences)- Files:
packages/db/src/schema/user-overrides.ts; export frompackages/db/src/schema/index.ts - Acceptance:
- One row per user:
userIdFK withonDelete: "cascade",unique("user_overrides_user_unique"). overrides jsonb("overrides").$type<UserOverrides>().default(...)holdingversion,weightOverrides,phrasingPrefs,alwaysRules,neverRules.timestamp("created_at")+timestamp("updated_at")both.notNull().defaultNow().- Exported as
import { userOverrides } from "@ever-hust/db".
- One row per user:
- Estimate: 0.5 day
- Files:
-
T03 — Push schema to the database
- Files: (no source) run
pnpm db:pushagainstpackages/db/drizzle.config.ts(schema./src/schema/index.ts) - Acceptance:
pnpm db:pushcompletes with no errors;user_feedbackanduser_overridesexist in the DB.pnpm db:studioshows both tables with the expected columns/indexes.
- Estimate: 0.5 day
- Files: (no source) run
-
T04 — Reconciliation core (
reconcile.ts) +UserOverridesZod type- Files:
packages/ai/src/learning/reconcile.ts - Acceptance:
- Exports a Zod
UserOverridesschema (versioned per #5) with.min()/.max()-clampedweightOverridesand.max()-bounded string arrays for phrasing/always/never rules. - Exports pure
reconcileOverrides(systemDefaults, userOverrides)→ user-wins merged result; no I/O, no DB access. - On version mismatch, applies a migration shim rather than throwing.
- Exports a Zod
- Estimate: 1 day
- Files:
-
T05 — Unit tests for schema typing + reconciliation (user-wins)
- Files:
packages/ai/src/learning/reconcile.test.ts;packages/db/src/schema/user-overrides.test.ts - Acceptance:
reconcile.test.ts: user override wins over a conflicting system default; empty overrides pass system defaults through unchanged; a simulated system-pack bump leaves the user-overrides input object unmutated (Layer-1 immutability).- Out-of-range weight override is clamped, not accepted raw.
pnpm test -- --selectProjects aiand--selectProjects dbgreen.
- Estimate: 0.5 day
- Files:
Phase 2 — Capture path (service + tool + API route)
-
T06 — Feedback read/write service
- Files:
packages/ai/src/learning/feedback-service.ts - Acceptance:
recordFeedback({ userId, kind, jobId?, targetRef?, value })inserts oneuser_feedbackrow viaimport { db } from "@ever-hust/db".getActiveOverrides(userId)returns the user'sUserOverrides(or an empty validated default if no row).proposeOverrideFromFeedback(userId, event)returns a proposed override (withconfidence) and does NOT writeuser_overrides(activation is a separate, explicit accept).
- Estimate: 1 day
- Files:
-
T07 —
recordFeedbackorchestrator tool + barrel export- Files:
packages/ai/src/tools/record-feedback.ts; exportrecordFeedbackToolfrompackages/ai/src/tools/index.ts - Acceptance:
tool({ description, inputSchema: z.object({...}).max()-bounded, execute })matching the existing 14-tool pattern;userIdis NOT in the LLM-facing schema (injected server-side).inputSchemaenforces.max()on every string/array (Article 8.3);kindis an enum matchinguser_feedback.kind.executedelegates tofeedback-service.recordFeedbackand returns a structured object ({ recorded: true, kind, proposedOverride?: {...} }).
- Estimate: 1 day
- Files:
-
T08 — Register
recordFeedbackin the orchestrator + document in the system prompt- Files:
packages/ai/src/agents/orchestrator.ts(add to thetools: { ... }map with server-sideuserId);packages/ai/src/prompts.ts(getOrchestratorPrompt, mirror in Langfuseorchestrator-system) - Acceptance:
recordFeedbackappears in thestreamTexttoolsmap, wrappingexecuteto inject{ ...params, userId }likefavoriteJob/savePreferences.MAX_AI_STEPS_PER_TURN/stepCountIs(5)unchanged.- System prompt lists the tool with when-to-use guidance (dispute a score, save an edit as style, record an outcome) and the human-in-the-loop note (propose, don't auto-apply).
- Estimate: 0.5 day
- Files:
-
T09 —
POST /api/feedbackroute + Zod body schema- Files:
apps/web/app/api/feedback/route.ts;feedbackBodySchemainapps/web/lib/api-schemas.ts - Acceptance:
- Route calls
requireSessionUser(), thenapplyRateLimit(userId, "authenticated"). - Body validated by
feedbackBodySchema(.max()-bounded); invalid →apiBadRequest(); failures →apiError()(fromapps/web/lib/api-response.ts). - Persists via
feedback-service.recordFeedback; never accepts a client-supplieduserId.
- Route calls
- Estimate: 1 day
- Files:
-
T10 — Unit tests: service, tool, route
- Files:
packages/ai/src/learning/feedback-service.test.ts;packages/ai/src/tools/record-feedback.test.ts;apps/web/app/api/feedback/route.test.ts - Acceptance:
- Service test: a recorded event persists;
getActiveOverridesreturns empty default for a new user;proposeOverrideFromFeedbackreturns a proposal and writes NO override row. - Tool test: schema rejects oversized input and any LLM-supplied
userId;executereturns the structured shape. - Route test: 401 without session; 400 on bad body; 200 + persisted on valid body; rate-limit tier applied.
pnpm test -- --selectProjects aiand--selectProjects web-libgreen.
- Service test: a recorded event persists;
- Estimate: 1 day
- Files:
Phase 3 — Application: wire overrides into evaluation (#3) + generation defaults
-
T11 — Apply
weightOverridesinside the #3 evaluator weight-merge- Files:
packages/ai/src/tools/evaluate-job.ts(the #3 evaluator); reusepackages/ai/src/learning/reconcile.ts+feedback-service.getActiveOverrides - Acceptance:
- The evaluator fetches
getActiveOverrides(userId)and feedsweightOverridesinto its existing merge order viareconcileOverrides(user wins over system-pack defaults). - When no override exists, the computed score is byte-for-byte identical to pre-epic behaviour.
- #3's recommendation bands are unchanged.
- The evaluator fetches
- Estimate: 1 day
- Files:
-
T12 — Apply phrasing prefs / always-never rules in generation tools
- Files:
packages/ai/src/tools/generate-cover-letter.ts;packages/ai/src/tools/resume-builder.ts;packages/ai/src/tools/interview-prep.ts - Acceptance:
- Each tool reads
getActiveOverrides(userId)and appliesphrasingPrefs/alwaysRules/neverRulesto the generation instruction viareconcile.ts. - #6 grounding/no-invent helpers run AFTER override application; overrides restyle only and cannot introduce ungrounded facts.
- No-override path produces the current output.
- Each tool reads
- Estimate: 1 day
- Files:
-
T13 — Integration test: override measurably changes next evaluation + generation
- Files:
packages/ai/src/learning/apply-overrides.test.ts - Acceptance:
- For a fixed (user, job), a stored
weightOverrideproduces a different (deterministic) score than the default; reverting the override restores the default score. - A stored phrasing pref demonstrably changes the generated draft's instruction/style; a grounded facts-only assertion confirms no new invented facts (Article 7).
pnpm test -- --selectProjects aigreen.
- For a fixed (user, job), a stored
- Estimate: 1 day
- Files:
Phase 4 — UI affordances + canvas sync + E2E
-
T14 — Feedback controls component (thumb / dispute / keep-my-edit)
- Files:
apps/web/components/canvas/feedback-controls.tsx(template:apps/web/components/canvas/salary-insights-card.tsx) - Acceptance:
- Renders thumb-up / thumb-down + "dispute this score" + "keep my edit as my style", built with
@ever-hust/ui/button,@ever-hust/ui/badge,@ever-hust/ui/dialog,cn()from@ever-hust/ui/lib/utils. - On action, POSTs to
/api/feedback; shows a confirm/undo state; never auto-applies (the user must confirm to activate a proposed override). - Embeddable into the evaluation breakdown card and generated-artifact cards under
apps/web/components/canvas/.
- Renders thumb-up / thumb-down + "dispute this score" + "keep my edit as my style", built with
- Estimate: 1 day
- Files:
-
T15 — Canvas sync: handle
recordFeedback- Files:
apps/web/hooks/use-canvas-sync.ts - Acceptance:
- New
case "recordFeedback"inhandleToolResultsurfaces the structured result (e.g. confirm toast / proposed-override prompt) and updates canvas state without overwriting jobs. - Unknown-tool default branch remains intact.
- New
- Estimate: 0.5 day
- Files:
-
T16 — Surface the funnel-proposed override accept affordance (#8 seam)
- Files:
apps/web/components/canvas/feedback-controls.tsx(accept variant); wire where #8 renders its insight underapps/web/components/canvas/ - Acceptance:
- When #8 supplies a score-floor proposal, an "accept this floor" control persists it as a
user_overridesrule via/api/feedback. - If #8 is not yet merged, the affordance is absent / no-ops cleanly (no runtime error).
- When #8 supplies a score-floor proposal, an "accept this floor" control persists it as a
- Estimate: 0.5 day
- Files:
-
T17 — Component test for feedback controls
- Files:
apps/web/components/canvas/feedback-controls.test.tsx - Acceptance:
- Renders all affordances; clicking dispute opens the confirm flow; confirm fires a POST to
/api/feedback; no network call fires before explicit confirm (human-in-the-loop). pnpm test -- --selectProjects web-libgreen.
- Renders all affordances; clicking dispute opens the confirm flow; confirm fires a POST to
- Estimate: 0.5 day
- Files:
-
T18 — Playwright E2E: full dispute → re-evaluate → reflected loop
- Files:
tests/e2e/learning-loop.spec.ts - Acceptance:
- Authenticated flow against
http://localhost:8443: dispute a score, confirm; re-evaluate the same job and assert the override is reflected. - Edit a generated artifact, choose "keep my style", regenerate and assert the style pref is reflected.
pnpm test:e2egreen for the new spec.
- Authenticated flow against
- Estimate: 1 day
- Files:
-
T19 — Rollback flag + docs + competitor-clean self-check
- Files:
packages/ai/src/agents/orchestrator.ts(guardrecordFeedback+ override reads behindLEARNING_LOOP_ENABLED);apps/web/.env.example(document the flag); updatedocs/specs/ROADMAP.mdprogress - Acceptance:
- With
LEARNING_LOOP_ENABLEDoff, overrides are not read and behaviour matches pre-epic (proven by a toggled test). docs/specs/ROADMAP.mdepic-13 progress updated.- Grep of all changed files returns zero competitor references (Article 11) before commit.
- With
- Estimate: 0.5 day
- Files:
Notes
- Write tests alongside each implementation task; do not batch testing into a final task.
- Verify zero competitor references before every commit (see constitution Article 11).
- Human-in-the-loop (Article 4): the loop proposes overrides; activation requires an explicit user accept. Never auto-apply, auto-submit, or auto-send.
- Standalone-first (Article 2): no Gauzy coupling and no Ever Jobs API calls are introduced by this epic.
- Update
docs/specs/ROADMAP.mdprogress when an epic's tasks complete.