arctx-cli 0.2.0b2__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 (49) hide show
  1. arctx_cli/__init__.py +1 -0
  2. arctx_cli/alias.py +238 -0
  3. arctx_cli/append_batch.py +90 -0
  4. arctx_cli/commands/__init__.py +85 -0
  5. arctx_cli/commands/alias_cmd.py +174 -0
  6. arctx_cli/commands/anchor.py +82 -0
  7. arctx_cli/commands/current.py +69 -0
  8. arctx_cli/commands/cut.py +89 -0
  9. arctx_cli/commands/dump.py +72 -0
  10. arctx_cli/commands/ext.py +236 -0
  11. arctx_cli/commands/git.py +216 -0
  12. arctx_cli/commands/graph.py +73 -0
  13. arctx_cli/commands/guide.py +360 -0
  14. arctx_cli/commands/init.py +223 -0
  15. arctx_cli/commands/list.py +45 -0
  16. arctx_cli/commands/migrate.py +135 -0
  17. arctx_cli/commands/node.py +55 -0
  18. arctx_cli/commands/outcomes.py +58 -0
  19. arctx_cli/commands/payload.py +192 -0
  20. arctx_cli/commands/reachable.py +75 -0
  21. arctx_cli/commands/show.py +113 -0
  22. arctx_cli/commands/sync.py +244 -0
  23. arctx_cli/commands/trace.py +46 -0
  24. arctx_cli/commands/transition.py +212 -0
  25. arctx_cli/commands/use.py +67 -0
  26. arctx_cli/commands/view.py +82 -0
  27. arctx_cli/commands/work_session.py +330 -0
  28. arctx_cli/context.py +38 -0
  29. arctx_cli/ext/__init__.py +1 -0
  30. arctx_cli/ext/command/__init__.py +110 -0
  31. arctx_cli/ext/git/__init__.py +1 -0
  32. arctx_cli/ext/git/branch.py +140 -0
  33. arctx_cli/ext/git/cherry_pick.py +144 -0
  34. arctx_cli/ext/git/commit.py +205 -0
  35. arctx_cli/ext/git/hook.py +758 -0
  36. arctx_cli/ext/git/merge.py +204 -0
  37. arctx_cli/ext/git/reset.py +138 -0
  38. arctx_cli/ext/git/revert.py +157 -0
  39. arctx_cli/ext/git/verify.py +140 -0
  40. arctx_cli/ext/git/worktree.py +173 -0
  41. arctx_cli/ext_registry.py +34 -0
  42. arctx_cli/main.py +133 -0
  43. arctx_cli/paths.py +27 -0
  44. arctx_cli/payload_builder.py +23 -0
  45. arctx_cli/workspace.py +64 -0
  46. arctx_cli-0.2.0b2.dist-info/METADATA +48 -0
  47. arctx_cli-0.2.0b2.dist-info/RECORD +49 -0
  48. arctx_cli-0.2.0b2.dist-info/WHEEL +4 -0
  49. arctx_cli-0.2.0b2.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,204 @@
