cyberhybridhub/question_system_expantion_guide.md
nathanAndrinoid a521a74214
Some checks failed
Admin portal tests / test (push) Has been cancelled
balance controled working spelling and market questions.
2026-06-17 04:20:45 +00:00

203 lines
12 KiB
Markdown

# Question System Expansion Guide
This document explains the architecture, databases, and control flow of the Question System in CyberHybridHub. It serves as a design reference and checklist for developers who wish to introduce new types of questions.
---
## 1. System Overview & Architecture
The question system serves active, unanswered questions to users based on weighted random selection ratios. Currently, it supports:
- **Market History Questions**: Prospective price-movement guessing games where users swipe to guess price directions.
- **Spelling Questions**: Audio-driven quiz items where users listen to a word read aloud via Text-to-Speech (TTS) and type the correct spelling.
### System Diagram
```mermaid
graph TD
Client[Flutter Client] <-->|SignalR / HTTP| API[Shelf API Router]
API <--> Dispatcher[QuestionDispatcher]
Dispatcher <--> |Weighted Mix Roll| Mix[QuestionSourceMix]
Dispatcher <--> QService[QuestionService]
Dispatcher <--> Pipeline[QuestionPipeline]
QService <--> |Create & Fetch| DB[(PostgreSQL)]
Pipeline <--> |Evaluate & Transition| DB
subgraph Client-Side Presentation
Router[QuestionPresentationRouter] -->|usesSpellingWordTile| SpellingTile[SpellingQuestionTile]
Router -->|isMarket / Swipe| SwipeTile[SwipeQuestionTile]
end
Client --> Router
```
---
## 2. Core Database Schema
The questions and their pipeline states are stored in the following PostgreSQL tables:
### The `questions` Table
Tracks individual questions generated for, queued for, or answered by a user.
- `id` (`UUID PRIMARY KEY`): Unique question identifier.
- `assigned_user_id` (`TEXT`): References the user's Firebase UID.
- `question_text` (`TEXT`): The text prompt displayed to the user.
- `user_response` (`NUMERIC`): Stores numeric answers (e.g., slider inputs or swipe values like `10` / `-10`).
- `user_response_text` (`TEXT`): Stores string-based typed answers (e.g., spelling inputs).
- `correct_answer` (`NUMERIC`): The correct answer/benchmark to grade against.
- `created_at` / `modified_at` (`TIMESTAMPTZ`): Auditing and ordering timestamps.
- `source_tag` (`TEXT`): Category tag (e.g., `market_history:prospective`, `spelling:word_quiz`).
- `pipeline_key` (`TEXT`): The active pipeline context (e.g., `trading`, `spelling`, `weather`).
- `pipeline_step` (`TEXT`): Active step within the pipeline (e.g., `spelling_word_quiz`).
- `metadata` (`JSONB`): Key-value payload for question-specific properties (e.g., symbol tickers, audio URLs, canonical word answers).
### The `user_pipeline_state` Table
Maintains multi-step session progress for branching or sequential question chains.
- `assigned_user_id` (`TEXT PRIMARY KEY`): User's Firebase UID.
- `pipeline_key` (`TEXT`): The active branching pipeline identifier.
- `step` (`TEXT`): The current step index or name (e.g., `temperature_quiz`, `idle`).
- `context` (`JSONB`): Arbitrary session context (e.g., weather facts, selected words, target numbers).
- `updated_at` (`TIMESTAMPTZ`): Last state transition timestamp.
---
## 3. Configuration & Mix Ratios
The ratio of different questions served to a user is governed by **Question Source Preferences**.
### Definition & Mix Allocation
Source definitions are registered statically in [question_source_preferences.dart](file:///home/chh/cyberhybridhub.com/server/lib/question_source_preferences.dart):
```dart
static const List<QuestionSourcePreferenceDefinition> definitions = [
QuestionSourcePreferenceDefinition(
id: 'market_history',
label: 'Market question',
description: 'Prospective price-move guessing game',
),
QuestionSourcePreferenceDefinition(
id: 'spelling',
label: 'Spelling',
description: 'Spell-check quiz from curated word lists',
),
];
```
1. **Preference Selection**: Users can enable/disable individual question sources and configure their relative weights (ratios) from 0 to 100 via the settings panel.
2. **Effective Mix**: The server resolves configuration details and computes the normalized relative percentages using `QuestionSourcePreferences.effectiveMix()`.
3. **Dispatcher Selection**: When dispatching or queueing questions, the system calls `QuestionSourceMix.pickNextSourceId()` to perform a weighted random roll matching the configured percentages.
4. **Zero-Ratio Suppression**: If a source is enabled but configured with a ratio of `0`, it is suppressed and will not yield questions.
---
## 4. Question Lifecycle & Serving Flow
### 1. Queue Maintenance & Seeding
- The server maintains a small cache of unanswered questions for each active user (typically limited to `maxQueuedQuestions = 3`).
- The periodic cron or a login event invokes `QuestionDispatcher.topUpQueue()` in [question_dispatcher.dart](file:///home/chh/cyberhybridhub.com/server/lib/question_dispatcher.dart).
- If the current count of unanswered questions is below the threshold, the dispatcher rolls for a preferred source and attempts to produce one question using `_produceFromSource()`. If it fails or is capped, it rolls over to other enabled sources sorted by descending ratio weight.
### 2. Retrieval & Dispatch
- The client initiates a request via `GET /v1/me/questions`.
- The [questions_handler.dart](file:///home/chh/cyberhybridhub.com/server/lib/handlers/questions_handler.dart) delegates to `QuestionDispatcher.listPanelQuestions()`.
- The dispatcher retrieves unanswered questions and orders them so that the currently rolled source preference resides at the head of the list (using `QuestionSourceMix.orderUnansweredByPreferredSource()`).
### 3. Client Routing & Presentation
- On the Flutter client, the [question_presentation_router.dart](file:///home/chh/cyberhybridhub.com/lib/widgets/question_presentation_router.dart) examines incoming properties:
- If `IncomingQuestion.usesSpellingWordTile` is true, it renders a [spelling_question_tile.dart](file:///home/chh/cyberhybridhub.com/lib/widgets/spelling_question_tile.dart).
- Otherwise, it defaults to rendering a [swipe_question_tile.dart](file:///home/chh/cyberhybridhub.com/lib/widgets/swipe_question_tile.dart) (binary choice swipe interface).
### 4. Deferral & Skip Actions
- When a user chooses to skip or defer a question (e.g., swiping left or clicking skip):
- A `POST /v1/me/questions/<id>/defer` request is sent.
- The database updates the prioritization parameters (moving the question to the back of the queue by editing `view_order_at` or `created_at`).
- **Special Spelling Behavior**: In [question_dispatcher.dart](file:///home/chh/cyberhybridhub.com/server/lib/question_dispatcher.dart#L154-L163), if spelling was the rolled preference but the user just deferred the only spelling question in their queue, the system forcefully enqueues a *new* spelling question immediately so the user is not served the exact same word again.
### 5. Deferred Preparation (Tile Load)
- For performance and synchronization, some questions defer part of their configuration until they are actively displayed.
- For example, spelling questions are created with a placeholder title and `needsPrepare = true`.
- When [spelling_question_tile.dart](file:///home/chh/cyberhybridhub.com/lib/widgets/spelling_question_tile.dart) is mounted, it sends a `POST /v1/me/questions/<id>/spelling/prepare` request.
- The pipeline selects a word, saves it in the database and pipeline context, links spelling progress trackers, and replies with the updated client payload (containing the spoken word and audio properties).
### 6. Submission, Grading, & Progression
- When a user submits an answer via `POST /v1/me/questions/<id>/answer`:
- [questions_db.dart](file:///home/chh/cyberhybridhub.com/server/lib/questions_db.dart) logs and writes the answer to PostgreSQL.
- The dispatcher runs grading logic:
- **Spelling**: String comparison matching `userResponseText` against the `canonicalWord` metadata field via [spelling_answer_policy.dart](file:///home/chh/cyberhybridhub.com/server/lib/spelling/spelling_answer_policy.dart).
- **Market History**: Numeric evaluation using `gradeProspectiveAnswer()`.
- The pipeline runner (`QuestionPipeline.onAnswerSubmitted()`) processes the answer. Based on the matching/scoring outcome, it transitions the `user_pipeline_state` and optionally enqueues a branched follow-up question.
- Finally, the server schedules an asynchronous `topUpQueue()` execution to replenish the unanswered queue.
---
## 5. Guide: Adding a New Question Type
To introduce a new type of question (e.g., a "Math Trivia" question), follow this systematic implementation checklist:
### Step 1: Define Database Fields & Custom Tables (Server)
Determine if your question type requires dedicated tracking tables (like spelling word catalogs or trade order details).
- Create a SQL migration file in `server/migrations/` to define any new tables or constraints.
- Ensure any additional metadata needed to present or grade the question can be stored inside the JSONB `metadata` column of the `questions` table.
### Step 2: Register the Question Source
1. Open [question_source_preferences.dart](file:///home/chh/cyberhybridhub.com/server/lib/question_source_preferences.dart).
2. Append a new `QuestionSourcePreferenceDefinition` to the static `definitions` list:
```dart
QuestionSourcePreferenceDefinition(
id: 'math_trivia',
label: 'Math Puzzle',
description: 'Solve arithmetic equations and logic puzzles',
defaultRatio: 30,
)
```
### Step 3: Implement Database Resolution (Server)
Modify the static resolution helpers in [questions_db.dart](file:///home/chh/cyberhybridhub.com/server/lib/questions_db.dart):
1. Locate `resolveSourceId(Map<String, dynamic> question)`. Add a case to return your new source identifier:
```dart
if (question['pipelineKey'] == 'math_trivia' || (sourceTag != null && sourceTag.startsWith('math:'))) {
return 'math_trivia';
}
```
2. Locate `resolvePresentation(Map<String, dynamic> question)`. Return the correct frontend layout identifier:
```dart
if (resolveSourceId(question) == 'math_trivia') {
return 'math_quiz_keyboard'; // Target layout format
}
```
### Step 4: Add Queue Maintenance Production (Server)
Integrate your question generator into the top-up flow:
1. Open [question_dispatcher.dart](file:///home/chh/cyberhybridhub.com/server/lib/question_dispatcher.dart).
2. Inside `_produceFromSource()`, add a case for your source ID that invokes your generator:
```dart
case 'math_trivia':
await _questionService.tryEnqueueMathQuestions(firebaseUid);
```
### Step 5: Build the Generation & Pipeline Engine (Server)
Create a service class (or extend `QuestionPipeline`) to handle generation and state progression:
1. **Generation**: Implement a method that creates a database row using `questionsDb.createQuestion()`. Specify your custom `sourceTag`, `pipelineKey`, `pipelineStep`, and grade variables. Ensure it respects batch parameters to avoid flooding the user's queue.
2. **State Progression**: In `QuestionPipeline.onAnswerSubmitted()`, add a switch case for your new `pipelineKey`. Read the answer, evaluate correctness, update any progress records, and transition the user's pipeline state as required.
### Step 6: Parse the Client Model (Frontend)
1. Open [incoming_question.dart](file:///home/chh/cyberhybridhub.com/lib/models/incoming_question.dart).
2. Add a getter to identify the new question type:
```dart
bool get isMathTrivia => sourceId == 'math_trivia';
```
3. Update key mapping properties like `usesSpellingWordTile` or custom tile configuration parameters if you need to bypass default layouts.
### Step 7: Create the UI Component & Router (Frontend)
1. If your question type requires a custom layout (e.g. keyboard input, multi-choice grids), create a new widget in `lib/widgets/` (e.g., `math_question_tile.dart`).
2. Open [question_presentation_router.dart](file:///home/chh/cyberhybridhub.com/lib/widgets/question_presentation_router.dart).
3. Update the `build` method to intercept your presentation format and route to the new widget:
```dart
if (question.presentation == 'math_quiz_keyboard') {
return MathQuestionTile(
key: ValueKey<String>(question.id),
question: question,
onSubmit: onAnswerText,
onDefer: onDefer,
);
}
```