core-framework 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. core_framework/__init__.py +0 -0
  2. core_framework/alembic/comment/alembic/README +1 -0
  3. core_framework/alembic/comment/alembic/env.py +72 -0
  4. core_framework/alembic/comment/alembic/script.py.mako +28 -0
  5. core_framework/alembic/comment/alembic/versions/30334fd1347b_init.py +59 -0
  6. core_framework/alembic/comment/alembic/versions/a2b3c4d5e6f7_improve_comment_indexes.py +54 -0
  7. core_framework/alembic/comment/alembic/versions/bcc8e00cfc8b_add_extra_tables.py +64 -0
  8. core_framework/alembic/comment/alembic/versions/d1e2f3a4b5c6_add_comment_stats_dirty_table.py +29 -0
  9. core_framework/alembic/comment/alembic/versions/e3f4a5b6c7d8_cascade_delete_comment_descendants.py +49 -0
  10. core_framework/alembic/comment/alembic/versions/f7e6d5c4b3a2_comments_path_to_ltree.py +60 -0
  11. core_framework/alembic/comment/alembic.ini +52 -0
  12. core_framework/alembic/extension/alembic/README +1 -0
  13. core_framework/alembic/extension/alembic/env.py +98 -0
  14. core_framework/alembic/extension/alembic/script.py.mako +28 -0
  15. core_framework/alembic/extension/alembic/versions/0389226049cb_add_pg_trgm_extension.py +25 -0
  16. core_framework/alembic/extension/alembic/versions/5dc58b016cf5_add_citext_extension.py +25 -0
  17. core_framework/alembic/extension/alembic/versions/b0ba0d8a284e_add_pg_stat_statements_extension.py +25 -0
  18. core_framework/alembic/extension/alembic/versions/c9d0e1f2a3b4_add_ltree_extension.py +25 -0
  19. core_framework/alembic/extension/alembic.ini +147 -0
  20. core_framework/alembic/moderation/alembic/README +1 -0
  21. core_framework/alembic/moderation/alembic/env.py +98 -0
  22. core_framework/alembic/moderation/alembic/script.py.mako +28 -0
  23. core_framework/alembic/moderation/alembic/versions/085ba9021850_add_category_to_user_restrictions.py +93 -0
  24. core_framework/alembic/moderation/alembic/versions/5f9e4fc14a41_create_moderation_appeals_table.py +69 -0
  25. core_framework/alembic/moderation/alembic/versions/63e37381e73b_add_user_reports_table.py +33 -0
  26. core_framework/alembic/moderation/alembic/versions/6a2ae31b7ac6_add_moderation_actions_table.py +34 -0
  27. core_framework/alembic/moderation/alembic/versions/716aa1735c03_improve_indexes.py +36 -0
  28. core_framework/alembic/moderation/alembic/versions/7d243ddbfde1_add_post_reports_table.py +35 -0
  29. core_framework/alembic/moderation/alembic/versions/8fba1f72dd46_add_indexes.py +64 -0
  30. core_framework/alembic/moderation/alembic/versions/95cc35a51984_update_restriction_history.py +91 -0
  31. core_framework/alembic/moderation/alembic/versions/9ad79d0af730_add_unique_constraint_user_reports_.py +28 -0
  32. core_framework/alembic/moderation/alembic/versions/a5e569f5df1a_create_user_restrictions_table.py +38 -0
  33. core_framework/alembic/moderation/alembic/versions/b2c3d4e5f6a7_add_indexes.py +42 -0
  34. core_framework/alembic/moderation/alembic/versions/c3d4e5f6a7b8_improve_report_indexes.py +48 -0
  35. core_framework/alembic/moderation/alembic/versions/d4af74643ff5_add_internal_notes_table.py +38 -0
  36. core_framework/alembic/moderation/alembic/versions/db20f2fb7390_add_comment_reports_table.py +35 -0
  37. core_framework/alembic/moderation/alembic/versions/e66226952ea6_add_report_category_to_user_reports_.py +54 -0
  38. core_framework/alembic/moderation/alembic/versions/f5e8cb275c30_enforce_1_pending_appeal.py +29 -0
  39. core_framework/alembic/moderation/alembic/versions/fe1faad2832d_create_restriction_history_table.py +69 -0
  40. core_framework/alembic/moderation/alembic.ini +147 -0
  41. core_framework/alembic/post/alembic/README +1 -0
  42. core_framework/alembic/post/alembic/env.py +97 -0
  43. core_framework/alembic/post/alembic/script.py.mako +28 -0
  44. core_framework/alembic/post/alembic/versions/51542673f5c8_add_tables_for_muted_banned_users.py +41 -0
  45. core_framework/alembic/post/alembic/versions/5beeeae40a4a_add_post_views_table.py +45 -0
  46. core_framework/alembic/post/alembic/versions/620971509a8b_init.py +55 -0
  47. core_framework/alembic/post/alembic/versions/a1b2c3d4e5f6_add_indexes.py +44 -0
  48. core_framework/alembic/post/alembic/versions/c1d2e3f4a5b6_add_post_hashtags_table.py +36 -0
  49. core_framework/alembic/post/alembic/versions/e56723f2afff_add_post_stats_table.py +39 -0
  50. core_framework/alembic/post/alembic/versions/fbc723ac58cc_add_post_likes_table.py +32 -0
  51. core_framework/alembic/post/alembic.ini +149 -0
  52. core_framework/alembic/user/alembic/README +1 -0
  53. core_framework/alembic/user/alembic/env.py +98 -0
  54. core_framework/alembic/user/alembic/script.py.mako +28 -0
  55. core_framework/alembic/user/alembic/versions/1a8bb99726ed_remove_avatar_id_from_users.py +81 -0
  56. core_framework/alembic/user/alembic/versions/2ccacf455941_improve_indexes.py +34 -0
  57. core_framework/alembic/user/alembic/versions/47f47ce2110e_create_user_deletions_table.py +31 -0
  58. core_framework/alembic/user/alembic/versions/5976db3f0175_drop_user_states.py +26 -0
  59. core_framework/alembic/user/alembic/versions/62417002cf32_add_indexes.py +46 -0
  60. core_framework/alembic/user/alembic/versions/6f7ccf3c226b_refactor_user_login_events.py +66 -0
  61. core_framework/alembic/user/alembic/versions/73432817015b_add_user_preferences_table.py +33 -0
  62. core_framework/alembic/user/alembic/versions/765bc01a7a59_create_user_blocks_table.py +33 -0
  63. core_framework/alembic/user/alembic/versions/7a56631f9927_create_user_login_events_table.py +49 -0
  64. core_framework/alembic/user/alembic/versions/831611e589bc_create_user_state.py +31 -0
  65. core_framework/alembic/user/alembic/versions/83c98ab2a779_add_user_profiles_table.py +88 -0
  66. core_framework/alembic/user/alembic/versions/8a94362cad6d_create_user_role.py +31 -0
  67. core_framework/alembic/user/alembic/versions/94b973923895_add_user_change_history_table.py +97 -0
  68. core_framework/alembic/user/alembic/versions/cbc0f4efe84f_add_avatar_id_column_to_users_table.py +31 -0
  69. core_framework/alembic/user/alembic/versions/d8b98ac6b073_add_index_for_get_admin_user_ids_query.py +29 -0
  70. core_framework/alembic/user/alembic/versions/ddb70cc09d16_create_user_states_table.py +34 -0
  71. core_framework/alembic/user/alembic/versions/f9ba10815ecd_add_users_table.py +33 -0
  72. core_framework/alembic/user/alembic.ini +147 -0
  73. core_framework/api/__init__.py +0 -0
  74. core_framework/api/admin/__init__.py +0 -0
  75. core_framework/api/admin/comments/router.py +69 -0
  76. core_framework/api/admin/comments/schemas.py +53 -0
  77. core_framework/api/admin/moderation/__init__.py +0 -0
  78. core_framework/api/admin/moderation/router.py +205 -0
  79. core_framework/api/admin/moderation/schemas.py +110 -0
  80. core_framework/api/admin/posts/router.py +62 -0
  81. core_framework/api/admin/posts/schemas.py +29 -0
  82. core_framework/api/admin/router.py +17 -0
  83. core_framework/api/admin/users/__init__.py +0 -0
  84. core_framework/api/admin/users/router.py +181 -0
  85. core_framework/api/admin/users/schemas.py +137 -0
  86. core_framework/api/auth/__init__.py +0 -0
  87. core_framework/api/auth/router.py +21 -0
  88. core_framework/api/auth/schemas.py +28 -0
  89. core_framework/api/comments/authenticated/router.py +126 -0
  90. core_framework/api/comments/authenticated/schemas.py +27 -0
  91. core_framework/api/comments/public/router.py +103 -0
  92. core_framework/api/comments/public/schemas.py +36 -0
  93. core_framework/api/comments/router.py +9 -0
  94. core_framework/api/comments/schemas.py +17 -0
  95. core_framework/api/dependencies.py +168 -0
  96. core_framework/api/events/router.py +39 -0
  97. core_framework/api/events/schemas.py +20 -0
  98. core_framework/api/posts/authenticated/router.py +83 -0
  99. core_framework/api/posts/authenticated/schemas.py +37 -0
  100. core_framework/api/posts/public/router.py +100 -0
  101. core_framework/api/posts/public/schemas.py +39 -0
  102. core_framework/api/posts/router.py +9 -0
  103. core_framework/api/posts/schemas.py +39 -0
  104. core_framework/api/router.py +19 -0
  105. core_framework/api/schemas.py +9 -0
  106. core_framework/api/system/__init__.py +0 -0
  107. core_framework/api/system/router.py +108 -0
  108. core_framework/api/users/__init__.py +0 -0
  109. core_framework/api/users/authenticated/__init__.py +0 -0
  110. core_framework/api/users/authenticated/router.py +244 -0
  111. core_framework/api/users/authenticated/schemas.py +81 -0
  112. core_framework/api/users/public/__init__.py +0 -0
  113. core_framework/api/users/public/router.py +25 -0
  114. core_framework/api/users/public/schemas.py +7 -0
  115. core_framework/api/users/router.py +9 -0
  116. core_framework/api/users/shared/schemas.py +174 -0
  117. core_framework/application/__init__.py +0 -0
  118. core_framework/application/auth/__init__.py +0 -0
  119. core_framework/application/auth/access_service.py +26 -0
  120. core_framework/application/auth/auth_service.py +10 -0
  121. core_framework/application/auth/models.py +10 -0
  122. core_framework/application/bootstrap.py +19 -0
  123. core_framework/application/comments/admin_service.py +236 -0
  124. core_framework/application/comments/aggregation_service.py +28 -0
  125. core_framework/application/comments/authenticated_service.py +89 -0
  126. core_framework/application/comments/public_service.py +218 -0
  127. core_framework/application/events/README.md +26 -0
  128. core_framework/application/events/event_service.py +51 -0
  129. core_framework/application/events/event_token.py +46 -0
  130. core_framework/application/events/models.py +9 -0
  131. core_framework/application/moderation/__init__.py +0 -0
  132. core_framework/application/moderation/appeal_service.py +98 -0
  133. core_framework/application/moderation/moderator_service.py +46 -0
  134. core_framework/application/moderation/report_service.py +180 -0
  135. core_framework/application/moderation/scheduled_service.py +5 -0
  136. core_framework/application/moderation/user_service.py +180 -0
  137. core_framework/application/posts/admin_service.py +104 -0
  138. core_framework/application/posts/aggregation_service.py +28 -0
  139. core_framework/application/posts/authenticated_service.py +72 -0
  140. core_framework/application/posts/public_service.py +197 -0
  141. core_framework/application/shared/__init__.py +0 -0
  142. core_framework/application/shared/enums.py +16 -0
  143. core_framework/application/shared/exceptions.py +16 -0
  144. core_framework/application/shared/user_agent.py +24 -0
  145. core_framework/application/users/__init__.py +0 -0
  146. core_framework/application/users/admin_service.py +298 -0
  147. core_framework/application/users/authenticated_service.py +179 -0
  148. core_framework/application/users/public_service.py +7 -0
  149. core_framework/application/users/scheduled_service.py +5 -0
  150. core_framework/bundled_alembic.py +57 -0
  151. core_framework/core/__init__.py +37 -0
  152. core_framework/core/cache.py +234 -0
  153. core_framework/core/context.py +14 -0
  154. core_framework/core/database.py +111 -0
  155. core_framework/core/exception_handlers/__init__.py +3 -0
  156. core_framework/core/exception_handlers/comment.py +99 -0
  157. core_framework/core/exception_handlers/common.py +5 -0
  158. core_framework/core/exception_handlers/moderation.py +104 -0
  159. core_framework/core/exception_handlers/post.py +54 -0
  160. core_framework/core/exception_handlers/setup.py +80 -0
  161. core_framework/core/exception_handlers/user.py +72 -0
  162. core_framework/core/http_client.py +64 -0
  163. core_framework/core/logging.py +99 -0
  164. core_framework/core/middleware.py +64 -0
  165. core_framework/core/observability.py +36 -0
  166. core_framework/core/pagination.py +203 -0
  167. core_framework/core/redis.py +135 -0
  168. core_framework/core/runtime.py +66 -0
  169. core_framework/core/settings.py +189 -0
  170. core_framework/domains/__init__.py +0 -0
  171. core_framework/domains/comment/README.md +243 -0
  172. core_framework/domains/comment/__init__.py +25 -0
  173. core_framework/domains/comment/constants.py +3 -0
  174. core_framework/domains/comment/dependencies.py +29 -0
  175. core_framework/domains/comment/enums.py +11 -0
  176. core_framework/domains/comment/exceptions.py +31 -0
  177. core_framework/domains/comment/models.py +54 -0
  178. core_framework/domains/comment/repository.py +947 -0
  179. core_framework/domains/comment/service.py +259 -0
  180. core_framework/domains/moderation/README.md +138 -0
  181. core_framework/domains/moderation/__init__.py +47 -0
  182. core_framework/domains/moderation/dependencies.py +29 -0
  183. core_framework/domains/moderation/enums.py +62 -0
  184. core_framework/domains/moderation/exceptions.py +31 -0
  185. core_framework/domains/moderation/models.py +94 -0
  186. core_framework/domains/moderation/repository.py +828 -0
  187. core_framework/domains/moderation/service.py +334 -0
  188. core_framework/domains/post/README.md +182 -0
  189. core_framework/domains/post/__init__.py +22 -0
  190. core_framework/domains/post/constants.py +3 -0
  191. core_framework/domains/post/dependencies.py +29 -0
  192. core_framework/domains/post/enums.py +18 -0
  193. core_framework/domains/post/exceptions.py +21 -0
  194. core_framework/domains/post/models.py +53 -0
  195. core_framework/domains/post/repository.py +791 -0
  196. core_framework/domains/post/service.py +204 -0
  197. core_framework/domains/user/README.md +74 -0
  198. core_framework/domains/user/__init__.py +39 -0
  199. core_framework/domains/user/constants.py +8 -0
  200. core_framework/domains/user/dependencies.py +29 -0
  201. core_framework/domains/user/enums.py +19 -0
  202. core_framework/domains/user/exceptions.py +31 -0
  203. core_framework/domains/user/models.py +124 -0
  204. core_framework/domains/user/repository.py +612 -0
  205. core_framework/domains/user/service.py +257 -0
  206. core_framework/domains/user/utils.py +182 -0
  207. core_framework/main.py +104 -0
  208. core_framework/worker/__init__.py +0 -0
  209. core_framework/worker/main.py +56 -0
  210. core_framework/worker/schedules/__init__.py +35 -0
  211. core_framework/worker/schedules/schedule_aggregate_comment_stats.py +32 -0
  212. core_framework/worker/schedules/schedule_aggregate_post_view_counts.py +28 -0
  213. core_framework/worker/schedules/schedule_expired_account_deletions.py +24 -0
  214. core_framework/worker/schedules/schedule_expired_mute_lifts.py +24 -0
  215. core_framework/worker/tasks/__init__.py +11 -0
  216. core_framework/worker/tasks/process_account_deletion.py +13 -0
  217. core_framework/worker/tasks/process_aggregate_comment_stats.py +19 -0
  218. core_framework/worker/tasks/process_aggregate_post_stats.py +12 -0
  219. core_framework/worker/tasks/process_mute_lift.py +13 -0
  220. core_framework-0.3.0.dist-info/METADATA +22 -0
  221. core_framework-0.3.0.dist-info/RECORD +222 -0
  222. core_framework-0.3.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,243 @@
