@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,144 @@
1
+ """分类结果。
2
+
3
+ 分类走两级:规则引擎优先(确定性、零成本、可解释),规则未命中或置信度低
4
+ 时才调用 AI。人工覆盖过的结果不会被自动任务覆盖回去 ——
5
+ 这是对使用者预期的基本尊重。
6
+ """
7
+
8
+ import uuid
9
+ from datetime import datetime
10
+ from enum import StrEnum
11
+
12
+ from sqlalchemy import (
13
+ BigInteger,
14
+ Boolean,
15
+ DateTime,
16
+ Enum,
17
+ Float,
18
+ ForeignKey,
19
+ Index,
20
+ String,
21
+ Text,
22
+ UniqueConstraint,
23
+ func,
24
+ )
25
+ from sqlalchemy.dialects.postgresql import ARRAY, UUID
26
+ from sqlalchemy.orm import Mapped, mapped_column
27
+
28
+ from app.models.base import Base, TimestampMixin
29
+
30
+
31
+ class SubjectType(StrEnum):
32
+ PULL_REQUEST = "pull_request"
33
+ ISSUE = "issue"
34
+
35
+
36
+ class PRKind(StrEnum):
37
+ """PR/Issue 的内容类别。
38
+
39
+ 取值与 ``app.domain.classification.Kind`` 一一对应,两侧必须一致 ——
40
+ 规则算出的值写不进数据库的表现是"分类静默失败"。
41
+ """
42
+
43
+ CVE = "cve"
44
+ RELEASE = "release"
45
+ BACKPORT = "backport"
46
+ DRIVER_NEW = "driver_new"
47
+ DRIVER_UPDATE = "driver_update"
48
+ SOC_SUPPORT = "soc_support"
49
+ OUT_OF_TREE = "out_of_tree"
50
+ BUGFIX = "bugfix"
51
+ FEATURE = "feature"
52
+ PERF = "perf"
53
+ REFACTOR = "refactor"
54
+ DOC = "docs"
55
+ CLEANUP = "cleanup"
56
+ UNKNOWN = "unknown"
57
+
58
+
59
+ class ClassificationSource(StrEnum):
60
+ RULE = "rule"
61
+ AI = "ai"
62
+ MANUAL = "manual"
63
+
64
+
65
+ class Classification(Base, TimestampMixin):
66
+ __tablename__ = "ksp_classification"
67
+ __table_args__ = (
68
+ UniqueConstraint("subject_type", "subject_id", name="uq_ksp_classification_subject"),
69
+ Index("ix_ksp_classification_kind", "kind"),
70
+ )
71
+
72
+ id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
73
+ subject_type: Mapped[SubjectType] = mapped_column(
74
+ Enum(
75
+ SubjectType,
76
+ name="classification_subject_type",
77
+ values_callable=lambda e: [m.value for m in e],
78
+ ),
79
+ nullable=False,
80
+ )
81
+ subject_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False, index=True)
82
+ repository_id: Mapped[uuid.UUID] = mapped_column(
83
+ UUID(as_uuid=True),
84
+ ForeignKey("ksp_repository.id", ondelete="CASCADE"),
85
+ nullable=False,
86
+ index=True,
87
+ )
88
+
89
+ kind: Mapped[PRKind] = mapped_column(
90
+ Enum(PRKind, name="pr_kind", values_callable=lambda e: [m.value for m in e]),
91
+ nullable=False,
92
+ default=PRKind.UNKNOWN,
93
+ )
94
+ # 从变更文件路径推导的子系统,如 net / mm / arm64
95
+ subsystem: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
96
+ related_subsystems: Mapped[list[str] | None] = mapped_column(ARRAY(Text), nullable=True)
97
+
98
+ cve_ids: Mapped[list[str] | None] = mapped_column(ARRAY(Text), nullable=True, index=True)
99
+ # 从正文 fixes #N / resolves #N 解析出的关联 Issue 编号
100
+ linked_issues: Mapped[list[int] | None] = mapped_column(ARRAY(BigInteger), nullable=True)
101
+
102
+ confidence: Mapped[float] = mapped_column(Float, nullable=False, default=1.0)
103
+ source: Mapped[ClassificationSource] = mapped_column(
104
+ Enum(
105
+ ClassificationSource,
106
+ name="classification_source",
107
+ values_callable=lambda e: [m.value for m in e],
108
+ ),
109
+ nullable=False,
110
+ default=ClassificationSource.RULE,
111
+ )
112
+ # 命中的规则名,便于排查"为什么被分到这一类"
113
+ rule_name: Mapped[str | None] = mapped_column(String(64), nullable=True)
114
+ reason: Mapped[str | None] = mapped_column(Text, nullable=True)
115
+
116
+ # 人工覆盖后置位,自动任务不再改写
117
+ is_override: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
118
+ overridden_by: Mapped[uuid.UUID | None] = mapped_column(
119
+ UUID(as_uuid=True), ForeignKey("ksp_user.id", ondelete="SET NULL"), nullable=True
120
+ )
121
+ overridden_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
122
+
123
+ def __repr__(self) -> str:
124
+ return f"<Classification {self.subject_type.value}:{self.kind.value}>"
125
+
126
+
127
+ class LabelSyncState(Base):
128
+ """记录已建到 AtomGit 的标签,避免重复创建与重复写入。"""
129
+
130
+ __tablename__ = "ksp_label_sync_state"
131
+ __table_args__ = (
132
+ UniqueConstraint("repository_id", "label_name", name="uq_ksp_label_sync_repo_name"),
133
+ )
134
+
135
+ id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
136
+ repository_id: Mapped[uuid.UUID] = mapped_column(
137
+ UUID(as_uuid=True),
138
+ ForeignKey("ksp_repository.id", ondelete="CASCADE"),
139
+ nullable=False,
140
+ )
141
+ label_name: Mapped[str] = mapped_column(String(128), nullable=False)
142
+ created_at: Mapped[datetime] = mapped_column(
143
+ DateTime(timezone=True), server_default=func.now(), nullable=False
144
+ )
@@ -0,0 +1,56 @@
1
+ """外部凭据(AtomGit token、LLM API Key 等)。
2
+
3
+ 安全约束:明文只以 AES-256-GCM 密文形态落库,密钥由 KSC_SECRET_KEY
4
+ 经 HKDF 派生。界面永不回显明文,只展示指纹供人工比对。
5
+ """
6
+
7
+ import uuid
8
+ from datetime import datetime
9
+ from enum import StrEnum
10
+
11
+ from sqlalchemy import DateTime, Enum, ForeignKey, LargeBinary, String
12
+ from sqlalchemy.dialects.postgresql import JSONB, UUID
13
+ from sqlalchemy.orm import Mapped, mapped_column
14
+
15
+ from app.models.base import Base, TimestampMixin
16
+
17
+
18
+ class CredentialKind(StrEnum):
19
+ ATOMGIT_TOKEN = "atomgit_token"
20
+ LLM_API_KEY = "llm_api_key"
21
+
22
+
23
+ class Credential(Base, TimestampMixin):
24
+ __tablename__ = "ksp_credential"
25
+
26
+ id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
27
+ kind: Mapped[CredentialKind] = mapped_column(
28
+ Enum(
29
+ CredentialKind,
30
+ name="credential_kind",
31
+ values_callable=lambda e: [m.value for m in e],
32
+ ),
33
+ nullable=False,
34
+ index=True,
35
+ )
36
+ name: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
37
+
38
+ # 密文(AES-256-GCM,格式 v1:<nonce>:<ciphertext>)
39
+ encrypted_value: Mapped[bytes] = mapped_column(LargeBinary, nullable=False)
40
+ # SHA-256 前 16 字符,用于界面展示"已配置哪一份"而无需回显明文
41
+ fingerprint: Mapped[str] = mapped_column(String(32), nullable=False)
42
+
43
+ # 非敏感附加信息,如 token 对应的 AtomGit 账号名
44
+ meta: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
45
+
46
+ last_verified_at: Mapped[datetime | None] = mapped_column(
47
+ DateTime(timezone=True), nullable=True
48
+ )
49
+ last_verify_error: Mapped[str | None] = mapped_column(String(500), nullable=True)
50
+
51
+ created_by: Mapped[uuid.UUID | None] = mapped_column(
52
+ UUID(as_uuid=True), ForeignKey("ksp_user.id", ondelete="SET NULL"), nullable=True
53
+ )
54
+
55
+ def __repr__(self) -> str:
56
+ return f"<Credential {self.name} kind={self.kind.value} fp={self.fingerprint}>"
@@ -0,0 +1,132 @@
1
+ """Issue 及其关联数据。"""
2
+
3
+ import uuid
4
+ from datetime import datetime
5
+ from enum import StrEnum
6
+
7
+ from sqlalchemy import (
8
+ BigInteger,
9
+ DateTime,
10
+ Enum,
11
+ ForeignKey,
12
+ Index,
13
+ Integer,
14
+ String,
15
+ Text,
16
+ UniqueConstraint,
17
+ func,
18
+ )
19
+ from sqlalchemy.dialects.postgresql import ARRAY, JSONB, UUID
20
+ from sqlalchemy.orm import Mapped, mapped_column
21
+
22
+ from app.models.base import Base, TimestampMixin
23
+
24
+
25
+ class IssueType(StrEnum):
26
+ """AtomGit 原生提供 issue_type,无需从标签推断。"""
27
+
28
+ BUG = "内核缺陷"
29
+ FEATURE = "内核需求"
30
+ TASK = "任务"
31
+ UNKNOWN = "未知"
32
+
33
+
34
+ class IssueLinkSource(StrEnum):
35
+ PR_BODY = "pr_body"
36
+ ISSUE_BODY = "issue_body"
37
+ ATOMGIT_LINK = "atomgit_link"
38
+ MANUAL = "manual"
39
+
40
+
41
+ class Issue(Base, TimestampMixin):
42
+ __tablename__ = "ksp_issue"
43
+ __table_args__ = (
44
+ UniqueConstraint("repository_id", "number", name="uq_ksp_issue_repo_number"),
45
+ Index("ix_ksp_issue_label_names", "label_names", postgresql_using="gin"),
46
+ )
47
+
48
+ id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
49
+ repository_id: Mapped[uuid.UUID] = mapped_column(
50
+ UUID(as_uuid=True),
51
+ ForeignKey("ksp_repository.id", ondelete="CASCADE"),
52
+ nullable=False,
53
+ index=True,
54
+ )
55
+ number: Mapped[int] = mapped_column(Integer, nullable=False)
56
+
57
+ title: Mapped[str] = mapped_column(Text, nullable=False)
58
+ body: Mapped[str | None] = mapped_column(Text, nullable=True)
59
+ state: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
60
+
61
+ # AtomGit 的结构化字段,比用标签猜测更可靠
62
+ issue_type: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
63
+ issue_state: Mapped[str | None] = mapped_column(String(64), nullable=True)
64
+ priority: Mapped[int | None] = mapped_column(Integer, nullable=True)
65
+ priority_label: Mapped[str | None] = mapped_column(String(64), nullable=True)
66
+
67
+ author_login: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
68
+ assignee_login: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
69
+
70
+ labels: Mapped[list | None] = mapped_column(JSONB, nullable=True)
71
+ label_names: Mapped[list[str] | None] = mapped_column(ARRAY(Text), nullable=True)
72
+
73
+ comments_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
74
+
75
+ atomgit_created_at: Mapped[datetime | None] = mapped_column(
76
+ DateTime(timezone=True), nullable=True
77
+ )
78
+ atomgit_updated_at: Mapped[datetime | None] = mapped_column(
79
+ DateTime(timezone=True), nullable=True
80
+ )
81
+ finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
82
+ html_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
83
+
84
+ first_seen_at: Mapped[datetime] = mapped_column(
85
+ DateTime(timezone=True), server_default=func.now(), nullable=False
86
+ )
87
+ last_synced_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
88
+ content_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
89
+
90
+ def __repr__(self) -> str:
91
+ return f"<Issue #{self.number} {self.issue_type}>"
92
+
93
+
94
+ class PRIssueLink(Base):
95
+ """PR ↔ Issue 关联。
96
+
97
+ 优先使用 AtomGit 的显式关联接口;正文正则匹配作为补充,
98
+ 因此用 confidence 区分可信度。
99
+ """
100
+
101
+ __tablename__ = "ksp_pr_issue_link"
102
+ __table_args__ = (
103
+ UniqueConstraint("pull_request_id", "issue_number", name="uq_ksp_pr_issue_link"),
104
+ )
105
+
106
+ id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
107
+ pull_request_id: Mapped[uuid.UUID] = mapped_column(
108
+ UUID(as_uuid=True),
109
+ ForeignKey("ksp_pull_request.id", ondelete="CASCADE"),
110
+ nullable=False,
111
+ index=True,
112
+ )
113
+ repository_id: Mapped[uuid.UUID] = mapped_column(
114
+ UUID(as_uuid=True),
115
+ ForeignKey("ksp_repository.id", ondelete="CASCADE"),
116
+ nullable=False,
117
+ )
118
+ issue_number: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
119
+
120
+ source: Mapped[IssueLinkSource] = mapped_column(
121
+ Enum(
122
+ IssueLinkSource,
123
+ name="issue_link_source",
124
+ values_callable=lambda e: [m.value for m in e],
125
+ ),
126
+ nullable=False,
127
+ )
128
+ # 显式关联为 1.0,正文正则匹配略低,便于按可信度筛选
129
+ confidence: Mapped[float] = mapped_column(nullable=False, default=1.0)
130
+ created_at: Mapped[datetime] = mapped_column(
131
+ DateTime(timezone=True), server_default=func.now(), nullable=False
132
+ )
@@ -0,0 +1,189 @@
1
+ """SIG 例会及其版本进展报告。
2
+
3
+ 数据源是 Etherpad 上的活文档,按时间倒序累积。这不是一个"同步一次就完"
4
+ 的数据源 —— 每次例会前主持人在同一页追加新一节,因此同步是按页整体解析后
5
+ 按日期 upsert。
6
+
7
+ 会议里最有价值的是「进展update」议题下的版本进展报告:tag 区间、合入补丁数、
8
+ CVE 清单、按类别归好的补丁列表。它同时充当双周合入总结与各版本的 release notes,
9
+ 所以单独建表而不是塞进一个 JSON 字段 —— 补丁条目要能按 PR 号反查
10
+ ("这个 PR 是在哪次例会上宣布合入的")。
11
+ """
12
+
13
+ import uuid
14
+ from datetime import date, datetime
15
+ from enum import StrEnum
16
+
17
+ from sqlalchemy import (
18
+ Date,
19
+ DateTime,
20
+ ForeignKey,
21
+ Index,
22
+ Integer,
23
+ String,
24
+ Text,
25
+ UniqueConstraint,
26
+ )
27
+ from sqlalchemy.dialects.postgresql import ARRAY, JSONB, UUID
28
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
29
+
30
+ from app.models.base import Base, TimestampMixin
31
+
32
+
33
+ class MeetingStatus(StrEnum):
34
+ SCHEDULED = "scheduled"
35
+ DONE = "done"
36
+ CANCELLED = "cancelled"
37
+
38
+
39
+ class Meeting(Base, TimestampMixin):
40
+ __tablename__ = "ksp_meeting"
41
+ __table_args__ = (
42
+ # 一场会议一个日期。唯一约束本身建了索引,不再重复加。
43
+ UniqueConstraint("held_on", name="uq_ksp_meeting_held_on"),
44
+ )
45
+
46
+ id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
47
+
48
+ held_on: Mapped[date] = mapped_column(Date, nullable=False)
49
+ start_time: Mapped[str | None] = mapped_column(String(8), nullable=True)
50
+ title: Mapped[str] = mapped_column(String(128), nullable=False)
51
+
52
+ host: Mapped[str | None] = mapped_column(String(64), nullable=True)
53
+ next_host: Mapped[str | None] = mapped_column(String(64), nullable=True)
54
+ host_order: Mapped[list[str] | None] = mapped_column(ARRAY(String(64)), nullable=True)
55
+
56
+ meeting_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
57
+ minutes_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
58
+
59
+ # 与 PullRequest.gate_stage 一致:存字符串而非枚举,便于以后加取值
60
+ status: Mapped[str] = mapped_column(
61
+ String(16), nullable=False, default=MeetingStatus.SCHEDULED.value, index=True
62
+ )
63
+ legacy: Mapped[str | None] = mapped_column(Text, nullable=True)
64
+
65
+ source_url: Mapped[str] = mapped_column(String(512), nullable=False)
66
+ source_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
67
+ """本节纪要内容指纹。Etherpad 整页同步,未变化的会议不必重写。"""
68
+
69
+ last_synced_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
70
+
71
+ agendas: Mapped[list["MeetingAgenda"]] = relationship(
72
+ back_populates="meeting",
73
+ cascade="all, delete-orphan",
74
+ order_by="MeetingAgenda.sequence",
75
+ )
76
+ reports: Mapped[list["ReleaseReport"]] = relationship(
77
+ back_populates="meeting",
78
+ cascade="all, delete-orphan",
79
+ )
80
+
81
+ def __repr__(self) -> str:
82
+ return f"<Meeting {self.held_on} {self.title}>"
83
+
84
+
85
+ class MeetingAgenda(Base, TimestampMixin):
86
+ __tablename__ = "ksp_meeting_agenda"
87
+ __table_args__ = (
88
+ # 唯一约束的首列就是 meeting_id,按会议取议题走这个索引,
89
+ # 再给 meeting_id 单加一个索引是重复的
90
+ UniqueConstraint("meeting_id", "sequence", name="uq_ksp_meeting_agenda_seq"),
91
+ Index("ix_ksp_meeting_agenda_owner", "owner"),
92
+ )
93
+
94
+ id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
95
+ meeting_id: Mapped[uuid.UUID] = mapped_column(
96
+ UUID(as_uuid=True),
97
+ ForeignKey("ksp_meeting.id", ondelete="CASCADE"),
98
+ nullable=False,
99
+ )
100
+
101
+ sequence: Mapped[int] = mapped_column(Integer, nullable=False)
102
+ """出现顺序。议题编号在纪要里会重复(实测两处「议题一」),不能当键。"""
103
+
104
+ num: Mapped[str | None] = mapped_column(String(8), nullable=True)
105
+ title: Mapped[str] = mapped_column(Text, nullable=False)
106
+ owner: Mapped[str | None] = mapped_column(String(64), nullable=True)
107
+ body: Mapped[str | None] = mapped_column(Text, nullable=True)
108
+
109
+ pull_numbers: Mapped[list[int] | None] = mapped_column(ARRAY(Integer), nullable=True)
110
+ issue_numbers: Mapped[list[int] | None] = mapped_column(ARRAY(Integer), nullable=True)
111
+
112
+ meeting: Mapped[Meeting] = relationship(back_populates="agendas")
113
+
114
+ def __repr__(self) -> str:
115
+ return f"<Agenda {self.sequence} {self.title[:24]}>"
116
+
117
+
118
+ class ReleaseReport(Base, TimestampMixin):
119
+ """一次例会上一个分支的版本进展报告。
120
+
121
+ 对应纪要里的一段 ``【OLK-6.6】...``。既是双周合入总结,也是该版本的
122
+ release notes —— 它给出的正是"这半个月合入了什么"。
123
+ """
124
+
125
+ __tablename__ = "ksp_release_report"
126
+ __table_args__ = (
127
+ # 同上:唯一约束首列覆盖了按会议查询
128
+ UniqueConstraint("meeting_id", "branch", name="uq_ksp_release_report_branch"),
129
+ Index("ix_ksp_release_report_branch", "branch"),
130
+ )
131
+
132
+ id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
133
+ meeting_id: Mapped[uuid.UUID] = mapped_column(
134
+ UUID(as_uuid=True),
135
+ ForeignKey("ksp_meeting.id", ondelete="CASCADE"),
136
+ nullable=False,
137
+ )
138
+
139
+ branch: Mapped[str] = mapped_column(String(64), nullable=False)
140
+ """规范化后的分支名,与 ``domain.release`` 一致。"""
141
+
142
+ from_tag: Mapped[str | None] = mapped_column(String(32), nullable=True)
143
+ to_tag: Mapped[str | None] = mapped_column(String(32), nullable=True)
144
+
145
+ patch_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
146
+ """作者自报的合入补丁数。与 ``listed_count`` 不同,后者是正文里真正列出的条数。"""
147
+
148
+ listed_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
149
+
150
+ iso_urls: Mapped[list[str] | None] = mapped_column(ARRAY(String(512)), nullable=True)
151
+ cve_ids: Mapped[list[str] | None] = mapped_column(ARRAY(String(32)), nullable=True)
152
+
153
+ groups: Mapped[list | None] = mapped_column(JSONB, nullable=True)
154
+ """分组元信息(名称与自报条数)。条目本身在 ``ReleaseReportPatch``。"""
155
+
156
+ meeting: Mapped[Meeting] = relationship(back_populates="reports")
157
+
158
+ def __repr__(self) -> str:
159
+ return f"<ReleaseReport {self.branch} {self.from_tag}..{self.to_tag}>"
160
+
161
+
162
+ class ReleaseReportPatch(Base, TimestampMixin):
163
+ """进展报告里列出的一条补丁。
164
+
165
+ 单独建表是为了能按 ``pull_number`` 反查:PR 详情页可以显示
166
+ "该 PR 在 2026-09-04 的例会上宣布合入",这是把会议与代码连起来的那根线。
167
+ """
168
+
169
+ __tablename__ = "ksp_release_report_patch"
170
+ __table_args__ = (
171
+ Index("ix_ksp_release_report_patch_pull", "pull_number"),
172
+ Index("ix_ksp_release_report_patch_report", "report_id", "sequence"),
173
+ )
174
+
175
+ id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
176
+ report_id: Mapped[uuid.UUID] = mapped_column(
177
+ UUID(as_uuid=True),
178
+ ForeignKey("ksp_release_report.id", ondelete="CASCADE"),
179
+ nullable=False,
180
+ )
181
+
182
+ sequence: Mapped[int] = mapped_column(Integer, nullable=False)
183
+ group_name: Mapped[str | None] = mapped_column(String(64), nullable=True)
184
+ title: Mapped[str] = mapped_column(Text, nullable=False)
185
+ pull_number: Mapped[int | None] = mapped_column(Integer, nullable=True)
186
+ url: Mapped[str | None] = mapped_column(String(512), nullable=True)
187
+
188
+ def __repr__(self) -> str:
189
+ return f"<ReportPatch {self.pull_number} {self.title[:24]}>"