build: add no-device M1 acceptance checkpoint
This commit is contained in:
parent
7dc1abc8a7
commit
f8d3c889e3
11 changed files with 457 additions and 31 deletions
19
README.md
19
README.md
|
|
@ -41,11 +41,12 @@ screenshots, the closed-application preferences folder, machine photos, and
|
||||||
operator notes directly from the actual control computer. The bundle and
|
operator notes directly from the actual control computer. The bundle and
|
||||||
exported preferences are already preserved and analyzed.
|
exported preferences are already preserved and analyzed.
|
||||||
|
|
||||||
The offline M1 reuse proofs are progressing: the Rayforge/MeerK40t path now
|
The offline M1 reuse proofs now pass as one consolidated acceptance checkpoint:
|
||||||
has deterministic mock encoding, fail-closed JCZ profile and parameter gates,
|
the Rayforge/MeerK40t path has deterministic mock encoding, fail-closed JCZ
|
||||||
list buffering, cancellation/recovery, and a transport-free workflow preview.
|
profile and parameter gates, list buffering, cancellation/recovery, layer and
|
||||||
The remaining gates are a private known-working `.lbrn2` project and a reviewed
|
recipe semantics, and a transport-free workflow preview. The remaining gates
|
||||||
Windows GTK runtime for the packaged no-device workflow test. See the
|
are a private known-working `.lbrn2` project and a reviewed Windows GTK runtime
|
||||||
|
for the packaged no-device workflow test. See the
|
||||||
[Phase 0 status](docs/status/phase-0.md) and
|
[Phase 0 status](docs/status/phase-0.md) and
|
||||||
[Rayforge GUI preflight](docs/research/rayforge-windows-gui-preflight.md).
|
[Rayforge GUI preflight](docs/research/rayforge-windows-gui-preflight.md).
|
||||||
|
|
||||||
|
|
@ -74,10 +75,9 @@ hardware-free:
|
||||||
|
|
||||||
1. collect known-working `.lbrn2` projects and the remaining screenshots,
|
1. collect known-working `.lbrn2` projects and the remaining screenshots,
|
||||||
photos, preferences folder and operator notes;
|
photos, preferences folder and operator notes;
|
||||||
2. extend the synthetic import tests with the now-observed `QPulseWidth`
|
2. obtain or produce a reviewed Windows Rayforge build artifact without
|
||||||
evidence, without assuming its `.lbrn2` representation;
|
installing a toolchain through this repository;
|
||||||
3. define the minimum Rayforge JCZ device-profile/import extension and finish
|
3. run and record the packaged no-device desktop interaction checklist;
|
||||||
controller-list buffering and safe state ordering offline;
|
|
||||||
4. accept, revise, or reject the proposed reuse architecture from evidence;
|
4. accept, revise, or reject the proposed reuse architecture from evidence;
|
||||||
5. write and review a separate hardware protocol before any live controller
|
5. write and review a separate hardware protocol before any live controller
|
||||||
communication.
|
communication.
|
||||||
|
|
@ -103,6 +103,7 @@ project workspace or development destination.
|
||||||
- [LightBurn-to-Rayforge UX capability map](docs/research/lightburn-rayforge-ux-capability-map.md)
|
- [LightBurn-to-Rayforge UX capability map](docs/research/lightburn-rayforge-ux-capability-map.md)
|
||||||
- [Rayforge layer visibility and output spike](docs/research/rayforge-layer-model-spike.md)
|
- [Rayforge layer visibility and output spike](docs/research/rayforge-layer-model-spike.md)
|
||||||
- [Rayforge material-recipe model spike](docs/research/rayforge-recipe-model-spike.md)
|
- [Rayforge material-recipe model spike](docs/research/rayforge-recipe-model-spike.md)
|
||||||
|
- [M1 no-device build and acceptance checkpoint](docs/research/m1-no-device-acceptance.md)
|
||||||
- [Proposed Rayforge/BalorMK reuse decision](docs/adr/0002-reuse-rayforge-and-balormk.md)
|
- [Proposed Rayforge/BalorMK reuse decision](docs/adr/0002-reuse-rayforge-and-balormk.md)
|
||||||
- [Live USB enumeration](docs/research/live-usb-enumeration-2026-10-17.md)
|
- [Live USB enumeration](docs/research/live-usb-enumeration-2026-10-17.md)
|
||||||
- [LightBurn device-export analysis](docs/research/lightburn-device-exports-2026-10-19.md)
|
- [LightBurn device-export analysis](docs/research/lightburn-device-exports-2026-10-19.md)
|
||||||
|
|
|
||||||
|
|
@ -118,8 +118,13 @@ Subsequent offline evidence:
|
||||||
canvas visibility and job participation are already independent serialized
|
canvas visibility and job participation are already independent serialized
|
||||||
states; LightBurn-style layer Output is a narrow aggregate UI concern, not a
|
states; LightBurn-style layer Output is a narrow aggregate UI concern, not a
|
||||||
new pipeline or document-model requirement.
|
new pipeline or document-model requirement.
|
||||||
|
- [M1 no-device acceptance checkpoint](../research/m1-no-device-acceptance.md):
|
||||||
|
the public regression suite and leak guard now form one transport-free gate
|
||||||
|
over addon lifecycle, workflow, layers, recipes, encoding, negative cases,
|
||||||
|
cancellation/recovery and synthetic imports. The packaged Windows GUI and a
|
||||||
|
known-working private project remain external gates.
|
||||||
|
|
||||||
These results support continuing toward complete list buffering and production
|
These results satisfy the current model, buffering and no-device integration
|
||||||
integration, but they do not yet satisfy every proof obligation or change this
|
proofs. They do not yet satisfy the packaged human-interaction or real-project
|
||||||
ADR's proposed status.
|
import obligations, so this ADR remains proposed.
|
||||||
|
|
||||||
|
|
|
||||||
91
docs/research/m1-no-device-acceptance.md
Normal file
91
docs/research/m1-no-device-acceptance.md
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
# M1 no-device build and acceptance checkpoint
|
||||||
|
|
||||||
|
- Date: 2026-10-20
|
||||||
|
- Rayforge commit: `c28918c87f8273dd1d244f5a057addc192807fa5`
|
||||||
|
- Scope: packaged Windows desktop and consolidated offline acceptance
|
||||||
|
- Hardware access: prohibited
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Close the remaining software-controlled part of M1 with one reproducible
|
||||||
|
checkpoint rather than treating isolated spikes as a finished desktop flow.
|
||||||
|
The checkpoint has two outputs:
|
||||||
|
|
||||||
|
1. a pinned Windows Rayforge bundle containing the external PirateRay
|
||||||
|
no-device addon;
|
||||||
|
2. one machine-readable acceptance report covering all current transport-free
|
||||||
|
evidence.
|
||||||
|
|
||||||
|
Neither output authorizes USB discovery, controller commands, galvo movement,
|
||||||
|
physical framing, arming or laser output.
|
||||||
|
|
||||||
|
## On-demand build
|
||||||
|
|
||||||
|
Run the build only when a Windows artifact is required:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
scripts/build_rayforge_windows_checkpoint.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
The script fetches the exact revision from `upstreams/rayforge.json`, stages
|
||||||
|
the no-device addon into the ignored build checkout, invokes Rayforge's
|
||||||
|
upstream MSYS2 build and creates a portable ZIP plus installer hashes under
|
||||||
|
`.build/artifacts/`. Normal clone, documentation and test workflows do not
|
||||||
|
download or install the build stack.
|
||||||
|
|
||||||
|
The checkpoint script never installs the Windows toolchain. If the reviewed
|
||||||
|
runtime is unavailable, it stops and reports the missing prerequisite. Any
|
||||||
|
toolchain preparation remains a separate, explicitly invoked upstream action
|
||||||
|
or should happen on a dedicated build runner.
|
||||||
|
|
||||||
|
Before staging, the script rejects transport imports in the addon. The build
|
||||||
|
manifest records the exact upstream commit and hashes every addon source file,
|
||||||
|
the portable archive and installer. It does not claim the GUI has been tested.
|
||||||
|
|
||||||
|
## Automated acceptance
|
||||||
|
|
||||||
|
Run with the isolated Rayforge Python environment:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python scripts/run_m1_no_device_acceptance.py <rayforge-checkout> `
|
||||||
|
--output .build/artifacts/m1-no-device-acceptance.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The runner executes the complete repository test suite and public-repository
|
||||||
|
guard, then collects and hashes the regression-verified golden reports for the
|
||||||
|
addon lifecycle, graphical workflow model, layer model, recipe model, JCZ
|
||||||
|
equivalence, negative cases, cancellation/recovery and both LightBurn import
|
||||||
|
spikes. It passes only when the repository checks pass, transport remains
|
||||||
|
absent, the controller backend remains unreachable, negative cases fail
|
||||||
|
closed, cancellation requires recovery and incomplete LightBurn import still
|
||||||
|
blocks encoding.
|
||||||
|
|
||||||
|
## Manual packaged-app checklist
|
||||||
|
|
||||||
|
Use only the isolated no-device profile and an empty work area. Do not connect
|
||||||
|
or configure a controller.
|
||||||
|
|
||||||
|
1. Verify the artifact and addon hashes against the build manifest.
|
||||||
|
2. Launch the portable build with a fresh application-data directory.
|
||||||
|
3. Confirm the UI identifies the state as offline/no-device.
|
||||||
|
4. Open synthetic geometry and inspect the 110 × 110 mm synthetic field.
|
||||||
|
5. Name, color, select and reorder layers.
|
||||||
|
6. Verify canvas visibility and output participation are independent.
|
||||||
|
7. Edit power, speed, frequency and pulse width and confirm Hz/ns labels.
|
||||||
|
8. Apply the synthetic unverified recipe; confirm mismatched compatibility is
|
||||||
|
blocked and no approved label appears.
|
||||||
|
9. Preview enabled geometry, ordering, hatch and out-of-field diagnostics.
|
||||||
|
10. Save and reopen; confirm geometry, order and supported settings survive.
|
||||||
|
11. Start simulation, hold, resume, cancel and explicitly recover.
|
||||||
|
12. Exit and preserve only the sanitized acceptance report and screenshots.
|
||||||
|
|
||||||
|
Any device discovery prompt, physical-device status, transport attempt or
|
||||||
|
unlabelled framing action is a stop condition and fails the checkpoint.
|
||||||
|
|
||||||
|
## Remaining external gates
|
||||||
|
|
||||||
|
- A known-working private `.lbrn2` is still required before implementing
|
||||||
|
project-level `QPulseWidth` mapping.
|
||||||
|
- Human interaction results from the packaged build must be recorded before
|
||||||
|
ADR 0002 can be accepted.
|
||||||
|
- Physical controller behavior remains outside M1.
|
||||||
|
|
@ -45,10 +45,11 @@ revision into the ignored `.build/upstreams/` cache only when explicitly
|
||||||
invoked. Its default action prepares source only; it neither installs the
|
invoked. Its default action prepares source only; it neither installs the
|
||||||
Windows toolchain nor starts a build.
|
Windows toolchain nor starts a build.
|
||||||
|
|
||||||
Windows dependencies and bundle creation require separate
|
The repository scripts do not install Windows dependencies. Bundle creation
|
||||||
`-InstallWindowsDependencies` and `-BuildWindowsBundle` switches. This keeps a
|
requires the explicit `-BuildWindowsBundle` switch and an already reviewed,
|
||||||
multi-gigabyte MSYS2/UCRT64 toolchain out of normal clone, test and documentation
|
prepared runtime. This keeps a multi-gigabyte MSYS2/UCRT64 toolchain out of
|
||||||
workflows and makes a CI-produced bundle the preferred interaction-test input.
|
normal clone, test and documentation workflows and makes a CI-produced bundle
|
||||||
|
the preferred interaction-test input.
|
||||||
|
|
||||||
## Next action
|
## Next action
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -316,6 +316,12 @@ has no MSYS2/UCRT64 Rayforge runtime. No dependencies were installed and no
|
||||||
GUI success is claimed. The packaged interaction test therefore remains
|
GUI success is claimed. The packaged interaction test therefore remains
|
||||||
blocked on a reviewed Windows GTK runtime, not on controller behavior.
|
blocked on a reviewed Windows GTK runtime, not on controller behavior.
|
||||||
|
|
||||||
|
The transport-free portions are now consolidated by the
|
||||||
|
[M1 no-device acceptance checkpoint](research/m1-no-device-acceptance.md).
|
||||||
|
It reruns the public suite and leak guard and hashes the validated regression
|
||||||
|
reports. The on-demand build entry point fetches pinned source and stages the
|
||||||
|
addon but deliberately refuses to install a missing Windows toolchain.
|
||||||
|
|
||||||
## Decision rules
|
## Decision rules
|
||||||
|
|
||||||
- A simulator test precedes every equivalent hardware behavior.
|
- A simulator test precedes every equivalent hardware behavior.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Phase 0 status
|
# Phase 0 status
|
||||||
|
|
||||||
Last updated: 2026-10-19
|
Last updated: 2026-10-20
|
||||||
|
|
||||||
## Completed
|
## Completed
|
||||||
|
|
||||||
|
|
@ -72,13 +72,19 @@ evidence for:
|
||||||
- fail-closed profile and encoder negative cases;
|
- fail-closed profile and encoder negative cases;
|
||||||
- Rayforge workflow-model serialization and preview behavior for power,
|
- Rayforge workflow-model serialization and preview behavior for power,
|
||||||
frequency and pulse width.
|
frequency and pulse width.
|
||||||
|
- independent layer visibility/output state and ordered serialization;
|
||||||
|
- material-recipe matching plus a fail-closed PirateRay provenance and exact
|
||||||
|
machine/source/lens/correction compatibility gate;
|
||||||
|
- one consolidated no-device acceptance report that reruns all 69 public
|
||||||
|
tests and the leak guard, then hashes the regression evidence.
|
||||||
|
|
||||||
The remaining M1 gates are deliberately explicit:
|
The remaining M1 gates are deliberately explicit:
|
||||||
|
|
||||||
- a private known-working `.lbrn2` project is still required before mapping
|
- a private known-working `.lbrn2` project is still required before mapping
|
||||||
project-level `QPulseWidth` into workflow settings;
|
project-level `QPulseWidth` into workflow settings;
|
||||||
- packaged Windows/GTK interaction is blocked until a reviewed Rayforge
|
- packaged Windows/GTK interaction is blocked until a reviewed Rayforge
|
||||||
runtime with PyGObject and MSYS2/UCRT64 is available;
|
runtime with PyGObject and MSYS2/UCRT64 is available; the repository build
|
||||||
|
entry point fetches pinned source but never installs that runtime;
|
||||||
- graphical workflow, import, lens correction and controller protocol
|
- graphical workflow, import, lens correction and controller protocol
|
||||||
behavior remain unproven at the physical boundary;
|
behavior remain unproven at the physical boundary;
|
||||||
- ADR 0002 remains **Proposed** until those gaps are resolved or rejected by
|
- ADR 0002 remains **Proposed** until those gaps are resolved or rejected by
|
||||||
|
|
|
||||||
111
scripts/build_rayforge_windows_checkpoint.ps1
Normal file
111
scripts/build_rayforge_windows_checkpoint.ps1
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
[CmdletBinding(SupportsShouldProcess)]
|
||||||
|
param(
|
||||||
|
[string]$CacheRoot,
|
||||||
|
[string]$ArtifactRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$repositoryRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
$upstreamManifest = Get-Content -Raw -LiteralPath (
|
||||||
|
Join-Path $repositoryRoot "upstreams\rayforge.json"
|
||||||
|
) | ConvertFrom-Json
|
||||||
|
$shortCommit = $upstreamManifest.commit.Substring(0, 12)
|
||||||
|
|
||||||
|
if (-not $CacheRoot) {
|
||||||
|
$CacheRoot = Join-Path $repositoryRoot ".build\upstreams"
|
||||||
|
}
|
||||||
|
if (-not $ArtifactRoot) {
|
||||||
|
$ArtifactRoot = Join-Path $repositoryRoot ".build\artifacts"
|
||||||
|
}
|
||||||
|
|
||||||
|
& (Join-Path $PSScriptRoot "prepare_rayforge_build.ps1") `
|
||||||
|
-CacheRoot $CacheRoot
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Pinned Rayforge source preparation failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
$checkout = Join-Path $CacheRoot "rayforge-$shortCommit"
|
||||||
|
$verifiedCommit = (& git -C $checkout rev-parse HEAD).Trim()
|
||||||
|
if ($verifiedCommit -ne $upstreamManifest.commit) {
|
||||||
|
throw "Rayforge pin mismatch before packaging"
|
||||||
|
}
|
||||||
|
|
||||||
|
$addonSource = Join-Path $repositoryRoot (
|
||||||
|
"spikes\rayforge-jcz-no-device-addon"
|
||||||
|
)
|
||||||
|
$addonText = Get-ChildItem -LiteralPath $addonSource -Recurse -File |
|
||||||
|
Where-Object { $_.Extension -eq ".py" } |
|
||||||
|
ForEach-Object { Get-Content -Raw -LiteralPath $_.FullName }
|
||||||
|
$forbidden = @("import usb", "import serial", "import socket", "pyusb")
|
||||||
|
foreach ($needle in $forbidden) {
|
||||||
|
if (($addonText -join "`n").ToLowerInvariant().Contains($needle)) {
|
||||||
|
throw "No-device addon contains forbidden transport token: $needle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$addonDestination = Join-Path $checkout (
|
||||||
|
"rayforge\builtin_addons\rayforge-addon-the-pirate-ray-jcz"
|
||||||
|
)
|
||||||
|
if ($PSCmdlet.ShouldProcess($addonDestination, "Stage no-device addon")) {
|
||||||
|
New-Item -ItemType Directory -Force -Path $addonDestination | Out-Null
|
||||||
|
Copy-Item -Path (Join-Path $addonSource "*") `
|
||||||
|
-Destination $addonDestination -Recurse -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
$prepareArguments = @{
|
||||||
|
CacheRoot = $CacheRoot
|
||||||
|
BuildWindowsBundle = $true
|
||||||
|
}
|
||||||
|
& (Join-Path $PSScriptRoot "prepare_rayforge_build.ps1") @prepareArguments
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Pinned Rayforge Windows bundle build failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
$bundleName = "rayforge-vpirateray-$shortCommit"
|
||||||
|
$bundleDirectory = Join-Path $checkout "dist\$bundleName"
|
||||||
|
$application = Join-Path $bundleDirectory "$bundleName.exe"
|
||||||
|
$installer = Join-Path $checkout "dist\$bundleName-installer.exe"
|
||||||
|
if (-not (Test-Path -LiteralPath $application)) {
|
||||||
|
throw "Expected packaged application was not produced: $application"
|
||||||
|
}
|
||||||
|
if (-not (Test-Path -LiteralPath $installer)) {
|
||||||
|
throw "Expected installer was not produced: $installer"
|
||||||
|
}
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Force -Path $ArtifactRoot | Out-Null
|
||||||
|
$archive = Join-Path $ArtifactRoot "$bundleName.zip"
|
||||||
|
if ($PSCmdlet.ShouldProcess($archive, "Create portable checkpoint archive")) {
|
||||||
|
Compress-Archive -LiteralPath $bundleDirectory -DestinationPath $archive -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
$addonFiles = Get-ChildItem -LiteralPath $addonSource -Recurse -File |
|
||||||
|
Sort-Object FullName |
|
||||||
|
ForEach-Object {
|
||||||
|
[ordered]@{
|
||||||
|
path = $_.FullName.Substring($addonSource.Length + 1).Replace("\", "/")
|
||||||
|
sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $_.FullName).Hash.ToLowerInvariant()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$result = [ordered]@{
|
||||||
|
checkpoint = "Rayforge Windows no-device bundle"
|
||||||
|
rayforge_commit = $verifiedCommit
|
||||||
|
addon = "the_pirate_ray_jcz"
|
||||||
|
addon_source_files = $addonFiles
|
||||||
|
hardware_access = $false
|
||||||
|
transport = $null
|
||||||
|
bundle = [ordered]@{
|
||||||
|
path = $archive
|
||||||
|
sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $archive).Hash.ToLowerInvariant()
|
||||||
|
bytes = (Get-Item -LiteralPath $archive).Length
|
||||||
|
}
|
||||||
|
installer = [ordered]@{
|
||||||
|
path = $installer
|
||||||
|
sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $installer).Hash.ToLowerInvariant()
|
||||||
|
bytes = (Get-Item -LiteralPath $installer).Length
|
||||||
|
}
|
||||||
|
manual_interaction_required = $true
|
||||||
|
}
|
||||||
|
$manifestPath = Join-Path $ArtifactRoot "rayforge-windows-checkpoint.json"
|
||||||
|
$result | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $manifestPath -Encoding utf8
|
||||||
|
Write-Output "Checkpoint manifest: $manifestPath"
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
[CmdletBinding(SupportsShouldProcess)]
|
[CmdletBinding(SupportsShouldProcess)]
|
||||||
param(
|
param(
|
||||||
[string]$CacheRoot,
|
[string]$CacheRoot,
|
||||||
[switch]$InstallWindowsDependencies,
|
|
||||||
[switch]$BuildWindowsBundle
|
[switch]$BuildWindowsBundle
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -58,7 +57,7 @@ if ($verifiedCommit -ne $manifest.commit) {
|
||||||
Write-Output "Rayforge source ready at $checkout"
|
Write-Output "Rayforge source ready at $checkout"
|
||||||
Write-Output "Verified commit: $verifiedCommit"
|
Write-Output "Verified commit: $verifiedCommit"
|
||||||
|
|
||||||
if (-not $InstallWindowsDependencies -and -not $BuildWindowsBundle) {
|
if (-not $BuildWindowsBundle) {
|
||||||
Write-Output "No dependencies were installed and no build was started."
|
Write-Output "No dependencies were installed and no build was started."
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -70,19 +69,10 @@ if (-not (Test-Path -LiteralPath $bash)) {
|
||||||
|
|
||||||
$msysCheckout = (& $bash -lc "cygpath -u '$checkout'").Trim()
|
$msysCheckout = (& $bash -lc "cygpath -u '$checkout'").Trim()
|
||||||
|
|
||||||
if ($InstallWindowsDependencies) {
|
|
||||||
if ($PSCmdlet.ShouldProcess("C:\msys64", "Install Rayforge Windows build dependencies")) {
|
|
||||||
& $bash -lc "cd '$msysCheckout' && bash scripts/win/win_setup.sh"
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "Rayforge Windows dependency setup failed with exit code $LASTEXITCODE"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($BuildWindowsBundle) {
|
if ($BuildWindowsBundle) {
|
||||||
$environmentFile = Join-Path $checkout ".msys2_env"
|
$environmentFile = Join-Path $checkout ".msys2_env"
|
||||||
if (-not (Test-Path -LiteralPath $environmentFile)) {
|
if (-not (Test-Path -LiteralPath $environmentFile)) {
|
||||||
throw "Windows dependencies are not prepared. Re-run with -InstallWindowsDependencies."
|
throw "A reviewed Rayforge Windows build runtime is not prepared. No dependencies were installed."
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($PSCmdlet.ShouldProcess($checkout, "Build pinned Rayforge Windows bundle")) {
|
if ($PSCmdlet.ShouldProcess($checkout, "Build pinned Rayforge Windows bundle")) {
|
||||||
|
|
|
||||||
156
scripts/run_m1_no_device_acceptance.py
Normal file
156
scripts/run_m1_no_device_acceptance.py
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
"""Consolidate the transport-free M1 regression evidence."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
PINNED_COMMIT = "c28918c87f8273dd1d244f5a057addc192807fa5"
|
||||||
|
|
||||||
|
EVIDENCE = {
|
||||||
|
"addon_lifecycle": "rayforge-addon-spike.json",
|
||||||
|
"graphical_workflow_model": "rayforge-graphical-workflow.json",
|
||||||
|
"layer_model": "rayforge-layer-model.json",
|
||||||
|
"recipe_model": "rayforge-recipe-model.json",
|
||||||
|
"jcz_equivalence": "rayforge-jcz-equivalence.json",
|
||||||
|
"jcz_negative_cases": "rayforge-jcz-negative-cases.json",
|
||||||
|
"cancel_recovery": "rayforge-jcz-cancel-recovery.json",
|
||||||
|
"lightburn_mopa_import": "rayforge-lightburn-mopa-import.json",
|
||||||
|
"lightburn_device_import": "rayforge-lightburn-device-import.json",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _commit_of(source: Path) -> str:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "-C", str(source), "rev-parse", "HEAD"],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _run_check(command: list[str], environment: dict[str, str]) -> dict:
|
||||||
|
result = subprocess.run(
|
||||||
|
command,
|
||||||
|
cwd=ROOT,
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=120,
|
||||||
|
env=environment,
|
||||||
|
)
|
||||||
|
combined = (result.stdout + result.stderr).strip()
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(combined or f"check failed: {command}")
|
||||||
|
return {
|
||||||
|
"status": "passed",
|
||||||
|
"command": command,
|
||||||
|
"output_tail": combined.splitlines()[-4:],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _load_evidence() -> dict[str, dict]:
|
||||||
|
fixture_dir = ROOT / "tests" / "fixtures"
|
||||||
|
results = {}
|
||||||
|
for name, filename in EVIDENCE.items():
|
||||||
|
path = fixture_dir / filename
|
||||||
|
results[name] = {
|
||||||
|
"path": path.relative_to(ROOT).as_posix(),
|
||||||
|
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
|
||||||
|
"report": json.loads(path.read_text(encoding="utf-8")),
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def run(source: Path) -> dict:
|
||||||
|
commit = _commit_of(source)
|
||||||
|
if commit != PINNED_COMMIT:
|
||||||
|
raise RuntimeError(f"expected Rayforge {PINNED_COMMIT}, found {commit}")
|
||||||
|
|
||||||
|
temp_dir = ROOT / ".build" / "acceptance-temp"
|
||||||
|
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
environment = os.environ.copy()
|
||||||
|
environment["TEMP"] = str(temp_dir)
|
||||||
|
environment["TMP"] = str(temp_dir)
|
||||||
|
|
||||||
|
repository_checks = {
|
||||||
|
"unit_tests": _run_check(
|
||||||
|
[sys.executable, "-m", "unittest", "discover", "-s", "tests"],
|
||||||
|
environment,
|
||||||
|
),
|
||||||
|
"public_repo_guard": _run_check(
|
||||||
|
[sys.executable, "scripts/check_public_repo.py", "."],
|
||||||
|
environment,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
evidence = _load_evidence()
|
||||||
|
|
||||||
|
graphical = evidence["graphical_workflow_model"]["report"]
|
||||||
|
negative = evidence["jcz_negative_cases"]["report"]
|
||||||
|
cancellation = evidence["cancel_recovery"]["report"]
|
||||||
|
device_import = evidence["lightburn_device_import"]["report"]
|
||||||
|
transport_free = all(
|
||||||
|
item["report"].get("transport") is None
|
||||||
|
for item in evidence.values()
|
||||||
|
if "transport" in item["report"]
|
||||||
|
)
|
||||||
|
controller_unreachable = (
|
||||||
|
graphical["controller_backend_reachable"] is False
|
||||||
|
)
|
||||||
|
checks = {
|
||||||
|
"repository_checks_passed": all(
|
||||||
|
item["status"] == "passed" for item in repository_checks.values()
|
||||||
|
),
|
||||||
|
"transport_free": transport_free,
|
||||||
|
"controller_backend_unreachable": controller_unreachable,
|
||||||
|
"negative_cases_fail_closed": negative["all_failed_closed"],
|
||||||
|
"cancel_requires_recovery": cancellation[
|
||||||
|
"restart_without_recovery_rejected"
|
||||||
|
],
|
||||||
|
"incomplete_import_blocks_encoding": not device_import[
|
||||||
|
"addon_profile_seam"
|
||||||
|
]["encoding_allowed"],
|
||||||
|
}
|
||||||
|
passed = all(checks.values())
|
||||||
|
return {
|
||||||
|
"checkpoint": "M1 no-device acceptance",
|
||||||
|
"status": "passed" if passed else "failed",
|
||||||
|
"rayforge_commit": commit,
|
||||||
|
"hardware_access": False,
|
||||||
|
"controller_backend_reachable": not controller_unreachable,
|
||||||
|
"checks": checks,
|
||||||
|
"repository_checks": repository_checks,
|
||||||
|
"evidence": evidence,
|
||||||
|
"remaining_manual_gates": [
|
||||||
|
"packaged Windows GUI interaction",
|
||||||
|
"known-working private LightBurn project import",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("rayforge_checkout", type=Path)
|
||||||
|
parser.add_argument("--output", type=Path, required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
(ROOT / ".build").mkdir(exist_ok=True)
|
||||||
|
result = run(args.rayforge_checkout.resolve())
|
||||||
|
rendered = json.dumps(result, indent=2, sort_keys=True) + "\n"
|
||||||
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
args.output.write_text(rendered, encoding="utf-8", newline="\n")
|
||||||
|
print(f"status={result['status']}")
|
||||||
|
print(f"sha256={hashlib.sha256(rendered.encode()).hexdigest()}")
|
||||||
|
return 0 if result["status"] == "passed" else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
59
tests/test_m1_acceptance_pipeline.py
Normal file
59
tests/test_m1_acceptance_pipeline.py
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import json
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
ACCEPTANCE = ROOT / "scripts" / "run_m1_no_device_acceptance.py"
|
||||||
|
BUILD = ROOT / "scripts" / "build_rayforge_windows_checkpoint.ps1"
|
||||||
|
UPSTREAM = ROOT / "upstreams" / "rayforge.json"
|
||||||
|
|
||||||
|
|
||||||
|
class M1AcceptancePipelineTests(unittest.TestCase):
|
||||||
|
def test_acceptance_collects_every_current_offline_proof(self):
|
||||||
|
source = ACCEPTANCE.read_text(encoding="utf-8")
|
||||||
|
expected = (
|
||||||
|
"rayforge-addon-spike.json",
|
||||||
|
"rayforge-graphical-workflow.json",
|
||||||
|
"rayforge-layer-model.json",
|
||||||
|
"rayforge-recipe-model.json",
|
||||||
|
"rayforge-jcz-equivalence.json",
|
||||||
|
"rayforge-jcz-negative-cases.json",
|
||||||
|
"rayforge-jcz-cancel-recovery.json",
|
||||||
|
"rayforge-lightburn-mopa-import.json",
|
||||||
|
"rayforge-lightburn-device-import.json",
|
||||||
|
)
|
||||||
|
for script in expected:
|
||||||
|
self.assertIn(script, source)
|
||||||
|
|
||||||
|
def test_acceptance_is_pinned_and_reports_remaining_manual_gates(self):
|
||||||
|
manifest = json.loads(UPSTREAM.read_text(encoding="utf-8"))
|
||||||
|
source = ACCEPTANCE.read_text(encoding="utf-8")
|
||||||
|
self.assertIn(manifest["commit"], source)
|
||||||
|
self.assertIn("packaged Windows GUI interaction", source)
|
||||||
|
self.assertIn("known-working private LightBurn project import", source)
|
||||||
|
self.assertIn('"-m", "unittest", "discover"', source)
|
||||||
|
self.assertIn("scripts/check_public_repo.py", source)
|
||||||
|
self.assertIn("timeout=120", source)
|
||||||
|
|
||||||
|
def test_build_is_explicit_pinned_and_packages_the_addon(self):
|
||||||
|
source = BUILD.read_text(encoding="utf-8")
|
||||||
|
self.assertNotIn("InstallWindowsDependencies", source)
|
||||||
|
self.assertIn("prepare_rayforge_build.ps1", source)
|
||||||
|
self.assertIn("rayforge-addon-the-pirate-ray-jcz", source)
|
||||||
|
self.assertIn("Compress-Archive", source)
|
||||||
|
self.assertIn("Get-FileHash -Algorithm SHA256", source)
|
||||||
|
|
||||||
|
def test_acceptance_has_no_hardware_commands(self):
|
||||||
|
source = ACCEPTANCE.read_text(encoding="utf-8").lower()
|
||||||
|
for forbidden in ("usb.core", "import usb", "open_device"):
|
||||||
|
self.assertNotIn(forbidden, source)
|
||||||
|
|
||||||
|
def test_build_rejects_transport_imports_in_staged_addon(self):
|
||||||
|
source = BUILD.read_text(encoding="utf-8").lower()
|
||||||
|
for forbidden in ("import usb", "import serial", "import socket"):
|
||||||
|
self.assertIn(forbidden, source)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
|
|
@ -21,7 +21,7 @@ class UpstreamBuildReferenceTests(unittest.TestCase):
|
||||||
script = PREPARE.read_text(encoding="utf-8")
|
script = PREPARE.read_text(encoding="utf-8")
|
||||||
self.assertIn('".build\\upstreams"', script)
|
self.assertIn('".build\\upstreams"', script)
|
||||||
self.assertIn("SupportsShouldProcess", script)
|
self.assertIn("SupportsShouldProcess", script)
|
||||||
self.assertIn("InstallWindowsDependencies", script)
|
self.assertNotIn("InstallWindowsDependencies", script)
|
||||||
self.assertIn("BuildWindowsBundle", script)
|
self.assertIn("BuildWindowsBundle", script)
|
||||||
self.assertIn("No dependencies were installed", script)
|
self.assertIn("No dependencies were installed", script)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue