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/artifacts.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""Artifact defaults and filesystem helpers for Agentflow."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from contextvars import ContextVar
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
12
|
+
|
|
13
|
+
from .contracts import (
|
|
14
|
+
ARTIFACT_COMPATIBILITY_POLICIES,
|
|
15
|
+
ARTIFACT_PATHS,
|
|
16
|
+
ARTIFACT_SCHEMA_VERSIONS,
|
|
17
|
+
ASSUMPTIONS_SCHEMA_VERSION,
|
|
18
|
+
CONTEXT_RECEIPTS_SCHEMA_VERSION,
|
|
19
|
+
DRIFT_REPORT_SCHEMA_VERSION,
|
|
20
|
+
EVIDENCE_SCHEMA_VERSION,
|
|
21
|
+
FAILURES_SCHEMA_VERSION,
|
|
22
|
+
PLAN_SCHEMA_VERSION,
|
|
23
|
+
PROOF_PACK_SCHEMA_VERSION,
|
|
24
|
+
EXECUTION_ARTIFACT_SCHEMA_VERSIONS,
|
|
25
|
+
)
|
|
26
|
+
from .versioning import validate_schema_version_policy
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
SCHEMA_VERSION = PLAN_SCHEMA_VERSION
|
|
30
|
+
_VALIDATE_ARTIFACT_VERSIONS = ContextVar(
|
|
31
|
+
"agentflow_validate_artifact_versions", default=True
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def utc_now() -> str:
|
|
36
|
+
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def dumps_json(data: Dict[str, Any]) -> str:
|
|
40
|
+
return json.dumps(data, indent=2, sort_keys=True) + "\n"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# lock-plan restamps locked/locked_at on every re-lock, so a raw file hash
|
|
44
|
+
# would invalidate plan-bound evidence even when nothing semantic changed.
|
|
45
|
+
_PLAN_LOCK_METADATA_FIELDS = ("locked", "locked_at")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def plan_binding_sha256(plan: Dict[str, Any]) -> str:
|
|
49
|
+
"""Canonical content hash used to bind evidence to a plan.
|
|
50
|
+
|
|
51
|
+
Excludes lock bookkeeping so a no-op re-lock keeps existing bindings;
|
|
52
|
+
any semantic plan change still invalidates them.
|
|
53
|
+
"""
|
|
54
|
+
content = {
|
|
55
|
+
key: value
|
|
56
|
+
for key, value in plan.items()
|
|
57
|
+
if key not in _PLAN_LOCK_METADATA_FIELDS
|
|
58
|
+
}
|
|
59
|
+
payload = json.dumps(content, sort_keys=True, separators=(",", ":"))
|
|
60
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def default_plan() -> Dict[str, Any]:
|
|
64
|
+
return {
|
|
65
|
+
"schema_version": PLAN_SCHEMA_VERSION,
|
|
66
|
+
"objective": "",
|
|
67
|
+
"scope": [],
|
|
68
|
+
"non_goals": [],
|
|
69
|
+
"invariants": [],
|
|
70
|
+
"allowed_files": [],
|
|
71
|
+
"blocked_files": [],
|
|
72
|
+
"validation_gates": [],
|
|
73
|
+
"rollback_plan": "",
|
|
74
|
+
"risk_level": "low",
|
|
75
|
+
"drift_budget": {
|
|
76
|
+
"unrelated_edits": 0,
|
|
77
|
+
"new_dependencies": 0,
|
|
78
|
+
"formatting_drift": "minimal",
|
|
79
|
+
"architecture_drift": "requires_approval",
|
|
80
|
+
"test_weakening": 0,
|
|
81
|
+
},
|
|
82
|
+
"steps": [],
|
|
83
|
+
"evidence_ids": [],
|
|
84
|
+
"locked": False,
|
|
85
|
+
"locked_at": None,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def default_assumptions() -> Dict[str, Any]:
|
|
90
|
+
return {
|
|
91
|
+
"schema_version": ASSUMPTIONS_SCHEMA_VERSION,
|
|
92
|
+
"assumptions": [],
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def default_drift_report() -> Dict[str, Any]:
|
|
97
|
+
return {
|
|
98
|
+
"schema_version": DRIFT_REPORT_SCHEMA_VERSION,
|
|
99
|
+
"status": "warning",
|
|
100
|
+
"changed_files": [],
|
|
101
|
+
"unmapped_hunks": [],
|
|
102
|
+
"out_of_scope_files": [],
|
|
103
|
+
"blocked_files_changed": [],
|
|
104
|
+
"dependency_changes": [],
|
|
105
|
+
"test_weakening": [],
|
|
106
|
+
"notes": ["No drift audit has been run yet."],
|
|
107
|
+
"generated_at": utc_now(),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def default_proof_pack() -> str:
|
|
112
|
+
return """# Proof Pack
|
|
113
|
+
|
|
114
|
+
## Objective
|
|
115
|
+
|
|
116
|
+
TBD.
|
|
117
|
+
|
|
118
|
+
## Scope
|
|
119
|
+
|
|
120
|
+
TBD.
|
|
121
|
+
|
|
122
|
+
## Plan Steps Completed
|
|
123
|
+
|
|
124
|
+
TBD.
|
|
125
|
+
|
|
126
|
+
## Files Changed
|
|
127
|
+
|
|
128
|
+
TBD.
|
|
129
|
+
|
|
130
|
+
## Evidence
|
|
131
|
+
|
|
132
|
+
TBD.
|
|
133
|
+
|
|
134
|
+
## Validation
|
|
135
|
+
|
|
136
|
+
TBD.
|
|
137
|
+
|
|
138
|
+
## Drift Audit
|
|
139
|
+
|
|
140
|
+
TBD.
|
|
141
|
+
|
|
142
|
+
## Remaining Risk
|
|
143
|
+
|
|
144
|
+
TBD.
|
|
145
|
+
|
|
146
|
+
## Follow-Ups
|
|
147
|
+
|
|
148
|
+
TBD.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def model_profile(provider: str, name: str, cost_tier: str) -> Dict[str, Any]:
|
|
153
|
+
return {
|
|
154
|
+
"schema_version": SCHEMA_VERSION,
|
|
155
|
+
"name": name,
|
|
156
|
+
"provider": provider,
|
|
157
|
+
"context_window": 0,
|
|
158
|
+
"supports_tool_calls": False,
|
|
159
|
+
"supports_json_schema": False,
|
|
160
|
+
"supports_vision": False,
|
|
161
|
+
"cost_tier": cost_tier,
|
|
162
|
+
"strengths": [],
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def initial_files() -> List[Tuple[str, str]]:
|
|
167
|
+
return [
|
|
168
|
+
(".agent/plan.lock.json", dumps_json(default_plan())),
|
|
169
|
+
(".agent/evidence.jsonl", ""),
|
|
170
|
+
(".agent/assumptions.json", dumps_json(default_assumptions())),
|
|
171
|
+
(".agent/context-receipts.jsonl", ""),
|
|
172
|
+
(".agent/failures.jsonl", ""),
|
|
173
|
+
(".agent/amendments.jsonl", ""),
|
|
174
|
+
(".agent/review-runs.jsonl", ""),
|
|
175
|
+
(".agent/capability-receipts.jsonl", ""),
|
|
176
|
+
(".agent/runtime-snapshots.jsonl", ""),
|
|
177
|
+
(".agent/drift-report.json", dumps_json(default_drift_report())),
|
|
178
|
+
(".agent/proof-pack.md", default_proof_pack()),
|
|
179
|
+
(
|
|
180
|
+
".agent/model-profiles/openai.example.json",
|
|
181
|
+
dumps_json(model_profile("openai", "model-id", "medium")),
|
|
182
|
+
),
|
|
183
|
+
(
|
|
184
|
+
".agent/model-profiles/anthropic.example.json",
|
|
185
|
+
dumps_json(model_profile("anthropic", "model-id", "medium")),
|
|
186
|
+
),
|
|
187
|
+
(
|
|
188
|
+
".agent/model-profiles/local.example.json",
|
|
189
|
+
dumps_json(model_profile("local", "model-id", "low")),
|
|
190
|
+
),
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _artifact_name(path: Path) -> Optional[str]:
|
|
195
|
+
candidate = path.as_posix()
|
|
196
|
+
paths = {**ARTIFACT_PATHS, "proof-pack": ".agent/proof-pack.json"}
|
|
197
|
+
return next(
|
|
198
|
+
(
|
|
199
|
+
name
|
|
200
|
+
for name, relative in paths.items()
|
|
201
|
+
if candidate == relative or candidate.endswith("/" + relative)
|
|
202
|
+
),
|
|
203
|
+
None,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _validate_artifact_version(
|
|
208
|
+
path: Path, data: Any, line_number: Optional[int] = None
|
|
209
|
+
) -> None:
|
|
210
|
+
if not _VALIDATE_ARTIFACT_VERSIONS.get():
|
|
211
|
+
return
|
|
212
|
+
artifact = _artifact_name(path)
|
|
213
|
+
reader_gated = set(ARTIFACT_COMPATIBILITY_POLICIES) - {
|
|
214
|
+
"aggregation",
|
|
215
|
+
"proof-pack",
|
|
216
|
+
}
|
|
217
|
+
if (
|
|
218
|
+
artifact not in reader_gated
|
|
219
|
+
or not isinstance(data, dict)
|
|
220
|
+
):
|
|
221
|
+
return
|
|
222
|
+
supported = EXECUTION_ARTIFACT_SCHEMA_VERSIONS.get(
|
|
223
|
+
artifact, ARTIFACT_SCHEMA_VERSIONS.get(artifact)
|
|
224
|
+
)
|
|
225
|
+
if supported is None:
|
|
226
|
+
return
|
|
227
|
+
errors = validate_schema_version_policy(
|
|
228
|
+
data.get("schema_version"),
|
|
229
|
+
supported,
|
|
230
|
+
artifact,
|
|
231
|
+
ARTIFACT_COMPATIBILITY_POLICIES[artifact],
|
|
232
|
+
)
|
|
233
|
+
if errors:
|
|
234
|
+
location = f"{path}:{line_number}" if line_number is not None else str(path)
|
|
235
|
+
raise ValueError(f"{location}: {errors[0]}")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@contextmanager
|
|
239
|
+
def historical_proof_reads() -> Iterable[None]:
|
|
240
|
+
"""Let verify-proof read its hash-bound historical source artifacts."""
|
|
241
|
+
token = _VALIDATE_ARTIFACT_VERSIONS.set(False)
|
|
242
|
+
try:
|
|
243
|
+
yield
|
|
244
|
+
finally:
|
|
245
|
+
_VALIDATE_ARTIFACT_VERSIONS.reset(token)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def read_json(path: Path) -> Dict[str, Any]:
|
|
249
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
250
|
+
data = json.load(handle)
|
|
251
|
+
_validate_artifact_version(path, data)
|
|
252
|
+
return data
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def try_read_json(path: Path) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
|
256
|
+
"""Read a JSON object without raising for malformed or non-object JSON."""
|
|
257
|
+
try:
|
|
258
|
+
data = read_json(path)
|
|
259
|
+
except json.JSONDecodeError as exc:
|
|
260
|
+
return None, f"malformed JSON in {path.name}: {exc}"
|
|
261
|
+
except ValueError as exc:
|
|
262
|
+
# Version-gate rejection: the file is well-formed JSON, so calling it
|
|
263
|
+
# malformed would send users chasing a parse error that does not exist.
|
|
264
|
+
return None, str(exc)
|
|
265
|
+
if not isinstance(data, dict):
|
|
266
|
+
return None, f"{path.name} top-level value must be a JSON object"
|
|
267
|
+
return data, None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def write_json(path: Path, data: Dict[str, Any]) -> None:
|
|
271
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
272
|
+
path.write_text(dumps_json(data), encoding="utf-8")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def append_jsonl(path: Path, item: Dict[str, Any]) -> None:
|
|
276
|
+
artifact = _artifact_name(path)
|
|
277
|
+
supported = (
|
|
278
|
+
EXECUTION_ARTIFACT_SCHEMA_VERSIONS.get(
|
|
279
|
+
artifact, ARTIFACT_SCHEMA_VERSIONS.get(artifact)
|
|
280
|
+
)
|
|
281
|
+
if artifact is not None
|
|
282
|
+
else None
|
|
283
|
+
)
|
|
284
|
+
if supported is not None and "schema_version" not in item:
|
|
285
|
+
item = {"schema_version": supported, **item}
|
|
286
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
287
|
+
with path.open("a", encoding="utf-8") as handle:
|
|
288
|
+
handle.write(json.dumps(item, sort_keys=True) + "\n")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def read_jsonl(path: Path) -> List[Dict[str, Any]]:
|
|
292
|
+
if not path.exists():
|
|
293
|
+
return []
|
|
294
|
+
|
|
295
|
+
items: List[Dict[str, Any]] = []
|
|
296
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
297
|
+
for line_number, line in enumerate(handle, start=1):
|
|
298
|
+
stripped = line.strip()
|
|
299
|
+
if not stripped:
|
|
300
|
+
continue
|
|
301
|
+
try:
|
|
302
|
+
item = json.loads(stripped)
|
|
303
|
+
_validate_artifact_version(path, item, line_number)
|
|
304
|
+
items.append(item)
|
|
305
|
+
except json.JSONDecodeError as exc:
|
|
306
|
+
raise ValueError(f"{path}:{line_number}: invalid JSONL: {exc}") from exc
|
|
307
|
+
return items
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def create_initial_artifacts(root: Path, force: bool = False) -> Tuple[List[str], List[str]]:
|
|
311
|
+
created: List[str] = []
|
|
312
|
+
skipped: List[str] = []
|
|
313
|
+
|
|
314
|
+
for relative_path, content in initial_files():
|
|
315
|
+
path = root / relative_path
|
|
316
|
+
if path.exists() and not force:
|
|
317
|
+
skipped.append(relative_path)
|
|
318
|
+
continue
|
|
319
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
320
|
+
path.write_text(content, encoding="utf-8")
|
|
321
|
+
created.append(relative_path)
|
|
322
|
+
|
|
323
|
+
return created, skipped
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def require_files(paths: Iterable[Path]) -> List[str]:
|
|
327
|
+
missing = []
|
|
328
|
+
for path in paths:
|
|
329
|
+
if not path.exists():
|
|
330
|
+
missing.append(str(path))
|
|
331
|
+
return missing
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Capability receipts: record and summarize specialized-practice usage.
|
|
2
|
+
|
|
3
|
+
Agentflow declares required capabilities (via the workflow contract) and records
|
|
4
|
+
evidence that they were used or knowingly waived. It never invokes a provider;
|
|
5
|
+
``provider`` is a free string.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from .artifacts import append_jsonl, read_jsonl, utc_now
|
|
14
|
+
from .contracts import CAPABILITY_RECEIPTS_SCHEMA_VERSION, CAPABILITY_STATUSES
|
|
15
|
+
|
|
16
|
+
CAPABILITY_RECEIPTS_PATH = ".agent/capability-receipts.jsonl"
|
|
17
|
+
|
|
18
|
+
KNOWN_FIELDS = {
|
|
19
|
+
"schema_version",
|
|
20
|
+
"id",
|
|
21
|
+
"capability",
|
|
22
|
+
"status",
|
|
23
|
+
"provider",
|
|
24
|
+
"reason",
|
|
25
|
+
"evidence",
|
|
26
|
+
"recorded_at",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _non_empty_string(value: Any) -> bool:
|
|
31
|
+
return isinstance(value, str) and bool(value.strip())
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def validate_capability_receipt(row: Any) -> List[str]:
|
|
35
|
+
errors: List[str] = []
|
|
36
|
+
if not isinstance(row, dict):
|
|
37
|
+
return ["capability receipt must be a JSON object"]
|
|
38
|
+
for field in sorted(set(row) - KNOWN_FIELDS):
|
|
39
|
+
errors.append(f"unknown capability receipt field: {field}")
|
|
40
|
+
if row.get("schema_version") != CAPABILITY_RECEIPTS_SCHEMA_VERSION:
|
|
41
|
+
errors.append(f"schema_version must be {CAPABILITY_RECEIPTS_SCHEMA_VERSION}")
|
|
42
|
+
for field in ("id", "capability", "reason", "recorded_at"):
|
|
43
|
+
if not _non_empty_string(row.get(field)):
|
|
44
|
+
errors.append(f"{field} must be a non-empty string")
|
|
45
|
+
status = row.get("status")
|
|
46
|
+
if status not in CAPABILITY_STATUSES:
|
|
47
|
+
errors.append("status must be one of: " + ", ".join(CAPABILITY_STATUSES))
|
|
48
|
+
provider = row.get("provider")
|
|
49
|
+
if status == "used":
|
|
50
|
+
if not _non_empty_string(provider):
|
|
51
|
+
errors.append("provider must be a non-empty string when status is used")
|
|
52
|
+
elif provider is not None and not _non_empty_string(provider):
|
|
53
|
+
errors.append("provider must be a non-empty string when present")
|
|
54
|
+
evidence = row.get("evidence", [])
|
|
55
|
+
if not isinstance(evidence, list) or not all(_non_empty_string(item) for item in evidence):
|
|
56
|
+
errors.append("evidence must be a list of non-empty strings")
|
|
57
|
+
return errors
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def build_capability_receipt(
|
|
61
|
+
receipt_id: str,
|
|
62
|
+
capability: str,
|
|
63
|
+
status: str,
|
|
64
|
+
reason: str,
|
|
65
|
+
provider: Optional[str] = None,
|
|
66
|
+
evidence: Optional[List[str]] = None,
|
|
67
|
+
) -> Dict[str, Any]:
|
|
68
|
+
row: Dict[str, Any] = {
|
|
69
|
+
"schema_version": CAPABILITY_RECEIPTS_SCHEMA_VERSION,
|
|
70
|
+
"id": receipt_id,
|
|
71
|
+
"capability": capability,
|
|
72
|
+
"status": status,
|
|
73
|
+
"reason": reason,
|
|
74
|
+
"evidence": list(evidence or []),
|
|
75
|
+
"recorded_at": utc_now(),
|
|
76
|
+
}
|
|
77
|
+
if provider is not None:
|
|
78
|
+
row["provider"] = provider
|
|
79
|
+
errors = validate_capability_receipt(row)
|
|
80
|
+
if errors:
|
|
81
|
+
raise ValueError("; ".join(errors))
|
|
82
|
+
return row
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def read_capability_receipts(root: Path) -> List[Dict[str, Any]]:
|
|
86
|
+
rows = read_jsonl(root / CAPABILITY_RECEIPTS_PATH)
|
|
87
|
+
for index, row in enumerate(rows, start=1):
|
|
88
|
+
errors = validate_capability_receipt(row)
|
|
89
|
+
if errors:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"capability-receipts.jsonl row {index}: " + "; ".join(errors)
|
|
92
|
+
)
|
|
93
|
+
return rows
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def append_capability_receipt(root: Path, row: Dict[str, Any]) -> None:
|
|
97
|
+
append_jsonl(root / CAPABILITY_RECEIPTS_PATH, row)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def capability_summary(root: Path, required_ids: List[str]) -> Dict[str, Any]:
|
|
101
|
+
rows = read_capability_receipts(root)
|
|
102
|
+
used = {row["capability"] for row in rows if row.get("status") == "used"}
|
|
103
|
+
waived = {row["capability"] for row in rows if row.get("status") == "waived"}
|
|
104
|
+
satisfied = used | waived
|
|
105
|
+
return {
|
|
106
|
+
"required": list(required_ids),
|
|
107
|
+
"recorded": sorted(used),
|
|
108
|
+
"waived": sorted(waived),
|
|
109
|
+
"missing": sorted({cap for cap in required_ids if cap not in satisfied}),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def capability_checks(summary: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
114
|
+
missing = summary.get("missing", [])
|
|
115
|
+
if missing:
|
|
116
|
+
return [
|
|
117
|
+
{
|
|
118
|
+
"id": "required_capabilities_satisfied",
|
|
119
|
+
"status": "warning",
|
|
120
|
+
"count": len(missing),
|
|
121
|
+
"message": "missing required capabilities: " + ", ".join(missing),
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
return [{"id": "required_capabilities_satisfied", "status": "passed", "count": 0}]
|