Flowchart PRD
Data Model
| Field | Type | Notes |
|---|---|---|
id | string | System generated. |
short_uid | string | System generated. |
title | string | Required. |
hide_title | boolean | If true, the title is not shown as a caption when rendered. |
description | string | Optional. |
thumbnail | object | Required. The flowchart image. |
mermaid_code | string | Optional. The Mermaid source related to the thumbnail. |
license | enum | Required. Same list of licenses used for images. |
needs_review | boolean | ”Mark for review” flag. |
confidence_score | number | 0–100. |
source_url | string (URL) | Optional. |
linked_entities | array | List of { content_id, content_type }. Where this flowchart is attached. |
used_in_l1_ids / used_in_l2_ids / used_in_l3_ids | array | Denormalized taxonomy usage, derived from each linked entity’s chain (linked_entities → Block/MCQ → Docket → L3 → L2 → L1). Powers the admin-listing filter by L1/L2/L3. |
status | enum | published or archived. |
Step 1 — Create a Flowchart
An editor creates a flowchart by filling out a form.
Required fields:
- Title
- Thumbnail
- License (pick from the same license list as images)
Optional fields:
- Description
- Mermaid code (related to the thumbnail)
- Mark for review (yes/no)
- Confidence score (0–100)
- Source URL
- Hide title (yes/no) — controls whether the title shows as a caption later
Default behavior: When a flowchart is created, its status is automatically set to published.
Step 2 — Attach a Flowchart
In the Admin Panel, an editor can attach a flowchart inside the editor, either in an MCQ Solution or in Block Content.
When a flowchart is attached:
- The flowchart’s
linked_entitiesis updated with the content it was attached to, based on the content’s type (MCQ or Block) and itscontent_id(_id).
For a Block, the data stored is:
content— In the PlateJS editor, attaching a flowchart inserts a node:{ id: xyz, type: flowchart }.flowchart_ids— array of attached flowchart IDs.flowchart_objects— array of the full flowchart objects.
Step 3 — Admin Listing & Rendering
Make the admin listing the same as the Image Bank.
Filter by taxonomy (L1 / L2 / L3). The listing can be filtered by L1 / L2 / L3, matching the Table Repository. The filter reads the denormalized used_in_l1_ids / used_in_l2_ids / used_in_l3_ids on the flowchart directly — no join through linked entities. A flowchart attached under multiple taxonomies appears under each: it matches when used under any of the selected L1/L2/L3 values.
Step 4 — Rendering (Client & Admin Preview)
When a flowchart needs to be shown:
- Fetch the flowchart by its
id. - Render the flowchart inline.
- Note: “Render inline” needs more detailing. For now, render it the same way an image is rendered.
- If
hide_titleis nottrue, show the title as a caption below it.
Step 5 — Archive & Edit (Cascading Updates)
Changes to a flowchart or to a linked item must keep linkage in sync in both directions.
- If a linked entity (Block or MCQ) is archived: remove that linkage from the flowchart.
- Constraint — archiving a flowchart: archive is blocked while the flowchart has any linked entity (
linked_entitiesnon-empty). Ask the editor to manually remove all links first, and show them everywhere it is currently linked. (Matches the shipped code:FlowchartService.archiveraisesFLOWCHART_ARCHIVE_BLOCKEDfor ≥1 link; 0 links → archive.) - Keep taxonomy usage in sync: whenever
linked_entitieschanges — on attach (Step 2), on manual detach, and on the archive-cascade detach above — re-deriveused_in_l1_ids/used_in_l2_ids/used_in_l3_idsthrough the same single write path that updateslinked_entities. Block→Docket moves and Docket re-parenting do not happen (disallowed by the platform), so a linked entity’s taxonomy chain is immutable and there is no other recompute trigger.
Addendum — Taxonomy Filter (delta on executed PRD)
Added 2026-06-03. The flowchart entity shipped before this filter, so this section is a delta on the already-executed PRD.
- Backfill. Existing flowcharts need
used_in_l1_ids/used_in_l2_ids/used_in_l3_idspopulated once from their currentlinked_entities(one-time migration), then maintained going forward by the Step 5 sync. - Archive rule — aligned (resolved 2026-06-08). Flowchart, Table, and the Image Bank all block archive while any linkage exists (
linked_entitiesnon-empty); the editor must unlink all references first. This matches the shipped code (FlowchartService.archiveblocks on ≥1), and supersedes the earlier draft text in Step 5 that allowed archiving a flowchart linked to ≤ 1 entity.
Technical Details (as built)
Implemented in PR #747 (feature/flowchart) and PR #750 (bug/flowchart-search). Design spec: keystone/docs/superpowers/specs/2026-06-02-flowchart-design.md; backend plan: keystone/docs/superpowers/plans/2026-06-02-flowchart-backend.md. Backend only — the PlateJS flowchart editor node, admin screens, and inline rendering live in keystone-web / parixa (this repo exposes the API contract).
Naming: PRD vs code
| PRD term | Code |
|---|---|
| Flowchart entity | FlowchartDBModel (collection flowchart) / FlowchartProjection |
linked_entities[].content_id | FlowchartLinkedEntity.content_id — the linked Block/MCQ _id |
linked_entities[].content_type | FlowchartLinkedContentTypeEnum (MCQ=1, BLOCK=2) |
status (published/archived) | FlowchartStatusEnum (PUBLISHED=1, ARCHIVED=2), default PUBLISHED |
thumbnail | thumbnail: ImageVariant + variants: Dict[str, ImageVariant] (Image-Bank S3 pipeline) |
| Block/MCQ “flowchart IDs” | flowchart_ids: List[str] storing flowchart short_uids |
Deviations from the PRD (as shipped):
flowchart_objectswas not implemented. PRD Step 2 listsflowchart_objectson the Block; the content side stores IDs only (flowchart_ids) and the client fetches each flowchart by id. Because the reference is a stableshort_uid(not an embedded snapshot), editing a flowchart’s metadata never goes stale — so there is no content-side re-sync on edit; the only flowchart-side linkage edit is archive.- ID asymmetry (intentional).
flowchart_idsstores flowchartshort_uids (matches the image-thumbnail precedent), whereaslinked_entities[].content_idstores the Block/MCQ Mongo_id. confidence_scoreis anintvalidated to0–100(PRD said “number”).- Images always private. Uploads run through the Image Bank S3 pipeline with
is_public=Falseandcreate_variants=True(medium/large; SVG skips variants); there is nois_publicmodel field.
Entity (src/models/flowchart_models/)
FlowchartDBModel(BaseBeanieDocumentModel), collection flowchart, short_uid prefix FLW. Own fields: course_id (CourseEnum), title, hide_title (False), description, thumbnail, variants, mermaid_code (stored only, never server-rendered), license (LicenseType, same list as images), needs_review, confidence_score, source_url, linked_entities, used_in_l1_ids/l2_ids/l3_ids (denormalized taxonomy), status — plus inherited audit fields (created_at/updated_at/is_deleted/created_by/updated_by).
8 indexes (flw_ prefix): unique short_uid; course_id; (course_id, is_deleted, updated_at DESC, _id DESC) (bidirectional listing); (course_id, status); linked_entities.content_id (archive-cascade reverse lookup); (course_id, used_in_l{1,2,3}_ids) (L1/L2/L3 filter).
Cross-entity linkage (Block & MCQ)
- Block and MCQ each carry top-level
flowchart_ids: List[str](flowchartshort_uids,default_factory=list). The PlateJS{ id, type: "flowchart" }node lives inside free-form content (Block.content/ MCQsolution_client) and is never parsed server-side — the client sendsflowchart_idsexplicitly through the av1 Block/MCQ request schemas. - Single synchronous write path.
FlowchartService.sync_linked_entities_for_content(course_id, content_id, content_type, old_flowchart_ids, new_flowchart_ids)— invoked by Block/MCQ create+update via the unified service container (services don’t import each other) — diffs added/removedshort_uids and appends/pulls{content_id, content_type}on each flowchart’slinked_entities. No Celery. - In the same
update_by_id,used_in_l*_idsis re-derived: MCQ →mcq.taxonomy_ids; Block →block.docket_id→docket.taxonomy_ids([L1, L2, L3], deduped). Re-derivation triggers only whenlinked_entitieschanges — taxonomy chains are immutable (no Block→Docket move, no Docket re-parent), so there is no other recompute path.
Archive & cascade
- Archive rule resolved → block whenever ≥1 link exists. (Resolves the open question in the addendum above: flowchart was aligned to the stricter Table rule.)
FlowchartService.archiveraisesFLOWCHART_ARCHIVE_BLOCKEDwhilelinked_entitiesis non-empty; only 0 links archive (defensively clearingused_in_*). No auto-unlink — the editor detaches all references first via the normal Block/MCQ save. - Reverse cascade. When a linked Block/MCQ is archived or deleted,
unlink_content_from_flowchartspulls thatcontent_idfrom every flowchart and recomputesused_in_*. MCQ archive additionally zeroes the MCQ’s ownflowchart_ids(andlinked_table_ids) in the same status write. - Known gap. The archive-blocked error currently reports only the link count, not the per-entity list that PRD Step 5 asks to “show … everywhere it is currently linked” (the list is computed in the service but dropped from the message). keystone-web’s “where used” panel covers this on the UI side.
Surface & semantics
| Surface | Routes | Notes |
|---|---|---|
| av1 (admin, write) | POST /upload (multipart), GET /, GET /list-updated-at-bidirectional, GET /filters, GET /{id}, PATCH /{id}, POST /{id}/archive | JWT + editor/admin role; admin GET can preview archived. |
| v1 (mobile, read) | GET /v1/flowchart/{id} | published_only=True; wrapped in v1 client-payload encryption. |
| v2 (web, read) | GET /v2/flowchart/{id} | published_only=True; plain response. |
- Archived flowcharts are hidden from clients via
published_only=Trueon the client get path; admin omits it. - v1/v2 schema modules re-export av1’s
FlowchartDetailsResponse(one shared shape; URLs resolved withis_public=False). - Listing/search is MongoDB-filter based, no Typesense: bidirectional keyset cursor on
(updated_at, _id)newest-first (FlowchartTimestampCursor); filter paramsshort_uid(exact),title,license,source_url,needs_review,status,used_in_l{1,2,3}_ids,updated_at__gte/__lte. - New error codes:
FLOWCHART_NOT_FOUND=9300,FLOWCHART_OPERATION_FAILED=9303,FLOWCHART_ARCHIVE_BLOCKED=9304.
Migration
One-time, idempotent backfill src/core/migrations/jun_03_26/backfill_flowchart_taxonomy_usage.py populates used_in_l*_ids on existing flowcharts (reuses _derive_taxonomy_usage; supports --dry-run). Block/MCQ flowchart_ids need no backfill (default []).