@fleetless/contracts 1.0.0 → 1.0.3

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 (49) hide show
  1. package/CHANGELOG.md +97 -2
  2. package/CODE_OF_CONDUCT.md +83 -0
  3. package/CONTRIBUTING.md +136 -0
  4. package/README.md +49 -13
  5. package/SECURITY.md +55 -0
  6. package/artifacts/openapi.json +11 -11
  7. package/artifacts/routes.json +12 -12
  8. package/artifacts/schema/create-app-oidc-provider-request.schema.json +1 -1
  9. package/dist/alerts.d.ts +23 -28
  10. package/dist/alerts.js +23 -29
  11. package/dist/app-users.d.ts +18 -19
  12. package/dist/app-users.js +18 -20
  13. package/dist/apps.d.ts +21 -25
  14. package/dist/apps.js +42 -52
  15. package/dist/assets.d.ts +70 -132
  16. package/dist/assets.js +130 -223
  17. package/dist/audit.d.ts +14 -15
  18. package/dist/audit.js +28 -55
  19. package/dist/client-auth.d.ts +9 -9
  20. package/dist/client-auth.js +8 -9
  21. package/dist/common.d.ts +29 -37
  22. package/dist/common.js +28 -37
  23. package/dist/config-issues.d.ts +23 -25
  24. package/dist/config-issues.js +17 -17
  25. package/dist/config.d.ts +37 -44
  26. package/dist/config.js +145 -187
  27. package/dist/errors.d.ts +4 -3
  28. package/dist/errors.js +83 -116
  29. package/dist/identity.d.ts +24 -27
  30. package/dist/identity.js +23 -27
  31. package/dist/index.d.ts +4 -4
  32. package/dist/index.js +14 -15
  33. package/dist/introspection.d.ts +7 -6
  34. package/dist/introspection.js +6 -6
  35. package/dist/jobs.d.ts +16 -16
  36. package/dist/jobs.js +24 -29
  37. package/dist/mcp.d.ts +14 -15
  38. package/dist/mcp.js +12 -14
  39. package/dist/oauth.d.ts +21 -27
  40. package/dist/oauth.js +33 -43
  41. package/dist/protocol.d.ts +51 -62
  42. package/dist/protocol.js +107 -139
  43. package/dist/realtime.d.ts +53 -68
  44. package/dist/realtime.js +78 -104
  45. package/dist/rest.d.ts +183 -244
  46. package/dist/rest.js +305 -399
  47. package/dist/routes.d.ts +4 -3
  48. package/dist/routes.js +33 -32
  49. package/package.json +12 -7
