labcode 0.1.8__tar.gz → 0.3.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 (89) hide show
  1. {labcode-0.1.8 → labcode-0.3.0}/.gitignore +3 -0
  2. {labcode-0.1.8 → labcode-0.3.0}/PKG-INFO +71 -16
  3. {labcode-0.1.8 → labcode-0.3.0}/README.md +67 -12
  4. {labcode-0.1.8 → labcode-0.3.0}/SPECIFICATIONS.md +89 -31
  5. {labcode-0.1.8 → labcode-0.3.0}/examples/README.md +121 -13
  6. {labcode-0.1.8 → labcode-0.3.0}/examples/plate_line.env.yaml +0 -3
  7. labcode-0.3.0/examples/preflight_sila2_env.py +369 -0
  8. {labcode-0.1.8 → labcode-0.3.0}/examples/run_all_sila2_examples.py +8 -4
  9. {labcode-0.1.8 → labcode-0.3.0}/examples/run_sila2_plate_cycle.py +12 -0
  10. labcode-0.3.0/examples/run_sila2_plate_cycle_no_atc.py +398 -0
  11. {labcode-0.1.8 → labcode-0.3.0}/examples/run_sila2_seal.py +16 -0
  12. {labcode-0.1.8 → labcode-0.3.0}/examples/sila2_plate_cycle.wrapped.env.yaml +0 -3
  13. labcode-0.3.0/examples/sila2_plate_cycle_no_atc.boundary.yaml +22 -0
  14. labcode-0.3.0/examples/sila2_plate_cycle_no_atc.workflow.yaml +115 -0
  15. labcode-0.3.0/examples/sila2_plate_cycle_no_atc.wrapped.env.yaml +389 -0
  16. {labcode-0.1.8 → labcode-0.3.0}/examples/sila2_seal.env.yaml +0 -3
  17. {labcode-0.1.8 → labcode-0.3.0}/examples/sila2_seal.wrapped.env.yaml +0 -3
  18. {labcode-0.1.8 → labcode-0.3.0}/labcode/_child.py +11 -4
  19. {labcode-0.1.8 → labcode-0.3.0}/labcode/backend.py +69 -0
  20. {labcode-0.1.8 → labcode-0.3.0}/labcode/dialect.py +62 -2
  21. {labcode-0.1.8 → labcode-0.3.0}/labcode/extension.py +12 -0
  22. {labcode-0.1.8 → labcode-0.3.0}/labcode/probe.py +4 -1
  23. {labcode-0.1.8 → labcode-0.3.0}/labcode/record.py +5 -0
  24. {labcode-0.1.8 → labcode-0.3.0}/labcode/run_cli.py +15 -0
  25. {labcode-0.1.8 → labcode-0.3.0}/labcode/runner.py +4 -0
  26. {labcode-0.1.8 → labcode-0.3.0}/labcode/sila2_instrument.py +3 -3
  27. {labcode-0.1.8 → labcode-0.3.0}/labcode.egg-info/PKG-INFO +71 -16
  28. {labcode-0.1.8 → labcode-0.3.0}/labcode.egg-info/SOURCES.txt +6 -0
  29. {labcode-0.1.8 → labcode-0.3.0}/labcode.egg-info/requires.txt +3 -3
  30. {labcode-0.1.8 → labcode-0.3.0}/labcode.egg-info/scm_file_list.json +6 -0
  31. labcode-0.3.0/labcode.egg-info/scm_version.json +8 -0
  32. {labcode-0.1.8 → labcode-0.3.0}/pyproject.toml +30 -10
  33. {labcode-0.1.8 → labcode-0.3.0}/tests/fixtures/device_script.env.yaml +0 -3
  34. labcode-0.3.0/tests/fixtures/replenishment.env.yaml +51 -0
  35. {labcode-0.1.8 → labcode-0.3.0}/tests/fixtures/reroute_device.env.yaml +0 -3
  36. {labcode-0.1.8 → labcode-0.3.0}/tests/fixtures/reroute_transporter.env.yaml +0 -3
  37. {labcode-0.1.8 → labcode-0.3.0}/tests/fixtures/transport.env.yaml +0 -3
  38. {labcode-0.1.8 → labcode-0.3.0}/tests/test_backend.py +87 -4
  39. {labcode-0.1.8 → labcode-0.3.0}/tests/test_dialect.py +93 -0
  40. {labcode-0.1.8 → labcode-0.3.0}/tests/test_recording.py +0 -1
  41. {labcode-0.1.8 → labcode-0.3.0}/tests/test_run_cli.py +40 -2
  42. labcode-0.1.8/labcode.egg-info/scm_version.json +0 -8
  43. {labcode-0.1.8 → labcode-0.3.0}/.github/workflows/ci.yml +0 -0
  44. {labcode-0.1.8 → labcode-0.3.0}/.github/workflows/publish.yml +0 -0
  45. {labcode-0.1.8 → labcode-0.3.0}/LICENSE +0 -0
  46. {labcode-0.1.8 → labcode-0.3.0}/MANIFEST.in +0 -0
  47. {labcode-0.1.8 → labcode-0.3.0}/examples/outputs/plate_line.boundary.yaml +0 -0
  48. {labcode-0.1.8 → labcode-0.3.0}/examples/outputs/plate_line.observation.yaml +0 -0
  49. {labcode-0.1.8 → labcode-0.3.0}/examples/outputs/plate_line.plan.yaml +0 -0
  50. {labcode-0.1.8 → labcode-0.3.0}/examples/outputs/plate_line.svg +0 -0
  51. {labcode-0.1.8 → labcode-0.3.0}/examples/outputs/sila2_plate_cycle.boundary.yaml +0 -0
  52. {labcode-0.1.8 → labcode-0.3.0}/examples/outputs/sila2_plate_cycle.observation.yaml +0 -0
  53. {labcode-0.1.8 → labcode-0.3.0}/examples/outputs/sila2_plate_cycle.plan.yaml +0 -0
  54. {labcode-0.1.8 → labcode-0.3.0}/examples/outputs/sila2_plate_cycle.svg +0 -0
  55. {labcode-0.1.8 → labcode-0.3.0}/examples/plate_line.boundary.yaml +0 -0
  56. {labcode-0.1.8 → labcode-0.3.0}/examples/plate_line.workflow.yaml +0 -0
  57. {labcode-0.1.8 → labcode-0.3.0}/examples/render_plate_line.py +0 -0
  58. {labcode-0.1.8 → labcode-0.3.0}/examples/render_sila2_plate_cycle.py +0 -0
  59. {labcode-0.1.8 → labcode-0.3.0}/examples/sila2_plate_cycle.boundary.yaml +0 -0
  60. {labcode-0.1.8 → labcode-0.3.0}/examples/sila2_plate_cycle.workflow.yaml +0 -0
  61. {labcode-0.1.8 → labcode-0.3.0}/examples/sila2_seal.boundary.yaml +0 -0
  62. {labcode-0.1.8 → labcode-0.3.0}/examples/sila2_seal.workflow.yaml +0 -0
  63. {labcode-0.1.8 → labcode-0.3.0}/labcode/__init__.py +0 -0
  64. {labcode-0.1.8 → labcode-0.3.0}/labcode/__main__.py +0 -0
  65. {labcode-0.1.8 → labcode-0.3.0}/labcode/cli.py +0 -0
  66. {labcode-0.1.8 → labcode-0.3.0}/labcode/idgen.py +0 -0
  67. {labcode-0.1.8 → labcode-0.3.0}/labcode/objectid.py +0 -0
  68. {labcode-0.1.8 → labcode-0.3.0}/labcode/otel.py +0 -0
  69. {labcode-0.1.8 → labcode-0.3.0}/labcode/otel_sila2.py +0 -0
  70. {labcode-0.1.8 → labcode-0.3.0}/labcode/py.typed +0 -0
  71. {labcode-0.1.8 → labcode-0.3.0}/labcode/sila2.py +0 -0
  72. {labcode-0.1.8 → labcode-0.3.0}/labcode/sila2_commands.py +0 -0
  73. {labcode-0.1.8 → labcode-0.3.0}/labcode.egg-info/dependency_links.txt +0 -0
  74. {labcode-0.1.8 → labcode-0.3.0}/labcode.egg-info/entry_points.txt +0 -0
  75. {labcode-0.1.8 → labcode-0.3.0}/labcode.egg-info/top_level.txt +0 -0
  76. {labcode-0.1.8 → labcode-0.3.0}/setup.cfg +0 -0
  77. {labcode-0.1.8 → labcode-0.3.0}/tests/fixtures/device_script.workflow.yaml +0 -0
  78. {labcode-0.1.8 → labcode-0.3.0}/tests/fixtures/transport.workflow.yaml +0 -0
  79. {labcode-0.1.8 → labcode-0.3.0}/tests/test_cli.py +0 -0
  80. {labcode-0.1.8 → labcode-0.3.0}/tests/test_objectid.py +0 -0
  81. {labcode-0.1.8 → labcode-0.3.0}/tests/test_otel.py +0 -0
  82. {labcode-0.1.8 → labcode-0.3.0}/tests/test_otel_child.py +0 -0
  83. {labcode-0.1.8 → labcode-0.3.0}/tests/test_otel_grpc.py +0 -0
  84. {labcode-0.1.8 → labcode-0.3.0}/tests/test_otel_sila2.py +0 -0
  85. {labcode-0.1.8 → labcode-0.3.0}/tests/test_probe.py +0 -0
  86. {labcode-0.1.8 → labcode-0.3.0}/tests/test_record.py +0 -0
  87. {labcode-0.1.8 → labcode-0.3.0}/tests/test_sila2.py +0 -0
  88. {labcode-0.1.8 → labcode-0.3.0}/tests/test_sila2_commands.py +0 -0
  89. {labcode-0.1.8 → labcode-0.3.0}/tests/test_sila2_instrument.py +0 -0
