Owner Burhan Kapdawala

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. solution appears 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_analysis is a distinct, unrelated field — it shares none of solution’s meaning, storage model, encryption, or client behaviour.

Audience & sensitivity (decided): admin-only, plaintext. mcq_analysis is 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_analysis is a single Optional[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 the current_affairs.body field already uses.) The FE sends/receives a bare string.

Editor (decided): plain rich text. The admin field reuses the existing MCQPlateEditorField component 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:

  1. mcq_analysis field on MCQ — an Optional[str] PlateJS JSON string, plaintext, populated by admins, null by default.
  2. av1 write pathmcq_analysis accepted 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.
  3. Admin form — a plain MCQPlateEditorField for mcq_analysis, placed at the bottom of both the create and edit MCQ forms.
  4. Admin preview — an “Analysis” section at the bottom of the preview, rendered read-only, shown only when mcq_analysis is non-empty.

Explicit non-goals

  • No client (v1/v2) exposure. mcq_analysis is not added to any v1 or v2 client MCQ response, not indexed into question_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_analysis is 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.

FieldTypeNotes
mcq_analysisOptional[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.

RuleWhen
No server-side PlateJS-schema validationplaintext string trusted from the admin RTE, as with other admin-authored PlateJS content
mcq_analysis optionalcreate + update
Empty editor normalized to nullFE-side (see Update semantics)

Update / clear semantics (decided). Field omitted → unchanged. Sent with content → replaced. Empty editor → the FE sends null (via a hasMeaningfulContent-style check reusing the existing PlateJS helpers), so the backend stores null and the preview hides the Analysis section. The service applies mcq_analysis to 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 to MCQCreateRequest and MCQUpdateRequest.
  • av1 MCQDetailsResponse returns mcq_analysis (straight from the projection) so the edit form can round-trip it.
  • Create/update routes pass mcq_analysis into MCQService.create / update_by_id, which thread it into McqRepository.create / update_by_id as a plain string (no conversion, no encryption sync).

Frontend (keystone-web).

  • api/mcqs.ts: mcq_analysis?: string on both MCQPayload (create) and MCQResponse (read) — same key both directions (no _admin alias; it is admin-only).
  • Create (CreateMcqNew.tsx) and Edit (EditMcqLatest.tsx): a new plain MCQPlateEditorField at the bottom of the form. Submit serializes with the existing JSON.stringify(trimEmptyBlocksFromPlateValue(...)) pattern; edit hydrates via JSON.parse of mcqData.mcq_analysis (reusing the existing generic PlateJS serialize/parse helpers in mcq-solution.ts).
  • Preview (PreviewMcq.tsx): a new “Analysis” section at the bottom of the preview, rendered via McqContentPlateContentDisplay (read-only), shown only when mcq_analysis is 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

#MilestoneOutcomeStatusPlan
1Backend field + av1 write/read pathMcqDBModel/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.pendingBE plan ¹
2Admin form + previewPlain 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.pendingFE plan ²

Plan key: BE = backend (keystone); FE = frontend (keystone-web). ¹ Author from this PRD via /plan into keystone/docs/superpowers/specs/ + …/plans/. ² Likewise into keystone-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 → null clear semantics; labels “Analysis”.

Risks

RiskLikelihoodImpactMitigation
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).LowMed — needless ciphertext column / decrypt on readExplicitly 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.LowMed — internal notes exposed to end usersNon-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.MedLow — stray empty “Analysis” section in previewFE 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.LowLow — false validation errormcq_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

LayerFileWhereChange
DB modelsrc/models/mcq_models/db_model.pywith the other MCQ content fields (~L247–262)add mcq_analysis: Optional[str] = None
Projectionsrc/models/mcq_models/projection_model.pymirror (~L147–160)add mcq_analysis: Optional[str] = None
av1 requestsrc/api/av1/mcq/schemas.py MCQCreateRequest (~L110–433)add mcq_analysis: Optional[str] = None
av1 requestsrc/api/av1/mcq/schemas.py MCQUpdateRequest (~L436–642)add mcq_analysis: Optional[str] = None
av1 responsesrc/api/av1/mcq/schemas.py MCQDetailsResponse (~L715–917, populate ~L906)add mcq_analysis, mcq_analysis=projection.mcq_analysis
av1 routessrc/api/av1/mcq/routes.py create (~L99), update (~L598)pass mcq_analysis=request_model.mcq_analysis into the service
Servicesrc/services/mcq_services.py create (~L719–917), update_by_id (~L1009–1258)add mcq_analysis param; set into update_data when present (presence-gated)
Repositorysrc/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
Untouchedfield_encryption_utils.py, v1/mcq/schemas.py, v2/mcq/schemas.py, backfill migrationplaintext, admin-only — no changes

Frontend (keystone-web) — files to touch

LayerFileWhereChange
API typessrc/api/mcqs.tsMCQPayload (~L53), MCQResponse (~L122)add mcq_analysis?: string to both
Create formsrc/pages/mcq/CreateMcqNew.tsxstate (~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 formsrc/pages/mcq/EditMcqLatest.tsxstate (~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
Previewsrc/pages/mcq/PreviewMcq.tsxnew 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.tstrimEmptyBlocksFromPlateValue, hasMeaningfulSolutionContentreuse as-is (already generic over a PlateJS value)

Naming: PRD vs code (planned)

PRD termCode (planned)
mcq_analysis fieldMcqDBModel.mcq_analysis: Optional[str] (db_model.py); mirrored on McqProjection; collection mcqs
admin create/update carries itMCQCreateRequest / MCQUpdateRequest mcq_analysis (av1/mcq/schemas.py) → routes.py create/update → MCQService.create / update_by_idMcqRepository.create / update_by_id
admin read (edit round-trip)mcq_analysis on av1 MCQDetailsResponse (av1/mcq/schemas.py)
admin editorreused MCQPlateEditorField (src/components/mcq/components/MCQPlateEditorField.tsx), default props
admin preview renderMcqContentPlateContentDisplay (src/components/editor/PlateContentDisplay.tsx)
FE payload/responsemcq_analysis?: string on MCQPayload / MCQResponse (src/api/mcqs.ts)