Block unresolved LightBurn imports in offline driver preflight

This commit is contained in:
Codex 2026-10-24 12:57:23 +02:00
parent be3fb933c1
commit a9fb945af4
6 changed files with 169 additions and 4 deletions

View file

@ -82,9 +82,11 @@ synthetic tests now exist; see
`docs/research/lightburn-project-mopa-import.md`. It preserves both process
element types and requires an explicit unit contract for normalization. The
bridge binds source layer IDs, uses the host MOPA setters, serializes review
metadata and disables every staged step. It never authorizes encoding. The
next import task is UI/preflight presentation plus independently established
units; absent pulse width and image equivalence remain blockers.
metadata and disables every staged step. Document preflight presents those
blockers and the offline driver rejects them before buffer preparation. It
never authorizes encoding. The next import task is GTK presentation plus an
independently established unit contract; absent pulse width and image
equivalence remain blockers.
If a Windows artifact is available, keep the machine disconnected and run, in
order:

View file

@ -35,6 +35,12 @@ machine, lens, correction and parameter limits before a separate reviewed
promotion path can exist. Rayforge's relevant Step and LaserStep files are now
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
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.
Validation covers unit conversion, absent fields, case sensitivity, duplicate
values, invalid numbers, image modes, mixed layers, exact layer binding,
transactional rollback and malformed XML. Existing packaging, no-device and

View file

@ -0,0 +1,79 @@
"""Present and enforce unresolved PirateRay workflow import blockers."""
from __future__ import annotations
from collections.abc import Iterable, Mapping
from typing import Any
from .workflow_import import METADATA_KEY
class ImportPreflightError(RuntimeError):
"""A staged import is not eligible for job execution."""
def _walk(items: Iterable[Any]):
for item in items:
yield item
children = getattr(item, "children", ())
if children:
yield from _walk(children)
def inspect_imported_workflow(doc: Any) -> dict[str, Any]:
"""Return stable, UI-ready errors for every staged import in a document."""
issues = []
items = getattr(doc, "children", None)
if items is None:
issues.append({
"code": "IMPORT_DOCUMENT_INVALID",
"severity": "error",
"message": "Document tree is unavailable for PirateRay preflight.",
})
return {"ready": False, "issues": issues}
for step in _walk(items):
extra = getattr(step, "extra", None)
if not isinstance(extra, Mapping) or METADATA_KEY not in extra:
continue
record = extra[METADATA_KEY]
name = getattr(step, "name", "Imported workflow step")
if not isinstance(record, Mapping):
issues.append({
"code": "IMPORT_METADATA_INVALID", "severity": "error",
"step": name,
"message": "PirateRay import review data is malformed.",
})
continue
layer_id = record.get("source_layer_id")
if getattr(step, "visible", False):
issues.append({
"code": "IMPORT_OUTPUT_REENABLED", "severity": "error",
"step": name, "source_layer_id": layer_id,
"message": "Imported output was re-enabled before review completed.",
})
blockers = record.get("blockers")
if not isinstance(blockers, list) or not blockers or not all(
isinstance(value, str) and value for value in blockers
):
issues.append({
"code": "IMPORT_METADATA_INVALID", "severity": "error",
"step": name, "source_layer_id": layer_id,
"message": "PirateRay import blockers are missing or malformed.",
})
continue
issues.append({
"code": "IMPORT_REVIEW_REQUIRED", "severity": "error",
"step": name, "source_layer_id": layer_id,
"message": "Imported MOPA settings require review: " + "; ".join(blockers),
"blockers": list(blockers),
})
return {"ready": not issues, "issues": issues}
def assert_no_unresolved_imports(doc: Any) -> None:
"""Fail before an encoded job reaches even the offline session."""
report = inspect_imported_workflow(doc)
if not report["ready"]:
codes = ", ".join(issue["code"] for issue in report["issues"])
raise ImportPreflightError(f"PirateRay import preflight failed: {codes}")

View file

@ -23,6 +23,7 @@ from .offline_session import (
OfflineJCZSession,
OfflineSessionState,
)
from .import_preflight import assert_no_unresolved_imports
from .rayforge_compat import register_driver, unregister_driver
@ -312,7 +313,8 @@ class PirateRayJCZNoDeviceDriver(NoDeviceDriver):
async def run(
self, encoded, doc, ops, on_command_done=None
) -> None:
del doc, ops
del ops
assert_no_unresolved_imports(doc)
self.offline_session.prepare(encoded)
self.offline_session.start()
op_count = encoded.op_map.op_count if encoded.op_map else 0

View file

@ -17,6 +17,7 @@
],
"packaged_files": [
"pirate_ray_jcz/__init__.py",
"pirate_ray_jcz/import_preflight.py",
"pirate_ray_jcz/offline_session.py",
"pirate_ray_jcz/profile_import.py",
"pirate_ray_jcz/project_import.py",

View file

@ -0,0 +1,75 @@
import sys
import unittest
from pathlib import Path
from types import SimpleNamespace
ROOT = Path(__file__).parents[1]
ADDON = ROOT / "spikes" / "rayforge-jcz-no-device-addon"
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.workflow_import import METADATA_KEY
def document(*steps):
return SimpleNamespace(children=[SimpleNamespace(children=list(steps))])
def imported_step(*, visible=False, record=None):
if record is None:
record = {
"status": "staged-review-required",
"source_layer_id": "3",
"blockers": ["machine identity unverified", "unit contract unresolved"],
"encoding_allowed": False,
"hardware_use_allowed": False,
}
return SimpleNamespace(name="Imported contour", visible=visible,
extra={METADATA_KEY: record}, children=[])
class ImportPreflightTests(unittest.TestCase):
def test_plain_document_is_ready(self):
report = inspect_imported_workflow(document(
SimpleNamespace(name="ordinary", visible=True, extra={}, children=[])))
self.assertEqual(report, {"ready": True, "issues": []})
assert_no_unresolved_imports(document())
def test_staged_import_is_presented_and_execution_is_rejected(self):
doc = document(imported_step())
report = inspect_imported_workflow(doc)
self.assertFalse(report["ready"])
self.assertEqual(["IMPORT_REVIEW_REQUIRED"],
[issue["code"] for issue in report["issues"]])
self.assertIn("unit contract unresolved", report["issues"][0]["message"])
with self.assertRaisesRegex(ImportPreflightError, "IMPORT_REVIEW_REQUIRED"):
assert_no_unresolved_imports(doc)
def test_manual_reenable_adds_a_distinct_error(self):
report = inspect_imported_workflow(document(imported_step(visible=True)))
self.assertEqual(
["IMPORT_OUTPUT_REENABLED", "IMPORT_REVIEW_REQUIRED"],
[issue["code"] for issue in report["issues"]],
)
def test_malformed_metadata_and_document_fail_closed(self):
for doc in (
object(),
document(imported_step(record="bad")),
document(imported_step(record={"source_layer_id": "3", "blockers": []})),
):
with self.subTest(doc=doc):
report = inspect_imported_workflow(doc)
self.assertFalse(report["ready"])
self.assertTrue(any("INVALID" in issue["code"] for issue in report["issues"]))
def test_offline_driver_runs_preflight_before_session_prepare(self):
source = (ADDON / "pirate_ray_jcz" / "worker.py").read_text(encoding="utf-8")
preflight = source.index("assert_no_unresolved_imports(doc)")
prepare = source.index("self.offline_session.prepare(encoded)")
self.assertLess(preflight, prepare)
if __name__ == "__main__":
unittest.main()