@@ -12,3 +12,6 @@ venv/
12
12
  # Local working notes, not part of the published project (until explicitly committed).
13
13
  CLAUDE.md
14
14
  dev-notes/
15
+
16
+ # The bench-specific environments name real hosts and ports; this repository is public.
17
+ *.remote.env.yaml
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labcode
3
- Version: 0.1.8
3
+ Version: 0.3.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
@@ -20,9 +20,9 @@ Classifier: Typing :: Typed
20
20
  Requires-Python: >=3.10
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
- Requires-Dist: ofplang-validate>=0.1.3
24
- Requires-Dist: ofplang-schedule>=0.1.3
25
- Requires-Dist: ofplang-run>=0.1.13
23
+ Requires-Dist: ofplang-validate<0.3,>=0.2
24
+ Requires-Dist: ofplang-schedule<0.4,>=0.3
25
+ Requires-Dist: ofplang-run<0.5,>=0.4
26
26
  Provides-Extra: test
27
27
  Requires-Dist: pytest>=7.0; extra == "test"
28
28
  Requires-Dist: opentelemetry-sdk>=1.20; extra == "test"
@@ -98,24 +98,28 @@ repository, and what labcode adds to it in [`SPECIFICATIONS.md`](SPECIFICATIONS.
98
98
 
99
99
  What `lc run` brings of its own, beyond dispatching:
100
100
 
101
- - **the labcode backend** — each device operation's `x-labcode.script` runs
102
- out-of-process on a wall clock (§1.2, §1.3), so a real operation that takes minutes
103
- does not block the replan loop, and an operation that never returns is stopped by
104
- **`op_timeout`** (§1.8; `--op-timeout` / `--no-op-timeout`).
101
+ - **the labcode backend** — each operation's `x-labcode.script` runs out-of-process on a
102
+ wall clock (§1.2–§1.4), so a real operation that takes minutes does not block the
103
+ replan loop, and an operation that never returns is stopped by **`op_timeout`**
104
+ (§1.9; `--op-timeout` / `--no-op-timeout`).
105
+ - **refilling a stock** — where the environment says a replenisher can reach a device, a
106
+ stock that would run out is topped up rather than ending the run: the refill's own
107
+ script runs like any other, holding both machines while it works (§1.4). See
108
+ *Refilling a stock* below.
105
109
  - **the dialect front door** — the environment's `x-labcode` extension is validated
106
110
  before anything runs, on top of the portable-v0 check `lc validate` performs (§1, §2).
107
111
  - **availability probing** — each machine is checked as often as its `probe` policy says,
108
112
  and one that cannot be reached is taken out of the environment the scheduler plans
109
- against, so the run routes around it (§1.5; `--no-probe`).
113
+ against, so the run routes around it (§1.6; `--no-probe`).
110
114
  - **object identity** — the reserved `_id` view key is declared on Object types and minted
111
115
  per object, so a physical thing can be followed through a run (§4).
112
116
  - **`flavor: sila2`** — a script that speaks SiLA2 gets its clients opened around it
113
- (§1.6). The client library itself is the `sila2` extra: `pip install labcode[sila2]`,
117
+ (§1.7). The client library itself is the `sila2` extra: `pip install labcode[sila2]`,
114
118
  installed into whichever interpreter runs the scripts.
115
119
  - **recording a run** — with `--trace`, what the run did is recorded as OpenTelemetry
116
120
  traces: one trace per run, a span per operation, and — measured inside the process that
117
- issued them — a span per SiLA2 connection, per command, and per gRPC call underneath
118
- either. Off by default; the extra is `pip install labcode[otel]`.
121
+ issued them — a span per SiLA2 connection, per command, and per gRPC call each of those
122
+ made. Off by default; the extra is `pip install labcode[otel]`.
119
123
 
120
124
  ## Usage
121
125
 
@@ -139,7 +143,7 @@ described below.
139
143
  lc run <workflow> --env <env>
140
144
  [--boundary DOC] [-o OUT] [--boundary-out FILE] [--observation-out FILE]
141
145
  [--seconds-per-tick S] [--op-timeout S | --no-op-timeout] [--no-probe]
142
- [--trace] [--mission-id ID] [--object-ids seeded|real]
146
+ [--ignore-resources] [--trace] [--mission-id ID] [--object-ids seeded|real]
143
147
  ```
144
148
 
145
149
  - `<workflow>` — the portable v0 workflow: *what* happens.
@@ -149,7 +153,12 @@ lc run <workflow> --env <env>
149
153
  `{spot, view}` descriptor per entry input / final output port. `spot` says where a
150
154
  boundary Object sits; `view` supplies an input's value. A workflow with Object-bearing
151
155
  entry inputs needs one, since each must be placed on a spot and only the operator knows
152
- where the labware is.
156
+ where the labware is. Where a device declares a consumable and some mode draws on it,
157
+ an `inventories: {levels: ...}` section says what each stock holds **at the start of
158
+ the run** — the level later on is never stated, it is worked out from that and what the
159
+ run has done since. It is not echoed into `--boundary-out`, because that document is
160
+ written to be fed back and the next run would take this run's opening stock for its
161
+ own.
153
162
  - `-o OUT` — write the final execution status (spec §6/§7) here; the default is stdout.
154
163
  - `--boundary-out FILE` — write the result boundary: the same schema as `--boundary`, with
155
164
  each produced output's `view` filled in, including the `_id` its Object was minted with
@@ -163,10 +172,13 @@ lc run <workflow> --env <env>
163
172
  completed reads as discrete, observable steps; a demo against a fast mock wants a small
164
173
  value.
165
174
  - `--op-timeout S` / `--no-op-timeout` — how long one operation may run before it is
166
- stopped and failed (§1.8). The default is the environment root's `x-labcode.op_timeout`,
175
+ stopped and failed (§1.9). The default is the environment root's `x-labcode.op_timeout`,
167
176
  else 7200 real seconds. The two forms exclude each other.
168
177
  - `--no-probe` — ignore the environment's `x-labcode.probe` policies and treat every
169
- machine as reachable (§1.5). The documents are still validated.
178
+ machine as reachable (§1.6). The documents are still validated.
179
+ - `--ignore-resources` — switch the consumable model off. The environment's resource
180
+ declarations are still checked for shape but none is applied, so a bench whose devices
181
+ declare stocks nobody is tracking runs without the boundary saying what they held.
170
182
  - `--trace` — record what the run did (see below). Off by default.
171
183
  - `--mission-id ID` — the campaign this run belongs to. Recorded with the run and given no
172
184
  meaning by labcode: several runs may share one, and nothing here reads it back.
@@ -190,6 +202,49 @@ The remaining options tune the replan loop rather than describe the run —
190
202
  `--poll-interval`, `--margin`, `--seed`, `--speed`, `--max-ticks`, `--no-validate` — and
191
203
  are covered by `lc run --help`.
192
204
 
205
+ ### Refilling a stock
206
+
207
+ Where a device declares a consumable and the environment says a replenisher can reach it,
208
+ a stock that would run out is **topped up instead of ending the run**. The procedure goes
209
+ on the `replenishments[]` route — the pair is what has a procedure, while the machine has
210
+ only an address, the same division `transporters` and `transports` have:
211
+
212
+ ```yaml
213
+ replenishers:
214
+ - id: dispenser
215
+ x-labcode:
216
+ connection: { kind: sila2, host: 10.0.0.9, port: 50055, insecure: true }
217
+
218
+ replenishments:
219
+ - replenisher: dispenser
220
+ device: reader
221
+ duration: 4 # ticks: the scheduler's estimate of the visit
222
+ x-labcode:
223
+ script:
224
+ language: python
225
+ code: |
226
+ import time
227
+ time.sleep(80) # real seconds: what the visit actually takes
228
+ ```
229
+
230
+ The script is handed `replenisher`, `device` and the `amounts` the scheduler derived, and
231
+ is expected to put that in. It is **not** handed the duration: a real refill takes as long
232
+ as it takes, so a stand-in says so in its own code — which is why the two numbers above
233
+ are written separately. Like a transport script it returns nothing; it acts.
234
+
235
+ A route with no script runs as a timed visit: both machines are held for the declared
236
+ duration and nothing is commanded. That is a real thing to write (an operator tops the
237
+ stock up while the schedule waits for them) and an easy one to write by accident, so it is
238
+ warned about.
239
+
240
+ `flavor: sila2` is **refused on a refill route** for now: a sila2 script is handed clients,
241
+ and which machine's clients a refill should receive — the replenisher's, or both ends' as a
242
+ transport may ask for — is not settled. Use `python`.
243
+
244
+ A refill holds the device it fills *and* the replenisher filling it, so it never overlaps
245
+ the work it feeds. It is recorded (`--trace`) as a `replenishment` span naming both
246
+ machines.
247
+
193
248
  ### Recording a run
194
249
 
195
250
  `--trace` records what the run did as OpenTelemetry traces. It needs the extra, in the
@@ -54,24 +54,28 @@ repository, and what labcode adds to it in [`SPECIFICATIONS.md`](SPECIFICATIONS.
54
54
 
55
55
  What `lc run` brings of its own, beyond dispatching:
56
56
 
57
- - **the labcode backend** — each device operation's `x-labcode.script` runs
58
- out-of-process on a wall clock (§1.2, §1.3), so a real operation that takes minutes
59
- does not block the replan loop, and an operation that never returns is stopped by
60
- **`op_timeout`** (§1.8; `--op-timeout` / `--no-op-timeout`).
57
+ - **the labcode backend** — each operation's `x-labcode.script` runs out-of-process on a
58
+ wall clock (§1.2–§1.4), so a real operation that takes minutes does not block the
59
+ replan loop, and an operation that never returns is stopped by **`op_timeout`**
60
+ (§1.9; `--op-timeout` / `--no-op-timeout`).
61
+ - **refilling a stock** — where the environment says a replenisher can reach a device, a
62
+ stock that would run out is topped up rather than ending the run: the refill's own
63
+ script runs like any other, holding both machines while it works (§1.4). See
64
+ *Refilling a stock* below.
61
65
  - **the dialect front door** — the environment's `x-labcode` extension is validated
62
66
  before anything runs, on top of the portable-v0 check `lc validate` performs (§1, §2).
63
67
  - **availability probing** — each machine is checked as often as its `probe` policy says,
64
68
  and one that cannot be reached is taken out of the environment the scheduler plans
65
- against, so the run routes around it (§1.5; `--no-probe`).
69
+ against, so the run routes around it (§1.6; `--no-probe`).
66
70
  - **object identity** — the reserved `_id` view key is declared on Object types and minted
67
71
  per object, so a physical thing can be followed through a run (§4).
68
72
  - **`flavor: sila2`** — a script that speaks SiLA2 gets its clients opened around it
69
- (§1.6). The client library itself is the `sila2` extra: `pip install labcode[sila2]`,
73
+ (§1.7). The client library itself is the `sila2` extra: `pip install labcode[sila2]`,
70
74
  installed into whichever interpreter runs the scripts.
71
75
  - **recording a run** — with `--trace`, what the run did is recorded as OpenTelemetry
72
76
  traces: one trace per run, a span per operation, and — measured inside the process that
73
- issued them — a span per SiLA2 connection, per command, and per gRPC call underneath
74
- either. Off by default; the extra is `pip install labcode[otel]`.
77
+ issued them — a span per SiLA2 connection, per command, and per gRPC call each of those
78
+ made. Off by default; the extra is `pip install labcode[otel]`.
75
79
 
76
80
  ## Usage
77
81
 
@@ -95,7 +99,7 @@ described below.
95
99
  lc run <workflow> --env <env>
96
100
  [--boundary DOC] [-o OUT] [--boundary-out FILE] [--observation-out FILE]
97
101
  [--seconds-per-tick S] [--op-timeout S | --no-op-timeout] [--no-probe]
98
- [--trace] [--mission-id ID] [--object-ids seeded|real]
102
+ [--ignore-resources] [--trace] [--mission-id ID] [--object-ids seeded|real]
99
103
  ```
100
104
 
101
105
  - `<workflow>` — the portable v0 workflow: *what* happens.
@@ -105,7 +109,12 @@ lc run <workflow> --env <env>
105
109
  `{spot, view}` descriptor per entry input / final output port. `spot` says where a
106
110
  boundary Object sits; `view` supplies an input's value. A workflow with Object-bearing
107
111
  entry inputs needs one, since each must be placed on a spot and only the operator knows
108
- where the labware is.
112
+ where the labware is. Where a device declares a consumable and some mode draws on it,
113
+ an `inventories: {levels: ...}` section says what each stock holds **at the start of
114
+ the run** — the level later on is never stated, it is worked out from that and what the
115
+ run has done since. It is not echoed into `--boundary-out`, because that document is
116
+ written to be fed back and the next run would take this run's opening stock for its
117
+ own.
109
118
  - `-o OUT` — write the final execution status (spec §6/§7) here; the default is stdout.
110
119
  - `--boundary-out FILE` — write the result boundary: the same schema as `--boundary`, with
111
120
  each produced output's `view` filled in, including the `_id` its Object was minted with
@@ -119,10 +128,13 @@ lc run <workflow> --env <env>
119
128
  completed reads as discrete, observable steps; a demo against a fast mock wants a small
120
129
  value.
121
130
  - `--op-timeout S` / `--no-op-timeout` — how long one operation may run before it is
122
- stopped and failed (§1.8). The default is the environment root's `x-labcode.op_timeout`,
131
+ stopped and failed (§1.9). The default is the environment root's `x-labcode.op_timeout`,
123
132
  else 7200 real seconds. The two forms exclude each other.
124
133
  - `--no-probe` — ignore the environment's `x-labcode.probe` policies and treat every
125
- machine as reachable (§1.5). The documents are still validated.
134
+ machine as reachable (§1.6). The documents are still validated.
135
+ - `--ignore-resources` — switch the consumable model off. The environment's resource
136
+ declarations are still checked for shape but none is applied, so a bench whose devices
137
+ declare stocks nobody is tracking runs without the boundary saying what they held.
126
138
  - `--trace` — record what the run did (see below). Off by default.
127
139
  - `--mission-id ID` — the campaign this run belongs to. Recorded with the run and given no
128
140
  meaning by labcode: several runs may share one, and nothing here reads it back.
@@ -146,6 +158,49 @@ The remaining options tune the replan loop rather than describe the run —
146
158
  `--poll-interval`, `--margin`, `--seed`, `--speed`, `--max-ticks`, `--no-validate` — and
147
159
  are covered by `lc run --help`.
148
160
 
161
+ ### Refilling a stock
162
+
163
+ Where a device declares a consumable and the environment says a replenisher can reach it,
164
+ a stock that would run out is **topped up instead of ending the run**. The procedure goes
165
+ on the `replenishments[]` route — the pair is what has a procedure, while the machine has
166
+ only an address, the same division `transporters` and `transports` have:
167
+
168
+ ```yaml
169
+ replenishers:
170
+ - id: dispenser
171
+ x-labcode:
172
+ connection: { kind: sila2, host: 10.0.0.9, port: 50055, insecure: true }
173
+
174
+ replenishments:
175
+ - replenisher: dispenser
176
+ device: reader
177
+ duration: 4 # ticks: the scheduler's estimate of the visit
178
+ x-labcode:
179
+ script:
180
+ language: python
181
+ code: |
182
+ import time
183
+ time.sleep(80) # real seconds: what the visit actually takes
184
+ ```
185
+
186
+ The script is handed `replenisher`, `device` and the `amounts` the scheduler derived, and
187
+ is expected to put that in. It is **not** handed the duration: a real refill takes as long
188
+ as it takes, so a stand-in says so in its own code — which is why the two numbers above
189
+ are written separately. Like a transport script it returns nothing; it acts.
190
+
191
+ A route with no script runs as a timed visit: both machines are held for the declared
192
+ duration and nothing is commanded. That is a real thing to write (an operator tops the
193
+ stock up while the schedule waits for them) and an easy one to write by accident, so it is
194
+ warned about.
195
+
196
+ `flavor: sila2` is **refused on a refill route** for now: a sila2 script is handed clients,
197
+ and which machine's clients a refill should receive — the replenisher's, or both ends' as a
198
+ transport may ask for — is not settled. Use `python`.
199
+
200
+ A refill holds the device it fills *and* the replenisher filling it, so it never overlaps
201
+ the work it feeds. It is recorded (`--trace`) as a `replenishment` span naming both
202
+ machines.
203
+
149
204
  ### Recording a run
150
205
 
151
206
  `--trace` records what the run did as OpenTelemetry traces. It needs the extra, in the
@@ -12,10 +12,12 @@ conformance validator, run at the `lc run` front door.
12
12
 
13
13
  ## 1. `x-labcode` in an environment (P5)
14
14
 
15
- The extension appears in four places, each answering a different question: on a **process
16
- mode** and on a **transport route** it says *what to run* (a `script`, §1.1–§1.3); on a
17
- **device** and on a **transporter** it says *how to reach the machine* (a `connection`,
18
- §1.4), which is what lets a script be the commands alone (§1.6). Nowhere else — see §1.7.
15
+ The extension answers two different questions, in two kinds of place. On a **process
16
+ mode**, a **transport route** and a **replenishment route** it says *what to run* (a
17
+ `script`, §1.1–§1.4); on a **device**, a **transporter** and a **replenisher** it says
18
+ *how to reach the machine* (a `connection`, §1.5), which is what lets a script be the
19
+ commands alone (§1.7). The division is the same each time: the machine has an address,
20
+ and the thing it does to something else has a procedure. Nowhere else — see §1.8.
19
21
 
20
22
  An environment process mode (§5) may carry an `x-labcode` mapping holding a `script`: the
21
23
  Python that carries out that `(process, mode)`.
@@ -39,19 +41,20 @@ still validates and schedules as plain v0. Only labcode interprets it.
39
41
 
40
42
  ### 1.1 Shape
41
43
 
42
- - `x-labcode` MUST be a mapping. On a process mode or a transport route its only key is
43
- `script`.
44
+ - `x-labcode` MUST be a mapping. On a process mode, a transport route or a replenishment
45
+ route its only key is `script`.
44
46
  - `x-labcode.script`, if present, MUST be a mapping with:
45
47
  - `language`: MUST be `python`.
46
48
  - `code`: MUST be a string (an implementation-provided Python function body).
47
49
  - `flavor` (optional, default `raw`): MUST be `raw` or `sila2` — how `code` is meant to
48
- be run (§1.6). `sila2` is the **recommended** way to drive a SiLA2 lab: the code is the
50
+ be run (§1.7). `sila2` is the **recommended** way to drive a SiLA2 lab: the code is the
49
51
  commands alone and labcode supplies the clients. `raw` is the whole function body,
50
52
  written by its author — the general escape hatch, and what a script that connects for
51
- itself (or speaks something other than SiLA2) uses.
53
+ itself (or speaks something other than SiLA2) uses. On a **replenishment route**
54
+ `sila2` is an error in this version (§1.4).
52
55
  - `endpoints` (**transport routes only**, optional, default `false`): MUST be a boolean —
53
56
  whether this move is also given clients for the devices at either **end** of its route,
54
- not only its `transporter` (§1.6). A process mode may not declare it: a mode's machines
57
+ not only its `transporter` (§1.7). A process mode may not declare it: a mode's machines
55
58
  are the ones it lists.
56
59
 
57
60
  **Unknown keys are an error** — in `x-labcode` at every position, and in the mappings it
@@ -121,12 +124,58 @@ output is verified. Success is "it ran without raising"; an exception is a grace
121
124
  A route with no `x-labcode.script` runs as a plain timed move — the runner's material
122
125
  bookkeeping only, with no device command (a warned no-op for a real move, from != to).
123
126
 
124
- ### 1.4 `x-labcode` on a device or a transporter
127
+ ### 1.4 `x-labcode` on a replenishment route
125
128
 
126
- An environment `devices[]` or `transporters[]` entry may carry an `x-labcode` with two
127
- keys: `connection` **where that machine is**, written once per physical machine rather
128
- than repeated in every script that drives it — and `probe` (§1.5) — whether to check that
129
- it still answers.
129
+ An environment `replenishments[]` route may carry an `x-labcode` with a `script`: the
130
+ Python that physically refills that device from that replenisher (e.g. commanding a
131
+ dispenser). Same shape as §1.1 (`language: python`, string `code`).
132
+
133
+ The procedure lives on the **route**, not on the machine — the same division transports
134
+ and transporters have. A dispenser's address is a property of the dispenser (§1.5); how it
135
+ fills *this* device is a property of the pair.
136
+
137
+ ```yaml
138
+ replenishments:
139
+ - replenisher: dispenser
140
+ device: reader
141
+ duration: 4 # ticks: the scheduler's estimate of the visit
142
+ x-labcode:
143
+ script:
144
+ language: python
145
+ code: |
146
+ import time
147
+ time.sleep(80) # real seconds: what the visit actually takes
148
+ ```
149
+
150
+ **Calling convention (replenishment).** The script runs as a function body with these
151
+ locals: `replenisher`, `device` (the two machines the visit holds) and `amounts` — the
152
+ `{resource: amount}` the scheduler derived, which a planned refill fills to the device's
153
+ capacity. It is **not** given the duration: a real refill takes as long as it takes, and
154
+ the ticks the plan reserved are the scheduler's estimate rather than an instruction, so a
155
+ stand-in states its own time (which is why the two numbers above are written separately).
156
+
157
+ A replenishment script is **side-effect only**, as a transport's is: its return value is
158
+ ignored and no output is verified. An exception is a graceful failure — the refill ends
159
+ `failed` and the run stops, like any activity failure.
160
+
161
+ `flavor: sila2` is **an error** on a replenishment route in this version. A `sila2` script
162
+ is handed clients (§1.7), and which machine's clients a refill should receive — the
163
+ replenisher's, or both ends' as a transport route may ask for — is not settled. Refusing
164
+ says so; running the script without the clients it asked for would not. Use `raw` (the
165
+ default), which may of course connect for itself.
166
+
167
+ A route with no `x-labcode.script` runs as a plain timed visit: both machines are held for
168
+ the declared duration and nothing is commanded. That is a legitimate environment to write
169
+ — an operator tops the stock up while the schedule waits for them — and an easy one to
170
+ write by accident, so it is **warned** about, as a scriptless real move is.
171
+
172
+ ### 1.5 `x-labcode` on a device, a transporter or a replenisher
173
+
174
+ An environment `devices[]`, `transporters[]` or `replenishers[]` entry may carry an
175
+ `x-labcode` with two keys: `connection` — **where that machine is**, written once per
176
+ physical machine rather than repeated in every script that drives it — and `probe` (§1.6)
177
+ — whether to check that it still answers. The three kinds are treated alike because they
178
+ are alike: each is a machine with an address that a run may find unreachable.
130
179
 
131
180
  ```yaml
132
181
  devices:
@@ -165,7 +214,7 @@ a script uses it.
165
214
  one.
166
215
 
167
216
  A transport that declares `endpoints: true` is also handed the clients of the devices at
168
- either **end** of its route (§1.6), but those are *not* required to declare a `connection`:
217
+ either **end** of its route (§1.7), but those are *not* required to declare a `connection`:
169
218
  a route through a plain holding location is ordinary, and the end without an address is
170
219
  simply not connected to (a **warning** when *neither* end has one, since then the request
171
220
  does nothing). The transporter is the one that must be reachable, because it is the machine
@@ -176,7 +225,7 @@ honoured.
176
225
  Declaring a `connection` on a device no script connects to is allowed — it is how an
177
226
  environment is prepared before the scripts that use it are written.
178
227
 
179
- ### 1.5 Availability — `probe`
228
+ ### 1.6 Availability — `probe`
180
229
 
181
230
  A machine that stops answering should not keep receiving work. A `probe` policy asks labcode
182
231
  to check the machines it knows how to reach, and to tell the scheduler about the ones it
@@ -263,7 +312,7 @@ belongs — as the operation that tried to command it failing.
263
312
  document is still validated, so an environment that is wrong about probing stays wrong).
264
313
  Each machine whose reachability changes is reported on stderr.
265
314
 
266
- ### 1.6 Calling convention (`flavor: sila2`)
315
+ ### 1.7 Calling convention (`flavor: sila2`)
267
316
 
268
317
  A `sila2` script is the **commands alone**: labcode opens a client to each of the
269
318
  operation's machines, runs the code with them in scope, and closes them afterwards. On top
@@ -271,7 +320,7 @@ of the input ports of §1.2 (or the transport locals of §1.3), the code sees:
271
320
 
272
321
  | name | meaning |
273
322
  |---|---|
274
- | `sila2_clients` | the clients by **machine id**, in the order the operation holds its machines: a mode's `devices[]` order, or — for a transport — its `transporter`, followed by the devices at either **end of the route** when it declares `endpoints: true` |
323
+ | `sila2_clients` | the clients by **machine id**, in the order the operation names its machines: a mode's `devices[]` order, or — for a transport — its `transporter`, followed by the devices at either **end of the route** when it declares `endpoints: true`. Named, not held: a mode declaring `device_access: false` (ofplang-schedule §4.4.2) rests on its devices rather than occupying them, and its script is still handed their clients |
275
324
  | `sila2_client` | the first of them — for a transport always its `transporter`; the one name a single-machine operation needs |
276
325
 
277
326
  ```yaml
@@ -371,7 +420,7 @@ equally available to a `raw` script, and stays visible in the code that depends
371
420
  is to turn a hang into a diagnosable failure.
372
421
  - **It is the inner of two limits.** This one is per command, chosen by the script that
373
422
  knows what it is waiting for, and its failure can name the command that hung. The outer
374
- one (§1.8) is per operation and lab-wide, and catches the hangs no script is watching
423
+ one (§1.9) is per operation and lab-wide, and catches the hangs no script is watching
375
424
  for. The outer default is looser than this one, so where both apply this is what fires.
376
425
  - **Its timeout is in real seconds**, and is unrelated to the mode's `duration` — which is
377
426
  an *estimate*, in environment time, for scheduling. A schedule's estimate is not a
@@ -381,11 +430,11 @@ equally available to a `raw` script, and stays visible in the code that depends
381
430
  - A `sila2` script is only interpreted where the dialect is — in an environment
382
431
  `x-labcode`. A workflow's own `script` (v0 §22) has no `flavor`.
383
432
 
384
- ### 1.7 Where an `x-labcode` may appear
433
+ ### 1.8 Where an `x-labcode` may appear
385
434
 
386
435
  The positions of §1 are the only ones: the environment **root** (`probe` defaults and
387
- `op_timeout`), `processes.<p>.modes[]`, `transports[]`, `devices[]` and
388
- `transporters[]`. An `x-labcode`
436
+ `op_timeout`), `processes.<p>.modes[]`, `transports[]`, `replenishments[]`, `devices[]`,
437
+ `transporters[]` and `replenishers[]`. An `x-labcode`
389
438
  anywhere else in the environment — on a process, beside `time` — is an **error**, as is a
