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.
Files changed (66) hide show
  1. ptsip/__init__.py +3 -0
  2. ptsip/__main__.py +4 -0
  3. ptsip/app/__init__.py +5 -0
  4. ptsip/app/client.py +53 -0
  5. ptsip/app/github_client.py +200 -0
  6. ptsip/app/server.py +130 -0
  7. ptsip/app/service.py +285 -0
  8. ptsip/app/store.py +233 -0
  9. ptsip/artifact_evidence.py +150 -0
  10. ptsip/build_resolution.py +298 -0
  11. ptsip/clarification/__init__.py +6 -0
  12. ptsip/clarification/generator.py +120 -0
  13. ptsip/clarification/generator_core.py +98 -0
  14. ptsip/clarification/i18n.py +106 -0
  15. ptsip/clarification/model.py +33 -0
  16. ptsip/clarification/render.py +78 -0
  17. ptsip/clarification/resolution/__init__.py +29 -0
  18. ptsip/clarification/resolution/model.py +33 -0
  19. ptsip/clarification/resolution/parser.py +56 -0
  20. ptsip/clarification/resolution/profile_projection.py +198 -0
  21. ptsip/clarification/resolution/resolver.py +30 -0
  22. ptsip/clarification/transports/__init__.py +1 -0
  23. ptsip/clarification/transports/github_issue.py +162 -0
  24. ptsip/cli.py +402 -0
  25. ptsip/conformance.py +531 -0
  26. ptsip/conformance_audit.py +143 -0
  27. ptsip/conformance_engine.py +396 -0
  28. ptsip/constants.py +9 -0
  29. ptsip/doctor.py +33 -0
  30. ptsip/inspection/__init__.py +1 -0
  31. ptsip/inspection/components.py +74 -0
  32. ptsip/inspection/dependencies.py +496 -0
  33. ptsip/inspection/dependencies_030.py +69 -0
  34. ptsip/inspection/dotnet.py +188 -0
  35. ptsip/inspection/go.py +157 -0
  36. ptsip/inspection/inventory.py +161 -0
  37. ptsip/inspection/javascript.py +376 -0
  38. ptsip/inspection/lexing.py +121 -0
  39. ptsip/inspection/source_adapters.py +60 -0
  40. ptsip/lifecycle_evidence.py +245 -0
  41. ptsip/model.py +105 -0
  42. ptsip/pilot/__init__.py +1 -0
  43. ptsip/pilot/runner.py +170 -0
  44. ptsip/repository/__init__.py +1 -0
  45. ptsip/repository/discover.py +68 -0
  46. ptsip/repository/remote.py +54 -0
  47. ptsip/repository/snapshot.py +173 -0
  48. ptsip/review_evidence.py +406 -0
  49. ptsip/spec_identity.py +22 -0
  50. ptsip/specdata/ptsip-agent-classification.schema.json +35 -0
  51. ptsip/specdata/ptsip-artifact-evidence.schema.json +68 -0
  52. ptsip/specdata/ptsip-diagnostic.schema.json +29 -0
  53. ptsip/specdata/ptsip-profile.schema.json +310 -0
  54. ptsip/specdata/ptsip-registry.yaml +189 -0
  55. ptsip/storage/__init__.py +1 -0
  56. ptsip/storage/local_state.py +32 -0
  57. ptsip/validation/__init__.py +1 -0
  58. ptsip/validation/components.py +164 -0
  59. ptsip/validation/profile.py +172 -0
  60. ptsip/validation/rules.py +163 -0
  61. ptsip-0.3.1.dist-info/METADATA +338 -0
  62. ptsip-0.3.1.dist-info/RECORD +66 -0
  63. ptsip-0.3.1.dist-info/WHEEL +5 -0
  64. ptsip-0.3.1.dist-info/entry_points.txt +3 -0
  65. ptsip-0.3.1.dist-info/licenses/LICENSE +201 -0
  66. 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