labcode 0.0.1__tar.gz → 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {labcode-0.0.1 → labcode-0.1.0}/PKG-INFO +25 -8
  2. {labcode-0.0.1 → labcode-0.1.0}/README.md +22 -5
  3. labcode-0.1.0/SPECIFICATIONS.md +135 -0
  4. labcode-0.1.0/examples/README.md +40 -0
  5. labcode-0.1.0/examples/plate_line.env.yaml +86 -0
  6. labcode-0.1.0/examples/plate_line.workflow.yaml +71 -0
  7. labcode-0.1.0/labcode/_child.py +62 -0
  8. labcode-0.1.0/labcode/backend.py +211 -0
  9. {labcode-0.0.1 → labcode-0.1.0}/labcode/cli.py +7 -5
  10. labcode-0.1.0/labcode/dialect.py +140 -0
  11. labcode-0.1.0/labcode/run_cli.py +186 -0
  12. {labcode-0.0.1 → labcode-0.1.0}/labcode.egg-info/PKG-INFO +25 -8
  13. {labcode-0.0.1 → labcode-0.1.0}/labcode.egg-info/SOURCES.txt +16 -1
  14. {labcode-0.0.1 → labcode-0.1.0}/labcode.egg-info/requires.txt +2 -2
  15. labcode-0.1.0/labcode.egg-info/scm_file_list.json +30 -0
  16. labcode-0.1.0/labcode.egg-info/scm_version.json +8 -0
  17. {labcode-0.0.1 → labcode-0.1.0}/pyproject.toml +8 -6
  18. labcode-0.1.0/tests/fixtures/device_script.env.yaml +28 -0
  19. labcode-0.1.0/tests/fixtures/device_script.workflow.yaml +26 -0
  20. labcode-0.1.0/tests/fixtures/transport.env.yaml +42 -0
  21. labcode-0.1.0/tests/fixtures/transport.workflow.yaml +41 -0
  22. labcode-0.1.0/tests/test_backend.py +196 -0
  23. labcode-0.1.0/tests/test_dialect.py +93 -0
  24. labcode-0.1.0/tests/test_run_cli.py +161 -0
  25. labcode-0.0.1/labcode.egg-info/scm_file_list.json +0 -15
  26. labcode-0.0.1/labcode.egg-info/scm_version.json +0 -8
  27. {labcode-0.0.1 → labcode-0.1.0}/.github/workflows/ci.yml +0 -0
  28. {labcode-0.0.1 → labcode-0.1.0}/.github/workflows/publish.yml +0 -0
  29. {labcode-0.0.1 → labcode-0.1.0}/.gitignore +0 -0
  30. {labcode-0.0.1 → labcode-0.1.0}/LICENSE +0 -0
  31. {labcode-0.0.1 → labcode-0.1.0}/labcode/__init__.py +0 -0
  32. {labcode-0.0.1 → labcode-0.1.0}/labcode/__main__.py +0 -0
  33. {labcode-0.0.1 → labcode-0.1.0}/labcode/py.typed +0 -0
  34. {labcode-0.0.1 → labcode-0.1.0}/labcode.egg-info/dependency_links.txt +0 -0
  35. {labcode-0.0.1 → labcode-0.1.0}/labcode.egg-info/entry_points.txt +0 -0
  36. {labcode-0.0.1 → labcode-0.1.0}/labcode.egg-info/top_level.txt +0 -0
  37. {labcode-0.0.1 → labcode-0.1.0}/setup.cfg +0 -0
  38. {labcode-0.0.1 → labcode-0.1.0}/tests/test_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labcode
3
- Version: 0.0.1
3
+ Version: 0.1.0
4
4
  Summary: labcode -- a dialect wrapper over the Object-Flow Programming Language toolchain
5
5
  Author-email: Kazunari Kaizu <kwaizu@gmail.com>
6
6
  License-Expression: MIT
@@ -21,8 +21,8 @@ Requires-Python: >=3.10
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
23
  Requires-Dist: ofplang-validate>=0.1.0
24
- Requires-Dist: ofplang-schedule>=0.1.0
25
- Requires-Dist: ofplang-run>=0.1.0
24
+ Requires-Dist: ofplang-schedule>=0.1.2
25
+ Requires-Dist: ofplang-run>=0.1.6
26
26
  Provides-Extra: test
