- Added REPO_INVENTORY.md with all repos, branches, remotes, and staging info - Added .gitignore - Synced all existing docs from local workspace - Centralized documentation hub for GrowQR team
849 lines
16 KiB
Markdown
849 lines
16 KiB
Markdown
> **Missions are not chat flows. Missions are event-driven product journeys.**
|
||
> Chat/Grow Agent is only one optional surface that can read mission state and help the user decide what to do next.
|
||
|
||
The mission dashboard should work even if the user never opens chat.
|
||
|
||
I read the Rivet docs you linked, the current backend mission/actor code, the old `growqr-app/orchestrator`, and the service event patterns. The correct production design is a **mission event system + dashboard projection**, not a chat-first workflow.
|
||
|
||
---
|
||
|
||
# Revised product model
|
||
|
||
## User buys a mission
|
||
|
||
Example:
|
||
|
||
> **Interview-to-Offer Accelerator — $10/month**
|
||
|
||
Includes limits like:
|
||
|
||
- 10 mock interviews/month
|
||
- X resume analyses/optimizations
|
||
- maybe 1 final readiness report
|
||
- optional add-ons later: roleplay, extra interviews, video analysis, etc.
|
||
|
||
After purchase, the user gets an **active mission dashboard**.
|
||
|
||
The dashboard shows:
|
||
|
||
- current mission progress
|
||
- next required action
|
||
- completed service actions
|
||
- generated artifacts
|
||
- interview/resume results
|
||
- Q Score/readiness trend
|
||
- quota usage
|
||
- recommendations
|
||
|
||
Chat can help, but the dashboard is the primary surface.
|
||
|
||
---
|
||
|
||
# Important correction for Interview-to-Offer
|
||
|
||
If the team says this mission currently uses only:
|
||
|
||
- Resume
|
||
- Interview
|
||
|
||
Then don’t force Roleplay into the core package.
|
||
|
||
Roleplay can become:
|
||
|
||
1. an upsell,
|
||
2. a recommended recovery action,
|
||
3. a premium variant,
|
||
4. or a later stage only if interview results show communication weakness.
|
||
|
||
So the core mission becomes:
|
||
|
||
## Interview-to-Offer Accelerator v1
|
||
|
||
### Included services
|
||
|
||
- Resume Building
|
||
- Mock Interview
|
||
- Q Score/readiness analytics
|
||
- Grow Agent/planning only as backend intelligence, not necessarily chat
|
||
|
||
### Core stages
|
||
|
||
1. Purchase / mission activated
|
||
2. Resume uploaded or selected
|
||
3. Resume analyzed/improved for target interview
|
||
4. Interview prep plan generated
|
||
5. Mock interview configured
|
||
6. Mock interview completed
|
||
7. Review received
|
||
8. Final readiness score/report generated
|
||
|
||
That is much cleaner.
|
||
|
||
---
|
||
|
||
# What exists today
|
||
|
||
From the code:
|
||
|
||
## Backend already has
|
||
|
||
- `grow_active_missions`
|
||
- mission actors like `interviewToOfferMissionActor`
|
||
- mission snapshots
|
||
- explicit APIs to update stages and artifacts
|
||
- PG-first persistence for active missions
|
||
- Q Score snapshots/workflow event tables from older workflow infra
|
||
|
||
But right now, mission progress updates mostly happen through explicit backend calls like:
|
||
|
||
```txt
|
||
POST /missions/active/:instanceId/stages/:stageId
|
||
POST /missions/active/:instanceId/artifacts
|
||
```
|
||
|
||
That means the dashboard does not automatically know when a microservice finishes something unless we explicitly update the mission.
|
||
|
||
## Old orchestrator already had a useful pattern
|
||
|
||
The old `growqr-app/orchestrator`:
|
||
|
||
- routes actions to services
|
||
- receives `agent_data` messages
|
||
- extracts Q Score signals from service results
|
||
- publishes signals to `qscore.signal_events`
|
||
- handles real-time score updates
|
||
|
||
But it has too much routing registry logic:
|
||
|
||
```py
|
||
ACTION_TO_AGENT
|
||
PAGE_TO_AGENT
|
||
COMPOSITE_WORKFLOWS
|
||
```
|
||
|
||
We should not rebuild that as another large registry.
|
||
|
||
## Services emit useful events/messages already
|
||
|
||
Examples:
|
||
|
||
Resume emits A2A-style messages:
|
||
|
||
- `resume_created`
|
||
- `resume_loaded`
|
||
- `version_saved`
|
||
- `ai_analysis_complete`
|
||
- `ai_summary_optimized`
|
||
- `ai_skills_suggested`
|
||
- `export_ready`
|
||
|
||
Interview emits:
|
||
|
||
- `interview_configured`
|
||
- `session.completed` over live WS
|
||
- review endpoint returns `completed`, `processing`, `failed`, `not_eligible`
|
||
- `review_loaded` through agentic path
|
||
|
||
Roleplay emits similar patterns.
|
||
|
||
Q Score already has a strong event ingestion path:
|
||
|
||
```txt
|
||
qscore.signal_events Redis stream
|
||
```
|
||
|
||
So the missing layer is not “service capability.” It is a **GrowQR mission event ingestion/projection layer**.
|
||
|
||
---
|
||
|
||
# Correct architecture
|
||
|
||
## Principle
|
||
|
||
> Postgres is the source of truth. Actors are live reducers/projectors. Chat is just one consumer.
|
||
|
||
## Event flow
|
||
|
||
```txt
|
||
Service action happens
|
||
↓
|
||
GrowQR backend receives/creates event
|
||
↓
|
||
Raw event is stored in Postgres event inbox
|
||
↓
|
||
Event is forwarded to the relevant mission actor
|
||
↓
|
||
Mission actor updates mission snapshot/progress/artifacts
|
||
↓
|
||
Dashboard reads updated mission snapshot
|
||
↓
|
||
Q Score signals are published/updated
|
||
↓
|
||
Grow/chat may optionally read the same state
|
||
```
|
||
|
||
---
|
||
|
||
# New concept: dynamic mission events
|
||
|
||
We do **not** need a rigid event registry.
|
||
|
||
We need one flexible event envelope.
|
||
|
||
## Generic event envelope
|
||
|
||
```ts
|
||
type GrowEvent = {
|
||
id: string;
|
||
userId: string;
|
||
orgId?: string;
|
||
|
||
source: string;
|
||
// resume-service, interview-service, roleplay-service, qscore-service, grow-backend
|
||
|
||
type: string;
|
||
// resume.ai_analysis_complete
|
||
// interview.session_completed
|
||
// interview.review_completed
|
||
// mission.purchased
|
||
// qscore.updated
|
||
|
||
category:
|
||
| "mission"
|
||
| "service"
|
||
| "artifact"
|
||
| "usage"
|
||
| "qscore"
|
||
| "entitlement"
|
||
| "system";
|
||
|
||
occurredAt: string;
|
||
|
||
mission?: {
|
||
instanceId?: string;
|
||
missionId?: string;
|
||
stageId?: string;
|
||
};
|
||
|
||
subject?: {
|
||
kind: string;
|
||
id: string;
|
||
};
|
||
// example: { kind: "interview_session", id: sessionId }
|
||
|
||
correlation?: {
|
||
requestId?: string;
|
||
sessionId?: string;
|
||
resumeId?: string;
|
||
externalId?: string;
|
||
};
|
||
|
||
payload: Record<string, unknown>;
|
||
};
|
||
```
|
||
|
||
The important thing: **we store unknown events too.**
|
||
|
||
If an event type is new, we don’t reject it. We save it, attach it to the user/mission if possible, and show it in debugging/timeline if useful.
|
||
|
||
---
|
||
|
||
# Postgres tables needed
|
||
|
||
Do not rely on actor state for durable history.
|
||
|
||
Add something like:
|
||
|
||
## `grow_events`
|
||
|
||
Append-only event inbox.
|
||
|
||
Fields:
|
||
|
||
- `id`
|
||
- `user_id`
|
||
- `mission_instance_id`
|
||
- `source`
|
||
- `type`
|
||
- `category`
|
||
- `subject_kind`
|
||
- `subject_id`
|
||
- `correlation_id`
|
||
- `payload`
|
||
- `occurred_at`
|
||
- `received_at`
|
||
- `processed_at`
|
||
- `processing_status`
|
||
- `error`
|
||
|
||
## `mission_service_sessions`
|
||
|
||
Tracks external service objects created for a mission.
|
||
|
||
Examples:
|
||
|
||
- interview session ID
|
||
- resume ID
|
||
- roleplay session ID
|
||
- qscore run ID
|
||
|
||
Fields:
|
||
|
||
- `id`
|
||
- `user_id`
|
||
- `mission_instance_id`
|
||
- `stage_id`
|
||
- `service_id`
|
||
- `external_type`
|
||
- `external_id`
|
||
- `status`
|
||
- `last_event_id`
|
||
- `last_checked_at`
|
||
- `created_at`
|
||
- `updated_at`
|
||
|
||
This table is crucial because the dashboard needs to know:
|
||
|
||
> “This mission’s interview stage corresponds to interview session `abc123`.”
|
||
|
||
## `mission_artifacts`
|
||
|
||
Could either reuse current snapshot artifacts or make durable table.
|
||
|
||
Examples:
|
||
|
||
- resume fit scan
|
||
- interview prep plan
|
||
- interview review
|
||
- readiness report
|
||
|
||
---
|
||
|
||
# Actor design
|
||
|
||
Do **not** put this all in `growActor`.
|
||
|
||
`growActor` is becoming chat/conversation/user home state. Missions should not depend on it.
|
||
|
||
## Best actor model
|
||
|
||
### 1. Mission actor per active mission
|
||
|
||
Already exists:
|
||
|
||
```txt
|
||
interviewToOfferMissionActor[userId, instanceId]
|
||
```
|
||
|
||
Extend it conceptually to support:
|
||
|
||
```ts
|
||
ingestEvent(event)
|
||
recomputeProjection()
|
||
scheduleExternalCheck()
|
||
getDashboardSnapshot()
|
||
```
|
||
|
||
The actor stores only bounded state:
|
||
|
||
- current stage
|
||
- progress
|
||
- latest known service session IDs
|
||
- latest scores
|
||
- artifact summaries
|
||
- next action
|
||
- last few events maybe
|
||
|
||
Raw events stay in Postgres.
|
||
|
||
### 2. Optional user analytics actor
|
||
|
||
A per-user analytics actor can exist later:
|
||
|
||
```txt
|
||
userAnalyticsActor[userId]
|
||
```
|
||
|
||
Its job:
|
||
|
||
- update user-level Q Score trend
|
||
- aggregate events across missions
|
||
- maintain dashboard analytics
|
||
- detect recommendations
|
||
|
||
But don’t make it mandatory for v1. The mission actor + PG event store is enough.
|
||
|
||
### 3. Do not create one god event actor
|
||
|
||
Avoid:
|
||
|
||
```txt
|
||
globalEventProcessorActor
|
||
```
|
||
|
||
That becomes a bottleneck.
|
||
|
||
Better:
|
||
|
||
```txt
|
||
event ingest route → Postgres → mission actor for that mission/user
|
||
```
|
||
|
||
Rivet docs support actor queues and schedules, which are good primitives here. Use queues for ordered event processing and schedules for later status checks. The docs also note actor `noSleep` is deprecated in current docs, so we should avoid relying on permanent awake actors long-term, even though the project is pinned to `rivetkit ^2.2.1`.
|
||
|
||
---
|
||
|
||
# Event ingestion paths
|
||
|
||
We need multiple ingestion paths because not every service currently emits backend webhooks.
|
||
|
||
## 1. Backend gateway-generated events
|
||
|
||
Whenever the dashboard calls GrowQR backend gateway:
|
||
|
||
```txt
|
||
POST /services/interview/configure
|
||
POST /services/resume/resumes/:id/analyze
|
||
POST /services/interview/review/:sessionId
|
||
```
|
||
|
||
The backend should emit internal events like:
|
||
|
||
```txt
|
||
interview.configured
|
||
resume.analysis_requested
|
||
resume.analysis_completed
|
||
interview.review_completed
|
||
```
|
||
|
||
This is the easiest first layer.
|
||
|
||
## 2. Service webhook/event endpoint
|
||
|
||
Add a backend route:
|
||
|
||
```txt
|
||
POST /events/ingest
|
||
```
|
||
|
||
Services can call this when they complete things:
|
||
|
||
```txt
|
||
resume.ai_analysis_complete
|
||
interview.session_completed
|
||
interview.review_completed
|
||
roleplay.review_completed
|
||
```
|
||
|
||
This is ideal long term.
|
||
|
||
## 3. Redis stream bridge
|
||
|
||
Old services already use Redis streams for task responses.
|
||
|
||
We can add a backend consumer that listens to service response messages and converts them into Grow events.
|
||
|
||
But I would not make Redis the only source of truth. It should feed Postgres.
|
||
|
||
## 4. Polling/checker fallback
|
||
|
||
This is mandatory.
|
||
|
||
Some events will be missed. Some services currently only expose state through REST. Some live WebSocket events go straight to the browser.
|
||
|
||
So when a mission creates an interview session, store:
|
||
|
||
```txt
|
||
mission instance → interview session id
|
||
```
|
||
|
||
Then schedule checks:
|
||
|
||
```txt
|
||
check interview session/review in 30s
|
||
check again in 2m
|
||
check again in 5m
|
||
stop when completed/failed/expired
|
||
```
|
||
|
||
Actors are good for this because Rivet supports scheduled actions.
|
||
|
||
---
|
||
|
||
# How the mission updates automatically
|
||
|
||
Example: Interview-to-Offer.
|
||
|
||
## User buys mission
|
||
|
||
Backend creates event:
|
||
|
||
```txt
|
||
mission.purchased
|
||
```
|
||
|
||
Mission actor receives it:
|
||
|
||
- status: active
|
||
- stage: resume
|
||
- progress: 0–10%
|
||
- next action: upload/select resume
|
||
|
||
Dashboard shows:
|
||
|
||
> Next: Update your resume for this interview.
|
||
|
||
---
|
||
|
||
## User analyzes resume
|
||
|
||
Dashboard calls Resume service through backend.
|
||
|
||
Backend stores:
|
||
|
||
```txt
|
||
resume.analysis_requested
|
||
```
|
||
|
||
When result returns or poller finds result:
|
||
|
||
```txt
|
||
resume.ai_analysis_complete
|
||
```
|
||
|
||
Mission actor applies dynamic reducer:
|
||
|
||
- source = resume-service
|
||
- event contains resume analysis
|
||
- mission stage = resume
|
||
- mark resume stage done
|
||
- create artifact: Resume Fit Scan
|
||
- update progress
|
||
- next action: Start mock interview
|
||
|
||
No chat required.
|
||
|
||
---
|
||
|
||
## User starts interview
|
||
|
||
Backend gateway calls Interview service configure.
|
||
|
||
Backend stores:
|
||
|
||
```txt
|
||
interview.configured
|
||
```
|
||
|
||
Mission actor:
|
||
|
||
- links session ID to mission
|
||
- marks interview stage ready/in_progress
|
||
- next action: Start interview session
|
||
|
||
---
|
||
|
||
## User completes interview
|
||
|
||
Live WS sends `session.completed` to browser today.
|
||
|
||
But backend should know through one of:
|
||
|
||
1. service webhook,
|
||
2. backend polling review endpoint,
|
||
3. frontend callback to backend,
|
||
4. Redis bridge if session completion is emitted there.
|
||
|
||
Backend stores:
|
||
|
||
```txt
|
||
interview.session_completed
|
||
```
|
||
|
||
Mission actor:
|
||
|
||
- stage status: review pending
|
||
- next action: wait for review / view processing
|
||
|
||
---
|
||
|
||
## Review becomes available
|
||
|
||
Backend fetches:
|
||
|
||
```txt
|
||
GET /services/interview/review/:sessionId
|
||
```
|
||
|
||
If completed, store:
|
||
|
||
```txt
|
||
interview.review_completed
|
||
```
|
||
|
||
Mission actor:
|
||
|
||
- creates artifact: Mock Interview Review
|
||
- marks interview stage done
|
||
- extracts score/weaknesses
|
||
- publishes Q Score signals
|
||
- computes/requests final readiness
|
||
- updates dashboard
|
||
|
||
---
|
||
|
||
# Q Score integration
|
||
|
||
Q Score should be event-fed, not chat-fed.
|
||
|
||
Current old orchestrator extracts signals from service messages. We can reuse that idea but move it into backend event processing.
|
||
|
||
## Signal mapper should be dynamic-ish
|
||
|
||
Instead of a giant event registry, use a few generic extractors by category/source:
|
||
|
||
```txt
|
||
resume event → resume signal extractor
|
||
interview event → interview signal extractor
|
||
roleplay event → roleplay signal extractor
|
||
engagement/mission event → workflow signal extractor
|
||
```
|
||
|
||
This is not a mission registry. It is a small set of signal extractors.
|
||
|
||
Example interview review signals:
|
||
|
||
```txt
|
||
interview.completed
|
||
interview.overall_score
|
||
interview.language_score
|
||
interview.voice_score
|
||
interview.body_language_score
|
||
interview.filler_words
|
||
interview.technical_quality
|
||
```
|
||
|
||
Resume signals:
|
||
|
||
```txt
|
||
resume.uploaded
|
||
resume.ats_compatibility
|
||
resume.keyword_relevance
|
||
resume.grammar_clarity
|
||
resume.impact_statements
|
||
```
|
||
|
||
Mission signals:
|
||
|
||
```txt
|
||
mission.interview_to_offer.started
|
||
mission.interview_to_offer.resume_completed
|
||
mission.interview_to_offer.interview_completed
|
||
mission.interview_to_offer.completed
|
||
```
|
||
|
||
These are then sent to Q Score.
|
||
|
||
---
|
||
|
||
# Dashboard behavior
|
||
|
||
The mission dashboard should be driven by mission state, not chat messages.
|
||
|
||
## Active mission page
|
||
|
||
For Interview-to-Offer:
|
||
|
||
```txt
|
||
Interview-to-Offer Accelerator
|
||
|
||
Progress: 55%
|
||
Plan: $10/month
|
||
Usage:
|
||
- Interviews: 2 / 10
|
||
- Resume analyses: 1 / 3
|
||
|
||
Current step:
|
||
Mock Interview
|
||
|
||
Next action:
|
||
Start your 15-minute behavioral mock interview.
|
||
|
||
Completed:
|
||
✓ Mission purchased
|
||
✓ Resume selected
|
||
✓ Resume analyzed
|
||
✓ Resume fit scan generated
|
||
|
||
Artifacts:
|
||
- Resume Fit Scan
|
||
- Interview Prep Plan
|
||
- Mock Interview Review, pending
|
||
|
||
Readiness:
|
||
Baseline Q Score: 64
|
||
Current Q Score: 71
|
||
Delta: +7
|
||
```
|
||
|
||
## Stage cards
|
||
|
||
Each card has its own CTA:
|
||
|
||
Resume stage:
|
||
|
||
- Upload resume
|
||
- Select resume
|
||
- Analyze resume
|
||
- View fit scan
|
||
|
||
Interview stage:
|
||
|
||
- Configure mock interview
|
||
- Start interview
|
||
- View review
|
||
|
||
Final stage:
|
||
|
||
- Generate readiness report
|
||
- View final Q Score
|
||
|
||
No chat required.
|
||
|
||
---
|
||
|
||
# Chat/Grow Agent’s new role
|
||
|
||
Chat becomes a reader/writer of the same mission system.
|
||
|
||
It can:
|
||
|
||
- explain current mission status
|
||
- recommend next action
|
||
- call mission/service tools
|
||
- summarize artifacts
|
||
- answer “what should I do next?”
|
||
|
||
But if the user never opens chat, the mission still progresses.
|
||
|
||
So the Grow Agent should not own the mission. It should consume the mission projection.
|
||
|
||
---
|
||
|
||
# Subscription/usage tracking
|
||
|
||
For paid missions, usage should also be event-based.
|
||
|
||
Events:
|
||
|
||
```txt
|
||
entitlement.granted
|
||
usage.interview_session_created
|
||
usage.interview_session_completed
|
||
usage.resume_analysis_requested
|
||
usage.resume_analysis_completed
|
||
usage.limit_reached
|
||
subscription.renewed
|
||
subscription.cancelled
|
||
```
|
||
|
||
The gateway should enforce limits before creating expensive service sessions.
|
||
|
||
Example:
|
||
|
||
```txt
|
||
User clicks Start Interview
|
||
↓
|
||
Backend checks entitlement
|
||
↓
|
||
If allowed, create interview session
|
||
↓
|
||
Insert usage event
|
||
↓
|
||
Increment monthly usage
|
||
```
|
||
|
||
Do not rely on frontend hiding buttons.
|
||
|
||
---
|
||
|
||
# What I would build first
|
||
|
||
## Phase 1 — Mission event backbone
|
||
|
||
No new product logic yet. Just make events durable.
|
||
|
||
- `grow_events`
|
||
- `mission_service_sessions`
|
||
- `/events/ingest`
|
||
- backend helper: `recordGrowEvent()`
|
||
- event forwarding to mission actor
|
||
- mission actor `ingestEvent()`
|
||
|
||
## Phase 2 — Backend gateway emits mission events
|
||
|
||
Whenever dashboard uses service routes, backend records events.
|
||
|
||
Examples:
|
||
|
||
- interview configured
|
||
- interview review fetched
|
||
- resume analyzed
|
||
- resume version saved
|
||
|
||
## Phase 3 — Mission actor becomes projection reducer
|
||
|
||
Add generic dynamic handling:
|
||
|
||
- store unknown events
|
||
- update timeline
|
||
- if event matches current stage service, update stage
|
||
- attach artifacts if payload has review/analysis/export
|
||
- update next action
|
||
|
||
## Phase 4 — Polling fallback
|
||
|
||
For linked external sessions:
|
||
|
||
- poll interview review until completed
|
||
- poll roleplay review until completed
|
||
- poll resume analysis state if needed
|
||
|
||
## Phase 5 — Q Score bridge
|
||
|
||
Convert service events into Q Score signals and publish them.
|
||
|
||
## Phase 6 — Dashboard reads projection
|
||
|
||
Dashboard should not guess. It reads:
|
||
|
||
```txt
|
||
GET /missions/active/:instanceId
|
||
```
|
||
|
||
and renders:
|
||
|
||
- stages
|
||
- events
|
||
- artifacts
|
||
- next action
|
||
- usage
|
||
- score trend
|
||
|
||
---
|
||
|
||
# Key recommendation
|
||
|
||
Do not create another large service/action registry.
|
||
|
||
Use this instead:
|
||
|
||
1. **Feature registry** remains the product source of truth.
|
||
2. **Mission definitions** define stages and included services.
|
||
3. **Events are dynamic** and stored raw.
|
||
4. **Small generic reducers** interpret categories/sources.
|
||
5. **Unknown events are accepted, stored, and visible.**
|
||
6. **Actors project events into live mission state.**
|
||
7. **Postgres remains authoritative.**
|
||
|
||
That gives you the cohesive, event-driven mission system you’re describing without recreating the old orchestrator registry. |