@kernel-sig/console 0.1.0

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 (198) hide show
  1. package/.env.example +50 -0
  2. package/LICENSE +127 -0
  3. package/Makefile +66 -0
  4. package/README.md +279 -0
  5. package/backend/.dockerignore +12 -0
  6. package/backend/Dockerfile +43 -0
  7. package/backend/alembic/env.py +75 -0
  8. package/backend/alembic/script.py.mako +25 -0
  9. package/backend/alembic/versions/0001_initial.py +88 -0
  10. package/backend/alembic/versions/350e2d9b6553_add_pr_comment_table.py +48 -0
  11. package/backend/alembic/versions/4f8342e727fe_sig_info_roster_fields_and_branch_.py +39 -0
  12. package/backend/alembic/versions/5a31ade90136_add_repository_credential_pull_request_.py +293 -0
  13. package/backend/alembic/versions/87a008142f17_add_classification_attention_and_ai_.py +436 -0
  14. package/backend/alembic/versions/a1c7d90e4b52_branch_belongs_to_a_repository.py +72 -0
  15. package/backend/alembic/versions/b41c9d7e5f28_extend_pr_kind_categories.py +43 -0
  16. package/backend/alembic/versions/c8e4f2a71b93_add_release_kind.py +40 -0
  17. package/backend/alembic/versions/d1275091dfdc_add_needs_detail_flag_to_pull_request.py +44 -0
  18. package/backend/alembic/versions/e5b27c9d3a41_downgrade_cve_without_ids.py +54 -0
  19. package/backend/alembic/versions/ee3159a4eff0_add_sig_meeting_member_and_release_.py +164 -0
  20. package/backend/alembic/versions/fcf3c186d63b_attention_rule_subscriptions.py +42 -0
  21. package/backend/alembic.ini +40 -0
  22. package/backend/app/__init__.py +0 -0
  23. package/backend/app/api/__init__.py +0 -0
  24. package/backend/app/api/deps.py +74 -0
  25. package/backend/app/api/v1/__init__.py +0 -0
  26. package/backend/app/api/v1/ai.py +386 -0
  27. package/backend/app/api/v1/analytics.py +48 -0
  28. package/backend/app/api/v1/attention.py +142 -0
  29. package/backend/app/api/v1/auth.py +106 -0
  30. package/backend/app/api/v1/classification.py +192 -0
  31. package/backend/app/api/v1/health.py +29 -0
  32. package/backend/app/api/v1/issues.py +143 -0
  33. package/backend/app/api/v1/pulls.py +388 -0
  34. package/backend/app/api/v1/repositories.py +273 -0
  35. package/backend/app/api/v1/router.py +32 -0
  36. package/backend/app/api/v1/sig.py +565 -0
  37. package/backend/app/api/v1/users.py +87 -0
  38. package/backend/app/api/v1/webhooks.py +135 -0
  39. package/backend/app/core/__init__.py +0 -0
  40. package/backend/app/core/config.py +72 -0
  41. package/backend/app/core/crypto.py +74 -0
  42. package/backend/app/core/db.py +79 -0
  43. package/backend/app/core/exceptions.py +60 -0
  44. package/backend/app/core/logging.py +69 -0
  45. package/backend/app/core/permissions.py +87 -0
  46. package/backend/app/core/queue.py +57 -0
  47. package/backend/app/core/security.py +69 -0
  48. package/backend/app/domain/__init__.py +0 -0
  49. package/backend/app/domain/attention.py +618 -0
  50. package/backend/app/domain/classification.py +689 -0
  51. package/backend/app/domain/meeting.py +408 -0
  52. package/backend/app/domain/release.py +163 -0
  53. package/backend/app/domain/review.py +416 -0
  54. package/backend/app/domain/sig.py +178 -0
  55. package/backend/app/domain/sig_info.py +201 -0
  56. package/backend/app/integrations/__init__.py +0 -0
  57. package/backend/app/integrations/atomgit/__init__.py +0 -0
  58. package/backend/app/integrations/atomgit/client.py +505 -0
  59. package/backend/app/integrations/atomgit/models.py +311 -0
  60. package/backend/app/integrations/llm/__init__.py +0 -0
  61. package/backend/app/integrations/llm/prompts.py +222 -0
  62. package/backend/app/integrations/llm/provider.py +326 -0
  63. package/backend/app/main.py +242 -0
  64. package/backend/app/middleware/__init__.py +0 -0
  65. package/backend/app/middleware/audit.py +129 -0
  66. package/backend/app/middleware/request_context.py +42 -0
  67. package/backend/app/models/__init__.py +112 -0
  68. package/backend/app/models/ai.py +214 -0
  69. package/backend/app/models/attention.py +131 -0
  70. package/backend/app/models/attention_settings.py +61 -0
  71. package/backend/app/models/audit.py +53 -0
  72. package/backend/app/models/base.py +35 -0
  73. package/backend/app/models/classification.py +144 -0
  74. package/backend/app/models/credential.py +56 -0
  75. package/backend/app/models/issue.py +132 -0
  76. package/backend/app/models/meeting.py +189 -0
  77. package/backend/app/models/pull_request.py +288 -0
  78. package/backend/app/models/repository.py +196 -0
  79. package/backend/app/models/sig.py +194 -0
  80. package/backend/app/models/user.py +44 -0
  81. package/backend/app/schemas/__init__.py +0 -0
  82. package/backend/app/schemas/ai.py +120 -0
  83. package/backend/app/schemas/attention.py +43 -0
  84. package/backend/app/schemas/auth.py +26 -0
  85. package/backend/app/schemas/classification.py +58 -0
  86. package/backend/app/schemas/common.py +43 -0
  87. package/backend/app/schemas/pull_request.py +214 -0
  88. package/backend/app/schemas/repository.py +94 -0
  89. package/backend/app/schemas/sig.py +246 -0
  90. package/backend/app/schemas/user.py +79 -0
  91. package/backend/app/services/__init__.py +0 -0
  92. package/backend/app/services/ai_service.py +569 -0
  93. package/backend/app/services/analytics_service.py +390 -0
  94. package/backend/app/services/attention_queue.py +451 -0
  95. package/backend/app/services/attention_service.py +432 -0
  96. package/backend/app/services/auth_service.py +74 -0
  97. package/backend/app/services/classification_service.py +658 -0
  98. package/backend/app/services/credential_service.py +105 -0
  99. package/backend/app/services/pull_query.py +273 -0
  100. package/backend/app/services/release_service.py +446 -0
  101. package/backend/app/services/repository_service.py +132 -0
  102. package/backend/app/services/sig_service.py +385 -0
  103. package/backend/app/services/sync_service.py +752 -0
  104. package/backend/app/services/user_service.py +68 -0
  105. package/backend/app/worker.py +389 -0
  106. package/backend/entrypoint.sh +10 -0
  107. package/backend/pyproject.toml +68 -0
  108. package/backend/tests/test_analysis_api.py +154 -0
  109. package/backend/tests/test_atomgit_client.py +360 -0
  110. package/backend/tests/test_atomgit_models.py +257 -0
  111. package/backend/tests/test_attention.py +291 -0
  112. package/backend/tests/test_audit_middleware.py +135 -0
  113. package/backend/tests/test_classification.py +498 -0
  114. package/backend/tests/test_config.py +41 -0
  115. package/backend/tests/test_crypto.py +68 -0
  116. package/backend/tests/test_exceptions.py +62 -0
  117. package/backend/tests/test_health.py +63 -0
  118. package/backend/tests/test_llm_provider.py +320 -0
  119. package/backend/tests/test_meeting_domain.py +169 -0
  120. package/backend/tests/test_permissions.py +69 -0
  121. package/backend/tests/test_pull_query_wiring.py +66 -0
  122. package/backend/tests/test_pull_schemas.py +82 -0
  123. package/backend/tests/test_release_domain.py +82 -0
  124. package/backend/tests/test_review_parser.py +301 -0
  125. package/backend/tests/test_schemas_user.py +97 -0
  126. package/backend/tests/test_security.py +92 -0
  127. package/cli/index.js +338 -0
  128. package/compose.yaml +105 -0
  129. package/frontend/.dockerignore +4 -0
  130. package/frontend/Dockerfile +27 -0
  131. package/frontend/index.html +14 -0
  132. package/frontend/nginx.conf +47 -0
  133. package/frontend/package-lock.json +5020 -0
  134. package/frontend/package.json +35 -0
  135. package/frontend/src/api/ai.ts +124 -0
  136. package/frontend/src/api/analytics.ts +66 -0
  137. package/frontend/src/api/attention.ts +181 -0
  138. package/frontend/src/api/auth.ts +74 -0
  139. package/frontend/src/api/classification.ts +170 -0
  140. package/frontend/src/api/issues.ts +90 -0
  141. package/frontend/src/api/pulls.ts +233 -0
  142. package/frontend/src/api/repositories.ts +95 -0
  143. package/frontend/src/api/sig.ts +232 -0
  144. package/frontend/src/app/antd-theme.ts +94 -0
  145. package/frontend/src/app/providers.tsx +59 -0
  146. package/frontend/src/app/router.tsx +411 -0
  147. package/frontend/src/app/search.ts +30 -0
  148. package/frontend/src/components/ClassificationBadge.tsx +58 -0
  149. package/frontend/src/components/GateBadge.tsx +13 -0
  150. package/frontend/src/components/SeverityBadge.tsx +20 -0
  151. package/frontend/src/components/layout/AppShell.tsx +16 -0
  152. package/frontend/src/components/layout/AuthLayout.tsx +32 -0
  153. package/frontend/src/components/layout/Sidebar.tsx +223 -0
  154. package/frontend/src/components/layout/TopBar.tsx +47 -0
  155. package/frontend/src/components/pulls/DiscussionTimeline.tsx +145 -0
  156. package/frontend/src/components/pulls/FacetRail.tsx +199 -0
  157. package/frontend/src/components/pulls/LabelChips.tsx +87 -0
  158. package/frontend/src/components/ui/alert.tsx +30 -0
  159. package/frontend/src/components/ui/badge.tsx +53 -0
  160. package/frontend/src/components/ui/button.tsx +51 -0
  161. package/frontend/src/components/ui/card.tsx +64 -0
  162. package/frontend/src/components/ui/chart-theme.ts +65 -0
  163. package/frontend/src/components/ui/data-table.tsx +39 -0
  164. package/frontend/src/components/ui/echart.tsx +70 -0
  165. package/frontend/src/components/ui/empty-state.tsx +22 -0
  166. package/frontend/src/components/ui/input.tsx +39 -0
  167. package/frontend/src/components/ui/lazy-chart.tsx +21 -0
  168. package/frontend/src/components/ui/skeleton.tsx +19 -0
  169. package/frontend/src/hooks/use-current-repository.ts +44 -0
  170. package/frontend/src/hooks/use-current-user.ts +38 -0
  171. package/frontend/src/lib/api-client.ts +93 -0
  172. package/frontend/src/lib/css-color.ts +60 -0
  173. package/frontend/src/lib/utils.ts +45 -0
  174. package/frontend/src/main.tsx +22 -0
  175. package/frontend/src/pages/attention/AttentionQueuePage.tsx +316 -0
  176. package/frontend/src/pages/attention/RuleSettingsPanel.tsx +177 -0
  177. package/frontend/src/pages/branches/BranchDetailPage.tsx +632 -0
  178. package/frontend/src/pages/branches/BranchListPage.tsx +308 -0
  179. package/frontend/src/pages/dashboard/DashboardPage.tsx +657 -0
  180. package/frontend/src/pages/issues/IssueListPage.tsx +284 -0
  181. package/frontend/src/pages/login/LoginPage.tsx +95 -0
  182. package/frontend/src/pages/meetings/MeetingDetailPage.tsx +403 -0
  183. package/frontend/src/pages/meetings/MeetingListPage.tsx +264 -0
  184. package/frontend/src/pages/members/MembersPage.tsx +534 -0
  185. package/frontend/src/pages/pulls/PullDetailPage.tsx +833 -0
  186. package/frontend/src/pages/pulls/PullListPage.tsx +399 -0
  187. package/frontend/src/pages/settings/AiSettingsPage.tsx +449 -0
  188. package/frontend/src/pages/settings/SettingsPage.tsx +321 -0
  189. package/frontend/src/pages/setup/SetupPage.tsx +144 -0
  190. package/frontend/src/styles/globals.css +147 -0
  191. package/frontend/tsconfig.json +22 -0
  192. package/frontend/vite.config.ts +57 -0
  193. package/package.json +48 -0
  194. package/scripts/e2e-auth-flow.py +131 -0
  195. package/scripts/e2e-pr-detail.py +128 -0
  196. package/scripts/e2e-verify.py +773 -0
  197. package/scripts/verify-ai-pipeline.py +616 -0
  198. package/scripts/verify-analysis-pipeline.py +303 -0
