collective-capability-runtime 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.
- ccr/__init__.py +8 -0
- ccr/__main__.py +9 -0
- ccr/adapters/__init__.py +4 -0
- ccr/adapters/base.py +38 -0
- ccr/adapters/pic.py +205 -0
- ccr/audit/__init__.py +10 -0
- ccr/audit/pic.py +384 -0
- ccr/audit/release.py +337 -0
- ccr/audit/repo.py +486 -0
- ccr/blackboard/__init__.py +4 -0
- ccr/blackboard/events.py +45 -0
- ccr/blackboard/replay.py +16 -0
- ccr/blackboard/store.py +16 -0
- ccr/cli.py +1564 -0
- ccr/constants.py +116 -0
- ccr/data/INTEROP_PIC.md +242 -0
- ccr/data/__init__.py +4 -0
- ccr/data/agent-manifest.json +304 -0
- ccr/data/schemas/agent-manifest.schema.json +55 -0
- ccr/data/schemas/asi-proxy-threshold.schema.json +78 -0
- ccr/data/schemas/audit-report.schema.json +63 -0
- ccr/data/schemas/baseline.schema.json +43 -0
- ccr/data/schemas/blackboard-event.schema.json +50 -0
- ccr/data/schemas/effective-graph.schema.json +91 -0
- ccr/data/schemas/packet.schema.json +1065 -0
- ccr/data/schemas/phase-certificate-candidate.schema.json +54 -0
- ccr/data/schemas/phase-observation.schema.json +58 -0
- ccr/data/schemas/phase-report.schema.json +34 -0
- ccr/data/schemas/phase-state.schema.json +46 -0
- ccr/data/schemas/provider.schema.json +43 -0
- ccr/data/schemas/residual.schema.json +85 -0
- ccr/data/schemas/task.schema.json +477 -0
- ccr/data/schemas/verifier-report.schema.json +43 -0
- ccr/errors.py +34 -0
- ccr/ids.py +36 -0
- ccr/io.py +79 -0
- ccr/metrics/__init__.py +4 -0
- ccr/metrics/phase.py +8 -0
- ccr/packets/__init__.py +4 -0
- ccr/packets/distill.py +21 -0
- ccr/packets/model.py +27 -0
- ccr/packets/promotion.py +219 -0
- ccr/packets/status.py +39 -0
- ccr/packets/store.py +83 -0
- ccr/paths.py +40 -0
- ccr/phase/__init__.py +21 -0
- ccr/phase/baseline.py +56 -0
- ccr/phase/certify.py +70 -0
- ccr/phase/eligibility.py +106 -0
- ccr/phase/form.py +197 -0
- ccr/phase/graph.py +194 -0
- ccr/phase/observe.py +107 -0
- ccr/phase/threshold.py +93 -0
- ccr/providers/__init__.py +8 -0
- ccr/providers/base.py +45 -0
- ccr/providers/http.py +154 -0
- ccr/providers/pic.py +107 -0
- ccr/providers/registry.py +23 -0
- ccr/py.typed +1 -0
- ccr/reports/__init__.py +4 -0
- ccr/reports/json_report.py +42 -0
- ccr/reports/markdown.py +46 -0
- ccr/residuals/__init__.py +4 -0
- ccr/residuals/model.py +76 -0
- ccr/residuals/store.py +59 -0
- ccr/runtime/__init__.py +4 -0
- ccr/runtime/config.py +25 -0
- ccr/runtime/init.py +68 -0
- ccr/runtime/state.py +34 -0
- ccr/schemas/__init__.py +4 -0
- ccr/schemas/loader.py +88 -0
- ccr/schemas/validation.py +84 -0
- ccr/storage/__init__.py +24 -0
- ccr/storage/sqlite.py +305 -0
- ccr/tasks/__init__.py +4 -0
- ccr/tasks/lease.py +142 -0
- ccr/tasks/model.py +39 -0
- ccr/tasks/scheduler.py +29 -0
- ccr/tasks/store.py +66 -0
- ccr/time.py +71 -0
- collective_capability_runtime-1.0.0.dist-info/METADATA +303 -0
- collective_capability_runtime-1.0.0.dist-info/RECORD +86 -0
- collective_capability_runtime-1.0.0.dist-info/WHEEL +4 -0
- collective_capability_runtime-1.0.0.dist-info/entry_points.txt +2 -0
- collective_capability_runtime-1.0.0.dist-info/licenses/LICENSE +181 -0
- collective_capability_runtime-1.0.0.dist-info/licenses/NOTICE +7 -0
ccr/__init__.py
ADDED
ccr/__main__.py
ADDED
ccr/adapters/__init__.py
ADDED
ccr/adapters/base.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Base verifier provider interface."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseVerifierProvider(ABC):
|
|
11
|
+
"""Common interface for optional verifier providers."""
|
|
12
|
+
|
|
13
|
+
provider_name: str
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def availability(self) -> dict[str, Any]:
|
|
17
|
+
"""Return provider availability data."""
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def plan_verify(
|
|
21
|
+
self, packet: dict[str, Any], *, profile: str, packet_path: str
|
|
22
|
+
) -> dict[str, Any]:
|
|
23
|
+
"""Return a dry-run verification plan."""
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def execute_verify(
|
|
27
|
+
self,
|
|
28
|
+
packet: dict[str, Any],
|
|
29
|
+
*,
|
|
30
|
+
profile: str,
|
|
31
|
+
packet_path: str,
|
|
32
|
+
timeout_seconds: int,
|
|
33
|
+
) -> dict[str, Any]:
|
|
34
|
+
"""Execute verification only after explicit operator request."""
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def normalize_report(self, report: dict[str, Any]) -> dict[str, Any]:
|
|
38
|
+
"""Normalize a provider report into CCR structures."""
|
ccr/adapters/pic.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Optional PIC verifier provider adapter."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import importlib.util
|
|
7
|
+
import json
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ccr.adapters.base import BaseVerifierProvider
|
|
14
|
+
from ccr.ids import stable_id
|
|
15
|
+
from ccr.io import json_file_name
|
|
16
|
+
from ccr.packets.distill import packet_summary_text
|
|
17
|
+
from ccr.time import now_iso
|
|
18
|
+
|
|
19
|
+
PIC_PROFILES = {"development", "research", "controlled", "federated", "production", "adversarial"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PicVerifierProvider(BaseVerifierProvider):
|
|
23
|
+
"""PIC adapter with dry-run planning by default."""
|
|
24
|
+
|
|
25
|
+
provider_name = "pic"
|
|
26
|
+
|
|
27
|
+
def availability(self) -> dict[str, Any]:
|
|
28
|
+
"""Return PIC availability without importing it as a hard dependency."""
|
|
29
|
+
|
|
30
|
+
executable = shutil.which("pic")
|
|
31
|
+
module_available = importlib.util.find_spec("percolation_inversion_compiler") is not None
|
|
32
|
+
return {
|
|
33
|
+
"available": bool(executable or module_available),
|
|
34
|
+
"executable": executable,
|
|
35
|
+
"module_available": module_available,
|
|
36
|
+
"provider": self.provider_name,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
def plan_verify(
|
|
40
|
+
self, packet: dict[str, Any], *, profile: str, packet_path: str
|
|
41
|
+
) -> dict[str, Any]:
|
|
42
|
+
"""Build a PIC dry-run plan. The command is never executed here."""
|
|
43
|
+
|
|
44
|
+
if profile not in PIC_PROFILES:
|
|
45
|
+
raise ValueError(f"unknown PIC profile: {profile}")
|
|
46
|
+
text = packet_summary_text(packet)
|
|
47
|
+
argv = ["pic", "agent", "check", "--compact", "--text", text, "--profile", profile]
|
|
48
|
+
return {
|
|
49
|
+
"dry_run": True,
|
|
50
|
+
"expected_import_location": "reports/pic/",
|
|
51
|
+
"packet_id": packet.get("packet_id"),
|
|
52
|
+
"packet_path": packet_path,
|
|
53
|
+
"provider": self.provider_name,
|
|
54
|
+
"recommended_alternate_argv": ["pic", "packet", "inspect", "--packet", packet_path],
|
|
55
|
+
"verification_argv": argv,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
def execute_verify(
|
|
59
|
+
self,
|
|
60
|
+
packet: dict[str, Any],
|
|
61
|
+
*,
|
|
62
|
+
profile: str,
|
|
63
|
+
packet_path: str,
|
|
64
|
+
timeout_seconds: int,
|
|
65
|
+
) -> dict[str, Any]:
|
|
66
|
+
"""Execute PIC through subprocess with no shell expansion."""
|
|
67
|
+
|
|
68
|
+
availability = self.availability()
|
|
69
|
+
executable = availability.get("executable")
|
|
70
|
+
if not executable:
|
|
71
|
+
return {
|
|
72
|
+
"availability": availability,
|
|
73
|
+
"error": (
|
|
74
|
+
"PIC executable 'pic' is unavailable; install PIC or run dry-run planning."
|
|
75
|
+
),
|
|
76
|
+
"ok": False,
|
|
77
|
+
"provider": self.provider_name,
|
|
78
|
+
}
|
|
79
|
+
plan = self.plan_verify(packet, profile=profile, packet_path=packet_path)
|
|
80
|
+
argv = [str(executable), *plan["verification_argv"][1:]]
|
|
81
|
+
completed = subprocess.run(
|
|
82
|
+
argv,
|
|
83
|
+
capture_output=True,
|
|
84
|
+
check=False,
|
|
85
|
+
shell=False,
|
|
86
|
+
text=True,
|
|
87
|
+
timeout=timeout_seconds,
|
|
88
|
+
)
|
|
89
|
+
stdout_json: dict[str, Any] | None = None
|
|
90
|
+
try:
|
|
91
|
+
parsed = json.loads(completed.stdout)
|
|
92
|
+
if isinstance(parsed, dict):
|
|
93
|
+
stdout_json = parsed
|
|
94
|
+
except json.JSONDecodeError:
|
|
95
|
+
stdout_json = None
|
|
96
|
+
return {
|
|
97
|
+
"argv": argv,
|
|
98
|
+
"created_at": now_iso(),
|
|
99
|
+
"ok": completed.returncode == 0,
|
|
100
|
+
"packet_id": packet.get("packet_id"),
|
|
101
|
+
"packet_path": packet_path,
|
|
102
|
+
"profile": profile,
|
|
103
|
+
"provider": self.provider_name,
|
|
104
|
+
"returncode": completed.returncode,
|
|
105
|
+
"stderr": completed.stderr,
|
|
106
|
+
"stdout": completed.stdout,
|
|
107
|
+
"stdout_json": stdout_json,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
def normalize_report(self, report: dict[str, Any]) -> dict[str, Any]:
|
|
111
|
+
"""Normalize PIC-like report fields into CCR status and residual inputs."""
|
|
112
|
+
|
|
113
|
+
source = (
|
|
114
|
+
report.get("stdout_json") if isinstance(report.get("stdout_json"), dict) else report
|
|
115
|
+
)
|
|
116
|
+
if not isinstance(source, dict):
|
|
117
|
+
source = report
|
|
118
|
+
workflow_usable = bool(source.get("workflow_usable", False))
|
|
119
|
+
accepted = bool(source.get("accepted", workflow_usable))
|
|
120
|
+
settled = bool(source.get("settled", False))
|
|
121
|
+
candidate_only_reasons = _combine_lists(
|
|
122
|
+
source,
|
|
123
|
+
"candidate_only_reasons",
|
|
124
|
+
"candidate_only",
|
|
125
|
+
)
|
|
126
|
+
settled_blockers = _combine_lists(source, "settled_blockers", "blockers")
|
|
127
|
+
missing_obligations = _combine_lists(source, "missing_obligations")
|
|
128
|
+
cannot_promote_because = _combine_lists(source, "cannot_promote_because")
|
|
129
|
+
residuals = _combine_lists(source, "residuals", "residual_ledger")
|
|
130
|
+
safe_commands = _combine_lists(source, "safe_commands", "next_safe_actions")
|
|
131
|
+
reasons = _as_list(source.get("reasons", []))
|
|
132
|
+
blocking_residuals = [
|
|
133
|
+
item
|
|
134
|
+
for item in residuals
|
|
135
|
+
if isinstance(item, dict) and bool(item.get("blocking", False))
|
|
136
|
+
]
|
|
137
|
+
status_blockers = (
|
|
138
|
+
candidate_only_reasons
|
|
139
|
+
+ settled_blockers
|
|
140
|
+
+ missing_obligations
|
|
141
|
+
+ cannot_promote_because
|
|
142
|
+
+ blocking_residuals
|
|
143
|
+
)
|
|
144
|
+
unsafe = any(
|
|
145
|
+
token in " ".join(str(item).lower() for item in reasons + settled_blockers)
|
|
146
|
+
for token in ("unsafe", "hazard", "authority", "malformed")
|
|
147
|
+
)
|
|
148
|
+
if not accepted:
|
|
149
|
+
ccr_status = "quarantined" if unsafe else "rejected"
|
|
150
|
+
elif settled and not status_blockers:
|
|
151
|
+
ccr_status = "checked"
|
|
152
|
+
elif status_blockers:
|
|
153
|
+
ccr_status = "provisional"
|
|
154
|
+
else:
|
|
155
|
+
ccr_status = "checked"
|
|
156
|
+
|
|
157
|
+
packet_id = source.get("packet_id", report.get("packet_id"))
|
|
158
|
+
profile = source.get("profile", report.get("profile", "development"))
|
|
159
|
+
import_id = stable_id("pic-import", report)
|
|
160
|
+
return {
|
|
161
|
+
"accepted": accepted,
|
|
162
|
+
"bottlenecks": _as_list(source.get("bottlenecks", [])),
|
|
163
|
+
"candidate_only_reasons": candidate_only_reasons,
|
|
164
|
+
"ccr_status": ccr_status,
|
|
165
|
+
"cannot_promote_because": cannot_promote_because,
|
|
166
|
+
"import_id": import_id,
|
|
167
|
+
"missing_obligations": missing_obligations,
|
|
168
|
+
"notes": (
|
|
169
|
+
"PIC accepted output imported as checked/provisional, not settled. "
|
|
170
|
+
"Final CCR settlement requires CCR gates."
|
|
171
|
+
),
|
|
172
|
+
"packet_id": packet_id,
|
|
173
|
+
"phase_gap_vector": source.get("phase_gap_vector"),
|
|
174
|
+
"pic_profile": profile,
|
|
175
|
+
"pic_report_type": str(source.get("report_type", source.get("type", "PICReport"))),
|
|
176
|
+
"residuals": residuals,
|
|
177
|
+
"safe_commands": safe_commands,
|
|
178
|
+
"schema_version": "ccr.pic_import.v0.1",
|
|
179
|
+
"sdk_calls": _as_list(source.get("sdk_calls", [])),
|
|
180
|
+
"settled": settled,
|
|
181
|
+
"settled_blockers": settled_blockers,
|
|
182
|
+
"settled_candidate": bool(accepted and settled),
|
|
183
|
+
"workflow_usable": workflow_usable,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _as_list(value: Any) -> list[Any]:
|
|
188
|
+
if value is None:
|
|
189
|
+
return []
|
|
190
|
+
if isinstance(value, list):
|
|
191
|
+
return value
|
|
192
|
+
return [value]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _combine_lists(source: dict[str, Any], *keys: str) -> list[Any]:
|
|
196
|
+
values: list[Any] = []
|
|
197
|
+
for key in keys:
|
|
198
|
+
values.extend(_as_list(source.get(key)))
|
|
199
|
+
return values
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def report_output_path(root: Path, report_id: str) -> Path:
|
|
203
|
+
"""Return PIC report output path."""
|
|
204
|
+
|
|
205
|
+
return root / "reports" / "pic" / json_file_name(report_id)
|
ccr/audit/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""CCR repository audit."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from ccr.audit.pic import audit_pic_compatibility
|
|
7
|
+
from ccr.audit.release import audit_release
|
|
8
|
+
from ccr.audit.repo import audit_repository
|
|
9
|
+
|
|
10
|
+
__all__ = ["audit_pic_compatibility", "audit_release", "audit_repository"]
|
ccr/audit/pic.py
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""PIC compatibility audit for the optional provider boundary."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import importlib.metadata as importlib_metadata
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ccr.errors import CCRMissingError
|
|
13
|
+
from ccr.ids import stable_id
|
|
14
|
+
from ccr.providers.pic import PicProvider
|
|
15
|
+
from ccr.residuals.model import build_residual
|
|
16
|
+
from ccr.time import now_iso
|
|
17
|
+
|
|
18
|
+
EXPECTED_PIC_COMMANDS = [
|
|
19
|
+
"pic agent check --compact",
|
|
20
|
+
"pic packet inspect",
|
|
21
|
+
"pic phase plan --compact",
|
|
22
|
+
"pic runtime collective-certify",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
SUPPORTED_PIC_IMPORT_FIELDS = [
|
|
26
|
+
"accepted",
|
|
27
|
+
"workflow_usable",
|
|
28
|
+
"settled",
|
|
29
|
+
"candidate_only_reasons",
|
|
30
|
+
"settled_blockers",
|
|
31
|
+
"safe_commands",
|
|
32
|
+
"phase_gap_vector",
|
|
33
|
+
"bottlenecks",
|
|
34
|
+
"missing_obligations",
|
|
35
|
+
"residuals",
|
|
36
|
+
"cannot_promote_because",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
PIC_ROUTE_TEXT = "python -m pip install percolation-inversion-compiler"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def default_pic_root_candidates(ccr_root: Path) -> list[Path]:
|
|
43
|
+
"""Return deterministic PIC source-root candidates for local compatibility checks."""
|
|
44
|
+
|
|
45
|
+
candidates = [
|
|
46
|
+
Path.home() / "percolation-inversion-compiler",
|
|
47
|
+
ccr_root.parent / "percolation-inversion-compiler",
|
|
48
|
+
]
|
|
49
|
+
seen: set[str] = set()
|
|
50
|
+
unique: list[Path] = []
|
|
51
|
+
for candidate in candidates:
|
|
52
|
+
key = str(candidate.resolve()) if candidate.exists() else str(candidate)
|
|
53
|
+
if key not in seen:
|
|
54
|
+
unique.append(candidate)
|
|
55
|
+
seen.add(key)
|
|
56
|
+
return unique
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def resolve_pic_root(ccr_root: Path, explicit_pic_root: Path | None = None) -> Path:
|
|
60
|
+
"""Resolve a PIC root or raise a CLI-ready missing-path error."""
|
|
61
|
+
|
|
62
|
+
if explicit_pic_root is not None:
|
|
63
|
+
pic_root = explicit_pic_root.expanduser()
|
|
64
|
+
if not pic_root.exists():
|
|
65
|
+
raise CCRMissingError(
|
|
66
|
+
f"PIC root does not exist: {pic_root}",
|
|
67
|
+
{
|
|
68
|
+
"error": "pic root missing",
|
|
69
|
+
"ok": False,
|
|
70
|
+
"pic_root": str(pic_root),
|
|
71
|
+
"schema_version": "ccr.pic_compat_audit.v1",
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
return pic_root
|
|
75
|
+
searched = default_pic_root_candidates(ccr_root)
|
|
76
|
+
for candidate in searched:
|
|
77
|
+
if candidate.exists():
|
|
78
|
+
return candidate
|
|
79
|
+
raise CCRMissingError(
|
|
80
|
+
"PIC root was not found in default search candidates.",
|
|
81
|
+
{
|
|
82
|
+
"error": "pic root missing",
|
|
83
|
+
"ok": False,
|
|
84
|
+
"searched": [str(path) for path in searched],
|
|
85
|
+
"schema_version": "ccr.pic_compat_audit.v1",
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def audit_pic_compatibility(ccr_root: Path, *, pic_root: Path | None = None) -> dict[str, Any]:
|
|
91
|
+
"""Audit the CCR/PIC operational compatibility boundary."""
|
|
92
|
+
|
|
93
|
+
resolved_pic_root = resolve_pic_root(ccr_root, pic_root)
|
|
94
|
+
findings: list[dict[str, Any]] = []
|
|
95
|
+
pic_repo_version = _read_pyproject_version(resolved_pic_root / "pyproject.toml")
|
|
96
|
+
package_version = _installed_distribution_version("percolation-inversion-compiler")
|
|
97
|
+
pic_executable = shutil.which("pic")
|
|
98
|
+
|
|
99
|
+
if package_version is None:
|
|
100
|
+
findings.append(
|
|
101
|
+
_finding(
|
|
102
|
+
"provider_missing",
|
|
103
|
+
"python-package:percolation-inversion-compiler",
|
|
104
|
+
"medium",
|
|
105
|
+
False,
|
|
106
|
+
"PIC package is not installed in the active Python environment.",
|
|
107
|
+
repair_hint=PIC_ROUTE_TEXT,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
if pic_executable is None:
|
|
111
|
+
findings.append(
|
|
112
|
+
_finding(
|
|
113
|
+
"provider_missing",
|
|
114
|
+
"cli:pic",
|
|
115
|
+
"medium",
|
|
116
|
+
False,
|
|
117
|
+
"PIC CLI executable is not available on PATH.",
|
|
118
|
+
repair_hint=PIC_ROUTE_TEXT,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
_check_pic_source_tree(resolved_pic_root, findings)
|
|
123
|
+
_check_pic_repo_version(pic_repo_version, findings)
|
|
124
|
+
_check_provider_mapping(findings)
|
|
125
|
+
_check_non_claim_boundary(ccr_root, resolved_pic_root, findings)
|
|
126
|
+
|
|
127
|
+
blocking = [finding for finding in findings if finding["blocking"]]
|
|
128
|
+
return {
|
|
129
|
+
"accepted": not blocking,
|
|
130
|
+
"blocking_finding_count": len(blocking),
|
|
131
|
+
"created_at": now_iso(),
|
|
132
|
+
"expected_pic_commands": EXPECTED_PIC_COMMANDS,
|
|
133
|
+
"finding_count": len(findings),
|
|
134
|
+
"findings": findings,
|
|
135
|
+
"installed_package_version": package_version,
|
|
136
|
+
"ok": not blocking,
|
|
137
|
+
"pic_cli": {"available": pic_executable is not None, "path": pic_executable},
|
|
138
|
+
"pic_repo_version": pic_repo_version,
|
|
139
|
+
"pic_root": str(resolved_pic_root),
|
|
140
|
+
"report_id": stable_id(
|
|
141
|
+
"pic-compat-audit",
|
|
142
|
+
str(resolved_pic_root),
|
|
143
|
+
pic_repo_version,
|
|
144
|
+
package_version,
|
|
145
|
+
[finding["finding_id"] for finding in findings],
|
|
146
|
+
),
|
|
147
|
+
"schema_version": "ccr.audit_report.v1",
|
|
148
|
+
"settled": False,
|
|
149
|
+
"supported_import_fields": SUPPORTED_PIC_IMPORT_FIELDS,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _check_pic_source_tree(pic_root: Path, findings: list[dict[str, Any]]) -> None:
|
|
154
|
+
required_files = {
|
|
155
|
+
"README.md": [
|
|
156
|
+
"percolation-inversion-compiler",
|
|
157
|
+
"pic agent check --compact",
|
|
158
|
+
"pic phase plan --compact",
|
|
159
|
+
"safe_commands",
|
|
160
|
+
"settled=false",
|
|
161
|
+
],
|
|
162
|
+
"pyproject.toml": [
|
|
163
|
+
'name = "percolation-inversion-compiler"',
|
|
164
|
+
'pic = "percolation_inversion_compiler.cli:app"',
|
|
165
|
+
],
|
|
166
|
+
"docs/porting.md": ["workflow_usable", "settled", "candidate-only"],
|
|
167
|
+
"docs/phase-acceleration.md": [
|
|
168
|
+
"phase_gap_vector",
|
|
169
|
+
"bottlenecks",
|
|
170
|
+
"cannot_promote_because",
|
|
171
|
+
"settled_blockers",
|
|
172
|
+
],
|
|
173
|
+
"docs/v050-audit.md": ["Package version: `0.5.0`", "safe_commands"],
|
|
174
|
+
"examples/portability_conformance/phase_acceleration_plan.json": [
|
|
175
|
+
"PhaseAccelerationPlan",
|
|
176
|
+
"candidate_only_reasons",
|
|
177
|
+
"cannot_promote_because",
|
|
178
|
+
"settled_blockers",
|
|
179
|
+
],
|
|
180
|
+
}
|
|
181
|
+
for relative, needles in required_files.items():
|
|
182
|
+
path = pic_root / relative
|
|
183
|
+
if not path.exists():
|
|
184
|
+
findings.append(
|
|
185
|
+
_finding(
|
|
186
|
+
"missing-pic-source-file",
|
|
187
|
+
relative,
|
|
188
|
+
"high",
|
|
189
|
+
True,
|
|
190
|
+
f"PIC source tree does not contain {relative}.",
|
|
191
|
+
repair_hint="Point --pic-root at a PIC v0.5.0-compatible source tree.",
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
continue
|
|
195
|
+
text = path.read_text(encoding="utf-8")
|
|
196
|
+
for needle in needles:
|
|
197
|
+
if needle not in text:
|
|
198
|
+
findings.append(
|
|
199
|
+
_finding(
|
|
200
|
+
"missing-pic-compat-marker",
|
|
201
|
+
relative,
|
|
202
|
+
"medium",
|
|
203
|
+
True,
|
|
204
|
+
f"PIC compatibility marker is missing: {needle}",
|
|
205
|
+
repair_hint="Update the PIC root or compatibility matrix.",
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _check_pic_repo_version(version: str | None, findings: list[dict[str, Any]]) -> None:
|
|
211
|
+
if version is None:
|
|
212
|
+
findings.append(
|
|
213
|
+
_finding(
|
|
214
|
+
"missing-pic-version",
|
|
215
|
+
"pyproject.toml",
|
|
216
|
+
"high",
|
|
217
|
+
True,
|
|
218
|
+
"PIC pyproject.toml does not expose a package version.",
|
|
219
|
+
repair_hint="Use a PIC source root with explicit version metadata.",
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
return
|
|
223
|
+
if not version.startswith("0.5."):
|
|
224
|
+
findings.append(
|
|
225
|
+
_finding(
|
|
226
|
+
"unsupported-pic-version",
|
|
227
|
+
"pyproject.toml",
|
|
228
|
+
"medium",
|
|
229
|
+
False,
|
|
230
|
+
f"PIC source version is {version}; CCR v1 matrix targets PIC v0.5.x.",
|
|
231
|
+
repair_hint="Review INTEROP_PIC.md before relying on this PIC version.",
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _check_provider_mapping(findings: list[dict[str, Any]]) -> None:
|
|
237
|
+
capabilities = PicProvider().capabilities()
|
|
238
|
+
commands = capabilities.get("expected_pic_commands", [])
|
|
239
|
+
fields = capabilities.get("supported_import_fields", [])
|
|
240
|
+
for command in EXPECTED_PIC_COMMANDS:
|
|
241
|
+
if command not in commands:
|
|
242
|
+
findings.append(
|
|
243
|
+
_finding(
|
|
244
|
+
"provider-command-mapping-gap",
|
|
245
|
+
"src/ccr/providers/pic.py",
|
|
246
|
+
"high",
|
|
247
|
+
True,
|
|
248
|
+
f"PicProvider.capabilities() does not expose command: {command}",
|
|
249
|
+
repair_hint="Expose the PIC command in PicProvider.capabilities().",
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
for field in SUPPORTED_PIC_IMPORT_FIELDS:
|
|
253
|
+
if field not in fields:
|
|
254
|
+
findings.append(
|
|
255
|
+
_finding(
|
|
256
|
+
"provider-field-mapping-gap",
|
|
257
|
+
"src/ccr/providers/pic.py",
|
|
258
|
+
"high",
|
|
259
|
+
True,
|
|
260
|
+
f"PicProvider.capabilities() does not expose import field: {field}",
|
|
261
|
+
repair_hint="Expose the field in PicProvider.capabilities().",
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _check_non_claim_boundary(
|
|
267
|
+
ccr_root: Path, pic_root: Path, findings: list[dict[str, Any]]
|
|
268
|
+
) -> None:
|
|
269
|
+
ccr_text = _read_texts(
|
|
270
|
+
ccr_root,
|
|
271
|
+
[
|
|
272
|
+
"INTEROP_PIC.md",
|
|
273
|
+
"SECURITY.md",
|
|
274
|
+
"README.md",
|
|
275
|
+
"SPEC.md",
|
|
276
|
+
],
|
|
277
|
+
)
|
|
278
|
+
pic_text = _read_texts(
|
|
279
|
+
pic_root,
|
|
280
|
+
[
|
|
281
|
+
"README.md",
|
|
282
|
+
"docs/porting.md",
|
|
283
|
+
"docs/phase-acceleration.md",
|
|
284
|
+
"docs/v050-audit.md",
|
|
285
|
+
],
|
|
286
|
+
)
|
|
287
|
+
required_ccr = [
|
|
288
|
+
"PIC output never settles CCR by itself",
|
|
289
|
+
"safe commands",
|
|
290
|
+
"not real ASI",
|
|
291
|
+
"ccr audit pic",
|
|
292
|
+
]
|
|
293
|
+
required_pic = [
|
|
294
|
+
"accepted=true",
|
|
295
|
+
"settled=false",
|
|
296
|
+
"safe_commands",
|
|
297
|
+
"workflow_usable",
|
|
298
|
+
]
|
|
299
|
+
for needle in required_ccr:
|
|
300
|
+
if needle not in ccr_text:
|
|
301
|
+
findings.append(
|
|
302
|
+
_finding(
|
|
303
|
+
"missing-ccr-pic-boundary",
|
|
304
|
+
"CCR docs",
|
|
305
|
+
"high",
|
|
306
|
+
True,
|
|
307
|
+
f"CCR documentation does not preserve PIC boundary text: {needle}",
|
|
308
|
+
repair_hint="Update README, SPEC, SECURITY, or INTEROP_PIC.md.",
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
for needle in required_pic:
|
|
312
|
+
if needle not in pic_text:
|
|
313
|
+
findings.append(
|
|
314
|
+
_finding(
|
|
315
|
+
"missing-pic-boundary",
|
|
316
|
+
"PIC docs",
|
|
317
|
+
"medium",
|
|
318
|
+
True,
|
|
319
|
+
f"PIC documentation does not expose compatibility boundary text: {needle}",
|
|
320
|
+
repair_hint="Review the supplied --pic-root for v0.5.0 compatibility.",
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _read_texts(root: Path, relatives: list[str]) -> str:
|
|
326
|
+
chunks: list[str] = []
|
|
327
|
+
for relative in relatives:
|
|
328
|
+
path = root / relative
|
|
329
|
+
if path.exists():
|
|
330
|
+
chunks.append(path.read_text(encoding="utf-8"))
|
|
331
|
+
return "\n".join(chunks)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _installed_distribution_version(name: str) -> str | None:
|
|
335
|
+
try:
|
|
336
|
+
return importlib_metadata.version(name)
|
|
337
|
+
except importlib_metadata.PackageNotFoundError:
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _read_pyproject_version(path: Path) -> str | None:
|
|
342
|
+
if not path.exists():
|
|
343
|
+
return None
|
|
344
|
+
match = re.search(r'^version\s*=\s*"([^"]+)"', path.read_text(encoding="utf-8"), re.M)
|
|
345
|
+
return match.group(1) if match else None
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _finding(
|
|
349
|
+
kind: str,
|
|
350
|
+
location: str,
|
|
351
|
+
severity: str,
|
|
352
|
+
blocking: bool,
|
|
353
|
+
description: str,
|
|
354
|
+
*,
|
|
355
|
+
repair_hint: str,
|
|
356
|
+
) -> dict[str, Any]:
|
|
357
|
+
finding_id = stable_id("finding", kind, location, description)
|
|
358
|
+
if kind == "provider_missing":
|
|
359
|
+
residual_kind = "provider_missing"
|
|
360
|
+
elif kind.startswith("missing"):
|
|
361
|
+
residual_kind = "missing_evidence"
|
|
362
|
+
else:
|
|
363
|
+
residual_kind = "other"
|
|
364
|
+
residual = build_residual(
|
|
365
|
+
kind=residual_kind,
|
|
366
|
+
description=description,
|
|
367
|
+
blocking=blocking,
|
|
368
|
+
object_type="runtime",
|
|
369
|
+
object_id=location,
|
|
370
|
+
severity=severity,
|
|
371
|
+
refs=[location],
|
|
372
|
+
source="ccr.audit.pic",
|
|
373
|
+
repair_hint=repair_hint,
|
|
374
|
+
extensions={"finding_id": finding_id, "finding_kind": kind},
|
|
375
|
+
)
|
|
376
|
+
return {
|
|
377
|
+
"blocking": blocking,
|
|
378
|
+
"description": description,
|
|
379
|
+
"finding_id": finding_id,
|
|
380
|
+
"kind": kind,
|
|
381
|
+
"location": location,
|
|
382
|
+
"residual_ready": residual,
|
|
383
|
+
"severity": severity,
|
|
384
|
+
}
|