390
439
  key at a position that does not define it (a `connection` at the root, a `probe` on a mode).
391
440
  Nothing would read it, and `ofplang-schedule` tolerates an `x-` key at *every* position
@@ -395,7 +444,7 @@ This rule covers the environment only. An `x-labcode` in the **workflow** is not
395
444
  that document is portable v0, read by other implementations, and what extension keys it
396
445
  carries is not labcode's business.
397
446
 
398
- ### 1.8 Operation timeout — `op_timeout`
447
+ ### 1.9 Operation timeout — `op_timeout`
399
448
 
400
449
  How long **one operation** may run before labcode stops waiting for it, in **real
401
450
  seconds**. It lives at the environment root and nowhere else:
@@ -410,7 +459,7 @@ x-labcode:
410
459
  way to say "no limit" and is an error; a machine may not declare one (a per-machine key
411
460
  is an unknown key, §1.1).
412
461
  - **One value for the whole lab.** The fine-grained waits belong to the scripts, which know
413
- what they are waiting for (`settle`, §1.6.1); this value only has to clear the longest
462
+ what they are waiting for (`settle`, §1.7.1); this value only has to clear the longest
414
463
  operation the lab legitimately runs. Its default (7200 s) is twice the `settle` default,
415
464
  so where both apply the inner one — which can name the command — fires first.