@@ -0,0 +1,436 @@
1
+ """新增分类、关注项与 AI 分析相关的表
2
+
3
+ Revision ID: 87a008142f17
4
+ Revises: d1275091dfdc
5
+ Create Date: 2026-09-16
6
+
7
+ - ksp_classification:PR/Issue 的类型、子系统、CVE 归属。规则引擎先算,
8
+ AI 兜底,人工覆盖后不再被自动任务改写
9
+ - ksp_attention_item:由 domain.attention 的规则求值得到的待办队列。
10
+ 每次重算保留 first_detected_at,已解决的置 resolved_at 而非删除 ——
11
+ 「这条挂了多久」本身就是维护者要看的信号
12
+ - ksp_label_sync_state:记录已建到 AtomGit 的分类标签,避免重复创建
13
+ - ksp_llm_provider / ksp_task_routing / ksp_ai_analysis / ksp_prompt_template:
14
+ AI 对接。分析结果以 (subject, task) 唯一约束 + input_hash 做幂等,
15
+ 内容未变不重复调用模型
16
+
17
+ 枚举类型 ai_task 被三张表共用,而 op.create_table 默认会为每张表各发一次
18
+ CREATE TYPE,第二次即报「类型已存在」。因此这里先显式建类型,
19
+ 列定义上关闭自动创建。
20
+ """
21
+
22
+ from collections.abc import Sequence
23
+
24
+ import sqlalchemy as sa
25
+ from alembic import op
26
+ from sqlalchemy.dialects import postgresql
27
+
28
+ revision: str = "87a008142f17"
29
+ down_revision: str | None = "d1275091dfdc"
30
+ branch_labels: str | Sequence[str] | None = None
31
+ depends_on: str | Sequence[str] | None = None
32
+
33
+
34
+ # 跨表共用的枚举类型:先建一次,列上不再自动创建
35
+ AI_TASK_VALUES = ("classify", "summarize", "risk_review", "backport_verify")
36
+
37
+
38
+ def _ai_task_enum() -> postgresql.ENUM:
39
+ return postgresql.ENUM(*AI_TASK_VALUES, name="ai_task", create_type=False)
40
+
41
+
42
+ def upgrade() -> None:
43
+ ai_task = _ai_task_enum()
44
+ ai_task.create(op.get_bind(), checkfirst=True)
45
+
46
+ op.create_table(
47
+ "ksp_prompt_template",
48
+ sa.Column("id", sa.UUID(), nullable=False),
49
+ sa.Column("task", _ai_task_enum(), nullable=False),
50
+ sa.Column("name", sa.String(length=128), nullable=False),
51
+ sa.Column("version", sa.Integer(), nullable=False),
52
+ sa.Column("system_prompt", sa.Text(), nullable=False),
53
+ sa.Column("user_template", sa.Text(), nullable=False),
54
+ sa.Column("is_active", sa.Boolean(), nullable=False),
55
+ sa.Column("created_by", sa.UUID(), nullable=True),
56
+ sa.Column(
57
+ "created_at",
58
+ sa.DateTime(timezone=True),
59
+ server_default=sa.text("now()"),
60
+ nullable=False,
61
+ ),
62
+ sa.Column(
63
+ "updated_at",
64
+ sa.DateTime(timezone=True),
65
+ server_default=sa.text("now()"),
66
+ nullable=False,
67
+ ),
68
+ sa.ForeignKeyConstraint(
69
+ ["created_by"],
70
+ ["ksp_user.id"],
71
+ name=op.f("fk_ksp_prompt_template_created_by_ksp_user"),
72
+ ondelete="SET NULL",
73
+ ),
74
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_ksp_prompt_template")),
75
+ sa.UniqueConstraint("task", "version", name="uq_ksp_prompt_task_version"),
76
+ )
77
+ op.create_index(op.f("ix_ksp_prompt_template_task"), "ksp_prompt_template", ["task"])
78
+
79
+ op.create_table(
80
+ "ksp_llm_provider",
81
+ sa.Column("id", sa.UUID(), nullable=False),
82
+ sa.Column("name", sa.String(length=64), nullable=False),
83
+ sa.Column(
84
+ "kind",
85
+ sa.Enum(
86
+ "openai_compatible",
87
+ name="ai_provider_kind",
88
+ ),
89
+ nullable=False,
90
+ ),
91
+ sa.Column("base_url", sa.String(length=255), nullable=False),
92
+ sa.Column("credential_id", sa.UUID(), nullable=True),
93
+ sa.Column("model", sa.String(length=128), nullable=False),
94
+ sa.Column("temperature", sa.Float(), nullable=False),
95
+ sa.Column("max_tokens", sa.Integer(), nullable=False),
96
+ sa.Column("timeout_seconds", sa.Integer(), nullable=False),
97
+ sa.Column("enabled", sa.Boolean(), nullable=False),
98
+ sa.Column("is_default", sa.Boolean(), nullable=False),
99
+ sa.Column("last_used_at", sa.DateTime(timezone=True), nullable=True),
100
+ sa.Column("last_error", sa.Text(), nullable=True),
101
+ sa.Column(
102
+ "created_at",
103
+ sa.DateTime(timezone=True),
104
+ server_default=sa.text("now()"),
105
+ nullable=False,
106
+ ),
107
+ sa.Column(
108
+ "updated_at",
109
+ sa.DateTime(timezone=True),
110
+ server_default=sa.text("now()"),
111
+ nullable=False,
112
+ ),
113
+ sa.ForeignKeyConstraint(
114
+ ["credential_id"],
115
+ ["ksp_credential.id"],
116
+ name=op.f("fk_ksp_llm_provider_credential_id_ksp_credential"),
117
+ ondelete="SET NULL",
118
+ ),
119
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_ksp_llm_provider")),
120
+ sa.UniqueConstraint("name", name=op.f("uq_ksp_llm_provider_name")),
121
+ )
122
+
123
+ op.create_table(
124
+ "ksp_ai_analysis",
125
+ sa.Column("id", sa.UUID(), nullable=False),
126
+ sa.Column("repository_id", sa.UUID(), nullable=False),
127
+ sa.Column("subject_type", sa.String(length=32), nullable=False),
128
+ sa.Column("subject_id", sa.UUID(), nullable=False),
129
+ sa.Column("task", _ai_task_enum(), nullable=False),
130
+ sa.Column(
131
+ "status",
132
+ sa.Enum(
133
+ "pending",
134
+ "running",
135
+ "succeeded",
136
+ "failed",
137
+ name="ai_analysis_status",
138
+ ),
139
+ nullable=False,
140
+ ),
141
+ sa.Column("provider_name", sa.String(length=64), nullable=True),
142
+ sa.Column("model", sa.String(length=128), nullable=True),
143
+ sa.Column("input_hash", sa.String(length=64), nullable=True),
144
+ sa.Column("prompt_tokens", sa.Integer(), nullable=False),
145
+ sa.Column("completion_tokens", sa.Integer(), nullable=False),
146
+ sa.Column("result", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
147
+ sa.Column("raw_response", sa.Text(), nullable=True),
148
+ sa.Column("error", sa.Text(), nullable=True),
149
+ sa.Column("triggered_by", sa.UUID(), nullable=True),
150
+ sa.Column("started_at", sa.DateTime(timezone=True), nullable=True),
151
+ sa.Column("finished_at", sa.DateTime(timezone=True), nullable=True),
152
+ sa.Column(
153
+ "created_at",
154
+ sa.DateTime(timezone=True),
155
+ server_default=sa.text("now()"),
156
+ nullable=False,
157
+ ),
158
+ sa.ForeignKeyConstraint(
159
+ ["repository_id"],
160
+ ["ksp_repository.id"],
161
+ name=op.f("fk_ksp_ai_analysis_repository_id_ksp_repository"),
162
+ ondelete="CASCADE",
163
+ ),
164
+ sa.ForeignKeyConstraint(
165
+ ["triggered_by"],
166
+ ["ksp_user.id"],
167
+ name=op.f("fk_ksp_ai_analysis_triggered_by_ksp_user"),
168
+ ondelete="SET NULL",
169
+ ),
170
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_ksp_ai_analysis")),
171
+ sa.UniqueConstraint(
172
+ "subject_type", "subject_id", "task", name="uq_ksp_ai_analysis_subject_task"
173
+ ),
174
+ )
175
+ op.create_index(op.f("ix_ksp_ai_analysis_created_at"), "ksp_ai_analysis", ["created_at"])
176
+ op.create_index(op.f("ix_ksp_ai_analysis_input_hash"), "ksp_ai_analysis", ["input_hash"])
177
+ op.create_index(op.f("ix_ksp_ai_analysis_repository_id"), "ksp_ai_analysis", ["repository_id"])
178
+ op.create_index(op.f("ix_ksp_ai_analysis_status"), "ksp_ai_analysis", ["status"])
179
+ op.create_index(op.f("ix_ksp_ai_analysis_subject_id"), "ksp_ai_analysis", ["subject_id"])
180
+ op.create_index(op.f("ix_ksp_ai_analysis_task"), "ksp_ai_analysis", ["task"])
181
+
182
+ op.create_table(
183
+ "ksp_attention_item",
184
+ sa.Column("id", sa.UUID(), nullable=False),
185
+ sa.Column("repository_id", sa.UUID(), nullable=False),
186
+ sa.Column("subject_type", sa.String(length=32), nullable=False),
187
+ sa.Column("subject_id", sa.UUID(), nullable=False),
188
+ sa.Column("subject_number", sa.BigInteger(), nullable=False),
189
+ sa.Column("subject_title", sa.Text(), nullable=False),
190
+ sa.Column("subject_url", sa.String(length=512), nullable=True),
191
+ sa.Column(
192
+ "rule",
193
+ sa.Enum(
194
+ "stale",
195
+ "review_sla_breach",
196
+ "ci_failed",
197
+ "merge_conflict",
198
+ "needs_issue",
199
+ "cla_denied",
200
+ "cla_pending_long",
201
+ "rejected",
202
+ "binary_file",
203
+ "missing_signed_off",
204
+ "cve_aging",
205
+ "issue_unassigned",
206
+ "issue_stale",
207
+ name="attention_rule",
208
+ ),
209
+ nullable=False,
210
+ ),
211
+ sa.Column(
212
+ "severity",
213
+ sa.Enum(
214
+ "blocker",
215
+ "critical",
216
+ "warning",
217
+ "info",
218
+ name="attention_severity",
219
+ ),
220
+ nullable=False,
221
+ ),
222
+ sa.Column("title", sa.String(length=255), nullable=False),
223
+ sa.Column("detail", sa.Text(), nullable=True),
224
+ sa.Column("evidence", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
225
+ sa.Column(
226
+ "first_detected_at",
227
+ sa.DateTime(timezone=True),
228
+ server_default=sa.text("now()"),
229
+ nullable=False,
230
+ ),
231
+ sa.Column(
232
+ "last_evaluated_at",
233
+ sa.DateTime(timezone=True),
234
+ server_default=sa.text("now()"),
235
+ nullable=False,
236
+ ),
237
+ sa.Column("resolved_at", sa.DateTime(timezone=True), nullable=True),
238
+ sa.Column("acknowledged_by", sa.UUID(), nullable=True),
239
+ sa.Column("acknowledged_at", sa.DateTime(timezone=True), nullable=True),
240
+ sa.ForeignKeyConstraint(
241
+ ["acknowledged_by"],
242
+ ["ksp_user.id"],
243
+ name=op.f("fk_ksp_attention_item_acknowledged_by_ksp_user"),
244
+ ondelete="SET NULL",
245
+ ),
246
+ sa.ForeignKeyConstraint(
247
+ ["repository_id"],
248
+ ["ksp_repository.id"],
249
+ name=op.f("fk_ksp_attention_item_repository_id_ksp_repository"),
250
+ ondelete="CASCADE",
251
+ ),
252
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_ksp_attention_item")),
253
+ sa.UniqueConstraint(
254
+ "subject_type", "subject_id", "rule", name="uq_ksp_attention_subject_rule"
255
+ ),
256
+ )
257
+ op.create_index(
258
+ op.f("ix_ksp_attention_item_repository_id"), "ksp_attention_item", ["repository_id"]
259
+ )
260
+ op.create_index(op.f("ix_ksp_attention_item_rule"), "ksp_attention_item", ["rule"])
261
+ op.create_index(op.f("ix_ksp_attention_item_severity"), "ksp_attention_item", ["severity"])
262
+ op.create_index(op.f("ix_ksp_attention_item_subject_id"), "ksp_attention_item", ["subject_id"])
263
+ op.create_index("ix_ksp_attention_open", "ksp_attention_item", ["resolved_at", "severity"])
264
+
265
+ op.create_table(
266
+ "ksp_classification",
267
+ sa.Column("id", sa.UUID(), nullable=False),
268
+ sa.Column(
269
+ "subject_type",
270
+ sa.Enum(
271
+ "pull_request",
272
+ "issue",
273
+ name="classification_subject_type",
274
+ ),
275
+ nullable=False,
276
+ ),
277
+ sa.Column("subject_id", sa.UUID(), nullable=False),
278
+ sa.Column("repository_id", sa.UUID(), nullable=False),
279
+ sa.Column(
280
+ "kind",
281
+ sa.Enum(
282
+ "cve",
283
+ "backport",
284
+ "bugfix",
285
+ "feature",
286
+ "refactor",
287
+ "docs",
288
+ "cleanup",
289
+ "unknown",
290
+ name="pr_kind",
291
+ ),
292
+ nullable=False,
293
+ ),
294
+ sa.Column("subsystem", sa.String(length=64), nullable=True),
295
+ sa.Column("related_subsystems", postgresql.ARRAY(sa.Text()), nullable=True),
296
+ sa.Column("cve_ids", postgresql.ARRAY(sa.Text()), nullable=True),
297
+ sa.Column("linked_issues", postgresql.ARRAY(sa.BigInteger()), nullable=True),
298
+ sa.Column("confidence", sa.Float(), nullable=False),
299
+ sa.Column(
300
+ "source",
301
+ sa.Enum(
302
+ "rule",
303
+ "ai",
304
+ "manual",
305
+ name="classification_source",
306
+ ),
307
+ nullable=False,
308
+ ),
309
+ sa.Column("rule_name", sa.String(length=64), nullable=True),
310
+ sa.Column("reason", sa.Text(), nullable=True),
311
+ sa.Column("is_override", sa.Boolean(), nullable=False),
312
+ sa.Column("overridden_by", sa.UUID(), nullable=True),
313
+ sa.Column("overridden_at", sa.DateTime(timezone=True), nullable=True),
314
+ sa.Column(
315
+ "created_at",
316
+ sa.DateTime(timezone=True),
317
+ server_default=sa.text("now()"),
318
+ nullable=False,
319
+ ),
320
+ sa.Column(
321
+ "updated_at",
322
+ sa.DateTime(timezone=True),
323
+ server_default=sa.text("now()"),
324
+ nullable=False,
325
+ ),
326
+ sa.ForeignKeyConstraint(
327
+ ["overridden_by"],
328
+ ["ksp_user.id"],
329
+ name=op.f("fk_ksp_classification_overridden_by_ksp_user"),
330
+ ondelete="SET NULL",
331
+ ),
332
+ sa.ForeignKeyConstraint(
333
+ ["repository_id"],
334
+ ["ksp_repository.id"],
335
+ name=op.f("fk_ksp_classification_repository_id_ksp_repository"),
336
+ ondelete="CASCADE",
337
+ ),
338
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_ksp_classification")),
339
+ sa.UniqueConstraint("subject_type", "subject_id", name="uq_ksp_classification_subject"),
340
+ )
341
+ op.create_index(op.f("ix_ksp_classification_cve_ids"), "ksp_classification", ["cve_ids"])
342
+ op.create_index("ix_ksp_classification_kind", "ksp_classification", ["kind"])
343
+ op.create_index(
344
+ op.f("ix_ksp_classification_repository_id"), "ksp_classification", ["repository_id"]
345
+ )
346
+ op.create_index(
347
+ op.f("ix_ksp_classification_subject_id"), "ksp_classification", ["subject_id"]
348
+ )
349
+ op.create_index(op.f("ix_ksp_classification_subsystem"), "ksp_classification", ["subsystem"])
350
+
351
+ op.create_table(
352
+ "ksp_label_sync_state",
353
+ sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
354
+ sa.Column("repository_id", sa.UUID(), nullable=False),
355
+ sa.Column("label_name", sa.String(length=128), nullable=False),
356
+ sa.Column(
357
+ "created_at",
358
+ sa.DateTime(timezone=True),
359
+ server_default=sa.text("now()"),
360
+ nullable=False,
361
+ ),
362
+ sa.ForeignKeyConstraint(
363
+ ["repository_id"],
364
+ ["ksp_repository.id"],
365
+ name=op.f("fk_ksp_label_sync_state_repository_id_ksp_repository"),
366
+ ondelete="CASCADE",
367
+ ),
368
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_ksp_label_sync_state")),
369
+ sa.UniqueConstraint("repository_id", "label_name", name="uq_ksp_label_sync_repo_name"),
370
+ )
371
+
372
+ op.create_table(
373
+ "ksp_task_routing",
374
+ sa.Column("id", sa.UUID(), nullable=False),
375
+ sa.Column("task", _ai_task_enum(), nullable=False),
376
+ sa.Column("provider_id", sa.UUID(), nullable=False),
377
+ sa.Column("fallback_provider_id", sa.UUID(), nullable=True),
378
+ sa.Column("system_prompt", sa.Text(), nullable=True),
379
+ sa.Column("enabled", sa.Boolean(), nullable=False),
380
+ sa.Column(
381
+ "created_at",
382
+ sa.DateTime(timezone=True),
383
+ server_default=sa.text("now()"),
384
+ nullable=False,
385
+ ),
386
+ sa.Column(
387
+ "updated_at",
388
+ sa.DateTime(timezone=True),
389
+ server_default=sa.text("now()"),
390
+ nullable=False,
391
+ ),
392
+ sa.ForeignKeyConstraint(
393
+ ["fallback_provider_id"],
394
+ ["ksp_llm_provider.id"],
395
+ name=op.f("fk_ksp_task_routing_fallback_provider_id_ksp_llm_provider"),
396
+ ondelete="SET NULL",
397
+ ),
398
+ sa.ForeignKeyConstraint(
399
+ ["provider_id"],
400
+ ["ksp_llm_provider.id"],
401
+ name=op.f("fk_ksp_task_routing_provider_id_ksp_llm_provider"),
402
+ ondelete="CASCADE",
403
+ ),
404
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_ksp_task_routing")),
405
+ sa.UniqueConstraint("task", name=op.f("uq_ksp_task_routing_task")),
406
+ )
407
+
408
+
409
+ def downgrade() -> None:
410
+ op.drop_table("ksp_task_routing")
411
+ op.drop_table("ksp_label_sync_state")
412
+ op.drop_index(op.f("ix_ksp_classification_subsystem"), table_name="ksp_classification")
413
+ op.drop_index(op.f("ix_ksp_classification_subject_id"), table_name="ksp_classification")
414
+ op.drop_index(op.f("ix_ksp_classification_repository_id"), table_name="ksp_classification")
415
+ op.drop_index("ix_ksp_classification_kind", table_name="ksp_classification")
416
+ op.drop_index(op.f("ix_ksp_classification_cve_ids"), table_name="ksp_classification")
417
+ op.drop_table("ksp_classification")
418
+ op.drop_index("ix_ksp_attention_open", table_name="ksp_attention_item")
419
+ op.drop_index(op.f("ix_ksp_attention_item_subject_id"), table_name="ksp_attention_item")
420
+ op.drop_index(op.f("ix_ksp_attention_item_severity"), table_name="ksp_attention_item")
421
+ op.drop_index(op.f("ix_ksp_attention_item_rule"), table_name="ksp_attention_item")
422
+ op.drop_index(op.f("ix_ksp_attention_item_repository_id"), table_name="ksp_attention_item")
423
+ op.drop_table("ksp_attention_item")
424
+ op.drop_index(op.f("ix_ksp_ai_analysis_task"), table_name="ksp_ai_analysis")
425
+ op.drop_index(op.f("ix_ksp_ai_analysis_subject_id"), table_name="ksp_ai_analysis")
426
+ op.drop_index(op.f("ix_ksp_ai_analysis_status"), table_name="ksp_ai_analysis")
427
+ op.drop_index(op.f("ix_ksp_ai_analysis_repository_id"), table_name="ksp_ai_analysis")
428
+ op.drop_index(op.f("ix_ksp_ai_analysis_input_hash"), table_name="ksp_ai_analysis")
429
+ op.drop_index(op.f("ix_ksp_ai_analysis_created_at"), table_name="ksp_ai_analysis")
430
+ op.drop_table("ksp_ai_analysis")
431
+ op.drop_table("ksp_llm_provider")
432
+ op.drop_index(op.f("ix_ksp_prompt_template_task"), table_name="ksp_prompt_template")
433
+ op.drop_table("ksp_prompt_template")
434
+
435
+ # 枚举类型由本迁移创建,且为多表共用,需显式删除
436
+ postgresql.ENUM(*AI_TASK_VALUES, name="ai_task").drop(op.get_bind(), checkfirst=True)
@@ -0,0 +1,72 @@
1
+ """分支登记表按仓库区分
2
+
3
+ Revision ID: a1c7d90e4b52
4
+ Revises: fcf3c186d63b
5
+ Create Date: 2026-09-17
6
+
7
+ sig-info.yaml 的 repo_branch 段里,每条分支都写着它属于哪个仓库,但写入时
8
+ 只取了分支名 —— 于是这张表成了全局表。纳管第二个仓库后,OLK-6.6 会出现在
9
+ src-openeuler/kernel 的分支页上;而 master、openEuler-24.03-LTS-SP4 这类
10
+ 两个仓库都有的名字,页面上根本分不出指的是哪一个。
11
+
12
+ 已存在的行归属不明,一律留空:回填需要有仓库名才能对上,而旧数据里没有。
13
+ 下一次 sig_info 同步会把它们写正确。
14
+ """
15
+
16
+ from collections.abc import Sequence
17
+
18
+ import sqlalchemy as sa
19
+ from alembic import op
20
+
21
+ revision: str = "a1c7d90e4b52"
22
+ down_revision: str | None = "fcf3c186d63b"
23
+ branch_labels: str | Sequence[str] | None = None
24
+ depends_on: str | Sequence[str] | None = None
25
+
26
+
27
+ def upgrade() -> None:
28
+ op.add_column(
29
+ "ksp_release_branch",
30
+ sa.Column("repository_id", sa.UUID(), nullable=True),
31
+ )
32
+ op.create_foreign_key(
33
+ "fk_ksp_release_branch_repository_id_ksp_repository",
34
+ "ksp_release_branch",
35
+ "ksp_repository",
36
+ ["repository_id"],
37
+ ["id"],
38
+ ondelete="CASCADE",
39
+ )
40
+ # 唯一约束由 name 改成 (repository_id, name):同名分支在两个仓库里是两件事
41
+ op.drop_constraint("uq_ksp_release_branch_name", "ksp_release_branch", type_="unique")
42
+ op.create_unique_constraint(
43
+ "uq_ksp_release_branch_repo_name",
44
+ "ksp_release_branch",
45
+ ["repository_id", "name"],
46
+ )
47
+ op.create_index(
48
+ "ix_ksp_release_branch_repository_id",
49
+ "ksp_release_branch",
50
+ ["repository_id"],
51
+ )
52
+
53
+ # 旧行的归属无从判断(原先根本没存),而同一批分支会在下一次 sig_info
54
+ # 同步时带着正确归属重新写入。留着它们只会变成永远不会显示的孤儿 ——
55
+ # 查询按仓库过滤,归属为空的行谁也看不到。
56
+ #
57
+ # 删之前核对过:这些行没有人工填写的内容,state 全是默认值,
58
+ # note 与 aliases 全为空。若某个环境里有人填过生命周期说明,
59
+ # 这一步会连说明一起删掉 —— 那种情况需要先把列值搬到新行上。
60
+ op.execute("DELETE FROM ksp_release_branch WHERE repository_id IS NULL")
61
+
62
+
63
+ def downgrade() -> None:
64
+ op.drop_index("ix_ksp_release_branch_repository_id", table_name="ksp_release_branch")
65
+ op.drop_constraint("uq_ksp_release_branch_repo_name", "ksp_release_branch", type_="unique")
66
+ op.create_unique_constraint("uq_ksp_release_branch_name", "ksp_release_branch", ["name"])
67
+ op.drop_constraint(
68
+ "fk_ksp_release_branch_repository_id_ksp_repository",
69
+ "ksp_release_branch",
70
+ type_="foreignkey",
71
+ )
72
+ op.drop_column("ksp_release_branch", "repository_id")
@@ -0,0 +1,43 @@
1
+ """扩展 pr_kind 的取值
2
+
3
+ Revision ID: b41c9d7e5f28
4
+ Revises: 350e2d9b6553
5
+
6
+ 新增五类:新增驱动、驱动更新、处理器/SoC 支持、自研特性、性能优化。
7
+
8
+ 依据来自实测:作者在提交信息里自填 ``category:`` 与 ``<X> inclusion``
9
+ (1264 条提交中 1235 条带 category),以及厂商与自研子系统的命名习惯。
10
+ 原来的七类无法区分"回合自上游的补丁"和"厂商新驱动"—— 而这两者
11
+ 的评审方式完全不同,混在一起维护者没法按类别分流。
12
+
13
+ Alembic 不会自动检测枚举取值的新增,因此这里手写。
14
+ PostgreSQL 12 起允许在事务中 ADD VALUE,只要同一事务内不使用该新值,
15
+ 本迁移正是如此(只加值,不改数据)。
16
+ """
17
+
18
+ from collections.abc import Sequence
19
+
20
+ from alembic import op
21
+
22
+ revision: str = "b41c9d7e5f28"
23
+ down_revision: str | None = "350e2d9b6553"
24
+ branch_labels: str | Sequence[str] | None = None
25
+ depends_on: str | Sequence[str] | None = None
26
+
27
+ # 顺序需与 models.classification.PRKind 保持一致,便于人工比对
28
+ NEW_VALUES = ("driver_new", "driver_update", "soc_support", "out_of_tree", "perf")
29
+
30
+
31
+ def upgrade() -> None:
32
+ for value in NEW_VALUES:
33
+ op.execute(f"ALTER TYPE pr_kind ADD VALUE IF NOT EXISTS '{value}'")
34
+
35
+
36
+ def downgrade() -> None:
37
+ # PostgreSQL 不支持从枚举中删除取值。要回退只能重建类型并改写
38
+ # 所有引用列,代价高且需在无数据依赖时进行。
39
+ # 因此这里显式报错,而不是留下一个"看起来成功"的空操作。
40
+ raise NotImplementedError(
41
+ "pr_kind 的取值无法删除(PostgreSQL 限制)。"
42
+ "如需回退,请重建类型并重写 ksp_classification.kind 列。"
43
+ )
@@ -0,0 +1,40 @@
1
+ """pr_kind 增加「版本发布」一类
2
+
3
+ Revision ID: c8e4f2a71b93
4
+ Revises: a1c7d90e4b52
5
+
6
+ 纳管 src-openeuler/kernel 之后发现的:那个仓库的 PR 是版本发布动作
7
+ (``release 6.6.0-173.0.0.155``),而现有十三类是针对内核补丁的内容划分,
8
+ 没有一类装得下它 —— 实测 1309 个 PR 里 1156 个落进 unknown,界面上看
9
+ 像是分类没跑。同一条规则在 openeuler/kernel 上只命中 1 个 PR,
10
+ 所以加这一类不会影响那边的统计。
11
+
12
+ Alembic 不会自动检测枚举取值的新增,因此这里手写。
13
+ PostgreSQL 12 起允许在事务中 ADD VALUE,只要同一事务内不使用该新值,
14
+ 本迁移正是如此(只加值,不改数据)。
15
+ """
16
+
17
+ from collections.abc import Sequence
18
+
19
+ from alembic import op
20
+
21
+ revision: str = "c8e4f2a71b93"
22
+ down_revision: str | None = "a1c7d90e4b52"
23
+ branch_labels: str | Sequence[str] | None = None
24
+ depends_on: str | Sequence[str] | None = None
25
+
26
+ NEW_VALUE = "release"
27
+
28
+
29
+ def upgrade() -> None:
30
+ op.execute(f"ALTER TYPE pr_kind ADD VALUE IF NOT EXISTS '{NEW_VALUE}'")
31
+
32
+
33
+ def downgrade() -> None:
34
+ # PostgreSQL 不支持从枚举中删除取值。要回退只能重建类型并改写
35
+ # 所有引用列,代价高且需在无数据依赖时进行。
36
+ # 因此这里显式报错,而不是留下一个"看起来成功"的空操作。
37
+ raise NotImplementedError(
38
+ "pr_kind 的取值无法删除(PostgreSQL 限制)。"
39
+ "如需回退,请重建类型并重写 ksp_classification.kind 列。"
40
+ )
@@ -0,0 +1,44 @@
1
+ """为 Pull Request 添加增量同步所需的派生列
2
+
3
+ Revision ID: d1275091dfdc
4
+ Revises: 5a31ade90136
5
+ Create Date: 2026-09-16
6
+
7
+ - needs_detail:内容变化后才置位,把详情同步量从"每轮全部 PR"
8
+ 降到"每轮仅变化项",否则每轮会产生数千次上游请求
9
+ - gate_stage:由标签推导的门禁阶段,供列表按"待评审/可合入"筛选与排序。
10
+ 属于刻意的反规范化 —— 1000+ 条记录无法在应用层逐条推导再分页
11
+ """
12
+
13
+ from collections.abc import Sequence
14
+
15
+ import sqlalchemy as sa
16
+ from alembic import op
17
+
18
+ revision: str = "d1275091dfdc"
19
+ down_revision: str | None = "5a31ade90136"
20
+ branch_labels: str | Sequence[str] | None = None
21
+ depends_on: str | Sequence[str] | None = None
22
+
23
+
24
+ def upgrade() -> None:
25
+ op.add_column(
26
+ "ksp_pull_request",
27
+ sa.Column("needs_detail", sa.Boolean(), server_default=sa.text("true"), nullable=False),
28
+ )
29
+ op.add_column(
30
+ "ksp_pull_request",
31
+ sa.Column(
32
+ "gate_stage",
33
+ sa.String(32),
34
+ server_default="cla_pending",
35
+ nullable=False,
36
+ ),
37
+ )
38
+ op.create_index("ix_ksp_pull_request_gate_stage", "ksp_pull_request", ["gate_stage"])
39
+
40
+
41
+ def downgrade() -> None:
42
+ op.drop_index("ix_ksp_pull_request_gate_stage", table_name="ksp_pull_request")
43
+ op.drop_column("ksp_pull_request", "gate_stage")
44
+ op.drop_column("ksp_pull_request", "needs_detail")