h2h-dev-tools 0.2.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.
@@ -0,0 +1,209 @@
1
+ """Checksum-pinned TLC acquisition and offline host/container execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import hashlib
7
+ import os
8
+ import re
9
+ import shutil
10
+ import subprocess
11
+ import tempfile
12
+ import tomllib
13
+ import urllib.request
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from typing import Literal
17
+
18
+ TlaRuntime = Literal["auto", "host", "docker"]
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class TlcLock:
23
+ filename: str
24
+ url: str
25
+ sha256: str
26
+ runtime_image: str
27
+
28
+
29
+ def load_tlc_lock(path: Path, *, expected_version: str) -> TlcLock:
30
+ with path.open("rb") as stream:
31
+ document = tomllib.load(stream)
32
+ value = document.get("tla_plus", {}).get("tlc")
33
+ if not isinstance(value, dict):
34
+ raise RuntimeError(f"Missing [tla_plus.tlc] in {path}")
35
+ if value.get("version") != expected_version:
36
+ raise RuntimeError(f"Unsupported tla_plus.tlc.version in {path}")
37
+ for field in ("filename", "url", "sha256", "runtime_image"):
38
+ if not isinstance(value.get(field), str) or not value[field]:
39
+ raise RuntimeError(f"Invalid tla_plus.tlc.{field} in {path}")
40
+ filename, url, digest, image = (
41
+ str(value[field]) for field in ("filename", "url", "sha256", "runtime_image")
42
+ )
43
+ if Path(filename).name != filename or filename in {".", ".."}:
44
+ raise RuntimeError(f"tla_plus.tlc.filename in {path} must be a basename")
45
+ if not url.startswith("https://"):
46
+ raise RuntimeError(f"tla_plus.tlc.url in {path} must use HTTPS")
47
+ if re.fullmatch(r"[0-9a-fA-F]{64}", digest) is None:
48
+ raise RuntimeError(f"tla_plus.tlc.sha256 in {path} is invalid")
49
+ if re.fullmatch(r"[^@]+@sha256:[0-9a-f]{64}", image) is None:
50
+ raise RuntimeError(
51
+ f"tla_plus.tlc.runtime_image in {path} must be digest-pinned"
52
+ )
53
+ return TlcLock(filename, url, digest.lower(), image)
54
+
55
+
56
+ def sha256(path: Path) -> str:
57
+ digest = hashlib.sha256()
58
+ with path.open("rb") as stream:
59
+ for block in iter(lambda: stream.read(1024 * 1024), b""):
60
+ digest.update(block)
61
+ return digest.hexdigest()
62
+
63
+
64
+ def verify_jar(path: Path, expected: str) -> None:
65
+ actual = sha256(path)
66
+ if actual != expected:
67
+ raise RuntimeError(
68
+ f"TLC checksum mismatch: expected={expected} actual={actual}"
69
+ )
70
+
71
+
72
+ def fetch_tlc(lock: TlcLock, output_directory: Path) -> Path:
73
+ """Explicit online acquisition; verification never calls this function."""
74
+ output_directory.mkdir(parents=True, exist_ok=True)
75
+ destination = output_directory / lock.filename
76
+ if destination.is_file() and sha256(destination) == lock.sha256:
77
+ return destination
78
+ with tempfile.NamedTemporaryFile(
79
+ dir=output_directory, prefix=".tlc-", delete=False
80
+ ) as temporary:
81
+ temporary_path = Path(temporary.name)
82
+ try:
83
+ with urllib.request.urlopen(lock.url, timeout=60) as response:
84
+ shutil.copyfileobj(response, temporary)
85
+ except BaseException:
86
+ temporary_path.unlink(missing_ok=True)
87
+ raise
88
+ try:
89
+ verify_jar(temporary_path, lock.sha256)
90
+ os.replace(temporary_path, destination)
91
+ finally:
92
+ temporary_path.unlink(missing_ok=True)
93
+ return destination
94
+
95
+
96
+ def fetch_main(repository_root: Path, *, expected_version: str) -> None:
97
+ parser = argparse.ArgumentParser(description=__doc__)
98
+ parser.add_argument(
99
+ "--output-directory", type=Path, default=repository_root / ".formal-tools"
100
+ )
101
+ arguments = parser.parse_args()
102
+ lock = load_tlc_lock(
103
+ repository_root / "verification/tools.lock.toml",
104
+ expected_version=expected_version,
105
+ )
106
+ print(fetch_tlc(lock, arguments.output_directory))
107
+
108
+
109
+ def host_java_available() -> bool:
110
+ java = shutil.which("java")
111
+ if java is None:
112
+ return False
113
+ result = subprocess.run(
114
+ [java, "-version"],
115
+ check=False,
116
+ stdout=subprocess.DEVNULL,
117
+ stderr=subprocess.DEVNULL,
118
+ )
119
+ return result.returncode == 0
120
+
121
+
122
+ def _run(command: list[str], *, cwd: Path) -> None:
123
+ print("+", " ".join(command), flush=True)
124
+ subprocess.run(command, cwd=cwd, check=True)
125
+
126
+
127
+ def verify_tla(
128
+ repository_root: Path,
129
+ tla_jar: Path,
130
+ *,
131
+ profile: str,
132
+ runtime: TlaRuntime,
133
+ expected_version: str,
134
+ ) -> None:
135
+ """Execute the explicitly selected finite model profile, without downloading."""
136
+ repository_root = repository_root.resolve()
137
+ if not tla_jar.is_file():
138
+ raise FileNotFoundError(f"TLC JAR does not exist: {tla_jar}")
139
+ lock = load_tlc_lock(
140
+ repository_root / "verification/tools.lock.toml",
141
+ expected_version=expected_version,
142
+ )
143
+ verify_jar(tla_jar, lock.sha256)
144
+ configs = sorted((repository_root / "verification/tla").glob(f"*{profile}.cfg"))
145
+ if not configs:
146
+ raise RuntimeError("No TLA+ model configurations were found")
147
+ use_host_java = runtime == "host" or (runtime == "auto" and host_java_available())
148
+ if runtime == "host" and not host_java_available():
149
+ raise RuntimeError("The requested host Java runtime is not available")
150
+ if not use_host_java and shutil.which("docker") is None:
151
+ raise RuntimeError("Neither a working Java runtime nor Docker is available")
152
+ for config in configs:
153
+ module_stem = config.name.removesuffix(f"{profile}.cfg")
154
+ if not module_stem:
155
+ raise RuntimeError(f"Cannot infer a TLA+ module from {config.name}")
156
+ module = config.with_name(f"{module_stem}.tla")
157
+ if not module.is_file():
158
+ raise RuntimeError(f"Missing TLA+ module for {config.name}")
159
+ with tempfile.TemporaryDirectory(prefix="h2h-tlc-") as state_directory:
160
+ if use_host_java:
161
+ command = [
162
+ "java",
163
+ "-XX:+UseParallelGC",
164
+ "-Xmx4g",
165
+ "-cp",
166
+ str(tla_jar.resolve()),
167
+ "tlc2.TLC",
168
+ "-workers",
169
+ "auto",
170
+ "-metadir",
171
+ str(Path(state_directory) / config.stem),
172
+ "-config",
173
+ config.name,
174
+ module.name,
175
+ ]
176
+ _run(command, cwd=config.parent)
177
+ else:
178
+ command = [
179
+ "docker",
180
+ "run",
181
+ "--rm",
182
+ "--pull=never",
183
+ "--network",
184
+ "none",
185
+ "--read-only",
186
+ "--tmpfs",
187
+ "/tmp:rw,nosuid,nodev,size=512m",
188
+ "--mount",
189
+ f"type=bind,src={repository_root},dst=/workspace,readonly",
190
+ "--mount",
191
+ f"type=bind,src={tla_jar.resolve()},dst=/tools/tla2tools.jar,readonly",
192
+ "--workdir",
193
+ "/workspace/verification/tla",
194
+ lock.runtime_image,
195
+ "java",
196
+ "-XX:+UseParallelGC",
197
+ "-Xmx4g",
198
+ "-cp",
199
+ "/tools/tla2tools.jar",
200
+ "tlc2.TLC",
201
+ "-workers",
202
+ "auto",
203
+ "-metadir",
204
+ f"/tmp/tlc/{config.stem}",
205
+ "-config",
206
+ config.name,
207
+ module.name,
208
+ ]
209
+ _run(command, cwd=repository_root)
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ fail() {
6
+ printf 'git-flow-merge: %s\n' "$1" >&2
7
+ exit 1
8
+ }
9
+
10
+ usage() {
11
+ printf 'usage: scripts/git-flow-merge.sh [--cleanup-only]\n' >&2
12
+ }
13
+
14
+ assert_clean() {
15
+ local status_output worktree=$1
16
+
17
+ status_output="$(
18
+ git -C "$worktree" status --porcelain=v1 --untracked-files=all
19
+ )" || fail "could not inspect worktree status: $worktree"
20
+ [[ -z "$status_output" ]] \
21
+ || fail "worktree is not clean: $worktree"
22
+ }
23
+
24
+ assert_no_operation() {
25
+ local git_path name worktree=$1
26
+
27
+ for name in \
28
+ MERGE_HEAD \
29
+ CHERRY_PICK_HEAD \
30
+ REVERT_HEAD \
31
+ REBASE_HEAD \
32
+ rebase-apply \
33
+ rebase-merge \
34
+ sequencer \
35
+ BISECT_START; do
36
+ git_path="$(
37
+ git -C "$worktree" \
38
+ rev-parse --path-format=absolute --git-path "$name"
39
+ )" || fail "could not inspect Git operation state: $worktree"
40
+ [[ ! -e "$git_path" ]] || fail "Git operation is active: $name"
41
+ done
42
+ }
43
+
44
+ assert_ref_equals() {
45
+ local actual expected=$3 ref=$2 worktree=$1
46
+
47
+ actual="$(
48
+ git -C "$worktree" rev-parse --verify "$ref^{commit}" 2>/dev/null
49
+ )" || fail "Git ref is no longer available: $ref"
50
+ [[ "$actual" == "$expected" ]] \
51
+ || fail "Git ref changed during cleanup: $ref"
52
+ }
53
+
54
+ is_ancestor() {
55
+ local ancestor=$2 descendant=$3 status worktree=$1
56
+
57
+ if git -C "$worktree" merge-base --is-ancestor "$ancestor" "$descendant"; then
58
+ return 0
59
+ else
60
+ status=$?
61
+ fi
62
+ [[ "$status" -eq 1 ]] \
63
+ || fail "could not verify commit ancestry: $ancestor $descendant"
64
+ return 1
65
+ }
66
+
67
+ abort_merge_if_needed() {
68
+ local worktree=$1 merge_head
69
+
70
+ merge_head="$(
71
+ git -C "$worktree" \
72
+ rev-parse --path-format=absolute --git-path MERGE_HEAD
73
+ )" || fail "could not inspect merge state: $worktree"
74
+ if [[ -e "$merge_head" ]]; then
75
+ git -C "$worktree" merge --abort || return 1
76
+ fi
77
+ }
78
+
79
+ restore_after_failure() {
80
+ local status=$?
81
+
82
+ trap - EXIT HUP INT TERM
83
+ if (( status != 0 )); then
84
+ if [[ -n "$reviewer_pid" ]]; then
85
+ # wait is interruptible; stop review before discarding its candidate.
86
+ kill -TERM "$reviewer_pid" 2>/dev/null || true
87
+ wait "$reviewer_pid" || true
88
+ fi
89
+ if abort_merge_if_needed "$merge_worktree"; then
90
+ # A signal or cleanup failure after the commit must not undo success.
91
+ if [[ "$merge_worktree" == "$repository_root" ]] \
92
+ && [[ "$(git -C "$merge_worktree" rev-parse HEAD)" == "$primary_oid" ]]; then
93
+ git -C "$repository_root" switch "$task_branch" || {
94
+ printf 'git-flow-merge: task branch retained; could not switch to %s\n' \
95
+ "$task_branch" >&2
96
+ }
97
+ fi
98
+ fi
99
+ fi
100
+ exit "$status"
101
+ }
102
+
103
+ repository_root=${1:?repository root is required}
104
+ strategy=${2:?merge strategy is required}
105
+ review_command=${3-}
106
+ shift 3
107
+ [[ "$strategy" == direct || "$strategy" == candidate ]] || fail 'invalid merge strategy'
108
+
109
+ mode='finish'
110
+ if [[ $# -gt 1 ]]; then
111
+ usage
112
+ fail 'too many arguments'
113
+ fi
114
+ if [[ $# -eq 1 ]]; then
115
+ [[ "$1" == '--cleanup-only' ]] || {
116
+ usage
117
+ fail "unsupported argument: $1"
118
+ }
119
+ mode='cleanup-only'
120
+ fi
121
+
122
+ cd "$repository_root"
123
+
124
+ primary="$(scripts/detect-primary-branch.sh)"
125
+ task_branch="$(git branch --show-current)"
126
+ [[ -n "$task_branch" ]] || fail 'detached HEAD is not supported'
127
+ [[ "$task_branch" != "$primary" ]] \
128
+ || fail 'run this script from the task branch'
129
+
130
+ assert_clean "$repository_root"
131
+ assert_no_operation "$repository_root"
132
+
133
+ primary_ref="refs/heads/$primary"
134
+ task_ref="refs/heads/$task_branch"
135
+ primary_oid="$(git rev-parse --verify "$primary_ref^{commit}")" \
136
+ || fail "primary branch is not a commit: $primary"
137
+ task_oid="$(git rev-parse --verify "$task_ref^{commit}")" \
138
+ || fail "task branch is not a commit: $task_branch"
139
+ git merge-base "$primary_oid" "$task_oid" >/dev/null \
140
+ || fail "task branch and primary have no common ancestor: $primary"
141
+
142
+ primary_worktree=''
143
+ candidate_worktree=''
144
+ candidate_branch=''
145
+ worktree_listing="$(git worktree list --porcelain)" \
146
+ || fail 'could not inspect repository worktrees'
147
+ while IFS= read -r line || [[ -n "$line" ]]; do
148
+ case "$line" in
149
+ 'worktree '*)
150
+ candidate_worktree="${line#worktree }"
151
+ candidate_branch=''
152
+ ;;
153
+ 'branch refs/heads/'*)
154
+ candidate_branch="${line#branch refs/heads/}"
155
+ if [[ "$candidate_branch" == "$primary" ]]; then
156
+ primary_worktree="$candidate_worktree"
157
+ fi
158
+ ;;
159
+ esac
160
+ done <<< "$worktree_listing"
161
+
162
+ if is_ancestor "$repository_root" "$task_oid" "$primary_oid"; then
163
+ if [[ -n "$primary_worktree" ]]; then
164
+ assert_clean "$primary_worktree"
165
+ assert_no_operation "$primary_worktree"
166
+ cleanup_worktree="$primary_worktree"
167
+ else
168
+ cleanup_worktree="$repository_root"
169
+ fi
170
+
171
+ assert_ref_equals "$repository_root" "$task_ref" "$task_oid"
172
+ assert_ref_equals "$repository_root" "$primary_ref" "$primary_oid"
173
+ is_ancestor "$repository_root" "$task_oid" "$primary_oid" \
174
+ || fail 'task is no longer contained in primary; branch was retained'
175
+
176
+ if [[ "$cleanup_worktree" == "$repository_root" ]]; then
177
+ git switch "$primary" \
178
+ || fail "could not switch to primary; task branch was retained: $primary"
179
+ else
180
+ cd "$cleanup_worktree"
181
+ git -C "$cleanup_worktree" worktree remove "$repository_root" \
182
+ || fail "no-op task worktree remains: $repository_root"
183
+ fi
184
+
185
+ assert_clean "$cleanup_worktree"
186
+ assert_no_operation "$cleanup_worktree"
187
+ assert_ref_equals "$cleanup_worktree" "$task_ref" "$task_oid"
188
+ assert_ref_equals "$cleanup_worktree" "$primary_ref" "$primary_oid"
189
+ is_ancestor "$cleanup_worktree" "$task_oid" "$primary_oid" \
190
+ || fail 'task is no longer contained in primary; branch was retained'
191
+ git -C "$cleanup_worktree" branch -d "$task_branch" \
192
+ || fail "no-op task branch remains: $task_branch"
193
+
194
+ printf 'Cleaned up no-op task %s already contained in %s at %s\n' \
195
+ "$task_branch" "$primary" "$task_oid"
196
+ exit 0
197
+ fi
198
+
199
+ [[ "$mode" != 'cleanup-only' ]] \
200
+ || fail 'task contains commits not present in primary; branch was retained'
201
+
202
+ hooks_path="$(git config --local --get core.hooksPath || true)"
203
+ [[ "$hooks_path" == '.githooks' ]] \
204
+ || fail 'repository hooks are not installed; run scripts/install-git-hooks.sh'
205
+ [[ -x .githooks/pre-merge-commit ]] \
206
+ || fail '.githooks/pre-merge-commit is missing or not executable'
207
+ if [[ "$strategy" == candidate ]]; then
208
+ [[ -x .githooks/pre-commit ]] || fail '.githooks/pre-commit is missing or not executable'
209
+ fi
210
+
211
+ if [[ -n "$primary_worktree" ]]; then
212
+ assert_clean "$primary_worktree"
213
+ assert_no_operation "$primary_worktree"
214
+ merge_worktree="$primary_worktree"
215
+ else
216
+ merge_worktree="$repository_root"
217
+ fi
218
+
219
+ reviewer_pid=''
220
+ trap restore_after_failure EXIT
221
+ trap 'exit 129' HUP
222
+ trap 'exit 130' INT
223
+ trap 'exit 143' TERM
224
+ if [[ -z "$primary_worktree" ]]; then
225
+ git switch "$primary"
226
+ fi
227
+ assert_ref_equals "$merge_worktree" "$primary_ref" "$primary_oid"
228
+ assert_ref_equals "$merge_worktree" "$task_ref" "$task_oid"
229
+ export WORKFLOW_MERGE_TASK_REF="$task_branch"
230
+ if [[ "$strategy" == direct ]]; then
231
+ # Git invokes pre-merge-commit for the exact candidate in these repositories.
232
+ git -C "$merge_worktree" merge --no-ff --no-edit "$task_branch" \
233
+ || fail 'merge or merge gate failed; task branch was retained'
234
+ else
235
+ # These repositories' pre-commit hooks delegate pending merges to their gate.
236
+ git -C "$merge_worktree" merge --no-ff --no-commit "$task_branch" \
237
+ || fail 'merge failed; task branch was retained'
238
+ if [[ -n "$review_command" ]]; then
239
+ (cd "$merge_worktree" && exec bash "$review_command") &
240
+ reviewer_pid=$!
241
+ if ! wait "$reviewer_pid"; then
242
+ reviewer_pid=''
243
+ fail 'code review failed; task branch was retained'
244
+ fi
245
+ reviewer_pid=''
246
+ fi
247
+ git -C "$merge_worktree" commit --no-edit \
248
+ || fail 'merge commit or release gate failed; task branch was retained'
249
+ fi
250
+
251
+ cd "$merge_worktree"
252
+ if [[ "$repository_root" != "$merge_worktree" ]]; then
253
+ assert_clean "$repository_root"
254
+ git -C "$merge_worktree" worktree remove "$repository_root" \
255
+ || fail "merge succeeded, but task worktree remains: $repository_root"
256
+ fi
257
+
258
+ git -C "$merge_worktree" branch -d "$task_branch" \
259
+ || fail "merge succeeded, but task branch remains: $task_branch"
260
+
261
+ merge_commit="$(git -C "$merge_worktree" rev-parse HEAD)"
262
+ printf 'Merged %s into %s at %s\n' \
263
+ "$task_branch" "$primary" "$merge_commit"
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root=${1:?repository root is required}
5
+ resource_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ cd "$repo_root"
7
+
8
+ existing_hooks_path="$(git config --get core.hooksPath || true)"
9
+ if [[ -n "$existing_hooks_path" && "$existing_hooks_path" != ".githooks" ]]; then
10
+ echo "Refusing to replace existing core.hooksPath: $existing_hooks_path" >&2
11
+ exit 1
12
+ fi
13
+
14
+ if [[ -z "$existing_hooks_path" ]]; then
15
+ default_hooks_directory="$(git rev-parse --git-path hooks)"
16
+ shopt -s nullglob
17
+ for existing_hook in "$default_hooks_directory"/*; do
18
+ if [[ -f "$existing_hook" \
19
+ && -x "$existing_hook" \
20
+ && "$existing_hook" != *.sample ]]; then
21
+ echo "Refusing to disable existing Git hook: $existing_hook" >&2
22
+ exit 1
23
+ fi
24
+ done
25
+ fi
26
+
27
+ git config --local core.hooksPath .githooks
28
+ primary="$(bash "$resource_dir/primary-branch.sh" "$repo_root")"
29
+ git config --local "branch.$primary.mergeOptions" --no-ff
30
+ git config --local "branch.$primary.rebase" false
31
+ git config --local pull.rebase false
32
+ git config --local pull.ff only
33
+
34
+ printf 'Installed repository Git hooks; primary branch: %s\n' "$primary"
@@ -0,0 +1,61 @@
1
+ """One-time conversion of verified toolkit 0.1 source bundles to components.
2
+
3
+ Normal verification and synchronization accept only the current schema. This
4
+ explicit converter verifies all legacy hashes before replacing generated files;
5
+ it never executes the old payload or adopts local edits.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from pathlib import Path
12
+
13
+ from .bundle import replace_bundle
14
+ from .bundle_verify import (
15
+ MANIFEST_NAME,
16
+ BundleError,
17
+ BundleManifest,
18
+ _reject_duplicate_keys,
19
+ bundle_directory,
20
+ regular_bytes,
21
+ validate_manifest,
22
+ verify_payload,
23
+ )
24
+
25
+
26
+ def _legacy_manifest(repository: Path) -> BundleManifest:
27
+ try:
28
+ document = json.loads(
29
+ regular_bytes(bundle_directory(repository) / MANIFEST_NAME),
30
+ object_pairs_hook=_reject_duplicate_keys,
31
+ )
32
+ except (UnicodeError, json.JSONDecodeError) as error:
33
+ raise BundleError(f"Invalid legacy manifest: {error}") from error
34
+ if (
35
+ not isinstance(document, dict)
36
+ or set(document)
37
+ != {
38
+ "schema_version",
39
+ "toolkit_name",
40
+ "toolkit_version",
41
+ "payload_sha256",
42
+ "files",
43
+ }
44
+ or type(document["schema_version"]) is not int
45
+ or document["schema_version"] != 1
46
+ ):
47
+ raise BundleError("migrate-v1 requires an unmodified schema-1 bundle")
48
+ document["schema_version"] = 2
49
+ document["components"] = [
50
+ "change-scope",
51
+ "dependencies",
52
+ "documentation",
53
+ "publish-version",
54
+ ]
55
+ return verify_payload(repository, validate_manifest(document))
56
+
57
+
58
+ def migrate_v1(repository: Path) -> bool:
59
+ """Convert once using explicit current pyproject component configuration."""
60
+ _legacy_manifest(repository)
61
+ return replace_bundle(repository, lambda: _legacy_manifest(repository))