27
27
  Requires-Dist: pytest>=7.0; extra == "test"
28
28
  Provides-Extra: dev
@@ -43,14 +43,31 @@ ofplang toolchain and exposes it under a single command:
43
43
  ```sh
44
44
  lc validate ... # check a workflow is well-formed portable v0
45
45
  lc schedule ... # compute a schedule for a workflow
46
- lc run ... # execute a workflow (rolling-horizon runner / simulator)
46
+ lc run ... # execute a workflow on the labcode backend
47
47
  ```
48
48
 
49
49
  labcode is where a site-specific dialect and a custom runner (real lab hardware)
50
- are developed on top of the ofplang toolchain. At this version `lc` is a thin
51
- dispatcher that forwards each subcommand to the ofplang siblings **unchanged**;
52
- the dialect and custom-runner behavior will land in later versions, replacing
53
- individual subcommands behind the same interface.
50
+ are developed on top of the ofplang toolchain. `lc validate` and `lc schedule`
51
+ forward to the ofplang siblings unchanged; **`lc run` is the labcode dialect's own
52
+ runner**: it drives the workflow on the labcode backend, running each device
53
+ operation's script supplied in the environment as an `x-labcode.script` extension
54
+ on a process mode — out-of-process on a wall clock, so a long-running real operation
55
+ never blocks the replan loop. See [`SPECIFICATIONS.md`](SPECIFICATIONS.md) for the
56
+ `x-labcode` extension.
57
+
58
+ ```yaml
59
+ # in the execution environment: how a (process, mode) is carried out
60
+ processes:
61
+ measure_od:
62
+ modes:
63
+ - id: v0
64
+ duration: 45
65
+ x-labcode:
66
+ script:
67
+ language: python
68
+ code: |
69
+ return {"od": read_plate(plate)}
70
+ ```
54
71
 
55
72
  ## Install
56
73
 
@@ -10,14 +10,31 @@ ofplang toolchain and exposes it under a single command:
10
10
  ```sh
11
11
  lc validate ... # check a workflow is well-formed portable v0
12
12
  lc schedule ... # compute a schedule for a workflow
13
- lc run ... # execute a workflow (rolling-horizon runner / simulator)
13
+ lc run ... # execute a workflow on the labcode backend
14
14
  ```
15
15
 
16
16
  labcode is where a site-specific dialect and a custom runner (real lab hardware)
17
- are developed on top of the ofplang toolchain. At this version `lc` is a thin
18
- dispatcher that forwards each subcommand to the ofplang siblings **unchanged**;
19
- the dialect and custom-runner behavior will land in later versions, replacing
20
- individual subcommands behind the same interface.
17
+ are developed on top of the ofplang toolchain. `lc validate` and `lc schedule`
18
+ forward to the ofplang siblings unchanged; **`lc run` is the labcode dialect's own
19
+ runner**: it drives the workflow on the labcode backend, running each device
20
+ operation's script supplied in the environment as an `x-labcode.script` extension
21
+ on a process mode — out-of-process on a wall clock, so a long-running real operation
22
+ never blocks the replan loop. See [`SPECIFICATIONS.md`](SPECIFICATIONS.md) for the
23
+ `x-labcode` extension.
24
+
25
+ ```yaml
26
+ # in the execution environment: how a (process, mode) is carried out
27
+ processes:
28
+ measure_od:
29
+ modes:
30
+ - id: v0
31
+ duration: 45
32
+ x-labcode:
33
+ script:
34
+ language: python
35
+ code: |
36
+ return {"od": read_plate(plate)}
37
+ ```
21
38
 
22
39
  ## Install
23
40
 
