MCQ Analysis Field — PRD
Context
The MCQ module already ships — MCQs carry a question, options, a solution, taxonomy, docket
links, and a course_id (CourseEnum). MCQs are authored in the admin app (keystone-web) and read
by the mobile/web client via the v1/v2 MCQ APIs.
This PRD adds one standalone field, mcq_analysis, to hold free-form “details / analysis about the
MCQ” as a PlateJS rich-text value. It is an internal, admin-only annotation — authored and viewed
only in the admin app, never sent to the client. It is self-contained: it does not change or depend on
options, solution, taxonomy, dockets, exams, encryption, or the client (v1/v2) MCQ APIs.
Not related to
solution.solutionappears in this doc only as the existing implementation reference for how a PlateJS field is already wired through the MCQ stack (the editor component, the serialize/parse helpers, and the form + preview placement to copy the pattern from).mcq_analysisis a distinct, unrelated field — it shares none of solution’s meaning, storage model, encryption, or client behaviour.
Audience & sensitivity (decided): admin-only, plaintext.
mcq_analysisis internal — authored and viewed only in the admin app (create/edit form + admin preview) and never returned on the v1/v2 client MCQ APIs. Because it never leaves the JWT-gated admin surface, it is stored plaintext with no at-rest encryption. This is the single decision that collapses the scope.
Storage shape (decided): raw string.
mcq_analysisis a singleOptional[str]holding the PlateJS JSON serialized to a string, stored as-is — no{type, text_value}wrapper and no secondary representation. (Same plaintext-string storage thecurrent_affairs.bodyfield already uses.) The FE sends/receives a bare string.
Editor (decided): plain rich text. The admin field reuses the existing
MCQPlateEditorFieldcomponent with default features (bold/italic/lists/links/etc.): no emoji restriction, no AI toolbar, and no flowchart/table plugins.
Scope
MVP — one optional admin-only rich-text field on the MCQ:
mcq_analysisfield on MCQ — anOptional[str]PlateJS JSON string, plaintext, populated by admins,nullby default.- av1 write path —
mcq_analysisaccepted on MCQ create (POST /av1/mcqs) and update (PATCH /av1/mcqs/{id}), threaded service → repository, and returned on the av1 MCQ detail response for the edit round-trip. - Admin form — a plain
MCQPlateEditorFieldformcq_analysis, placed at the bottom of both the create and edit MCQ forms. - Admin preview — an “Analysis” section at the bottom of the preview, rendered read-only, shown only
when
mcq_analysisis non-empty.
Explicit non-goals
- No client (v1/v2) exposure.
mcq_analysisis not added to any v1 or v2 client MCQ response, not indexed intoquestion_preview, and does not participate in search. - No encryption. No ciphertext column; no changes to
field_encryption_utils,_sync_mcq_at_rest_encrypted_columns, or the at-rest backfill migration. - No secondary representation.
mcq_analysisis a single stored string — no conversion, no model validator, no derived/companion field. - No AI generation for analysis.
Data Model (requirements-level)
McqDBModel gains one field; everything else is unchanged.
| Field | Type | Notes |
|---|---|---|
mcq_analysis | Optional[str] | Default null. PlateJS JSON array serialized to a string, stored plaintext, as-is (same format MCQPlateEditorField emits and PlateContentDisplay renders). Optional on create and update. |
Mirrored on McqProjection. No index, no encryption column, no derived field.
Validation
Deliberately minimal — the value is a plaintext string produced by the admin editor and consumed only by the admin preview.
| Rule | When |
|---|---|
| No server-side PlateJS-schema validation | plaintext string trusted from the admin RTE, as with other admin-authored PlateJS content |
mcq_analysis optional | create + update |
Empty editor normalized to null | FE-side (see Update semantics) |
Update / clear semantics (decided). Field omitted → unchanged. Sent with content → replaced. Empty editor → the FE sends
null(via ahasMeaningfulContent-style check reusing the existing PlateJS helpers), so the backend storesnulland the preview hides the Analysis section. The service appliesmcq_analysisto the update only when the key is present in the request (the same presence-detection the existing update path already uses for its fields).
Admin surface (av1 + keystone-web)
Backend (av1 only).
mcq_analysis: Optional[str]added toMCQCreateRequestandMCQUpdateRequest.- av1
MCQDetailsResponsereturnsmcq_analysis(straight from the projection) so the edit form can round-trip it. - Create/update routes pass
mcq_analysisintoMCQService.create/update_by_id, which thread it intoMcqRepository.create/update_by_idas a plain string (no conversion, no encryption sync).
Frontend (keystone-web).
api/mcqs.ts:mcq_analysis?: stringon bothMCQPayload(create) andMCQResponse(read) — same key both directions (no_adminalias; it is admin-only).- Create (
CreateMcqNew.tsx) and Edit (EditMcqLatest.tsx): a new plainMCQPlateEditorFieldat the bottom of the form. Submit serializes with the existingJSON.stringify(trimEmptyBlocksFromPlateValue(...))pattern; edit hydrates viaJSON.parseofmcqData.mcq_analysis(reusing the existing generic PlateJS serialize/parse helpers inmcq-solution.ts). - Preview (
PreviewMcq.tsx): a new “Analysis” section at the bottom of the preview, rendered viaMcqContent→PlateContentDisplay(read-only), shown only whenmcq_analysisis non-empty.
Placement (decided). At the bottom of the create/edit form and at the bottom of the preview, per the requirement. The exact insertion point — just after the Solution field / before the docket section, vs. truly last after the dockets — is an Open Question; proposed: just after Solution.
Labels (decided). Form label “Analysis”, placeholder “Add analysis / details about this question…”; preview heading “Analysis”.
Client surface
None. mcq_analysis is not exposed on any v1 or v2 client MCQ response. No client read or write path.
Migration & Rollout
No data backfill — mcq_analysis is a new optional field; existing MCQ documents read as null. No index
added. No encryption/backfill changes. Ships as a normal additive field (backward-compatible: old admin
builds simply omit it).
Delivery Milestones
| # | Milestone | Outcome | Status | Plan |
|---|---|---|---|---|
| 1 | Backend field + av1 write/read path | McqDBModel/McqProjection store mcq_analysis; av1 create/update accept it and the av1 detail response returns it; service → repository thread it as a plaintext string. No v1/v2, encryption, or migration changes. | pending | BE plan ¹ |
| 2 | Admin form + preview | Plain MCQPlateEditorField for mcq_analysis at the bottom of the create & edit forms (serialize/parse via existing PlateJS helpers; empty → null); “Analysis” section at the bottom of the preview (read-only, hidden when empty); mcq_analysis?: string wired through api/mcqs.ts. | pending | FE plan ² |
Plan key:
BE= backend (keystone);FE= frontend (keystone-web). ¹ Author from this PRD via/planintokeystone/docs/superpowers/specs/+…/plans/. ² Likewise intokeystone-web/docs/superpowers/. Milestones are independent (FE only needs the av1 contract from M1); ship one PR per repo.
Open Questions
- Placement (exact insertion point): just after the Solution field (proposed) vs dead-last after the docket section. Both satisfy “at the bottom.”
- Length cap: none proposed (existing PlateJS content is uncapped too). Add a soft cap later only if abuse appears.
- Everything else resolved during PRD review: standalone field, unrelated to
solution; admin-only plaintext (no client exposure, no encryption); raw-string storage (single field, no secondary representation); plain editor (no emoji restriction / AI / flowchart / table); optional on create+edit; empty →nullclear semantics; labels “Analysis”.
Risks
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
Accidentally wiring mcq_analysis into an encryption path. Because the FE/BE reuse existing PlateJS-field plumbing, a copy could drag in at-rest encryption (build_mcq_sole_column / _sync_mcq_at_rest_encrypted_columns). | Low | Med — needless ciphertext column / decrypt on read | Explicitly plaintext: mcq_analysis is a bare str, never passed to any *_at_rest helper. Call this out in the BE PR. |
Leaks to the client. mcq_analysis added to a v1/v2 response schema by mistake. | Low | Med — internal notes exposed to end users | Non-goal stated; add to av1 schemas only. No v1/v2 schema edits in the diff. |
Malformed / empty PlateJS stored. Empty editor persists "[]" or a blank block instead of clearing. | Med | Low — stray empty “Analysis” section in preview | FE normalizes empty → null (reuse hasMeaningfulSolutionContent / trimEmptyBlocksFromPlateValue); preview hides the section when empty. |
| Copied required-validation. Analysis accidentally inherits a “required on create” rule from a copied field. | Low | Low — false validation error | mcq_analysis is Optional on both create and edit; no required check. |
Technical Details (planned)
Not yet built. Author BE + FE specs/plans from this PRD via /plan. The touch-lists below are the
recon-verified starting points (line numbers approximate to current HEAD).
Backend (keystone) — files to touch
| Layer | File | Where | Change |
|---|---|---|---|
| DB model | src/models/mcq_models/db_model.py | with the other MCQ content fields (~L247–262) | add mcq_analysis: Optional[str] = None |
| Projection | src/models/mcq_models/projection_model.py | mirror (~L147–160) | add mcq_analysis: Optional[str] = None |
| av1 request | src/api/av1/mcq/schemas.py MCQCreateRequest (~L110–433) | add mcq_analysis: Optional[str] = None | |
| av1 request | src/api/av1/mcq/schemas.py MCQUpdateRequest (~L436–642) | add mcq_analysis: Optional[str] = None | |
| av1 response | src/api/av1/mcq/schemas.py MCQDetailsResponse (~L715–917, populate ~L906) | add mcq_analysis, mcq_analysis=projection.mcq_analysis | |
| av1 routes | src/api/av1/mcq/routes.py create (~L99), update (~L598) | pass mcq_analysis=request_model.mcq_analysis into the service | |
| Service | src/services/mcq_services.py create (~L719–917), update_by_id (~L1009–1258) | add mcq_analysis param; set into update_data when present (presence-gated) | |
| Repository | src/repository/mcq_repository.py create (~L185–366), update_by_id (~L810–909) | add mcq_analysis param → McqDBModel; include in update_data. Do NOT add to _sync_mcq_at_rest_encrypted_columns | |
| Untouched | field_encryption_utils.py, v1/mcq/schemas.py, v2/mcq/schemas.py, backfill migration | plaintext, admin-only — no changes |
Frontend (keystone-web) — files to touch
| Layer | File | Where | Change |
|---|---|---|---|
| API types | src/api/mcqs.ts | MCQPayload (~L53), MCQResponse (~L122) | add mcq_analysis?: string to both |
| Create form | src/pages/mcq/CreateMcqNew.tsx | state (~L109), new section at the form bottom (proposed: after Solution, ~after L1224), submit (~L496–499), reset (~L575) | analysisContent state; plain MCQPlateEditorField at the form bottom; mcq_analysis: hasMeaningful ? JSON.stringify(trimEmptyBlocksFromPlateValue(analysisContent)) : null |
| Edit form | src/pages/mcq/EditMcqLatest.tsx | state (~L154), load in applyMcqDataToForm (~L299–308), new section at the form bottom (proposed: after Solution, ~after L1628), submit (~L1065–1068) | hydrate via JSON.parse(mcqData.mcq_analysis); same editor + submit as create |
| Preview | src/pages/mcq/PreviewMcq.tsx | new section at the preview bottom (proposed: after the Solution section, ~after L219) | “Analysis” <section> via McqContent content={mcq.mcq_analysis}, rendered only when non-empty |
| Helpers (reuse) | src/utils/mcq-solution.ts | trimEmptyBlocksFromPlateValue, hasMeaningfulSolutionContent | reuse as-is (already generic over a PlateJS value) |
Naming: PRD vs code (planned)
| PRD term | Code (planned) |
|---|---|
mcq_analysis field | McqDBModel.mcq_analysis: Optional[str] (db_model.py); mirrored on McqProjection; collection mcqs |
| admin create/update carries it | MCQCreateRequest / MCQUpdateRequest mcq_analysis (av1/mcq/schemas.py) → routes.py create/update → MCQService.create / update_by_id → McqRepository.create / update_by_id |
| admin read (edit round-trip) | mcq_analysis on av1 MCQDetailsResponse (av1/mcq/schemas.py) |
| admin editor | reused MCQPlateEditorField (src/components/mcq/components/MCQPlateEditorField.tsx), default props |
| admin preview render | McqContent → PlateContentDisplay (src/components/editor/PlateContentDisplay.tsx) |
| FE payload/response | mcq_analysis?: string on MCQPayload / MCQResponse (src/api/mcqs.ts) |