416
465
  - **The clock is real seconds**, from the moment the operation starts, covering everything
@@ -422,13 +471,13 @@ x-labcode:
422
471
  failure — the run stops, the status document is written, the reason is reported, the exit
423
472
  code is 1 — which is the point: without a limit, an instrument that stops answering
424
473
  leaves a run polling with *no* status document and no reason at all.
425
- - **A timeout is not a cancel**, exactly as in §1.6.1: nothing here can stop a command the
474
+ - **A timeout is not a cancel**, exactly as in §1.7.1: nothing here can stop a command the
426
475
  instrument has already accepted. It keeps running, and the state that leaves behind —
427
476
  including material a transport was part way through moving — is the operator's to
428
477
  restore. The run stops there, so labcode's own picture of the lab is not relied on
429
478
  afterwards.
430
479
  - The machine that hung is **not** treated as unavailable: `op_timeout` does not add it to
431
- the down machines (§1.5), because "not answering" is not "not there", and re-routing work
480
+ the down machines (§1.6), because "not answering" is not "not there", and re-routing work
432
481
  onto other machines while this one is still physically running its command would make the
433
482
  lab less consistent, not more.
434
483
  - `lc run` overrides it for one run: `--op-timeout SECONDS`, or `--no-op-timeout` for no
@@ -450,6 +499,11 @@ For a dispatched `(process, mode)`, labcode resolves the code to run in this ord
450
499
  will run as a typed-default no-op. This is allowed — convenient while mocking a device —
