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.
Files changed (76) hide show
  1. defect_check/__init__.py +31 -0
  2. defect_check/contracts/__init__.py +23 -0
  3. defect_check/contracts/adapters.py +287 -0
  4. defect_check/contracts/models.py +159 -0
  5. defect_check/cross/__init__.py +191 -0
  6. defect_check/cross/checker.py +332 -0
  7. defect_check/cross/matcher.py +295 -0
  8. defect_check/cross/models.py +54 -0
  9. defect_check/cross/prompt_builder.py +67 -0
  10. defect_check/cross/scoring.py +385 -0
  11. defect_check/defect_check_main.py +278 -0
  12. defect_check/llm/__init__.py +24 -0
  13. defect_check/llm/adapters/__init__.py +5 -0
  14. defect_check/llm/adapters/anthropic_adapter.py +343 -0
  15. defect_check/llm/adapters/base.py +89 -0
  16. defect_check/llm/adapters/dashscope_adapter.py +161 -0
  17. defect_check/llm/adapters/openai_adapter.py +373 -0
  18. defect_check/llm/base.py +61 -0
  19. defect_check/llm/config.py +68 -0
  20. defect_check/llm/exceptions.py +52 -0
  21. defect_check/llm/factory.py +44 -0
  22. defect_check/llm/messages.py +147 -0
  23. defect_check/llm/providers/__init__.py +4 -0
  24. defect_check/llm/providers/anthropic.py +160 -0
  25. defect_check/llm/providers/dashscope.py +46 -0
  26. defect_check/llm/providers/openai.py +144 -0
  27. defect_check/llm/text_client.py +58 -0
  28. defect_check/lookup_table.py +394 -0
  29. defect_check/models.py +51 -0
  30. defect_check/orchestrator.py +162 -0
  31. defect_check/qdp/__init__.py +58 -0
  32. defect_check/qdp/checker.py +321 -0
  33. defect_check/qdp/models.py +206 -0
  34. defect_check/qdp/prompt_builder.py +47 -0
  35. defect_check/qdp/scoring.py +245 -0
  36. defect_check/qds/__init__.py +21 -0
  37. defect_check/qds/checker/__init__.py +7 -0
  38. defect_check/qds/checker/parallel.py +194 -0
  39. defect_check/qds/checker/parser.py +70 -0
  40. defect_check/qds/checker/prompts.py +24 -0
  41. defect_check/qds/checklist.py +1237 -0
  42. defect_check/qds/config.py +37 -0
  43. defect_check/qds/models.py +100 -0
  44. defect_check/qds/prompt_builder.py +43 -0
  45. defect_check/qds/qds0/__init__.py +7 -0
  46. defect_check/qds/qds0/checker.py +135 -0
  47. defect_check/qds/qds0/parser.py +55 -0
  48. defect_check/qds/qds0/validators.py +70 -0
  49. defect_check/qds/scoring.py +569 -0
  50. defect_check/qds/service.py +176 -0
  51. defect_check/qds/view/__init__.py +6 -0
  52. defect_check/qds/view/generator.py +129 -0
  53. defect_check/qds/view/patterns.py +184 -0
  54. defect_check/qdt/__init__.py +116 -0
  55. defect_check/qdt/checker.py +1202 -0
  56. defect_check/qdt/models.py +54 -0
  57. defect_check/qdt/prompt_builder.py +295 -0
  58. defect_check/qdt/scoring.py +335 -0
  59. defect_check/rating/__init__.py +38 -0
  60. defect_check/rating/determiner.py +154 -0
  61. defect_check/rating/keywords.py +100 -0
  62. defect_check/rating/level.py +67 -0
  63. defect_check/resources/__init__.py +19 -0
  64. defect_check/resources/config/cross_scoring.yaml +419 -0
  65. defect_check/resources/config/qdp_scoring.yaml +505 -0
  66. defect_check/resources/config/qds_scoring.yaml +1044 -0
  67. defect_check/resources/config/qdt_scoring.yaml +414 -0
  68. defect_check/resources/prompts/prompt_cross.yaml +366 -0
  69. defect_check/resources/prompts/prompt_qdp.yaml +497 -0
  70. defect_check/resources/prompts/prompt_qds.yaml +39 -0
  71. defect_check/resources/prompts/prompt_qdt.yaml +622 -0
  72. defect_check-0.0.1.dist-info/METADATA +355 -0
  73. defect_check-0.0.1.dist-info/RECORD +76 -0
  74. defect_check-0.0.1.dist-info/WHEEL +5 -0
  75. defect_check-0.0.1.dist-info/licenses/LICENSE +201 -0
  76. defect_check-0.0.1.dist-info/top_level.txt +1 -0
@@ -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"]