@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.
- package/.env.example +50 -0
- package/LICENSE +127 -0
- package/Makefile +66 -0
- package/README.md +279 -0
- package/backend/.dockerignore +12 -0
- package/backend/Dockerfile +43 -0
- package/backend/alembic/env.py +75 -0
- package/backend/alembic/script.py.mako +25 -0
- package/backend/alembic/versions/0001_initial.py +88 -0
- package/backend/alembic/versions/350e2d9b6553_add_pr_comment_table.py +48 -0
- package/backend/alembic/versions/4f8342e727fe_sig_info_roster_fields_and_branch_.py +39 -0
- package/backend/alembic/versions/5a31ade90136_add_repository_credential_pull_request_.py +293 -0
- package/backend/alembic/versions/87a008142f17_add_classification_attention_and_ai_.py +436 -0
- package/backend/alembic/versions/a1c7d90e4b52_branch_belongs_to_a_repository.py +72 -0
- package/backend/alembic/versions/b41c9d7e5f28_extend_pr_kind_categories.py +43 -0
- package/backend/alembic/versions/c8e4f2a71b93_add_release_kind.py +40 -0
- package/backend/alembic/versions/d1275091dfdc_add_needs_detail_flag_to_pull_request.py +44 -0
- package/backend/alembic/versions/e5b27c9d3a41_downgrade_cve_without_ids.py +54 -0
- package/backend/alembic/versions/ee3159a4eff0_add_sig_meeting_member_and_release_.py +164 -0
- package/backend/alembic/versions/fcf3c186d63b_attention_rule_subscriptions.py +42 -0
- package/backend/alembic.ini +40 -0
- package/backend/app/__init__.py +0 -0
- package/backend/app/api/__init__.py +0 -0
- package/backend/app/api/deps.py +74 -0
- package/backend/app/api/v1/__init__.py +0 -0
- package/backend/app/api/v1/ai.py +386 -0
- package/backend/app/api/v1/analytics.py +48 -0
- package/backend/app/api/v1/attention.py +142 -0
- package/backend/app/api/v1/auth.py +106 -0
- package/backend/app/api/v1/classification.py +192 -0
- package/backend/app/api/v1/health.py +29 -0
- package/backend/app/api/v1/issues.py +143 -0
- package/backend/app/api/v1/pulls.py +388 -0
- package/backend/app/api/v1/repositories.py +273 -0
- package/backend/app/api/v1/router.py +32 -0
- package/backend/app/api/v1/sig.py +565 -0
- package/backend/app/api/v1/users.py +87 -0
- package/backend/app/api/v1/webhooks.py +135 -0
- package/backend/app/core/__init__.py +0 -0
- package/backend/app/core/config.py +72 -0
- package/backend/app/core/crypto.py +74 -0
- package/backend/app/core/db.py +79 -0
- package/backend/app/core/exceptions.py +60 -0
- package/backend/app/core/logging.py +69 -0
- package/backend/app/core/permissions.py +87 -0
- package/backend/app/core/queue.py +57 -0
- package/backend/app/core/security.py +69 -0
- package/backend/app/domain/__init__.py +0 -0
- package/backend/app/domain/attention.py +618 -0
- package/backend/app/domain/classification.py +689 -0
- package/backend/app/domain/meeting.py +408 -0
- package/backend/app/domain/release.py +163 -0
- package/backend/app/domain/review.py +416 -0
- package/backend/app/domain/sig.py +178 -0
- package/backend/app/domain/sig_info.py +201 -0
- package/backend/app/integrations/__init__.py +0 -0
- package/backend/app/integrations/atomgit/__init__.py +0 -0
- package/backend/app/integrations/atomgit/client.py +505 -0
- package/backend/app/integrations/atomgit/models.py +311 -0
- package/backend/app/integrations/llm/__init__.py +0 -0
- package/backend/app/integrations/llm/prompts.py +222 -0
- package/backend/app/integrations/llm/provider.py +326 -0
- package/backend/app/main.py +242 -0
- package/backend/app/middleware/__init__.py +0 -0
- package/backend/app/middleware/audit.py +129 -0
- package/backend/app/middleware/request_context.py +42 -0
- package/backend/app/models/__init__.py +112 -0
- package/backend/app/models/ai.py +214 -0
- package/backend/app/models/attention.py +131 -0
- package/backend/app/models/attention_settings.py +61 -0
- package/backend/app/models/audit.py +53 -0
- package/backend/app/models/base.py +35 -0
- package/backend/app/models/classification.py +144 -0
- package/backend/app/models/credential.py +56 -0
- package/backend/app/models/issue.py +132 -0
- package/backend/app/models/meeting.py +189 -0
- package/backend/app/models/pull_request.py +288 -0
- package/backend/app/models/repository.py +196 -0
- package/backend/app/models/sig.py +194 -0
- package/backend/app/models/user.py +44 -0
- package/backend/app/schemas/__init__.py +0 -0
- package/backend/app/schemas/ai.py +120 -0
- package/backend/app/schemas/attention.py +43 -0
- package/backend/app/schemas/auth.py +26 -0
- package/backend/app/schemas/classification.py +58 -0
- package/backend/app/schemas/common.py +43 -0
- package/backend/app/schemas/pull_request.py +214 -0
- package/backend/app/schemas/repository.py +94 -0
- package/backend/app/schemas/sig.py +246 -0
- package/backend/app/schemas/user.py +79 -0
- package/backend/app/services/__init__.py +0 -0
- package/backend/app/services/ai_service.py +569 -0
- package/backend/app/services/analytics_service.py +390 -0
- package/backend/app/services/attention_queue.py +451 -0
- package/backend/app/services/attention_service.py +432 -0
- package/backend/app/services/auth_service.py +74 -0
- package/backend/app/services/classification_service.py +658 -0
- package/backend/app/services/credential_service.py +105 -0
- package/backend/app/services/pull_query.py +273 -0
- package/backend/app/services/release_service.py +446 -0
- package/backend/app/services/repository_service.py +132 -0
- package/backend/app/services/sig_service.py +385 -0
- package/backend/app/services/sync_service.py +752 -0
- package/backend/app/services/user_service.py +68 -0
- package/backend/app/worker.py +389 -0
- package/backend/entrypoint.sh +10 -0
- package/backend/pyproject.toml +68 -0
- package/backend/tests/test_analysis_api.py +154 -0
- package/backend/tests/test_atomgit_client.py +360 -0
- package/backend/tests/test_atomgit_models.py +257 -0
- package/backend/tests/test_attention.py +291 -0
- package/backend/tests/test_audit_middleware.py +135 -0
- package/backend/tests/test_classification.py +498 -0
- package/backend/tests/test_config.py +41 -0
- package/backend/tests/test_crypto.py +68 -0
- package/backend/tests/test_exceptions.py +62 -0
- package/backend/tests/test_health.py +63 -0
- package/backend/tests/test_llm_provider.py +320 -0
- package/backend/tests/test_meeting_domain.py +169 -0
- package/backend/tests/test_permissions.py +69 -0
- package/backend/tests/test_pull_query_wiring.py +66 -0
- package/backend/tests/test_pull_schemas.py +82 -0
- package/backend/tests/test_release_domain.py +82 -0
- package/backend/tests/test_review_parser.py +301 -0
- package/backend/tests/test_schemas_user.py +97 -0
- package/backend/tests/test_security.py +92 -0
- package/cli/index.js +338 -0
- package/compose.yaml +105 -0
- package/frontend/.dockerignore +4 -0
- package/frontend/Dockerfile +27 -0
- package/frontend/index.html +14 -0
- package/frontend/nginx.conf +47 -0
- package/frontend/package-lock.json +5020 -0
- package/frontend/package.json +35 -0
- package/frontend/src/api/ai.ts +124 -0
- package/frontend/src/api/analytics.ts +66 -0
- package/frontend/src/api/attention.ts +181 -0
- package/frontend/src/api/auth.ts +74 -0
- package/frontend/src/api/classification.ts +170 -0
- package/frontend/src/api/issues.ts +90 -0
- package/frontend/src/api/pulls.ts +233 -0
- package/frontend/src/api/repositories.ts +95 -0
- package/frontend/src/api/sig.ts +232 -0
- package/frontend/src/app/antd-theme.ts +94 -0
- package/frontend/src/app/providers.tsx +59 -0
- package/frontend/src/app/router.tsx +411 -0
- package/frontend/src/app/search.ts +30 -0
- package/frontend/src/components/ClassificationBadge.tsx +58 -0
- package/frontend/src/components/GateBadge.tsx +13 -0
- package/frontend/src/components/SeverityBadge.tsx +20 -0
- package/frontend/src/components/layout/AppShell.tsx +16 -0
- package/frontend/src/components/layout/AuthLayout.tsx +32 -0
- package/frontend/src/components/layout/Sidebar.tsx +223 -0
- package/frontend/src/components/layout/TopBar.tsx +47 -0
- package/frontend/src/components/pulls/DiscussionTimeline.tsx +145 -0
- package/frontend/src/components/pulls/FacetRail.tsx +199 -0
- package/frontend/src/components/pulls/LabelChips.tsx +87 -0
- package/frontend/src/components/ui/alert.tsx +30 -0
- package/frontend/src/components/ui/badge.tsx +53 -0
- package/frontend/src/components/ui/button.tsx +51 -0
- package/frontend/src/components/ui/card.tsx +64 -0
- package/frontend/src/components/ui/chart-theme.ts +65 -0
- package/frontend/src/components/ui/data-table.tsx +39 -0
- package/frontend/src/components/ui/echart.tsx +70 -0
- package/frontend/src/components/ui/empty-state.tsx +22 -0
- package/frontend/src/components/ui/input.tsx +39 -0
- package/frontend/src/components/ui/lazy-chart.tsx +21 -0
- package/frontend/src/components/ui/skeleton.tsx +19 -0
- package/frontend/src/hooks/use-current-repository.ts +44 -0
- package/frontend/src/hooks/use-current-user.ts +38 -0
- package/frontend/src/lib/api-client.ts +93 -0
- package/frontend/src/lib/css-color.ts +60 -0
- package/frontend/src/lib/utils.ts +45 -0
- package/frontend/src/main.tsx +22 -0
- package/frontend/src/pages/attention/AttentionQueuePage.tsx +316 -0
- package/frontend/src/pages/attention/RuleSettingsPanel.tsx +177 -0
- package/frontend/src/pages/branches/BranchDetailPage.tsx +632 -0
- package/frontend/src/pages/branches/BranchListPage.tsx +308 -0
- package/frontend/src/pages/dashboard/DashboardPage.tsx +657 -0
- package/frontend/src/pages/issues/IssueListPage.tsx +284 -0
- package/frontend/src/pages/login/LoginPage.tsx +95 -0
- package/frontend/src/pages/meetings/MeetingDetailPage.tsx +403 -0
- package/frontend/src/pages/meetings/MeetingListPage.tsx +264 -0
- package/frontend/src/pages/members/MembersPage.tsx +534 -0
- package/frontend/src/pages/pulls/PullDetailPage.tsx +833 -0
- package/frontend/src/pages/pulls/PullListPage.tsx +399 -0
- package/frontend/src/pages/settings/AiSettingsPage.tsx +449 -0
- package/frontend/src/pages/settings/SettingsPage.tsx +321 -0
- package/frontend/src/pages/setup/SetupPage.tsx +144 -0
- package/frontend/src/styles/globals.css +147 -0
- package/frontend/tsconfig.json +22 -0
- package/frontend/vite.config.ts +57 -0
- package/package.json +48 -0
- package/scripts/e2e-auth-flow.py +131 -0
- package/scripts/e2e-pr-detail.py +128 -0
- package/scripts/e2e-verify.py +773 -0
- package/scripts/verify-ai-pipeline.py +616 -0
- 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]}>"
|