patchshuttle 0.1.0a2__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 (44) hide show
  1. patchshuttle/__init__.py +98 -0
  2. patchshuttle/_diff.py +317 -0
  3. patchshuttle/_process.py +198 -0
  4. patchshuttle/_version.py +3 -0
  5. patchshuttle/actions/__init__.py +80 -0
  6. patchshuttle/actions/constructors.py +211 -0
  7. patchshuttle/actions/create.py +155 -0
  8. patchshuttle/actions/modify.py +174 -0
  9. patchshuttle/audit.py +588 -0
  10. patchshuttle/backup.py +712 -0
  11. patchshuttle/checks/__init__.py +37 -0
  12. patchshuttle/checks/constructors.py +67 -0
  13. patchshuttle/checks/runner.py +233 -0
  14. patchshuttle/cli.py +766 -0
  15. patchshuttle/config.py +247 -0
  16. patchshuttle/context.py +370 -0
  17. patchshuttle/errors.py +291 -0
  18. patchshuttle/execution.py +651 -0
  19. patchshuttle/formatters/__init__.py +25 -0
  20. patchshuttle/formatters/runner.py +240 -0
  21. patchshuttle/identifiers.py +20 -0
  22. patchshuttle/inventory.py +331 -0
  23. patchshuttle/logging.py +741 -0
  24. patchshuttle/models.py +496 -0
  25. patchshuttle/operations.py +292 -0
  26. patchshuttle/parser.py +243 -0
  27. patchshuttle/planner.py +1144 -0
  28. patchshuttle/policy.py +377 -0
  29. patchshuttle/py.typed +1 -0
  30. patchshuttle/registry.py +275 -0
  31. patchshuttle/resources/AI_GUIDE.md +163 -0
  32. patchshuttle/resources/AUDIT-EXAMPLE.psh.yaml +10 -0
  33. patchshuttle/resources/PATCH-EXAMPLE.psh.yaml +17 -0
  34. patchshuttle/resources/PATCHSHUTTLE_PROTOCOL.md +109 -0
  35. patchshuttle/resources/__init__.py +1 -0
  36. patchshuttle/rollback.py +306 -0
  37. patchshuttle/runner.py +880 -0
  38. patchshuttle/verification.py +107 -0
  39. patchshuttle/workspace.py +382 -0
  40. patchshuttle-0.1.0a2.dist-info/METADATA +535 -0
  41. patchshuttle-0.1.0a2.dist-info/RECORD +44 -0
  42. patchshuttle-0.1.0a2.dist-info/WHEEL +4 -0
  43. patchshuttle-0.1.0a2.dist-info/entry_points.txt +2 -0
  44. patchshuttle-0.1.0a2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,651 @@