package/dist/config.js CHANGED
@@ -10,15 +10,14 @@ import { applyError, slug, rosName, rosTypeName, fieldPath, SLUG_RULE, ROS_NAME_
10
10
  */
11
11
  import { alertSeverity } from './alerts.js';
12
12
  /**
13
- * The exposure model (spec §4): what a developer configures per robot, how a
13
+ * The exposure model: what a developer configures per robot, how a
14
14
  * configuration moves from draft to published, and how the cloud reports what
15
15
  * it refuses.
16
16
  *
17
17
  * ## What this schema decides, and what it leaves to the cloud
18
18
  *
19
- * FL-002 names thirteen validation codes and this file implements some of
20
- * them. The line was drawn four times while the format was written and never
21
- * written down, so here it is.
19
+ * The format names thirteen validation codes and this file implements some of
20
+ * them. Here is where the line runs.
22
21
  *
23
22
  * **Decided here** — everything a single entry, plus its own declared types,
24
23
  * answers on its own: `unknown_key` (every object is this file's own
@@ -49,29 +48,26 @@ import { alertSeverity } from './alerts.js';
49
48
  * index. Splitting them would put one rule here and its three siblings there
50
49
  * — the shape this file has twice had to undo.
51
50
  *
52
- * **Every refusal that answers one of the spec's codes carries
51
+ * **Every refusal that answers one of the format's validation codes carries
53
52
  * `params: { code }`** with that code, which zod passes through `safeParse`
54
53
  * untouched. The cloud maps an issue to a code and its repair by reading that
55
54
  * field, never by matching the message prose — a join nobody notices
56
55
  * breaking.
57
56
  *
58
- * Read the sentence narrowly, because a wider reading is false and was
59
- * written here once. Plenty of refusals in this file carry no `params.code`,
60
- * and correctly: the section caps (`parameterMap`'s fifty, `messageMap`'s two
61
- * hundred), the camera device-path rules, and every refusal zod raises on its
62
- * own — `unrecognized_keys` behind `unknown_key`, `too_big` behind
63
- * `invalid_rate`. Those are not spec codes wearing a different hat; the cloud
64
- * reaches them through zod's own issue codes. The one *spec* code with no
65
- * `params` is a reversed pair of bounds — `min_value`/`max_value` on a
66
- * parameter, `y_min`/`y_max` on a chart: `invalid_range` was deleted with
67
- * `expected_range`, and no code replaced it.
57
+ * Read that sentence narrowly. Plenty of refusals in this file carry no
58
+ * `params.code`, and correctly: the section caps (`parameterMap`'s fifty,
59
+ * `messageMap`'s two hundred), the camera device-path rules, and every refusal
60
+ * zod raises on its own — `unrecognized_keys` behind `unknown_key`, `too_big`
61
+ * behind `invalid_rate`. Those are not validation codes wearing a different
62
+ * hat; a consumer reaches them through zod's own issue codes. The one named
63
+ * code with no `params` is a reversed pair of bounds — `min_value`/`max_value`
64
+ * on a parameter, `y_min`/`y_max` on a chart — for which no code exists.
68
65
  *
69
66
  * `robotConfigDoc` carries all six sections — messages, datapoints, actions,
70
- * services, publishers and cameras — plus, since FL-002, the alerts, the
71
- * chart bounds and the camera credentials that used to live outside it.
72
- * Everything configurable about a robot is in this document, and there is one
73
- * door to it. FL-002 rewrote the slug grammar (underscores, not dashes) and
74
- * keyed every section by name; draft/publish and versioning are unchanged.
67
+ * services, publishers and cameras — plus the alerts, the chart bounds and the
68
+ * camera credentials. Everything configurable about a robot is in this
69
+ * document, and there is one door to it. Every section is keyed by name, and
70
+ * the document moves from draft to published under a version.
75
71
  */
76
72
  /**
77
73
  * What every `pattern` in this document means, said in words.
@@ -93,16 +89,12 @@ import { alertSeverity } from './alerts.js';
93
89
  * which is a published artifact other tools validate against and which a person
94
90
  * reads. One constant with two readers, never two strings that happen to agree
95
91
  * — `config-zod-messages.test.ts` asserts the two readings are the same string
96
- * at all 24 pattern positions the document has, because under D3 nothing
97
- * consumes `patternErrorMessage` at runtime and an unwatched second spelling of
98
- * a live rule drifts word for word, forever and invisibly.
92
+ * at every pattern position the document has. An unwatched second spelling of a
93
+ * live rule drifts word for word, forever and invisibly.
99
94
  *
100
- * In the console `patternErrorMessage` is also the live pattern diagnostic
101
- * **until wave 3 lands**: `useMonacoYaml.ts` still passes `validate: true`, so
102
- * between task 7's artifacts and D3 these sentences are what monaco-yaml shows.
103
- * D3 then turns that validation off, and from there the message a developer
104
- * sees comes from this schema's own parser and from the cloud — the same
105
- * sentence, which is the point of there being one.
95
+ * An editor that validates against the published JSON Schema shows
96
+ * `patternErrorMessage`; one that does not shows whatever the server's parser
97
+ * says. They are the same sentence, which is the point of there being one.
106
98
  *
107
99
  * The tense matters because the two states look identical from inside this
108
100
  * file. Whoever reads it after wave 3 should find a claim that was true when
@@ -121,33 +113,27 @@ const DEVICE_PATH_RULE = 'A capture device is a path under `/dev/`, and the char
121
113
  *
122
114
  * **`slug` itself stays plain in `common.ts`, and the reason is blast radius.**
123
115
  * Not metadata loss: `.meta()` on a clone *merges* with the parent's entry per
124
- * key and resolves it lazily, measured against zod 4.4.3 and written up at
125
- * `messageBody`'s own `.meta()` below — a later `description` on a use of
126
- * `slug` would keep the sentence, not drop it.
127
- *
128
- * What that reach would cost was measured instead, by adding the one `.meta()`
129
- * line to `slug` in a copy of `src/` and re-exporting every artifact under each
130
- * schema's own `io`: **42 of the 159 published schema artifacts** would carry
131
- * it, `bridge-hello`, `datapoint-frame`, `snapshot-header` and
132
- * `bridge-camera-state` among them — protocol frames the bridge **vendors**
133
- * under `bridge/test/contracts/schema/`, so rewording one sentence would become
134
- * a re-vendor plus a `SOURCE.md` edit in another repo. As landed the same
135
- * search finds **7**, all config-derived.
136
- *
137
- * Whether `vscode-json-languageservice` honours `patternErrorMessage` on a
138
- * `propertyNames` schema at all is **not measured** — §1.3 measured a value
139
- * position, not a key one. It ships for the same reason as the rest: the
140
- * artifact is read by tools and by people.
116
+ * key and resolves it lazily — a later `description` on a use of `slug` keeps
117
+ * the sentence rather than dropping it.
118
+ *
119
+ * The cost is reach. Annotating `slug` itself puts the sentence into dozens of
120
+ * the published schema artifacts, protocol frames included, so rewording one
121
+ * sentence becomes a change to every consumer that vendors those. Annotating
122
+ * the key here reaches only the configuration schemas, which is where the
123
+ * sentence is useful.
124
+ *
125
+ * Whether a given YAML language service honours `patternErrorMessage` on a
126
+ * `propertyNames` schema is not something this package can promise. It ships
127
+ * for the same reason as the rest: the artifact is read by tools and by people.
141
128
  */
142
129
  const mapKey = slug.meta({ patternErrorMessage: SLUG_RULE });
143
130
  /**
144
131
  * One field, carrying the sentence it says when it is absent.
145
132
  *
146
- * **Three shapes of "this key is not here", and the first version of this
147
- * helper caught one of them.** A walk over every required key of a fully
148
- * populated document — `config-zod-messages.test.ts`, which is the guard that
149
- * found it — says the format has 52 required-key positions and that 14 were
150
- * still answering in zod's words:
133
+ * **Three shapes of "this key is not here", and a helper has to cover all
134
+ * three.** A walk over every required key of a fully populated document —
135
+ * `config-zod-messages.test.ts` — enumerates every required-key position in the
136
+ * format and asserts none of them still answers in zod's words:
151
137
  *
152
138
  * - `invalid_type`, the ordinary case: a string, a number, an object.
153
139
  * - `invalid_value` from a `z.literal` or a `z.enum`. A missing `fleetless:`
@@ -167,10 +153,10 @@ const mapKey = slug.meta({ patternErrorMessage: SLUG_RULE });
167
153
  * **`z.unknown()` needs a wrapper before it can be given a sentence at all.**
168
154
  * It accepts `undefined`, so zod marks the key required and raises its own
169
155
  * `expected nonoptional, received undefined` — an issue it attributes to
170
- * neither the field nor the object, so no error map of ours is consulted
171
- * (measured). `z.nonoptional` puts a schema there that can carry one. The JSON
172
- * Schema and the inferred type are byte-identical either way (measured, zod
173
- * 4.4.3), and a field that is genuinely optional is `.optional()` and is
156
+ * neither the field nor the object, so no error map of ours is consulted.
157
+ * `z.nonoptional` puts a schema there that can carry one. The JSON Schema and
158
+ * the inferred type are identical either way, and a field that is genuinely
159
+ * optional is `.optional()` and is
174
160
  * skipped here — but it is **not** behaviourally free, and that is the one
175
161
  * place this task changed what the format accepts: a required key *present*
176
162
  * holding `undefined` is now refused where zod's internal check accepted it.
@@ -186,11 +172,11 @@ const mapKey = slug.meta({ patternErrorMessage: SLUG_RULE });
186
172
  *
187
173
  * `clone` is the only way to add an `error` to a schema that is already built,
188
174
  * and **it drops the schema's registry entry** — its `description`, its
189
- * `examples`, every annotation this wave added, all of which live in
175
+ * `examples` and every other annotation, all of which live in
190
176
  * `z.globalRegistry` keyed by the schema instance rather than in its
191
- * definition. So the entry is read back and put on the clone. Measured on zod
192
- * 4.4.3: `z.globalRegistry.get` resolves the whole `.meta()` parent chain into
193
- * one object, so what is copied is what the export would have produced, and a
177
+ * definition. So the entry is read back and put on the clone.
178
+ * `z.globalRegistry.get` resolves the whole `.meta()` parent chain into one
179
+ * object, so what is copied is what the export would have produced, and a
194
180
  * later `.meta()` on the result merges with it as it did before. A wrapper that
195
181
  * silently emptied every hover in the format would be the worst available way
196
182
  * to improve one message.
@@ -241,8 +227,8 @@ const saysItIsMissing = (field) => {
241
227
  * The second arm is the discriminated union: zod hands its error map the whole
242
228
  * object and points the path at the discriminator, so `input` is not
243
229
  * `undefined` and the first arm cannot see it. `Object.hasOwn` rather than
244
- * `in`, on this project's own rule — a document's keys are chosen by a
245
- * developer, and `constructor` satisfies the slug grammar.
230
+ * `in`, because a document's keys are chosen by a developer and `constructor`
231
+ * satisfies the slug grammar.
246
232
  */
247
233
  const absent = (issue, key) => issue.input === undefined
248
234
  || (issue.code === 'invalid_union'
@@ -256,37 +242,28 @@ const namesItsAbsence = (shape) => Object.fromEntries(Object.entries(shape).map(
256
242
  * its own name when it is absent.
257
243
  *
258
244
  * **This is not `z.strictObject`** — it is this file's, wrapping it. The
259
- * difference is the second half: `Invalid input: expected object, received
260
- * undefined` was the whole of what a developer was told when a camera had no
261
- * `source:` (design §1.5), naming neither the key nor the fact that it was
262
- * required. monaco-yaml said `Missing property "source".` for the same
263
- * document, and was right to.
245
+ * difference is the second half. Plain zod tells a developer whose camera has
246
+ * no `source:` only `Invalid input: expected object, received undefined`,
247
+ * naming neither the key nor the fact that it was required. A YAML language
248
+ * service says `Missing property "source".` for the same document, and is right
249
+ * to.
264
250
  *
265
251
  * It has to be done a field at a time. zod attributes a missing key to the
266
252
  * **field's own** schema — an `invalid_type` whose input is `undefined` — and
267
- * an `error` on the containing object is never consulted for it; measured on
268
- * zod 4.4.3, an error map on the object saw no such issue at all. So the
253
+ * an `error` on the containing object is never consulted for it. So the
269
254
  * sentence is attached to every field of every shape, here, in one place,
270
- * rather than at the eighteen objects and hundred-odd fields it would
271
- * otherwise have to be remembered at.
272
- *
273
- * **How "every" is enforced, because the first version of this comment said
274
- * "every" and was wrong.** Six of the eighteen objects were written
275
- * `z\n .strictObject({`, so `z.strictObject` never appeared on one line and a
276
- * `grep` for it returned only prose. Twelve conversions read as eighteen, and
277
- * eleven required keys — `datapoints.<slug>.topic` and `.type` among them,
278
- * which is the commonest entry in the whole format — went on reciting the
279
- * sentence §1.5 calls unusable. The claim was in the source, which is what the
280
- * next person reads.
281
- *
282
- * What makes it true now is not this paragraph. It is
255
+ * rather than at each of the objects and fields it would otherwise have to be
256
+ * remembered at.
257
+ *
258
+ * **How "every" is enforced.** Not by this paragraph: a claim in a comment is
259
+ * what the next person reads and not what holds. It is
283
260
  * `config-zod-messages.test.ts`'s *"a required key that is absent names
284
- * itself"*: a walk of the exported schema's `required` arrays against a
261
+ * itself"* — a walk of the exported schema's `required` arrays against a
285
262
  * fully-populated document, `oneOf` branches resolved by their discriminator,
286
- * deleting one key at a time and asserting the message names it. It reaches 52
287
- * positions across 19 objects, and the count comes out of the walk rather than
288
- * off a list — a required key added to the format later is swept the day it
289
- * exists, and an object that skips this helper is red before it is merged.
263
+ * deleting one key at a time and asserting the message names it. The count
264
+ * comes out of the walk rather than off a list, so a required key added to the
265
+ * format later is swept the day it exists, and an object that skips this helper
266
+ * is red before it is merged.
290
267
  */
291
268
  const strictObject = (shape) => z.strictObject(namesItsAbsence(shape));
292
269
  /**
@@ -343,8 +320,8 @@ const describeValues = (values, table) => values.map((value) => table[value]);
343
320
  *
344
321
  * The two syntaxes collide. `defaultSnippets` bodies are inserted as LSP
345
322
  * snippets, where `${1:front}` is a tab stop and `${speed}` is a *variable* —
346
- * and an unknown variable is not left alone. Measured against
347
- * monaco-editor 0.52.2's own `SnippetParser`, which is what the console runs:
323
+ * and an unknown variable is not left alone. Against monaco-editor's own
324
+ * `SnippetParser`:
348
325
  *
349
326
  * | body holds | the editor inserts |
350
327
  * |---|---|
@@ -367,52 +344,45 @@ const param = (name) => `\\\${${name}}`;
367
344
  * `{ battery: … }`, and `battery: ▮` offers the `…`. Both positions are real
368
345
  * and both were silent, but they are **one skeleton**, so each is authored once
369
346
  * as a `Snippet` constant and wrapped here for the section — never copied.
370
- * Two copies of one skeleton is the drift this wave caught three times in three
371
- * reviews: a body inventing a value its sibling had already answered, under a
372
- * label that still agreed. `config-snippets.test.ts` deep-compares the two
373
- * positions rather than trusting this.
347
+ * Two copies of one skeleton drift: a body invents a value its sibling had
348
+ * already answered, under a label that still agrees.
349
+ * `config-snippets.test.ts` deep-compares the two positions rather than
350
+ * trusting this.
374
351
  *
375
352
  * **The slug key belongs to the wrapper, not to the skeleton**, because it
376
353
  * differs per snippet — `battery_voltage` for a plain datapoint, `battery` for
377
354
  * the numeric one. It therefore takes tab stop `${1}`, and an entry body's own
378
355
  * stops are numbered from `${2}` throughout. At the entry position that leaves
379
- * no `${1}` at all, which costs nothing — measured against
380
- * monaco-editor 0.52.2's own `SnippetParser`, the version the console runs: it
356
+ * no `${1}` at all, which costs nothing: monaco-editor's `SnippetParser`
381
357
  * sorts placeholders by index and requires neither that they start at 1 nor
382
358
  * that they be contiguous, so a body of `a: ${2:x}` visits `2` first, and one
383
359
  * of `a: ${2:x}` / `b: ${5:y}` visits `2` then `5`.
384
360
  */
385
361
  const underSlug = (slugKey, snippet) => ({ ...snippet, body: { [slugKey]: snippet.body } });
386
362
  /**
387
- * What an exposed service *is*, in the developer's own words (§17).
363
+ * What an exposed service *is*, in the developer's own words.
388
364
  *
389
365
  * This is what `robot_describe` carries verbatim, so it is read by a model
390
366
  * that has never seen this robot and cannot ask a follow-up question.
391
367
  * `unit` and `range` already say what a number *is*; this says what it
392
368
  * *means*.
393
369
  *
394
- * **It lives on the configuration rather than on the app, and that was a
395
- * decision with a cost.** §17's own wording put the semantic descriptions in
396
- * the MCP app; André moved them here on 2026-08-18 so that a description is
397
- * written once per service and true for every app that reaches the robot,
398
- * beside the other metadata. What is given up is real and should not be
399
- * rediscovered as a bug: **two apps can no longer describe one service
400
- * differently for two audiences.** §17 was reworded in the same wave rather
401
- * than left contradicting this field.
370
+ * **It lives on the configuration rather than on the app, and that is a
371
+ * decision with a cost.** A description written here is written once per
372
+ * service and is true for every app that reaches the robot, beside the other
373
+ * metadata. What is given up is real and should not be rediscovered as a bug:
374
+ * **two apps cannot describe one service differently for two audiences.**
402
375
  *
403
376
  * **`.optional()` and not `.nullable().default(null)`, deliberately.** The
404
377
  * established shape in this file is a default — and every use of it has
405
378
  * added an instance to a known contradiction: `.default()` publishes the
406
379
  * field as **required** in the generated JSON Schema, because after parsing
407
- * it is always present. That is recorded four times over in
408
- * `scripts/export-schemas.ts`, whose fix (`io: 'input'`, applied per schema)
409
- * is a judgement call across roughly sixty schemas plus a re-vendor and a
410
- * re-pin in four repos. W7c's playbook said task 0 would do it; reading the
411
- * measured blast radius — 90 artifacts, 436 deletions for the blanket
412
- * version — said otherwise, at the start of a wave with five people blocked
413
- * on this pin. So the field simply does not create a fifth instance:
414
- * optional is optional in both modes, and *absent* is the single spelling of
415
- * "not described". `.min(1)` keeps the empty string from becoming a second.
380
+ * it is always present. That is recorded in `scripts/export-schemas.ts`, whose
381
+ * remedy (`io: 'input'`, applied per schema) is a judgement call across dozens
382
+ * of schemas and every consumer that vendors them. So this field simply does
383
+ * not add another instance: optional is optional in both modes, and *absent* is
384
+ * the single spelling of "not described". `.min(1)` keeps the empty string from
385
+ * becoming a second.
416
386
  */
417
387
  export const serviceDescription = z.string().min(1).max(2000).optional();
418
388
  /**
@@ -610,9 +580,9 @@ export const parameterSpec = strictObject({
610
580
  * A default must satisfy the same constraints a caller's value must.
611
581
  *
612
582
  * Without this, a default is the one way past bounds that are otherwise
613
- * the enforcement point — the spec calls `min_value`/`max_value` "the
614
- * speed limit that actually holds", enforced in the cloud before anything
615
- * reaches the robot. But a caller who simply omits the parameter gets the
583
+ * the enforcement point. `min_value`/`max_value` are the speed limit that
584
+ * actually holds, enforced in the cloud before anything reaches the robot.
585
+ * But a caller who simply omits the parameter gets the
616
586
  * default, and the bridge fills it at the template walk without
617
587
  * re-checking bounds, deliberately: a second enforcement point there
618
588
  * would be the weaker of two policies. So `{min_value: -1, max_value: 1,
@@ -673,7 +643,7 @@ export const parameterMap = slugKeyed(parameterSpec)
673
643
  defaultSnippets: [underSlug('${1:speed}', PARAMETER_SNIPPET)],
674
644
  });
675
645
  /**
676
- * Slugs no configured entry may take (spec §4.3), across **all five exposure
646
+ * Slugs no configured entry may take, across **all five exposure
677
647
  * sections at once** — slugs are one namespace, so a name reserved here is
678
648
  * reserved everywhere.
679
649
  *
@@ -686,12 +656,12 @@ export const parameterMap = slugKeyed(parameterSpec)
686
656
  * name is the honest half of that: the alternative is a slug the format
687
657
  * accepts and one route silently cannot address.
688
658
  *
689
- * **This constant is the only list.** The cloud's `validation.ts` builds its
690
- * set from it and emits `reserved_slug`; `config-store.ts` reads it for the
691
- * rename target; the console reads it for slug suggestion and repairs. Nothing
692
- * copies the members. Note that it is NOT the enumeration of built-in
693
- * datapoints — the cloud keeps that separately, and it must, now that a
694
- * reserved name exists that no plane serves.
659
+ * **This constant is the only list.** The cloud builds its set from it and
660
+ * emits `reserved_slug`; the rename path reads it for the target; an editor
661
+ * reads it for slug suggestion and repairs. Nothing copies the members. Note
662
+ * that it is NOT the enumeration of built-in datapoints — those are kept
663
+ * separately, and must be, because a reserved name exists that nothing
664
+ * publishes.
695
665
  *
696
666
  * **What it does not do: `robotConfigDoc` does not enforce it.** Reservation is
697
667
  * a semantic check that belongs with the ones that need the robot's context,
@@ -809,10 +779,9 @@ export const datapointAlert = strictObject({
809
779
  * `getInsertTextForProperty` (`yaml.worker.js:8520`) takes
810
780
  * `defaultSnippets[0].body` only when a node carries **exactly one**
811
781
  * snippet, so accepting `condition` from the key list writes the bare key
812
- * here where every other node this wave touched writes its whole block.
813
- * The two stay anyway: the value position — a developer who has written
814
- * `condition:` and pressed ⏎ — is where the question "what goes here?" is
815
- * actually asked, and that is the position this wave exists to answer.
782
+ * here where every other node writes its whole block. The two stay anyway:
783
+ * the value position — a developer who has written `condition:` and pressed
784
+ * ⏎ — is where the question "what goes here?" is actually asked.
816
785
  * Merging them into one would buy back the key completion by deleting the
817
786
  * choice the schema deliberately does not name, which is the worse trade;
818
787
  * anyone tempted to make it should change the key-completion behaviour
@@ -984,9 +953,9 @@ const NUMERIC_DATAPOINT_SNIPPET = {
984
953
  /**
985
954
  * The quotes inside `unit` are part of the inserted text and are not
986
955
  * decoration. A body string is written into the document verbatim, and `%`
987
- * is a YAML directive indicator: measured with `yaml` 2.9.0, `unit: %` is a
988
- * **syntax error** ("Plain value cannot start with directive indicator
989
- * character %") while `unit: "%"` parses to `%`. Nothing between here and
956
+ * is a YAML directive indicator: `unit: %` is a **syntax error** ("Plain
957
+ * value cannot start with directive indicator character %") while
958
+ * `unit: "%"` parses to `%`. Nothing between here and
990
959
  * the buffer quotes a scalar for us.
991
960
  */
992
961
  numeric: { scale: 100, unit: '"%"', decimals: 1 },
@@ -1166,10 +1135,9 @@ export const datapointConfig = strictObject({
1166
1135
  * `z.unknown()`, because a template can be any shape a ROS message can — so
1167
1136
  * nothing below the top of it is checked by the type at all.
1168
1137
  *
1169
- * That gap was measured and missed once already: a top-level `message: null`
1170
- * was refused while `message: { linear: { x: null } }` parsed clean, and a
1171
- * check written to catch exactly this was deleted on the strength of six test
1172
- * cases, none of which reached inside a body.
1138
+ * The gap is easy to miss: a top-level `message: null` is refused while
1139
+ * `message: { linear: { x: null } }` parses clean, and a test suite that never
1140
+ * reaches inside a body cannot tell the two apart.
1173
1141
  *
1174
1142
  * Walked with an explicit stack and a seen-set, not recursion: a YAML anchor
1175
1143
  * can make a template both very deep and genuinely cyclic, and a developer can
@@ -1253,10 +1221,9 @@ export const messageBody = messageTemplate;
1253
1221
  * contract.** This runs inside `publisherConfig`'s failsafe refinement, so a
1254
1222
  * `RangeError: Maximum call stack size exceeded` did not stay here: it
1255
1223
  * propagated out of `safeParse`, which is specified to return a result and
1256
- * not to throw. Measured on the recursive version — fine at 8 000 levels of
1257
- * nesting, throwing at 20 000 — and a flow-style YAML one-liner reaches that
1258
- * in about 120 KB of input. A draft PUT would have answered 500 where it
1259
- * meant 400.
1224
+ * not to throw. A recursive walk survives a few thousand levels of nesting and
1225
+ * throws somewhere above that, which a flow-style YAML one-liner reaches in
1226
+ * about 120 KB of input — so a draft PUT would answer 500 where it meant 400.
1260
1227
  *
1261
1228
  * `seen` is not an optimisation. YAML anchors can express a cycle
1262
1229
  * (`&a { b: *a }`), and the parser resolves an alias to the same object, so
@@ -1324,14 +1291,13 @@ const SHARED_MESSAGE_SNIPPET = {
1324
1291
  * that paragraph would be a second thing to keep true.
1325
1292
  *
1326
1293
  * **How the description gets here is zod behaviour, not something written
1327
- * below.** Measured against zod 4.4.3: `.meta()` on an already-registered
1328
- * schema merges rather than replaces, and the clone resolves the parent's entry
1294
+ * below.** `.meta()` on an already-registered schema merges rather than
1295
+ * replaces, and the clone resolves the parent's entry
1329
1296
  * *lazily* — a clone taken before the parent was registered at all still sees
1330
1297
  * the parent's description afterwards. So no spread is needed and there is no
1331
- * evaluation-order hazard. This was first written as
1332
- * `.meta({ ...messageTemplate.meta(), … })`; dropping the spread was measured
1333
- * to change nothing in the export, and two mechanisms for one description is
1334
- * the shape this file removes rather than adds.
1298
+ * evaluation-order hazard. A defensive `.meta({ ...messageTemplate.meta(), … })`
1299
+ * spread changes nothing in the export and is a second mechanism for one
1300
+ * description, so it is deliberately absent.
1335
1301
  *
1336
1302
  * It is undocumented behaviour all the same, so `config-snippets.test.ts`
1337
1303
  * asserts this node still carries a description and that it is the same string
@@ -1366,7 +1332,7 @@ const ACTION_SNIPPET = {
1366
1332
  },
1367
1333
  };
1368
1334
  /**
1369
- * An action the robot can be asked to perform (spec §4.2, §11.3). At most one
1335
+ * An action the robot can be asked to perform. At most one
1370
1336
  * job runs per action slug; a second call is refused `busy`, and every
1371
1337
  * observer of the slug watches the same job.
1372
1338
  */
@@ -1406,7 +1372,7 @@ const SERVICE_SNIPPET = {
1406
1372
  description: '${4:Resets odometry to the origin.}',
1407
1373
  },
1408
1374
  };
1409
- /** A ROS service call with validated parameters (spec §4.2). */
1375
+ /** A ROS service call with validated parameters. */
1410
1376
  export const serviceConfig = strictObject({
1411
1377
  ros_name: rosName.meta({
1412
1378
  description: 'The ROS service the robot answers on, as an absolute graph name. The call is one request and one reply with no progress in between, so whatever this service does has to finish inside that reply; anything long-running belongs in `actions`.',
@@ -1612,7 +1578,7 @@ export const cameraCredentials = strictObject({
1612
1578
  }],
1613
1579
  });
1614
1580
  /**
1615
- * Where a camera's frames come from (spec §10 names four sources).
1581
+ * Where a camera's frames come from — one of four sources.
1616
1582
  *
1617
1583
  * A discriminated union rather than optional fields, so an impossible camera
1618
1584
  * is **unrepresentable** rather than merely invalid — there is no way to
@@ -1620,8 +1586,8 @@ export const cameraCredentials = strictObject({
1620
1586
  * therefore no validation rule to forget.
1621
1587
  *
1622
1588
  * **Each branch carries its own `defaultSnippets`, rather than one list on the
1623
- * union.** Both placements were measured and both work; this one keeps a
1624
- * label beside the branch it names, so the two cannot drift, and it makes a
1589
+ * union.** Both placements work; this one keeps a label beside the branch it
1590
+ * names, so the two cannot drift, and it makes a
1625
1591
  * fifth source impossible to add without one — `config-snippets.test.ts`
1626
1592
  * walks the exported branches and fails on any that carries none.
1627
1593
  */
@@ -1659,15 +1625,14 @@ export const cameraSource = z.discriminatedUnion('kind', [
1659
1625
  * `rosTypeName` accepts either, publish accepts either, no diagnostic
1660
1626
  * fires anywhere, and the bridge then subscribes with the wrong type and
1661
1627
  * delivers no frames. A snippet supplying a wrong answer where it could
1662
- * have supplied a question is this project's *check that cannot fire*,
1663
- * arriving through a hint the developer trusts.
1628
+ * have supplied a question is a defect arriving through a hint the
1629
+ * developer trusts.
1664
1630
  *
1665
1631
  * `kind: 'ros'` stays a literal, because the branch really does fix it.
1666
1632
  *
1667
- * Measured through the actual pipeline rather than assumed, because
1668
- * choice syntax is the one construct here that three layers must each
1633
+ * Choice syntax is the one construct here that three layers must each
1669
1634
  * pass through unharmed: yaml-language-server's `stringifyObject` emits
1670
- * the body verbatim, and monaco-editor 0.52.2's `SnippetParser` parses
1635
+ * the body verbatim, and monaco-editor's `SnippetParser` parses
1671
1636
  * `${2|a,b|}` into a placeholder carrying both options whose
1672
1637
  * `toString()` — the text on the buffer before anyone chooses — is the
1673
1638
  * first one. So a developer who tabs past this gets a document
@@ -1686,16 +1651,12 @@ export const cameraSource = z.discriminatedUnion('kind', [
1686
1651
  description: 'Selects the RTSP source: this camera then carries `url`, and optionally `transport` and `credentials`.',
1687
1652
  }),
1688
1653
  /**
1689
- * Scheme-constrained deliberately. The playbook drafted `z.string().url()`
1690
- * here and the shipped contract was `z.string().min(1).max(2048)` — nobody
1691
- * recorded the change, and the W6 review found the consequence: the bridge
1692
- * opens these with libraries that honour `file:` and `ftp:`, so an
1693
- * unconstrained URL turns a configuration document into an arbitrary
1694
- * local-file read on the robot, with the two distinct failure codes
1695
- * doubling as a file-existence oracle. Spec §7.6 is ROS-pure exposure with
1696
- * no shell or http features; that rule came back by omission rather than
1697
- * by intent. The bridge re-checks this too — a robot must not become a
1698
- * file server because a validator changed.
1654
+ * Scheme-constrained deliberately. The bridge opens these with libraries
1655
+ * that honour `file:` and `ftp:`, so an unconstrained URL turns a
1656
+ * configuration document into an arbitrary local-file read on the robot,
1657
+ * with the two distinct failure codes doubling as a file-existence oracle.
1658
+ * The bridge re-checks this too — a robot must not become a file server
1659
+ * because a validator changed.
1699
1660
  */
1700
1661
  url: z
1701
1662
  .string()
@@ -1743,8 +1704,8 @@ export const cameraSource = z.discriminatedUnion('kind', [
1743
1704
  /**
1744
1705
  * The host and the path this branch's own snippet body inserts, and the
1745
1706
  * URL its rule sentence names — one answer to "what goes here?", not a
1746
- * third. The sibling `rtsp` url had an `examples` from the first day and
1747
- * this position was the format's only silent URL (§1.1).
1707
+ * third. Every URL position in the format carries an example; a silent
1708
+ * one is the position a developer has to guess at.
1748
1709
  */
1749
1710
  examples: ['http://cam-1.plant.local/video.mjpg'],
1750
1711
  }),
@@ -1769,16 +1730,14 @@ export const cameraSource = z.discriminatedUnion('kind', [
1769
1730
  * on the robot, never by the cloud.
1770
1731
  *
1771
1732
  * Constrained to `/dev/` for the same reason the `rtsp` and `mjpeg` URLs
1772
- * are constrained to their schemes, and it was missed the first time
1773
- * (Momus, W6 verification). The device string reaches
1733
+ * are constrained to their schemes. The device string reaches
1774
1734
  * `cv2.VideoCapture(device)` on the robot, and OpenCV does not restrict
1775
- * itself to devices: measured on cv2 4.5.4, an ordinary local video file
1776
- * opens and its pixels are published to the cloud, and so does
1777
- * `http://127.0.0.1:8899/secret.jpg`. Unconstrained, this field is an
1778
- * arbitrary local-file read *and* an outbound fetch from inside the robot
1779
- * — the §7.6 violation closed for the other two source kinds, reachable
1780
- * through the fourth, because "it is just a device path" read like a
1781
- * reason not to check.
1735
+ * itself to devices: an ordinary local video file opens and its pixels are
1736
+ * published to the cloud, and so does an `http://` URL pointing back inside
1737
+ * the robot's own network. Unconstrained, this field is an arbitrary
1738
+ * local-file read *and* an outbound fetch from inside the robot — the same
1739
+ * hole closed for the other two source kinds, reachable through the fourth,
1740
+ * because "it is just a device path" reads like a reason not to check.
1782
1741
  *
1783
1742
  * Narrower than the URL hole in one respect worth recording: a non-media
1784
1743
  * file and a missing file both fail to open, so this branch never worked
@@ -1823,9 +1782,9 @@ export const cameraSource = z.discriminatedUnion('kind', [
1823
1782
  *
1824
1783
  * The bound lives here once, and `rest.ts`'s `cameraDescriptor` reuses it —
1825
1784
  * the same treatment `rateThrottleHz` got, and for the same reason: the
1826
- * descriptor used to say `snapshot_interval_ms` while the document said
1827
- * seconds, so the cloud converted on one descriptor and not its sibling, with
1828
- * nothing in either file saying so.
1785
+ * two spellings of one interval — milliseconds in a descriptor, seconds in the
1786
+ * document — mean a server converting on one and not the other, with nothing in
1787
+ * either file saying so.
1829
1788
  */
1830
1789
  export const snapshotIntervalSeconds = z.number().int().min(1).max(3600);
1831
1790
  /**
@@ -1851,14 +1810,14 @@ const CAMERA_SNIPPET = {
1851
1810
  },
1852
1811
  };
1853
1812
  /**
1854
- * A camera the robot exposes (spec §10).
1813
+ * A camera the robot exposes.
1855
1814
  *
1856
- * `width`/`height`/`fps`/`bitrate_kbps` are not cosmetic: §10 makes them the
1815
+ * `width`/`height`/`fps`/`bitrate_kbps` are not cosmetic: they are the
1857
1816
  * developer's control over **the robot's own bandwidth**, which is why they
1858
1817
  * live in the configuration rather than in a viewer's request. A viewer never
1859
1818
  * gets to make a robot send more.
1860
1819
  *
1861
- * The two modes are deliberately independent (§10):
1820
+ * The two modes are deliberately independent:
1862
1821
  *
1863
1822
  * - **Snapshot** runs always, at `snapshot_interval_seconds`, whether or not
1864
1823
  * anyone is watching live. The cloud caches the one frame and serves every
@@ -1913,7 +1872,7 @@ const capped = (entry, max, what) => slugKeyed(entry).refine((m) => Object.keys(
1913
1872
  * their own name. A duplicate name is then a YAML syntax error rather than a
1914
1873
  * rule somebody has to write, and the name reads as the entry's heading.
1915
1874
  *
1916
- * Slugs remain ONE namespace across all five exposure sections (§4.1), which
1875
+ * Slugs remain ONE namespace across all five exposure sections, which
1917
1876
  * is what lets a role grant say `{robot, slug}` without naming a kind. That
1918
1877
  * check spans sections and therefore lives in the cloud, not here.
1919
1878
  */
@@ -1950,12 +1909,12 @@ export const robotConfigDoc = strictObject({
1950
1909
  }).optional(),
1951
1910
  });
1952
1911
  /**
1953
- * One thing the cloud has to say about a configuration (spec §11.5: field +
1954
- * violated rule).
1912
+ * One thing the cloud has to say about a configuration: the field, and the
1913
+ * rule it violates.
1955
1914
  *
1956
1915
  * `error` blocks the publish. `warning` does not — an unknown topic is a
1957
- * warning on purpose, because configuring a robot that has never been
1958
- * connected must stay possible (spec §4.1).
1916
+ * warning on purpose, because configuring a robot that has never been connected
1917
+ * must stay possible.
1959
1918
  */
1960
1919
  export const validationIssue = z.object({
1961
1920
  path: z.string().min(1),
@@ -1966,8 +1925,7 @@ export const validationIssue = z.object({
1966
1925
  });
1967
1926
  /**
1968
1927
  * Where a robot's configuration stands — the material for the console's
1969
- * "draft newer than published", "published v2 · applied v1 · bridge offline"
1970
- * (spec §15.2, robot tab 1).
1928
+ * "draft newer than published", "published v2 · applied v1 · bridge offline".
1971
1929
  */
1972
1930
  export const configState = z.object({
1973
1931
  published_version: z.number().int().positive().nullable(),