1
+ # Comment Domain
2
+
3
+ ## Implementation Status
4
+
5
+ - **Implemented**: Root and reply creation; comment retrieval (by subject, replies under a parent, by author with viewer restrictions/blocks). Materialized path stored as PostgreSQL `ltree` (path labels are comment IDs); max reply depth enforced in application code. Author comment edit with a fixed max edit count and `edited_count` / `edited_at`. Soft delete via `comment_status` (`active` / `deleted`) for the author’s own comments and status updates by comment id (used by admin inactive/restore flows outside this package). Comment likes (idempotent like/unlike; counts feed into stats). Engagement stats materialized in `comment_stats`, driven by a `comment_stats_dirty` table and periodic aggregation (see `docs/flows/comments/comment_stats_aggregation.md`).
6
+ - **Planned**: Media attachments on comments (metadata, ordering, processing lifecycle as described in this document).
7
+
8
+ ## Scope
9
+
10
+ Owns comment content lifecycle, subject attachment, reply hierarchy, media attachments, and comment likes. Provides the authoritative source for comment existence and current content state. Comments are always attached to a subject (e.g., post, user wall); the subject cannot be null.
11
+
12
+ ## Owns
13
+
14
+ ### Comment Content
15
+
16
+ - Comment creation, retrieval, editing, and deletion state
17
+ - Author-owned content fields (body and optional metadata)
18
+ - Comment timestamps (created_at, updated_at, edited_at)
19
+ - Edit limit (max 5 edits) and `edited_count` for author capability display
20
+ - Comment ID uses ULID format (varchar(26)), same as posts
21
+
22
+ ### Subject Attachment
23
+
24
+ - Every comment is attached to exactly one subject
25
+ - Subject is identified by `subject_type` (e.g., post, user_wall) and `subject_id`
26
+ - Subject cannot be null; a comment always belongs to something
27
+ - Domain stores only IDs for external entities (no cross-domain foreign keys)
28
+
29
+ ### Reply Hierarchy
30
+
31
+ - Comments can be replies to other comments (nested structure)
32
+ - Top-level comments have `parent_comment_id` = null
33
+ - Replies have `parent_comment_id` pointing to the parent comment
34
+ - **Database layer**: No restriction on nesting depth
35
+ - **Application layer**: Maximum depth of 5 levels (configurable in code; can be increased later without schema changes)
36
+ - **Materialized path (`ltree`)**: Each comment stores `path` as an `ltree` built from **comment IDs** (ULIDs) as labels, plus `level` (0 = root, 1 = first reply, …) for depth checks. Used for ancestor/descendant and subtree queries, not for display order.
37
+
38
+ ### Comment Lifecycle
39
+
40
+ - Soft-delete state for comment lifecycle management
41
+ - Readability/editability rules based on state
42
+
43
+ ### Media Attachments
44
+
45
+ - Comment-linked media metadata for images and videos
46
+ - Attachment ordering and per-comment attachment limits
47
+ - Media state lifecycle (processing, ready, failed, removed)
48
+
49
+ ### Comment Likes
50
+
51
+ - User-to-comment like relationships
52
+ - Idempotent like/unlike behavior as relationship state
53
+ - Aggregate like counts derived from comment_like rows
54
+
55
+ ### Engagement Analytics
56
+
57
+ - Comment engagement counters and derived analytics (likes, replies, reports)
58
+ - Aggregation strategy for comment metrics (similar to post_stats)
59
+ - Retrieval of comment-level stats for display and sorting
60
+
61
+ ### Post `comment_count` (read by post domain)
62
+
63
+ For `subject_type = post`, **`post.post_stats.comment_count`** is derived in **`aggregate_post_stats`** using **`select_active_comment_count_for_post`**: count **`active`** comments where **no ancestor on that post** has **`deleted`** status, using **`ltree` `path`** and the `@>` ancestor operator. A soft-deleted comment therefore removes itself and **all descendants** from that tally until counts are recomputed. See `docs/flows/posts/post_stats_aggregation.md` (Comment count semantics).
64
+
65
+ ## Does Not Own
66
+
67
+ - User identity, authentication, or profile data
68
+ - User relationship graph (follows/friends/blocks)
69
+ - Subject entities (posts, user walls, etc.)—comment domain stores subject_type and subject_id only
70
+ - Comment reports (owned by moderation domain)
71
+ - Enforcement decisions (ban/mute/warn) and appeal lifecycle—owned by moderation domain
72
+ - Hashtags (comments do not support hashtags)
73
+
74
+ ## User removal and redacted authors (design)
75
+
76
+ When an account is removed, **comments authored by that user are not hard-deleted**. The row and `id` stay so **replies and `ltree` structure remain valid** and other participants can still read the thread.
77
+
78
+ - Set `author_id` to a **well-known sentinel** (e.g. `DELETED`) defined in application/domain code; it must not match a real user id.
79
+ - **Redact** `content` to an agreed placeholder; the comment remains **visible** in listings where policy allows (e.g. not soft-deleted).
80
+ - **Moderation**: keep existing `comment_reports` and `comment_likes`; do **not** rewrite `reporter_id` / `liker_id` to the sentinel (orphan UUIDs are fine for current product). Public comment **GET** responses set **`engagement_allowed`** to `false` when `author_id` is the redaction sentinel so clients can hide like/report/reply affordances; **write** endpoints do not enforce that flag.
81
+ - **User identity**: resolve the sentinel to a default identity in read paths (same idea as “deleted user” for missing users).
82
+
83
+ See `docs/flows/users/user_removal.md` for the full cross-domain policy.
84
+
85
+ ## Hard delete when a post is removed (design)
86
+
87
+ When an admin **hard-deletes a post** (`DELETE /admin/posts/{post_id}`), **all comments** with `subject_type = post` and `subject_id = that post` are **removed from the database** (roots and nested replies), together with **`comment_likes`**, **`comment_stats`**, **`comment_stats_dirty`**, **`comment_attachments`**, and **`comment_reports`** for those comments. This is **not** redaction: comment ids under that post cease to exist after a successful hard delete.
88
+
89
+ User **account removal** does **not** do this; it redacts authored comments in place and keeps ids for thread stability. See `docs/flows/posts/admin_posts.md` (Hard delete post) and `docs/flows/users/user_removal.md`.
90
+
91
+ ## Invariants
92
+
93
+ - A comment has exactly one author_id
94
+ - A comment has exactly one subject (subject_type + subject_id, both not null)
95
+ - A comment in deleted state is not editable
96
+ - Attachments are immutable once published on an active comment
97
+ - Attachment type must be image or video
98
+ - A user can like a given comment at most once
99
+ - Engagement counters are non-negative
100
+ - Domain stores only IDs for external entities (no cross-domain foreign keys)
101
+ - Comment reads/writes only touch the comment schema
102
+
103
+ ## Feed Visibility Filtering Rules
104
+
105
+ - Comment domain may keep local lookup mirrors for cross-domain visibility checks:
106
+ - `user_restrictions_lookup` (restricted authors: muted/banned)
107
+ - `user_blocks_lookup` (directed block edge: blocker -> blocked)
108
+ - For viewer `A` and comment author `B`, hide the comment when either:
109
+ - `A` has blocked `B` (exists in `user_blocks_lookup`)
110
+ - `B` is restricted as muted/banned (exists in `user_restrictions_lookup`)
111
+ - Special rule: if viewer `A` is muted, `A` can still see their own comments.
112
+ - These lookup tables are mirrors for read-time filtering only; source-of-truth remains outside comment domain.
113
+
114
+ ## Lookup Sync Contract
115
+
116
+ - Source-of-truth for restriction state is the moderation domain.
117
+ - Source-of-truth for user-to-user blocks is the user domain.
118
+ - Comment domain mirrors (`user_restrictions_lookup`, `user_blocks_lookup`) exist only to support read-time filtering.
119
+ - Sync operations should be idempotent and safe to run repeatedly.
120
+ - No-op moderation operations (for example, banning an already banned user or clearing an already active user)
121
+ should still reconcile comment-domain lookup rows to heal prior cross-domain partial failures.
122
+
123
+ ## Materialized Path (`ltree` + `level`)
124
+
125
+ To avoid recursive CTEs or multiple round-trips when resolving comment trees, each comment stores:
126
+
127
+ - **path** (`ltree`): A dot-separated chain of **labels**, where each label is that comment’s **own** `id` (ULID) from the root down to this node.
128
+ - **Root**: `path` is a single label—the new comment’s `id` (e.g. `01HZXK...` as one `ltree` value).
129
+ - **Reply**: `path` is `parent.path` with this comment’s `id` appended as the next label (e.g. `01HZROOT...01HZCHILD...` in `ltree` form).
130
+ - Stored type is PostgreSQL `ltree`; the `ltree` extension lives in the shared **extension** schema (see repo Postgres conventions). App connections must resolve `ltree` types/operators (e.g. via `search_path` including the extension schema).
131
+ - **level** (int): Depth in the tree (0 = root, 1 = first reply, …). Used for max-depth enforcement and API rules (e.g. `can_reply`). May be kept in sync with `nlevel(path) - 1` conceptually.
132
+
133
+ **Display order**: List and reply endpoints use **`created_at` (or other explicit sort keys)**. Do not use `ORDER BY path` for Reddit-style chronological or scored ordering—`ltree` compares labels as text; ULID labels are not a reliable sibling order.
134
+
135
+ With **max depth 5**, paths are short (at most five labels, each 26 characters for a ULID), which keeps rows and indexes reasonable.
136
+
137
+ ### Example
138
+
139
+ Illustrative (real IDs are 26-character ULIDs):
140
+
141
+ | comment_id | parent_comment_id | path (`ltree`) | level |
142
+ | ---------- | ----------------- | ---------------------- | ----- |
143
+ | `01HZ…A` | null | `01HZ…A` | 0 |
144
+ | `01HZ…B` | `01HZ…A` | `01HZ…A.01HZ…B` | 1 |
145
+ | `01HZ…C` | `01HZ…A` | `01HZ…A.01HZ…C` | 1 |
146
+ | `01HZ…D` | `01HZ…B` | `01HZ…A.01HZ…B.01HZ…D` | 2 |
147
+
148
+ ### Query patterns
149
+
150
+ - **Subtree (descendants of a node, including the node)**: `WHERE path <@ root.path` (and usually restrict by `subject_type` / `subject_id` for safety).
151
+ - **Ancestor chain**: walk `parent_comment_id` or use `ltree` operators / `subpath` as needed.
152
+ - **Depth limit**: `WHERE level < 5` (application max depth); or `nlevel(path) <= 5` if aligned with the same policy.
153
+ - **Indexing**: GiST on `path` supports `<@`, `@>`, and related `ltree` operators (see PostgreSQL `ltree` docs).
154
+
155
+ ### Insert behavior
156
+
157
+ 1. Generate the new comment `id` (ULID) before insert.
158
+ 1. **Root**: set `path` to that single id as `ltree`; set `level` to 0.
159
+ 1. **Reply**: read parent’s `path` and `level`; set `path` to `parent.path || new_id`; set `level` to `parent.level + 1`.
160
+
161
+ No sibling index or `count(*)` is required for `path` construction—each new segment is unique because the new `id` is unique.
162
+
163
+ ### Path format (summary)
164
+
165
+ - **Chosen format**: ID labels only (`comment_id` per segment).
166
+ - **Not used**: Numeric sibling indices (`1.2.3`) for `path`; those are superseded by this design.
167
+
168
+ ## Nesting Depth Policy
169
+
170
+ - **Database**: No constraint on reply depth. The schema allows arbitrary nesting via `parent_comment_id`.
171
+ - **Application**: Enforce a maximum depth of 5 levels when creating replies. This limit lives in the application/service layer, not in the database.
172
+ - **Extensibility**: To increase the nesting limit, change the application-layer constant or configuration; no migration required.
173
+
174
+ ## Persistence Model
175
+
176
+ ### Tables
177
+
178
+ #### comments
179
+
180
+ - Canonical comment record (id, author_id, content, subject_type, subject_id, parent_comment_id, path, level, status, timestamps)
181
+ - `id`: ULID format (varchar(26)), same as posts
182
+ - `path`: `ltree` materialized path; each label is the comment `id` along the chain from root to this row (see Materialized Path above)
183
+ - `level`: Depth in reply tree (0 = root, 1 = first reply, …)
184
+ - Supports soft deletion and edit tracking (max 5 edits)
185
+ - Indexed for subject-based listing, author timeline, parent lookups, and subtree queries (e.g. GiST on `path` for `ltree` operators)
186
+
187
+ #### comment_stats
188
+
189
+ - Denormalized engagement counters per comment
190
+ - Stores comment_id, like_count, reply_count, report_count, timestamps
191
+ - like_count from comment_likes; reply_count from comments (parent_comment_id); report_count from moderation domain comment_reports
192
+ - Aggregated periodically via dirty-table pattern. See `docs/flows/comments/comment_stats_aggregation.md`.
193
+ - One row per comment; created when comment is created
194
+
195
+ #### comment_stats_dirty
196
+
197
+ - Tracks which comments need stats aggregation
198
+ - One row per comment_id; INSERT ON CONFLICT DO NOTHING
199
+ - Marked by like, reply, report events (async after commit)
200
+ - Drained by scheduled job every 5 min; worker aggregates and deletes
201
+
202
+ #### comment_likes
203
+
204
+ - Tracks user likes on comments as a relationship table
205
+ - Stores comment_id, liker_id, and created_at
206
+ - One row per (comment_id, liker_id) pair to enforce idempotent likes
207
+
208
+ #### user_restrictions_lookup
209
+
210
+ - Read-time mirror for restricted users (muted/banned). Source-of-truth: moderation domain.
211
+ - Stores user_id, restriction_type (muted, banned)
212
+ - Used in comment queries to hide comments from restricted authors. Sync operations reconcile from moderation domain; see Lookup Sync Contract above.
213
+
214
+ #### user_blocks_lookup
215
+
216
+ - Read-time mirror for user-to-user blocks. Source-of-truth: user domain.
217
+ - Stores blocker_id, blocked_id (directed edge: blocker has blocked blocked)
218
+ - Used in comment queries to hide comments from authors the viewer has blocked. Sync operations reconcile from user domain; see Lookup Sync Contract above.
219
+
220
+ #### comment_attachments
221
+
222
+ - Tracks media attached to comments (image/video)
223
+ - Stores comment_id, media_id/storage_key, media_type, order_index, and processing status
224
+ - Supports deterministic ordering for multi-attachment comments
225
+
226
+ ### Enums
227
+
228
+ #### comment_subject_type
229
+
230
+ - Subject type for comment attachment (post, user_wall, …)
231
+ - Extensible: add new values as new subject types are introduced
232
+
233
+ #### comment_status
234
+
235
+ - Lifecycle state of a comment (active, deleted)
236
+
237
+ #### comment_media_type
238
+
239
+ - Attachment media type (image, video)
240
+
241
+ #### comment_attachment_status
242
+
243
+ - Attachment processing lifecycle (processing, ready, failed, removed)
@@ -0,0 +1,25 @@
1
+ from core_framework.domains.comment.enums import CommentStatus, CommentSubjectType
2
+ from core_framework.domains.comment.exceptions import (
3
+ BaseCommentException,
4
+ CommentEditLimitReachedException,
5
+ CommentNotFoundException,
6
+ CommentUpdateNotFoundException,
7
+ MaxReplyDepthException,
8
+ ParentCommentNotFoundException,
9
+ )
10
+ from core_framework.domains.comment.models import Comment, CommentPreview, CommentStats, CommentWithMetadata
11
+
12
+ __all__ = [
13
+ "CommentStatus",
14
+ "CommentSubjectType",
15
+ "BaseCommentException",
16
+ "CommentEditLimitReachedException",
17
+ "CommentNotFoundException",
18
+ "CommentUpdateNotFoundException",
19
+ "MaxReplyDepthException",
20
+ "ParentCommentNotFoundException",
21
+ "Comment",
22
+ "CommentPreview",
23
+ "CommentStats",
24
+ "CommentWithMetadata",
25
+ ]
@@ -0,0 +1,3 @@
1
+ from typing import Final
2
+
3
+ REDACTED_CONTENT_PLACEHOLDER: Final[str] = "<deleted>"
@@ -0,0 +1,29 @@
1
+ from core_framework.core.runtime import CoreRuntime, unconfigured_dependency
2
+ from core_framework.domains.comment.repository import CommentRepository
3
+ from core_framework.domains.comment.service import CommentService
4
+
5
+
6
+ def build_comment_repository(runtime: CoreRuntime) -> CommentRepository:
7
+ return CommentRepository(runtime.write_postgres, runtime.read_postgres, runtime.write_postgres)
8
+
9
+
10
+ def build_comment_service(runtime: CoreRuntime) -> CommentService:
11
+ return CommentService(build_comment_repository(runtime))
12
+
13
+
14
+ def configure_comment_dependencies(runtime: CoreRuntime) -> None:
15
+ global comment_repository, comment_service
16
+ comment_repository = build_comment_repository(runtime)
17
+ comment_service = build_comment_service(runtime)
18
+
19
+
20
+ comment_repository = unconfigured_dependency("CommentRepository")
21
+ comment_service = unconfigured_dependency("CommentService")
22
+
23
+ __all__ = [
24
+ "build_comment_repository",
25
+ "build_comment_service",
26
+ "configure_comment_dependencies",
27
+ "comment_repository",
28
+ "comment_service",
29
+ ]
@@ -0,0 +1,11 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class CommentSubjectType(StrEnum):
5
+ POST = "post"
6
+ USER_WALL = "user_wall"
7
+
8
+
9
+ class CommentStatus(StrEnum):
10
+ ACTIVE = "active"
11
+ DELETED = "deleted"
@@ -0,0 +1,31 @@
1
+ class BaseCommentException(Exception):
2
+ message: str
3
+
4
+ def __init__(self, message: str):
5
+ self.message = message
6
+ super().__init__(self.message)
7
+
8
+
9
+ class ParentCommentNotFoundException(BaseCommentException):
10
+ def __init__(self) -> None:
11
+ super().__init__("Parent comment not found or not active")
12
+
13
+
14
+ class MaxReplyDepthException(BaseCommentException):
15
+ def __init__(self) -> None:
16
+ super().__init__("Maximum reply depth reached")
17
+
18
+
19
+ class CommentUpdateNotFoundException(BaseCommentException):
20
+ def __init__(self) -> None:
21
+ super().__init__("Unable to process request")
22
+
23
+
24
+ class CommentEditLimitReachedException(BaseCommentException):
25
+ def __init__(self) -> None:
26
+ super().__init__("Edit limit reached. Maximum edits allowed per comment.")
27
+
28
+
29
+ class CommentNotFoundException(BaseCommentException):
30
+ def __init__(self) -> None:
31
+ super().__init__("Comment not found")
@@ -0,0 +1,54 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime
3
+ from typing import ClassVar
4
+
5
+ from core_framework.domains.comment.enums import CommentStatus, CommentSubjectType
6
+
7
+
8
+ @dataclass(frozen=True, slots=True, kw_only=True)
9
+ class CommentStats:
10
+ like_count: int
11
+ reply_count: int
12
+ report_count: int
13
+
14
+ DEFAULT: ClassVar[CommentStats]
15
+
16
+
17
+ @dataclass(frozen=True, slots=True, kw_only=True)
18
+ class Comment:
19
+ id: str
20
+ author_id: str
21
+ content: str
22
+ edited_count: int
23
+ edited_at: datetime | None
24
+ level: int
25
+ created_at: datetime
26
+
27
+
28
+ @dataclass(frozen=True, slots=True, kw_only=True)
29
+ class CommentWithMetadata:
30
+ id: str
31
+ author_id: str
32
+ content: str
33
+ subject_type: CommentSubjectType
34
+ subject_id: str
35
+ parent_comment_id: str | None
36
+ status: CommentStatus
37
+ edited_count: int
38
+ edited_at: datetime | None
39
+ created_at: datetime
40
+
41
+
42
+ @dataclass(frozen=True, slots=True, kw_only=True)
43
+ class CommentPreview:
44
+ id: str
45
+ author_id: str
46
+ content: str
47
+ status: CommentStatus
48
+
49
+
50
+ CommentStats.DEFAULT = CommentStats(
51
+ like_count=0,
52
+ reply_count=0,
53
+ report_count=0,
54
+ )