Expose import preflight in packaged Rayforge UI

This commit is contained in:
Codex 2026-10-24 15:38:02 +02:00
parent 8ab25827db
commit f87135657c
9 changed files with 149 additions and 8 deletions

View file

@ -94,9 +94,10 @@ element types and applies the evidence-bound Hz/ns contract only to LightBurn
2.1.03. The bridge binds source layer IDs, uses the host MOPA setters,
serializes exact unit provenance and disables every staged step. Document
preflight presents the remaining blockers and the offline driver rejects them
before buffer preparation. It never authorizes encoding. The next import task
is packaged GTK presentation; absent pulse width and image equivalence remain
blockers.
before buffer preparation. The packaged frontend now exposes that report as a
read-only Tools-menu dialog with stable blocker codes. It never authorizes
encoding. Packaged interaction evidence, absent pulse width and image
equivalence remain blockers.
If a Windows artifact is available, keep the machine disconnected and run, in
order:

View file

@ -41,7 +41,9 @@ unverified. Rayforge's relevant Step and LaserStep files are included in the
pinned upstream contract manifest.
The addon now also exposes a transport-free document preflight report with
stable issue codes and human-readable blocker messages. The offline driver
stable issue codes and human-readable blocker messages. Its packaged frontend
adds a read-only `PirateRay Import Preflight` action to Rayforge's Tools menu;
the dialog renders those codes and keeps output disabled. The offline driver
runs this check before it prepares encoded buffers. A staged import is rejected;
manually re-enabling its step adds a distinct error, and malformed review data
fails closed. There is intentionally no accepted or promoted import state yet.

View file

@ -70,7 +70,7 @@ required by real workshop jobs and supported by evidence.
| Preserve MOPA units and values | Galvo frequency is presented in kHz and Q-pulse width in ns | Rayforge's workflow uses Hz and ns; the addon maps complete LightBurn 2.1.03 vector settings with an evidence-bound raw-Hz/display-kHz and direct-ns contract | Proven offline for complete vector settings; image/absent-pulse cases gated | Verify editing and persistence in the packaged no-device GUI while retaining the version and provenance checks |
| Apply a reusable material recipe | LightBurn libraries organize presets by material, thickness and description and apply/link them to layers | Rayforge recipes already match machine, head, material, thickness and step type and preserve Hz/ns settings; the external addon now validates versioned provenance plus exact source/lens/correction compatibility while withholding hardware authorization | Proven offline at model/source level; approval policy and portable collections remain | Integrate the validated metadata into recipe selection before changing the existing recipe UI |
| Inspect planned behavior before any run | LightBurn Preview reflects enabled layers and planned order | Rayforge produced a populated `JobArtifact` with Ops, path length and no machine code | Proven offline; human interaction untested | Verify layer filtering, order, hatch and warnings in the packaged no-device GUI |
| Reject a visibly invalid job | Preview/preflight should reveal unintended output; the target product must reject profile mismatch and out-of-field geometry | Existing JCZ negative tests reject profile mismatch, out-of-field geometry, invalid values and unsupported Ops | Proven offline at encoder boundary | Surface these failures as understandable preflight messages without weakening the fail-closed gate |
| Reject a visibly invalid job | Preview/preflight should reveal unintended output; the target product must reject profile mismatch and out-of-field geometry | Existing JCZ negative tests reject profile mismatch, out-of-field geometry, invalid values and unsupported Ops; the packaged addon exposes a read-only Tools-menu dialog for stable import blocker codes | Proven offline at encoder and frontend-contract boundaries; packaged interaction pending | Exercise the dialog in the packaged no-device checklist without weakening the fail-closed gate |
| Keep machine and lens selection out of the normal start flow | LightBurn device settings are separate from ordinary layer work | Rayforge has machine profiles and capability-gated settings; the desired administrator-oriented placement is not interaction-tested | Product requirement | Verify default-profile startup and move profile/lens changes behind an advanced path if upstream behavior requires it |
| Save and reopen without losing job intent | Project files retain artwork and layer settings | The synthetic step retained power, speed, frequency and pulse width through serialization/deserialization; full project interaction is untested | Partly proven offline | Add save/reload to the packaged no-device checklist |
| Frame safely | Galvo framing is a distinct physical action used to align a job | No physical framing is authorized; galvo framing semantics and Rayforge fit remain unproven | Gated | Keep only an explicitly labelled on-canvas simulation until a reviewed hardware protocol authorizes exact behavior |

View file

@ -113,7 +113,8 @@ The remaining M1 release gates are deliberately narrow:
- vector project frequency and `QPulseWidth` now use a LightBurn 2.1.03,
evidence-bound Hz/ns contract; absent pulse width and image equivalence stay
blocked;
- packaged Windows/GTK interaction is blocked until a reviewed Rayforge
- the addon now registers a read-only GTK Tools-menu preflight action, but
packaged Windows/GTK interaction is blocked until a reviewed Rayforge
runtime with PyGObject and MSYS2/UCRT64 is available; the repository build
entry point fetches pinned source but never installs that runtime;
- three operator-verified projects establish project-level `frequency` and

View file