1
+ """arctx CLI merge command.
2
+
3
+ Drives a git merge (or arctx-only join) and records a multi-input Transition
4
+ with MergePayload or JoinPayload.
5
+
6
+ Usage:
7
+ arctx merge --other <ref> [-m <message>] [--join]
8
+ arctx merge --other branch:<name> [--join]
9
+ arctx merge --other node:<id> [--join]
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import sys
17
+
18
+ from arctx_cli.append_batch import graph_counts, maybe_append_or_save
19
+ from arctx_cli.context import (
20
+ resolve_run_id_from_args,
21
+ resolve_store,
22
+ resolve_user_id_from_args,
23
+ resolve_work_session_id_from_args,
24
+ )
25
+
26
+
27
+ def _parse_merge_ref(ref: str) -> tuple[str | None, str | None]:
28
+ """Parse a merge ref into (other_branch, other_node_id).
29
+
30
+ Formats accepted:
31
+ - "branch:<name>" → other_branch
32
+ - "node:<id>" → other_node_id
33
+ - "<anything>" → treated as branch name
34
+ """
35
+ if ref.startswith("branch:"):
36
+ return ref[len("branch:"):], None
37
+ if ref.startswith("node:"):
38
+ return None, ref[len("node:"):]
39
+ return ref, None
40
+
41
+
42
+ def add_parser(subparsers) -> argparse.ArgumentParser:
43
+ """Register the ``merge`` subcommand parser."""
44
+ p = subparsers.add_parser(
45
+ "merge",
46
+ help="Drive a git merge and record a multi-input arctx transition",
47
+ )
48
+ p.add_argument(
49
+ "--other",
50
+ required=True,
51
+ metavar="REF",
52
+ help=(
53
+ "The branch or node to merge in. Format: 'branch:<name>', "
54
+ "'node:<id>', or just '<name>' (auto-detected as branch name)."
55
+ ),
56
+ )
57
+ p.add_argument(
58
+ "-m",
59
+ "--message",
60
+ default=None,
61
+ help="Override the merge commit message",
62
+ )
63
+ p.add_argument(
64
+ "--branch",
65
+ default=None,
66
+ help="Override the current branch name (default: inferred from git)",
67
+ )
68
+ p.add_argument(
69
+ "--join",
70
+ action="store_true",
71
+ help=(
72
+ "Treat as a arctx-only join (no common ancestor). "
73
+ "Records JoinPayload instead of MergePayload. "
74
+ "Does NOT run git merge."
75
+ ),
76
+ )
77
+ p.add_argument("--run", default=None, help="Explicit run id")
78
+ p.add_argument("--store-dir", default=None, help="Store directory")
79
+ p.add_argument("--user", default=None, help="User id for attribution")
80
+ p.add_argument("--work-session", default=None, help="Work session id")
81
+ return p
82
+
83
+
84
+ def run_merge_command(
85
+ *,
86
+ other: str,
87
+ message: str | None,
88
+ branch: str | None,
89
+ run_id: str | None,
90
+ store_dir: str | None,
91
+ user_id: str | None,
92
+ work_session_id: str | None,
93
+ join: bool = False,
94
+ # Test-only parameters; not exposed in the CLI parser.
95
+ dry_run: bool = False,
96
+ head_commit: str | None = None,
97
+ ) -> dict:
98
+ """Execute a merge (or join) and persist the resulting graph records.
99
+
100
+ Parameters
101
+ ----------
102
+ other:
103
+ Merge target reference. Format: 'branch:<name>', 'node:<id>', or '<name>'.
104
+ message:
105
+ Override the merge commit message. If None, git uses its default.
106
+ branch:
107
+ Current branch name override (None → infer from git).
108
+ run_id:
109
+ Explicit run id. If None, resolved from env / <gitdir>/arctx-id.
110
+ store_dir:
111
+ Store directory. If None, resolved from ARCTX_HOME.
112
+ user_id:
113
+ User id for work event attribution.
114
+ work_session_id:
115
+ Work session id.
116
+ join:
117
+ If True, use JoinPayload instead of MergePayload.
118
+
119
+ Returns
120
+ -------
121
+ dict with transition_id, output_node_id, branch, head_commit,
122
+ input_node_ids, merge_payload_type.
123
+ """
124
+ other_branch, other_node_id = _parse_merge_ref(other)
125
+
126
+ store = resolve_store(store_dir)
127
+ handle = store.load_run(run_id)
128
+
129
+ before = graph_counts(handle)
130
+
131
+ transition = handle.git.merge(
132
+ other_branch=other_branch,
133
+ other_node_id=other_node_id,
134
+ message=message,
135
+ branch=branch,
136
+ user_id=user_id,
137
+ work_session_id=work_session_id,
138
+ join=join,
139
+ dry_run=dry_run,
140
+ head_commit=head_commit,
141
+ )
142
+
143
+ maybe_append_or_save(
144
+ store=store,
145
+ handle=handle,
146
+ user_id=user_id,
147
+ work_session_id=work_session_id,
148
+ before=before,
149
+ )
150
+
151
+ git_payloads = handle.run_graph.payloads_for_transition(
152
+ transition.transition_id, payload_type="git_change"
153
+ )
154
+ head_commit = git_payloads[-1].head_commit if git_payloads else ""
155
+ branch_payloads = handle.run_graph.payloads_for_transition(
156
+ transition.transition_id, payload_type="branch"
157
+ )
158
+ resolved_branch = branch_payloads[-1].branch if branch_payloads else ""
159
+
160
+ payload_type = "join" if join else "merge"
161
+
162
+ return {
163
+ "transition_id": transition.transition_id,
164
+ "output_node_id": transition.output_node_id,
165
+ "input_node_ids": list(transition.input_node_ids),
166
+ "branch": resolved_branch,
167
+ "head_commit": head_commit,
168
+ "merge_payload_type": payload_type,
169
+ }
170
+
171
+
172
+ def cli_merge(args) -> int:
173
+ """Entry point for ``arctx merge`` subcommand."""
174
+ from arctx.ext.git.verbs._forward_transition import ParallelSessionConflict # noqa: PLC0415
175
+
176
+ run_id = resolve_run_id_from_args(args)
177
+ user_id = resolve_user_id_from_args(args)
178
+ work_session_id = resolve_work_session_id_from_args(args)
179
+
180
+ try:
181
+ result = run_merge_command(
182
+ other=args.other,
183
+ message=args.message,
184
+ branch=args.branch,
185
+ run_id=run_id,
186
+ store_dir=args.store_dir,
187
+ user_id=user_id,
188
+ work_session_id=work_session_id,
189
+ join=args.join,
190
+ )
191
+ except ParallelSessionConflict as exc:
192
+ print(f"error: {exc}", file=sys.stderr)
193
+ print(
194
+ "hint: another session has advanced this branch. "
195
+ "Rebase / pull before committing.",
196
+ file=sys.stderr,
197
+ )
198
+ return 2
199
+ except Exception as exc: # noqa: BLE001
200
+ print(f"error: {exc}", file=sys.stderr)
201
+ return 1
202
+
203
+ print(json.dumps(result, indent=2))
204
+ return 0
@@ -0,0 +1,138 @@
1
+ """arctx CLI reset command.
2
+
3
+ Resets the session pointer to a past node WITHOUT creating a new Transition.
4
+ Analogous to ``git reset``: moves HEAD back but does not produce a new commit.
5
+
6
+ For mode="hard", discarded transitions receive a CutPayload. For "mixed" and
7
+ "soft" the transitions are left active (working tree / index changes remain).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ import sys
15
+
16
+ from arctx_cli.append_batch import graph_counts, maybe_append_or_save
17
+ from arctx_cli.context import (
18
+ resolve_run_id_from_args,
19
+ resolve_store,
20
+ resolve_user_id_from_args,
21
+ resolve_work_session_id_from_args,
22
+ )
23
+
24
+
25
+ def add_parser(subparsers) -> argparse.ArgumentParser:
26
+ """Register the ``reset`` subcommand parser."""
27
+ p = subparsers.add_parser(
28
+ "reset",
29
+ help="Reset to a past node (no new transition)",
30
+ )
31
+ g = p.add_mutually_exclusive_group(required=True)
32
+ g.add_argument("--node", default=None, help="Target node id")
33
+ g.add_argument(
34
+ "--sha", default=None, help="Target commit sha (lookup via transition_by_sha)"
35
+ )
36
+ p.add_argument(
37
+ "--mode",
38
+ choices=["hard", "mixed", "soft"],
39
+ default="hard",
40
+ help="Reset mode (default: hard)",
41
+ )
42
+ p.add_argument("--branch", default=None, help="Override branch name")
43
+ p.add_argument("--run", default=None, help="Explicit run id")
44
+ p.add_argument("--store-dir", default=None, help="Store directory")
45
+ p.add_argument("--user", default=None, help="User id for attribution")
46
+ p.add_argument("--work-session", default=None, help="Work session id")
47
+ return p
48
+
49
+
50
+ def run_reset_command(
51
+ *,
52
+ to_node_id: str | None,
53
+ to_sha: str | None,
54
+ mode: str,
55
+ branch: str | None,
56
+ run_id: str | None,
57
+ store_dir: str | None,
58
+ user_id: str | None,
59
+ work_session_id: str | None,
60
+ dry_run: bool = False,
61
+ ) -> dict:
62
+ """Execute a reset and persist the resulting graph records.
63
+
64
+ Parameters
65
+ ----------
66
+ to_node_id:
67
+ Target node id. Mutually exclusive with to_sha.
68
+ to_sha:
69
+ Target commit sha (looked up via transition_by_sha).
70
+ mode:
71
+ "hard" | "mixed" | "soft".
72
+ branch:
73
+ Branch name override for the SessionPointerEvent.
74
+ run_id:
75
+ Explicit run id.
76
+ store_dir:
77
+ Store directory.
78
+ user_id:
79
+ User id for work event attribution.
80
+ work_session_id:
81
+ Work session id.
82
+ dry_run:
83
+ If True, skip actual git operations.
84
+
85
+ Returns
86
+ -------
87
+ dict with to_node_id, from_node_id, discarded_transition_ids, mode,
88
+ event_id.
89
+ """
90
+ store = resolve_store(store_dir)
91
+ handle = store.load_run(run_id)
92
+
93
+ before = graph_counts(handle)
94
+
95
+ result = handle.git.reset(
96
+ to_node_id=to_node_id,
97
+ to_sha=to_sha,
98
+ mode=mode,
99
+ branch=branch,
100
+ user_id=user_id,
101
+ work_session_id=work_session_id,
102
+ dry_run=dry_run,
103
+ )
104
+
105
+ maybe_append_or_save(
106
+ store=store,
107
+ handle=handle,
108
+ user_id=user_id,
109
+ work_session_id=work_session_id,
110
+ before=before,
111
+ )
112
+
113
+ return result
114
+
115
+
116
+ def cli_reset(args) -> int:
117
+ """Entry point for ``arctx reset`` subcommand."""
118
+ run_id = resolve_run_id_from_args(args)
119
+ user_id = resolve_user_id_from_args(args)
120
+ work_session_id = resolve_work_session_id_from_args(args)
121
+
122
+ try:
123
+ result = run_reset_command(
124
+ to_node_id=args.node,
125
+ to_sha=args.sha,
126
+ mode=args.mode,
127
+ branch=args.branch,
128
+ run_id=run_id,
129
+ store_dir=args.store_dir,
130
+ user_id=user_id,
131
+ work_session_id=work_session_id,
132
+ )
133
+ except Exception as exc: # noqa: BLE001
134
+ print(f"error: {exc}", file=sys.stderr)
135
+ return 1
136
+
137
+ print(json.dumps(result, indent=2))
138
+ return 0
@@ -0,0 +1,157 @@
1
+ """arctx CLI revert command.
2
+
3
+ Drives a ``git revert`` and records the corresponding arctx Transition with
4
+ BranchPayload, GitChangePayload, RevertPayload, BranchTipEvent, and
5
+ SessionPointerEvent.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ import sys
13
+
14
+ from arctx_cli.append_batch import graph_counts, maybe_append_or_save
15
+ from arctx_cli.context import (
16
+ resolve_run_id_from_args,
17
+ resolve_store,
18
+ resolve_user_id_from_args,
19
+ resolve_work_session_id_from_args,
20
+ )
21
+
22
+
23
+ def add_parser(subparsers) -> argparse.ArgumentParser:
24
+ """Register the ``revert`` subcommand parser."""
25
+ p = subparsers.add_parser(
26
+ "revert",
27
+ help="Revert a commit (or transition) and record a arctx transition",
28
+ )
29
+ g = p.add_mutually_exclusive_group(required=True)
30
+ g.add_argument("--sha", default=None, help="Commit sha to revert")
31
+ g.add_argument("--transition", default=None, help="Transition id (lookup latest sha)")
32
+ p.add_argument("-m", "--message", default=None, help="Override commit message")
33
+ p.add_argument("--branch", default=None, help="Override branch name")
34
+ p.add_argument("--run", default=None, help="Explicit run id")
35
+ p.add_argument("--store-dir", default=None, help="Store directory")
36
+ p.add_argument("--user", default=None, help="User id for attribution")
37
+ p.add_argument("--work-session", default=None, help="Work session id")
38
+ return p
39
+
40
+
41
+ def run_revert_command(
42
+ *,
43
+ target_sha: str | None,
44
+ target_transition: str | None,
45
+ message: str | None,
46
+ branch: str | None,
47
+ run_id: str | None,
48
+ store_dir: str | None,
49
+ user_id: str | None,
50
+ work_session_id: str | None,
51
+ ) -> dict:
52
+ """Execute a revert and persist the resulting graph records.
53
+
54
+ Parameters
55
+ ----------
56
+ target_sha:
57
+ Commit SHA to revert. Mutually exclusive with target_transition.
58
+ target_transition:
59
+ Transition ID whose latest sha to revert.
60
+ message:
61
+ Override commit message.
62
+ branch:
63
+ Branch name override.
64
+ run_id:
65
+ Explicit run id.
66
+ store_dir:
67
+ Store directory.
68
+ user_id:
69
+ User id for work event attribution.
70
+ work_session_id:
71
+ Work session id.
72
+
73
+ Returns
74
+ -------
75
+ dict with transition_id, output_node_id, branch, head_commit,
76
+ reverted_transition, reverted_commit.
77
+ """
78
+ store = resolve_store(store_dir)
79
+ handle = store.load_run(run_id)
80
+
81
+ before = graph_counts(handle)
82
+
83
+ transition = handle.git.revert(
84
+ target_sha=target_sha,
85
+ target_transition=target_transition,
86
+ message=message,
87
+ branch=branch,
88
+ user_id=user_id,
89
+ work_session_id=work_session_id,
90
+ )
91
+
92
+ maybe_append_or_save(
93
+ store=store,
94
+ handle=handle,
95
+ user_id=user_id,
96
+ work_session_id=work_session_id,
97
+ before=before,
98
+ )
99
+
100
+ # Extract payload info for the result.
101
+ git_payloads = handle.run_graph.payloads_for_transition(
102
+ transition.transition_id, payload_type="git_change"
103
+ )
104
+ head_commit = git_payloads[-1].head_commit if git_payloads else ""
105
+ branch_payloads = handle.run_graph.payloads_for_transition(
106
+ transition.transition_id, payload_type="branch"
107
+ )
108
+ resolved_branch = branch_payloads[-1].branch if branch_payloads else ""
109
+ revert_payloads = handle.run_graph.payloads_for_transition(
110
+ transition.transition_id, payload_type="revert"
111
+ )
112
+ reverted_transition = revert_payloads[-1].reverted_transition if revert_payloads else ""
113
+ reverted_commit = revert_payloads[-1].reverted_commit if revert_payloads else ""
114
+
115
+ return {
116
+ "transition_id": transition.transition_id,
117
+ "output_node_id": transition.output_node_id,
118
+ "branch": resolved_branch,
119
+ "head_commit": head_commit,
120
+ "reverted_transition": reverted_transition,
121
+ "reverted_commit": reverted_commit,
122
+ }
123
+
124
+
125
+ def cli_revert(args) -> int:
126
+ """Entry point for ``arctx revert`` subcommand."""
127
+ from arctx.ext.git.verbs._forward_transition import ParallelSessionConflict # noqa: PLC0415
128
+
129
+ run_id = resolve_run_id_from_args(args)
130
+ user_id = resolve_user_id_from_args(args)
131
+ work_session_id = resolve_work_session_id_from_args(args)
132
+
133
+ try:
134
+ result = run_revert_command(
135
+ target_sha=args.sha,
136
+ target_transition=args.transition,
137
+ message=args.message,
138
+ branch=args.branch,
139
+ run_id=run_id,
140
+ store_dir=args.store_dir,
141
+ user_id=user_id,
142
+ work_session_id=work_session_id,
143
+ )
144
+ except ParallelSessionConflict as exc:
145
+ print(f"error: {exc}", file=sys.stderr)
146
+ print(
147
+ "hint: another session has advanced this branch. "
148
+ "Rebase / pull before committing.",
149
+ file=sys.stderr,
150
+ )
151
+ return 2
152
+ except Exception as exc: # noqa: BLE001
153
+ print(f"error: {exc}", file=sys.stderr)
154
+ return 1
155
+
156
+ print(json.dumps(result, indent=2))
157
+ return 0
@@ -0,0 +1,140 @@
1
+ """arctx CLI verify command.
2
+
3
+ Validates the Descendant constraint (REDESIGN §10.9 invariant 7) over all
4
+ non-cut transitions in the current run.
5
+
6
+ Exit codes:
7
+ 0 — no violations
8
+ 1 — one or more violations (or command error)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ from arctx_cli.context import resolve_run_id_from_args, resolve_store
19
+
20
+
21
+ def add_parser(subparsers) -> argparse.ArgumentParser:
22
+ """Register the ``verify`` subcommand parser."""
23
+ p = subparsers.add_parser(
24
+ "verify",
25
+ help="Verify the descendant constraint over all transitions",
26
+ )
27
+ p.add_argument(
28
+ "--repo",
29
+ default=None,
30
+ help="Repo path (default: cwd)",
31
+ )
32
+ p.add_argument("--run", default=None, help="Explicit run id")
33
+ p.add_argument("--store-dir", default=None, help="Store directory")
34
+ p.add_argument(
35
+ "--json",
36
+ action="store_true",
37
+ dest="json_output",
38
+ help="Output results as JSON",
39
+ )
40
+ return p
41
+
42
+
43
+ def run_verify_command(
44
+ *,
45
+ run_id: str | None,
46
+ store_dir: str | None,
47
+ repo_path: Path | None = None,
48
+ skip_dead_sha_check: bool = False,
49
+ ) -> dict:
50
+ """Execute verify and return a result dict.
51
+
52
+ Parameters
53
+ ----------
54
+ run_id:
55
+ Explicit run id (or None to auto-resolve).
56
+ store_dir:
57
+ Store directory (or None to use ARCTX_HOME default).
58
+ repo_path:
59
+ Path to git repo root. Defaults to cwd.
60
+ skip_dead_sha_check:
61
+ If True, skip ``git cat-file -e`` pre-checks. For testing.
62
+
63
+ Returns
64
+ -------
65
+ dict with keys:
66
+ violations: list of violation dicts
67
+ summary: {"checked": int, "violations": int, "by_kind": {kind: int}}
68
+ """
69
+ store = resolve_store(store_dir)
70
+ handle = store.load_run(run_id)
71
+
72
+ violations = handle.git.verify(
73
+ repo_path=repo_path,
74
+ skip_dead_sha_check=skip_dead_sha_check,
75
+ )
76
+
77
+ # Count non-cut transitions that were checked.
78
+ from arctx.core.cuts import inactive_transition_ids # noqa: PLC0415
79
+ graph = handle.run_graph
80
+ inactive = inactive_transition_ids(graph)
81
+ checked = sum(1 for t_id in graph.transitions if t_id not in inactive)
82
+
83
+ by_kind: dict[str, int] = {}
84
+ for v in violations:
85
+ by_kind[v.kind] = by_kind.get(v.kind, 0) + 1
86
+
87
+ violation_dicts = [
88
+ {
89
+ "transition_id": v.transition_id,
90
+ "kind": v.kind,
91
+ "message": v.message,
92
+ "details": v.details,
93
+ }
94
+ for v in violations
95
+ ]
96
+
97
+ return {
98
+ "violations": violation_dicts,
99
+ "summary": {
100
+ "checked": checked,
101
+ "violations": len(violations),
102
+ "by_kind": by_kind,
103
+ },
104
+ }
105
+
106
+
107
+ def cli_verify(args) -> int:
108
+ """Entry point for ``arctx verify`` subcommand."""
109
+ run_id = resolve_run_id_from_args(args)
110
+ repo_path = Path(args.repo) if args.repo else None
111
+
112
+ try:
113
+ result = run_verify_command(
114
+ run_id=run_id,
115
+ store_dir=args.store_dir,
116
+ repo_path=repo_path,
117
+ )
118
+ except Exception as exc: # noqa: BLE001
119
+ print(f"error: {exc}", file=sys.stderr)
120
+ return 1
121
+
122
+ if args.json_output:
123
+ print(json.dumps(result, indent=2))
124
+ else:
125
+ summary = result["summary"]
126
+ violations = result["violations"]
127
+ if not violations:
128
+ print(
129
+ f"ok: {summary['checked']} transition(s) checked, "
130
+ "no violations found"
131
+ )
132
+ else:
133
+ print(
134
+ f"FAIL: {summary['violations']} violation(s) in "
135
+ f"{summary['checked']} transition(s) checked"
136
+ )
137
+ for v in violations:
138
+ print(f" [{v['kind']}] {v['transition_id']}: {v['message']}")
139
+
140
+ return 0 if not result["violations"] else 1