feat: initial docs repo with project inventory and all documentation

- 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
This commit is contained in:
-Puter
2026-06-22 15:04:27 +05:30
commit e6685203fe
71 changed files with 35044 additions and 0 deletions
+818
View File
@@ -0,0 +1,818 @@
# GrowQR Q-Score Microservice Implementation Plan
Status: Draft for team confirmation in review meeting
Owner: Rahul (Engineering)
Prepared on: 2026-02-11
Related docs: `growqr/Q-Score_Service.md`
---
## 1) Executive Summary
We are building a Python FastAPI + Postgres microservice that is the core Q-Score engine.
MVP behavior:
- Accept processed/scored signals (per-rule/per-input) from other services via a pub/sub stream.
- Compute Q-Score from those stored signals using the internal formula (CI/SV, pillar weights, pillar CI, contributions).
- Return and store a full explainable breakdown (rule-level + pillar-level + CI/SV math).
- Expose a small surface area to read signals and read the latest computed Q-Score.
- Publish live Q-Score updates so the orchestrator agent receives changes instantly.
Additional requirements (post-meeting):
- Multi-tenant from day 1: every signal and score is keyed by `org_id`.
- Formula can vary by profession/profile type (e.g., student vs professor) within an org.
- Team members must be able to update formulas via API (in addition to JSON-on-disk).
Explicitly out of scope for this delivery:
- Tier assignment (removed for now).
- Any upstream processing or enrichment (LinkedIn scraping, resume parsing/ATS, education validation, etc.).
- Complex multi-broker support (we start with one broker; can swap later).
Target: delivery ASAP; pub/sub may extend delivery by 1-2 days beyond Saturday if needed.
---
## 2) Confirmed Product/Engineering Decisions
These are locked based on latest discussion:
1. This service is the Q-Score calculation engine only.
2. All rule processing happens outside this service. This service only accepts processed/scored signals.
3. GET/read endpoint must return full scoring transparency:
- each rule score
- each pillar score and contribution
- CI/SV values and final math
4. Engagement signals exist but are not required for initial (day-1) score computations.
5. Auth is deferred; keep middleware hook to add later without endpoint logic rewrite.
6. Pillar 11 (Recognition & Rewards) uses 3 onboarding achievements; points mapping is pending confirmation.
7. Goals signals can be ingested and stored now; they do not impact score in current MVP.
8. Pillar CI formula clarification:
```
Pillar_CI = weighted average of input CIs contributing to that pillar
weight_i = proportion of pillar data points from input_i
```
Where "data points" for v1 = number of rules from that input used in that pillar.
9. For Saturday, keep orchestration simple:
- no separate job queue system (e.g., RQ)
- use a Redis Stream consumer worker + a small scheduler/micro-batcher for near-real-time updates
10. Signal storage must be versioned per user:
- we keep an append-only ledger of every ingested signal update (`signal_ledger`)
- we keep a per-user latest materialized view/table for fast compute (`user_signal_latest`)
- we store every computed Q-Score run (`qscore_runs`) to track how outputs evolve over time
11. Provide a validation endpoint for producers (fire-and-forget): upstream services can call a check endpoint with `user_id`, `signal_id`, and `score` to validate that the signal is acceptable for the current formula registry.
12. Pub/sub is required:
- orchestrator agent publishes a live stream of signals for each user
- Q-Score engine consumes signals and publishes a live stream of Q-Score updates
- agent subscribes to Q-Score updates (bi-directional communication via streams)
13. Freshness target: score update latency should be < 10 seconds.
- intermediate updates are acceptable
- compute/publish is paced (micro-batched) to handle continuous signal dumps
---
## 3) Scope
### MVP Scope (this sprint)
- Signal ingest (stream-based; plus HTTP fallback endpoint)
- Compute Q-Score endpoint (writes a qscore run)
- Read endpoints for signals and latest Q-Score run
- Scoring engine (signals -> rules -> inputs -> pillars -> final Q)
- Postgres persistence (signals + qscore history)
- Pub/sub broker (Redis Streams) + consumer/producer worker
- Docker Compose local stack (API + Postgres + Redis + worker)
### Future Scope (not blocking MVP)
- Full auth middleware implementation
- Additional signals / pillars / formula versions (Excel-driven expansion)
- Batch recompute tuning (dynamic micro-batching, backpressure, autoscaling)
- Broker swap (Kafka/NATS/etc.) behind a small adapter layer if needed
---
## 4) System Architecture
## Components
1. **FastAPI API service**
- request validation
- orchestration
- scoring invocation
- DB writes/reads
2. **Scoring engine module (pure Python)**
- deterministic aggregation and formula application
- CI/SV calculations
- pillar aggregation and contributions
- final score output
3. **Postgres**
- stored signals (latest)
- optional signal event log (append-only)
- qscore history
4. **Redis (Streams)**
- input signal stream (agent -> qscore)
- output score update stream (qscore -> agent)
5. **Worker process**
- consumes input signal stream
- writes ledger + latest state
- recomputes Q-Score (micro-batch)
- publishes score update events
Notes:
- This service does not fetch/enrich any upstream data. Upstream services are responsible for producing scored signals.
---
## 5) High-Level Flows
### Flow A: Live Signal Publish (agent -> qscore)
1. Orchestrator agent publishes signal events to the input stream.
2. Worker consumes signal events.
3. Worker appends each signal update into `signal_ledger` (append-only history).
4. Worker upserts latest signal values into `user_signal_latest`.
5. Worker marks the user as dirty for compute (e.g., updates `user_dirty.last_signal_at`).
Pacing behavior (continuous stream):
- Worker schedules computes per user using a pacer (debounce + max staleness).
- Recency-first priority for user updates; periodic sweep ensures no long-running backlog.
### Flow B: Q-Score Compute
Two triggers are supported:
1) Manual compute-now:
1. Client/producer calls `POST /v1/qscore/compute` with `user_id`.
2. API reads latest signals for user from `user_signal_latest`.
3. Engine computes input CI/SV, pillar scores, pillar CI, contributions, and final Q.
4. API writes a new row in `qscore_runs` with full breakdown.
5. API returns score + breakdown.
2) Scheduled batch compute:
1. Scheduler runs every `BATCH_INTERVAL_SECONDS` (default: 300) and checks if there are dirty users.
2. For each dirty user, compute Q-Score from `user_signal_latest`.
3. Write `qscore_runs` and mark user clean.
Additionally (live updates):
- Worker performs micro-batched computes when new signal events arrive (debounced per user).
- When a user's Q-Score changes, worker publishes an update event to the output stream.
Freshness guarantee (configurable):
- even under continuous events, each dirty user should get recomputed at least once per `MAX_STALENESS_MS` (default: 10000)
### Flow C: Read
1. Client calls `GET /v1/signals/{user_id}` to read latest signals.
2. Client calls `GET /v1/qscore/{user_id}` to read latest Q-Score + breakdown.
---
## 6) API Endpoints
## 6.1 Signal Ingest
`POST /v1/signals/ingest`
Purpose:
- HTTP fallback for producers that cannot publish directly to the broker.
- Server validates and then publishes into the input stream.
Profession handling:
- If `profession` is provided, the service upserts it into `user_profile`.
- If `profession` is omitted, the service uses the existing `user_profile.profession`.
- If neither exists, the service uses a default profession (configurable) and records it.
Request (example):
```json
{
"org_id": "growqr",
"user_id": "b5f9e91f-6a95-4f2f-9f11-4f8f7f5f4c77",
"profession": "student",
"source": "social_branding_service",
"signals": [
{
"signal_id": "linkedin.account_connected",
"present": true,
"score": 100,
"raw": "CONNECTED"
},
{
"signal_id": "resume.ats_compatibility",
"present": true,
"score": 72,
"raw": {"ats_score": 72}
}
],
"occurred_at": "2026-02-11T12:00:00Z"
}
```
Response (example):
```json
{
"org_id": "growqr",
"user_id": "b5f9e91f-6a95-4f2f-9f11-4f8f7f5f4c77",
"formula_version": "v1",
"upserted": 2
}
```
## 6.2 Compute Q-Score
`POST /v1/qscore/compute`
Request (example):
```json
{
"org_id": "growqr",
"user_id": "b5f9e91f-6a95-4f2f-9f11-4f8f7f5f4c77"
}
```
Response (example):
```json
{
"org_id": "growqr",
"user_id": "b5f9e91f-6a95-4f2f-9f11-4f8f7f5f4c77",
"q_score": 67.42,
"profession": "student",
"calculated_at": "2026-02-11T12:01:10Z",
"breakdown": {
"inputs": {},
"rules": {},
"pillars": {},
"math": {}
}
}
```
## 6.3 Read Latest Score (full explainability)
`GET /v1/qscore/{user_id}`
Query params:
- `org_id`
Returns latest `qscore_runs.breakdown` in full detail.
## 6.4 Read Latest Signals
`GET /v1/signals/{user_id}`
Query params:
- `org_id`
Returns latest stored signals for the user (including `present`, `score`, and `raw`).
## 6.5 Signal Check (producer validation)
`POST /v1/signals/check`
Purpose:
- Producer-side validation so upstream can verify a signal is acceptable for the current registry before ingesting.
Request (example):
```json
{
"org_id": "growqr",
"user_id": "b5f9e91f-6a95-4f2f-9f11-4f8f7f5f4c77",
"profession": "student",
"signal_id": "resume.ats_compatibility",
"score": 72,
"raw": {"ats_score": 72}
}
```
Response (example):
```json
{
"valid": true,
"errors": [],
"warnings": []
}
```
Validation rules (v1):
- `signal_id` exists in current formula registry OR is in an allowed "unknown" namespace list (configurable)
- `score` is numeric and within `0..100`
- optional metadata size limits (to protect DB)
## 6.6 Registry Endpoints (sanitized)
`GET /v1/registry/signals`
Returns the list of accepted `signal_id`s and minimal metadata needed by producers (no secret sauce formula details).
Query params (recommended):
- `org_id`
- `profession`
`POST /v1/formula/reload`
Reload formula JSON from disk and swap in-memory formula if validation passes (auth to be added later).
## 6.7 Formula Management (team updates)
We support both:
- JSON-on-disk for version control
- DB-backed formula definitions for API updates
Formula resolution (which formula is used for a user):
1) Read `user_profile.profession` (set via ingest).
2) Resolve active formula version for `(org_id, profession)`.
3) Allow per-user override via `user_profile.formula_version_override` (future).
4) Effective formula key: `{org_id}:{profession}:{version}`.
Filesystem layout (when `FORMULA_STORE=filesystem`):
- `FORMULA_DIR/{version}/{profession}/formula.json`
DB layout (when `FORMULA_STORE=db`):
- `formula_definitions` rows keyed by `(org_id, profession, version)`.
Endpoints:
`GET /v1/formulas`
- lists available formulas (org_id + profession + version + active)
`PUT /v1/formulas/{org_id}/{profession}/{version}`
- upserts a formula JSON blob into Postgres
`POST /v1/formulas/{org_id}/{profession}/activate`
- sets the active formula version for a profession within an org
`GET /v1/users/{org_id}/{user_id}/profile`
- returns the stored profession + active formula resolution (debugging)
## 6.8 Stream Contracts (pub/sub)
Input stream (signals):
- stream name (default): `qscore.signal_events`
- event shape matches `POST /v1/signals/ingest` payload
Output stream (qscore updates):
- stream name (default): `qscore.score_updates`
- event shape (example):
```json
{
"org_id": "growqr",
"user_id": "b5f9e91f-6a95-4f2f-9f11-4f8f7f5f4c77",
"profession": "student",
"q_score": 67.42,
"formula_version": "v1",
"formula_id": "growqr:student:v1",
"qscore_run_id": "...",
"events_considered": {
"ledger_seq_from": 1201,
"ledger_seq_to": 1215,
"count": 15,
"sample_ledger_seqs": [1201, 1202, 1203]
},
"changed": true,
"created_at": "2026-02-11T12:01:10Z",
"breakdown": {"inputs": {}, "rules": {}, "pillars": {}, "math": {}}
}
```
Note:
- agent subscribes to the output stream to receive score updates instantly.
- agent can also read the latest computed value from Redis key `qscore:latest:{org_id}:{user_id}` (updated on every publish).
- `ledger_seq_from` is typically `user_dirty.last_computed_seq + 1` and `ledger_seq_to` is `user_dirty.last_signal_seq` at compute time.
---
## 7) Scoring Model Design
## 7.1 Input CI
For each input `i`:
```
SV_i = present_rules_i / total_rules_i
CI_i = (0.7 * VS_i) + (0.3 * SV_i)
```
VS constants (current):
- Input 1 LinkedIn: 1.0
- Input 2 Resume: 0.9
- Input 3 Education: 0.9
- Input 4 Engagement: 0.7
- Input 5 Goals: 0.5 (stored only for now)
- Input 6 Platform (for pillar 11 placeholder): configurable (default 1.0)
## 7.2 Rule Scoring Contract
Every rule returns:
```json
{
"present": true,
"included_in_pillar_average": true,
"score": 75.0,
"raw": "source value used for this rule"
}
```
Missing data behavior (confirmed):
- If the input data for a rule is not available, return:
- `present=false`
- `score=0`
- `included_in_pillar_average=false`
- Missing rules do not penalize `pillar_score` directly, but they reduce `SV_i` and therefore `CI_i`.
## 7.3 Pillar Score
```
pillar_score = average(score of rules where included_in_pillar_average=true)
```
If no rules are included in the pillar average, the pillar is inactive unless configured as always-on placeholder.
## 7.4 Pillar CI (confirmed logic)
```
pillar_ci = sum(weight_i * CI_i)
weight_i = rules_from_input_i_in_pillar / total_rules_in_pillar
```
## 7.5 Pillar Contribution
```
contribution = pillar_score * pillar_weight * pillar_ci
```
## 7.6 Final Q-Score
```
Q = sum(contributions_of_active_pillars) / sum(active_pillar_weights)
```
Notes:
- no tier assignment in this release
- no percentile requirement in this release
---
## 8) Pillars and Rules Handling
Pillars locked for Saturday release (from `growqr/Q-Score_Service.md`):
- Pillar 01 Presence & Profile (20.0%)
- Pillar 02 Learning & Credentials (19.3%)
- Pillar 05 Engagement & Consistency (3.3%)
- Pillar 06 Communication Signals (4.0%)
- Pillar 11 Recognition & Rewards (0.9%)
Initial (day-1) score computations are expected to activate pillars 01, 02, 06, 11 if those signals are ingested.
Engagement pillar (05) is not required for day-1 and becomes active only when engagement signals are later ingested.
Important current behavior choices:
- Goals input is persisted but not used in score math right now.
- Engagement signal IDs are part of the formula registry, but are not expected to be present on day-1.
- Platform pillar (11) uses 3 onboarding achievements; scoring/points mapping is an open item to confirm.
Config examples:
- `P11_ACHIEVEMENT_COUNT=3`
- `P11_ACHIEVEMENT_SCORE_DEFAULT=100`
---
## 9) Persistence Design
Migration approach:
- Use Alembic migrations for all schema changes (job-service currently uses `metadata.create_all`, but Q-Score should be migration-driven).
## 9.1 `signal_ledger` (append-only, versioned history)
- `seq` bigserial pk
- `id` uuid unique
- `org_id` text not null
- `user_id` uuid not null
- `profession` text null
- `signal_id` text not null
- `source` text null
- `score` numeric(5,2) not null
- `raw` jsonb null
- `occurred_at` timestamptz null
- `formula_version` text not null
- `created_at` timestamptz not null
Indexes:
- `(org_id, user_id, created_at desc)`
- `(org_id, user_id, signal_id, created_at desc)`
## 9.2 `user_signal_latest` (materialized latest state)
- `org_id` text not null
- `user_id` uuid not null
- `signal_id` text not null
- `source` text null
- `score` numeric(5,2) not null
- `raw` jsonb null
- `last_occurred_at` timestamptz null
- `last_seen_at` timestamptz not null
- `formula_version` text not null
Primary key / unique:
- `(org_id, user_id, signal_id)`
## 9.3 `user_profile` (formula resolution)
- `org_id` text not null
- `user_id` uuid not null
- `profession` text not null
- `formula_version_override` text null
- `updated_at` timestamptz not null
Primary key / unique:
- `(org_id, user_id)`
## 9.4 `formula_definitions` (team-managed formulas)
- `org_id` text not null
- `profession` text not null
- `version` text not null
- `active` boolean not null
- `formula_json` jsonb not null
- `created_at` timestamptz not null
- `updated_at` timestamptz not null
Primary key / unique:
- `(org_id, profession, version)`
## 9.5 `user_dirty` (compute scheduling helper)
- `org_id` text not null
- `user_id` uuid not null
- `last_signal_seq` bigint not null
- `last_computed_seq` bigint null
- `next_compute_at` timestamptz null
- `last_published_at` timestamptz null
Primary key / unique:
- `(org_id, user_id)`
## 9.6 `qscore_runs` (append-only, versioned outputs)
- `id` uuid pk
- `org_id` text not null
- `user_id` uuid not null
- `profession` text not null
- `q_score` numeric(5,2) not null
- `breakdown` jsonb not null
- `formula_version` text not null
- `formula_id` text not null
- `ledger_seq_from` bigint null
- `ledger_seq_to` bigint null
- `created_at` timestamptz not null
Indexes:
- `qscore_runs(org_id, user_id, created_at desc)`
- `user_signal_latest(org_id, user_id)`
- `signal_ledger(org_id, user_id, created_at desc)`
---
## 10) Batch Recompute
Compute orchestration (MVP):
1) Live pacer (primary):
- worker updates `user_dirty.last_signal_seq` on every ingested ledger event
- worker sets `user_dirty.next_compute_at` using debounce logic
- pacer loop selects users whose `next_compute_at <= now` (recency-first)
- pacer loop must also ensure progress on older dirty users under sustained load (avoid starvation)
- compute uses `user_signal_latest`
- write `qscore_runs` + publish to `qscore.score_updates`
- update `user_dirty.last_computed_seq` + `last_published_at`
2) Periodic sweep (backlog safety):
- every `BATCH_INTERVAL_SECONDS` (default: 300), select users where `last_signal_seq > last_computed_seq`
- compute and publish
Freshness goal:
- cap staleness with `MAX_STALENESS_MS` (default: 10000)
Post-Saturday enhancements:
- selects users whose signals changed
- computes Q-Score in bulk
- writes new `qscore_runs`
Implementation note (Saturday):
- simplest is a second process/container that runs a loop (sleep -> check dirty -> compute) using the same codebase.
- we can also run it as a background task in the API process, but a separate worker is cleaner for production.
---
## 11) Codebase Layout
Proposed project path: `growqr/qscore-service/`
Reference scaffold (UV + FastAPI, job-service-like layout): `growqr/plans/qscore-service-uv-scaffold.md`
```
app/
main.py
api/
signals.py
qscore.py
registry.py
formulas.py
health.py
core/
config.py
logging.py
db/
session.py
models.py
repositories/
scoring/
engine/
loader.py
evaluator.py
breakdown.py
formula/
v1/
formula.json
worker/
stream_consumer.py
pacer.py
sweep.py
alembic/
docker-compose.yml
Dockerfile
README.md
```
---
## 12) Configuration (env vars)
Required:
- `DATABASE_URL`
Pub/Sub:
- `REDIS_URL`
- `SIGNAL_STREAM=qscore.signal_events`
- `QSCORE_UPDATE_STREAM=qscore.score_updates`
- `SIGNAL_CONSUMER_GROUP=qscore_engine`
- `SIGNAL_CONSUMER_NAME` (unique per worker instance)
Scoring/behavior:
- `FORMULA_VERSION=v1`
- `FORMULA_STORE=filesystem|db` (default: filesystem for local)
- `FORMULA_DIR=./scoring/formula` (used when FORMULA_STORE=filesystem)
- `BATCH_INTERVAL_SECONDS=300`
- `LIVE_DEBOUNCE_MS=750`
- `MAX_STALENESS_MS=10000`
- `MIN_PUBLISH_INTERVAL_MS=1500`
- `SCORE_CHANGE_EPSILON=0.01`
- `RAW_METADATA_MAX_BYTES=65536`
- `QSCORE_LATEST_TTL_SECONDS=86400`
- `P11_ACHIEVEMENT_COUNT=3`
- `P11_ACHIEVEMENT_SCORE_DEFAULT=100`
- optional VS overrides (`VS_INPUT_1`, etc.)
Ops:
- `LOG_LEVEL`
- `ENV`
---
## 13) Non-Functional Requirements
- API endpoints should be fast (target: < 300ms local for ingest/reads).
- Q-Score compute should remain under 5 seconds for a single user.
- No external network calls during Q-Score calculation (DB-only).
- Deterministic scoring logic for same stored inputs.
- Full explainability payload stored and retrievable.
---
## 14) Test Strategy
Unit tests:
- CI/SV formulas
- pillar CI weighted-average formula
- rule scoring edge cases per input
- final score active-weight denominator behavior
- formula.json schema validation and hot reload swap rules
- signal check endpoint validation rules
Integration tests:
- ingest signals -> compute qscore -> read latest
- ingest partial signals -> compute qscore (missing rules excluded from pillar avg; still impacts CI via SV)
- scheduler processes dirty users and updates `qscore_runs`
- stream consumer ingests signals and publishes score updates
- per-profession formula selection: same signals yield different Q when profession differs
- org_id isolation: signals from org A do not affect org B
Contract tests:
- validate signal ingest payload shape and `signal_id` naming conventions
Performance checks:
- ingest/read endpoints stay fast; compute remains under target runtime
---
## 15) Delivery Plan (Wed to Sat)
## Wednesday
- finalize spec mapping in code constants
- scaffold service, DB models, migrations, compose
- implement baseline scoring engine and response schema
- implement signals ingest contract + persistence
## Thursday
- implement `/v1/qscore/compute`
- implement `/v1/qscore/{user_id}` read
- implement `/v1/signals/check` and `/v1/registry/signals`
- add Redis streams to docker-compose and implement stream publish/consume
## Friday
- complete full rule-level breakdown output
- implement `/v1/signals/{user_id}` read
- implement batch scheduler loop (every 5 minutes, configurable)
- publish score update events on recompute
- write tests and fix gaps
## Saturday (buffer + integration)
- integration fixes with upstream callers
- perf pass and hardening
- docs and handoff
---
## 16) Meeting Walkthrough Script (for team confirmation)
Use this sequence in the call:
1. **Goal**: one explainable Q-Score service, ready by Saturday.
2. **Scope**: live signal stream ingest + compute Q-Score + publish live score updates + read endpoints.
3. **Decisions locked**:
- no tiers now
- this service only computes Q (no rule processing)
- full breakdown response required
- upstream services produce scored signals; this service only computes Q
4. **Math**: explain CI per input, weighted pillar CI, final active-weight normalization.
5. **Data flow**: agent publishes signal events to the input stream; engine consumes and publishes Q-Score update events.
6. **Future-safe design**: formula versioning + ledger history + scalable micro-batching.
7. **Timeline**: Wed/Thu/Fri/Sat milestones.
8. **Sign-off checklist** (below).
---
## 17) Sign-off Checklist
Please confirm in meeting:
- [ ] Endpoint contracts are approved.
- [ ] Rule and pillar mapping in `Q-Score_Service.md` is final for this sprint.
- [ ] Pillar 11 placeholder scoring is acceptable for MVP.
- [ ] Pillar 11 achievements/points mapping confirmed (open item for tonight).
- [ ] Goals stored-only behavior is approved.
- [ ] No-tier response format is approved.
- [ ] Signal ingest contract is approved (signal_id naming + score semantics).
- [ ] Signal check contract is approved (validation rules + unknown-signal behavior).
- [ ] Registry exposure scope approved (sanitized signals list vs full formula exposure).
- [ ] Multi-tenancy decision: include `org_id` now or defer.
---
## 18) Risks and Mitigations
1. **Spec ambiguity in pillar/rule mapping**
Mitigation: freeze a code-side config map and share before implementation lock.
2. **Upstream signal contract churn**
Mitigation: define stable `signal_id` registry and allow unknown signals to be stored but ignored by formula v1.
3. **Formula evolution complexity**
Mitigation: version the formula (`FORMULA_VERSION`) and make formula definitions config-driven.
---
## 19) Final Delivery Artifacts
By handoff, we will provide:
- running service via docker compose
- API endpoints documented
- DB migrations
- rule/pillar scoring engine
- explainable read response
- operational README for local + staging setup
---
This document is intended to be the single source of implementation alignment for the current sprint.
+179
View File
@@ -0,0 +1,179 @@
# GrowQR Q-Score Engine - Meeting Brief (Top-Down)
Prepared for: Team alignment meeting
## 1) What We Are Building (Conceptual)
The Q-Score service is a dedicated "scoring engine" with live pub/sub feeds.
- Other services (agents) compute individual signals (rule outputs) from raw data.
- Example producers: social branding/LinkedIn agent, resume analyzer, education verifier, engagement logger, etc.
- The Q-Score engine does NOT scrape, parse, or analyze raw inputs.
- The Q-Score engine owns the formula (the "secret sauce") that converts signals into:
- pillar scores and contributions
- a final Q-Score
- a fully explainable breakdown that can be shown in the UI
Live communication requirement:
- The orchestrator agent publishes a continuous stream of signals.
- The Q-Score engine publishes a continuous stream of score updates.
- The agent subscribes to score updates so it can react instantly.
Practical meaning:
- Every microservice can remain "agent-owned" (each owns its own processing).
- Q-Score remains the shared engine that only aggregates and scores.
Agent "latest value" access without APIs:
- In addition to consuming the update stream, the engine writes `qscore:latest:{org_id}:{user_id}` in Redis.
- Agent can read that key to recover the last known score after restart.
In short:
"Producers compute signals. Q-Score engine aggregates and scores."
## 2) How We Implement It (Service Responsibilities)
The service has three core responsibilities:
1) Accept signals from producers and store them (with history).
2) Resolve the correct formula for the user (org + profession).
3) Compute Q-Score using that formula.
4) Publish score updates when the computed score changes.
5) Serve explainable outputs (rules + pillars + math) to the frontend/clients.
We keep the implementation modular so we can scale from today's ~44 rules to hundreds of signals and multiple formula versions.
## 3) How Rules, Pillars, and the Formula Work
### 3.1 Signals and Rules
- A "rule" in v1 is effectively a named slot that expects a processed signal.
- Each signal arrives already normalized to a `score` in `0..100`.
- Producers send only what they have; missing signals are interpreted as missing at compute time.
### 3.2 Inputs and CI (Confidence/Completeness)
We group rules into inputs (LinkedIn, Resume, Education, Engagement, Goals, Platform). Each input has:
- VS (verification score): fixed constant per input (from the current spec)
- SV (signal volume): how complete the input is based on which rules have signals
CI per input:
`CI = (0.7 * VS) + (0.3 * SV)`
Important behavior (agreed):
- If a signal is missing, it does NOT reduce the pillar average directly.
- Missing signals DO reduce SV, which reduces CI (so completeness still matters).
### 3.3 Pillars
- A pillar is a weighted group of rules.
- Pillar score: average of rule scores that are present (missing rules are excluded from the average).
Pillar CI (agreed): weighted average of input CIs contributing to the pillar.
- weights are based on "data points" = number of rules from each input inside that pillar.
### 3.4 Final Q-Score
- Each pillar contributes:
- `contribution = pillar_score * pillar_weight * pillar_ci`
- Final Q-Score is normalized by active pillar weights:
- `Q = sum(contributions) / sum(active_weights)`
## 4) The API Surface (How Teams Use the Engine)
Producers (upstream services):
1) Discover accepted signal IDs
- `GET /v1/registry/signals`
- returns a sanitized list of accepted `signal_id`s + basic metadata
Multi-tenant + profession aware:
- Requests/streams are keyed by `org_id`.
- Each user has a `profession`, which selects the active formula.
2) Validate a signal payload (fire-and-forget safety)
- `POST /v1/signals/check`
- input: `{ user_id, signal_id, score, raw? }`
- output: `{ valid, errors[], warnings[] }`
3) Ingest signals
- `POST /v1/signals/ingest`
- input: `{ user_id, source, occurred_at?, signals:[{signal_id, score, raw?}] }`
Streaming (preferred integration):
- Agent publishes signal events to the input stream (same shape as ingest).
- Agent subscribes to the output stream to receive Q-Score updates instantly.
Output updates include the full explainability breakdown so the agent/UI can render without extra RPC.
Consumers (frontend / read clients):
4) Compute now (manual trigger)
- `POST /v1/qscore/compute` with `{ user_id }`
5) Read latest score (full explainability)
- `GET /v1/qscore/{user_id}`
6) Read latest signals (debug + transparency)
- `GET /v1/signals/{user_id}`
## 5) Dataflow and Storage (Ledger + Latest + Outputs)
We store data in a way that preserves history AND keeps compute fast.
1) Append-only ledger (history)
- Every signal update is written to `signal_ledger`.
- This provides a timeline of how the user evolved.
2) Latest materialized state (fast compute)
- For each `(user_id, signal_id)` we keep the latest entry in `user_signal_latest`.
- Q-score compute reads from `user_signal_latest` (not from the full ledger).
3) Q-score outputs are versioned
- Every compute produces a new row in `qscore_runs` (append-only).
- This lets us track how computed outputs evolve over time.
4) Scheduler orchestration (batch)
- A small scheduler runs every `BATCH_INTERVAL_SECONDS` (default 300).
- It recomputes only users whose signals changed since the last compute.
- This is the path to scalable batch processing (intraday/hourly) without real-time pressure.
5) Live update loop (pub/sub)
- The engine consumes signal events continuously.
- The engine recomputes quickly (micro-batched / debounced).
- When the score changes, the engine publishes a score update event.
## 6) Formula Storage and Hot Reload
- Formula definitions live in a committed JSON file (version controlled): `formula.json`.
- We can support v1/v2 side-by-side.
- We support a reload mechanism:
- `POST /v1/formula/reload` reloads JSON from disk and swaps it only if validation passes.
## 7) Saturday Scope (What Is "Done")
For Saturday, we aim to deliver:
- A working engine-only service (no upstream processing logic)
- v1 formula JSON for the current rules and pillars (from `Q-Score_Service.md`)
- The ingest/check/registry/compute/read endpoints
- Ledger + latest + qscore_runs persistence
- Scheduler running every 5 minutes (configurable) + live micro-batched updates
- Explainability payload that is UI-ready
## 8) Clarifications to Close in Meeting
These are the only remaining items that affect integration and long-term scaling:
1) Canonical `signal_id` naming convention (and who owns the list going forward)
2) What raw metadata we store in the ledger (and size/PII limits)
3) Registry exposure scope:
- sanitized signals list only (recommended)
- or full formula exposure
4) Multi-tenancy decision:
- include `org_id` now (default "growqr")
- or defer until the first B2B client
5) Pillar 11 achievements: exact mapping of the 3 achievements to scores/points
+75
View File
@@ -0,0 +1,75 @@
**Q-SCORE SERVICE \- DEVELOPER HANDOFF SPECIFICATION**
FEBRUARY 28TH LAUNCH | Minimal Viable Product | From: Katie | To: Rahul (Developer)
1\. THE 5 KPI (Current Release Only)
| Input \# | KPI Name | Data Source | VS Score | What to Extract |
| :---- | :---- | :---- | :---- | :---- |
| **1** | LinkedIn Profile | LinkedIn API (OAuth) | 1.0 | Connections, skills, endorsements, experience, headline, summary, photo |
| **2** | Resume/CV Upload | File Upload \+ GPT-4 | 0.9 | ATS score, keywords, projects, grammar, achievements, years exp |
| **3** | Education Docs | Document Upload | 0.9 | Institution name, degree, GPA, major, graduation year, verification |
| **4** | Engagement Activity | Backend Tracking | 0.7 | Login frequency, streaks, session time, features used, trends |
| **5** | Goals & Self-Assessment | User Form | 0.5 | Career goals, timeline, urgency, work preferences, target role |
**2\. RULES & CALCULATIONS (Grouped by Input Source)**
| KPI 1: LINKEDIN (11 rules) • Account connected: Binary (0 or 100\) • Profile photo: Binary (0 or 100\) • Headline quality: Length \+ keywords (0-100) • Summary complete: % filled (0-100) • Experience detail: Entries × detail (0-100) • Skills listed: 0=0, 5=50, 10+=100 • Endorsements: 0=0, 10=50, 50+=100 • Recommendations: 0=0, 3=50, 10+=100 • Connections: 0-50=20, 51-200=50, 201-500=75, 500+=100 • Post engagement (90d): (Likes+comments+shares)/posts • Education listed: Binary (0 or 100\) KPI 2: RESUME (15 rules) • Resume uploaded: Binary (0 or 100\) • ATS compatibility: GPT-4 scan (0-100) • Keyword relevance: Match to role (0-100) • Quantified achievements: Count metrics (0-100) • Grammar & clarity: AI analysis (0-100) • Format & structure: Sections/hierarchy (0-100) • Contact info: Email+phone+LinkedIn (0-100) • Page count: 1-2 pages=100, 3+=50 • Technical keywords: Count (0-100) • Project count: 0=0, 2=50, 3=75, 5+=100 • Project complexity: Tech stack × impact (0-100) • Years experience: 0=0, 1=40, 2=70, 3+=100 • Leadership indicators: Led/managed keywords • Impact statements: Increased/reduced verbs • Education section: Degree+institution+year KPI 3: EDUCATION (4 rules) • Documents uploaded: Binary (0 or 100\) • Institution tier: IIT/IIM=100, NIT=80, Tier2=60, Tier3=40 • Degree relevance: Match to career path (0-100) • GPA/percentage: Normalized (0-100) | KPI 4: ENGAGEMENT (8 rules) • Login frequency: Logins per week (0-7 scale) • Session duration avg: Minutes per session • Features used: Count of different features • Current streak: Consecutive days active • Longest streak: Historical max streak • Streak recovery: Ability to resume streaks • 30-day trend: Activity increase/decrease % • 90-day trend: Long-term activity pattern KPI 5: GOALS (6 rules) • Goals set: Binary \+ count • Goal clarity: Specific=100, Vague=50 • Timeline urgency: Immediate=100, Long-term=50 • Work preferences: Remote/hybrid/office INPUT6: |
| :---- | :---- |
**3\. KPI WEIGHTAGES (11 Pillars \- Must Total 100%)**
| Pillar | | Primary Inputs Used | Example Calculation |
| :---- | :---- | :---- | :---- |
| **01\. Presence & Profile** | | KPI 1, 2 | LinkedIn(11 rules) \+ Resume(15) |
| **02\. Learning & Credentials\*** | | KPI 3 | Education(4) |
| | | | |
| | | | |
| **05\. Engagement & Consistency** | | KPI 4 | All 8 engagement rules → Avg → Pillar Score |
| **06\. Communication Signals** | | KPI 1, 2 | LinkedIn summary \+ Resume grammar → Avg |
| | | | |
| | | | |
| | | | |
| | | | |
| **11\. Recognition & Rewards** | | Platform (Input 6\) | Badges \+ Points → Simple scoring \[BASIC\] |
\*the educations needs to be validated
| Input \# | KPI Name | Student Weightage & Q’s it gives | Prof-Technical Weightage & Q’s it gives | Prof-Creative Weightage & Q’s it gives | Prof-Sales/BD Weightage & Q’s it gives | Prof-Leadership Weightage & Q’s it gives | Prof-Marketing Weightage & Q’s it gives | Prof-Entrepreneur Weightage & Q’s it gives |
| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- |
| **1** | LinkedIn Profile | 2.84, SQ, CQm, NQ, CuQ, | 2.15, SQ, CQm, NQ, CuQ, RQx | 2.58, SQ, CQm, NQ, CuQ | 3.49, SQ, CQm, NQ, CuQ, RQx | 3.34, SQ, CQm, NQ, CuQ, RQx | 2.96, SQ, CQm, NQ, CuQ | 2.05, SQ, CQm, NQ, CuQ |
| **2** | Resume/CV Upload | 3.78, XQ, CQm, VQ | 3.01, XQ, CQm, VQ | 3.01, XQ, CQm, VQ | 3.1, XQ, CQm, VQ, RQx | 2.92, XQ, CQm, VQ, RQx | 2.96, XQ, CQm, VQ | 2.05, XQ, CQm, VQ |
| **3** | Education Docs | 3.78 | 2.58, DQm | 2.58 | 1.94, RQx | 2.5, RQx | 2.11 | 1.37 |
| **4** | Engagement Activity | 4.25, XQ, GQ, DQ | 3.44, XQ, GQ, DQ | 3.44, XQ, GQ, DQ | 3.1, XQ, GQ, DQ | 2.92, XQ, GQ, DQ | 3.37, XQ, GQ, DQ | 2.4, XQ, GQ, DQ |
| **5** | Goals & Self-Assessment | 3.5, GQ, VQ, DQ | 3.25, GQ, VQ, DQ | 3.5, GQ, VQ, DQ | 6, GQ, VQ, DQ | 4.25, GQ, VQ, DQ | 4, GQ, VQ, DQ | 7.5, GQ, VQ, DQ |
| **6\.** | Cover Letter | 1.89, CQm,VQ | 1.29, CQm,VQ | 1.72, CQm,VQ | 1.94, CQm,VQ, InQ | 1.67, CQm,VQ, InQ | 1.69, CQm,VQ | 1.03, CQm,VQ |
**SQ- Social Quotient, XQ- Execution Quotient, CQm- Communication Quotient, VQ- Vision Quotient, DQ- Drive Quotient,**
**GQ- Growth Quotient , RQx- Reputation Quotient ,DQM- Domain Quotient**
**4\. CORE ALGORITHM (Step-by-Step Implementation)**
| STEP 1: Calculate CI Formula: CI \= (0.7 × VS) \+ (0.3 × SV) Where: VS \= Verification Score Input 1 (LinkedIn) \= 1.0 Input 2 (Resume) \= 0.9 Input 3 (Education) \= 0.9 Input 4 (Engagement) \= 0.7 Input 5 (Goals) \= 0.5 SV \= Signal Volume Count of rules with data Normalize to 0-1 scale Example: LinkedIn: VS=1.0, SV=0.9 CI \= (0.7×1.0)+(0.3×0.9) CI \= 0.97 | STEP 2: Pillar Scores Formula: Pillar \= Score × Weight × CI Process: 1\. For each pillar: Get all rules for that pillar Calculate avg score 2\. Apply formula: Contribution \= Score × Weight × CI Example (Pillar 01): LinkedIn score: 75/100 Weight: 20% \= 0.20 CI: 0.97 Contribution: 75 × 0.20 × 0.97 \= 14.55 Do this for all 11 pillars | STEP 3: Final Q-Score Formula: Q \= Σ(Pillars) / Σ(Weights) Process: 1\. Sum all pillar contributions 2\. Sum active weights (Exclude Pillars 03, 08, 09 if not enabled / no data) 3\. Divide: Q-Score \= Total / Active Weights Example: Total contributions: 50.20 Active weights: 0.752 (Excludes Pillar 03, 08, 09\) Q-Score: 50.20 / 0.752 \= 66.76 Output: • Q-Score: 67.85/100 • Tier: Job Ready • Percentile: Calculate rank |
| :---- | :---- | :---- |
**5\. SIGNALS MEASURED (What Each Input Captures)**
| Input | Signal Type | What It Tells Us |
| :---- | :---- | :---- |
| **1\. LinkedIn** | Professional Network | |
| **2\. Resume** | Skills & Experience | |
| **3\. Education** | Academic Foundation | |
| **4\. Engagement** | Commitment & Drive | |
| **5\. Goals** | Vision & Planning | |
**6\. ONBOARDING INTEGRATION (API Contract)**
| | Processing Logic: 1\. Receive onboarding data 2\. Validate all 5 inputs present 3\. Run all \~44 rules 4\. Calculate 11 pillar scores 5\. Apply weights & CI 6\. Generate Q-Score (0-100) 7\. Determine tier & percentile 8\. Store in database 9\. Return response Processing Time: \< 5 seconds |
| :---- | :---- |
**7\. MINIMAL REQUIREMENTS FOR FEB 28TH LAUNCH**
| MUST HAVE ✓ ✓ 5 input parameters working ✓ \~44 core rules implemented ✓ 11 pillar calculations ✓ CI formula working ✓ Q-Score calculation (0-100) ✓ Tier assignment (5 tiers) ✓ Onboarding endpoint ✓ Read endpoint ✓ Database persistence ✓ Basic error handling ✓ Returns score in | SHOULD HAVE \~ \~ Update endpoint (for future) \~ Admin interface skeleton \~ Percentile calculation \~ Score breakdown in response \~ Logging & monitoring \~ Basic validation \~ API documentation \~ Scalable architecture |
| :---- | :---- |
CRITICAL: Focus on the \~44 rules from 5 inputs. Get the calculation engine working. The onboarding endpoint must return accurate Q-Score. Everything else is Phase 2\.
+789
View File
@@ -0,0 +1,789 @@
{
"_meta": {
"document": "GrowQR Social Branding API — LinkedIn Profile Fetch",
"version": "1.0",
"generated_from": "Live database record (2026-02-11T07:47:30Z)",
"base_url": "https://<your-domain>/api/v1/profiles",
"authentication": "Bearer <clerk_jwt_token> in Authorization header"
},
"endpoint": {
"method": "POST",
"path": "/api/v1/profiles/fetch-by-url",
"description": "Fetches full LinkedIn profile data by scraping the provided URL via two external APIs (Bright Data + Scrapetable), merges the results, stores in database, and returns the enriched profile.",
"headers": {
"Authorization": "Bearer <clerk_jwt_token>",
"Content-Type": "application/json"
}
},
"request": {
"linkedin_url": "https://www.linkedin.com/in/rahul2608/"
},
"data_extraction_pipeline": {
"_note": "This section documents HOW each field in the response is sourced and transformed. Understanding this pipeline confirms the response structure is correct.",
"step_1_parallel_fetch": {
"description": "Two external APIs are called concurrently via asyncio.gather()",
"source_a": {
"name": "Bright Data Web Scraper API",
"url": "https://api.brightdata.com/datasets/v3/scrape",
"provides": [
"experience",
"education",
"skills",
"certifications",
"languages",
"courses",
"projects",
"organizations",
"honors_awards",
"recommendations",
"activity",
"current_company",
"banner_image_url",
"industry",
"headline",
"summary",
"first_name",
"last_name",
"location",
"profile_picture_url",
"followers_count",
"connections_count"
],
"field_mapping": {
"raw → normalized": {
"name → first_name/last_name": "Split on first space, or use first_name/last_name directly",
"position|headline → headline": "Falls back to 'headline' if 'position' missing",
"about → summary": "Direct map",
"city|location → location": "Prefers 'city' field",
"avatar|profile_picture_url → profile_picture_url": "Prefers 'avatar'",
"banner_image|background_image → banner_image_url": "Prefers 'banner_image'",
"url|linkedin_url → public_profile_url": "Direct map",
"followers|followers_count → followers_count": "Prefers 'followers'",
"connections|connections_count → connections_count": "Prefers 'connections'",
"current_company.industry → industry": "Extracted from nested company object",
"current_company.link|url|linkedin_url → current_company.url": "Prefers 'link'",
"current_company.logo|logo_url → current_company.logo_url": "Prefers 'logo'",
"experience[].company_linkedin_url → experience[].company_url": "Renamed field",
"education[].title|school → education[].school": "Prefers 'title'",
"education[].start_year|start_date → education[].start_date": "Prefers 'start_year'",
"certifications[].title|name → certifications[].name": "Prefers 'title'",
"certifications[].subtitle|authority → certifications[].authority": "Prefers 'subtitle'",
"certifications[].meta → certifications[].start_date": "Parsed from 'Issued MMM YYYY'",
"skills[]: string|{name|skill} → skills[]: string": "Extracts name from object if needed",
"languages[].title|name → languages[].name": "Prefers 'title'",
"languages[].subtitle|proficiency → languages[].proficiency": "Prefers 'subtitle'",
"courses[].title → courses[].name": "Renamed",
"courses[].subtitle → courses[].number": "Renamed",
"organizations[].title → organizations[].name": "Renamed",
"organizations[].membership_type → organizations[].position": "Renamed",
"honors_and_awards[].publication → honors_awards[].issuer": "Renamed",
"activity[].link → activity[].url": "Renamed",
"activity[].interaction → activity[].type": "Renamed",
"activity[].reactions → activity[].likes": "Renamed",
"recommendations[]: string → {text, recommender_name}": "Parsed from 'Name \"text\"' format"
}
}
},
"source_b": {
"name": "Scrapetable API",
"url": "https://v3.scrapetable.com/linkedin/people",
"provides": [
"experience",
"education",
"skills",
"first_name",
"last_name",
"headline",
"summary",
"location",
"profile_picture_url",
"linkedin_id",
"followers_count",
"connections_count"
],
"field_mapping": {
"raw → normalized": {
"person.firstName → first_name": "Direct map",
"person.lastName → last_name": "Direct map",
"person.headline → headline": "Direct map",
"person.summary → summary": "Direct map",
"person.geoFull → location": "Full geo string",
"person.profilePicture → profile_picture_url": "CDN-hosted image URL",
"person.profileUrl → public_profile_url": "Direct map",
"person.linkedinId → linkedin_id": "Direct map",
"person.followers → followers_count": "Direct map",
"person.connections → connections_count": "Direct map",
"person.skills → skills": "Already a string array, used directly",
"person.positions[].companyName → experience[].company": "Renamed",
"person.positions[].startYear + startMonth → experience[].start_date": "Formatted as MM/YYYY",
"person.positions[].endYear + endMonth → experience[].end_date": "Formatted as MM/YYYY or 'Present'",
"person.positions[].companyId → experience[].company_url": "Constructed as linkedin.com/company/{id}",
"person.education[].schoolName → education[].school": "Renamed",
"person.education[].startYear → education[].start_date": "Year string only",
"person.education[].endYear → education[].end_date": "Year string only"
}
}
}
},
"step_2_merge_strategy": {
"description": "Fields are merged with priority rules — Scrapetable for structured data quality, Bright Data for breadth.",
"priority_rules": {
"experience": "Scrapetable first, Bright Data fallback",
"education": "Scrapetable first, Bright Data fallback",
"skills": "Scrapetable first, Bright Data fallback",
"headline": "Scrapetable first, Bright Data fallback",
"summary": "Longer of the two (more complete)",
"first_name": "Scrapetable first, Bright Data fallback",
"last_name": "Scrapetable first, Bright Data fallback",
"location": "Scrapetable first, Bright Data fallback",
"profile_picture_url": "Scrapetable first (higher quality CDN)",
"linkedin_id": "Scrapetable first, Bright Data fallback",
"followers_count": "Whichever has data",
"connections_count": "Whichever has data",
"industry": "Bright Data only",
"certifications": "Bright Data only",
"languages": "Bright Data only",
"courses": "Bright Data only",
"projects": "Bright Data only",
"organizations": "Bright Data only",
"honors_awards": "Bright Data only",
"recommendations": "Bright Data only",
"activity": "Bright Data only",
"current_company": "Bright Data only",
"banner_image_url": "Bright Data only"
}
},
"step_3_db_persistence": {
"description": "Merged data is written to linkedin_profiles table (PostgreSQL JSONB columns for arrays/objects, TEXT/VARCHAR for scalars). connection_status set to CONNECTED, last_synced_at set to now()."
},
"step_4_response_assembly": {
"description": "The _build_profile_detail_response() function reads the DB model and constructs LinkedInProfileDetailResponse Pydantic model, which is wrapped in FetchByUrlResponse."
}
},
"response": {
"success": true,
"message": "Profile fetched successfully",
"fetch_status": "completed",
"profile": {
"id": "70529a20-873d-4a6f-bc11-ab6bcec5b8b0",
"linkedin_id": "ACoAAB8LpMUB2XVVN-uGsshaioFR58XxqPmOwZM",
"first_name": "Rahul",
"last_name": "Gupta",
"headline": "Walmart Global Tech | MS in Computer Science",
"summary": "At Fleuxlabs, our team leverages artificial intelligence to transform user interaction with digital platforms, fostering a seamless fusion of human cognitive processes and technological innovation. Our solutions, powered by AI expertise, particularly in PyTorch and Jupyter, are designed to deliver intuitive experiences that resonate with our users.\n\nMy educational pursuits at San Diego State University in Computer Science are instrumental in shaping the strategic direction of Fleuxlabs. With a solid background in AI and deep learning, the insights gained from my studies are integral to the pioneering work we do, ensuring that our technological advancements continue to set industry standards.",
"industry": null,
"location": "United States",
"profile_picture_url": "https://scrapetable.b-cdn.net/linkedin-people/linkedin-profile-rahul2608-1770796033431.jpg",
"public_profile_url": "https://www.linkedin.com/in/rahul2608/",
"connection_status": "CONNECTED",
"connections_count": 500,
"followers_count": 623,
"last_synced_at": "2026-02-11T07:47:30.361228+00:00",
"created_at": "2026-02-11T07:47:10.284378+00:00",
"email": null,
"banner_image_url": "https://media.licdn.com/dms/image/v2/C5116AQGVMt-5-aC2TQ/profile-displaybackgroundimage-shrink_200_800/profile-displaybackgroundimage-shrink_200_800/0/1517506105412?e=2147483647&v=beta&t=YxKKlI8jdr7gzD3rg5c4GSwh9NZDr9yt2Y68CYG4wp4",
"current_company": {
"name": "Walmart Global Tech",
"industry": null,
"url": "https://www.linkedin.com/company/walmartglobaltech?trk=public_profile_topcard-current-company",
"logo_url": null
},
"experience": [
{
"title": "AI Engineer | ML Ops",
"company": "Walmart Global Tech",
"company_url": "https://www.linkedin.com/company/11174522",
"location": "Sunnyvale, California, United States",
"start_date": "06/2018",
"end_date": "06/2025",
"duration": null,
"is_current": false,
"description": null
},
{
"title": "Software Development Engineer II",
"company": "Walmart Global Tech",
"company_url": "https://www.linkedin.com/company/11174522",
"location": "Sunnyvale, California, United States",
"start_date": "06/2021",
"end_date": "08/2022",
"duration": null,
"is_current": false,
"description": null
},
{
"title": "Peer Mentor",
"company": "San Diego State University",
"company_url": "https://www.linkedin.com/company/6206",
"location": "International Student Centre, San Diego",
"start_date": "09/2016",
"end_date": "05/2018",
"duration": null,
"is_current": false,
"description": "As a student mentor, I worked with incoming international freshmen to help them adapt to a new culture and environment. I ensured that they were successful by guiding and counseling them on how to navigate their first semester at the university. I also made sure that they connected with the various resources available on campus."
},
{
"title": "Software Developer",
"company": "Cal Hacks",
"company_url": "https://www.linkedin.com/company/6430466",
"location": null,
"start_date": "10/2015",
"end_date": "10/2015",
"duration": null,
"is_current": false,
"description": "I developed a virtual reality game using the Myo motion detector, which allowed users to virtually interact with objects and characters in the game."
}
],
"education": [
{
"school": "San Diego State University",
"degree": "Master of Science - MS",
"field_of_study": "Computer Science",
"start_date": "2022",
"end_date": "2024",
"school_url": null,
"description": null,
"activities": null
},
{
"school": "San Diego State University-California State University",
"degree": "Bachelor of Science (B.S.)",
"field_of_study": "Computer Science",
"start_date": "2014",
"end_date": "2018",
"school_url": null,
"description": null,
"activities": null
}
],
"skills": [
"PyTorch",
"Jupyter",
"Artificial Intelligence (AI)",
"Vector Databases",
"Large Language Models (LLM)",
"Next.js",
"Machine Learning",
"Java",
"C",
"C++",
"Bash",
"Data Structures",
"Computer Architecture",
"Lua",
"Haskell",
"Unix",
"SQL",
"Object-Oriented Programming (OOP)",
"Python",
"JavaScript"
],
"certifications": [
{
"name": "Oracle Certified Professional, Java SE 8 Programmer II",
"issuing_organization": null,
"authority": "Oracle",
"issue_date": null,
"credential_url": null,
"license_number": null,
"url": "https://www.youracclaim.com/badges/95333ac3-13e2-422f-a75c-9d9ab8fc1d7b/linked_in_profile?trk=public_profile_see-credential",
"start_date": "Apr 2019",
"end_date": null
},
{
"name": "Oracle Certified Associate, Java SE 8 Programmer",
"issuing_organization": null,
"authority": "Oracle",
"issue_date": null,
"credential_url": null,
"license_number": null,
"url": "https://www.youracclaim.com/badges/a8858bd2-295e-4590-ad76-0cb1bd9b83e7/linked_in_profile?trk=public_profile_see-credential",
"start_date": "Feb 2019",
"end_date": null
}
],
"languages": [
{
"name": "English",
"proficiency": "Full professional proficiency"
},
{
"name": "Hindi",
"proficiency": "Native or bilingual proficiency"
}
],
"courses": [
{
"name": "Assembly Language and Machine Learning",
"number": "237",
"associated_with": null
},
{
"name": "Computer Architecture",
"number": "370",
"associated_with": null
},
{
"name": "Intermediate Java Programming",
"number": "108",
"associated_with": null
},
{
"name": "Programming Languages",
"number": "320",
"associated_with": null
},
{
"name": "Statistical Methods",
"number": "350A",
"associated_with": null
}
],
"projects": [
{
"name": "Fleuxlabs",
"url": null,
"start_date": "Aug 2022",
"end_date": null,
"description": null,
"associated_with": null
}
],
"organizations": [
{
"name": "Golden Key International Honour Society",
"position": "Member",
"start_date": "Sep 2016",
"end_date": "Present",
"description": "One of the most prestigious honour society at SDSU which only accepts students who are in the top 15 percent of their class.The purpose of the society is to recognize and encourage scholastic achievement and excellence in all undergraduate fields of study, to unite with collegiate faculties and administrators in developing and maintaining high educational standards, to provide scholarships to outstanding initiates, and to foster altruistic conduct through voluntary service."
},
{
"name": "Model United Nations (SDSU)",
"position": "Member",
"start_date": "Sep 2016",
"end_date": "Present",
"description": "This platform gave me a chance talk about issues that concern our entire humanity, and gain leadership skills. The chance to attend conferences in different campuses not only helped me inculcate people skills but also listening to my fellows made me realise that there are so many things that we can learn from each other."
},
{
"name": "Phi Eta Sigma National Honor Society, Inc.",
"position": "Member",
"start_date": "Sep 2015",
"end_date": "Present",
"description": "Phi Eta Sigma is an all-disciplines freshman national honor society established in 1923. The purpose of Phi Eta Sigma is to encourage and reward high scholastic achievement among freshmen students in institutions of higher education. This society definitely helped to evolve my character and care for my fellow beings. Now I truly believe that it is essential to use our academic prowess not only for our personal benefits but rather for the betterment of entire mankind."
},
{
"name": "The National Society of Collegiate Scholars",
"position": "Member",
"start_date": "Sep 2015",
"end_date": "Present",
"description": "The National Society of Collegiate Scholars (NSCS) is an honors organization that recognizes and elevates high achievers."
}
],
"honors_awards": [
{
"title": "Dean's List Awardee",
"issuer": "College of Sciences, San Diego State University",
"date": "2016-09-01T00:00:00.000Z",
"description": "Spring 2016",
"associated_with": null
},
{
"title": "Dean's List Awardee",
"issuer": "College of Sciences, San Diego State University",
"date": "2016-01-01T00:00:00.000Z",
"description": "Fall 2015",
"associated_with": null
},
{
"title": "Dean's List Awardee",
"issuer": "College of Sciences, San Diego State University",
"date": "2015-09-01T00:00:00.000Z",
"description": "Spring 2015",
"associated_with": null
},
{
"title": "Dean's List Awardee",
"issuer": "College of Sciences, San Diego State University",
"date": "2015-01-01T00:00:00.000Z",
"description": "Fall 2014",
"associated_with": null
}
],
"recommendations": [
{
"recommender_name": null,
"recommender_title": null,
"relationship": null,
"text": "Parvesh Koya “I have known Rahul from our first day at San Diego State University. From the first day I met him, it was very clear that he was committed, motivated, and profoundly intelligent. I have had the opportunity of working together with him on many projects and have seen first hand his leadership capabilities and his attention to detail. His leadership qualities combined with his in-depth knowledge of the various fields of computer science, Rahul is a priceless asset to any team. ”",
"date": null
}
],
"activity": [
{
"type": "Liked by Rahul Gupta",
"title": "I hosted the China-India Youth Dialogue on February 25th alongside Su Chen from the Political Department of the Embassy of the People's Republic of…",
"url": "https://www.linkedin.com/posts/amulyagupta1_bilateralties-globalcitizenship-collaborationbeyondborder-activity-7300845318680522752-UTAz",
"date": null,
"likes": null,
"comments": null
},
{
"type": "Liked by Rahul Gupta",
"title": "Are we all just performing? In a world where every moment is curated and shared, the line between authenticity and performance is blurrier than ever.…",
"url": "https://www.linkedin.com/posts/akarshkain_modernhumanspodcast-opherbrayer-authenticity-activity-7298798754554355712-VMOe",
"date": null,
"likes": null,
"comments": null
},
{
"type": "Liked by Rahul Gupta",
"title": "Leadership Exodus: Founders, CEOs Who Quit Their Startups In 2024👇 One of the most common occurrences in the world’s third-largest startup…",
"url": "https://www.linkedin.com/posts/inc42_leadershipexodus-founderstories-indianstartups-activity-7278993255994683392-0hEZ",
"date": null,
"likes": null,
"comments": null
},
{
"type": "Liked by Rahul Gupta",
"title": "I am excited to share that I have graduated from San Diego State University with a Bachelor of Science in Computer Science, achieving the distinction…",
"url": "https://www.linkedin.com/posts/bennett-hilck-96080226a_i-am-excited-to-share-that-i-have-graduated-activity-7201032802018959360-8tg3",
"date": null,
"likes": null,
"comments": null
},
{
"type": "Liked by Rahul Gupta",
"title": "The Empire State Building is getting a fresh new look with new uniforms for its employees 🏙️ Designed by renowned fashion designer Peyman Umay…",
"url": "https://www.linkedin.com/posts/naumd_empirestatebuilding-peymanumay-workwear-activity-7026300330476802048-BI4q",
"date": null,
"likes": null,
"comments": null
},
{
"type": "Liked by Rahul Gupta",
"title": "💬 As someone who's had many conversations with founders lately, I've come up with a helpful list for anyone looking to improve their company's…",
"url": "https://www.linkedin.com/posts/kharitojulia_as-someone-whos-had-many-conversations-activity-7026301274086146048-jFiP",
"date": null,
"likes": null,
"comments": null
},
{
"type": "Liked by Rahul Gupta",
"title": "Just wanted to share my top three tools for staying organized and on top of both work and personal life! 💻📅 1️⃣ Asana - a game-changer for project…",
"url": "https://www.linkedin.com/posts/kharitojulia_just-wanted-to-share-my-top-three-tools-for-activity-7025981147188789248-E0oc",
"date": null,
"likes": null,
"comments": null
},
{
"type": "Liked by Rahul Gupta",
"title": "⚠️ If you still think of your target audience as \"people of a certain age,\" you're missing out on an opportunity to connect with them. This might…",
"url": "https://www.linkedin.com/posts/kharitojulia_if-you-still-think-of-your-target-audience-activity-7025946215502536704-WiKR",
"date": null,
"likes": null,
"comments": null
},
{
"type": "Liked by Rahul Gupta",
"title": "Are you tired of posting on social media and not seeing the results you want? I know, it's frustrating when your hard work doesn't seem to be paying…",
"url": "https://www.linkedin.com/posts/kharitojulia_are-you-tired-of-posting-on-social-media-activity-7024181789279608832-DARV",
"date": null,
"likes": null,
"comments": null
},
{
"type": "Liked by Rahul Gupta",
"title": "Yes I'm still using the hypebeast stack core: - react - typescript - node.js - postgresql addons: - graphql -…",
"url": "https://www.linkedin.com/posts/benawad_coding-programming-softwareengineering-activity-6991436280869916673-a9l0",
"date": null,
"likes": null,
"comments": null
},
{
"type": "Liked by Rahul Gupta",
"title": "Did you know that 95% of total television coverage focuses on men’s sports? Women’s leagues are hardly the focus of mainstream media, nor are they…",
"url": "https://www.linkedin.com/posts/femalequotient_did-you-know-that-95-of-total-television-activity-6998073717616492544-m6rJ",
"date": null,
"likes": null,
"comments": null
},
{
"type": "Liked by Rahul Gupta",
"title": "Hi folks, wanted to inform everyone that I am one of the unfortunate 11000 people affected due to recent layoffs at Meta. My heart goes out to…",
"url": "https://www.linkedin.com/posts/neeraj-desh_opentowork-metalayoffs-h1bvisa-activity-6996348074730139648-h33E",
"date": null,
"likes": null,
"comments": null
},
{
"type": "Liked by Rahul Gupta",
"title": "The hardest part of being a software engineer is not getting fat eating all the free snacks #coding #programming #softwareengineering…",
"url": "https://www.linkedin.com/posts/benawad_coding-programming-softwareengineering-activity-6996576933727653888-CLVq",
"date": null,
"likes": null,
"comments": null
},
{
"type": "Liked by Rahul Gupta",
"title": "I think companies should own a bit of responsibility when treating international employees on Visa. Maybe work with Congress to give extra runway (6…",
"url": "https://www.linkedin.com/posts/7ankit_i-think-companies-should-own-a-bit-of-responsibility-activity-6994394684668198912-43Tv",
"date": null,
"likes": null,
"comments": null
}
]
}
},
"error_responses": {
"auth_missing": {
"status_code": 401,
"body": {
"detail": "Authentication required"
}
},
"invalid_url": {
"status_code": 400,
"body": {
"detail": "Invalid LinkedIn URL format. URL should be like: https://linkedin.com/in/username"
}
},
"both_sources_failed": {
"status_code": 200,
"body": {
"success": false,
"message": "Failed to fetch profile data from both sources. Please try again later.",
"profile": null,
"fetch_status": "failed"
}
}
},
"field_reference": {
"_note": "Complete field-level reference for every key in profile object",
"top_level_envelope": {
"success": {
"type": "boolean",
"values": [
true,
false
]
},
"message": {
"type": "string",
"example": "Profile fetched successfully"
},
"fetch_status": {
"type": "string",
"enum": [
"pending",
"fetching",
"completed",
"failed"
]
},
"profile": {
"type": "LinkedInProfileDetailResponse | null"
}
},
"profile_scalars": {
"id": {
"type": "uuid",
"source": "DB-generated"
},
"linkedin_id": {
"type": "string",
"source": "Scrapetable > Bright Data",
"example": "ACoAAB8LpMUB2XVVN-uGsshaioFR58XxqPmOwZM"
},
"first_name": {
"type": "string|null",
"source": "Scrapetable > Bright Data"
},
"last_name": {
"type": "string|null",
"source": "Scrapetable > Bright Data"
},
"headline": {
"type": "string|null",
"source": "Scrapetable > Bright Data",
"max_length": 500
},
"summary": {
"type": "string|null",
"source": "Longer of Scrapetable vs Bright Data"
},
"industry": {
"type": "string|null",
"source": "Bright Data only (from current_company)"
},
"location": {
"type": "string|null",
"source": "Scrapetable > Bright Data"
},
"profile_picture_url": {
"type": "string|null",
"source": "Scrapetable > Bright Data (Scrapetable preferred for CDN quality)"
},
"public_profile_url": {
"type": "string|null",
"source": "Input linkedin_url"
},
"connection_status": {
"type": "string",
"enum": [
"CONNECTED",
"DISCONNECTED"
]
},
"connections_count": {
"type": "integer|null",
"source": "Whichever source has data"
},
"followers_count": {
"type": "integer|null",
"source": "Whichever source has data"
},
"last_synced_at": {
"type": "ISO 8601 datetime|null",
"source": "Set on successful fetch"
},
"created_at": {
"type": "ISO 8601 datetime",
"source": "DB-generated on first insert"
},
"email": {
"type": "string|null",
"source": "Not populated by scraping"
},
"banner_image_url": {
"type": "string|null",
"source": "Bright Data only"
}
},
"profile_objects": {
"current_company": {
"type": "object|null",
"source": "Bright Data only",
"fields": {
"name": "string|null",
"industry": "string|null",
"url": "string|null — LinkedIn company page URL",
"logo_url": "string|null — Company logo image URL"
}
}
},
"profile_arrays": {
"experience[]": {
"source": "Scrapetable > Bright Data",
"fields": {
"title": "string|null — Job title",
"company": "string|null — Company name",
"company_url": "string|null — LinkedIn company page URL",
"location": "string|null — City, State, Country",
"start_date": "string|null — Format: MM/YYYY",
"end_date": "string|null — Format: MM/YYYY or 'Present'",
"duration": "string|null — e.g. '3 yrs 2 mos'",
"is_current": "boolean — true if no end_date",
"description": "string|null — Role description"
}
},
"education[]": {
"source": "Scrapetable > Bright Data",
"fields": {
"school": "string|null — Institution name",
"degree": "string|null — e.g. 'Master of Science - MS'",
"field_of_study": "string|null — e.g. 'Computer Science'",
"start_date": "string|null — Year only: 'YYYY'",
"end_date": "string|null — Year only: 'YYYY'",
"school_url": "string|null — LinkedIn school page URL",
"description": "string|null",
"activities": "string|null"
}
},
"skills[]": {
"source": "Scrapetable > Bright Data",
"type": "string[] — Flat array of skill name strings"
},
"certifications[]": {
"source": "Bright Data only",
"fields": {
"name": "string|null — Certification name",
"issuing_organization": "string|null",
"authority": "string|null — e.g. 'Oracle'",
"issue_date": "string|null",
"credential_url": "string|null",
"license_number": "string|null",
"url": "string|null — Credential verification URL",
"start_date": "string|null — e.g. 'Apr 2019'",
"end_date": "string|null"
}
},
"languages[]": {
"source": "Bright Data only",
"fields": {
"name": "string|null — e.g. 'English'",
"proficiency": "string|null — e.g. 'Full professional proficiency'"
}
},
"courses[]": {
"source": "Bright Data only",
"fields": {
"name": "string|null — Course name",
"number": "string|null — Course number e.g. '370'",
"associated_with": "string|null"
}
},
"projects[]": {
"source": "Bright Data only",
"fields": {
"name": "string|null — Project name",
"url": "string|null — Project URL",
"start_date": "string|null — e.g. 'Aug 2022'",
"end_date": "string|null",
"description": "string|null",
"associated_with": "string|null"
}
},
"organizations[]": {
"source": "Bright Data only",
"fields": {
"name": "string|null — Organization name",
"position": "string|null — e.g. 'Member'",
"start_date": "string|null — e.g. 'Sep 2016'",
"end_date": "string|null — e.g. 'Present'",
"description": "string|null"
}
},
"honors_awards[]": {
"source": "Bright Data only",
"fields": {
"title": "string|null — Award name",
"issuer": "string|null — Issuing organization",
"date": "string|null — ISO 8601 date or free text",
"description": "string|null",
"associated_with": "string|null"
}
},
"recommendations[]": {
"source": "Bright Data only",
"fields": {
"recommender_name": "string|null",
"recommender_title": "string|null",
"relationship": "string|null",
"text": "string|null — Full recommendation text",
"date": "string|null"
}
},
"activity[]": {
"source": "Bright Data only",
"fields": {
"type": "string|null — e.g. 'Liked by Rahul Gupta'",
"title": "string|null — Post preview text (truncated)",
"url": "string|null — LinkedIn post URL",
"date": "string|null",
"likes": "integer|null",
"comments": "integer|null"
}
}
}
}
}
+187
View File
@@ -0,0 +1,187 @@
# Plan: Quotients Derived from Pillars/Rules (QScore Service)
## Branch
- Working branch: `feature/quotients-from-pillars`
- Base branch: `admin-ui`
## Goal
Add **quotient outputs** (e.g., SQ, XQ, CQm, VQ, DQ, GQ, RQx, DQm, etc.) to QScore outputs so frontend/consumers can show which quotients are available for a user and their computed values.
This must be derived from existing scoring context (signals -> rules -> pillars -> weights -> CI), not hardcoded per user.
Delivery scope includes **every output path**:
- API responses (`/v1/qscore/compute`, `/v1/qscore/{user_id}`)
- Redis score update stream (`qscore.score_updates`)
- Redis latest cache key (`qscore:latest:{org_id}:{user_id}`)
---
## What changed in product doc
From updated `docs/Q-Score_Service.md` table:
- Each KPI/input now includes associated quotient tags per profession.
- Example: LinkedIn input for student maps to `SQ, CQm, NQ, CuQ`, Resume maps to `XQ, CQm, VQ`, etc.
---
## Design proposal (quick, extensible)
### 1) Formula metadata extension
Add optional quotient tags to each input in formula JSON:
```json
"inputs": {
"linkedin": {
"vs": 1.0,
"label": "LinkedIn Profile",
"signal_ids": [...],
"quotients": ["SQ", "CQm", "NQ", "CuQ"]
}
}
```
Why input-level first:
- Doc table currently maps quotients at KPI/input level.
- Rules already map to inputs; pillars map to rules.
- We can derive rule/pillar quotient linkage without duplicating metadata everywhere.
### 2) Rule + pillar quotient attribution
- A rule inherits quotients from its input.
- A pillar’s quotient exposure = union of quotient tags from all rules in that pillar.
- For each quotient within a pillar, compute rule-share:
- `quotient_rule_share = rules_in_pillar_with_quotient / total_rules_in_pillar`
### 3) Quotient scoring math
Use same normalization pattern as Q-Score with pillar weights:
- `quotient_contribution += pillar.contribution * quotient_rule_share`
- `quotient_active_weight += pillar.weight * quotient_rule_share` (only active pillars)
- `quotient_score = quotient_contribution / quotient_active_weight` (if weight > 0 else 0)
This keeps quotient math aligned with existing pillar-weighted scoring.
### 4) API response changes
Add new top-level field in both compute/read responses:
```json
"quotients": {
"SQ": { "score": 71.2, "active": true, "contribution_sum": 21.34, "active_weight_sum": 0.299 },
"CQm": { ... }
}
```
Also include quotient details in breakdown for transparency:
```json
"breakdown": {
...,
"quotients": {
"SQ": {
"score": 71.2,
"contribution_sum": 21.34,
"active_weight_sum": 0.299,
"pillars": ["p01_presence_profile", "p06_communication_signals"]
}
}
}
```
### 5) Redis output contract changes (producer/consumer path)
Add quotient data to worker-published stream payload (`qscore.score_updates`) and latest cache key:
- Stream event (`publisher.py`) should include:
- `quotients` (JSON string)
- `breakdown` already includes `breakdown.quotients`
- Latest key (`qscore:latest:{org_id}:{user_id}`) should include:
- `quotients`
Example stream fields added:
```json
{
"q_score": "67.42",
"formula_version": "v1",
"quotients": "{\"SQ\":{\"score\":71.2,...}}",
"breakdown": "{\"inputs\":...,\"quotients\":...}"
}
```
---
## Files to update
### Scoring engine
- `app/scoring/engine/loader.py`
- extend `FormulaInput` with optional `quotients: list[str] = []`
- `app/scoring/engine/evaluator.py`
- compute quotient attribution + scores from pillar/rule structure
- include quotient results in `QScoreResult`
- `app/scoring/engine/breakdown.py`
- append `quotients` section to breakdown payload
### API contracts
- `app/api/schemas.py`
- add `quotients: dict[str, Any]` to:
- `QScoreComputeResponse`
- `QScoreReadResponse`
- `app/api/qscore.py`
- return new `quotients` in both compute/read paths
### Stream/cache contracts
- `app/worker/publisher.py`
- publish `quotients` in `qscore.score_updates` stream event
- include `quotients` in `qscore:latest:{org_id}:{user_id}` cache payload
- `app/worker/compute.py`
- pass computed `quotients` into publisher
### Formula data
- `app/scoring/formula/v1/*/formula.json`
- add `quotients` array to each input, profession-specific per updated doc table
### Docs
- `README.md`
- update endpoint contract examples with `quotients`
- `docs/Q-Score_Service.md`
- keep as source-of-truth reference (already updated by product)
### Tests
- `tests/unit/test_loader.py`
- validate formulas with new `quotients` field
- `tests/unit/test_evaluator.py`
- verify quotient score derivation and deterministic behavior
- `tests/unit/test_breakdown.py`
- verify `breakdown.quotients` exists and is JSON-serializable
- `tests/unit` (new publisher-focused test or extend existing)
- verify stream payload includes `quotients` and latest cache payload includes `quotients`
---
## Backward compatibility
- Make `quotients` optional in formula schema, default `[]`.
- Existing formulas without quotient metadata still load.
- In that case, API returns empty `quotients` object.
---
## Implementation order (fast path)
1. Extend loader schema and defaults.
2. Add quotient computation in evaluator.
3. Add breakdown + API response fields.
4. Add Redis stream/latest payload quotient fields.
5. Patch all v1 formula files with quotient tags.
6. Update tests and docs.
---
## Open items to confirm before coding
1. **Output shape preference**: top-level `quotients` only, or both top-level and `breakdown.quotients`? (recommended: both)
2. **Code names**: keep exact short codes from doc (`SQ`, `CQm`, `RQx`, etc.) as API keys? (recommended: yes)
3. **Undefined codes in legend** (e.g., `NQ`, `CuQ`, `InQ`) should we still pass through as-is from doc? (recommended: yes, pass-through)
---
## Acceptance criteria
- `POST /v1/qscore/compute` and `GET /v1/qscore/{user_id}` return non-empty `quotients` when mapped signals exist.
- `qscore.score_updates` stream events include `quotients`.
- `qscore:latest:{org_id}:{user_id}` cache payload includes `quotients`.
- Quotient values change when pillar/rule scores change.
- Existing Q-Score output remains unchanged.
- Formula files load successfully with new metadata.
+123
View File
@@ -0,0 +1,123 @@
# Q-Score Service Scaffold (UV + FastAPI) - Reference
Goal: mirror `growqr/job-service/` structure, but use `uv` + `pyproject.toml` + `uv.lock`.
## 1) Target Folder Structure
Proposed repo path: `growqr/qscore-service/`
```
qscore-service/
.env.example
.gitignore
README.md
docker-compose.yml
Dockerfile
pyproject.toml
uv.lock
app/
__init__.py
main.py
config.py
database.py
api/
__init__.py
v1/
__init__.py
router.py
signals.py
qscore.py
registry.py
formulas.py
auth/
__init__.py
middleware.py
schemas/
__init__.py
signals.py
qscore.py
formulas.py
services/
__init__.py
engine_service.py
scoring/
__init__.py
engine/
__init__.py
loader.py
evaluator.py
breakdown.py
formula/
v1/
formula.json
alembic/
env.py
versions/
scripts/
run_worker.py
```
Notes:
- This keeps the same mental model as `growqr/job-service/app/...`.
- We include Alembic so schema changes are migration-driven (job-service currently uses create_all).
## 2) UV Commands (Day 1)
Create project:
```bash
uv init .
uv python pin 3.12
uv venv
```
Add runtime deps:
```bash
uv add fastapi "uvicorn[standard]" pydantic pydantic-settings sqlalchemy asyncpg
uv add redis
```
Add dev deps:
```bash
uv add --dev ruff black pytest pytest-asyncio
```
Lock + sync:
```bash
uv lock
uv sync --frozen
```
Run:
```bash
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
```
## 3) Dockerfile Pattern (UV)
Use the recommended uv-based Docker pattern:
- copy `pyproject.toml` + `uv.lock`
- `uv sync --frozen --no-dev`
- copy `app/`
## 4) Migration Setup (Alembic)
We use Alembic for Postgres schema:
- `alembic init alembic`
- configure `DATABASE_URL` in `app/config.py`
- import SQLAlchemy Base metadata in `alembic/env.py`
## 5) How This Matches job-service
`growqr/job-service/app/` has:
- `main.py`, `config.py`, `database.py`
- `api/v1/router.py` to include routes
- `schemas/` + `services/`
Q-score service follows the same layout so engineers can jump between services without relearning structure.