Characterize Rayforge JCZ device import gaps

This commit is contained in:
Codex 2026-10-19 19:17:17 +02:00
parent 202e756071
commit 131ca4b737
6 changed files with 274 additions and 0 deletions

View file

@ -0,0 +1,53 @@
# Rayforge synthetic LightBurn JCZ device-import spike
- Date: 2026-10-19
- Rayforge commit: `c28918c87f8273dd1d244f5a057addc192807fa5`
- Fixture: synthetic public `.lbdev`; no machine identifiers
- Scope: device-profile importer only, with no discovery or transport
## Result
Pinned Rayforge parses the synthetic `JCZFiber` profile and preserves its
display name, 110 x 110 mm work area and generic home-on-start value. It does
not select a driver because its LightBurn driver map has no `JCZFiber` entry.
The imported `DeviceProfile` also has no representation of the observed
`RequireFrameBeforeStart` setting or the synthetic cut-list `frequency` and
`QPulseWidth` fields. The import is therefore explicitly classified as unsafe
for JCZ use rather than receiving guessed defaults.
The checked result is
`tests/fixtures/rayforge-lightburn-device-import.json`. Two runs produced the
same SHA-256 digest,
`3c26f43be6b06e9b17a5decbe4ba7bb633eb1b8ac7bb0361449d7498a3b9279c`.
## Evidence boundary
The fixture uses field names already recorded by the sanitized LightBurn
device-export analysis. Its values are synthetic examples inside observed
ranges, not a copy of a private export and not certified machine limits or
material settings. It deliberately omits private GUIDs, source paths and
unpublished connection serialization.
This spike characterizes the current importer; it does not implement a driver
mapping. A future `JCZFiber` mapping must target the separately tested
no-transport addon driver and require an exact compatible profile. It must not
silently map `JCZFiber` to a G-code or Ruida driver.
## Reproduce
Run against the pinned Rayforge checkout:
```powershell
python scripts/spike_rayforge_lightburn_device_import.py C:\path\to\rayforge
```
## Next seam
The smallest evidence-backed extension is to recognize `JCZFiber` as a
distinct, fail-closed profile kind, preserve its raw settings with explicit
units/provenance, and associate it only with the external no-device JCZ driver.
Cut-list recipes and project-layer `QPulseWidth` mapping remain gated until a
known-working `.lbrn2` fixture establishes project serialization.
No USB enumeration, controller command, motion, framing or emission occurred.

View file

@ -276,6 +276,12 @@ The next work package should be small enough to review in a few commits:
Hardware work cannot proceed until item 6 is reviewed and the physical setup
is ready.
Item 2 is now covered by the
[synthetic LightBurn JCZ device-import spike](research/rayforge-lightburn-device-import-spike.md):
Rayforge preserves the common name and work-area fields, but maps no
`JCZFiber` driver and drops the JCZ-specific safety and MOPA fields from its
profile model. The fixture and regression test remain transport-free.
## Decision rules
- A simulator test precedes every equivalent hardware behavior.

View file

