agentflow-proof 1.0.0__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.
- agentflow/__init__.py +5 -0
- agentflow/__main__.py +5 -0
- agentflow/aggregate.py +1015 -0
- agentflow/artifacts.py +331 -0
- agentflow/capabilities.py +124 -0
- agentflow/cli.py +2108 -0
- agentflow/cli_contract.py +238 -0
- agentflow/contracts.py +263 -0
- agentflow/coverage.py +438 -0
- agentflow/draft_plan.py +402 -0
- agentflow/events.py +201 -0
- agentflow/execution.py +1027 -0
- agentflow/execution_coverage.py +646 -0
- agentflow/git.py +89 -0
- agentflow/handoff.py +112 -0
- agentflow/hunks.py +245 -0
- agentflow/locks.py +71 -0
- agentflow/mcp_server.py +710 -0
- agentflow/packs.py +398 -0
- agentflow/porcelain.py +1014 -0
- agentflow/proof.py +1062 -0
- agentflow/receipts.py +604 -0
- agentflow/recommend.py +299 -0
- agentflow/review.py +924 -0
- agentflow/review_runner.py +516 -0
- agentflow/risk.py +328 -0
- agentflow/runtime.py +863 -0
- agentflow/stuck.py +344 -0
- agentflow/validation.py +696 -0
- agentflow/versioning.py +120 -0
- agentflow/viewer.py +391 -0
- agentflow/workflow_contract.py +164 -0
- agentflow_proof-1.0.0.dist-info/METADATA +524 -0
- agentflow_proof-1.0.0.dist-info/RECORD +38 -0
- agentflow_proof-1.0.0.dist-info/WHEEL +5 -0
- agentflow_proof-1.0.0.dist-info/entry_points.txt +3 -0
- agentflow_proof-1.0.0.dist-info/licenses/LICENSE +21 -0
- agentflow_proof-1.0.0.dist-info/top_level.txt +1 -0
agentflow/coverage.py
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
"""Cross-artifact coverage and referential-integrity checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional, Set
|
|
6
|
+
|
|
7
|
+
from .contracts import recorded_review_depth, review_depth_satisfies
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_CRITERION_STATUSES = ("satisfied", "failed", "missing", "unmapped")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _step_ids(plan: Dict[str, Any]) -> Set[str]:
|
|
14
|
+
return {
|
|
15
|
+
step["id"]
|
|
16
|
+
for step in plan.get("steps", [])
|
|
17
|
+
if isinstance(step, dict) and isinstance(step.get("id"), str)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_coverage(
|
|
22
|
+
plan: Dict[str, Any],
|
|
23
|
+
evidence: List[Dict[str, Any]],
|
|
24
|
+
context_receipts: List[Dict[str, Any]],
|
|
25
|
+
runtime_config: Optional[Dict[str, Any]] = None,
|
|
26
|
+
) -> Dict[str, Any]:
|
|
27
|
+
steps = _step_ids(plan)
|
|
28
|
+
step_evidence_ids: Set[str] = set()
|
|
29
|
+
step_evidence_by_step: Dict[str, Set[str]] = {}
|
|
30
|
+
for step in plan.get("steps", []):
|
|
31
|
+
if not isinstance(step, dict):
|
|
32
|
+
continue
|
|
33
|
+
step_id = step.get("id")
|
|
34
|
+
ids = step.get("evidence_ids", [])
|
|
35
|
+
if isinstance(step_id, str):
|
|
36
|
+
step_evidence_by_step[step_id] = set()
|
|
37
|
+
for evidence_id in ids if isinstance(ids, list) else []:
|
|
38
|
+
if isinstance(evidence_id, str):
|
|
39
|
+
step_evidence_ids.add(evidence_id)
|
|
40
|
+
if isinstance(step_id, str):
|
|
41
|
+
step_evidence_by_step[step_id].add(evidence_id)
|
|
42
|
+
|
|
43
|
+
evidence_by_id = {
|
|
44
|
+
item["id"]: item
|
|
45
|
+
for item in evidence
|
|
46
|
+
if isinstance(item, dict) and isinstance(item.get("id"), str)
|
|
47
|
+
}
|
|
48
|
+
plan_evidence_ids = {
|
|
49
|
+
evidence_id
|
|
50
|
+
for evidence_id in plan.get("evidence_ids", [])
|
|
51
|
+
if isinstance(evidence_id, str)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
receipt_steps: Set[str] = set()
|
|
55
|
+
dangling_used_for: List[Dict[str, str]] = []
|
|
56
|
+
for receipt in context_receipts:
|
|
57
|
+
receipt_id = str(receipt.get("id", ""))
|
|
58
|
+
used_for = receipt.get("used_for", [])
|
|
59
|
+
if not isinstance(used_for, list):
|
|
60
|
+
continue
|
|
61
|
+
for step_id in used_for:
|
|
62
|
+
if not isinstance(step_id, str):
|
|
63
|
+
continue
|
|
64
|
+
if step_id in steps:
|
|
65
|
+
receipt_steps.add(step_id)
|
|
66
|
+
else:
|
|
67
|
+
dangling_used_for.append({"receipt_id": receipt_id, "step_id": step_id})
|
|
68
|
+
|
|
69
|
+
dangling_supports: List[Dict[str, str]] = []
|
|
70
|
+
supported_by_evidence: Set[str] = set()
|
|
71
|
+
evidence_supporting_steps: Set[str] = set()
|
|
72
|
+
for item in evidence:
|
|
73
|
+
evidence_id = str(item.get("id", ""))
|
|
74
|
+
supports = item.get("supports", [])
|
|
75
|
+
if not isinstance(supports, list):
|
|
76
|
+
continue
|
|
77
|
+
for step_id in supports:
|
|
78
|
+
if not isinstance(step_id, str):
|
|
79
|
+
continue
|
|
80
|
+
if step_id in steps:
|
|
81
|
+
supported_by_evidence.add(step_id)
|
|
82
|
+
evidence_supporting_steps.add(evidence_id)
|
|
83
|
+
else:
|
|
84
|
+
dangling_supports.append({"evidence_id": evidence_id, "step_id": step_id})
|
|
85
|
+
|
|
86
|
+
referenced_evidence_ids = plan_evidence_ids | step_evidence_ids
|
|
87
|
+
missing_plan_evidence_ids = sorted(
|
|
88
|
+
evidence_id for evidence_id in referenced_evidence_ids if evidence_id not in evidence_by_id
|
|
89
|
+
)
|
|
90
|
+
used_evidence_ids = step_evidence_ids | plan_evidence_ids | evidence_supporting_steps
|
|
91
|
+
unused_evidence_ids = sorted(
|
|
92
|
+
evidence_id
|
|
93
|
+
for evidence_id, item in evidence_by_id.items()
|
|
94
|
+
if evidence_id not in used_evidence_ids and item.get("kind") != "review"
|
|
95
|
+
)
|
|
96
|
+
steps_with_step_evidence = {
|
|
97
|
+
step_id
|
|
98
|
+
for step_id, evidence_ids in step_evidence_by_step.items()
|
|
99
|
+
if any(evidence_id in evidence_by_id for evidence_id in evidence_ids)
|
|
100
|
+
}
|
|
101
|
+
steps_without_support = sorted(
|
|
102
|
+
step_id
|
|
103
|
+
for step_id in steps
|
|
104
|
+
if step_id not in steps_with_step_evidence
|
|
105
|
+
and step_id not in supported_by_evidence
|
|
106
|
+
and step_id not in receipt_steps
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
dangling_route_runtimes: List[Dict[str, str]] = []
|
|
110
|
+
if runtime_config:
|
|
111
|
+
runtimes = runtime_config.get("runtimes", {})
|
|
112
|
+
runtime_ids = set(runtimes.keys()) if isinstance(runtimes, dict) else set()
|
|
113
|
+
routes = runtime_config.get("routes", {})
|
|
114
|
+
default_runtime = runtime_config.get("default_runtime")
|
|
115
|
+
if isinstance(routes, dict):
|
|
116
|
+
for route_name, route in routes.items():
|
|
117
|
+
if not isinstance(route, dict):
|
|
118
|
+
continue
|
|
119
|
+
primary = route.get("primary")
|
|
120
|
+
primary_field = "primary"
|
|
121
|
+
if primary is None and isinstance(default_runtime, str):
|
|
122
|
+
primary = default_runtime
|
|
123
|
+
primary_field = "default_runtime"
|
|
124
|
+
if isinstance(primary, str) and primary not in runtime_ids:
|
|
125
|
+
dangling_route_runtimes.append(
|
|
126
|
+
{"route": route_name, "field": primary_field, "runtime": primary}
|
|
127
|
+
)
|
|
128
|
+
fallbacks = route.get("fallbacks", [])
|
|
129
|
+
if isinstance(fallbacks, list):
|
|
130
|
+
for fallback in fallbacks:
|
|
131
|
+
if isinstance(fallback, str) and fallback not in runtime_ids:
|
|
132
|
+
dangling_route_runtimes.append(
|
|
133
|
+
{
|
|
134
|
+
"route": route_name,
|
|
135
|
+
"field": "fallbacks",
|
|
136
|
+
"runtime": fallback,
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
"steps_without_support": steps_without_support,
|
|
142
|
+
"missing_plan_evidence_ids": missing_plan_evidence_ids,
|
|
143
|
+
"unused_evidence_ids": unused_evidence_ids,
|
|
144
|
+
"dangling_supports": dangling_supports,
|
|
145
|
+
"dangling_used_for": dangling_used_for,
|
|
146
|
+
"dangling_route_runtimes": dangling_route_runtimes,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _wraps_run(command: Any, run: List[str]) -> bool:
|
|
151
|
+
# env-style wrappers prepend argv (["env", "K=V", *run]); anything that
|
|
152
|
+
# rewrites or extends the gate command is not evidence for that gate.
|
|
153
|
+
return (
|
|
154
|
+
isinstance(command, list)
|
|
155
|
+
and len(command) > len(run)
|
|
156
|
+
and command[-len(run):] == run
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _latest_command_receipt(
|
|
161
|
+
step_id: str,
|
|
162
|
+
gate: Dict[str, Any],
|
|
163
|
+
validation_alias: Optional[str],
|
|
164
|
+
receipts: List[Dict[str, Any]],
|
|
165
|
+
) -> Optional[Dict[str, Any]]:
|
|
166
|
+
run = gate.get("run")
|
|
167
|
+
if not isinstance(run, list):
|
|
168
|
+
return None
|
|
169
|
+
aliases = {item for item in (" ".join(run), validation_alias) if item}
|
|
170
|
+
for receipt in reversed(receipts):
|
|
171
|
+
if receipt.get("step_id") != step_id:
|
|
172
|
+
continue
|
|
173
|
+
command = receipt.get("command")
|
|
174
|
+
if command == run:
|
|
175
|
+
return receipt
|
|
176
|
+
# A gate label alone is not criterion proof: the labeled receipt must
|
|
177
|
+
# still have run the gate command (allowing env-prefix wrapping).
|
|
178
|
+
if receipt.get("gate") in aliases and _wraps_run(command, run):
|
|
179
|
+
return receipt
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _combined_status(statuses: List[str]) -> str:
|
|
184
|
+
for status in ("failed", "missing", "unmapped"):
|
|
185
|
+
if status in statuses:
|
|
186
|
+
return status
|
|
187
|
+
return "satisfied"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def build_requirement_coverage(
|
|
191
|
+
plan: Dict[str, Any],
|
|
192
|
+
evidence: List[Dict[str, Any]],
|
|
193
|
+
receipts: List[Dict[str, Any]],
|
|
194
|
+
review_runs: List[Dict[str, Any]],
|
|
195
|
+
plan_sha256: str,
|
|
196
|
+
) -> Dict[str, Any]:
|
|
197
|
+
"""Project optional plan criteria onto explicitly mapped deterministic evidence."""
|
|
198
|
+
requirements = plan.get("requirements")
|
|
199
|
+
if not isinstance(requirements, list) or not requirements:
|
|
200
|
+
return {}
|
|
201
|
+
|
|
202
|
+
steps_by_criterion: Dict[str, List[str]] = {}
|
|
203
|
+
evidence_by_criterion: Dict[str, List[Dict[str, Any]]] = {}
|
|
204
|
+
recorded_evidence_ids = {
|
|
205
|
+
item["id"]
|
|
206
|
+
for item in evidence
|
|
207
|
+
if isinstance(item, dict) and isinstance(item.get("id"), str)
|
|
208
|
+
}
|
|
209
|
+
for step in plan.get("steps", []):
|
|
210
|
+
if not isinstance(step, dict) or not isinstance(step.get("id"), str):
|
|
211
|
+
continue
|
|
212
|
+
step_id = step["id"]
|
|
213
|
+
for criterion_id in step.get("criterion_ids", []):
|
|
214
|
+
if isinstance(criterion_id, str):
|
|
215
|
+
steps_by_criterion.setdefault(criterion_id, []).append(step_id)
|
|
216
|
+
validation = step.get("validation", [])
|
|
217
|
+
for gate_index, gate in enumerate(step.get("gates", [])):
|
|
218
|
+
if not isinstance(gate, dict):
|
|
219
|
+
continue
|
|
220
|
+
if gate.get("kind") == "inspection":
|
|
221
|
+
evidence_id = gate.get("evidence_id")
|
|
222
|
+
status = (
|
|
223
|
+
"satisfied" if evidence_id in recorded_evidence_ids else "missing"
|
|
224
|
+
)
|
|
225
|
+
for criterion_id in gate.get("criterion_ids", []):
|
|
226
|
+
if isinstance(criterion_id, str):
|
|
227
|
+
evidence_by_criterion.setdefault(criterion_id, []).append(
|
|
228
|
+
{
|
|
229
|
+
"kind": "inspection",
|
|
230
|
+
"step_id": step_id,
|
|
231
|
+
"evidence_id": evidence_id,
|
|
232
|
+
"status": status,
|
|
233
|
+
}
|
|
234
|
+
)
|
|
235
|
+
continue
|
|
236
|
+
if gate.get("kind") != "command":
|
|
237
|
+
continue
|
|
238
|
+
run = gate.get("run")
|
|
239
|
+
if not isinstance(run, list):
|
|
240
|
+
continue
|
|
241
|
+
validation_alias = (
|
|
242
|
+
validation[gate_index]
|
|
243
|
+
if isinstance(validation, list)
|
|
244
|
+
and gate_index < len(validation)
|
|
245
|
+
and isinstance(validation[gate_index], str)
|
|
246
|
+
else None
|
|
247
|
+
)
|
|
248
|
+
receipt = _latest_command_receipt(
|
|
249
|
+
step_id, gate, validation_alias, receipts
|
|
250
|
+
)
|
|
251
|
+
status = "missing"
|
|
252
|
+
if receipt is not None:
|
|
253
|
+
failed = (
|
|
254
|
+
receipt.get("exit_code") != 0
|
|
255
|
+
or receipt.get("decision") in ("blocked", "timeout")
|
|
256
|
+
or receipt.get("timed_out") is True
|
|
257
|
+
)
|
|
258
|
+
status = "failed" if failed else "satisfied"
|
|
259
|
+
for criterion_id in gate.get("criterion_ids", []):
|
|
260
|
+
if not isinstance(criterion_id, str):
|
|
261
|
+
continue
|
|
262
|
+
row = {
|
|
263
|
+
"kind": "command",
|
|
264
|
+
"step_id": step_id,
|
|
265
|
+
"gate": " ".join(run),
|
|
266
|
+
"status": status,
|
|
267
|
+
}
|
|
268
|
+
if receipt is not None and isinstance(receipt.get("id"), str):
|
|
269
|
+
row["receipt_id"] = receipt["id"]
|
|
270
|
+
evidence_by_criterion.setdefault(criterion_id, []).append(row)
|
|
271
|
+
|
|
272
|
+
projected_requirements: List[Dict[str, Any]] = []
|
|
273
|
+
counts = {status: 0 for status in _CRITERION_STATUSES}
|
|
274
|
+
for requirement in requirements:
|
|
275
|
+
projected_criteria: List[Dict[str, Any]] = []
|
|
276
|
+
for criterion in requirement.get("acceptance_criteria", []):
|
|
277
|
+
criterion_id = criterion["id"]
|
|
278
|
+
criterion_evidence = list(evidence_by_criterion.get(criterion_id, []))
|
|
279
|
+
review = criterion.get("review")
|
|
280
|
+
if isinstance(review, dict):
|
|
281
|
+
minimum_depth = review["minimum_depth"]
|
|
282
|
+
qualifying = [
|
|
283
|
+
run
|
|
284
|
+
for run in review_runs
|
|
285
|
+
if run.get("plan_sha256") == plan_sha256
|
|
286
|
+
and review_depth_satisfies(
|
|
287
|
+
recorded_review_depth(run.get("depth_profile")), minimum_depth
|
|
288
|
+
)
|
|
289
|
+
]
|
|
290
|
+
review_row: Dict[str, Any] = {
|
|
291
|
+
"kind": "review",
|
|
292
|
+
"minimum_depth": minimum_depth,
|
|
293
|
+
"status": "missing",
|
|
294
|
+
}
|
|
295
|
+
if qualifying:
|
|
296
|
+
run = qualifying[-1]
|
|
297
|
+
review_row.update(
|
|
298
|
+
{
|
|
299
|
+
"status": (
|
|
300
|
+
"satisfied"
|
|
301
|
+
if run.get("gate_status") == "pass"
|
|
302
|
+
and not run.get("active_blocking")
|
|
303
|
+
else "failed"
|
|
304
|
+
),
|
|
305
|
+
"review_run_id": run.get("review_run_id"),
|
|
306
|
+
"plan_sha256": plan_sha256,
|
|
307
|
+
"depth_profile": recorded_review_depth(
|
|
308
|
+
run.get("depth_profile")
|
|
309
|
+
),
|
|
310
|
+
}
|
|
311
|
+
)
|
|
312
|
+
criterion_evidence.append(review_row)
|
|
313
|
+
status = (
|
|
314
|
+
_combined_status([row["status"] for row in criterion_evidence])
|
|
315
|
+
if criterion_evidence
|
|
316
|
+
else "unmapped"
|
|
317
|
+
)
|
|
318
|
+
counts[status] += 1
|
|
319
|
+
projected_criteria.append(
|
|
320
|
+
{
|
|
321
|
+
"id": criterion_id,
|
|
322
|
+
"text": criterion["text"],
|
|
323
|
+
"status": status,
|
|
324
|
+
"steps": steps_by_criterion.get(criterion_id, []),
|
|
325
|
+
"evidence": criterion_evidence,
|
|
326
|
+
}
|
|
327
|
+
)
|
|
328
|
+
projected_requirements.append(
|
|
329
|
+
{
|
|
330
|
+
"id": requirement["id"],
|
|
331
|
+
"text": requirement["text"],
|
|
332
|
+
"status": _combined_status(
|
|
333
|
+
[criterion["status"] for criterion in projected_criteria]
|
|
334
|
+
),
|
|
335
|
+
"acceptance_criteria": projected_criteria,
|
|
336
|
+
}
|
|
337
|
+
)
|
|
338
|
+
return {
|
|
339
|
+
"requirements": projected_requirements,
|
|
340
|
+
"criterion_status_counts": counts,
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def build_design_decision_coverage(plan: Dict[str, Any]) -> Dict[str, Any]:
|
|
345
|
+
"""Project optional design decisions in canonical plan order."""
|
|
346
|
+
decisions = plan.get("design_decisions")
|
|
347
|
+
if not isinstance(decisions, list) or not decisions:
|
|
348
|
+
return {}
|
|
349
|
+
|
|
350
|
+
steps_by_decision: Dict[str, List[str]] = {}
|
|
351
|
+
for step in plan.get("steps", []):
|
|
352
|
+
if not isinstance(step, dict) or not isinstance(step.get("id"), str):
|
|
353
|
+
continue
|
|
354
|
+
for decision_id in step.get("design_decision_ids", []):
|
|
355
|
+
if isinstance(decision_id, str):
|
|
356
|
+
steps_by_decision.setdefault(decision_id, []).append(step["id"])
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
"design_decisions": [
|
|
360
|
+
{
|
|
361
|
+
"id": decision["id"],
|
|
362
|
+
"text": decision["text"],
|
|
363
|
+
"references": list(decision.get("references", [])),
|
|
364
|
+
"steps": list(steps_by_decision.get(decision["id"], [])),
|
|
365
|
+
}
|
|
366
|
+
for decision in decisions
|
|
367
|
+
]
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def evaluate_context_budget(
|
|
372
|
+
plan: Dict[str, Any],
|
|
373
|
+
context_receipts: List[Dict[str, Any]],
|
|
374
|
+
failures: List[Dict[str, Any]],
|
|
375
|
+
) -> List[Dict[str, Any]]:
|
|
376
|
+
budget = plan.get("context_budget", {})
|
|
377
|
+
if not isinstance(budget, dict):
|
|
378
|
+
return []
|
|
379
|
+
|
|
380
|
+
findings: List[Dict[str, Any]] = []
|
|
381
|
+
total_bytes = sum(
|
|
382
|
+
item.get("bytes", 0)
|
|
383
|
+
for item in context_receipts
|
|
384
|
+
if isinstance(item.get("bytes", 0), int)
|
|
385
|
+
)
|
|
386
|
+
distinct_sources = {
|
|
387
|
+
item.get("source")
|
|
388
|
+
for item in context_receipts
|
|
389
|
+
if isinstance(item.get("source"), str)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
max_total_bytes = budget.get("max_total_bytes")
|
|
393
|
+
if isinstance(max_total_bytes, int) and total_bytes > max_total_bytes:
|
|
394
|
+
findings.append(
|
|
395
|
+
{
|
|
396
|
+
"id": "context_max_total_bytes_exceeded",
|
|
397
|
+
"status": "warning",
|
|
398
|
+
"severity": "warning",
|
|
399
|
+
"message": (
|
|
400
|
+
f"context receipts total {total_bytes} bytes exceeds "
|
|
401
|
+
f"max_total_bytes {max_total_bytes}"
|
|
402
|
+
),
|
|
403
|
+
}
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
max_files = budget.get("max_files")
|
|
407
|
+
if isinstance(max_files, int) and len(distinct_sources) > max_files:
|
|
408
|
+
findings.append(
|
|
409
|
+
{
|
|
410
|
+
"id": "context_max_files_exceeded",
|
|
411
|
+
"status": "warning",
|
|
412
|
+
"severity": "warning",
|
|
413
|
+
"message": (
|
|
414
|
+
f"context receipts use {len(distinct_sources)} sources "
|
|
415
|
+
f"exceeding max_files {max_files}"
|
|
416
|
+
),
|
|
417
|
+
}
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
max_log_lines = budget.get("max_log_lines_per_failure")
|
|
421
|
+
if isinstance(max_log_lines, int):
|
|
422
|
+
for failure in failures:
|
|
423
|
+
relevant_lines = failure.get("relevant_lines", [])
|
|
424
|
+
if isinstance(relevant_lines, list) and len(relevant_lines) > max_log_lines:
|
|
425
|
+
findings.append(
|
|
426
|
+
{
|
|
427
|
+
"id": "failure_log_lines_exceeded",
|
|
428
|
+
"status": "warning",
|
|
429
|
+
"severity": "info",
|
|
430
|
+
"message": (
|
|
431
|
+
f"failure for {failure.get('command', 'unknown')} records "
|
|
432
|
+
f"{len(relevant_lines)} relevant lines exceeding "
|
|
433
|
+
f"max_log_lines_per_failure {max_log_lines}"
|
|
434
|
+
),
|
|
435
|
+
}
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
return findings
|