diff --git a/.github/workflows/cargo-dist-npm-windows-extraction-candidate.yml b/.github/workflows/cargo-dist-npm-windows-extraction-candidate.yml new file mode 100644 index 0000000..73b273f --- /dev/null +++ b/.github/workflows/cargo-dist-npm-windows-extraction-candidate.yml @@ -0,0 +1,156 @@ +name: cargo-dist Windows npm extraction candidate + +on: + pull_request: + paths: + - ".github/workflows/cargo-dist-npm-windows-extraction-candidate.yml" + - "candidates/cargo-dist-npm-windows-extraction/**" + workflow_dispatch: + +permissions: + contents: read + +jobs: + restricted-policy-and-error-propagation: + runs-on: windows-2025 + timeout-minutes: 20 + + steps: + - name: Check out public lab inputs + uses: actions/checkout@v7 + with: + path: lab + persist-credentials: false + + - name: Check out exact public cargo-dist source + uses: actions/checkout@v7 + with: + repository: axodotdev/cargo-dist + ref: c65a1a932e2661e05d6640716850d36b0f47efd7 + path: upstream + persist-credentials: false + + - name: Verify upstream source identity + shell: pwsh + run: | + $sha = (git -C upstream rev-parse HEAD).Trim() + if ($sha -ne 'c65a1a932e2661e05d6640716850d36b0f47efd7') { throw "unexpected source $sha" } + Copy-Item upstream/cargo-dist/templates/installer/npm/binary-install.js upstream/baseline-binary-install.js + git -C upstream apply --check ../lab/candidates/cargo-dist-npm-windows-extraction/candidate.patch + + - name: Set up Node + uses: actions/setup-node@v7 + with: + node-version: '24' + + - name: Set up Python for local fixture server + uses: actions/setup-python@v7 + with: + python-version: '3.12' + architecture: x64 + + - name: Prepare ZIP fixtures and Package.install probe + shell: pwsh + run: | + New-Item -ItemType Directory -Force lab/fixture/valid | Out-Null + Set-Content -Encoding ascii lab/fixture/valid/fixture.exe 'fixture' + Compress-Archive -Path lab/fixture/valid/fixture.exe -DestinationPath lab/fixture/valid.zip -Force + Set-Content -Encoding ascii lab/fixture/invalid.zip 'not-a-zip' + @' + const path = require('path'); + + async function main() { + const modulePath = path.resolve(process.argv[2]); + const url = process.argv[3]; + const filename = process.argv[4]; + const { Package } = require(modulePath); + const pkg = new Package( + { artifactName: 'x86_64-pc-windows-msvc' }, + 'fixture', + url, + filename, + '.zip', + { fixture: 'fixture.exe' }, + ); + await pkg.install(true); + const existsAfter = pkg.exists(); + console.log(JSON.stringify({ install_resolved: true, exists_after: existsAfter })); + if (!existsAfter) process.exit(91); + } + + main().catch((err) => { + console.error(err); + process.exit(90); + }); + '@ | Set-Content -Encoding utf8 lab/package-install-probe.js + + - name: Exercise baseline and candidate under Restricted policy + shell: pwsh + run: | + $server = Start-Process python -ArgumentList '-m','http.server','8123','--bind','127.0.0.1','--directory','lab/fixture' -PassThru -WindowStyle Hidden + Start-Sleep -Seconds 2 + $prior = (powershell.exe -NoProfile -NonInteractive -Command 'Get-ExecutionPolicy -Scope CurrentUser').Trim() + try { + powershell.exe -NoProfile -NonInteractive -Command 'Set-ExecutionPolicy -Scope CurrentUser Restricted -Force' + + Remove-Item upstream/node_modules/.bin_real -Recurse -Force -ErrorAction SilentlyContinue + node lab/package-install-probe.js upstream/baseline-binary-install.js http://127.0.0.1:8123/valid.zip valid.zip *> baseline.log + $baselineExit = $LASTEXITCODE + + git -C upstream apply ../lab/candidates/cargo-dist-npm-windows-extraction/candidate.patch + Remove-Item upstream/cargo-dist/templates/installer/npm/node_modules/.bin_real -Recurse -Force -ErrorAction SilentlyContinue + node lab/package-install-probe.js upstream/cargo-dist/templates/installer/npm/binary-install.js http://127.0.0.1:8123/valid.zip valid.zip *> candidate-valid.log + $candidateExit = $LASTEXITCODE + + if ($candidateExit -ne 0) { Get-Content candidate-valid.log; throw "candidate failed under Restricted policy: exit $candidateExit" } + if ($baselineExit -eq 0) { Get-Content baseline.log; throw 'baseline unexpectedly installed successfully under Restricted policy' } + + @{ + upstream_commit = 'c65a1a932e2661e05d6640716850d36b0f47efd7' + baseline_exit = $baselineExit + baseline_silent_success_detected = ($baselineExit -eq 91) + candidate_exit = $candidateExit + candidate_install_verified_by_package_exists = $true + simulated_policy = 'CurrentUser Restricted' + } | ConvertTo-Json | Set-Content -Encoding utf8 restricted-policy-evidence.json + } + finally { + powershell.exe -NoProfile -NonInteractive -Command "Set-ExecutionPolicy -Scope CurrentUser $prior -Force" + Stop-Process -Id $server.Id -Force -ErrorAction SilentlyContinue + } + + - name: Candidate must propagate genuine corrupt-ZIP failure + shell: pwsh + run: | + $server = Start-Process python -ArgumentList '-m','http.server','8124','--bind','127.0.0.1','--directory','lab/fixture' -PassThru -WindowStyle Hidden + Start-Sleep -Seconds 2 + try { + Remove-Item upstream/cargo-dist/templates/installer/npm/node_modules/.bin_real -Recurse -Force -ErrorAction SilentlyContinue + node lab/package-install-probe.js upstream/cargo-dist/templates/installer/npm/binary-install.js http://127.0.0.1:8124/invalid.zip invalid.zip *> corrupt-zip.log + $exit = $LASTEXITCODE + if ($exit -eq 0 -or $exit -eq 91) { Get-Content corrupt-zip.log; throw "candidate did not propagate corrupt ZIP as extraction failure: exit $exit" } + @{ + corrupt_zip_exit = $exit + failure_propagated = $true + } | ConvertTo-Json | Set-Content -Encoding utf8 corrupt-zip-evidence.json + } + finally { + Stop-Process -Id $server.Id -Force -ErrorAction SilentlyContinue + } + # The non-zero Node status above is the expected oracle. After recording it, + # do not let that expected child status become the workflow step status. + $global:LASTEXITCODE = 0 + + - name: Retain public candidate evidence + if: always() + uses: actions/upload-artifact@v7 + with: + name: cargo-dist-npm-windows-extraction-candidate-evidence + path: | + restricted-policy-evidence.json + corrupt-zip-evidence.json + baseline.log + candidate-valid.log + corrupt-zip.log + if-no-files-found: warn + retention-days: 14 diff --git a/.github/workflows/oss-organ-qualification.yml b/.github/workflows/oss-organ-qualification.yml new file mode 100644 index 0000000..e13ca8b --- /dev/null +++ b/.github/workflows/oss-organ-qualification.yml @@ -0,0 +1,70 @@ +name: OSS organ six-family qualification + +on: + pull_request: + paths: + - ".github/workflows/oss-organ-qualification.yml" + - "tools/oss-organ-qualify.py" + - "tools/oss-organ-reps.py" + - "tools/oss-organ-followup.py" + - "lab-fixtures/**" + workflow_dispatch: + +permissions: + contents: read + +jobs: + qualify: + name: ${{ matrix.name }} + strategy: + fail-fast: false + matrix: + include: + - name: linux-x64 + runner: ubuntu-24.04 + - name: linux-arm64 + runner: ubuntu-24.04-arm + - name: windows-x64 + runner: windows-2025 + - name: windows-arm64 + runner: windows-11-arm + - name: macos-x64 + runner: macos-15-intel + - name: macos-arm64 + runner: macos-15 + + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + + steps: + - name: Check out public probe only + uses: actions/checkout@v5 + with: + persist-credentials: false + + - name: Record runner identity + run: python -c "import json,platform; print(json.dumps({'system':platform.system(),'machine':platform.machine(),'python':platform.python_version()}, indent=2))" + + - name: Qualify pinned public artifacts + env: + QUAL_GITHUB_TOKEN: ${{ github.token }} + run: python tools/oss-organ-qualify.py + + - name: Run bounded comparative reps + env: + QUAL_GITHUB_TOKEN: ${{ github.token }} + run: python tools/oss-organ-reps.py + + - name: Run corrective follow-up reps + env: + QUAL_GITHUB_TOKEN: ${{ github.token }} + run: python tools/oss-organ-followup.py + + - name: Retain structured qualification evidence + if: always() + uses: actions/upload-artifact@v4 + with: + name: oss-organ-${{ matrix.name }} + path: qualification-results/*.json + if-no-files-found: warn + retention-days: 14 diff --git a/.github/workflows/xa11y-windows-arm64-candidate.yml b/.github/workflows/xa11y-windows-arm64-candidate.yml new file mode 100644 index 0000000..f0834c3 --- /dev/null +++ b/.github/workflows/xa11y-windows-arm64-candidate.yml @@ -0,0 +1,153 @@ +name: xa11y Windows ARM64 Python candidate + +on: + pull_request: + paths: + - ".github/workflows/xa11y-windows-arm64-candidate.yml" + - "candidates/xa11y-windows-arm64-python-wheel/**" + workflow_dispatch: + +permissions: + contents: read + +jobs: + build-wheel: + name: cross-build Windows ARM64 wheel + runs-on: windows-latest + timeout-minutes: 45 + + steps: + - name: Check out exact public release-synchronized source + uses: actions/checkout@v5 + with: + repository: xa11y/xa11y + ref: 44594a9705a3f3213a9b58bc205f4e6335c9606b + persist-credentials: false + + - name: Verify source identity + shell: pwsh + run: | + $sha = (git rev-parse HEAD).Trim() + if ($sha -ne '44594a9705a3f3213a9b58bc205f4e6335c9606b') { throw "unexpected source $sha" } + Write-Host "source=$sha" + + - name: Set up host Python + uses: actions/setup-python@v7 + with: + python-version: "3.12" + architecture: x64 + + - name: Cross-build candidate wheel using upstream release machinery + uses: PyO3/maturin-action@v1 + with: + target: aarch64 + args: --release --out dist + working-directory: xa11y-python + + - name: Verify and describe produced wheel + shell: pwsh + run: | + $wheel = Get-ChildItem xa11y-python/dist/*.whl | Select-Object -First 1 + if (-not $wheel) { throw 'candidate wheel was not produced' } + if ($wheel.Name -notmatch 'win_arm64') { throw "candidate is not a Windows ARM64 wheel: $($wheel.Name)" } + $digest = (Get-FileHash -Algorithm SHA256 $wheel.FullName).Hash.ToLowerInvariant() + @{ + source_commit = '44594a9705a3f3213a9b58bc205f4e6335c9606b' + build_host = 'windows-latest-x64' + maturin_target = 'aarch64' + wheel = $wheel.Name + wheel_sha256 = $digest + } | ConvertTo-Json | Set-Content -Encoding utf8 build-evidence.json + Get-Content build-evidence.json + + - name: Publish exact candidate wheel for dogfood job + uses: actions/upload-artifact@v7 + with: + name: xa11y-crossbuilt-windows-arm64-wheel + path: | + xa11y-python/dist/*.whl + build-evidence.json + if-no-files-found: error + retention-days: 14 + + dogfood-wheel: + name: dogfood exact wheel on native Windows ARM64 + needs: build-wheel + runs-on: windows-11-arm + timeout-minutes: 20 + + steps: + - name: Set up native ARM64 Python + uses: actions/setup-python@v7 + with: + python-version: "3.12" + architecture: arm64 + + - name: Download exact cross-built candidate + uses: actions/download-artifact@v8 + with: + name: xa11y-crossbuilt-windows-arm64-wheel + path: candidate + + - name: Install and exercise exact downloaded wheel + shell: pwsh + run: | + $wheel = Get-ChildItem candidate -Recurse -Filter *.whl | Select-Object -First 1 + if (-not $wheel) { throw 'candidate wheel was not downloaded' } + if ($wheel.Name -notmatch 'win_arm64') { throw "candidate is not a Windows ARM64 wheel: $($wheel.Name)" } + $digest = (Get-FileHash -Algorithm SHA256 $wheel.FullName).Hash.ToLowerInvariant() + $build = Get-Content candidate/build-evidence.json | ConvertFrom-Json + if ($digest -ne $build.wheel_sha256) { throw "artifact digest changed in transfer" } + python -m pip install --disable-pip-version-check --no-index --no-deps --only-binary=:all: $wheel.FullName + @' + import importlib.metadata + import json + import platform + import xa11y + + evidence = { + "source_commit": "44594a9705a3f3213a9b58bc205f4e6335c9606b", + "package_version": importlib.metadata.version("xa11y"), + "system": platform.system(), + "machine": platform.machine(), + "python": platform.python_version(), + "imported": xa11y.__name__ == "xa11y", + "app_api_present": hasattr(xa11y, "App") and hasattr(xa11y.App, "by_name"), + } + try: + xa11y.App.by_name("__agent_dispatch_candidate_missing_app__", timeout=0) + evidence["missing_app"] = "unexpected-success" + except Exception as exc: + evidence["missing_app_exception"] = type(exc).__name__ + evidence["missing_app_message"] = str(exc)[:1000] + evidence["missing_app_is_xa11y_error"] = isinstance(exc, xa11y.XA11yError) + + print(json.dumps(evidence, indent=2, sort_keys=True)) + if evidence["package_version"] != "0.14.0": + raise SystemExit("wrong package version") + if evidence["machine"].upper() != "ARM64": + raise SystemExit("dogfood did not run on native ARM64") + if not evidence["imported"] or not evidence["app_api_present"]: + raise SystemExit("candidate API probe failed") + if evidence.get("missing_app") == "unexpected-success": + raise SystemExit("missing-app negative control unexpectedly succeeded") + if not evidence.get("missing_app_is_xa11y_error", False): + raise SystemExit("missing-app failure was not surfaced as xa11y error") + with open("candidate-evidence.json", "w", encoding="utf-8") as f: + json.dump(evidence, f, indent=2, sort_keys=True) + f.write("\n") + '@ | Set-Content -Encoding utf8 candidate_probe.py + python candidate_probe.py + xa11y --help + + - name: Retain qualified installable public candidate and evidence + if: always() + uses: actions/upload-artifact@v7 + with: + name: xa11y-0.14.0-windows-arm64-qualified-candidate + path: | + candidate/**/*.whl + candidate/build-evidence.json + candidate-evidence.json + if-no-files-found: warn + retention-days: 14 diff --git a/candidates/cargo-dist-npm-windows-extraction/README.md b/candidates/cargo-dist-npm-windows-extraction/README.md new file mode 100644 index 0000000..1b3def8 --- /dev/null +++ b/candidates/cargo-dist-npm-windows-extraction/README.md @@ -0,0 +1,30 @@ +# cargo-dist Windows npm extraction/error candidate + +Public candidate experiment against `axodotdev/cargo-dist` issue #2437 and public `main` commit `c65a1a932e2661e05d6640716850d36b0f47efd7`. + +## Publicly reconstructable problem + +The generated npm binary installer invokes Windows PowerShell `Expand-Archive` without an execution-policy override and treats process exit status `0` as extraction success. Under a Restricted PowerShell policy the Archive module can fail to load, and failures inside the command block are not guaranteed to produce a trustworthy non-zero process result. The installer may therefore claim success while no binary was extracted. + +Upstream issue: `axodotdev/cargo-dist#2437`. + +## Candidate hypothesis + +Keep the existing Windows PowerShell extraction path, but: + +1. invoke it with `-ExecutionPolicy Bypass` so a Restricted local policy does not prevent the built-in archive operation; +2. make `Expand-Archive` failure terminating with `-ErrorAction Stop`; +3. catch the failure and explicitly `exit 1`, preserving the existing Node-side non-zero rejection path. + +The adjacent patch is intentionally limited to `cargo-dist/templates/installer/npm/binary-install.js`. + +## Qualification + +The public lab should establish separately that: + +- the unmodified upstream shape fails or misreports under a simulated Restricted policy; +- the candidate extracts a valid Windows zip under the same policy; +- a genuinely invalid zip returns non-zero under the candidate rather than being reported as successful; +- no private fixtures, credentials, or rationale are required. + +This is candidate evidence only. It is not an upstream PR and does not imply maintainer acceptance. diff --git a/candidates/cargo-dist-npm-windows-extraction/candidate.patch b/candidates/cargo-dist-npm-windows-extraction/candidate.patch new file mode 100644 index 0000000..d1992c8 --- /dev/null +++ b/candidates/cargo-dist-npm-windows-extraction/candidate.patch @@ -0,0 +1,22 @@ +diff --git a/cargo-dist/templates/installer/npm/binary-install.js b/cargo-dist/templates/installer/npm/binary-install.js +--- a/cargo-dist/templates/installer/npm/binary-install.js ++++ b/cargo-dist/templates/installer/npm/binary-install.js +@@ -270,10 +270,17 @@ + result = spawnSync("powershell.exe", [ + "-NoProfile", + "-NonInteractive", ++ "-ExecutionPolicy", ++ "Bypass", + "-Command", + `& { + param([string]$LiteralPath, [string]$DestinationPath) +- Expand-Archive -LiteralPath $LiteralPath -DestinationPath $DestinationPath -Force ++ try { ++ Expand-Archive -LiteralPath $LiteralPath -DestinationPath $DestinationPath -Force -ErrorAction Stop ++ } catch { ++ Write-Error $_ ++ exit 1 ++ } + }`, + tempFile, + this.installDirectory, diff --git a/candidates/xa11y-windows-arm64-python-wheel/README.md b/candidates/xa11y-windows-arm64-python-wheel/README.md new file mode 100644 index 0000000..f8c82c9 --- /dev/null +++ b/candidates/xa11y-windows-arm64-python-wheel/README.md @@ -0,0 +1,33 @@ +# xa11y Windows ARM64 Python wheel candidate + +Public candidate experiment for upstream `xa11y/xa11y` release 0.14.0. + +## Publicly reconstructable problem + +`xa11y` publishes Python wheels for Linux x86_64/aarch64 and macOS x86_64/aarch64, but its Windows Python-wheel job targets x86_64 only. The same public release workflow already builds the JavaScript native binding for `aarch64-pc-windows-msvc`. A native Windows ARM64 GitHub-hosted runner cannot obtain `xa11y==0.14.0` with binary-only pip installation. + +## Candidate + +Extend the Windows Python-wheel build to include a native Windows ARM64 runner and `aarch64` maturin target while preserving the existing x86_64 build. + +The adjacent `publish.patch` is the intended minimal upstream-shaped change. + +## Public qualification + +The successful candidate run used exact public release-synchronized upstream commit `44594a9705a3f3213a9b58bc205f4e6335c9606b` on native `windows-11-arm` with ARM64 CPython 3.12.10 and the same `PyO3/maturin-action@v1` build mechanism used upstream. + +It produced: + +`xa11y-0.14.0-cp39-abi3-win_arm64.whl` + +The job then installed exactly that locally generated wheel with pip using `--no-index --only-binary=:all:`, imported `xa11y`, confirmed `App.by_name`, and verified a missing-application negative control surfaced as `SelectorNotMatchedError`, an `XA11yError`. + +The retained Actions artifact is `xa11y-0.14.0-windows-arm64-candidate` from candidate workflow run 34433964860. + +An earlier control against annotated tag target `7e623f3d4d24264dd9a56399090a8099020b46ea` successfully built a native ARM64 wheel but exposed an important release-process detail: that tagged commit still carried Python binding version 0.13.0. Upstream's subsequent release-synchronization commits advance the Python package to 0.14.0. The successful qualification therefore pins the exact release-synchronized public source state rather than silently overriding package metadata. + +## Claim boundary + +This proves the missing Python wheel can be built, installed, imported, and exercised on native Windows ARM64. It does not claim interactive desktop accessibility behavior; that remains a separate physical dogfood tier. + +No private source, fixtures, endpoints, rationale, or credentials are used. \ No newline at end of file diff --git a/candidates/xa11y-windows-arm64-python-wheel/publish.patch b/candidates/xa11y-windows-arm64-python-wheel/publish.patch new file mode 100644 index 0000000..cdc65a2 --- /dev/null +++ b/candidates/xa11y-windows-arm64-python-wheel/publish.patch @@ -0,0 +1,32 @@ +diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml +--- a/.github/workflows/publish.yml ++++ b/.github/workflows/publish.yml +@@ + build-wheels-windows: +- name: Build wheels (Windows) ++ name: Build wheels (Windows ${{ matrix.target }}) + needs: [preflight, version-bump] + if: always() && needs.preflight.result == 'success' + && (needs.version-bump.result == 'success' || needs.version-bump.result == 'skipped') + runs-on: windows-latest ++ strategy: ++ fail-fast: false ++ matrix: ++ target: [x86_64, aarch64] + steps: + - uses: actions/checkout@v7 + with: + ref: main +@@ + - uses: PyO3/maturin-action@v1 + with: +- target: x86_64 ++ target: ${{ matrix.target }} + args: --release --out dist + working-directory: xa11y-python + + - uses: actions/upload-artifact@v7 + with: +- name: wheels-windows-x86_64 ++ name: wheels-windows-${{ matrix.target }} + path: xa11y-python/dist diff --git a/lab-fixtures/candidate-v1/__main__.py b/lab-fixtures/candidate-v1/__main__.py new file mode 100644 index 0000000..674aaa9 --- /dev/null +++ b/lab-fixtures/candidate-v1/__main__.py @@ -0,0 +1,9 @@ +import argparse + +p = argparse.ArgumentParser() +p.add_argument("--version", action="store_true") +args = p.parse_args() +if args.version: + print("candidate-probe 1.0") +else: + print("candidate-probe:v1:ok") diff --git a/lab-fixtures/candidate-v2/__main__.py b/lab-fixtures/candidate-v2/__main__.py new file mode 100644 index 0000000..76a30a0 --- /dev/null +++ b/lab-fixtures/candidate-v2/__main__.py @@ -0,0 +1,9 @@ +import argparse + +p = argparse.ArgumentParser() +p.add_argument("--version", action="store_true") +args = p.parse_args() +if args.version: + print("candidate-probe 2.0") +else: + print("candidate-probe:v2:ok") diff --git a/tools/oss-organ-followup.py b/tools/oss-organ-followup.py new file mode 100644 index 0000000..b53d643 --- /dev/null +++ b/tools/oss-organ-followup.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import hashlib +import importlib.util +import json +import os +import tempfile +import zipapp +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +spec = importlib.util.spec_from_file_location("oss_qual", ROOT / "tools" / "oss-organ-qualify.py") +qual = importlib.util.module_from_spec(spec) +assert spec and spec.loader +spec.loader.exec_module(qual) + + +def sha(path: Path) -> str: + h = hashlib.sha256() + h.update(path.read_bytes()) + return h.hexdigest() + + +def main() -> int: + system, arch = qual.platform_key() + report = {"schema": 1, "system": system, "architecture": arch, "reps": {}} + # Windows hosted runners can retain a short-lived executable file handle after + # Task exits. The runner is disposable, so cleanup failure must not overwrite + # successful experiment evidence. Rep assertions still fail normally. + with tempfile.TemporaryDirectory(prefix="oss-organ-followup-", ignore_cleanup_errors=True) as tmp: + root = Path(tmp) + task, evidence = qual.install_tool("task", root, system, arch) + work = root / "task" + work.mkdir() + (work / "Taskfile.yml").write_text( + "version: '3'\ntasks:\n" + " success:\n desc: successful bounded workload\n cmds:\n - python -c \"print('ok')\"\n" + " fail:\n desc: intentional failure workload\n cmds:\n - python -c \"raise SystemExit(7)\"\n", + encoding="utf-8", + ) + listing = qual.run([str(task), "--list-all", "--json"], cwd=work) + parsed = json.loads(listing.stdout) + task_discovery = "success" in json.dumps(parsed) and "fail" in json.dumps(parsed) + if not task_discovery: + raise RuntimeError(f"Task JSON discovery failed after fixture correction: {listing.stdout}") + report["reps"]["E-005"] = { + "purpose": "correct E-004 fixture error; distinguish tool behavior from undescribed-task listing semantics", + "task_version": evidence["tag"], + "list_all_json": True, + "both_tasks_discovered": task_discovery, + } + + source = ROOT / "lab-fixtures" / "candidate-v1" / "__main__.py" + a = root / "a.pyz" + b = root / "b.pyz" + zipapp.create_archive(source.parent, target=a) + zipapp.create_archive(source.parent, target=b) + report["reps"]["A-003"] = { + "purpose": "separate same-runner rebuild determinism from cross-runner artifact divergence", + "source_sha256": sha(source), + "source_bytes": source.stat().st_size, + "build1_sha256": sha(a), + "build2_sha256": sha(b), + "same_runner_rebuild_equal": sha(a) == sha(b), + "source_commit": os.environ.get("GITHUB_SHA"), + } + if not report["reps"]["A-003"]["same_runner_rebuild_equal"]: + raise RuntimeError("same-runner zipapp rebuild was not deterministic") + + out_dir = ROOT / "qualification-results" + out_dir.mkdir(exist_ok=True) + out = out_dir / f"followup-{system}-{arch}.json" + out.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(json.dumps(report, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/oss-organ-qualify.py b/tools/oss-organ-qualify.py new file mode 100644 index 0000000..0de1666 --- /dev/null +++ b/tools/oss-organ-qualify.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import hashlib +import json +import os +import platform +import shutil +import stat +import subprocess +import sys +import tarfile +import tempfile +import time +import urllib.request +import zipfile +from pathlib import Path + +VERSIONS = { + "task": ("go-task/task", "v3.53.1"), + "just": ("casey/just", "1.58.0"), + "nu": ("nushell/nushell", "0.115.1"), + "minizinc": ("MiniZinc/libminizinc", "2.10.1"), +} + +def platform_key() -> tuple[str, str]: + system = platform.system().lower() + raw_arch = platform.machine().lower() + if raw_arch in {"x86_64", "amd64"}: + arch = "x64" + elif raw_arch in {"arm64", "aarch64"}: + arch = "arm64" + else: + raise RuntimeError(f"unsupported architecture: {raw_arch}") + if system not in {"linux", "darwin", "windows"}: + raise RuntimeError(f"unsupported operating system: {system}") + return system, arch + +def asset_name(tool: str, system: str, arch: str) -> str: + if tool == "task": + os_name = {"linux": "linux", "darwin": "darwin", "windows": "windows"}[system] + arch_name = {"x64": "amd64", "arm64": "arm64"}[arch] + ext = "zip" if system == "windows" else "tar.gz" + return f"task_{os_name}_{arch_name}.{ext}" + if tool == "just": + target = { + ("linux", "x64"): "x86_64-unknown-linux-musl", + ("linux", "arm64"): "aarch64-unknown-linux-musl", + ("darwin", "x64"): "x86_64-apple-darwin", + ("darwin", "arm64"): "aarch64-apple-darwin", + ("windows", "x64"): "x86_64-pc-windows-msvc", + ("windows", "arm64"): "aarch64-pc-windows-msvc", + }[(system, arch)] + ext = "zip" if system == "windows" else "tar.gz" + return f"just-1.58.0-{target}.{ext}" + if tool == "nu": + target = { + ("linux", "x64"): "x86_64-unknown-linux-gnu", + ("linux", "arm64"): "aarch64-unknown-linux-gnu", + ("darwin", "x64"): "x86_64-apple-darwin", + ("darwin", "arm64"): "aarch64-apple-darwin", + ("windows", "x64"): "x86_64-pc-windows-msvc", + ("windows", "arm64"): "aarch64-pc-windows-msvc", + }[(system, arch)] + ext = "zip" if system == "windows" else "tar.gz" + return f"nu-0.115.1-{target}.{ext}" + if tool == "minizinc": + target = { + ("linux", "x64"): "x86_64-linux-gnu", + ("linux", "arm64"): "aarch64-linux-gnu", + ("darwin", "x64"): "x86_64-apple-darwin", + ("darwin", "arm64"): "aarch64-apple-darwin", + ("windows", "x64"): "x86_64-windows", + ("windows", "arm64"): "aarch64-windows", + }[(system, arch)] + ext = "zip" if system == "windows" else "tar.gz" + return f"MiniZinc-2.10.1-{target}.{ext}" + raise KeyError(tool) + +def request_json(url: str) -> dict: + headers = { + "Accept": "application/vnd.github+json", + "User-Agent": "agent-dispatch-oss-organ-qualification/1", + "X-GitHub-Api-Version": "2022-11-28", + } + token = os.environ.get("QUAL_GITHUB_TOKEN") + if token: + headers["Authorization"] = f"Bearer {token}" + req = urllib.request.Request(url, headers=headers) + with urllib.request.urlopen(req, timeout=30) as response: + return json.load(response) + +def download(url: str, destination: Path) -> None: + req = urllib.request.Request(url, headers={"User-Agent": "agent-dispatch-oss-organ-qualification/1"}) + last_error = None + for attempt in range(3): + try: + with urllib.request.urlopen(req, timeout=60) as response, destination.open("wb") as out: + shutil.copyfileobj(response, out) + return + except Exception as exc: + last_error = exc + if attempt == 2: + break + time.sleep(2 ** attempt) + raise RuntimeError(f"download failed after retries: {url}: {last_error}") + +def verify_sha256(path: Path, digest_field: str | None) -> str: + if not digest_field or not digest_field.startswith("sha256:"): + raise RuntimeError(f"release asset has no GitHub SHA-256 digest: {path.name}") + expected = digest_field.split(":", 1)[1].lower() + h = hashlib.sha256() + with path.open("rb") as f: + for chunk in iter(lambda: f.read(1024 * 1024), b""): + h.update(chunk) + actual = h.hexdigest() + if actual != expected: + raise RuntimeError(f"SHA-256 mismatch for {path.name}: expected {expected}, got {actual}") + return actual + +def extract(archive: Path, destination: Path) -> None: + destination.mkdir(parents=True, exist_ok=True) + root = destination.resolve() + if archive.suffix.lower() == ".zip": + with zipfile.ZipFile(archive) as z: + for member in z.infolist(): + target = (destination / member.filename).resolve() + if root not in target.parents and target != root: + raise RuntimeError(f"unsafe archive path in {archive.name}: {member.filename}") + z.extractall(destination) + else: + with tarfile.open(archive, "r:gz") as t: + for member in t.getmembers(): + target = (destination / member.name).resolve() + if root not in target.parents and target != root: + raise RuntimeError(f"unsafe archive path in {archive.name}: {member.name}") + t.extractall(destination) + +def find_executable(root: Path, basename: str, system: str) -> Path: + expected = basename + (".exe" if system == "windows" else "") + candidates = [p for p in root.rglob(expected) if p.is_file()] + if not candidates: + raise RuntimeError(f"could not find {expected} under {root}") + candidates.sort(key=lambda p: (len(p.parts), len(str(p)))) + exe = candidates[0] + if system != "windows": + exe.chmod(exe.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + return exe + +def run(argv: list[str], cwd: Path | None = None) -> subprocess.CompletedProcess[str]: + clean_env = os.environ.copy() + clean_env.pop("QUAL_GITHUB_TOKEN", None) + clean_env.pop("GITHUB_TOKEN", None) + result = subprocess.run( + argv, + cwd=cwd, + text=True, + capture_output=True, + timeout=120, + env=clean_env, + ) + if result.returncode != 0: + raise RuntimeError( + f"command failed ({result.returncode}): {argv}\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}" + ) + return result + +def install_tool(tool: str, root: Path, system: str, arch: str) -> tuple[Path, dict]: + repo, tag = VERSIONS[tool] + metadata = request_json(f"https://api.github.com/repos/{repo}/releases/tags/{tag}") + name = asset_name(tool, system, arch) + match = next((a for a in metadata.get("assets", []) if a.get("name") == name), None) + if match is None: + available = [a.get("name") for a in metadata.get("assets", [])] + raise RuntimeError(f"{repo}@{tag} has no expected asset {name}; available={available}") + archive = root / name + download(match["browser_download_url"], archive) + digest = verify_sha256(archive, match.get("digest")) + unpacked = root / f"{tool}-unpacked" + extract(archive, unpacked) + exe_name = {"task": "task", "just": "just", "nu": "nu", "minizinc": "minizinc"}[tool] + exe = find_executable(unpacked, exe_name, system) + return exe, { + "repo": repo, + "tag": tag, + "asset": name, + "sha256": digest, + "executable": str(exe), + } + +def exercise_task(exe: Path, root: Path) -> dict: + work = root / "task-work" + work.mkdir() + (work / "Taskfile.yml").write_text( + "version: '3'\n" + "tasks:\n" + " hello:\n" + " desc: portable qualification probe\n" + " cmds:\n" + " - echo task-ok\n", + encoding="utf-8", + ) + version = run([str(exe), "--version"]).stdout.strip() + listing = run([str(exe), "--list", "--json"], cwd=work).stdout + parsed = json.loads(listing) + if "hello" not in json.dumps(parsed): + raise RuntimeError(f"Task JSON discovery did not expose hello: {listing}") + execution = run([str(exe), "hello"], cwd=work) + if "task-ok" not in execution.stdout: + raise RuntimeError(f"Task execution missing marker: {execution.stdout}") + return {"version": version, "structured_discovery": True, "execution": "task-ok"} + +def exercise_just(exe: Path, root: Path) -> dict: + work = root / "just-work" + work.mkdir() + justfile = work / "justfile" + justfile.write_text("hello:\n @echo just-ok\n", encoding="utf-8") + version = run([str(exe), "--version"]).stdout.strip() + summary = run([str(exe), "--justfile", str(justfile), "--summary"], cwd=work).stdout + if "hello" not in summary: + raise RuntimeError(f"just summary did not expose hello: {summary}") + execution = run([str(exe), "--justfile", str(justfile), "hello"], cwd=work) + if "just-ok" not in execution.stdout: + raise RuntimeError(f"just execution missing marker: {execution.stdout}") + return {"version": version, "static_discovery": True, "execution": "just-ok"} + +def exercise_nu(exe: Path) -> dict: + version = run([str(exe), "--version"]).stdout.strip() + result = run([str(exe), "-c", "print (([1 2 3] | math sum) == 6)"]).stdout.strip().lower() + if "true" not in result: + raise RuntimeError(f"Nushell structured-pipeline probe failed: {result}") + return {"version": version, "structured_pipeline": True} + +def exercise_minizinc(exe: Path, root: Path) -> dict: + version_result = run([str(exe), "--version"]) + version = (version_result.stdout + version_result.stderr).strip() + work = root / "minizinc-work" + work.mkdir() + model = work / "probe.mzn" + model.write_text( + "var 0..10: x;\n" + "constraint x >= 7;\n" + "solve minimize x;\n" + "output [show(x)];\n", + encoding="utf-8", + ) + solved = run([str(exe), "--solver", "gecode", str(model)], cwd=work) + if "7" not in solved.stdout: + raise RuntimeError(f"MiniZinc optimization probe did not return expected optimum 7: {solved.stdout}") + return {"version": version, "optimization_probe": "optimal x=7"} + +def main() -> int: + system, arch = platform_key() + report = { + "schema": 1, + "system": system, + "architecture": arch, + "python": sys.version, + "tools": {}, + } + with tempfile.TemporaryDirectory(prefix="oss-organ-qualification-") as tmp: + root = Path(tmp) + executables = {} + for tool in ("task", "just", "nu", "minizinc"): + exe, evidence = install_tool(tool, root, system, arch) + executables[tool] = exe + report["tools"][tool] = evidence + + report["tools"]["task"]["probe"] = exercise_task(executables["task"], root) + report["tools"]["just"]["probe"] = exercise_just(executables["just"], root) + report["tools"]["nu"]["probe"] = exercise_nu(executables["nu"]) + report["tools"]["minizinc"]["probe"] = exercise_minizinc(executables["minizinc"], root) + + out_dir = Path("qualification-results") + out_dir.mkdir(exist_ok=True) + out = out_dir / f"report-{system}-{arch}.json" + out.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(json.dumps(report, indent=2, sort_keys=True)) + print(f"wrote {out}") + return 0 + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/oss-organ-reps.py b/tools/oss-organ-reps.py new file mode 100644 index 0000000..20cfdff --- /dev/null +++ b/tools/oss-organ-reps.py @@ -0,0 +1,361 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import hashlib +import importlib.util +import itertools +import json +import os +import random +import shutil +import statistics +import subprocess +import sys +import tempfile +import time +import zipapp +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +QUAL_PATH = ROOT / "tools" / "oss-organ-qualify.py" +spec = importlib.util.spec_from_file_location("oss_qual", QUAL_PATH) +qual = importlib.util.module_from_spec(spec) +assert spec and spec.loader +spec.loader.exec_module(qual) + + +def clean_env() -> dict[str, str]: + env = os.environ.copy() + env.pop("QUAL_GITHUB_TOKEN", None) + env.pop("GITHUB_TOKEN", None) + return env + + +def raw(argv: list[str], cwd: Path | None = None, timeout: int = 120, env: dict[str, str] | None = None): + return subprocess.run( + argv, + cwd=cwd, + text=True, + capture_output=True, + timeout=timeout, + env=env or clean_env(), + ) + + +def digest(path: Path) -> str: + h = hashlib.sha256() + with path.open("rb") as f: + for chunk in iter(lambda: f.read(1024 * 1024), b""): + h.update(chunk) + return h.hexdigest() + + +def timed(call, repeats: int = 5) -> dict: + samples = [] + for _ in range(repeats): + t0 = time.perf_counter() + call() + samples.append(time.perf_counter() - t0) + return { + "repeats": repeats, + "median_seconds": statistics.median(samples), + "min_seconds": min(samples), + "max_seconds": max(samples), + } + + +def e004(exes: dict[str, Path], root: Path, system: str) -> dict: + work = root / "e004" + work.mkdir() + (work / "workload.py").write_text( + "from pathlib import Path\n" + "import sys\n" + "mode=sys.argv[1]\n" + "if mode=='fail':\n" + " print('intentional-failure', file=sys.stderr); raise SystemExit(7)\n" + "Path('result.txt').write_text('alpha\\nbeta\\ngamma\\n', encoding='utf-8')\n" + "print('workload-ok')\n", + encoding="utf-8", + ) + (work / "Taskfile.yml").write_text( + "version: '3'\ntasks:\n" + " success:\n cmds:\n - python workload.py success\n" + " fail:\n cmds:\n - python workload.py fail\n", + encoding="utf-8", + ) + (work / "justfile").write_text( + "success:\n python workload.py success\n\n" + "fail:\n python workload.py fail\n", + encoding="utf-8", + ) + + def check_success(argv: list[str]): + r = raw(argv, cwd=work) + if r.returncode != 0 or "workload-ok" not in r.stdout: + raise RuntimeError(f"success workload failed: {argv}: {r.returncode} {r.stdout} {r.stderr}") + p = work / "result.txt" + if p.read_text(encoding="utf-8") != "alpha\nbeta\ngamma\n": + raise RuntimeError("non-deterministic workload result") + return digest(p) + + if system == "windows": + native_success = ["cmd", "/d", "/s", "/c", "python workload.py success"] + native_fail = ["cmd", "/d", "/s", "/c", "python workload.py fail"] + else: + native_success = ["/bin/sh", "-c", "python workload.py success"] + native_fail = ["/bin/sh", "-c", "python workload.py fail"] + + commands = { + "native": (native_success, native_fail), + "task": ([str(exes["task"]), "success"], [str(exes["task"]), "fail"]), + "just": ([str(exes["just"]), "--justfile", str(work / "justfile"), "success"], [str(exes["just"]), "--justfile", str(work / "justfile"), "fail"]), + "nushell": ([str(exes["nu"]), "-c", "^python workload.py success"], [str(exes["nu"]), "-c", "^python workload.py fail"]), + } + + result = {"workload": "write deterministic result; deliberate exit 7 failure", "engines": {}} + expected_digest = None + for name, (success, fail) in commands.items(): + first = check_success(success) + second = check_success(success) + if first != second: + raise RuntimeError(f"{name} failed idempotence digest check") + if expected_digest is None: + expected_digest = first + elif first != expected_digest: + raise RuntimeError(f"{name} produced different semantic result") + fr = raw(fail, cwd=work) + if fr.returncode == 0: + raise RuntimeError(f"{name} swallowed intentional failure") + result["engines"][name] = { + "idempotent_digest": first, + "failure_returncode": fr.returncode, + "failure_visible": "intentional-failure" in (fr.stdout + fr.stderr), + "startup_workload_timing": timed(lambda argv=success: check_success(argv)), + } + + task_list = raw([str(exes["task"]), "--list", "--json"], cwd=work) + result["engines"]["task"]["machine_discovery"] = task_list.returncode == 0 and "success" in task_list.stdout + just_list = raw([str(exes["just"]), "--justfile", str(work / "justfile"), "--summary"], cwd=work) + result["engines"]["just"]["machine_discovery"] = just_list.returncode == 0 and "success" in just_list.stdout + result["engines"]["nushell"]["structured_values_native"] = True + result["engines"]["native"]["machine_discovery"] = False + return result + + +def brute_opt(cost, eligible, capacity): + t_count = len(cost) + a_count = len(capacity) + best = None + used = [0] * a_count + + order = sorted(range(t_count), key=lambda t: sum(eligible[t])) + + def visit(i: int, total: int): + nonlocal best + if best is not None and total >= best: + return + if i == t_count: + best = total + return + t = order[i] + for a in range(a_count): + if eligible[t][a] and used[a] < capacity[a]: + used[a] += 1 + visit(i + 1, total + cost[t][a]) + used[a] -= 1 + + visit(0, 0) + return best + + +def c003(minizinc: Path, root: Path) -> dict: + work = root / "c003" + work.mkdir() + model = work / "allocation.mzn" + model.write_text( + "int: T; int: A;\n" + "set of int: Tasks=1..T; set of int: Actors=1..A;\n" + "array[Tasks,Actors] of 0..1: eligible;\n" + "array[Actors] of int: capacity;\n" + "array[Tasks,Actors] of int: cost;\n" + "array[Tasks] of var Actors: assign;\n" + "constraint forall(t in Tasks)(eligible[t,assign[t]] = 1);\n" + "constraint forall(a in Actors)(sum(t in Tasks)(bool2int(assign[t]=a)) <= capacity[a]);\n" + "var int: objective = sum(t in Tasks)(cost[t,assign[t]]);\n" + "solve minimize objective;\n" + "output [\"objective=\", show(objective)];\n", + encoding="utf-8", + ) + rng = random.Random(20260910) + cases = [] + for idx in range(20): + T, A = 8, 4 + cost = [[rng.randint(1, 20) for _ in range(A)] for _ in range(T)] + eligible = [[0] * A for _ in range(T)] + capacity = [0] * A + if idx % 5 == 0: + for t in range(T): + eligible[t][0] = 1 + capacity = [T - 1, T, T, T] + else: + base = [rng.randrange(A) for _ in range(T)] + counts = [base.count(a) for a in range(A)] + capacity = [counts[a] + rng.randint(0, 2) for a in range(A)] + for t, a0 in enumerate(base): + eligible[t][a0] = 1 + for a in range(A): + if rng.random() < 0.55: + eligible[t][a] = 1 + expected = brute_opt(cost, eligible, capacity) + flat_e = ",".join(str(x) for row in eligible for x in row) + flat_c = ",".join(str(x) for row in cost for x in row) + dzn = work / f"case-{idx:02d}.dzn" + dzn.write_text( + f"T={T}; A={A};\n" + f"eligible=array2d(1..T,1..A,[{flat_e}]);\n" + f"capacity=[{','.join(map(str,capacity))}];\n" + f"cost=array2d(1..T,1..A,[{flat_c}]);\n", + encoding="utf-8", + ) + t0 = time.perf_counter() + r = raw([str(minizinc), "--solver", "gecode", str(model), str(dzn)], cwd=work, timeout=120) + elapsed = time.perf_counter() - t0 + text = r.stdout + r.stderr + if expected is None: + observed = None if "UNSATISFIABLE" in text else "unexpected-feasible" + agreement = observed is None + else: + marker = "objective=" + if marker not in text: + observed = None + else: + tail = text.split(marker, 1)[1] + num = "".join(ch for ch in tail.splitlines()[0] if ch in "-0123456789") + observed = int(num) if num else None + agreement = observed == expected + if not agreement: + raise RuntimeError(f"MiniZinc disagreement case {idx}: expected={expected}, observed={observed}, output={text}") + cases.append({"case": idx, "expected": expected, "observed": observed, "seconds": elapsed, "dzn_bytes": dzn.stat().st_size}) + return { + "seed": 20260910, + "cases": len(cases), + "feasible": sum(c["expected"] is not None for c in cases), + "infeasible": sum(c["expected"] is None for c in cases), + "agreement": sum(c["expected"] == c["observed"] for c in cases), + "model_bytes": model.stat().st_size, + "mean_dzn_bytes": statistics.mean(c["dzn_bytes"] for c in cases), + "median_solver_seconds": statistics.median(c["seconds"] for c in cases), + "max_solver_seconds": max(c["seconds"] for c in cases), + } + + +def a002(root: Path) -> dict: + work = root / "a002" + work.mkdir() + v1 = work / "candidate-v1.pyz" + v2 = work / "candidate-v2.pyz" + zipapp.create_archive(ROOT / "lab-fixtures" / "candidate-v1", target=v1) + zipapp.create_archive(ROOT / "lab-fixtures" / "candidate-v2", target=v2) + install = work / "install" + install.mkdir() + installed = install / "candidate.pyz" + + def version(): + r = raw([sys.executable, str(installed), "--version"]) + if r.returncode != 0: + raise RuntimeError(r.stderr) + return r.stdout.strip() + + shutil.copy2(v1, installed) + first_digest = digest(installed) + if version() != "candidate-probe 1.0": + raise RuntimeError("v1 install failed") + shutil.copy2(v1, installed) + reinstall_digest = digest(installed) + if reinstall_digest != first_digest or version() != "candidate-probe 1.0": + raise RuntimeError("idempotent reinstall failed") + shutil.copy2(v2, installed) + second_digest = digest(installed) + if second_digest == first_digest or version() != "candidate-probe 2.0": + raise RuntimeError("upgrade failed") + installed.unlink() + if installed.exists(): + raise RuntimeError("remove failed") + return { + "source_commit": os.environ.get("GITHUB_SHA"), + "artifact_format": "python-zipapp-lab-fixture", + "v1_sha256": first_digest, + "reinstall_same_digest": first_digest == reinstall_digest, + "v2_sha256": second_digest, + "upgrade_changed_digest": second_digest != first_digest, + "remove_verified": not installed.exists(), + } + + +def d002(root: Path) -> dict: + target = root / "xa11y-site" + target.mkdir() + install = raw( + [sys.executable, "-m", "pip", "install", "--disable-pip-version-check", "--only-binary=:all:", "--no-deps", "--target", str(target), "xa11y==0.14.0"], + timeout=180, + ) + result = { + "version": "0.14.0", + "wheel_installable": install.returncode == 0, + "install_stderr_tail": install.stderr[-1200:], + "claim_scope": "packaging/import/error semantics only; not interactive desktop dogfood", + } + if install.returncode != 0: + return result + env = clean_env() + env["PYTHONPATH"] = str(target) + probe_code = ( + "import json, xa11y\n" + "d={'imported':True,'module':xa11y.__name__}\n" + "try:\n" + " xa11y.App.by_name('__agent_dispatch_definitely_missing__', timeout=0)\n" + " d['missing_app']='unexpected-success'\n" + "except Exception as e:\n" + " d['missing_app_exception']=type(e).__name__; d['missing_app_message']=str(e)[:500]\n" + "print(json.dumps(d))\n" + ) + probe = raw([sys.executable, "-c", probe_code], timeout=60, env=env) + result["probe_returncode"] = probe.returncode + result["probe_stdout"] = probe.stdout[-1500:] + result["probe_stderr"] = probe.stderr[-1500:] + if probe.returncode == 0: + try: + result["probe"] = json.loads(probe.stdout.strip().splitlines()[-1]) + except Exception: + pass + return result + + +def main() -> int: + system, arch = qual.platform_key() + report = {"schema": 1, "system": system, "architecture": arch, "reps": {}} + with tempfile.TemporaryDirectory(prefix="oss-organ-reps-") as tmp: + root = Path(tmp) + exes = {} + install_evidence = {} + for tool in ("task", "just", "nu", "minizinc"): + exe, evidence = qual.install_tool(tool, root, system, arch) + exes[tool] = exe + install_evidence[tool] = evidence + report["pinned_tools"] = install_evidence + report["reps"]["E-004"] = e004(exes, root, system) + report["reps"]["C-003"] = c003(exes["minizinc"], root) + report["reps"]["A-002"] = a002(root) + report["reps"]["D-002"] = d002(root) + + out_dir = ROOT / "qualification-results" + out_dir.mkdir(exist_ok=True) + out = out_dir / f"reps-{system}-{arch}.json" + out.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(json.dumps(report, indent=2, sort_keys=True)) + print(f"wrote {out}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())