PTSIP 0.3.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.
- ptsip/__init__.py +3 -0
- ptsip/__main__.py +4 -0
- ptsip/app/__init__.py +5 -0
- ptsip/app/client.py +53 -0
- ptsip/app/github_client.py +200 -0
- ptsip/app/server.py +130 -0
- ptsip/app/service.py +285 -0
- ptsip/app/store.py +233 -0
- ptsip/artifact_evidence.py +150 -0
- ptsip/build_resolution.py +298 -0
- ptsip/clarification/__init__.py +6 -0
- ptsip/clarification/generator.py +120 -0
- ptsip/clarification/generator_core.py +98 -0
- ptsip/clarification/i18n.py +106 -0
- ptsip/clarification/model.py +33 -0
- ptsip/clarification/render.py +78 -0
- ptsip/clarification/resolution/__init__.py +29 -0
- ptsip/clarification/resolution/model.py +33 -0
- ptsip/clarification/resolution/parser.py +56 -0
- ptsip/clarification/resolution/profile_projection.py +198 -0
- ptsip/clarification/resolution/resolver.py +30 -0
- ptsip/clarification/transports/__init__.py +1 -0
- ptsip/clarification/transports/github_issue.py +162 -0
- ptsip/cli.py +402 -0
- ptsip/conformance.py +531 -0
- ptsip/conformance_audit.py +143 -0
- ptsip/conformance_engine.py +396 -0
- ptsip/constants.py +9 -0
- ptsip/doctor.py +33 -0
- ptsip/inspection/__init__.py +1 -0
- ptsip/inspection/components.py +74 -0
- ptsip/inspection/dependencies.py +496 -0
- ptsip/inspection/dependencies_030.py +69 -0
- ptsip/inspection/dotnet.py +188 -0
- ptsip/inspection/go.py +157 -0
- ptsip/inspection/inventory.py +161 -0
- ptsip/inspection/javascript.py +376 -0
- ptsip/inspection/lexing.py +121 -0
- ptsip/inspection/source_adapters.py +60 -0
- ptsip/lifecycle_evidence.py +245 -0
- ptsip/model.py +105 -0
- ptsip/pilot/__init__.py +1 -0
- ptsip/pilot/runner.py +170 -0
- ptsip/repository/__init__.py +1 -0
- ptsip/repository/discover.py +68 -0
- ptsip/repository/remote.py +54 -0
- ptsip/repository/snapshot.py +173 -0
- ptsip/review_evidence.py +406 -0
- ptsip/spec_identity.py +22 -0
- ptsip/specdata/ptsip-agent-classification.schema.json +35 -0
- ptsip/specdata/ptsip-artifact-evidence.schema.json +68 -0
- ptsip/specdata/ptsip-diagnostic.schema.json +29 -0
- ptsip/specdata/ptsip-profile.schema.json +310 -0
- ptsip/specdata/ptsip-registry.yaml +189 -0
- ptsip/storage/__init__.py +1 -0
- ptsip/storage/local_state.py +32 -0
- ptsip/validation/__init__.py +1 -0
- ptsip/validation/components.py +164 -0
- ptsip/validation/profile.py +172 -0
- ptsip/validation/rules.py +163 -0
- ptsip-0.3.1.dist-info/METADATA +338 -0
- ptsip-0.3.1.dist-info/RECORD +66 -0
- ptsip-0.3.1.dist-info/WHEEL +5 -0
- ptsip-0.3.1.dist-info/entry_points.txt +3 -0
- ptsip-0.3.1.dist-info/licenses/LICENSE +201 -0
- ptsip-0.3.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from .build_resolution import evaluate_independent_build_resolution
|
|
9
|
+
from .conformance import (
|
|
10
|
+
ConformanceResult,
|
|
11
|
+
_dependency_coverage_gaps,
|
|
12
|
+
_finding_diagnostic,
|
|
13
|
+
_unassigned_coverage_gaps,
|
|
14
|
+
_unsupported_source_coverage_gaps,
|
|
15
|
+
evaluate_conformance as evaluate_base_conformance,
|
|
16
|
+
)
|
|
17
|
+
from .conformance_audit import audit_conformance_report
|
|
18
|
+
from .inspection.dependencies import DependencyScan
|
|
19
|
+
from .inspection.dependencies_030 import scan_dependency_edges
|
|
20
|
+
from .inspection.source_adapters import SUPPORTED_SOURCE_SUFFIXES
|
|
21
|
+
from .lifecycle_evidence import evaluate_lifecycle_evidence
|
|
22
|
+
from .model import DependencyPhase, EvidenceNodeScope, EvidenceProvenance, ResolutionStatus
|
|
23
|
+
from .repository.discover import discover_repository
|
|
24
|
+
from .repository.snapshot import capture_snapshot, compare_snapshots
|
|
25
|
+
from .review_evidence import (
|
|
26
|
+
evaluate_agent_decisions,
|
|
27
|
+
load_agent_decisions,
|
|
28
|
+
load_external_evidence,
|
|
29
|
+
merge_external_dependencies,
|
|
30
|
+
)
|
|
31
|
+
from .validation.components import partition_components
|
|
32
|
+
from .validation.profile import find_profile, validate_profile
|
|
33
|
+
from .validation.rules import evaluate_component_dependency_policy, evaluate_declared_dependency_boundaries
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _recalculate_outcome(report: dict[str, object]) -> None:
|
|
37
|
+
diagnostics = report.get("diagnostics", [])
|
|
38
|
+
coverage = report.get("coverage", {})
|
|
39
|
+
non_conformant = any(
|
|
40
|
+
isinstance(item, dict) and item.get("outcome_effect") == "NON_CONFORMANT"
|
|
41
|
+
for item in diagnostics
|
|
42
|
+
if isinstance(diagnostics, list)
|
|
43
|
+
)
|
|
44
|
+
incomplete_diagnostic = any(
|
|
45
|
+
isinstance(item, dict) and item.get("outcome_effect") == "INCOMPLETE"
|
|
46
|
+
for item in diagnostics
|
|
47
|
+
if isinstance(diagnostics, list)
|
|
48
|
+
)
|
|
49
|
+
blocking = coverage.get("blocking_gaps", []) if isinstance(coverage, dict) else []
|
|
50
|
+
if non_conformant:
|
|
51
|
+
report["outcome"] = "NON_CONFORMANT"
|
|
52
|
+
elif incomplete_diagnostic or bool(blocking):
|
|
53
|
+
report["outcome"] = "INCOMPLETE"
|
|
54
|
+
else:
|
|
55
|
+
report["outcome"] = "CONFORMANT"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _externally_supplemented_native_ids(native: DependencyScan, external_edges: tuple) -> set[str]:
|
|
59
|
+
resolved_external = [
|
|
60
|
+
edge
|
|
61
|
+
for edge in external_edges
|
|
62
|
+
if edge.resolution in {ResolutionStatus.RESOLVED, ResolutionStatus.EXTERNAL}
|
|
63
|
+
and edge.target_scope != EvidenceNodeScope.UNRESOLVED_TARGET
|
|
64
|
+
and edge.provenance == EvidenceProvenance.OBSERVED
|
|
65
|
+
]
|
|
66
|
+
supplemented: set[str] = set()
|
|
67
|
+
for native_edge in native.edges:
|
|
68
|
+
# A DYNAMIC edge has no concrete target identity by definition. An
|
|
69
|
+
# imported document that repeats the sentinel target cannot turn that
|
|
70
|
+
# uncertainty into evidence of absence, so only native UNRESOLVED edges
|
|
71
|
+
# are eligible for strict external supplementation.
|
|
72
|
+
if native_edge.resolution != ResolutionStatus.UNRESOLVED:
|
|
73
|
+
continue
|
|
74
|
+
if any(
|
|
75
|
+
external.source == native_edge.source
|
|
76
|
+
and external.target == native_edge.target
|
|
77
|
+
and external.edge_type == native_edge.edge_type
|
|
78
|
+
and (external.phase == native_edge.phase or native_edge.phase == DependencyPhase.UNKNOWN)
|
|
79
|
+
for external in resolved_external
|
|
80
|
+
):
|
|
81
|
+
supplemented.add(native_edge.evidence_id)
|
|
82
|
+
return supplemented
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _external_conflict_gaps(native: DependencyScan, external_edges: tuple) -> list[dict[str, object]]:
|
|
86
|
+
gaps: list[dict[str, object]] = []
|
|
87
|
+
for external in external_edges:
|
|
88
|
+
for observed in native.edges:
|
|
89
|
+
if (
|
|
90
|
+
observed.source != external.source
|
|
91
|
+
or observed.target != external.target
|
|
92
|
+
or observed.edge_type != external.edge_type
|
|
93
|
+
or (observed.phase != external.phase and observed.phase != DependencyPhase.UNKNOWN)
|
|
94
|
+
):
|
|
95
|
+
continue
|
|
96
|
+
if observed.resolution in {ResolutionStatus.UNRESOLVED, ResolutionStatus.DYNAMIC}:
|
|
97
|
+
continue
|
|
98
|
+
contradictory_scope = observed.target_scope != external.target_scope
|
|
99
|
+
contradictory_path = (
|
|
100
|
+
observed.resolution == ResolutionStatus.RESOLVED
|
|
101
|
+
and external.resolution == ResolutionStatus.RESOLVED
|
|
102
|
+
and observed.resolved_path != external.resolved_path
|
|
103
|
+
)
|
|
104
|
+
if not (contradictory_scope or contradictory_path):
|
|
105
|
+
continue
|
|
106
|
+
gaps.append(
|
|
107
|
+
{
|
|
108
|
+
"id": "external-evidence:conflict:" + hashlib.sha256((observed.evidence_id + external.evidence_id).encode()).hexdigest()[:16],
|
|
109
|
+
"blocking": True,
|
|
110
|
+
"rule_ids": ["PTSIP-EVD-002", "PTSIP-EVD-003", "PTSIP-EVD-004"],
|
|
111
|
+
"evidence_ids": [observed.evidence_id, external.evidence_id],
|
|
112
|
+
"message": "Imported external dependency evidence contradicts resolved native repository evidence and therefore cannot override it silently.",
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
return gaps
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _external_input_gaps(external_load) -> list[dict[str, object]]:
|
|
119
|
+
return [
|
|
120
|
+
{
|
|
121
|
+
"id": "external-evidence:invalid:" + hashlib.sha256((issue.source_path + issue.message).encode()).hexdigest()[:16],
|
|
122
|
+
"blocking": True,
|
|
123
|
+
"rule_ids": ["PTSIP-EVD-003", "PTSIP-EVD-004"],
|
|
124
|
+
"evidence_ids": [f"external-input:{issue.source_path}"],
|
|
125
|
+
"message": f"Explicit external evidence input is unusable for strict conformance: {issue.message}",
|
|
126
|
+
}
|
|
127
|
+
for issue in external_load.issues
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _coverage_without_supplemented(gaps: list[dict[str, object]], supplemented_ids: set[str]) -> list[dict[str, object]]:
|
|
132
|
+
result: list[dict[str, object]] = []
|
|
133
|
+
for gap in gaps:
|
|
134
|
+
evidence_ids = {str(item) for item in gap.get("evidence_ids", [])}
|
|
135
|
+
if str(gap.get("id", "")).startswith("dependency-target:") and evidence_ids & supplemented_ids:
|
|
136
|
+
continue
|
|
137
|
+
result.append(gap)
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def evaluate_conformance(
|
|
142
|
+
path: str | Path = ".",
|
|
143
|
+
profile_path: str | Path | None = None,
|
|
144
|
+
artifact_evidence_paths: list[str | Path] | tuple[str | Path, ...] | None = None,
|
|
145
|
+
agent_decision_paths: list[str | Path] | tuple[str | Path, ...] | None = None,
|
|
146
|
+
external_evidence_paths: list[str | Path] | tuple[str | Path, ...] | None = None,
|
|
147
|
+
) -> ConformanceResult:
|
|
148
|
+
repo = discover_repository(path)
|
|
149
|
+
root = Path(repo.root).resolve()
|
|
150
|
+
final_before = capture_snapshot(root)
|
|
151
|
+
base = evaluate_base_conformance(
|
|
152
|
+
path,
|
|
153
|
+
profile_path,
|
|
154
|
+
artifact_evidence_paths,
|
|
155
|
+
_snapshot_before=final_before,
|
|
156
|
+
_finalize_snapshot=False,
|
|
157
|
+
)
|
|
158
|
+
report = base.report
|
|
159
|
+
profile = find_profile(root, profile_path)
|
|
160
|
+
validation = validate_profile(root, profile_path)
|
|
161
|
+
|
|
162
|
+
native_dependencies = scan_dependency_edges(root)
|
|
163
|
+
external_load = load_external_evidence(external_evidence_paths, repo)
|
|
164
|
+
dependencies = merge_external_dependencies(native_dependencies, external_load)
|
|
165
|
+
report["dependencies"] = dependencies.as_dict()
|
|
166
|
+
report["external_evidence"] = external_load.as_dict()
|
|
167
|
+
|
|
168
|
+
diagnostics = list(report.get("diagnostics", [])) if isinstance(report.get("diagnostics"), list) else []
|
|
169
|
+
diagnostics = [
|
|
170
|
+
item
|
|
171
|
+
for item in diagnostics
|
|
172
|
+
if not (
|
|
173
|
+
isinstance(item, dict)
|
|
174
|
+
and isinstance(item.get("evaluator"), dict)
|
|
175
|
+
and item["evaluator"].get("id") == "declared-dependency-boundaries"
|
|
176
|
+
)
|
|
177
|
+
]
|
|
178
|
+
coverage = report.get("coverage")
|
|
179
|
+
if not isinstance(coverage, dict):
|
|
180
|
+
coverage = {"blocking_gaps": [], "non_blocking_gaps": []}
|
|
181
|
+
report["coverage"] = coverage
|
|
182
|
+
blocking = list(coverage.get("blocking_gaps", [])) if isinstance(coverage.get("blocking_gaps"), list) else []
|
|
183
|
+
non_blocking = list(coverage.get("non_blocking_gaps", [])) if isinstance(coverage.get("non_blocking_gaps"), list) else []
|
|
184
|
+
replace_prefixes = (
|
|
185
|
+
"dependency-scan:",
|
|
186
|
+
"dependency-target:",
|
|
187
|
+
"component-ownership:",
|
|
188
|
+
"build-resolution:coverage",
|
|
189
|
+
"build-resolution:",
|
|
190
|
+
"lifecycle:",
|
|
191
|
+
"language-coverage:",
|
|
192
|
+
"agent-decision:",
|
|
193
|
+
"external-evidence:",
|
|
194
|
+
"conformance-audit:",
|
|
195
|
+
)
|
|
196
|
+
blocking = [item for item in blocking if not (isinstance(item, dict) and str(item.get("id", "")).startswith(replace_prefixes))]
|
|
197
|
+
non_blocking = [item for item in non_blocking if not (isinstance(item, dict) and str(item.get("id", "")).startswith(replace_prefixes))]
|
|
198
|
+
|
|
199
|
+
evaluators = report.get("evaluators")
|
|
200
|
+
if not isinstance(evaluators, dict):
|
|
201
|
+
evaluators = {}
|
|
202
|
+
report["evaluators"] = evaluators
|
|
203
|
+
evaluators["external_evidence_import"] = {
|
|
204
|
+
"status": "NOT_APPLICABLE" if not external_evidence_paths else ("BLOCKED" if external_load.issues else "RAN"),
|
|
205
|
+
"reason": None if not external_load.issues else "EXTERNAL_EVIDENCE_INVALID_OR_STALE",
|
|
206
|
+
"document_count": len(external_load.documents),
|
|
207
|
+
"edge_count": len(external_load.edges),
|
|
208
|
+
}
|
|
209
|
+
blocking.extend(_external_input_gaps(external_load))
|
|
210
|
+
blocking.extend(_external_conflict_gaps(native_dependencies, external_load.edges))
|
|
211
|
+
|
|
212
|
+
components: list[dict[str, object]] = []
|
|
213
|
+
payload: dict[str, object] | None = None
|
|
214
|
+
if profile is not None and validation.valid:
|
|
215
|
+
loaded = yaml.safe_load(profile.read_text(encoding="utf-8-sig"))
|
|
216
|
+
payload = loaded if isinstance(loaded, dict) else None
|
|
217
|
+
declared = payload.get("components") if payload is not None else None
|
|
218
|
+
if isinstance(declared, list):
|
|
219
|
+
components = [item for item in declared if isinstance(item, dict)]
|
|
220
|
+
|
|
221
|
+
agent_load = load_agent_decisions(agent_decision_paths)
|
|
222
|
+
agent_report, agent_gaps = evaluate_agent_decisions(agent_load, components)
|
|
223
|
+
report["agent_decisions"] = agent_report
|
|
224
|
+
blocking.extend(agent_gaps)
|
|
225
|
+
evaluators["agent_decision_review"] = {
|
|
226
|
+
"status": "NOT_APPLICABLE" if not agent_decision_paths else ("BLOCKED" if agent_load.issues else "RAN"),
|
|
227
|
+
"reason": None if not agent_load.issues else "AGENT_DECISION_INPUT_INVALID",
|
|
228
|
+
"document_count": len(agent_load.documents),
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if components:
|
|
232
|
+
partition = partition_components(root, components)
|
|
233
|
+
boundary_findings = evaluate_declared_dependency_boundaries(components, partition, dependencies)
|
|
234
|
+
diagnostics.extend(_finding_diagnostic(item, "declared-dependency-boundaries") for item in boundary_findings)
|
|
235
|
+
dependency_gaps = _dependency_coverage_gaps(dependencies, components, partition)
|
|
236
|
+
supplemented = _externally_supplemented_native_ids(native_dependencies, external_load.edges)
|
|
237
|
+
dependency_gaps = _coverage_without_supplemented(dependency_gaps, supplemented)
|
|
238
|
+
blocking.extend(item for item in dependency_gaps if item.get("blocking"))
|
|
239
|
+
non_blocking.extend(item for item in dependency_gaps if not item.get("blocking"))
|
|
240
|
+
ownership_gaps = _unassigned_coverage_gaps(partition)
|
|
241
|
+
blocking.extend(item for item in ownership_gaps if item.get("blocking"))
|
|
242
|
+
non_blocking.extend(item for item in ownership_gaps if not item.get("blocking"))
|
|
243
|
+
language_gaps = _unsupported_source_coverage_gaps(partition, components)
|
|
244
|
+
blocking.extend(language_gaps)
|
|
245
|
+
evaluators["source_language_coverage"] = {
|
|
246
|
+
"status": "BLOCKED" if language_gaps else "RAN",
|
|
247
|
+
"reason": "UNSUPPORTED_MANDATORY_SOURCE" if language_gaps else None,
|
|
248
|
+
"supported_suffixes": sorted(SUPPORTED_SOURCE_SUFFIXES),
|
|
249
|
+
"unsupported_source_count": sum(len(item.get("evidence_ids", [])) for item in language_gaps),
|
|
250
|
+
}
|
|
251
|
+
evaluators["declared_dependency_boundaries"] = {
|
|
252
|
+
"status": "RAN",
|
|
253
|
+
"reason": None,
|
|
254
|
+
"finding_count": len(boundary_findings),
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
component_policy = payload.get("component_dependency_policy") if payload is not None else None
|
|
258
|
+
policy_results = evaluate_component_dependency_policy(
|
|
259
|
+
component_policy if isinstance(component_policy, dict) else None,
|
|
260
|
+
components,
|
|
261
|
+
partition,
|
|
262
|
+
dependencies,
|
|
263
|
+
)
|
|
264
|
+
report["project_policy"] = {
|
|
265
|
+
"findings": [item.as_dict() for item in policy_results],
|
|
266
|
+
"finding_count": len(policy_results),
|
|
267
|
+
"affects_ptsip_outcome": False,
|
|
268
|
+
}
|
|
269
|
+
evaluators["component_dependency_policy"] = {
|
|
270
|
+
"status": "RAN" if isinstance(component_policy, dict) else "NOT_APPLICABLE",
|
|
271
|
+
"reason": None,
|
|
272
|
+
"finding_count": len(policy_results),
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
build = evaluate_independent_build_resolution(root, components, partition)
|
|
276
|
+
evaluators["independent_build_resolution"] = {
|
|
277
|
+
"status": build.status,
|
|
278
|
+
"reason": build.reason,
|
|
279
|
+
"manifest_count": len(build.manifests),
|
|
280
|
+
}
|
|
281
|
+
blocking.extend(build.blocking_gaps)
|
|
282
|
+
report["build_resolution"] = build.as_dict()
|
|
283
|
+
|
|
284
|
+
lifecycle = evaluate_lifecycle_evidence(root, components, partition)
|
|
285
|
+
evaluators["lifecycle_independence"] = {
|
|
286
|
+
"status": lifecycle.status,
|
|
287
|
+
"reason": lifecycle.reason,
|
|
288
|
+
"release_workflow_count": len(lifecycle.workflows),
|
|
289
|
+
}
|
|
290
|
+
blocking.extend(lifecycle.blocking_gaps)
|
|
291
|
+
report["lifecycle"] = lifecycle.as_dict()
|
|
292
|
+
else:
|
|
293
|
+
evaluators["source_language_coverage"] = {"status": "BLOCKED", "reason": "COMPONENT_DECLARATIONS_REQUIRED"}
|
|
294
|
+
evaluators["independent_build_resolution"] = {"status": "BLOCKED", "reason": "COMPONENT_DECLARATIONS_REQUIRED"}
|
|
295
|
+
evaluators["lifecycle_independence"] = {"status": "BLOCKED", "reason": "COMPONENT_DECLARATIONS_REQUIRED"}
|
|
296
|
+
report["build_resolution"] = {
|
|
297
|
+
"status": "BLOCKED",
|
|
298
|
+
"reason": "COMPONENT_DECLARATIONS_REQUIRED",
|
|
299
|
+
"manifests": [],
|
|
300
|
+
"blocking_gaps": [],
|
|
301
|
+
}
|
|
302
|
+
report["lifecycle"] = {
|
|
303
|
+
"status": "BLOCKED",
|
|
304
|
+
"reason": "COMPONENT_DECLARATIONS_REQUIRED",
|
|
305
|
+
"workflows": [],
|
|
306
|
+
"blocking_gaps": [],
|
|
307
|
+
"observations": [],
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
report["diagnostics"] = diagnostics
|
|
311
|
+
coverage["blocking_gaps"] = blocking
|
|
312
|
+
coverage["non_blocking_gaps"] = non_blocking
|
|
313
|
+
profile_report = report.get("profile", {})
|
|
314
|
+
profile_valid = isinstance(profile_report, dict) and profile_report.get("valid") is True
|
|
315
|
+
specification = report.get("specification", {})
|
|
316
|
+
spec_bound = isinstance(specification, dict) and bool(specification.get("revision"))
|
|
317
|
+
artifact_documents = report.get("artifacts", {}).get("documents", []) if isinstance(report.get("artifacts"), dict) else []
|
|
318
|
+
artifact_bound = bool(artifact_documents) and all(
|
|
319
|
+
isinstance(item, dict) and item.get("binding_valid") is True for item in artifact_documents
|
|
320
|
+
)
|
|
321
|
+
evaluators["profile_validation"] = {
|
|
322
|
+
"status": "RAN" if profile_valid else "BLOCKED",
|
|
323
|
+
"reason": None if profile_valid else "PROFILE_INVALID",
|
|
324
|
+
}
|
|
325
|
+
evaluators["specification_revision_binding"] = {
|
|
326
|
+
"status": "RAN" if profile_valid and spec_bound else "BLOCKED",
|
|
327
|
+
"reason": None if profile_valid and spec_bound else "IMMUTABLE_SPECIFICATION_BINDING_REQUIRED",
|
|
328
|
+
}
|
|
329
|
+
evaluators["artifact_snapshot_binding"] = {
|
|
330
|
+
"status": "RAN" if artifact_bound else "BLOCKED",
|
|
331
|
+
"reason": None if artifact_bound else "ARTIFACT_REVISION_BINDING_REQUIRED",
|
|
332
|
+
"document_count": len(artifact_documents),
|
|
333
|
+
}
|
|
334
|
+
coverage["applicability"] = {
|
|
335
|
+
"profile_and_specification": {"applicable": True, "status": "SUFFICIENT" if profile_valid and spec_bound else "INSUFFICIENT"},
|
|
336
|
+
"dependency_boundaries": {"applicable": True, "status": "SUFFICIENT" if evaluators.get("declared_dependency_boundaries", {}).get("status") == "RAN" else "INSUFFICIENT"},
|
|
337
|
+
"source_language_coverage": {"applicable": True, "status": "SUFFICIENT" if evaluators["source_language_coverage"]["status"] == "RAN" else "INSUFFICIENT"},
|
|
338
|
+
"product_artifact_boundary": {"applicable": True, "status": "SUFFICIENT" if evaluators.get("product_artifact_boundary", {}).get("status") == "RAN" and artifact_bound else "INSUFFICIENT"},
|
|
339
|
+
"independent_build_resolution": {"applicable": True, "status": "SUFFICIENT" if evaluators.get("independent_build_resolution", {}).get("status") == "RAN" else "INSUFFICIENT"},
|
|
340
|
+
"lifecycle_independence": {"applicable": True, "status": "SUFFICIENT" if evaluators.get("lifecycle_independence", {}).get("status") == "RAN" else "INSUFFICIENT"},
|
|
341
|
+
}
|
|
342
|
+
_recalculate_outcome(report)
|
|
343
|
+
|
|
344
|
+
# Run the semantic audit inside the observation envelope before the final
|
|
345
|
+
# repository snapshot. A final pure report audit below validates the
|
|
346
|
+
# snapshot result itself without collecting additional repository evidence.
|
|
347
|
+
audit_conformance_report(report)
|
|
348
|
+
final_after = capture_snapshot(root)
|
|
349
|
+
final_comparison = compare_snapshots(final_before, final_after)
|
|
350
|
+
report["snapshot"] = {
|
|
351
|
+
"before": final_before.as_dict(),
|
|
352
|
+
"after": final_after.as_dict(),
|
|
353
|
+
"comparison": final_comparison.as_dict(),
|
|
354
|
+
}
|
|
355
|
+
if not final_comparison.stable and not any(
|
|
356
|
+
isinstance(item, dict) and item.get("id") == "snapshot:invalidated"
|
|
357
|
+
for item in coverage["blocking_gaps"]
|
|
358
|
+
):
|
|
359
|
+
coverage["blocking_gaps"].append(
|
|
360
|
+
{
|
|
361
|
+
"id": "snapshot:invalidated",
|
|
362
|
+
"blocking": True,
|
|
363
|
+
"rule_ids": ["PTSIP-EVD-001", "PTSIP-EVD-003"],
|
|
364
|
+
"evidence_ids": ["snapshot:comparison"],
|
|
365
|
+
"message": "Repository evidence changed or was incomplete during the complete enforced conformance evaluation.",
|
|
366
|
+
}
|
|
367
|
+
)
|
|
368
|
+
evaluators["snapshot_integrity"] = {
|
|
369
|
+
"status": "RAN" if final_comparison.stable else "BLOCKED",
|
|
370
|
+
"reason": None if final_comparison.stable else "EVALUATION_SNAPSHOT_INVALIDATED",
|
|
371
|
+
}
|
|
372
|
+
coverage["applicability"]["snapshot_integrity"] = {
|
|
373
|
+
"applicable": True,
|
|
374
|
+
"status": "SUFFICIENT" if final_comparison.stable else "INSUFFICIENT",
|
|
375
|
+
}
|
|
376
|
+
_recalculate_outcome(report)
|
|
377
|
+
|
|
378
|
+
audit = audit_conformance_report(report)
|
|
379
|
+
report["audit"] = audit
|
|
380
|
+
evaluators["conformance_report_audit"] = {
|
|
381
|
+
"status": "RAN" if audit["status"] == "PASS" else "BLOCKED",
|
|
382
|
+
"reason": None if audit["status"] == "PASS" else "REPORT_CONTRACT_AUDIT_FAILED",
|
|
383
|
+
"problem_count": audit["problem_count"],
|
|
384
|
+
}
|
|
385
|
+
if audit["status"] != "PASS":
|
|
386
|
+
coverage["blocking_gaps"].append(
|
|
387
|
+
{
|
|
388
|
+
"id": "conformance-audit:report-contract",
|
|
389
|
+
"blocking": True,
|
|
390
|
+
"rule_ids": ["PTSIP-DIA-001", "PTSIP-EVD-003"],
|
|
391
|
+
"evidence_ids": ["conformance-audit:report-contract"],
|
|
392
|
+
"message": "Internal conformance report contract audit failed: " + "; ".join(str(item) for item in audit["problems"][:10]),
|
|
393
|
+
}
|
|
394
|
+
)
|
|
395
|
+
_recalculate_outcome(report)
|
|
396
|
+
return ConformanceResult(report=report)
|
ptsip/constants.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
TOOL_NAME = "PTSIP Tool"
|
|
4
|
+
TOOL_VERSION = "0.3.1"
|
|
5
|
+
SPEC_NAME = "Product–Toolchain SDK Isolation Policy"
|
|
6
|
+
SPEC_ACRONYM = "PTSIP"
|
|
7
|
+
SPEC_VERSION = "0.2.0-draft"
|
|
8
|
+
SPEC_SOURCE = "https://github.com/kwaksinwoo01/ptsip"
|
|
9
|
+
SPEC_REVISION = "a877b2f66a7f94c1b844c979e1b08fb08a9a8e45"
|
ptsip/doctor.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .storage.local_state import ptsip_home
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def doctor(path: str | Path = ".") -> dict[str, object]:
|
|
11
|
+
target = Path(path).expanduser().resolve()
|
|
12
|
+
current_minor = (sys.version_info.major, sys.version_info.minor)
|
|
13
|
+
metadata_supported = current_minor >= (3, 11)
|
|
14
|
+
ci_verified = (3, 11) <= current_minor <= (3, 14)
|
|
15
|
+
github_cli = shutil.which("gh")
|
|
16
|
+
return {
|
|
17
|
+
"python": sys.version.split()[0],
|
|
18
|
+
"python_ok": metadata_supported,
|
|
19
|
+
"python_supported_by_metadata": metadata_supported,
|
|
20
|
+
"python_ci_verified": ci_verified,
|
|
21
|
+
"python_ci_verified_range": "3.11-3.14",
|
|
22
|
+
"python_support_note": (
|
|
23
|
+
"This interpreter is within the current CI-verified range."
|
|
24
|
+
if ci_verified
|
|
25
|
+
else "Package metadata allows this interpreter, but this minor version is outside the current CI matrix."
|
|
26
|
+
),
|
|
27
|
+
"git": shutil.which("git"),
|
|
28
|
+
"github_cli": github_cli,
|
|
29
|
+
"github_issue_publish_available": github_cli is not None,
|
|
30
|
+
"target": str(target),
|
|
31
|
+
"target_exists": target.exists(),
|
|
32
|
+
"ptsip_home": str(ptsip_home()),
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Read-only repository inspection."""
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import asdict, dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ..model import DecisionStatus
|
|
8
|
+
from .dependencies import DependencyScan
|
|
9
|
+
from .inventory import Inventory
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class ComponentCandidate:
|
|
14
|
+
id: str
|
|
15
|
+
include: tuple[str, ...]
|
|
16
|
+
anchors: tuple[str, ...]
|
|
17
|
+
evidence_ids: tuple[str, ...]
|
|
18
|
+
decision_status: DecisionStatus = DecisionStatus.UNKNOWN
|
|
19
|
+
classification: None = None
|
|
20
|
+
|
|
21
|
+
def as_dict(self) -> dict[str, object]:
|
|
22
|
+
payload = asdict(self)
|
|
23
|
+
payload["decision_status"] = self.decision_status.value
|
|
24
|
+
return payload
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _slug(value: str) -> str:
|
|
28
|
+
text = re.sub(r"[^a-zA-Z0-9]+", "-", value).strip("-").lower()
|
|
29
|
+
return text or "repository-root"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def discover_component_candidates(root: str | Path, inventory: Inventory, dependencies: DependencyScan) -> list[ComponentCandidate]:
|
|
33
|
+
root = Path(root).resolve()
|
|
34
|
+
raw: dict[str, dict[str, set[str]]] = {}
|
|
35
|
+
|
|
36
|
+
def add(selector: str, anchor: str, evidence_id: str) -> None:
|
|
37
|
+
item = raw.setdefault(selector, {"anchors": set(), "evidence_ids": set()})
|
|
38
|
+
item["anchors"].add(anchor)
|
|
39
|
+
item["evidence_ids"].add(evidence_id)
|
|
40
|
+
|
|
41
|
+
for manifest in inventory.manifests:
|
|
42
|
+
parent = Path(manifest).parent.as_posix()
|
|
43
|
+
selector = manifest if parent in {".", ""} else f"{parent}/**"
|
|
44
|
+
add(selector, "manifest", f"manifest:{manifest}")
|
|
45
|
+
|
|
46
|
+
schema_parents: dict[str, int] = {}
|
|
47
|
+
for schema in inventory.schema_candidates:
|
|
48
|
+
parent = Path(schema).parent.as_posix()
|
|
49
|
+
schema_parents[parent] = schema_parents.get(parent, 0) + 1
|
|
50
|
+
for parent, count in schema_parents.items():
|
|
51
|
+
selector = f"{parent}/**" if parent not in {".", ""} else "*.schema.*"
|
|
52
|
+
add(selector, "contract/schema-group", f"schema-group:{parent}:{count}")
|
|
53
|
+
|
|
54
|
+
for name in inventory.tool_like_roots:
|
|
55
|
+
add(f"{name}/**", "top-level-tool-root", f"root:{name}")
|
|
56
|
+
for name in inventory.test_roots:
|
|
57
|
+
add(f"{name}/**", "top-level-test-root", f"root:{name}")
|
|
58
|
+
|
|
59
|
+
for edge in dependencies.edges:
|
|
60
|
+
if edge.adapter == "github-actions" and edge.resolved_path:
|
|
61
|
+
add(edge.resolved_path, "ci-invoked-script", edge.evidence_id)
|
|
62
|
+
|
|
63
|
+
candidates: list[ComponentCandidate] = []
|
|
64
|
+
for selector, details in sorted(raw.items()):
|
|
65
|
+
selector_path = selector.removesuffix("/**")
|
|
66
|
+
candidates.append(
|
|
67
|
+
ComponentCandidate(
|
|
68
|
+
id=_slug(selector_path),
|
|
69
|
+
include=(selector,),
|
|
70
|
+
anchors=tuple(sorted(details["anchors"])),
|
|
71
|
+
evidence_ids=tuple(sorted(details["evidence_ids"])),
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
return candidates
|