defect-check 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- defect_check/__init__.py +31 -0
- defect_check/contracts/__init__.py +23 -0
- defect_check/contracts/adapters.py +287 -0
- defect_check/contracts/models.py +159 -0
- defect_check/cross/__init__.py +191 -0
- defect_check/cross/checker.py +332 -0
- defect_check/cross/matcher.py +295 -0
- defect_check/cross/models.py +54 -0
- defect_check/cross/prompt_builder.py +67 -0
- defect_check/cross/scoring.py +385 -0
- defect_check/defect_check_main.py +278 -0
- defect_check/llm/__init__.py +24 -0
- defect_check/llm/adapters/__init__.py +5 -0
- defect_check/llm/adapters/anthropic_adapter.py +343 -0
- defect_check/llm/adapters/base.py +89 -0
- defect_check/llm/adapters/dashscope_adapter.py +161 -0
- defect_check/llm/adapters/openai_adapter.py +373 -0
- defect_check/llm/base.py +61 -0
- defect_check/llm/config.py +68 -0
- defect_check/llm/exceptions.py +52 -0
- defect_check/llm/factory.py +44 -0
- defect_check/llm/messages.py +147 -0
- defect_check/llm/providers/__init__.py +4 -0
- defect_check/llm/providers/anthropic.py +160 -0
- defect_check/llm/providers/dashscope.py +46 -0
- defect_check/llm/providers/openai.py +144 -0
- defect_check/llm/text_client.py +58 -0
- defect_check/lookup_table.py +394 -0
- defect_check/models.py +51 -0
- defect_check/orchestrator.py +162 -0
- defect_check/qdp/__init__.py +58 -0
- defect_check/qdp/checker.py +321 -0
- defect_check/qdp/models.py +206 -0
- defect_check/qdp/prompt_builder.py +47 -0
- defect_check/qdp/scoring.py +245 -0
- defect_check/qds/__init__.py +21 -0
- defect_check/qds/checker/__init__.py +7 -0
- defect_check/qds/checker/parallel.py +194 -0
- defect_check/qds/checker/parser.py +70 -0
- defect_check/qds/checker/prompts.py +24 -0
- defect_check/qds/checklist.py +1237 -0
- defect_check/qds/config.py +37 -0
- defect_check/qds/models.py +100 -0
- defect_check/qds/prompt_builder.py +43 -0
- defect_check/qds/qds0/__init__.py +7 -0
- defect_check/qds/qds0/checker.py +135 -0
- defect_check/qds/qds0/parser.py +55 -0
- defect_check/qds/qds0/validators.py +70 -0
- defect_check/qds/scoring.py +569 -0
- defect_check/qds/service.py +176 -0
- defect_check/qds/view/__init__.py +6 -0
- defect_check/qds/view/generator.py +129 -0
- defect_check/qds/view/patterns.py +184 -0
- defect_check/qdt/__init__.py +116 -0
- defect_check/qdt/checker.py +1202 -0
- defect_check/qdt/models.py +54 -0
- defect_check/qdt/prompt_builder.py +295 -0
- defect_check/qdt/scoring.py +335 -0
- defect_check/rating/__init__.py +38 -0
- defect_check/rating/determiner.py +154 -0
- defect_check/rating/keywords.py +100 -0
- defect_check/rating/level.py +67 -0
- defect_check/resources/__init__.py +19 -0
- defect_check/resources/config/cross_scoring.yaml +419 -0
- defect_check/resources/config/qdp_scoring.yaml +505 -0
- defect_check/resources/config/qds_scoring.yaml +1044 -0
- defect_check/resources/config/qdt_scoring.yaml +414 -0
- defect_check/resources/prompts/prompt_cross.yaml +366 -0
- defect_check/resources/prompts/prompt_qdp.yaml +497 -0
- defect_check/resources/prompts/prompt_qds.yaml +39 -0
- defect_check/resources/prompts/prompt_qdt.yaml +622 -0
- defect_check-0.0.1.dist-info/METADATA +355 -0
- defect_check-0.0.1.dist-info/RECORD +76 -0
- defect_check-0.0.1.dist-info/WHEEL +5 -0
- defect_check-0.0.1.dist-info/licenses/LICENSE +201 -0
- defect_check-0.0.1.dist-info/top_level.txt +1 -0
defect_check/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Standalone defect-checking package."""
|
|
2
|
+
|
|
3
|
+
from .defect_check_main import check, check_cross, check_single
|
|
4
|
+
from .contracts import (
|
|
5
|
+
ArtifactReference,
|
|
6
|
+
DefectCheckResponse,
|
|
7
|
+
DefectItem,
|
|
8
|
+
DefectSummary,
|
|
9
|
+
InspectionError,
|
|
10
|
+
InspectionResult,
|
|
11
|
+
ResponseSummary,
|
|
12
|
+
ScoreResult,
|
|
13
|
+
)
|
|
14
|
+
from .models import DefectCheckOptions, PromptArtifact, SkillArtifact
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"ArtifactReference",
|
|
18
|
+
"DefectCheckOptions",
|
|
19
|
+
"DefectCheckResponse",
|
|
20
|
+
"DefectItem",
|
|
21
|
+
"DefectSummary",
|
|
22
|
+
"InspectionError",
|
|
23
|
+
"InspectionResult",
|
|
24
|
+
"PromptArtifact",
|
|
25
|
+
"ResponseSummary",
|
|
26
|
+
"ScoreResult",
|
|
27
|
+
"SkillArtifact",
|
|
28
|
+
"check",
|
|
29
|
+
"check_cross",
|
|
30
|
+
"check_single",
|
|
31
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Stable public return contract for defect-check results."""
|
|
2
|
+
|
|
3
|
+
from .models import (
|
|
4
|
+
ArtifactReference,
|
|
5
|
+
DefectCheckResponse,
|
|
6
|
+
DefectItem,
|
|
7
|
+
DefectSummary,
|
|
8
|
+
InspectionError,
|
|
9
|
+
InspectionResult,
|
|
10
|
+
ResponseSummary,
|
|
11
|
+
ScoreResult,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ArtifactReference",
|
|
16
|
+
"DefectCheckResponse",
|
|
17
|
+
"DefectItem",
|
|
18
|
+
"DefectSummary",
|
|
19
|
+
"InspectionError",
|
|
20
|
+
"InspectionResult",
|
|
21
|
+
"ResponseSummary",
|
|
22
|
+
"ScoreResult",
|
|
23
|
+
]
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Adapters from module-native result payloads to the public contract."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from defect_check.lookup_table import lookup_defect
|
|
9
|
+
|
|
10
|
+
from .models import (
|
|
11
|
+
ArtifactReference,
|
|
12
|
+
DefectItem,
|
|
13
|
+
DefectSummary,
|
|
14
|
+
InspectionError,
|
|
15
|
+
InspectionResult,
|
|
16
|
+
ScoreResult,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _severity(value: Any) -> str:
|
|
21
|
+
text = getattr(value, "value", value)
|
|
22
|
+
return text if text in {"P0", "P1", "P2"} else "NONE"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _score(payload: dict[str, Any] | None, fallback_gate: str = "PASS") -> ScoreResult | None:
|
|
26
|
+
if not payload:
|
|
27
|
+
return None
|
|
28
|
+
total = payload.get("total_score", payload.get("score", payload.get("total")))
|
|
29
|
+
if total is None:
|
|
30
|
+
return None
|
|
31
|
+
qds0_passed = payload.get("qds0_passed")
|
|
32
|
+
if not isinstance(qds0_passed, bool):
|
|
33
|
+
qds0_passed = None
|
|
34
|
+
conformance_failed = payload.get("conformance_failed")
|
|
35
|
+
if not isinstance(conformance_failed, bool):
|
|
36
|
+
conformance_failed = None
|
|
37
|
+
gate = (
|
|
38
|
+
"FAIL"
|
|
39
|
+
if qds0_passed is False or conformance_failed is True
|
|
40
|
+
else payload.get("gate_result", payload.get("result", fallback_gate))
|
|
41
|
+
)
|
|
42
|
+
return ScoreResult(
|
|
43
|
+
total_score=float(total),
|
|
44
|
+
max_score=float(payload.get("max_score", 100)),
|
|
45
|
+
grade=payload.get("health_grade") or payload.get("grade"),
|
|
46
|
+
gate_result="FAIL" if gate == "FAIL" else "PASS",
|
|
47
|
+
qds0_passed=qds0_passed,
|
|
48
|
+
conformance_failed=conformance_failed,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _status(payload: dict[str, Any]) -> str:
|
|
53
|
+
return "failed" if payload.get("status") in {"failed", "error"} else "completed"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _result_error(payload: dict[str, Any]) -> InspectionError | None:
|
|
57
|
+
message = payload.get("error")
|
|
58
|
+
if not message:
|
|
59
|
+
return None
|
|
60
|
+
return InspectionError(code="INSPECTION_FAILED", message=str(message))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def adapt_qds_result(
|
|
64
|
+
payload: dict[str, Any],
|
|
65
|
+
*,
|
|
66
|
+
artifact_id: str,
|
|
67
|
+
artifact_name: str,
|
|
68
|
+
) -> InspectionResult:
|
|
69
|
+
check_level = payload.get("check_level")
|
|
70
|
+
defects: list[DefectItem] = []
|
|
71
|
+
for item in payload.get("defects", []):
|
|
72
|
+
if not isinstance(item, dict):
|
|
73
|
+
continue
|
|
74
|
+
defect_id = str(item.get("id", ""))
|
|
75
|
+
rule = lookup_defect("qds", defect_id, check_level)
|
|
76
|
+
raw_severity = item.get("severity")
|
|
77
|
+
defects.append(
|
|
78
|
+
DefectItem(
|
|
79
|
+
id=defect_id,
|
|
80
|
+
name=str(item.get("name") or (rule.name if rule else "")),
|
|
81
|
+
severity="P0" if raw_severity == "Conformance" else _severity(raw_severity),
|
|
82
|
+
category=str(item.get("dimension") or item.get("category") or ""),
|
|
83
|
+
description=str(item.get("brief_reason") or item.get("description") or ""),
|
|
84
|
+
location=item.get("location"),
|
|
85
|
+
impact=item.get("impact"),
|
|
86
|
+
fix_suggestion=item.get("fix_suggestion"),
|
|
87
|
+
artifact_refs=[artifact_id],
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
score_payload = payload.get("score") if isinstance(payload.get("score"), dict) else {}
|
|
91
|
+
return InspectionResult(
|
|
92
|
+
module="QDS",
|
|
93
|
+
status=_status(payload),
|
|
94
|
+
check_level=check_level,
|
|
95
|
+
artifacts=[ArtifactReference(type="skill", id=artifact_id, name=artifact_name)],
|
|
96
|
+
score=_score(score_payload, score_payload.get("gate_result", "PASS")),
|
|
97
|
+
defect_summary=DefectSummary.from_defects(defects),
|
|
98
|
+
defects=defects,
|
|
99
|
+
error=_result_error(payload),
|
|
100
|
+
details={},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def adapt_qdt_result(
|
|
105
|
+
payload: dict[str, Any],
|
|
106
|
+
*,
|
|
107
|
+
artifact_id: str,
|
|
108
|
+
artifact_name: str,
|
|
109
|
+
) -> InspectionResult:
|
|
110
|
+
defects = [
|
|
111
|
+
DefectItem(
|
|
112
|
+
id=str(item.get("defect_id") or item.get("id") or ""),
|
|
113
|
+
name=str(item.get("name", "")),
|
|
114
|
+
severity=_severity(item.get("defect_level") or item.get("severity")),
|
|
115
|
+
category=str(item.get("module") or item.get("category") or ""),
|
|
116
|
+
description=str(item.get("message") or item.get("description") or item.get("brief_reason") or ""),
|
|
117
|
+
location=item.get("path") or item.get("location"),
|
|
118
|
+
impact=item.get("impact"),
|
|
119
|
+
fix_suggestion=item.get("suggestion") or item.get("fix_suggestion"),
|
|
120
|
+
artifact_refs=[artifact_id],
|
|
121
|
+
)
|
|
122
|
+
for item in payload.get("issues", [])
|
|
123
|
+
if isinstance(item, dict)
|
|
124
|
+
]
|
|
125
|
+
summary = payload.get("summary") if isinstance(payload.get("summary"), dict) else {}
|
|
126
|
+
scoring = payload.get("scoring") if isinstance(payload.get("scoring"), dict) else {}
|
|
127
|
+
return InspectionResult(
|
|
128
|
+
module="QDT",
|
|
129
|
+
status=_status(payload),
|
|
130
|
+
check_level=payload.get("check_level") or summary.get("check_level"),
|
|
131
|
+
artifacts=[ArtifactReference(type="tool", id=artifact_id, name=artifact_name)],
|
|
132
|
+
score=_score(scoring, summary.get("gate_result", "PASS")),
|
|
133
|
+
defect_summary=DefectSummary.from_defects(defects),
|
|
134
|
+
defects=defects,
|
|
135
|
+
error=_result_error(payload),
|
|
136
|
+
details={},
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def adapt_qdp_result(
|
|
141
|
+
payload: dict[str, Any],
|
|
142
|
+
*,
|
|
143
|
+
artifact_id: str,
|
|
144
|
+
artifact_name: str,
|
|
145
|
+
) -> InspectionResult:
|
|
146
|
+
defects = [
|
|
147
|
+
DefectItem(
|
|
148
|
+
id=str(item.get("code") or item.get("id") or ""),
|
|
149
|
+
name=str(item.get("name", "")),
|
|
150
|
+
severity=_severity(item.get("severity")),
|
|
151
|
+
category=str(item.get("category") or (f"phase-{item['phase']}" if item.get("phase") is not None else "")),
|
|
152
|
+
description=str(item.get("description") or ""),
|
|
153
|
+
location=item.get("location"),
|
|
154
|
+
impact=item.get("impact"),
|
|
155
|
+
fix_suggestion=item.get("repair_suggestion") or item.get("fix_suggestion"),
|
|
156
|
+
artifact_refs=[artifact_id],
|
|
157
|
+
)
|
|
158
|
+
for item in payload.get("defects", [])
|
|
159
|
+
if isinstance(item, dict)
|
|
160
|
+
]
|
|
161
|
+
scoring = payload.get("scoring") if isinstance(payload.get("scoring"), dict) else {}
|
|
162
|
+
return InspectionResult(
|
|
163
|
+
module="QDP",
|
|
164
|
+
status=_status(payload),
|
|
165
|
+
check_level=payload.get("check_level"),
|
|
166
|
+
artifacts=[ArtifactReference(type="prompt", id=artifact_id, name=artifact_name)],
|
|
167
|
+
score=_score(scoring, scoring.get("gate_result", "PASS")),
|
|
168
|
+
defect_summary=DefectSummary.from_defects(defects),
|
|
169
|
+
defects=defects,
|
|
170
|
+
error=_result_error(payload),
|
|
171
|
+
details={},
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _artifact(value: dict[str, Any], artifact_type: str) -> ArtifactReference:
|
|
176
|
+
artifact_id = str(value.get("id") or value.get("skill_id") or value.get("name") or value.get("skill_name") or "unknown")
|
|
177
|
+
artifact_name = str(value.get("name") or value.get("skill_name") or artifact_id)
|
|
178
|
+
return ArtifactReference(type=artifact_type, id=artifact_id, name=artifact_name)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _cross_location(value: Any) -> str | None:
|
|
182
|
+
"""Convert Cross's structured location evidence to the public string contract."""
|
|
183
|
+
if value is None or isinstance(value, str):
|
|
184
|
+
return value
|
|
185
|
+
if isinstance(value, (dict, list)):
|
|
186
|
+
return json.dumps(value, ensure_ascii=False, sort_keys=True)
|
|
187
|
+
return str(value)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _cross_details(item: dict[str, Any]) -> dict[str, Any]:
|
|
191
|
+
details: dict[str, Any] = {}
|
|
192
|
+
if item.get("fix_priority"):
|
|
193
|
+
details["fix_priority"] = item["fix_priority"]
|
|
194
|
+
if isinstance(item.get("location"), (dict, list)):
|
|
195
|
+
details["location_detail"] = item["location"]
|
|
196
|
+
return details
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _adapt_cross_mode(
|
|
200
|
+
relation: dict[str, Any],
|
|
201
|
+
*,
|
|
202
|
+
check_type: str,
|
|
203
|
+
artifacts: list[ArtifactReference],
|
|
204
|
+
) -> InspectionResult:
|
|
205
|
+
native = relation.get("result")
|
|
206
|
+
if not isinstance(native, dict):
|
|
207
|
+
reason = str(relation.get("skip_reason") or "No matched artifact")
|
|
208
|
+
return InspectionResult(
|
|
209
|
+
module="CROSS",
|
|
210
|
+
check_type=check_type,
|
|
211
|
+
status="skipped",
|
|
212
|
+
artifacts=artifacts,
|
|
213
|
+
error=InspectionError(
|
|
214
|
+
code="NO_MATCHED_TOOL" if check_type == "ST" else "NO_MATCHED_ARTIFACT",
|
|
215
|
+
message=reason,
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
defects = [
|
|
219
|
+
DefectItem(
|
|
220
|
+
id=str(item.get("defect_id") or item.get("id") or ""),
|
|
221
|
+
name=str(item.get("check_name") or item.get("name") or ""),
|
|
222
|
+
severity=_severity(item.get("defect_level") or item.get("severity")),
|
|
223
|
+
category=str(item.get("relation") or item.get("category") or check_type),
|
|
224
|
+
description=str(item.get("description") or item.get("brief_reason") or item.get("message") or ""),
|
|
225
|
+
location=_cross_location(item.get("location")),
|
|
226
|
+
impact=item.get("impact"),
|
|
227
|
+
fix_suggestion=item.get("suggestion") or item.get("fix_suggestion"),
|
|
228
|
+
artifact_refs=[artifact.id for artifact in artifacts],
|
|
229
|
+
details=_cross_details(item),
|
|
230
|
+
)
|
|
231
|
+
for item in native.get("defects", [])
|
|
232
|
+
if isinstance(item, dict)
|
|
233
|
+
]
|
|
234
|
+
score = {
|
|
235
|
+
"total_score": native.get("score", 100),
|
|
236
|
+
"gate_result": native.get("result", "PASS"),
|
|
237
|
+
}
|
|
238
|
+
return InspectionResult(
|
|
239
|
+
module="CROSS",
|
|
240
|
+
check_type=check_type,
|
|
241
|
+
status="completed",
|
|
242
|
+
check_level=native.get("check_level"),
|
|
243
|
+
artifacts=artifacts,
|
|
244
|
+
score=_score(score),
|
|
245
|
+
defect_summary=DefectSummary.from_defects(defects),
|
|
246
|
+
defects=defects,
|
|
247
|
+
details={"matching": {"matched": True}},
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def adapt_cross_response(
|
|
252
|
+
payload: dict[str, Any],
|
|
253
|
+
*,
|
|
254
|
+
prompt_artifact: dict[str, Any] | None = None,
|
|
255
|
+
) -> list[InspectionResult]:
|
|
256
|
+
results: list[InspectionResult] = []
|
|
257
|
+
prompt_ref = _artifact(prompt_artifact, "prompt") if prompt_artifact else None
|
|
258
|
+
ps = payload.get("ps")
|
|
259
|
+
if isinstance(ps, dict):
|
|
260
|
+
artifacts = ([prompt_ref] if prompt_ref else []) + [
|
|
261
|
+
_artifact(item, "skill") for item in ps.get("matched_skills", []) if isinstance(item, dict)
|
|
262
|
+
]
|
|
263
|
+
results.append(_adapt_cross_mode(ps, check_type="PS", artifacts=artifacts))
|
|
264
|
+
pt = payload.get("pt")
|
|
265
|
+
if isinstance(pt, dict):
|
|
266
|
+
artifacts = ([prompt_ref] if prompt_ref else []) + [
|
|
267
|
+
ArtifactReference(type="tool", id=str(name), name=str(name))
|
|
268
|
+
for name in pt.get("matched_tools", [])
|
|
269
|
+
]
|
|
270
|
+
results.append(_adapt_cross_mode(pt, check_type="PT", artifacts=artifacts))
|
|
271
|
+
for relation in payload.get("st", []):
|
|
272
|
+
if not isinstance(relation, dict):
|
|
273
|
+
continue
|
|
274
|
+
skill = ArtifactReference(
|
|
275
|
+
type="skill",
|
|
276
|
+
id=str(relation.get("skill_id", "unknown")),
|
|
277
|
+
name=str(relation.get("skill_name") or relation.get("skill_id") or "unknown"),
|
|
278
|
+
)
|
|
279
|
+
tools = [
|
|
280
|
+
ArtifactReference(type="tool", id=str(name), name=str(name))
|
|
281
|
+
for name in relation.get("matched_tools", [])
|
|
282
|
+
]
|
|
283
|
+
results.append(_adapt_cross_mode(relation, check_type="ST", artifacts=[skill, *tools]))
|
|
284
|
+
return results
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
__all__ = ["adapt_cross_response", "adapt_qdp_result", "adapt_qds_result", "adapt_qdt_result"]
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Pydantic models for the canonical public JSON response."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ContractModel(BaseModel):
|
|
11
|
+
"""Strict base model for the public package contract."""
|
|
12
|
+
|
|
13
|
+
model_config = ConfigDict(extra="forbid")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ArtifactReference(ContractModel):
|
|
17
|
+
type: Literal["skill", "tool", "prompt"]
|
|
18
|
+
id: str
|
|
19
|
+
name: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DefectItem(ContractModel):
|
|
23
|
+
id: str
|
|
24
|
+
name: str = ""
|
|
25
|
+
severity: Literal["P0", "P1", "P2", "NONE"] = "NONE"
|
|
26
|
+
category: str = ""
|
|
27
|
+
description: str = ""
|
|
28
|
+
location: str | None = None
|
|
29
|
+
impact: str | None = None
|
|
30
|
+
fix_suggestion: str | None = None
|
|
31
|
+
artifact_refs: list[str] = Field(default_factory=list)
|
|
32
|
+
details: dict[str, Any] = Field(default_factory=dict)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ScoreResult(ContractModel):
|
|
36
|
+
total_score: float
|
|
37
|
+
max_score: float = 100.0
|
|
38
|
+
grade: str | None = None
|
|
39
|
+
gate_result: Literal["PASS", "FAIL"] = "PASS"
|
|
40
|
+
qds0_passed: bool | None = None
|
|
41
|
+
conformance_failed: bool | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DefectSummary(ContractModel):
|
|
45
|
+
total_defects: int = 0
|
|
46
|
+
p0_count: int = 0
|
|
47
|
+
p1_count: int = 0
|
|
48
|
+
p2_count: int = 0
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_defects(cls, defects: list[DefectItem]) -> "DefectSummary":
|
|
52
|
+
counts = {"P0": 0, "P1": 0, "P2": 0}
|
|
53
|
+
for defect in defects:
|
|
54
|
+
if defect.severity in counts:
|
|
55
|
+
counts[defect.severity] += 1
|
|
56
|
+
return cls(
|
|
57
|
+
total_defects=len(defects),
|
|
58
|
+
p0_count=counts["P0"],
|
|
59
|
+
p1_count=counts["P1"],
|
|
60
|
+
p2_count=counts["P2"],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class InspectionError(ContractModel):
|
|
65
|
+
code: str
|
|
66
|
+
message: str
|
|
67
|
+
retryable: bool = False
|
|
68
|
+
details: dict[str, Any] = Field(default_factory=dict)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class InspectionResult(ContractModel):
|
|
72
|
+
module: Literal["QDS", "QDT", "QDP", "CROSS"]
|
|
73
|
+
check_type: Literal["artifact", "PS", "PT", "ST"] = "artifact"
|
|
74
|
+
status: Literal["completed", "failed", "skipped"]
|
|
75
|
+
check_level: Literal["L1", "L2", "L3"] | None = None
|
|
76
|
+
artifacts: list[ArtifactReference] = Field(default_factory=list)
|
|
77
|
+
score: ScoreResult | None = None
|
|
78
|
+
defect_summary: DefectSummary = Field(default_factory=DefectSummary)
|
|
79
|
+
defects: list[DefectItem] = Field(default_factory=list)
|
|
80
|
+
error: InspectionError | None = None
|
|
81
|
+
details: dict[str, Any] = Field(default_factory=dict)
|
|
82
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ResponseSummary(ContractModel):
|
|
86
|
+
total_results: int = 0
|
|
87
|
+
completed_results: int = 0
|
|
88
|
+
failed_results: int = 0
|
|
89
|
+
skipped_results: int = 0
|
|
90
|
+
total_defects: int = 0
|
|
91
|
+
p0_count: int = 0
|
|
92
|
+
p1_count: int = 0
|
|
93
|
+
p2_count: int = 0
|
|
94
|
+
gate_result: Literal["PASS", "FAIL"] = "PASS"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class DefectCheckResponse(ContractModel):
|
|
98
|
+
schema_version: Literal["1.0"] = "1.0"
|
|
99
|
+
status: Literal["completed", "partial_failed", "failed"]
|
|
100
|
+
results: list[InspectionResult] = Field(default_factory=list)
|
|
101
|
+
summary: ResponseSummary = Field(default_factory=ResponseSummary)
|
|
102
|
+
errors: list[InspectionError] = Field(default_factory=list)
|
|
103
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def from_results(
|
|
107
|
+
cls,
|
|
108
|
+
results: list[InspectionResult],
|
|
109
|
+
*,
|
|
110
|
+
errors: list[InspectionError] | None = None,
|
|
111
|
+
execution_time_seconds: float = 0.0,
|
|
112
|
+
) -> "DefectCheckResponse":
|
|
113
|
+
completed = sum(item.status == "completed" for item in results)
|
|
114
|
+
failed = sum(item.status == "failed" for item in results)
|
|
115
|
+
skipped = sum(item.status == "skipped" for item in results)
|
|
116
|
+
if failed and completed:
|
|
117
|
+
status = "partial_failed"
|
|
118
|
+
elif failed or (not completed and errors):
|
|
119
|
+
status = "failed"
|
|
120
|
+
else:
|
|
121
|
+
status = "completed"
|
|
122
|
+
total_defects = sum(item.defect_summary.total_defects for item in results)
|
|
123
|
+
p0_count = sum(item.defect_summary.p0_count for item in results)
|
|
124
|
+
p1_count = sum(item.defect_summary.p1_count for item in results)
|
|
125
|
+
p2_count = sum(item.defect_summary.p2_count for item in results)
|
|
126
|
+
gate_result = "FAIL" if any(
|
|
127
|
+
item.score is not None and item.score.gate_result == "FAIL"
|
|
128
|
+
for item in results
|
|
129
|
+
if item.status == "completed"
|
|
130
|
+
) else "PASS"
|
|
131
|
+
return cls(
|
|
132
|
+
status=status,
|
|
133
|
+
results=results,
|
|
134
|
+
summary=ResponseSummary(
|
|
135
|
+
total_results=len(results),
|
|
136
|
+
completed_results=completed,
|
|
137
|
+
failed_results=failed,
|
|
138
|
+
skipped_results=skipped,
|
|
139
|
+
total_defects=total_defects,
|
|
140
|
+
p0_count=p0_count,
|
|
141
|
+
p1_count=p1_count,
|
|
142
|
+
p2_count=p2_count,
|
|
143
|
+
gate_result=gate_result,
|
|
144
|
+
),
|
|
145
|
+
errors=errors or [],
|
|
146
|
+
metadata={"execution_time_seconds": execution_time_seconds},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
__all__ = [
|
|
151
|
+
"ArtifactReference",
|
|
152
|
+
"DefectCheckResponse",
|
|
153
|
+
"DefectItem",
|
|
154
|
+
"DefectSummary",
|
|
155
|
+
"InspectionError",
|
|
156
|
+
"InspectionResult",
|
|
157
|
+
"ResponseSummary",
|
|
158
|
+
"ScoreResult",
|
|
159
|
+
]
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Cross-artifact matching, inspection, and scoring entry points."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from defect_check.llm import DefectCheckTextClient
|
|
9
|
+
|
|
10
|
+
from .checker import CrossChecker
|
|
11
|
+
from .matcher import CrossMatcher
|
|
12
|
+
from .scoring import CrossScoringCalculator
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _tool_name(tool: dict[str, Any]) -> str:
|
|
16
|
+
if "function" in tool and isinstance(tool["function"], dict):
|
|
17
|
+
return str(tool["function"].get("name", "unknown"))
|
|
18
|
+
return str(tool.get("name", "unknown"))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _normalize_skills(skills: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
22
|
+
"""Accept the SanityOps and CLI spellings while keeping one internal contract."""
|
|
23
|
+
normalized: list[dict[str, Any]] = []
|
|
24
|
+
for index, skill in enumerate(skills):
|
|
25
|
+
skill_id = skill.get("skill_id") or skill.get("id") or f"skill-{index + 1}"
|
|
26
|
+
normalized.append(
|
|
27
|
+
{
|
|
28
|
+
"skill_id": str(skill_id),
|
|
29
|
+
"skill_name": skill.get("skill_name") or skill.get("name") or str(skill_id),
|
|
30
|
+
"content": skill.get("content") or skill.get("skill_content") or "",
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
return normalized
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def _check_mode(
|
|
37
|
+
*,
|
|
38
|
+
mode: str,
|
|
39
|
+
prompt: str | None,
|
|
40
|
+
skills: list[dict[str, Any]],
|
|
41
|
+
tools: list[dict[str, Any]],
|
|
42
|
+
llm_client: DefectCheckTextClient,
|
|
43
|
+
check_level: str | None,
|
|
44
|
+
) -> dict[str, Any]:
|
|
45
|
+
result = await CrossChecker(llm_client=llm_client).check(
|
|
46
|
+
{
|
|
47
|
+
"mode": mode,
|
|
48
|
+
"prompt_text": prompt,
|
|
49
|
+
"skills_content": skills,
|
|
50
|
+
"tools_schemas": tools,
|
|
51
|
+
"check_level": check_level,
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
defects = result.get("defects", [])
|
|
55
|
+
check_level = result.get("check_level", "L1")
|
|
56
|
+
scoring = CrossScoringCalculator().calculate_score(defects, check_level, mode)
|
|
57
|
+
group_stats = scoring.get("group_stats", {})
|
|
58
|
+
counts = {
|
|
59
|
+
severity: sum(group.get(f"{severity.lower()}_count", 0) for group in group_stats.values())
|
|
60
|
+
for severity in ("P0", "P1", "P2")
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
"mode": mode,
|
|
64
|
+
"check_level": check_level,
|
|
65
|
+
"result": scoring.get("gate_result", "PASS"),
|
|
66
|
+
"score": scoring.get("total_score", 100),
|
|
67
|
+
"defect_summary": {**{f"{key.lower()}_count": value for key, value in counts.items()}, "total_defects": sum(counts.values())},
|
|
68
|
+
"defects": defects,
|
|
69
|
+
"scoring": scoring,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def check_cross(
|
|
74
|
+
*,
|
|
75
|
+
prompt: str | None,
|
|
76
|
+
skills: list[dict[str, Any]] | None,
|
|
77
|
+
tools: list[dict[str, Any]] | None,
|
|
78
|
+
llm_client: DefectCheckTextClient | None,
|
|
79
|
+
check_level: str | None = None,
|
|
80
|
+
) -> dict[str, Any]:
|
|
81
|
+
"""Run the current PS/PT/ST flow using caller-supplied artifacts.
|
|
82
|
+
|
|
83
|
+
PS and PT run only when their corresponding LLM match has candidates. After
|
|
84
|
+
matching, PS, PT, and every eligible ST check execute concurrently; ST is
|
|
85
|
+
recorded as skipped for each Skill without a matched Tool.
|
|
86
|
+
"""
|
|
87
|
+
if llm_client is None:
|
|
88
|
+
raise ValueError("check_cross requires a DefectCheckTextClient")
|
|
89
|
+
|
|
90
|
+
normalized_skills = _normalize_skills(skills or [])
|
|
91
|
+
normalized_tools = list(tools or [])
|
|
92
|
+
result: dict[str, Any] = {"enabled": True, "ps": None, "pt": None, "st": []}
|
|
93
|
+
if not prompt and not normalized_skills and not normalized_tools:
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
tool_names = [_tool_name(tool) for tool in normalized_tools]
|
|
97
|
+
matcher = CrossMatcher(llm_client=llm_client)
|
|
98
|
+
matches = await matcher.match_all(
|
|
99
|
+
prompt=prompt,
|
|
100
|
+
skills=normalized_skills,
|
|
101
|
+
tool_names=tool_names,
|
|
102
|
+
)
|
|
103
|
+
prompt_skills = matches.get("prompt_skills", [])
|
|
104
|
+
prompt_tools = matches.get("prompt_tools", [])
|
|
105
|
+
skill_tools = matches.get("skill_tools", {})
|
|
106
|
+
skill_name_map = {skill["skill_id"]: skill["skill_name"] for skill in normalized_skills}
|
|
107
|
+
result["matching"] = matches.get("details", {})
|
|
108
|
+
result["matching"].setdefault("prompt_tools", {"raw_response": None, "parsed_ids": []})[
|
|
109
|
+
"execution_objects"
|
|
110
|
+
] = [tool for tool in normalized_tools if _tool_name(tool) in prompt_tools]
|
|
111
|
+
result["matching"].setdefault("prompt_skills", {"raw_response": None, "parsed_ids": []})[
|
|
112
|
+
"execution_objects"
|
|
113
|
+
] = [skill for skill in normalized_skills if skill["skill_id"] in prompt_skills]
|
|
114
|
+
for skill in normalized_skills:
|
|
115
|
+
result["matching"].setdefault(
|
|
116
|
+
f"skill_tools:{skill['skill_id']}", {"raw_response": None, "parsed_ids": []}
|
|
117
|
+
)["execution_objects"] = [
|
|
118
|
+
tool for tool in normalized_tools if _tool_name(tool) in skill_tools.get(skill["skill_id"], [])
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
scheduled: list[tuple[str, int | None, Any]] = []
|
|
122
|
+
|
|
123
|
+
if prompt and prompt_skills:
|
|
124
|
+
matched_skills = [skill for skill in normalized_skills if skill["skill_id"] in prompt_skills]
|
|
125
|
+
result["ps"] = {
|
|
126
|
+
"matched_skills": [{"id": skill_id, "name": skill_name_map.get(skill_id, skill_id)} for skill_id in prompt_skills],
|
|
127
|
+
"result": None,
|
|
128
|
+
}
|
|
129
|
+
scheduled.append(
|
|
130
|
+
("ps", None, _check_mode(
|
|
131
|
+
mode="PS", prompt=prompt, skills=matched_skills, tools=[], llm_client=llm_client,
|
|
132
|
+
check_level=check_level,
|
|
133
|
+
))
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if prompt and prompt_tools:
|
|
137
|
+
matched_tools = [tool for tool in normalized_tools if _tool_name(tool) in prompt_tools]
|
|
138
|
+
result["pt"] = {
|
|
139
|
+
"matched_tools": prompt_tools,
|
|
140
|
+
"result": None,
|
|
141
|
+
}
|
|
142
|
+
scheduled.append(
|
|
143
|
+
("pt", None, _check_mode(
|
|
144
|
+
mode="PT", prompt=prompt, skills=[], tools=matched_tools, llm_client=llm_client,
|
|
145
|
+
check_level=check_level,
|
|
146
|
+
))
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if normalized_skills and normalized_tools:
|
|
150
|
+
for skill in normalized_skills:
|
|
151
|
+
matched_tool_names = skill_tools.get(skill["skill_id"], [])
|
|
152
|
+
if not matched_tool_names:
|
|
153
|
+
result["st"].append(
|
|
154
|
+
{
|
|
155
|
+
"skill_id": skill["skill_id"],
|
|
156
|
+
"skill_name": skill["skill_name"],
|
|
157
|
+
"matched_tools": [],
|
|
158
|
+
"result": None,
|
|
159
|
+
"skip_reason": "No matching Tool was found; ST inspection was skipped.",
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
continue
|
|
163
|
+
matched_tools = [tool for tool in normalized_tools if _tool_name(tool) in matched_tool_names]
|
|
164
|
+
result["st"].append(
|
|
165
|
+
{
|
|
166
|
+
"skill_id": skill["skill_id"],
|
|
167
|
+
"skill_name": skill["skill_name"],
|
|
168
|
+
"matched_tools": matched_tool_names,
|
|
169
|
+
"result": None,
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
scheduled.append(
|
|
173
|
+
("st", len(result["st"]) - 1, _check_mode(
|
|
174
|
+
mode="ST", prompt=None, skills=[skill], tools=matched_tools, llm_client=llm_client,
|
|
175
|
+
check_level=check_level,
|
|
176
|
+
))
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if scheduled:
|
|
180
|
+
completed = await asyncio.gather(*(task for _, _, task in scheduled))
|
|
181
|
+
for (mode, st_index, _), checked in zip(scheduled, completed):
|
|
182
|
+
if mode == "ps":
|
|
183
|
+
result["ps"]["result"] = checked
|
|
184
|
+
elif mode == "pt":
|
|
185
|
+
result["pt"]["result"] = checked
|
|
186
|
+
else:
|
|
187
|
+
result["st"][st_index]["result"] = checked
|
|
188
|
+
return result
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
__all__ = ["CrossChecker", "CrossMatcher", "check_cross"]
|