451
500
  but `lc run` warns about it, so an unimplemented device is not silently a no-op.
452
501
 
502
+ **Transport and replenishment routes have no such chain.** There is nothing for them to
503
+ fall back to: a workflow describes neither a physical move nor a refill, so the route's
504
+ own `x-labcode.script` is the only source. A route without one runs as a plain timed
505
+ activity (§1.3, §1.4), warned about for the same reason as above.
506
+
453
507
  ## 3. Execution model
454
508
 
455
509
  Each dispatched operation runs in its own child process (real, wall-clock-paced); the
@@ -457,7 +511,7 @@ runner discovers completion by polling, so a multi-minute computation never bloc
457
511
  The advisory `duration` is the scheduler's estimate; the real duration is the script's.
458
512
  A script error (an exception, a wrong/ missing output name, a non-conformant value) is a
459
513
  graceful runtime failure (§22.2): the operation ends `failed` and the run stops. An
460
- operation that never finishes at all ends the same way once it passes `op_timeout` (§1.8) —
514
+ operation that never finishes at all ends the same way once it passes `op_timeout` (§1.9) —
461
515
  polling for completion is not the same as waiting forever for it.
462
516
 
463
517
  Cadence: the nominal poll period is `poll_interval × seconds_per_tick`. labcode defaults
