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
ptsip/cli.py
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from .app.client import ControlPlaneClient
|
|
8
|
+
from .clarification.generator import analyze_clarifications
|
|
9
|
+
from .clarification.i18n import resolve_language
|
|
10
|
+
from .clarification.render import render_console, render_issue
|
|
11
|
+
from .clarification.resolution import (
|
|
12
|
+
DecisionAnswer,
|
|
13
|
+
prepare_local_profile,
|
|
14
|
+
validate_answer,
|
|
15
|
+
write_prepared_local_profile,
|
|
16
|
+
)
|
|
17
|
+
from .clarification.transports.github_issue import publish as publish_github_issues
|
|
18
|
+
from .conformance_engine import evaluate_conformance
|
|
19
|
+
from .constants import TOOL_VERSION
|
|
20
|
+
from .doctor import doctor
|
|
21
|
+
from .inspection.components import discover_component_candidates
|
|
22
|
+
from .inspection.dependencies_030 import scan_dependency_edges
|
|
23
|
+
from .inspection.inventory import collect_inventory
|
|
24
|
+
from .pilot.runner import run_pilot
|
|
25
|
+
from .repository.discover import discover_repository
|
|
26
|
+
from .repository.snapshot import capture_snapshot, compare_snapshots
|
|
27
|
+
from .spec_identity import current_spec_identity
|
|
28
|
+
from .validation.profile import validate_profile
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _configure_console_encoding() -> None:
|
|
32
|
+
for stream in (sys.stdout, sys.stderr):
|
|
33
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
34
|
+
if callable(reconfigure):
|
|
35
|
+
reconfigure(errors="backslashreplace")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _emit(payload: object, as_json: bool) -> None:
|
|
39
|
+
if as_json:
|
|
40
|
+
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
41
|
+
return
|
|
42
|
+
if isinstance(payload, dict):
|
|
43
|
+
for key, value in payload.items():
|
|
44
|
+
print(f"{key}: {value}")
|
|
45
|
+
else:
|
|
46
|
+
print(payload)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _yes_no(value: str) -> bool:
|
|
50
|
+
return value.casefold() == "yes"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _parser() -> argparse.ArgumentParser:
|
|
54
|
+
parser = argparse.ArgumentParser(prog="ptsip", description="Reference development tooling for PTSIP")
|
|
55
|
+
parser.add_argument("--version", action="version", version=f"PTSIP Tool {TOOL_VERSION}")
|
|
56
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
57
|
+
|
|
58
|
+
p_spec = sub.add_parser("spec", help="Show the canonical specification binding embedded in this tool")
|
|
59
|
+
p_spec.add_argument("--json", action="store_true")
|
|
60
|
+
|
|
61
|
+
p_doctor = sub.add_parser("doctor", help="Check the local environment without modifying the target repository")
|
|
62
|
+
p_doctor.add_argument("path", nargs="?", default=".")
|
|
63
|
+
p_doctor.add_argument("--json", action="store_true")
|
|
64
|
+
|
|
65
|
+
p_inspect = sub.add_parser("inspect", help="Read-only repository evidence collection")
|
|
66
|
+
p_inspect.add_argument("path", nargs="?", default=".")
|
|
67
|
+
p_inspect.add_argument("--json", action="store_true")
|
|
68
|
+
|
|
69
|
+
p_pilot = sub.add_parser("pilot", help="Run a read-only PTSIP pilot and store the report outside the repository by default")
|
|
70
|
+
p_pilot.add_argument("path", nargs="?", default=".")
|
|
71
|
+
p_pilot.add_argument("--report", help="Explicit report destination; may be inside the repository only when the user chooses it")
|
|
72
|
+
p_pilot.add_argument("--json", action="store_true")
|
|
73
|
+
|
|
74
|
+
p_validate = sub.add_parser("validate", help="Validate an existing PTSIP project profile")
|
|
75
|
+
p_validate.add_argument("path", nargs="?", default=".")
|
|
76
|
+
p_validate.add_argument("--profile", help="Explicit project-profile path")
|
|
77
|
+
p_validate.add_argument("--json", action="store_true")
|
|
78
|
+
|
|
79
|
+
p_conform = sub.add_parser(
|
|
80
|
+
"conform",
|
|
81
|
+
help="Evaluate Enforced PTSIP conformance from declared architecture and observed evidence",
|
|
82
|
+
)
|
|
83
|
+
p_conform.add_argument("path", nargs="?", default=".")
|
|
84
|
+
p_conform.add_argument("--profile", help="Explicit project-profile path")
|
|
85
|
+
p_conform.add_argument(
|
|
86
|
+
"--artifact-evidence",
|
|
87
|
+
action="append",
|
|
88
|
+
help="Explicit ptsip-artifact-evidence/v1 JSON/YAML input; repeatable and read-only",
|
|
89
|
+
)
|
|
90
|
+
p_conform.add_argument(
|
|
91
|
+
"--agent-decision",
|
|
92
|
+
action="append",
|
|
93
|
+
help="Explicit ptsip-agent-classification decision input; repeatable review evidence that never overrides the profile",
|
|
94
|
+
)
|
|
95
|
+
p_conform.add_argument(
|
|
96
|
+
"--external-evidence",
|
|
97
|
+
action="append",
|
|
98
|
+
help="Explicit ptsip-external-evidence/v1 JSON/YAML dependency evidence; repeatable and subject/revision checked",
|
|
99
|
+
)
|
|
100
|
+
p_conform.add_argument("--json", action="store_true")
|
|
101
|
+
|
|
102
|
+
p_clarify = sub.add_parser(
|
|
103
|
+
"clarify",
|
|
104
|
+
help="Generate deterministic human clarification requests instead of speculatively inferring missing architectural intent",
|
|
105
|
+
)
|
|
106
|
+
p_clarify.add_argument("path", nargs="?", default=".")
|
|
107
|
+
p_clarify.add_argument("--json", action="store_true")
|
|
108
|
+
p_clarify.add_argument("--lang", choices=("en", "ko"), help="Question language; otherwise PTSIP_LANG, OS locale, then English")
|
|
109
|
+
p_clarify.add_argument("--component", action="append", help="Limit clarification to a detected component candidate ID; repeatable")
|
|
110
|
+
p_clarify.add_argument("--publish", choices=("github-issue",), help="Manual/offline fallback: explicitly publish clarification requests")
|
|
111
|
+
p_clarify.add_argument("--repo", help="Override the detected GitHub origin using owner/repository; requires --publish github-issue")
|
|
112
|
+
|
|
113
|
+
p_gate = sub.add_parser(
|
|
114
|
+
"gate",
|
|
115
|
+
help="Poll/register architecture decisions only when an active coding-agent task requires them",
|
|
116
|
+
)
|
|
117
|
+
p_gate.add_argument("path", nargs="?", default=".")
|
|
118
|
+
p_gate.add_argument("--component", action="append", help="Limit the gate to a detected component candidate ID; repeatable")
|
|
119
|
+
p_gate.add_argument("--lang", choices=("en", "ko"), help="Issue language; otherwise PTSIP_LANG, OS locale, then English")
|
|
120
|
+
p_gate.add_argument("--control-plane", help="PTSIP control-plane base URL; otherwise PTSIP_CONTROL_PLANE_URL")
|
|
121
|
+
p_gate.add_argument("--json", action="store_true")
|
|
122
|
+
|
|
123
|
+
p_resolve = sub.add_parser(
|
|
124
|
+
"resolve",
|
|
125
|
+
help="Explicitly resolve a pending PTSIP decision from an active user/coding-agent session and apply the profile locally",
|
|
126
|
+
)
|
|
127
|
+
p_resolve.add_argument("path", nargs="?", default=".")
|
|
128
|
+
p_resolve.add_argument("--decision", required=True, help="Decision/clarification ID returned by ptsip gate")
|
|
129
|
+
p_resolve.add_argument("--classification", required=True, choices=("PRODUCT", "TOOLCHAIN", "NEUTRAL_CONTRACT"))
|
|
130
|
+
p_resolve.add_argument("--purpose", required=True)
|
|
131
|
+
p_resolve.add_argument("--shipped", required=True, choices=("yes", "no"))
|
|
132
|
+
p_resolve.add_argument("--runtime-required", required=True, choices=("yes", "no"))
|
|
133
|
+
p_resolve.add_argument(
|
|
134
|
+
"--lifecycle-owner",
|
|
135
|
+
required=True,
|
|
136
|
+
choices=("PRODUCT", "DEVELOPMENT_TOOLING", "INDEPENDENT"),
|
|
137
|
+
)
|
|
138
|
+
p_resolve.add_argument("--executable", required=True, choices=("yes", "no"))
|
|
139
|
+
p_resolve.add_argument("--actor", default="coding-agent-session", help="Audit actor label; no free-form inference is performed")
|
|
140
|
+
p_resolve.add_argument("--control-plane", help="PTSIP control-plane base URL; otherwise PTSIP_CONTROL_PLANE_URL")
|
|
141
|
+
p_resolve.add_argument("--json", action="store_true")
|
|
142
|
+
return parser
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def main(argv: list[str] | None = None) -> int:
|
|
146
|
+
_configure_console_encoding()
|
|
147
|
+
args = _parser().parse_args(argv)
|
|
148
|
+
try:
|
|
149
|
+
if args.command == "spec":
|
|
150
|
+
_emit(current_spec_identity().as_dict(), args.json)
|
|
151
|
+
return 0
|
|
152
|
+
if args.command == "doctor":
|
|
153
|
+
result = doctor(args.path)
|
|
154
|
+
_emit(result, args.json)
|
|
155
|
+
return 0 if result["python_ok"] and result["target_exists"] else 2
|
|
156
|
+
if args.command == "inspect":
|
|
157
|
+
repo = discover_repository(args.path)
|
|
158
|
+
before = capture_snapshot(repo.root)
|
|
159
|
+
inventory = collect_inventory(repo.root)
|
|
160
|
+
dependencies = scan_dependency_edges(repo.root)
|
|
161
|
+
candidates = discover_component_candidates(repo.root, inventory, dependencies)
|
|
162
|
+
after = capture_snapshot(repo.root)
|
|
163
|
+
comparison = compare_snapshots(before, after)
|
|
164
|
+
payload = {
|
|
165
|
+
"repository": repo.as_dict(),
|
|
166
|
+
"snapshot": {
|
|
167
|
+
"before": before.as_dict(),
|
|
168
|
+
"after": after.as_dict(),
|
|
169
|
+
"comparison": comparison.as_dict(),
|
|
170
|
+
},
|
|
171
|
+
"non_intrusion": {
|
|
172
|
+
"status": "VERIFIED_NO_OBSERVED_CHANGE" if comparison.stable else "CHANGE_OBSERVED_DURING_ANALYSIS",
|
|
173
|
+
"analysis_read_only_by_design": True,
|
|
174
|
+
},
|
|
175
|
+
"inventory": inventory.as_dict(),
|
|
176
|
+
"components": {
|
|
177
|
+
"status": "CANDIDATES_ONLY",
|
|
178
|
+
"candidates": [candidate.as_dict() for candidate in candidates],
|
|
179
|
+
},
|
|
180
|
+
"dependencies": dependencies.as_dict(),
|
|
181
|
+
}
|
|
182
|
+
_emit(payload, args.json)
|
|
183
|
+
return 0 if comparison.stable else 4
|
|
184
|
+
if args.command == "pilot":
|
|
185
|
+
result = run_pilot(args.path, args.report)
|
|
186
|
+
_emit({**result.report, "report_path": str(result.report_path)}, args.json)
|
|
187
|
+
stable = bool(result.report["snapshot"]["comparison"]["stable"])
|
|
188
|
+
return 0 if stable else 4
|
|
189
|
+
if args.command == "validate":
|
|
190
|
+
repo = discover_repository(args.path)
|
|
191
|
+
result = validate_profile(repo.root, args.profile)
|
|
192
|
+
_emit(result.as_dict(), args.json)
|
|
193
|
+
return 0 if result.valid else 3
|
|
194
|
+
if args.command == "conform":
|
|
195
|
+
result = evaluate_conformance(
|
|
196
|
+
args.path,
|
|
197
|
+
args.profile,
|
|
198
|
+
args.artifact_evidence,
|
|
199
|
+
args.agent_decision,
|
|
200
|
+
args.external_evidence,
|
|
201
|
+
)
|
|
202
|
+
_emit(result.report, args.json)
|
|
203
|
+
if result.outcome == "CONFORMANT":
|
|
204
|
+
return 0
|
|
205
|
+
if result.outcome == "NON_CONFORMANT":
|
|
206
|
+
return 5
|
|
207
|
+
return 6
|
|
208
|
+
if args.command == "clarify":
|
|
209
|
+
if args.repo and args.publish != "github-issue":
|
|
210
|
+
raise ValueError("--repo requires --publish github-issue")
|
|
211
|
+
language = resolve_language(args.lang)
|
|
212
|
+
analysis = analyze_clarifications(args.path, args.component)
|
|
213
|
+
payload = analysis.as_dict(language)
|
|
214
|
+
publications = ()
|
|
215
|
+
if args.publish == "github-issue":
|
|
216
|
+
if not analysis.comparison.stable:
|
|
217
|
+
raise RuntimeError("Repository state changed during clarification analysis; refusing to publish questions from invalidated evidence.")
|
|
218
|
+
publications = publish_github_issues(
|
|
219
|
+
repository_root=analysis.repository.root,
|
|
220
|
+
remote=analysis.repository.remote,
|
|
221
|
+
repository_revision=analysis.repository.commit,
|
|
222
|
+
requests=analysis.requests,
|
|
223
|
+
language=language,
|
|
224
|
+
repo_override=args.repo,
|
|
225
|
+
)
|
|
226
|
+
payload["publication"] = {
|
|
227
|
+
"transport": "github-issue",
|
|
228
|
+
"results": [item.as_dict() for item in publications],
|
|
229
|
+
}
|
|
230
|
+
if args.json:
|
|
231
|
+
_emit(payload, True)
|
|
232
|
+
else:
|
|
233
|
+
print(render_console(analysis.requests, language))
|
|
234
|
+
for item in publications:
|
|
235
|
+
print(f"github_issue[{item.status}]: {item.issue_url}")
|
|
236
|
+
return 0 if analysis.comparison.stable else 4
|
|
237
|
+
if args.command == "gate":
|
|
238
|
+
language = resolve_language(args.lang)
|
|
239
|
+
analysis = analyze_clarifications(args.path, args.component)
|
|
240
|
+
if not analysis.comparison.stable:
|
|
241
|
+
raise RuntimeError("Repository state changed during decision-gate analysis; retry against a stable snapshot.")
|
|
242
|
+
repo = analysis.repository
|
|
243
|
+
if not analysis.requests:
|
|
244
|
+
payload = {"status": "NO_DECISION_REQUIRED", "repository": repo.as_dict(), "decisions": []}
|
|
245
|
+
_emit(payload, args.json)
|
|
246
|
+
return 0
|
|
247
|
+
if not repo.remote or repo.remote.provider != "github" or not repo.remote.repository:
|
|
248
|
+
raise RuntimeError("ptsip gate requires a GitHub origin for the decision control plane")
|
|
249
|
+
if not repo.commit or not repo.branch:
|
|
250
|
+
raise RuntimeError("ptsip gate requires a checked-out Git branch and commit")
|
|
251
|
+
client = ControlPlaneClient(args.control_plane)
|
|
252
|
+
decisions: list[dict[str, object]] = []
|
|
253
|
+
blocked = False
|
|
254
|
+
errored = False
|
|
255
|
+
for request in analysis.requests:
|
|
256
|
+
title, body = render_issue(request, language, repo.commit)
|
|
257
|
+
response = client.gate(
|
|
258
|
+
{
|
|
259
|
+
"id": request.id,
|
|
260
|
+
"repository": repo.remote.repository,
|
|
261
|
+
"branch": repo.branch,
|
|
262
|
+
"subject_revision": repo.commit,
|
|
263
|
+
"component_id": request.component_id,
|
|
264
|
+
"request": request.as_dict(),
|
|
265
|
+
"issue": {"title": title, "body": body},
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
decisions.append(response)
|
|
269
|
+
status = str(response.get("status", ""))
|
|
270
|
+
if status == "DECISION_REQUIRED":
|
|
271
|
+
blocked = True
|
|
272
|
+
elif status in {"STALE", "CONFLICT", "INVALID", "RESOLVED_APPLICATION_REQUIRED"}:
|
|
273
|
+
errored = True
|
|
274
|
+
status = "DECISION_REQUIRED" if blocked else ("DECISION_ERROR" if errored else "RESOLVED")
|
|
275
|
+
_emit({"status": status, "repository": repo.as_dict(), "decisions": decisions}, args.json)
|
|
276
|
+
if blocked:
|
|
277
|
+
return 7
|
|
278
|
+
if errored:
|
|
279
|
+
return 8
|
|
280
|
+
return 0
|
|
281
|
+
if args.command == "resolve":
|
|
282
|
+
repo = discover_repository(args.path)
|
|
283
|
+
if not repo.commit or not repo.branch:
|
|
284
|
+
raise RuntimeError("ptsip resolve requires a checked-out Git branch and commit")
|
|
285
|
+
if not repo.remote or repo.remote.provider != "github" or not repo.remote.repository:
|
|
286
|
+
raise RuntimeError("ptsip resolve requires a GitHub origin matching the decision control plane")
|
|
287
|
+
answer = DecisionAnswer(
|
|
288
|
+
classification=args.classification,
|
|
289
|
+
purpose=args.purpose.strip(),
|
|
290
|
+
shipped=_yes_no(args.shipped),
|
|
291
|
+
runtime_required=_yes_no(args.runtime_required),
|
|
292
|
+
lifecycle_owner=args.lifecycle_owner,
|
|
293
|
+
executable=_yes_no(args.executable),
|
|
294
|
+
)
|
|
295
|
+
validation = validate_answer(answer)
|
|
296
|
+
if not validation.valid:
|
|
297
|
+
_emit({"status": "CONFLICT", "validation": validation.as_dict()}, args.json)
|
|
298
|
+
return 8
|
|
299
|
+
|
|
300
|
+
client = ControlPlaneClient(args.control_plane)
|
|
301
|
+
lookup = client.decision({"decision_id": args.decision})
|
|
302
|
+
decision = lookup.get("decision")
|
|
303
|
+
if not isinstance(decision, dict):
|
|
304
|
+
raise RuntimeError("Control plane returned no decision record")
|
|
305
|
+
if str(decision.get("repository", "")) != repo.remote.repository:
|
|
306
|
+
raise RuntimeError("Decision repository does not match the local GitHub origin")
|
|
307
|
+
if str(decision.get("branch", "")) != repo.branch:
|
|
308
|
+
raise RuntimeError("Decision branch does not match the checked-out local branch; run ptsip gate first")
|
|
309
|
+
if str(decision.get("subject_revision", "")) != repo.commit:
|
|
310
|
+
_emit(
|
|
311
|
+
{
|
|
312
|
+
"status": "STALE_REQUIRES_GATE",
|
|
313
|
+
"message": "Decision target revision differs from the active repository. Run ptsip gate for the affected component before retrying the same authoritative decision.",
|
|
314
|
+
"decision": decision,
|
|
315
|
+
},
|
|
316
|
+
args.json,
|
|
317
|
+
)
|
|
318
|
+
return 8
|
|
319
|
+
|
|
320
|
+
existing_status = str(decision.get("status", ""))
|
|
321
|
+
stored_answer = decision.get("answer")
|
|
322
|
+
application_status = str(decision.get("application_status", ""))
|
|
323
|
+
if existing_status == "RESOLVED":
|
|
324
|
+
if stored_answer != answer.as_dict():
|
|
325
|
+
_emit(
|
|
326
|
+
{
|
|
327
|
+
"status": "ALREADY_RESOLVED",
|
|
328
|
+
"accepted": False,
|
|
329
|
+
"message": "The authoritative decision already exists with different facts; this later answer cannot replace it.",
|
|
330
|
+
"decision": decision,
|
|
331
|
+
},
|
|
332
|
+
args.json,
|
|
333
|
+
)
|
|
334
|
+
return 9
|
|
335
|
+
if application_status in {"APPLIED", "LOCAL_APPLIED"}:
|
|
336
|
+
_emit({"status": "ALREADY_APPLIED", "decision": decision}, args.json)
|
|
337
|
+
return 0
|
|
338
|
+
elif existing_status != "PENDING":
|
|
339
|
+
_emit({"status": existing_status or "DECISION_ERROR", "decision": decision}, args.json)
|
|
340
|
+
return 8
|
|
341
|
+
|
|
342
|
+
request = decision.get("request")
|
|
343
|
+
if not isinstance(request, dict):
|
|
344
|
+
raise RuntimeError("Decision record has no request payload")
|
|
345
|
+
include = request.get("include")
|
|
346
|
+
component_id = str(decision.get("component_id", ""))
|
|
347
|
+
if not isinstance(include, list) or not component_id:
|
|
348
|
+
raise RuntimeError("Decision request has no component include selectors")
|
|
349
|
+
|
|
350
|
+
# Full local profile validation happens before the central CAS. A
|
|
351
|
+
# profile-conflicting answer therefore cannot become the first
|
|
352
|
+
# authoritative resolution merely because validation happened late.
|
|
353
|
+
try:
|
|
354
|
+
prepared = prepare_local_profile(repo.root, component_id, [str(item) for item in include], answer)
|
|
355
|
+
except (ValueError, RuntimeError) as exc:
|
|
356
|
+
_emit({"status": "CONFLICT", "message": str(exc), "decision": decision}, args.json)
|
|
357
|
+
return 8
|
|
358
|
+
|
|
359
|
+
response = client.resolve(
|
|
360
|
+
{
|
|
361
|
+
"decision_id": args.decision,
|
|
362
|
+
"answer": answer.as_dict(),
|
|
363
|
+
"actor": args.actor,
|
|
364
|
+
}
|
|
365
|
+
)
|
|
366
|
+
if response.get("status") != "RESOLVED" or not response.get("accepted"):
|
|
367
|
+
_emit(response, args.json)
|
|
368
|
+
return 9
|
|
369
|
+
resolved = response.get("decision")
|
|
370
|
+
if not isinstance(resolved, dict):
|
|
371
|
+
raise RuntimeError("Control plane returned no resolved decision record")
|
|
372
|
+
if str(resolved.get("subject_revision", "")) != repo.commit:
|
|
373
|
+
client.application({"decision_id": args.decision, "status": "STALE"})
|
|
374
|
+
_emit({"status": "STALE_REQUIRES_GATE", "decision": resolved}, args.json)
|
|
375
|
+
return 8
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
profile = write_prepared_local_profile(prepared)
|
|
379
|
+
except Exception:
|
|
380
|
+
client.application({"decision_id": args.decision, "status": "FAILED"})
|
|
381
|
+
raise
|
|
382
|
+
application = client.application(
|
|
383
|
+
{
|
|
384
|
+
"decision_id": args.decision,
|
|
385
|
+
"status": "LOCAL_APPLIED",
|
|
386
|
+
"applied_revision": repo.commit,
|
|
387
|
+
}
|
|
388
|
+
)
|
|
389
|
+
_emit(
|
|
390
|
+
{
|
|
391
|
+
"status": "RESOLVED",
|
|
392
|
+
"decision": resolved,
|
|
393
|
+
"profile_path": str(profile),
|
|
394
|
+
"application": application,
|
|
395
|
+
},
|
|
396
|
+
args.json,
|
|
397
|
+
)
|
|
398
|
+
return 0
|
|
399
|
+
except (FileNotFoundError, PermissionError, OSError, RuntimeError, ValueError) as exc:
|
|
400
|
+
print(f"PTSIP error: {exc}", file=sys.stderr)
|
|
401
|
+
return 2
|
|
402
|
+
return 2
|