Owner Abhi

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_audit for action history.
  • No composite “custom id” per docket; one user may post many comments, so each row gets a normal Mongo id.
  • No docket_id column; we use content_type plus content_id instead.
  • 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
    • Collection:
      • docket_comments; live name is comments
        • 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
          • content_id
          • content_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_id
            • CourseEnum (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: CommentAuthorTypeEnum
              • HUMAN = "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
          • 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
          • status
            • open
            • resolved
            • Enum: DocketCommentStatusEnum
              • OPEN = 1
              • RESOLVED = 2
            • Default: OPEN
            • Note: No DELETED state. Soft delete is handled by the inherited is_deleted flag alone
          • resolved_at
            • Unix timestamp (epoch ms), nullable
            • Set when status transitions to RESOLVED
          • resolved_by
            • PydanticObjectId, 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_deleted is the hard audit-level soft delete (hidden from all views including admin). status = DELETED is 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)
          • updated_by (default)
            • User id of the last updater (inherited from BaseBeanieDocumentModel)
          • parent_id (default = DEFAULT_PYDANTIC_OBJECT_ID, meaning “root”)
            • PydanticObjectId; self-reference to another comment
            • DEFAULT_PYDANTIC_OBJECT_ID (from src/constants/core.py) is reused as the root sentinel - the same constant taxonomy uses for L1 roots - so threading queries never have to branch on None
            • 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_id must exclude the sentinel via a partial filter rather than rely on sparse: 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
          • dktcmt1_docket_idx
            • Single: (docket_id ASC)
            • For count badge aggregation (open comment count per docket) and joins
          • dktcmt1_course_idx
            • Single: (course_id ASC)
            • Matches existing pattern; course-scoped queries
          • dktcmt1_author_idx
            • Single: (created_by ASC)
            • For “my comments” queries (future)
          • dktcmt1_parent_idx
            • Single: (parent_id ASC), partial filter { parent_id: { $ne: DEFAULT_PYDANTIC_OBJECT_ID } } (not sparse, since root comments carry the sentinel rather than null)
            • Only needed when threading is enabled
            • Can defer creation to Phase 3
      • comment_audit
        • to keep a track of actions happen on comment
        • fields
          • _id = [mongo_id]
          • comment_id
          • action_performed:
            • "change_status"
            • created
            • resolved
            • deleted
            • 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_deleted is the hard audit-level soft delete (hidden from all views including admin). status = DELETED is 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)
          • updated_by (default)
            • User id of the last updater (inherited from BaseBeanieDocumentModel)
        • indexes

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_type is stored on each comment (human vs AI). created_by is 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)

  1. Create - POST …/comments with course, content type, content id, body, optional body type. Server sets author type from the token.
  2. List - GET …/comments with filters and paging. Deleted rows hidden.
  3. Get one - GET …/comments/{id}.
  4. Edit body - PATCH …/comments/{id}. Own comment or elevated role in the reviewed text; shipped rules are tighter (see test-document and comment_permissions.py). AI-authored rows never editable.
  5. Resolve - POST …/comments/{id}/resolve. Human roles only in the reviewed text; shipped rules exclude editor (see test-document and comment_permissions.py).
  6. Reopen - POST …/comments/{id}/reopen. Same note as resolve.
  7. Delete - DELETE …/comments/{id}. Own vs “any” in the reviewed text; shipped “delete anyone’s” is system admin only (see test-document and comment_permissions.py).
  8. Counts - GET …/comments/counts with many content_ids query 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.py and 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.