1
+ """Public approved execution with registry, archive, and log coordination."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import stat
6
+ from dataclasses import dataclass, field
7
+ from enum import Enum
8
+ from os import PathLike
9
+ from pathlib import Path, PurePosixPath
10
+
11
+ import yaml
12
+
13
+ from patchshuttle.audit import (
14
+ AuditActionResult,
15
+ AuditRunResult,
16
+ execute_audit_locked,
17
+ )
18
+ from patchshuttle.checks import CheckResult
19
+ from patchshuttle.errors import ExecutionError, ExecutionErrorCode, JobError
20
+ from patchshuttle.formatters import FormattedFileState, FormatterResult
21
+ from patchshuttle.inventory import WorkspaceComparison
22
+ from patchshuttle.logging import (
23
+ RunClock,
24
+ RunLogData,
25
+ archive_job_source,
26
+ current_run_clock,
27
+ write_run_log,
28
+ )
29
+ from patchshuttle.models import Job, JobKind
30
+ from patchshuttle.parser import load_job
31
+ from patchshuttle.planner import Plan, normalized_job_hash
32
+ from patchshuttle.registry import (
33
+ Registry,
34
+ RegistryDecision,
35
+ decide_job,
36
+ load_registry,
37
+ update_registry,
38
+ )
39
+ from patchshuttle.runner import (
40
+ TransactionResult,
41
+ TransactionStatus,
42
+ acquire_workspace_lock,
43
+ execute_change_transaction_locked,
44
+ )
45
+ from patchshuttle.verification import (
46
+ VerificationRunResult,
47
+ execute_verification_locked,
48
+ )
49
+ from patchshuttle.workspace import Workspace
50
+
51
+
52
+ class RunStatus(str, Enum):
53
+ """Successful outcomes exposed by the public execution API."""
54
+
55
+ COMPLETED = "COMPLETED"
56
+ ALREADY_APPLIED = "ALREADY_APPLIED"
57
+ NO_CHANGE = "NO_CHANGE"
58
+
59
+
60
+ @dataclass(frozen=True, slots=True)
61
+ class RunResult:
62
+ """Immutable public result of one recorded job execution."""
63
+
64
+ status: RunStatus
65
+ plan: Plan = field(repr=False)
66
+ backup_path: Path | None
67
+ created_files: tuple[PurePosixPath, ...]
68
+ created_directories: tuple[PurePosixPath, ...]
69
+ modified_files: tuple[PurePosixPath, ...] = ()
70
+ initial_checks: tuple[CheckResult, ...] = ()
71
+ formatting_results: tuple[FormatterResult, ...] = ()
72
+ formatted_files: tuple[FormattedFileState, ...] = ()
73
+ final_checks: tuple[CheckResult, ...] = ()
74
+ workspace_comparison: WorkspaceComparison | None = None
75
+ audit_results: tuple[AuditActionResult, ...] = ()
76
+ log_path: Path | None = None
77
+ archived_job_path: Path | None = None
78
+
79
+
80
+ @dataclass(frozen=True, slots=True)
81
+ class RegisteredRunResult:
82
+ """An already-applied result resolved before planning is needed."""
83
+
84
+ status: RunStatus
85
+ job: Job
86
+ job_hash: str
87
+ log_path: Path
88
+ archived_job_path: Path
89
+
90
+
91
+ @dataclass(frozen=True, slots=True)
92
+ class _Artifacts:
93
+ log_path: Path
94
+ archived_job_path: Path
95
+
96
+
97
+ def execute_plan(
98
+ plan: Plan,
99
+ *,
100
+ approved: bool = False,
101
+ keep_changes: bool = False,
102
+ source_path: str | PathLike[str] | None = None,
103
+ ) -> RunResult:
104
+ """Execute and record an audit, patch, or approved verify plan.
105
+
106
+ A CLI-provided ``source_path`` is archived byte-for-byte. Python callers
107
+ without a source file receive a deterministic YAML archive of the immutable
108
+ job model. ``keep_changes`` is patch-only, remains subject to local policy,
109
+ and records a skipped rollback explicitly if execution fails.
110
+ """
111
+
112
+ if keep_changes and plan.job.kind is not JobKind.PATCH:
113
+ raise ExecutionError(
114
+ ExecutionErrorCode.JOB_KIND_UNSUPPORTED,
115
+ "keeping failed-job changes is supported only for patch jobs",
116
+ )
117
+ if keep_changes and not plan.workspace.config.execution.allow_keep_changes:
118
+ raise ExecutionError(
119
+ ExecutionErrorCode.KEEP_CHANGES_FORBIDDEN,
120
+ "local workspace policy does not allow keeping failed-job changes",
121
+ )
122
+ if plan.requires_confirmation and not approved:
123
+ raise ExecutionError(
124
+ ExecutionErrorCode.APPROVAL_REQUIRED,
125
+ "explicit approval is required before project code is executed",
126
+ )
127
+
128
+ source = _job_source_bytes(
129
+ plan.workspace,
130
+ plan.job,
131
+ source_path=source_path,
132
+ )
133
+ clock = current_run_clock(plan.workspace)
134
+ with acquire_workspace_lock(plan.workspace):
135
+ registry = load_registry(plan.workspace)
136
+ decision = decide_job(
137
+ registry,
138
+ job_id=plan.job.id,
139
+ job_hash=plan.job_hash,
140
+ )
141
+ if decision is RegistryDecision.PATCH_ID_CONFLICT:
142
+ error = _conflict_error(plan.job.id)
143
+ artifacts = _record_error(
144
+ plan.workspace,
145
+ registry,
146
+ plan.job,
147
+ plan.job_hash,
148
+ source,
149
+ clock,
150
+ error,
151
+ plan=plan,
152
+ )
153
+ _attach_artifacts(error, artifacts)
154
+ raise error
155
+ if decision is RegistryDecision.ALREADY_APPLIED:
156
+ artifacts = _record_success(
157
+ plan.workspace,
158
+ registry,
159
+ plan.job,
160
+ plan.job_hash,
161
+ source,
162
+ clock,
163
+ result=RunStatus.ALREADY_APPLIED,
164
+ plan=plan,
165
+ transaction=None,
166
+ )
167
+ return _already_applied_result(plan, artifacts)
168
+
169
+ transaction: TransactionResult | None = None
170
+ audit_run: AuditRunResult | None = None
171
+ verification: VerificationRunResult | None = None
172
+ try:
173
+ if plan.job.kind is JobKind.AUDIT:
174
+ audit_run = execute_audit_locked(plan)
175
+ status = RunStatus.COMPLETED
176
+ elif plan.job.kind is JobKind.VERIFY:
177
+ verification = execute_verification_locked(plan)
178
+ status = RunStatus.COMPLETED
179
+ else:
180
+ transaction = execute_change_transaction_locked(
181
+ plan,
182
+ approved=True,
183
+ keep_changes=keep_changes,
184
+ )
185
+ status = {
186
+ TransactionStatus.APPLIED: RunStatus.COMPLETED,
187
+ TransactionStatus.NO_CHANGE: RunStatus.NO_CHANGE,
188
+ }[transaction.status]
189
+ except ExecutionError as error:
190
+ artifacts = _record_error(
191
+ plan.workspace,
192
+ registry,
193
+ plan.job,
194
+ plan.job_hash,
195
+ source,
196
+ clock,
197
+ error,
198
+ plan=plan,
199
+ )
200
+ _attach_artifacts(error, artifacts)
201
+ raise
202
+
203
+ artifacts = _record_success(
204
+ plan.workspace,
205
+ registry,
206
+ plan.job,
207
+ plan.job_hash,
208
+ source,
209
+ clock,
210
+ result=status,
211
+ plan=plan,
212
+ transaction=transaction,
213
+ audit_run=audit_run,
214
+ verification=verification,
215
+ )
216
+ return _public_result(
217
+ plan,
218
+ status,
219
+ artifacts,
220
+ transaction=transaction,
221
+ audit_run=audit_run,
222
+ verification=verification,
223
+ )
224
+
225
+
226
+ def resolve_registered_job(
227
+ workspace: Workspace,
228
+ job: Job,
229
+ *,
230
+ source_path: str | PathLike[str] | None = None,
231
+ ) -> RegisteredRunResult | None:
232
+ """Resolve completed or conflicting IDs before mutable planning.
233
+
234
+ ``None`` means the ID/hash pair may proceed to normal planning. The final
235
+ decision is repeated by ``execute_plan`` under its transaction lock.
236
+ """
237
+
238
+ job_hash = normalized_job_hash(job)
239
+ source = _job_source_bytes(workspace, job, source_path=source_path)
240
+ clock = current_run_clock(workspace)
241
+ with acquire_workspace_lock(workspace):
242
+ registry = load_registry(workspace)
243
+ decision = decide_job(registry, job_id=job.id, job_hash=job_hash)
244
+ if decision is RegistryDecision.PROCEED:
245
+ return None
246
+ if decision is RegistryDecision.PATCH_ID_CONFLICT:
247
+ error = _conflict_error(job.id)
248
+ artifacts = _record_error(
249
+ workspace,
250
+ registry,
251
+ job,
252
+ job_hash,
253
+ source,
254
+ clock,
255
+ error,
256
+ plan=None,
257
+ )
258
+ _attach_artifacts(error, artifacts)
259
+ raise error
260
+
261
+ artifacts = _record_success(
262
+ workspace,
263
+ registry,
264
+ job,
265
+ job_hash,
266
+ source,
267
+ clock,
268
+ result=RunStatus.ALREADY_APPLIED,
269
+ plan=None,
270
+ transaction=None,
271
+ )
272
+ return RegisteredRunResult(
273
+ status=RunStatus.ALREADY_APPLIED,
274
+ job=job,
275
+ job_hash=job_hash,
276
+ log_path=artifacts.log_path,
277
+ archived_job_path=artifacts.archived_job_path,
278
+ )
279
+
280
+
281
+ def record_declined_plan(
282
+ plan: Plan,
283
+ *,
284
+ source_path: str | PathLike[str] | None = None,
285
+ ) -> ExecutionError:
286
+ """Record a reviewed patch or verify plan the user explicitly declined."""
287
+
288
+ source = _job_source_bytes(
289
+ plan.workspace,
290
+ plan.job,
291
+ source_path=source_path,
292
+ )
293
+ clock = current_run_clock(plan.workspace)
294
+ with acquire_workspace_lock(plan.workspace):
295
+ registry = load_registry(plan.workspace)
296
+ decision = decide_job(
297
+ registry,
298
+ job_id=plan.job.id,
299
+ job_hash=plan.job_hash,
300
+ )
301
+ error = (
302
+ _conflict_error(plan.job.id)
303
+ if decision is RegistryDecision.PATCH_ID_CONFLICT
304
+ else ExecutionError(
305
+ ExecutionErrorCode.USER_DECLINED,
306
+ "user declined the reviewed job plan",
307
+ )
308
+ )
309
+ artifacts = _record_error(
310
+ plan.workspace,
311
+ registry,
312
+ plan.job,
313
+ plan.job_hash,
314
+ source,
315
+ clock,
316
+ error,
317
+ plan=plan,
318
+ )
319
+ _attach_artifacts(error, artifacts)
320
+ return error
321
+
322
+
323
+ def execution_exit_code(code: ExecutionErrorCode) -> int:
324
+ """Map execution failures to the protocol's stable process groups."""
325
+
326
+ if code is ExecutionErrorCode.OPERATIONAL_RECORD_FAILED:
327
+ return 1
328
+ if code in {
329
+ ExecutionErrorCode.APPROVAL_REQUIRED,
330
+ ExecutionErrorCode.USER_DECLINED,
331
+ ExecutionErrorCode.KEEP_CHANGES_FORBIDDEN,
332
+ }:
333
+ return 4
334
+ if code in {
335
+ ExecutionErrorCode.PATCH_ID_CONFLICT,
336
+ ExecutionErrorCode.WORKSPACE_LOCKED,
337
+ ExecutionErrorCode.WORKSPACE_LOCK_FAILED,
338
+ ExecutionErrorCode.LOG_NOT_FOUND,
339
+ ExecutionErrorCode.JOB_NOT_FOUND,
340
+ }:
341
+ return 3
342
+ if code is ExecutionErrorCode.CHECK_FAILED:
343
+ return 6
344
+ if code is ExecutionErrorCode.FORMAT_FAILED:
345
+ return 7
346
+ if code is ExecutionErrorCode.ROLLBACK_FAILED:
347
+ return 8
348
+ return 5
349
+
350
+
351
+ def _record_success(
352
+ workspace: Workspace,
353
+ registry: Registry,
354
+ job: Job,
355
+ job_hash: str,
356
+ source: bytes,
357
+ clock: RunClock,
358
+ *,
359
+ result: RunStatus,
360
+ plan: Plan | None,
361
+ transaction: TransactionResult | None,
362
+ audit_run: AuditRunResult | None = None,
363
+ verification: VerificationRunResult | None = None,
364
+ ) -> _Artifacts:
365
+ archived = archive_job_source(
366
+ workspace,
367
+ job=job,
368
+ job_hash=job_hash,
369
+ clock=clock,
370
+ source=source,
371
+ successful=True,
372
+ )
373
+ log_path = write_run_log(
374
+ RunLogData(
375
+ workspace=workspace,
376
+ job=job,
377
+ job_hash=job_hash,
378
+ clock=clock,
379
+ result=result.value,
380
+ exit_code=0,
381
+ failure_stage=None,
382
+ failure_code=None,
383
+ archived_job_path=archived,
384
+ plan=plan,
385
+ transaction=transaction,
386
+ audit_results=(audit_run.results if audit_run is not None else ()),
387
+ verification_checks=(
388
+ verification.checks if verification is not None else ()
389
+ ),
390
+ workspace_comparison=(
391
+ verification.workspace_comparison
392
+ if verification is not None
393
+ else (audit_run.workspace_comparison if audit_run is not None else None)
394
+ ),
395
+ )
396
+ )
397
+ update_registry(
398
+ workspace,
399
+ registry,
400
+ job_id=job.id,
401
+ job_hash=job_hash,
402
+ kind=job.kind,
403
+ occurred_at=clock.iso_timestamp,
404
+ result=result.value,
405
+ backup_path=(transaction.backup_path if transaction is not None else None),
406
+ rollback_state="NOT_REQUIRED",
407
+ archived_job_path=archived,
408
+ completed=True,
409
+ )
410
+ return _Artifacts(log_path=log_path, archived_job_path=archived)
411
+
412
+
413
+ def _record_error(
414
+ workspace: Workspace,
415
+ registry: Registry,
416
+ job: Job,
417
+ job_hash: str,
418
+ source: bytes,
419
+ clock: RunClock,
420
+ error: ExecutionError,
421
+ *,
422
+ plan: Plan | None,
423
+ ) -> _Artifacts:
424
+ result, failure_code = _error_result(error)
425
+ archived = archive_job_source(
426
+ workspace,
427
+ job=job,
428
+ job_hash=job_hash,
429
+ clock=clock,
430
+ source=source,
431
+ successful=False,
432
+ )
433
+ log_path = write_run_log(
434
+ RunLogData(
435
+ workspace=workspace,
436
+ job=job,
437
+ job_hash=job_hash,
438
+ clock=clock,
439
+ result=result,
440
+ exit_code=execution_exit_code(error.code),
441
+ failure_stage=_failure_stage(error, plan),
442
+ failure_code=failure_code,
443
+ archived_job_path=archived,
444
+ plan=plan,
445
+ error=error,
446
+ )
447
+ )
448
+ rollback = _rollback_state(error)
449
+ update_registry(
450
+ workspace,
451
+ registry,
452
+ job_id=job.id,
453
+ job_hash=job_hash,
454
+ kind=job.kind,
455
+ occurred_at=clock.iso_timestamp,
456
+ result=result,
457
+ backup_path=error.backup_path,
458
+ rollback_state=rollback,
459
+ archived_job_path=archived,
460
+ completed=False,
461
+ )
462
+ return _Artifacts(log_path=log_path, archived_job_path=archived)
463
+
464
+
465
+ def _public_result(
466
+ plan: Plan,
467
+ status: RunStatus,
468
+ artifacts: _Artifacts,
469
+ *,
470
+ transaction: TransactionResult | None,
471
+ audit_run: AuditRunResult | None,
472
+ verification: VerificationRunResult | None,
473
+ ) -> RunResult:
474
+ return RunResult(
475
+ status=status,
476
+ plan=plan,
477
+ backup_path=(transaction.backup_path if transaction is not None else None),
478
+ created_files=(transaction.created_files if transaction is not None else ()),
479
+ created_directories=(
480
+ transaction.created_directories if transaction is not None else ()
481
+ ),
482
+ modified_files=(transaction.modified_files if transaction is not None else ()),
483
+ initial_checks=(
484
+ transaction.initial_checks
485
+ if transaction is not None
486
+ else verification.checks if verification is not None else ()
487
+ ),
488
+ formatting_results=(
489
+ transaction.formatting_results if transaction is not None else ()
490
+ ),
491
+ formatted_files=(
492
+ transaction.formatted_files if transaction is not None else ()
493
+ ),
494
+ final_checks=(transaction.final_checks if transaction is not None else ()),
495
+ workspace_comparison=(
496
+ transaction.workspace_comparison
497
+ if transaction is not None
498
+ else (
499
+ verification.workspace_comparison
500
+ if verification is not None
501
+ else (audit_run.workspace_comparison if audit_run is not None else None)
502
+ )
503
+ ),
504
+ audit_results=(audit_run.results if audit_run is not None else ()),
505
+ log_path=artifacts.log_path,
506
+ archived_job_path=artifacts.archived_job_path,
507
+ )
508
+
509
+
510
+ def _already_applied_result(plan: Plan, artifacts: _Artifacts) -> RunResult:
511
+ return RunResult(
512
+ status=RunStatus.ALREADY_APPLIED,
513
+ plan=plan,
514
+ backup_path=None,
515
+ created_files=(),
516
+ created_directories=(),
517
+ log_path=artifacts.log_path,
518
+ archived_job_path=artifacts.archived_job_path,
519
+ )
520
+
521
+
522
+ def _job_source_bytes(
523
+ workspace: Workspace,
524
+ job: Job,
525
+ *,
526
+ source_path: str | PathLike[str] | None,
527
+ ) -> bytes:
528
+ if source_path is None:
529
+ payload = job.model_dump(mode="json", exclude_none=True)
530
+ return yaml.safe_dump(
531
+ payload,
532
+ allow_unicode=True,
533
+ sort_keys=False,
534
+ ).encode("utf-8")
535
+
536
+ path = Path(source_path)
537
+ try:
538
+ metadata = path.lstat()
539
+ if not stat.S_ISREG(metadata.st_mode):
540
+ raise OSError("source job is not a regular file")
541
+ raw = path.read_bytes()
542
+ except OSError as exc:
543
+ raise ExecutionError(
544
+ ExecutionErrorCode.PLAN_STALE,
545
+ "source job file is missing, unsafe, or unreadable",
546
+ path=path.as_posix(),
547
+ ) from exc
548
+ if len(raw) > workspace.config.execution.max_job_bytes:
549
+ raise ExecutionError(
550
+ ExecutionErrorCode.PLAN_STALE,
551
+ "source job file exceeds the configured input limit",
552
+ path=path.as_posix(),
553
+ )
554
+ try:
555
+ current = load_job(
556
+ path,
557
+ max_bytes=workspace.config.execution.max_job_bytes,
558
+ )
559
+ except (JobError, ValueError) as exc:
560
+ raise ExecutionError(
561
+ ExecutionErrorCode.PLAN_STALE,
562
+ "source job file no longer matches the approved job",
563
+ path=path.as_posix(),
564
+ ) from exc
565
+ if current != job:
566
+ raise ExecutionError(
567
+ ExecutionErrorCode.PLAN_STALE,
568
+ "source job file no longer matches the approved job",
569
+ path=path.as_posix(),
570
+ )
571
+ return raw
572
+
573
+
574
+ def _error_result(error: ExecutionError) -> tuple[str, str]:
575
+ root = error.cause_code or error.code
576
+ if error.rollback_succeeded is True:
577
+ return "ROLLED_BACK", root.value
578
+ if error.rollback_succeeded is False:
579
+ return "ROLLBACK_FAILED", root.value
580
+ return error.code.value, root.value
581
+
582
+
583
+ def _rollback_state(error: ExecutionError) -> str:
584
+ if error.rollback_skipped:
585
+ return "SKIPPED_CHANGES_KEPT" if error.changes_kept else "SKIPPED_NO_CHANGES"
586
+ return {None: "NOT_STARTED", True: "SUCCESS", False: "FAILED"}[
587
+ error.rollback_succeeded
588
+ ]
589
+
590
+
591
+ def _failure_stage(error: ExecutionError, plan: Plan | None) -> str:
592
+ root = error.cause_code or error.code
593
+ if root is ExecutionErrorCode.PATCH_ID_CONFLICT:
594
+ return "JOB"
595
+ if root in {
596
+ ExecutionErrorCode.WORKSPACE_LOCKED,
597
+ ExecutionErrorCode.WORKSPACE_LOCK_FAILED,
598
+ }:
599
+ return "WORKSPACE"
600
+ if root in {
601
+ ExecutionErrorCode.PLAN_STALE,
602
+ ExecutionErrorCode.ACTION_UNSUPPORTED,
603
+ ExecutionErrorCode.KEEP_CHANGES_FORBIDDEN,
604
+ }:
605
+ return "PLAN"
606
+ if root is ExecutionErrorCode.USER_DECLINED:
607
+ return "PLAN"
608
+ if root is ExecutionErrorCode.BACKUP_FAILED:
609
+ return "BACKUP"
610
+ if root is ExecutionErrorCode.ACTION_FAILED:
611
+ return (
612
+ "AUDIT"
613
+ if plan is not None and plan.job.kind is JobKind.AUDIT
614
+ else "ACTIONS"
615
+ )
616
+ if root is ExecutionErrorCode.CHECK_FAILED:
617
+ if plan is not None and len(error.check_results) > len(plan.checks):
618
+ return "FINAL_CHECKS"
619
+ return "INITIAL_CHECKS"
620
+ if root is ExecutionErrorCode.FORMAT_FAILED:
621
+ return "FORMAT_BLACK" if error.path == "black" else "FORMAT_ISORT"
622
+ if root in {
623
+ ExecutionErrorCode.WORKSPACE_INVENTORY_FAILED,
624
+ ExecutionErrorCode.UNEXPECTED_WORKSPACE_CHANGE,
625
+ }:
626
+ return "WORKSPACE_COMPARISON"
627
+ return "SUMMARY"
628
+
629
+
630
+ def _conflict_error(job_id: str) -> ExecutionError:
631
+ return ExecutionError(
632
+ ExecutionErrorCode.PATCH_ID_CONFLICT,
633
+ "job ID is already registered with different normalized content",
634
+ item_id=job_id,
635
+ )
636
+
637
+
638
+ def _attach_artifacts(error: ExecutionError, artifacts: _Artifacts) -> None:
639
+ error.log_path = artifacts.log_path
640
+ error.archived_job_path = artifacts.archived_job_path
641
+
642
+
643
+ __all__ = [
644
+ "RegisteredRunResult",
645
+ "RunResult",
646
+ "RunStatus",
647
+ "execute_plan",
648
+ "execution_exit_code",
649
+ "record_declined_plan",
650
+ "resolve_registered_job",
651
+ ]
@@ -0,0 +1,25 @@
1
+ """Internal controlled-formatting execution surface."""
2
+
3
+ from patchshuttle.formatters.runner import (
4
+ FormattedFileState,
5
+ FormatterResult,
6
+ FormatterRunResult,
7
+ FormatterStatus,
8
+ PreparedFormatter,
9
+ capture_formatted_files,
10
+ prepare_formatters,
11
+ run_formatters,
12
+ verify_formatted_files,
13
+ )
14
+
15
+ __all__ = [
16
+ "FormattedFileState",
17
+ "FormatterResult",
18
+ "FormatterRunResult",
19
+ "FormatterStatus",
20
+ "PreparedFormatter",
21
+ "capture_formatted_files",
22
+ "prepare_formatters",
23
+ "run_formatters",
24
+ "verify_formatted_files",
25
+ ]