@@ -0,0 +1,135 @@
1
+ # labcode dialect specification
2
+
3
+ labcode is a dialect of the Object-Flow Programming Language (ofplang). A labcode
4
+ workflow **is** a portable v0 ofplang workflow; the dialect lives entirely in the
5
+ **execution environment** (§5), as an `x-labcode` extension that says *how* each device
6
+ operation is physically carried out. `lc run` drives the workflow on the labcode backend
7
+ (`ofplang.run.SubprocessBackend`), sourcing each operation's script from `x-labcode` and
8
+ running it out-of-process on a wall clock.
9
+
10
+ This document is the reference for the `x-labcode` extension; `labcode.dialect` is its
11
+ conformance validator, run at the `lc run` front door.
12
+
13
+ ## 1. `x-labcode` on a process mode (P5)
14
+
15
+ An environment process mode (§5) may carry an `x-labcode` mapping. In this version it
16
+ holds a single key, `script`: the Python that carries out that `(process, mode)`.
17
+
18
+ ```yaml
19
+ processes:
20
+ measure_od:
21
+ modes:
22
+ - id: v0
23
+ devices: [reader]
24
+ duration: 45 # the scheduler's estimate; real time is the script's own
25
+ x-labcode:
26
+ script:
27
+ language: python # the only supported language
28
+ code: |
29
+ return {"od": read_plate(plate)}
30
+ ```
31
+
32
+ `x-labcode` is tolerated (and ignored) by `ofplang-schedule` (>= 0.1.2): the environment
33
+ still validates and schedules as plain v0. Only labcode interprets it.
34
+
35
+ ### 1.1 Shape
36
+
37
+ - `x-labcode` MUST be a mapping.
38
+ - `x-labcode.script`, if present, MUST be a mapping with:
39
+ - `language`: MUST be `python`.
40
+ - `code`: MUST be a string (an implementation-provided Python function body).
41
+
42
+ ### 1.2 Calling convention (process)
43
+
44
+ The script runs as the body of a function whose parameters are the operation's **input
45
+ port names**, each bound to that port's view value (Pure Data or an Object's view record)
46
+ — as in a v0 §22 `python_script_processes` script. External `import` is allowed; there is
47
+ no sandbox.
48
+
49
+ **Partial outputs.** Unlike v0 §22.2 (which requires the script to return *every* output
50
+ exactly), a labcode process script `return`s only the outputs it **computes** — a subset.
51
+ The backend fills the rest:
52
+
53
+ - an Object output declared in `objects.map` is **carried from its input** (the same
54
+ Object, its view unchanged) — so a pass-through need not restate it;
55
+ - any other unset output gets a **typed default** for its type.
56
+
57
+ The script's returned values override these. Each returned value must conform to its port's
58
+ type, and **returning a name that is not a declared output is an error** (this catches a
59
+ typo'd output name). A §22.2-strict script — one that returns every output explicitly —
60
+ works unchanged.
61
+
62
+ So for `read` (input `plate`, outputs `plate` via `objects.map` + `od`), all three are
63
+ equivalent to returning `{"plate": plate, "od": 0.42}`… except the defaulted forms:
64
+
65
+ ```python
66
+ return {"plate": plate, "od": 0.42} # explicit
67
+ return {"od": 0.42} # plate carried by objects.map
68
+ return {} # plate carried; od defaults to 0.0
69
+ ```
70
+
71
+ ### 1.3 `x-labcode` on a transport route
72
+
73
+ An environment `transports[]` route may carry an `x-labcode` with a `script`: the Python
74
+ that physically carries out that move (e.g. commanding a robot arm). Same shape as §1.1
75
+ (`language: python`, string `code`).
76
+
77
+ ```yaml
78
+ transports:
79
+ - transporter: arm
80
+ from: reader.stage
81
+ to: sealer.stage
82
+ duration: 3
83
+ x-labcode:
84
+ script:
85
+ language: python
86
+ code: |
87
+ grip = "gentle" if (view or {}).get("fragile") else "firm"
88
+ move_plate(from_spot, to_spot, grip=grip)
89
+ ```
90
+
91
+ **Calling convention (transport).** The script runs as a function body with these locals:
92
+ `from_spot`, `to_spot`, `transporter` (the physical route), and `view` — the view value of
93
+ the moved Object. `view` is **best-effort and MAY be `None`** (the runner resolves it from
94
+ the producing arc; when it cannot, it is `None`), so a script that reads it should tolerate
95
+ `None`. A transport script is **side-effect only**: its return value is ignored and no
96
+ output is verified. Success is "it ran without raising"; an exception is a graceful failure
97
+ (the move ends `failed`, no material is moved — the run stops).
98
+
99
+ A route with no `x-labcode.script` runs as a plain timed move — the runner's material
100
+ bookkeeping only, with no device command (a warned no-op for a real move, from != to).
101
+
102
+ ## 2. Code source resolution and exclusivity
103
+
104
+ For a dispatched `(process, mode)`, labcode resolves the code to run in this order:
105
+
106
+ 1. the mode's `x-labcode.script.code` (2 — the labcode device script), else
107
+ 2. the workflow process's own `script.code` (1 — a v0 §22 script process), else
108
+ 3. none — the operation runs as a **typed-default no-op** (its outputs are typed
109
+ defaults; a device not yet scripted).
110
+
111
+ **Exclusivity (error).** A process MUST NOT carry both a workflow `script` (1) and an env
112
+ `x-labcode.script` (2) on any of its modes; that is ambiguous and is rejected.
113
+
114
+ **Typed-default reachability (warning).** A process with neither (1) nor (2) on any mode
115
+ will run as a typed-default no-op. This is allowed — convenient while mocking a device —
116
+ but `lc run` warns about it, so an unimplemented device is not silently a no-op.
117
+
118
+ ## 3. Execution model
119
+
120
+ Each dispatched operation runs in its own child process (real, wall-clock-paced); the
121
+ runner discovers completion by polling, so a multi-minute computation never blocks it.
122
+ The advisory `duration` is the scheduler's estimate; the real duration is the script's.
123
+ A script error (an exception, a wrong/ missing output name, a non-conformant value) is a
124
+ graceful runtime failure (§22.2): the operation ends `failed` and the run stops.
125
+
126
+ Cadence: the effective poll period is `poll_interval × seconds_per_tick`. labcode defaults
127
+ `seconds_per_tick` to ~20 s (so a real op is polled at an observable cadence, not
128
+ sub-second, which would flood the replan loop); `lc run --seconds-per-tick/--speed/
129
+ --poll-interval/--margin` override it.
130
+
131
+ ## 4. Not yet in this version (roadmap)
132
+
133
+ - **Device / transporter `x-labcode`** — connection and availability information
134
+ (e.g. SiLA2 address) consolidated on `devices[]` / `transporters[]`, used for a
135
+ connect/command/disconnect wrapper and for `down_devices` availability probing.
@@ -0,0 +1,40 @@
1
+ # labcode examples
2
+
3
+ ## `plate_line` — an Object-bearing line, driven by environment scripts
4
+
5
+ A `Plate` flows down a three-station line and a measurement is read off it:
6
+
7
+ ```
8
+ load ──[move]──> read ──[move]──> store
9
+ ```
10
+
11
+ - [`plate_line.workflow.yaml`](plate_line.workflow.yaml) is **portable ofplang v0** — it
12
+ says only *what* happens. `read` is **Object-bearing**: the Plate goes in and the *same*
13
+ Plate comes out (`objects.map` — identity preserved), together with a Pure Data
14
+ measurement `od`. The Plate is created inside the workflow, so no run boundary is needed.
15
+ - [`plate_line.env.yaml`](plate_line.env.yaml) is the **labcode environment** — it says
16
+ *how* each step is carried out, as an `x-labcode.script` (see [`../SPECIFICATIONS.md`](../SPECIFICATIONS.md)).
17
+ **Every process mode and every transport route carries a script**: `load` makes the
18
+ Plate, `read` measures `od` (the Plate is carried through automatically by `objects.map`,
19
+ so the script returns only what it computes — labcode's *partial outputs*, see the spec),
20
+ `store` takes it; the transport scripts perform the move and may read the moved Plate's
21
+ `view` (its barcode).
22
+
23
+ labcode runs each script **out-of-process** on a wall clock, discovering completion by
24
+ polling — so a real, slow device operation never blocks the runner.
25
+
26
+ ### Run it
27
+
28
+ ```sh
29
+ lc run examples/plate_line.workflow.yaml --env examples/plate_line.env.yaml --seconds-per-tick 0.2
30
+ ```
31
+
32
+ `--seconds-per-tick` sets the real seconds per environment time tick; it defaults to a
33
+ coarse value suited to real hardware, so a small value here keeps the demo quick. The run
34
+ completes with every activity `completed`; the produced measurement (`od = 0.42`) can be
35
+ written out with `--boundary-out result.yaml`.
36
+
37
+ > The scripts here are mocks (they just return values / reference their locals). Replace a
38
+ > script's body with real device calls — e.g. `robot.move(from_spot, to_spot)` in a
39
+ > transport, or an instrument read in `read` — to drive real hardware; the code may
40
+ > `import` anything the host Python can.
@@ -0,0 +1,86 @@
1
+ # The labcode environment for plate_line.workflow.yaml: it says *how* each step and
2
+ # each move is carried out, as an `x-labcode.script` (labcode runs these out-of-process).
3
+ # The workflow stays portable v0 (`ofplang-schedule` just tolerates the x- keys).
4
+ #
5
+ # Every process mode AND every transport route carries a script here. The process
6
+ # scripts are Object-bearing: `load` returns the new Plate's view; `read` returns the
7
+ # *same* plate it was given plus the measured `od`; `store` takes it (side-effect only).
8
+ # The transport scripts move the plate and may read its view (the barcode).
9
+ time:
10
+ unit: second
11
+
12
+ devices:
13
+ - { id: loader, spots: [stage] }
14
+ - { id: reader, spots: [stage] }
15
+ - { id: sealer, spots: [stage] }
16
+
17
+ transporters:
18
+ - { id: arm }
19
+
20
+ transports:
21
+ - transporter: arm
22
+ from: loader.stage
23
+ to: reader.stage
24
+ duration: 2
25
+ x-labcode:
26
+ script:
27
+ language: python
28
+ code: |
29
+ # from_spot / to_spot / transporter are the physical route; `view` is the
30
+ # moved Plate's view (barcode). Side-effect only -- no return.
31
+ barcode = (view or {}).get("barcode")
32
+ # a real arm would move the plate here; e.g. robot.move(from_spot, to_spot)
33
+
34
+ - transporter: arm
35
+ from: reader.stage
36
+ to: sealer.stage
37
+ duration: 2
38
+ x-labcode:
39
+ script:
40
+ language: python
41
+ code: |
42
+ barcode = (view or {}).get("barcode")
43
+
44
+ processes:
45
+
46
+ load:
47
+ modes:
48
+ - devices: [loader]
49
+ duration: 3
50
+ output_spots: { plate: loader.stage }
51
+ x-labcode:
52
+ script:
53
+ language: python
54
+ code: |
55
+ # Create the Plate's view (its observable data). Returns the one output port.
56
+ return {"plate": {"barcode": "P001"}}
57
+
58
+ read:
59
+ modes:
60
+ - devices: [reader]
61
+ duration: 3
62
+ input_spots: { plate: reader.stage }
63
+ output_spots: { plate: reader.stage } # same spot: the plate is read in place
64
+ x-labcode:
65
+ script:
66
+ language: python
67
+ code: |
68
+ # Measure the optical density. `plate` is the incoming Plate's view; it is
69
+ # carried through automatically (objects.map), so the script returns only
70
+ # what it computes -- `od` (labcode's partial outputs, SPECIFICATIONS.md §1.2).
71
+ return {"od": 0.42}
72
+
73
+ store:
74
+ modes:
75
+ - devices: [sealer]
76
+ duration: 3
77
+ input_spots: { plate: sealer.stage }
78
+ x-labcode:
79
+ script:
80
+ language: python
81
+ code: |
82
+ # `plate` is the incoming Plate's view; storing is side-effect only.
83
+ return {}
84
+
85
+ objective:
86
+ kind: makespan
@@ -0,0 +1,71 @@
1
+ # A small labcode example: a Plate flows down a three-station line and a measurement
2
+ # is read off it. Every step's *how* -- both the device operations and the moves --
3
+ # is supplied by the environment as an `x-labcode.script` (see plate_line.env.yaml);
4
+ # this file is portable ofplang v0 (it says only *what* happens).
5
+ #
6
+ # load ──[move]──> read ──[move]──> store
7
+ #
8
+ # `read` is Object-bearing: it takes the Plate in and passes the *same* Plate out
9
+ # (objects.map -- identity preserved), while also producing a Pure Data measurement
10
+ # `od`. So an Object and a data value flow together (cf. examples/plate_chain in
11
+ # ofplang-run). The Plate is created inside the workflow, so no run boundary is needed.
12
+ spec_version: "0.0"
13
+
14
+ types:
15
+ Plate:
16
+ domain: object
17
+ view:
18
+ barcode: { type: String }
19
+
20
+ processes:
21
+
22
+ # A source device: put a fresh Plate on the line.
23
+ load:
24
+ kind: atomic
25
+ outputs:
26
+ plate: { type: Plate, phase: data }
27
+ objects:
28
+ create: [outputs.plate]
29
+
30
+ # Object-bearing measurement: the Plate goes in and the *same* Plate comes out
31
+ # (objects.map), and its optical density `od` is measured as Pure Data.
32
+ read:
33
+ kind: atomic
34
+ inputs:
35
+ plate: { type: Plate, phase: data }
36
+ outputs:
37
+ plate: { type: Plate, phase: data } # same name as the input: the same plate
38
+ od: { type: Float, phase: data }
39
+ objects:
40
+ map:
41
+ outputs.plate: inputs.plate
42
+
43
+ # A sink device: take the Plate off the line.
44
+ store:
45
+ kind: atomic
46
+ inputs:
47
+ plate: { type: Plate, phase: data }
48
+ objects:
49
+ consume: [inputs.plate]
50
+
51
+ main:
52
+ kind: composite
53
+ inputs: {}
54
+ outputs:
55
+ od: { type: Float, phase: data }
56
+ body:
57
+ nodes:
58
+ - id: Load
59
+ process: load
60
+ - id: Read
61
+ process: read
62
+ state:
63
+ plate: { from: Load.plate } # Object arc (a move is scheduled for it)
64
+ - id: Store
65
+ process: store
66
+ state:
67
+ plate: { from: Read.plate }
68
+ returns:
69
+ od: { from: Read.od }
70
+
71
+ entry: main
@@ -0,0 +1,62 @@
1
+ """labcode's out-of-process child harness (the partial-output dialect).
2
+
3
+ Like ``ofplang.run.simulator._child``, but labcode process scripts follow the labcode
4
+ dialect's **partial** output convention (SPECIFICATIONS.md §1.2): a script returns only the
5
+ outputs it computes, and the backend fills the rest -- carrying an Object output through
6
+ ``objects.map`` (identity preserved) and defaulting the others. So this child does **no**
7
+ output verification; it just returns the script's raw result mapping. The merge, the
8
+ conformance check, and the rejection of any *extra* (undeclared) output name all happen in
9
+ `labcode.backend.LabcodeBackend`.
10
+
11
+ A transport script is side-effect only (its return is ignored). Protocol (JSON on stdin,
12
+ outcome to ``result_path``) is identical to the upstream child; only the process branch
13
+ differs (raw, unverified, vs verified-exactly)."""
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import sys
19
+ import traceback
20
+
21
+ from ofplang.run.simulator import DeviceComputationError, run_python_script
22
+
23
+
24
+ def main() -> int:
25
+ try:
26
+ job = json.load(sys.stdin)
27
+ result_path = job["result_path"]
28
+ except Exception as exc: # cannot even read the job -- a harness-level failure
29
+ print(f"lc child: cannot read job: {exc}", file=sys.stderr)
30
+ traceback.print_exc()
31
+ return 1
32
+
33
+ try:
34
+ raw = run_python_script(job.get("code") or "", job.get("inputs") or {})
35
+ if job.get("kind") == "transport":
36
+ payload: dict = {"outputs": {}} # side-effect only; the return is ignored
37
+ elif not isinstance(raw, dict):
38
+ payload = {"error": {
39
+ "code": "script_output_names",
40
+ "message": f"script process {job.get('process')!r} returned "
41
+ f"{type(raw).__name__}, not a mapping",
42
+ }}
43
+ else:
44
+ # A partial mapping is allowed: the backend merges and verifies it (§1.2).
45
+ payload = {"outputs": raw}
46
+ except DeviceComputationError as exc:
47
+ payload = {"error": {"code": exc.code, "message": str(exc)}}
48
+ except Exception as exc: # a script error run_python_script did not wrap
49
+ payload = {"error": {"code": "script_error", "message": f"{type(exc).__name__}: {exc}"}}
50
+
51
+ try:
52
+ with open(result_path, "w", encoding="utf-8") as fh:
53
+ json.dump(payload, fh)
54
+ except Exception as exc: # cannot deliver the result -- a harness-level failure
55
+ print(f"lc child: cannot write result: {exc}", file=sys.stderr)
56
+ traceback.print_exc()
57
+ return 1
58
+ return 0
59
+
60
+
61
+ if __name__ == "__main__": # pragma: no cover - module entry
62
+ raise SystemExit(main())