@ -0,0 +1,111 @@
"""Characterize a synthetic JCZ LightBurn device import in pinned Rayforge.
The fixture contains only public, synthetic values. This spike performs no
device discovery or transport and deliberately does not add a JCZ driver.
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from pathlib import Path
RAYFORGE_COMMIT = "c28918c87f8273dd1d244f5a057addc192807fa5"
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(source: Path, fixture: Path) -> dict[str, object]:
commit = _commit_of(source)
if commit != RAYFORGE_COMMIT:
raise RuntimeError(f"expected Rayforge {RAYFORGE_COMMIT}, found {commit}")
sys.path.insert(0, str(source))
from rayforge.machine.device.lightburn_importer import (
convert_to_profile,
parse_lbdev,
)
raw = parse_lbdev(fixture)
profile, summary = convert_to_profile(fixture)
settings = raw["Settings"]
cut_entry = raw["CutList"][0]
if raw["Type"] != "JCZFiber":
raise AssertionError("synthetic fixture is not a JCZFiber profile")
if profile.machine_config.axis_extents != (110.0, 110.0):
raise AssertionError("work area did not survive import")
if profile.machine_config.driver is not None or summary.driver is not None:
raise AssertionError("pinned Rayforge unexpectedly mapped a JCZ driver")
return {
"rayforge_commit": commit,
"transport": None,
"fixture": fixture.name,
"fixture_provenance": "synthetic public data",
"raw_profile": {
"name": raw["DisplayName"],
"type": raw["Type"],
"work_area_mm": [raw["Width"], raw["Height"]],
"require_frame_before_start": settings[
"RequireFrameBeforeStart"
],
"cut_entry": {
"frequency": cut_entry["frequency"],
"QPulseWidth": cut_entry["QPulseWidth"],
},
},
"mapped_profile": {
"name": profile.meta.name,
"driver": profile.machine_config.driver,
"work_area_mm": list(profile.machine_config.axis_extents),
"home_on_start": profile.machine_config.home_on_start,
},
"gap_matrix": {
"jczfiber_driver_mapped": profile.machine_config.driver is not None,
"require_frame_before_start_mapped": False,
"cut_list_mapped": False,
"frequency_mapped": False,
"q_pulse_width_mapped": False,
"safe_for_jcz_use": False,
},
"conclusion": (
"name and work area survive, but JCZFiber has no driver mapping; "
"RequireFrameBeforeStart, CutList, frequency and QPulseWidth are "
"not represented in the imported Rayforge profile"
),
}
def main() -> None:
root = Path(__file__).parents[1]
parser = argparse.ArgumentParser()
parser.add_argument("source", type=Path, help="pinned Rayforge checkout")
parser.add_argument(
"--fixture",
type=Path,
default=root / "tests" / "fixtures" / "synthetic-jcz-device.lbdev",
)
parser.add_argument("--output", type=Path)
args = parser.parse_args()
result = run(args.source.resolve(), args.fixture.resolve())
rendered = json.dumps(result, indent=2, sort_keys=True) + "\n"
if args.output:
args.output.write_text(rendered, encoding="utf-8", newline="\n")
else:
print(rendered, end="")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,37 @@
{
"conclusion": "name and work area survive, but JCZFiber has no driver mapping; RequireFrameBeforeStart, CutList, frequency and QPulseWidth are not represented in the imported Rayforge profile",
"fixture": "synthetic-jcz-device.lbdev",
"fixture_provenance": "synthetic public data",
"gap_matrix": {
"cut_list_mapped": false,
"frequency_mapped": false,
"jczfiber_driver_mapped": false,
"q_pulse_width_mapped": false,
"require_frame_before_start_mapped": false,
"safe_for_jcz_use": false
},
"mapped_profile": {
"driver": null,
"home_on_start": false,
"name": "Synthetic JCZ MOPA",
"work_area_mm": [
110.0,
110.0
]
},
"raw_profile": {
"cut_entry": {
"QPulseWidth": 100,
"frequency": 40000
},
"name": "Synthetic JCZ MOPA",
"require_frame_before_start": true,
"type": "JCZFiber",
"work_area_mm": [
110,
110
]
},
"rayforge_commit": "c28918c87f8273dd1d244f5a057addc192807fa5",
"transport": null
}

View file

@ -0,0 +1,23 @@
{
"FixtureProvenance": "synthetic public data; not a machine export",
"DeviceList": [
{
"DisplayName": "Synthetic JCZ MOPA",
"Name": "JCZ",
"Type": "JCZFiber",
"Width": 110,
"Height": 110,
"HomeOnStartup": false,
"Settings": {
"RequireFrameBeforeStart": true
},
"CutList": [
{
"Name": "Synthetic MOPA entry",
"frequency": 40000,
"QPulseWidth": 100
}
]
}
]
}

View file

@ -0,0 +1,44 @@
import json
import unittest
from pathlib import Path
ROOT = Path(__file__).parents[1]
RESULT = ROOT / "tests" / "fixtures" / "rayforge-lightburn-device-import.json"
SOURCE = ROOT / "tests" / "fixtures" / "synthetic-jcz-device.lbdev"
class RayforgeLightBurnDeviceImportSpikeTests(unittest.TestCase):
def test_public_fixture_is_synthetic_and_contains_no_private_identity(self):
source = json.loads(SOURCE.read_text(encoding="utf-8"))
self.assertEqual(
"synthetic public data; not a machine export",
source["FixtureProvenance"],
)
rendered = SOURCE.read_text(encoding="utf-8").lower()
self.assertNotIn("serial", rendered)
self.assertNotIn("guid", rendered)
def test_common_profile_fields_survive(self):
result = json.loads(RESULT.read_text(encoding="utf-8"))
self.assertIsNone(result["transport"])
self.assertEqual("JCZFiber", result["raw_profile"]["type"])
self.assertEqual([110, 110], result["mapped_profile"]["work_area_mm"])
self.assertEqual(
"Synthetic JCZ MOPA", result["mapped_profile"]["name"]
)
def test_jcz_specific_fields_fail_closed(self):
result = json.loads(RESULT.read_text(encoding="utf-8"))
gaps = result["gap_matrix"]
self.assertIsNone(result["mapped_profile"]["driver"])
self.assertFalse(gaps["jczfiber_driver_mapped"])
self.assertFalse(gaps["require_frame_before_start_mapped"])
self.assertFalse(gaps["cut_list_mapped"])
self.assertFalse(gaps["frequency_mapped"])
self.assertFalse(gaps["q_pulse_width_mapped"])
self.assertFalse(gaps["safe_for_jcz_use"])
if __name__ == "__main__":
unittest.main()