@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,303 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""分类与关注项流水线的端到端校验。
|
|
3
|
+
|
|
4
|
+
跑在真实数据库上,验证两件靠单测覆盖不到的事:
|
|
5
|
+
1. 全量数据上不抛异常、结果落在枚举取值域内
|
|
6
|
+
2. **重算是幂等的** —— 关注项的 first_detected_at 不被刷新、
|
|
7
|
+
解决状态不被重复置位、分类不因重复执行而漂移
|
|
8
|
+
|
|
9
|
+
用法(在 backend 目录下):
|
|
10
|
+
KSC_SECRET_KEY=... python ../scripts/verify-analysis-pipeline.py
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
from sqlalchemy import func, select
|
|
19
|
+
|
|
20
|
+
from app.core.config import get_settings
|
|
21
|
+
from app.core.db import dispose_engine, init_engine, session_scope
|
|
22
|
+
from app.domain.classification import SUBSYSTEM_LABELS
|
|
23
|
+
from app.models.attention import AttentionItem, AttentionRule, AttentionSeverity
|
|
24
|
+
from app.models.classification import Classification, PRKind
|
|
25
|
+
from app.models.issue import Issue
|
|
26
|
+
from app.models.pull_request import PullRequest
|
|
27
|
+
from app.services import (
|
|
28
|
+
attention_service,
|
|
29
|
+
classification_service,
|
|
30
|
+
repository_service,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
failures: list[str] = []
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def check(condition: bool, message: str) -> None:
|
|
37
|
+
if condition:
|
|
38
|
+
print(f" ok {message}")
|
|
39
|
+
else:
|
|
40
|
+
print(f" FAIL {message}")
|
|
41
|
+
failures.append(message)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def main() -> int:
|
|
45
|
+
settings = get_settings()
|
|
46
|
+
init_engine(settings)
|
|
47
|
+
|
|
48
|
+
async with session_scope() as session:
|
|
49
|
+
repositories = await repository_service.list_repositories(session)
|
|
50
|
+
check(bool(repositories), f"纳管仓库数 = {len(repositories)}")
|
|
51
|
+
if not repositories:
|
|
52
|
+
await dispose_engine()
|
|
53
|
+
return 1
|
|
54
|
+
|
|
55
|
+
for repository in repositories:
|
|
56
|
+
print(f"\n=== {repository.full_name} ===")
|
|
57
|
+
|
|
58
|
+
pulls = list(
|
|
59
|
+
(
|
|
60
|
+
await session.scalars(
|
|
61
|
+
select(PullRequest).where(
|
|
62
|
+
PullRequest.repository_id == repository.id,
|
|
63
|
+
PullRequest.state == "open",
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
).all()
|
|
67
|
+
)
|
|
68
|
+
issues = list(
|
|
69
|
+
(
|
|
70
|
+
await session.scalars(
|
|
71
|
+
select(Issue).where(
|
|
72
|
+
Issue.repository_id == repository.id, Issue.state == "open"
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
).all()
|
|
76
|
+
)
|
|
77
|
+
print(f" open PR = {len(pulls)}, open Issue = {len(issues)}")
|
|
78
|
+
|
|
79
|
+
# --- 分类 ---
|
|
80
|
+
await classification_service.classify_pulls(session, repository, pulls)
|
|
81
|
+
await classification_service.classify_issues(session, repository, issues)
|
|
82
|
+
|
|
83
|
+
kinds = dict(
|
|
84
|
+
(
|
|
85
|
+
await session.execute(
|
|
86
|
+
select(Classification.kind, func.count())
|
|
87
|
+
.where(Classification.repository_id == repository.id)
|
|
88
|
+
.group_by(Classification.kind)
|
|
89
|
+
)
|
|
90
|
+
).all()
|
|
91
|
+
)
|
|
92
|
+
total = sum(kinds.values())
|
|
93
|
+
print(f" 分类结果:{total} 条")
|
|
94
|
+
for kind, count in sorted(kinds.items(), key=lambda kv: -kv[1]):
|
|
95
|
+
share = count / total * 100 if total else 0
|
|
96
|
+
print(f" {kind.value:<10} {count:>5} {share:5.1f}%")
|
|
97
|
+
|
|
98
|
+
# 断言"每个 open 对象都被分类",而不是"分类条数等于 open 对象数":
|
|
99
|
+
# 分类覆盖全部状态(月度汇总要统计已合入的 PR 属于哪一类),
|
|
100
|
+
# 总数本来就大于 open 的数量,用等号衡量的是统计口径而非覆盖率。
|
|
101
|
+
classified_ids = set(
|
|
102
|
+
(await session.scalars(
|
|
103
|
+
select(Classification.subject_id).where(
|
|
104
|
+
Classification.repository_id == repository.id
|
|
105
|
+
)
|
|
106
|
+
)).all()
|
|
107
|
+
)
|
|
108
|
+
uncovered = [row.number for row in pulls if row.id not in classified_ids]
|
|
109
|
+
uncovered += [row.number for row in issues if row.id not in classified_ids]
|
|
110
|
+
print(f" 未分类的 open 对象:{len(uncovered)} 个")
|
|
111
|
+
check(not uncovered, "每个 open PR/Issue 都有分类结果")
|
|
112
|
+
check(
|
|
113
|
+
all(isinstance(k, PRKind) for k in kinds),
|
|
114
|
+
"全部类型落在 PRKind 枚举内",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# 覆盖率不做通过/失败判定 —— 内核补丁标题描述的是"改了什么"而非
|
|
118
|
+
# "属于哪一类",未命中是规则的固有上限,由 AI 兜底接住。
|
|
119
|
+
# 这里只报告数字,让覆盖率的退化能被看见。
|
|
120
|
+
unknown_share = kinds.get(PRKind.UNKNOWN, 0) / total if total else 0
|
|
121
|
+
print(f" → 规则未命中率 {unknown_share:.1%}(这部分由 AI 补判)")
|
|
122
|
+
|
|
123
|
+
# 真正要守住的是**精确率**,不是覆盖率
|
|
124
|
+
rows = (
|
|
125
|
+
await session.execute(
|
|
126
|
+
select(Classification).where(Classification.repository_id == repository.id)
|
|
127
|
+
)
|
|
128
|
+
).scalars()
|
|
129
|
+
cve_rows = []
|
|
130
|
+
rule_rows = []
|
|
131
|
+
for row in rows:
|
|
132
|
+
if row.kind is PRKind.CVE:
|
|
133
|
+
cve_rows.append(row)
|
|
134
|
+
if row.source.value == "rule":
|
|
135
|
+
rule_rows.append(row)
|
|
136
|
+
|
|
137
|
+
hit_rows = [row for row in rule_rows if row.kind is not PRKind.UNKNOWN]
|
|
138
|
+
|
|
139
|
+
# 自动判定的 CVE 必须带得出发现在正文里的编号:规则层靠匹配编号
|
|
140
|
+
# 才判 cve,所以天然满足;AI 层则一度凭"这是安全修复"的印象判 cve
|
|
141
|
+
# 而给不出编号,让对外报的「CVE 数」混进未编号的加固(已由
|
|
142
|
+
# normalize_ai_kind 堵住,存量由迁移 e5b27c9d3a41 修正)。
|
|
143
|
+
#
|
|
144
|
+
# 人工指定不在此列:那是维护者的显式判断,他可能知道编号之外的
|
|
145
|
+
# 实情,平台不该替他改,也不该因此判他错。
|
|
146
|
+
auto_cve_rows = [row for row in cve_rows if row.source.value != "manual"]
|
|
147
|
+
manual_cve = len(cve_rows) - len(auto_cve_rows)
|
|
148
|
+
check(
|
|
149
|
+
all(row.cve_ids for row in auto_cve_rows),
|
|
150
|
+
f"自动判定的 CVE 都带编号({len(auto_cve_rows)} 条,"
|
|
151
|
+
f"另有 {manual_cve} 条人工指定不计)",
|
|
152
|
+
)
|
|
153
|
+
check(
|
|
154
|
+
all(row.rule_name for row in hit_rows),
|
|
155
|
+
f"规则命中项都记录命中规则名({len(hit_rows)} 条,可解释性)",
|
|
156
|
+
)
|
|
157
|
+
check(
|
|
158
|
+
all(row.confidence > 0 for row in rule_rows if row.kind is not PRKind.UNKNOWN),
|
|
159
|
+
"规则命中项的置信度大于 0",
|
|
160
|
+
)
|
|
161
|
+
check(
|
|
162
|
+
all(
|
|
163
|
+
row.kind is PRKind.UNKNOWN and row.confidence == 0
|
|
164
|
+
for row in rule_rows
|
|
165
|
+
if row.kind is PRKind.UNKNOWN
|
|
166
|
+
),
|
|
167
|
+
"规则未命中的条目标为 unknown 且置信度为 0(AI 补判的识别信号)",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
subsystems = [
|
|
171
|
+
row[0]
|
|
172
|
+
for row in (
|
|
173
|
+
await session.execute(
|
|
174
|
+
select(Classification.subsystem)
|
|
175
|
+
.where(
|
|
176
|
+
Classification.repository_id == repository.id,
|
|
177
|
+
Classification.subsystem.is_not(None),
|
|
178
|
+
)
|
|
179
|
+
.distinct()
|
|
180
|
+
)
|
|
181
|
+
).all()
|
|
182
|
+
]
|
|
183
|
+
unknown_subsystems = [s for s in subsystems if s not in SUBSYSTEM_LABELS]
|
|
184
|
+
check(
|
|
185
|
+
not unknown_subsystems,
|
|
186
|
+
f"子系统取值全部在已知集合内(异常项:{unknown_subsystems[:5]})",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
cve_pulls = await session.scalar(
|
|
190
|
+
select(func.count())
|
|
191
|
+
.select_from(Classification)
|
|
192
|
+
.where(
|
|
193
|
+
Classification.repository_id == repository.id,
|
|
194
|
+
Classification.kind == PRKind.CVE,
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
print(f" 判定为 CVE 的条目:{cve_pulls}")
|
|
198
|
+
|
|
199
|
+
# --- 关注项,第一轮 ---
|
|
200
|
+
stats_before = await attention_service.compute_for_repository(session, repository)
|
|
201
|
+
print(f" 关注项:{stats_before}")
|
|
202
|
+
first_pass = {
|
|
203
|
+
(row.subject_type, row.subject_id, row.rule): row.first_detected_at
|
|
204
|
+
for row in (
|
|
205
|
+
await session.scalars(
|
|
206
|
+
select(AttentionItem).where(AttentionItem.repository_id == repository.id)
|
|
207
|
+
)
|
|
208
|
+
).all()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
# --- 关注项,第二轮(幂等性验证)---
|
|
212
|
+
await attention_service.compute_for_repository(session, repository)
|
|
213
|
+
second_pass = {
|
|
214
|
+
(row.subject_type, row.subject_id, row.rule): row.first_detected_at
|
|
215
|
+
for row in (
|
|
216
|
+
await session.scalars(
|
|
217
|
+
select(AttentionItem).where(AttentionItem.repository_id == repository.id)
|
|
218
|
+
)
|
|
219
|
+
).all()
|
|
220
|
+
}
|
|
221
|
+
check(
|
|
222
|
+
first_pass == second_pass,
|
|
223
|
+
"重算不改变任何关注项的 key 或 first_detected_at",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
unresolved = await session.scalar(
|
|
227
|
+
select(func.count())
|
|
228
|
+
.select_from(AttentionItem)
|
|
229
|
+
.where(
|
|
230
|
+
AttentionItem.repository_id == repository.id,
|
|
231
|
+
AttentionItem.resolved_at.is_(None),
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
check(
|
|
235
|
+
unresolved == stats_before["open_items"],
|
|
236
|
+
f"重算后未解决数不变({unresolved})",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
by_severity = await attention_service.severity_counts(session, repository.id)
|
|
240
|
+
print(f" 按严重程度:{by_severity}")
|
|
241
|
+
by_rule = await attention_service.rule_counts(session, repository.id)
|
|
242
|
+
active_rules = {k: v for k, v in by_rule.items() if v}
|
|
243
|
+
print(f" 命中的规则:{active_rules}")
|
|
244
|
+
|
|
245
|
+
check(
|
|
246
|
+
all(k in {s.value for s in AttentionSeverity} for k in by_severity),
|
|
247
|
+
"严重程度取值合法",
|
|
248
|
+
)
|
|
249
|
+
check(
|
|
250
|
+
all(k in {r.value for r in AttentionRule} for k in by_rule),
|
|
251
|
+
"规则取值合法",
|
|
252
|
+
)
|
|
253
|
+
# 规则与领域层的一致性:两边的枚举必须完全对应。
|
|
254
|
+
# 多一个值意味着库里能存下规则层永远不会产生的取值
|
|
255
|
+
from app.domain.attention import Rule as DomainRule
|
|
256
|
+
|
|
257
|
+
model_rules = {r.value for r in AttentionRule}
|
|
258
|
+
domain_rules = {r.value for r in DomainRule}
|
|
259
|
+
check(
|
|
260
|
+
model_rules == domain_rules,
|
|
261
|
+
f"模型枚举与领域规则一致(模型独有:{model_rules - domain_rules};"
|
|
262
|
+
f"领域独有:{domain_rules - model_rules})",
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# 分层抽样:列出 blocker / critical 的前几条,人工核对是否合理
|
|
266
|
+
top = list(
|
|
267
|
+
(
|
|
268
|
+
await session.scalars(
|
|
269
|
+
select(AttentionItem)
|
|
270
|
+
.where(
|
|
271
|
+
AttentionItem.repository_id == repository.id,
|
|
272
|
+
AttentionItem.resolved_at.is_(None),
|
|
273
|
+
AttentionItem.severity.in_(
|
|
274
|
+
[AttentionSeverity.BLOCKER.value, AttentionSeverity.CRITICAL.value]
|
|
275
|
+
),
|
|
276
|
+
)
|
|
277
|
+
.limit(8)
|
|
278
|
+
)
|
|
279
|
+
).all()
|
|
280
|
+
)
|
|
281
|
+
print(" 最高优先级的几条:")
|
|
282
|
+
for item in top:
|
|
283
|
+
print(f" [{item.severity.value}] #{item.subject_number} {item.title}")
|
|
284
|
+
|
|
285
|
+
check(
|
|
286
|
+
all(item.subject_id is not None for item in top),
|
|
287
|
+
"关注项都关联到具体对象",
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
await dispose_engine()
|
|
291
|
+
|
|
292
|
+
print()
|
|
293
|
+
if failures:
|
|
294
|
+
print(f"失败 {len(failures)} 项:")
|
|
295
|
+
for message in failures:
|
|
296
|
+
print(f" - {message}")
|
|
297
|
+
return 1
|
|
298
|
+
print("全部通过")
|
|
299
|
+
return 0
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
if __name__ == "__main__":
|
|
303
|
+
sys.exit(asyncio.run(main()))
|