@ -0,0 +1,50 @@
"""Packaged GTK presentation for PirateRay import preflight results."""
from gettext import gettext as _
from gi.repository import Adw, Gio
from rayforge.core.hooks import hookimpl
from rayforge.ui_gtk.action_registry import MenuPlacement, action_registry
from rayforge.ui_gtk.actions import action_extension_registry
from .import_preflight import inspect_imported_workflow
from .preflight_presentation import format_preflight_report
ADDON_NAME = "the_pirate_ray_jcz"
ACTION_NAME = "pirateray-import-preflight"
def _register_actions(action_manager):
action = Gio.SimpleAction.new(ACTION_NAME, None)
def on_activate(_action, _parameter):
report = inspect_imported_workflow(action_manager.doc)
presentation = format_preflight_report(report)
dialog = Adw.MessageDialog(
transient_for=action_manager.win,
heading=presentation["heading"],
body=presentation["body"],
)
dialog.add_response("close", _("Close"))
dialog.set_default_response("close")
dialog.set_close_response("close")
dialog.present()
action.connect("activate", on_activate)
action_manager.win.add_action(action)
action_manager.actions[ACTION_NAME] = action
action_registry.register(
action_name=ACTION_NAME,
action=action,
addon_name=ADDON_NAME,
label=_("PirateRay Import Preflight"),
menu=MenuPlacement(menu_id="tools", priority=60),
)
@hookimpl
def register_actions(_action_registry):
"""Expose the read-only preflight report in Rayforge's Tools menu."""
action_extension_registry.register_setup(_register_actions, ADDON_NAME)

View file

@ -0,0 +1,58 @@
"""Build a UI-neutral presentation of PirateRay import preflight results."""
from __future__ import annotations
from collections.abc import Mapping
from gettext import gettext as _
from typing import Any
def _invalid_report() -> dict[str, str]:
return {
"status": "blocked",
"heading": _("Import preflight blocked"),
"body": _(
"The preflight report is malformed. Output remains disabled."
),
}
def format_preflight_report(report: Any) -> dict[str, str]:
"""Return localized heading and body text for the packaged GTK dialog."""
if not isinstance(report, Mapping):
return _invalid_report()
ready = report.get("ready")
issues = report.get("issues")
if not isinstance(ready, bool) or not isinstance(issues, list):
return _invalid_report()
if ready:
if issues:
return _invalid_report()
return {
"status": "ready",
"heading": _("Import preflight ready"),
"body": _("No unresolved PirateRay import blockers were found."),
}
if not issues:
return _invalid_report()
lines = []
for issue in issues:
if not isinstance(issue, Mapping):
return _invalid_report()
code = issue.get("code")
message = issue.get("message")
if not isinstance(code, str) or not code:
return _invalid_report()
if not isinstance(message, str) or not message:
return _invalid_report()
lines.append(f"{code}: {message}")
prefix = _(
"Resolve every issue before enabling output. Output remains disabled."
)
return {
"status": "blocked",
"heading": _("Import preflight blocked"),
"body": prefix + "\n\n" + "\n".join(lines),
}

View file

@ -1,7 +1,7 @@
name: the_pirate_ray_jcz
display_name: The Pirate Ray JCZ (offline spike)
description: Hardware-free binary-driver registration proof for Rayforge.
version: 0.1.0
description: Hardware-free JCZ workflow and import-preflight integration.
version: 0.2.0
api_version: 23
depends:
- rayforge>=1.0.0
@ -9,4 +9,5 @@ author:
name: The Pirate Ray contributors
provides:
worker: pirate_ray_jcz.worker
frontend: pirate_ray_jcz.frontend
maturity: experimental

View file

@ -17,8 +17,10 @@
],
"packaged_files": [
"pirate_ray_jcz/__init__.py",
"pirate_ray_jcz/frontend.py",
"pirate_ray_jcz/import_preflight.py",
"pirate_ray_jcz/offline_session.py",
"pirate_ray_jcz/preflight_presentation.py",
"pirate_ray_jcz/profile_import.py",
"pirate_ray_jcz/project_import.py",
"pirate_ray_jcz/rayforge_compat.py",

View file

@ -9,6 +9,7 @@ sys.path.insert(0, str(ADDON))
from pirate_ray_jcz.import_preflight import (
ImportPreflightError, assert_no_unresolved_imports, inspect_imported_workflow,
)
from pirate_ray_jcz.preflight_presentation import format_preflight_report
from pirate_ray_jcz.workflow_import import METADATA_KEY
@ -70,6 +71,31 @@ class ImportPreflightTests(unittest.TestCase):
prepare = source.index("self.offline_session.prepare(encoded)")
self.assertLess(preflight, prepare)
def test_packaged_presentation_lists_stable_blocker_codes(self):
presentation = format_preflight_report(
inspect_imported_workflow(document(imported_step()))
)
self.assertEqual("blocked", presentation["status"])
self.assertEqual("Import preflight blocked", presentation["heading"])
self.assertIn("IMPORT_REVIEW_REQUIRED", presentation["body"])
self.assertIn("Output remains disabled", presentation["body"])
def test_packaged_presentation_fails_closed_on_malformed_report(self):
for report in (None, {}, {"ready": True, "issues": [{}]}):
with self.subTest(report=report):
presentation = format_preflight_report(report)
self.assertEqual("blocked", presentation["status"])
self.assertIn("malformed", presentation["body"])
def test_frontend_registers_read_only_tools_action(self):
source = (ADDON / "pirate_ray_jcz" / "frontend.py").read_text(
encoding="utf-8"
)
self.assertIn('ACTION_NAME = "pirateray-import-preflight"', source)
self.assertIn('menu_id="tools"', source)
self.assertIn("inspect_imported_workflow(action_manager.doc)", source)
self.assertNotIn("machine_cmd", source)
if __name__ == "__main__":
unittest.main()