@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.
- package/CHANGELOG.md +97 -2
- package/CODE_OF_CONDUCT.md +83 -0
- package/CONTRIBUTING.md +136 -0
- package/README.md +49 -13
- package/SECURITY.md +55 -0
- package/artifacts/openapi.json +11 -11
- package/artifacts/routes.json +12 -12
- package/artifacts/schema/create-app-oidc-provider-request.schema.json +1 -1
- package/dist/alerts.d.ts +23 -28
- package/dist/alerts.js +23 -29
- package/dist/app-users.d.ts +18 -19
- package/dist/app-users.js +18 -20
- package/dist/apps.d.ts +21 -25
- package/dist/apps.js +42 -52
- package/dist/assets.d.ts +70 -132
- package/dist/assets.js +130 -223
- package/dist/audit.d.ts +14 -15
- package/dist/audit.js +28 -55
- package/dist/client-auth.d.ts +9 -9
- package/dist/client-auth.js +8 -9
- package/dist/common.d.ts +29 -37
- package/dist/common.js +28 -37
- package/dist/config-issues.d.ts +23 -25
- package/dist/config-issues.js +17 -17
- package/dist/config.d.ts +37 -44
- package/dist/config.js +145 -187
- package/dist/errors.d.ts +4 -3
- package/dist/errors.js +83 -116
- package/dist/identity.d.ts +24 -27
- package/dist/identity.js +23 -27
- package/dist/index.d.ts +4 -4
- package/dist/index.js +14 -15
- package/dist/introspection.d.ts +7 -6
- package/dist/introspection.js +6 -6
- package/dist/jobs.d.ts +16 -16
- package/dist/jobs.js +24 -29
- package/dist/mcp.d.ts +14 -15
- package/dist/mcp.js +12 -14
- package/dist/oauth.d.ts +21 -27
- package/dist/oauth.js +33 -43
- package/dist/protocol.d.ts +51 -62
- package/dist/protocol.js +107 -139
- package/dist/realtime.d.ts +53 -68
- package/dist/realtime.js +78 -104
- package/dist/rest.d.ts +183 -244
- package/dist/rest.js +305 -399
- package/dist/routes.d.ts +4 -3
- package/dist/routes.js +33 -32
- 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
|
|
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
|
-
*
|
|
20
|
-
* them.
|
|
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
|
|
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
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* `
|
|
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
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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
|
|
97
|
-
*
|
|
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
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
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
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
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
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
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
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
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
|
|
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.
|
|
192
|
-
*
|
|
193
|
-
*
|
|
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`,
|
|
245
|
-
*
|
|
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
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
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
|
|
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
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
* **How "every" is enforced
|
|
274
|
-
*
|
|
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"
|
|
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.
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
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.
|
|
347
|
-
*
|
|
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
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
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
|
|
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
|
|
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
|
|
395
|
-
* decision with a cost.**
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
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
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
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
|
|
614
|
-
*
|
|
615
|
-
*
|
|
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
|
|
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
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
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
|
|
813
|
-
*
|
|
814
|
-
*
|
|
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:
|
|
988
|
-
*
|
|
989
|
-
*
|
|
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
|
-
*
|
|
1170
|
-
*
|
|
1171
|
-
*
|
|
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.
|
|
1257
|
-
*
|
|
1258
|
-
*
|
|
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.**
|
|
1328
|
-
*
|
|
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.
|
|
1332
|
-
*
|
|
1333
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1624
|
-
*
|
|
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
|
|
1663
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
1690
|
-
*
|
|
1691
|
-
*
|
|
1692
|
-
*
|
|
1693
|
-
*
|
|
1694
|
-
*
|
|
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.
|
|
1747
|
-
*
|
|
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
|
|
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:
|
|
1776
|
-
*
|
|
1777
|
-
*
|
|
1778
|
-
*
|
|
1779
|
-
*
|
|
1780
|
-
*
|
|
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
|
-
*
|
|
1827
|
-
*
|
|
1828
|
-
*
|
|
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
|
|
1813
|
+
* A camera the robot exposes.
|
|
1855
1814
|
*
|
|
1856
|
-
* `width`/`height`/`fps`/`bitrate_kbps` are not cosmetic:
|
|
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
|
|
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
|
|
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
|
|
1954
|
-
*
|
|
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
|
-
*
|
|
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(),
|