@@ -503,7 +557,7 @@ statement that the cycle was cheap.
503
557
 
504
558
  So: keep the budget comfortably larger than the cycle cost. What the cycle costs is not
505
559
  fixed — replanning grows with the workflow, and a dialect step such as availability probing
506
- (§1.5) can add seconds — so the margin wants to be generous rather than exact. A run whose
560
+ (§1.6) can add seconds — so the margin wants to be generous rather than exact. A run whose
507
561
  recorded times matter (a checked-in example, a comparison against the plan's estimates)
508
562
  needs this to hold; a run that only has to *complete* does not.
509
563
 
@@ -562,9 +616,13 @@ ids per physical Object swaps in `RealUuid4Generator` (via
562
616
 
563
617
  ## 5. Not yet in this version (roadmap)
564
618
 
619
+ - **`flavor: sila2` on a replenishment route** — refused today (§1.4). What has to be
620
+ settled first is which machine's clients a refill script receives: the replenisher's
621
+ alone, or both ends' as a transport route may ask for with `endpoints`. Until then a
622
+ refill that must speak SiLA2 uses a `raw` script and connects for itself.
565
623
  - **A deeper probe** — asking a machine something (a SiLA2 property read) rather than only
566
624
  opening a connection to it, so "answering" can be checked and not just "listening"
567
- (§1.5). It would be an opt-in depth, since it costs a real exchange per check.
625
+ (§1.6). It would be an opt-in depth, since it costs a real exchange per check.
568
626
  - **Probing in parallel** — checking machines concurrently, so a lab with many unreachable
569
- machines does not pay for them one timeout at a time (§1.5).
570
- - **TLS** — the fields a secure connection needs, lifting the restriction in §1.4.
627
+ machines does not pay for them one timeout at a time (§1.6).
628
+ - **TLS** — the fields a secure connection needs, lifting the restriction in §1.5.