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.
Files changed (86) hide show
  1. ccr/__init__.py +8 -0
  2. ccr/__main__.py +9 -0
  3. ccr/adapters/__init__.py +4 -0
  4. ccr/adapters/base.py +38 -0
  5. ccr/adapters/pic.py +205 -0
  6. ccr/audit/__init__.py +10 -0
  7. ccr/audit/pic.py +384 -0
  8. ccr/audit/release.py +337 -0
  9. ccr/audit/repo.py +486 -0
  10. ccr/blackboard/__init__.py +4 -0
  11. ccr/blackboard/events.py +45 -0
  12. ccr/blackboard/replay.py +16 -0
  13. ccr/blackboard/store.py +16 -0
  14. ccr/cli.py +1564 -0
  15. ccr/constants.py +116 -0
  16. ccr/data/INTEROP_PIC.md +242 -0
  17. ccr/data/__init__.py +4 -0
  18. ccr/data/agent-manifest.json +304 -0
  19. ccr/data/schemas/agent-manifest.schema.json +55 -0
  20. ccr/data/schemas/asi-proxy-threshold.schema.json +78 -0
  21. ccr/data/schemas/audit-report.schema.json +63 -0
  22. ccr/data/schemas/baseline.schema.json +43 -0
  23. ccr/data/schemas/blackboard-event.schema.json +50 -0
  24. ccr/data/schemas/effective-graph.schema.json +91 -0
  25. ccr/data/schemas/packet.schema.json +1065 -0
  26. ccr/data/schemas/phase-certificate-candidate.schema.json +54 -0
  27. ccr/data/schemas/phase-observation.schema.json +58 -0
  28. ccr/data/schemas/phase-report.schema.json +34 -0
  29. ccr/data/schemas/phase-state.schema.json +46 -0
  30. ccr/data/schemas/provider.schema.json +43 -0
  31. ccr/data/schemas/residual.schema.json +85 -0
  32. ccr/data/schemas/task.schema.json +477 -0
  33. ccr/data/schemas/verifier-report.schema.json +43 -0
  34. ccr/errors.py +34 -0
  35. ccr/ids.py +36 -0
  36. ccr/io.py +79 -0
  37. ccr/metrics/__init__.py +4 -0
  38. ccr/metrics/phase.py +8 -0
  39. ccr/packets/__init__.py +4 -0
  40. ccr/packets/distill.py +21 -0
  41. ccr/packets/model.py +27 -0
  42. ccr/packets/promotion.py +219 -0
  43. ccr/packets/status.py +39 -0
  44. ccr/packets/store.py +83 -0
  45. ccr/paths.py +40 -0
  46. ccr/phase/__init__.py +21 -0
  47. ccr/phase/baseline.py +56 -0
  48. ccr/phase/certify.py +70 -0
  49. ccr/phase/eligibility.py +106 -0
  50. ccr/phase/form.py +197 -0
  51. ccr/phase/graph.py +194 -0
  52. ccr/phase/observe.py +107 -0
  53. ccr/phase/threshold.py +93 -0
  54. ccr/providers/__init__.py +8 -0
  55. ccr/providers/base.py +45 -0
  56. ccr/providers/http.py +154 -0
  57. ccr/providers/pic.py +107 -0
  58. ccr/providers/registry.py +23 -0
  59. ccr/py.typed +1 -0
  60. ccr/reports/__init__.py +4 -0
  61. ccr/reports/json_report.py +42 -0
  62. ccr/reports/markdown.py +46 -0
  63. ccr/residuals/__init__.py +4 -0
  64. ccr/residuals/model.py +76 -0
  65. ccr/residuals/store.py +59 -0
  66. ccr/runtime/__init__.py +4 -0
  67. ccr/runtime/config.py +25 -0
  68. ccr/runtime/init.py +68 -0
  69. ccr/runtime/state.py +34 -0
  70. ccr/schemas/__init__.py +4 -0
  71. ccr/schemas/loader.py +88 -0
  72. ccr/schemas/validation.py +84 -0
  73. ccr/storage/__init__.py +24 -0
  74. ccr/storage/sqlite.py +305 -0
  75. ccr/tasks/__init__.py +4 -0
  76. ccr/tasks/lease.py +142 -0
  77. ccr/tasks/model.py +39 -0
  78. ccr/tasks/scheduler.py +29 -0
  79. ccr/tasks/store.py +66 -0
  80. ccr/time.py +71 -0
  81. collective_capability_runtime-1.0.0.dist-info/METADATA +303 -0
  82. collective_capability_runtime-1.0.0.dist-info/RECORD +86 -0
  83. collective_capability_runtime-1.0.0.dist-info/WHEEL +4 -0
  84. collective_capability_runtime-1.0.0.dist-info/entry_points.txt +2 -0
  85. collective_capability_runtime-1.0.0.dist-info/licenses/LICENSE +181 -0
  86. collective_capability_runtime-1.0.0.dist-info/licenses/NOTICE +7 -0
ccr/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Collective Capability Runtime."""
3
+
4
+ from __future__ import annotations
5
+
6
+ __version__ = "1.0.0"
7
+
8
+ __all__ = ["__version__"]
ccr/__main__.py ADDED
@@ -0,0 +1,9 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Module entry point for ``python -m ccr``."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from ccr.cli import main
7
+
8
+ if __name__ == "__main__":
9
+ raise SystemExit(main())
@@ -0,0 +1,4 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Verifier provider adapters."""
3
+
4
+ from __future__ import annotations
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
+ }