TRD: Comments System
Owner: Abhi
Reviewed / improved by: PC
What this file is - A frozen technical review from 23 Apr 2026. It is not updated every time code changes. If the app and this file disagree, trust OpenAPI (Swagger for /av1/comments), test-document, src/constants/comment_permissions.py, and implementation-delta.
Strikethrough text means “we decided not to do this.” It is left on purpose so you can see what was ruled out.
Where to look for today’s behavior: OpenAPI + src/api/av1/comment/ (routes and schemas), implementation-delta (on-purpose differences), test-document (test cases), release-checklist (pre-ship gates).
Review trail (short) - Database section signed off. API and admin UI were aligned to that schema in the same review. author_type is stored on each comment so we do not have to infer human vs AI on every read.
Plain summary (before the detailed sections)
- One Mongo collection
comments: one row per comment, scoped to course, content type, and content id (phase 1: docket). - One collection
comment_auditfor action history. - No composite “custom id” per docket; one user may post many comments, so each row gets a normal Mongo id.
- No
docket_idcolumn; we usecontent_typepluscontent_idinstead. - Threading is not in phase 1; every comment uses the same root parent sentinel until a later phase.
The long field list below is the reviewed record, including struck ideas.
Database
Current Structure- New Comment Structure
- In Settings (static)
comment_permission_matric- object
{"can comment": [roles]}etc
- object
- Collection:
; live name isdocket_commentscomments- One document per comment
- Fields
_id- Mongo-generated unique id
custom_id = course_id + content_type + content_id- The above id is incorrect since comment is user specific and 1 user can leave multiple comments so reverting back to
mongo_id - This only makes sense if the comments were nested
- The above id is incorrect since comment is user specific and 1 user can leave multiple comments so reverting back to
content_idcontent_type=CommentContentTypeEnum(int enum; Phase 1:DOCKET = 1; extend with new values as comments expand to other targets)docket_id- uuid of Docket
- The docket this comment belongs to
- Required
course_idCourseEnum(matches existing pattern; every entity is course-scoped)- Required
body_type=1[text - default],2[plate js when required]body(plate text vs static text?)- Plain text content of the comment
- Max length: 2000 characters
- Required
author_type- Enum:
CommentAuthorTypeEnumHUMAN = "human"AI = "ai"
- Distinguishes human-authored from AI-generated comments
- Required, set at creation, immutable
- Stored on the row (not derived at read time) so list/detail responses don’t need a per-row user lookup
- Enum:
author_role??- why does this matter
- Enum:
UserRoleEnum(existing:SYS_ADMIN,ADMIN,EDITOR) or literal"agent"for AI - Captures the role of the author at the time the comment was written
- Frozen at creation; reflects the role when the comment was made, not the author’s current role
statusopenresolved- Enum:
DocketCommentStatusEnumOPEN = 1RESOLVED = 2
- Default:
OPEN - Note: No
DELETEDstate. Soft delete is handled by the inheritedis_deletedflag alone
resolved_at- Unix timestamp (epoch ms), nullable
- Set when status transitions to
RESOLVED
resolved_byPydanticObjectId, nullable- User id of whoever resolved the comment
created_at(default)- default all tables
- Unix timestamp of creation time (inherited from
BaseBeanieDocumentModel)
updated_at(default)- default all tables
- Unix timestamp of last update time (inherited from
BaseBeanieDocumentModel)
is_deleted(default)- default all tables
- Soft delete flag (inherited from
BaseBeanieDocumentModel) - Note:
is_deletedis the hard audit-level soft delete (hidden from all views including admin).status = DELETEDis the user-facing soft delete (hidden from default view, retained for audit surface in Phase 2)
created_by(default)- User id or Agent id of the creator (inherited from
BaseBeanieDocumentModel)
- User id or Agent id of the creator (inherited from
updated_by(default)- User id of the last updater (inherited from
BaseBeanieDocumentModel)
- User id of the last updater (inherited from
parent_id(default =DEFAULT_PYDANTIC_OBJECT_ID, meaning “root”)PydanticObjectId; self-reference to anothercommentDEFAULT_PYDANTIC_OBJECT_ID(fromsrc/constants/core.py) is reused as the root sentinel - the same constant taxonomy uses for L1 roots - so threading queries never have to branch onNone- Phase 1: every comment is created with
parent_id = DEFAULT_PYDANTIC_OBJECT_ID(flat comments, no threading) - Pre-provisioned in schema to avoid migration later
- Activated in Phase 3 when threading ships (a Phase 3 index on
parent_idmust exclude the sentinel via a partial filter rather than rely onsparse: true)
- Indexes
dktcmt1_docket_status_created_idx- Compound:
(docket_id ASC, status ASC, created_at DESC) - Covers the default view: “all open comments on this docket, newest first”
- Also covers filtered views (all statuses, resolved only) on the same docket
- Compound:
dktcmt1_docket_idx- Single:
(docket_id ASC) - For count badge aggregation (open comment count per docket) and joins
- Single:
dktcmt1_course_idx- Single:
(course_id ASC) - Matches existing pattern; course-scoped queries
- Single:
dktcmt1_author_idx- Single:
(created_by ASC) - For “my comments” queries (future)
- Single:
dktcmt1_parent_idx- Single:
(parent_id ASC), partial filter{ parent_id: { $ne: DEFAULT_PYDANTIC_OBJECT_ID } }(notsparse, since root comments carry the sentinel rather thannull) - Only needed when threading is enabled
- Can defer creation to Phase 3
- Single:
comment_audit- to keep a track of actions happen on comment
- fields
_id= [mongo_id]comment_idaction_performed:"change_status"createdresolveddeleted- etc
created_at(default)- default all tables
- Unix timestamp of creation time (inherited from
BaseBeanieDocumentModel)
updated_at(default)- default all tables
- Unix timestamp of last update time (inherited from
BaseBeanieDocumentModel)
is_deleted(default)- default all tables
- Soft delete flag (inherited from
BaseBeanieDocumentModel) - Note:
is_deletedis the hard audit-level soft delete (hidden from all views including admin).status = DELETEDis the user-facing soft delete (hidden from default view, retained for audit surface in Phase 2)
created_by(default)- User id or Agent id of the creator (inherited from
BaseBeanieDocumentModel)
- User id or Agent id of the creator (inherited from
updated_by(default)- User id of the last updater (inherited from
BaseBeanieDocumentModel)
- User id of the last updater (inherited from
- indexes
- In Settings (static)
Client API (reviewed shape)
This section matched the reviewed database at sign-off. It does not list every later change.
Dropped from payloads (they are not in the DB): docket_id, author_role, resolved_at, resolved_by.
Still core
author_typeis stored on each comment (human vs AI).created_byis the user id (AI users are normal user rows).- Resolve and reopen only change status on the row. Who did it and when live in
comment_audit, not on the comment.
Response shape (idea) - id, course, content type and id, body and body type, status, parent id (root in phase 1), author type, timestamps, created by / updated by, soft-delete flag. author_name was added later in the shipped API; see OpenAPI and response schemas under src/api/av1/comment/schemas.py.
Base path - Admin app only: /av1/comments. Phase 1 only needs content_type = docket (integer 1).
Operations (names only)
- Create -
POST …/commentswith course, content type, content id, body, optional body type. Server sets author type from the token. - List -
GET …/commentswith filters and paging. Deleted rows hidden. - Get one -
GET …/comments/{id}. - Edit body -
PATCH …/comments/{id}. Own comment or elevated role in the reviewed text; shipped rules are tighter (see test-document andcomment_permissions.py). AI-authored rows never editable. - Resolve -
POST …/comments/{id}/resolve. Human roles only in the reviewed text; shipped rules exclude editor (see test-document andcomment_permissions.py). - Reopen -
POST …/comments/{id}/reopen. Same note as resolve. - Delete -
DELETE …/comments/{id}. Own vs “any” in the reviewed text; shipped “delete anyone’s” is system admin only (see test-document andcomment_permissions.py). - Counts -
GET …/comments/countswith manycontent_idsquery params for badges.
AI - Same create URL with a token that marks the caller as AI. No separate internal URL for phase 1. AI cannot call the other seven operations.
Character limits in this TRD said 2000; production raised the limit and added Plate JSON. See route validation and OpenAPI for current limits.
Test ideas from review: empty body, long body, bad type, missing target, list filters, paging, edit own vs other’s, resolve and reopen, delete paths, AI blocked on mutations, counts edge cases. Track coverage in test-document and release-checklist (automation sections).
Admin UI (reviewed intent)
- Panel stays next to the docket (not a random centered popup).
- List shows who, what, when, open vs resolved, and a clear AI treatment.
- Actions depend on role; exact visibility matches
comment_permissions.pyand test-document today (editor cannot resolve in production). - Filters - at least open / resolved / all. Author-type filter was deferred in the TRD; the API may still expose it now (see implementation-delta).
- Composer - plain text at minimum; production adds rich text and a higher limit (see OpenAPI and create/patch schemas).
- Empty state - friendly line when there are no comments.
- Count badge on docket lists, fed by the batch counts call; hide when zero.
Web UI (public product)
Left blank in the original TRD. Consumer-facing web was out of scope for this admin-focused review.