@fleetless/contracts 1.1.0 → 2.0.0

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 (66) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/artifacts/constants.json +30 -4
  3. package/artifacts/openapi.json +667 -98
  4. package/artifacts/routes.json +97 -6
  5. package/artifacts/schema/apply-error.schema.json +2 -1
  6. package/artifacts/schema/asset-list-response.schema.json +77 -12
  7. package/artifacts/schema/asset-sync-status.schema.json +35 -8
  8. package/artifacts/schema/asset.schema.json +2 -3
  9. package/artifacts/schema/assets-clear-response.schema.json +23 -0
  10. package/artifacts/schema/authorization-server-metadata.schema.json +1 -1
  11. package/artifacts/schema/bridge-asset-progress.schema.json +14 -8
  12. package/artifacts/schema/bridge-config-applied.schema.json +2 -1
  13. package/artifacts/schema/bridge-link-mode.schema.json +36 -0
  14. package/artifacts/schema/bridge-state.schema.json +6 -1
  15. package/artifacts/schema/client-robot-list-item.schema.json +6 -1
  16. package/artifacts/schema/client-robot-list-response.schema.json +6 -1
  17. package/artifacts/schema/cloud-config.schema.json +90 -5
  18. package/artifacts/schema/cloud-hello-ok.schema.json +45 -0
  19. package/artifacts/schema/cloud-ping.schema.json +27 -1
  20. package/artifacts/schema/config-draft-response.schema.json +90 -5
  21. package/artifacts/schema/config-state.schema.json +2 -1
  22. package/artifacts/schema/config-version-response.schema.json +90 -5
  23. package/artifacts/schema/datapoint-config.schema.json +5 -0
  24. package/artifacts/schema/datapoint-frame.schema.json +4 -0
  25. package/artifacts/schema/datapoint-list-response.schema.json +2 -2
  26. package/artifacts/schema/dynamic-client-registration-request.schema.json +1 -1
  27. package/artifacts/schema/dynamic-client-registration-response.schema.json +1 -1
  28. package/artifacts/schema/joint-state-put-request.schema.json +23 -0
  29. package/artifacts/schema/joint-state-put-response.schema.json +24 -0
  30. package/artifacts/schema/oauth-token-request.schema.json +79 -41
  31. package/artifacts/schema/oauth-token-response.schema.json +1 -1
  32. package/artifacts/schema/org-quota-usage-counts.schema.json +0 -5
  33. package/artifacts/schema/org-quota-usage.schema.json +1 -12
  34. package/artifacts/schema/org-quotas.schema.json +1 -7
  35. package/artifacts/schema/robot-config-doc.schema.json +90 -5
  36. package/artifacts/schema/robot-deletion-summary.schema.json +2 -1
  37. package/artifacts/schema/robot-detail-response.schema.json +63 -2
  38. package/artifacts/schema/robot-list-item.schema.json +15 -1
  39. package/artifacts/schema/robot-list-response.schema.json +15 -1
  40. package/artifacts/schema/robot-token-rotate-response.schema.json +15 -0
  41. package/artifacts/schema-outgoing/bridge-asset-progress.schema.json +14 -8
  42. package/artifacts/schema-outgoing/bridge-config-applied.schema.json +2 -1
  43. package/artifacts/schema-outgoing/bridge-link-mode.schema.json +37 -0
  44. package/artifacts/schema-outgoing/datapoint-frame.schema.json +4 -0
  45. package/dist/assets.d.ts +85 -50
  46. package/dist/assets.js +152 -62
  47. package/dist/audit.d.ts +1 -1
  48. package/dist/audit.js +1 -1
  49. package/dist/client-robots.d.ts +2 -0
  50. package/dist/common.d.ts +10 -0
  51. package/dist/common.js +16 -1
  52. package/dist/config.d.ts +69 -1
  53. package/dist/config.js +86 -6
  54. package/dist/errors.d.ts +1 -1
  55. package/dist/errors.js +1 -8
  56. package/dist/index.d.ts +10 -10
  57. package/dist/index.js +5 -5
  58. package/dist/oauth.d.ts +34 -19
  59. package/dist/oauth.js +39 -24
  60. package/dist/protocol.d.ts +150 -71
  61. package/dist/protocol.js +144 -87
  62. package/dist/rest.d.ts +137 -35
  63. package/dist/rest.js +98 -66
  64. package/dist/routes.js +68 -19
  65. package/package.json +1 -1
  66. package/artifacts/schema/bridge-pressure.schema.json +0 -292
@@ -29,7 +29,8 @@
29
29
  "action",
30
30
  "service",
31
31
  "publisher",
32
- "camera"
32
+ "camera",
33
+ "low_bandwidth"
33
34
  ]
34
35
  },
35
36
  "code": {
@@ -0,0 +1,37 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "properties": {
5
+ "type": {
6
+ "type": "string",
7
+ "const": "link_mode"
8
+ },
9
+ "low_bandwidth": {
10
+ "type": "boolean",
11
+ "description": "Whether the mode is active after this transition."
12
+ },
13
+ "reason": {
14
+ "type": "string",
15
+ "enum": [
16
+ "lag",
17
+ "dwell",
18
+ "forced",
19
+ "recovered"
20
+ ],
21
+ "description": "`lag`: the cloud-measured lag crossed the threshold; `dwell`: the bridge-measured queue dwell did; `forced`: `mode: on` or `off`; `recovered`: both measures stayed at or below the exit threshold."
22
+ },
23
+ "at_ms": {
24
+ "type": "integer",
25
+ "minimum": 0,
26
+ "maximum": 9007199254740991,
27
+ "description": "Bridge time of the transition, epoch milliseconds."
28
+ }
29
+ },
30
+ "required": [
31
+ "type",
32
+ "low_bandwidth",
33
+ "reason",
34
+ "at_ms"
35
+ ],
36
+ "additionalProperties": false
37
+ }
@@ -17,6 +17,10 @@
17
17
  "type": "integer",
18
18
  "minimum": 0,
19
19
  "maximum": 9007199254740991
20
+ },
21
+ "backfill": {
22
+ "description": "true when the sample was captured while the bridge was disconnected and is being replayed after the reconnect. The cloud keeps such a sample out of its lag measure; absent means live.",
23
+ "type": "boolean"
20
24
  }
21
25
  },
22
26
  "required": [
package/dist/assets.d.ts CHANGED
@@ -26,24 +26,27 @@ import { z } from 'zod';
26
26
  *
27
27
  * ---
28
28
  *
29
- * **`texture` is its own member of `assetKind` and not `other`.**
30
- *
31
- * Filing textures under `other` costs a real capability: a client that
32
- * renders a robot must know, from the asset list alone and before fetching
33
- * anything, which bytes it has to pre-fetch. Every load in the browser goes
34
- * through the SDK with the bearer token — there is no lazy second fetch a
35
- * renderer can make on its own account — so "what must be in memory before
36
- * anything renders" is a question the list has to be able to answer.
37
- *
38
- * A `texture` is an image referenced by the URDF's own `<material><texture>`
39
- * **or** by a mesh file internally (a `.dae`'s `<init_from>`). Both are
40
- * surfaces; neither is geometry; both must be resolvable by name.
29
+ * **Three kinds, and every one of them is a thing a renderer does something
30
+ * with.**
31
+ *
32
+ * A `urdf` is the description, a `mesh` is geometry, a `texture` is an image
33
+ * referenced by the URDF's own `<material><texture>` **or** by a mesh file
34
+ * internally (a `.dae`'s `<init_from>`). A client that renders a robot must
35
+ * know, from the asset list alone and before fetching anything, which bytes
36
+ * it has to pre-fetch. Every load in the browser goes through the SDK with
37
+ * the bearer token — there is no lazy second fetch a renderer can make on its
38
+ * own account — so "what must be in memory before anything renders" is a
39
+ * question the list has to be able to answer, and a kind meaning *something
40
+ * else* answers it for nothing.
41
+ *
42
+ * There was a fourth, `other`, and **no producer ever sent it**: the bridge
43
+ * classifies what it uploads and has only these three to choose from. It was
44
+ * a slot for a file nobody had, which every consumer still had to branch on.
41
45
  */
42
46
  export declare const assetKind: z.ZodEnum<{
43
47
  urdf: "urdf";
44
48
  mesh: "mesh";
45
49
  texture: "texture";
46
- other: "other";
47
50
  }>;
48
51
  export type AssetKind = z.infer<typeof assetKind>;
49
52
  /**
@@ -79,7 +82,6 @@ export declare const asset: z.ZodObject<{
79
82
  urdf: "urdf";
80
83
  mesh: "mesh";
81
84
  texture: "texture";
82
- other: "other";
83
85
  }>;
84
86
  name: z.ZodString;
85
87
  media_type: z.ZodString;
@@ -175,32 +177,41 @@ export type AssetSyncResponse = z.infer<typeof assetSyncResponse>;
175
177
  * field is additive; adding an enum member is not.
176
178
  */
177
179
  /**
178
- * **The upload ceiling both sides read.**
180
+ * **How much asset storage a robot has: one number, the same for every robot.**
179
181
  *
180
- * It is stated once, here, rather than once in the producer and once in the
181
- * cloud. A limit the sender guesses and the receiver enforces is not a limit;
182
- * it is two numbers that agree until one of them changes.
182
+ * It replaces two dials that answered neither question well — a per-file
183
+ * ceiling, which refused a single large mesh while saying nothing about the
184
+ * robot's total, and a per-organisation quota, which said nothing about any
185
+ * one robot. A developer syncing a robot asks *will this robot's description
186
+ * fit*, and only this number answers it.
183
187
  *
184
- * The bridge reads it **before** it reads a file into memory, and the cloud
185
- * enforces it. Without a shared number a producer cannot refuse an oversized
186
- * mesh without first buffering the whole of it.
188
+ * **Content addressing still stores a shared blob once, and each robot's
189
+ * counter still carries it.** Two robots referencing the same mesh cost one
190
+ * object and count against both stores, so a robot's number never depends on
191
+ * another robot's — which is the only way "used of 1 GB" means anything on a
192
+ * page about one robot.
187
193
  *
188
- * **It applies per file, not per sync.** Eight meshes of 30 MiB each pass; one
189
- * file of 65 MiB does not. Reading it as a ceiling on a whole transfer means
190
- * planning against a bound that does not exist — the total of a sync counts
191
- * against the organisation's storage quota, which is a different number in a
192
- * different place.
194
+ * Stated here, and in `constants.json`, because the bridge cannot import this
195
+ * package and the cloud enforces the check: a limit the sender guesses and
196
+ * the receiver enforces is two numbers that agree until one of them moves.
197
+ */
198
+ export declare const ROBOT_ASSET_STORE_BYTES = 1000000000;
199
+ /**
200
+ * What a full store tells the caller — the same discipline as `job_queue_full`
201
+ * and `publisher_busy`: a refusal that names a state and no number leaves the
202
+ * caller unable to decide anything.
193
203
  *
194
- * A robot whose meshes exceed this is not a contract question but a question
195
- * about storage, transfer time and quota, and it is answered by raising the
196
- * number here, in one place, for both sides.
204
+ * Three numbers, because two of them answer different questions. `store_bytes`
205
+ * and `used_bytes` say how much room there is; `size_bytes` says what did not
206
+ * fit. Without the pair a developer cannot tell whether to delete something or
207
+ * to shrink the mesh, and "the store is full" answers neither.
197
208
  */
198
- export declare const ASSET_UPLOAD_MAX_BYTES: number;
199
- export declare const assetTooLargeDetails: z.ZodObject<{
200
- limit_bytes: z.ZodNumber;
209
+ export declare const assetStoreRefusedDetails: z.ZodObject<{
210
+ store_bytes: z.ZodNumber;
211
+ used_bytes: z.ZodNumber;
201
212
  size_bytes: z.ZodNumber;
202
213
  }, z.core.$strip>;
203
- export type AssetTooLargeDetails = z.infer<typeof assetTooLargeDetails>;
214
+ export type AssetStoreRefusedDetails = z.infer<typeof assetStoreRefusedDetails>;
204
215
  /**
205
216
  * Why one reference did not make it into the store.
206
217
  *
@@ -214,10 +225,17 @@ export type AssetTooLargeDetails = z.infer<typeof assetTooLargeDetails>;
214
225
  * reconciliation may treat as gone.
215
226
  * - **`upload_failed`** — the bytes exist and the transfer did not succeed.
216
227
  * **Transient.** The asset is still wanted; a later sync will carry it.
217
- * - **`refused`** — never attempted, because a producer-side ceiling was hit
218
- * (for example a `.dae` carrying more internal references than one file or
219
- * one sync will report). **Transient in the same sense**: nothing is known
220
- * to be missing, only unexamined.
228
+ * - **`refused`** — never attempted. Either the robot's asset store had no
229
+ * room for it, in which case `details` carries the three numbers, or a
230
+ * producer-side ceiling was hit (a `.dae` carrying more internal references
231
+ * than one file or one sync will report), in which case it does not.
232
+ * **Transient in the same sense**: nothing is known to be missing, only
233
+ * unexamined.
234
+ *
235
+ * There was a fourth, `too_large`, for a file over a per-file ceiling. That
236
+ * ceiling is gone — a robot has one store and nothing is refused for its own
237
+ * size — so the kind had no producer left and one fewer thing to branch on is
238
+ * the whole of the gain.
221
239
  *
222
240
  * A consumer that cannot act on the distinction may still print `reference`
223
241
  * alone and lose nothing it had before.
@@ -226,7 +244,6 @@ export declare const assetFailureKind: z.ZodEnum<{
226
244
  unresolvable: "unresolvable";
227
245
  upload_failed: "upload_failed";
228
246
  refused: "refused";
229
- too_large: "too_large";
230
247
  }>;
231
248
  export type AssetFailureKind = z.infer<typeof assetFailureKind>;
232
249
  export declare const assetFailure: z.ZodObject<{
@@ -235,10 +252,10 @@ export declare const assetFailure: z.ZodObject<{
235
252
  unresolvable: "unresolvable";
236
253
  upload_failed: "upload_failed";
237
254
  refused: "refused";
238
- too_large: "too_large";
239
255
  }>;
240
256
  details: z.ZodOptional<z.ZodNullable<z.ZodObject<{
241
- limit_bytes: z.ZodNumber;
257
+ store_bytes: z.ZodNumber;
258
+ used_bytes: z.ZodNumber;
242
259
  size_bytes: z.ZodNumber;
243
260
  }, z.core.$strip>>>;
244
261
  }, z.core.$strip>;
@@ -265,14 +282,16 @@ export declare const assetSyncStatus: z.ZodObject<{
265
282
  unresolvable: "unresolvable";
266
283
  upload_failed: "upload_failed";
267
284
  refused: "refused";
268
- too_large: "too_large";
269
285
  }>;
270
286
  details: z.ZodOptional<z.ZodNullable<z.ZodObject<{
271
- limit_bytes: z.ZodNumber;
287
+ store_bytes: z.ZodNumber;
288
+ used_bytes: z.ZodNumber;
272
289
  size_bytes: z.ZodNumber;
273
290
  }, z.core.$strip>>>;
274
291
  }, z.core.$strip>>;
275
292
  reason: z.ZodNullable<z.ZodString>;
293
+ stored: z.ZodNullable<z.ZodNumber>;
294
+ announced: z.ZodNumber;
276
295
  started_at: z.ZodISODateTime;
277
296
  updated_at: z.ZodISODateTime;
278
297
  }, z.core.$strip>;
@@ -285,7 +304,6 @@ export declare const assetListResponse: z.ZodObject<{
285
304
  urdf: "urdf";
286
305
  mesh: "mesh";
287
306
  texture: "texture";
288
- other: "other";
289
307
  }>;
290
308
  name: z.ZodString;
291
309
  media_type: z.ZodString;
@@ -309,14 +327,16 @@ export declare const assetListResponse: z.ZodObject<{
309
327
  unresolvable: "unresolvable";
310
328
  upload_failed: "upload_failed";
311
329
  refused: "refused";
312
- too_large: "too_large";
313
330
  }>;
314
331
  details: z.ZodOptional<z.ZodNullable<z.ZodObject<{
315
- limit_bytes: z.ZodNumber;
332
+ store_bytes: z.ZodNumber;
333
+ used_bytes: z.ZodNumber;
316
334
  size_bytes: z.ZodNumber;
317
335
  }, z.core.$strip>>>;
318
336
  }, z.core.$strip>>;
319
337
  reason: z.ZodNullable<z.ZodString>;
338
+ stored: z.ZodNullable<z.ZodNumber>;
339
+ announced: z.ZodNumber;
320
340
  started_at: z.ZodISODateTime;
321
341
  updated_at: z.ZodISODateTime;
322
342
  }, z.core.$strip>>;
@@ -332,8 +352,28 @@ export declare const assetListResponse: z.ZodObject<{
332
352
  }, z.core.$strip>>;
333
353
  }, z.core.$strip>;
334
354
  urdf_available: z.ZodNullable<z.ZodBoolean>;
355
+ store: z.ZodObject<{
356
+ bytes: z.ZodNumber;
357
+ used_bytes: z.ZodNumber;
358
+ }, z.core.$strip>;
359
+ joint_state_slug: z.ZodNullable<z.ZodString>;
335
360
  }, z.core.$strip>;
336
361
  export type AssetListResponse = z.infer<typeof assetListResponse>;
362
+ /**
363
+ * What `DELETE /api/robots/:id/assets` answers — the store's escape hatch.
364
+ *
365
+ * A full store is never a dead end: the URDF upload is exempt from the gate,
366
+ * reconcile after every sync already frees what the new URDF stopped
367
+ * referencing, and this route is the third leg — an Owner can empty the
368
+ * store outright and let the next sync refill it. `deleted` and
369
+ * `bytes_freed` are what changed, not the store's state afterward — that is
370
+ * `0` by construction and not worth a field of its own.
371
+ */
372
+ export declare const assetsClearResponse: z.ZodObject<{
373
+ deleted: z.ZodNumber;
374
+ bytes_freed: z.ZodNumber;
375
+ }, z.core.$strip>;
376
+ export type AssetsClearResponse = z.infer<typeof assetsClearResponse>;
337
377
  /**
338
378
  * The query of `GET /api/robots/:id/assets/missing`, the placeholder a
339
379
  * rewritten URDF points at for a mesh Fleetless does not hold.
@@ -348,11 +388,6 @@ export declare const missingAssetQuery: z.ZodObject<{
348
388
  name: z.ZodOptional<z.ZodString>;
349
389
  }, z.core.$strip>;
350
390
  export type MissingAssetQuery = z.infer<typeof missingAssetQuery>;
351
- /**
352
- * What an `asset_too_large` refusal tells the caller — the same discipline as
353
- * `publisher_busy` and `job_queue_full`: a refusal that names a state and no
354
- * number leaves the caller unable to decide anything.
355
- */
356
391
  /**
357
392
  * What a `busy` refusal on an asset sync has to carry.
358
393
  *
package/dist/assets.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  import { z } from 'zod';
3
+ import { slug } from './common.js';
3
4
  /**
4
5
  * The asset store.
5
6
  *
@@ -26,20 +27,24 @@ import { z } from 'zod';
26
27
  *
27
28
  * ---
28
29
  *
29
- * **`texture` is its own member of `assetKind` and not `other`.**
30
+ * **Three kinds, and every one of them is a thing a renderer does something
31
+ * with.**
30
32
  *
31
- * Filing textures under `other` costs a real capability: a client that
32
- * renders a robot must know, from the asset list alone and before fetching
33
- * anything, which bytes it has to pre-fetch. Every load in the browser goes
34
- * through the SDK with the bearer token — there is no lazy second fetch a
35
- * renderer can make on its own account — so "what must be in memory before
36
- * anything renders" is a question the list has to be able to answer.
33
+ * A `urdf` is the description, a `mesh` is geometry, a `texture` is an image
34
+ * referenced by the URDF's own `<material><texture>` **or** by a mesh file
35
+ * internally (a `.dae`'s `<init_from>`). A client that renders a robot must
36
+ * know, from the asset list alone and before fetching anything, which bytes
37
+ * it has to pre-fetch. Every load in the browser goes through the SDK with
38
+ * the bearer token — there is no lazy second fetch a renderer can make on its
39
+ * own account — so "what must be in memory before anything renders" is a
40
+ * question the list has to be able to answer, and a kind meaning *something
41
+ * else* answers it for nothing.
37
42
  *
38
- * A `texture` is an image referenced by the URDF's own `<material><texture>`
39
- * **or** by a mesh file internally (a `.dae`'s `<init_from>`). Both are
40
- * surfaces; neither is geometry; both must be resolvable by name.
43
+ * There was a fourth, `other`, and **no producer ever sent it**: the bridge
44
+ * classifies what it uploads and has only these three to choose from. It was
45
+ * a slot for a file nobody had, which every consumer still had to branch on.
41
46
  */
42
- export const assetKind = z.enum(['urdf', 'mesh', 'texture', 'other']);
47
+ export const assetKind = z.enum(['urdf', 'mesh', 'texture']);
43
48
  /**
44
49
  * The `name` a URDF asset carries.
45
50
  *
@@ -70,7 +75,7 @@ export const asset = z.object({
70
75
  id: z.uuid().meta({ description: 'The asset\'s id in the store.' }),
71
76
  robot_id: z.uuid().meta({ description: 'The robot this asset belongs to.' }),
72
77
  kind: assetKind.meta({
73
- description: 'What the file is: the `urdf` itself, a `mesh` it references, a `texture` a mesh or the URDF paints with, or `other`. A renderer decides from this alone, before fetching anything, what to pre-fetch.',
78
+ description: 'What the file is: the `urdf` itself, a `mesh` it references, or a `texture` a mesh or the URDF paints with. A renderer decides from this alone, before fetching anything, what to pre-fetch.',
74
79
  }),
75
80
  /**
76
81
  * What the robot called it — for a mesh, the `package://` URI the URDF
@@ -227,33 +232,44 @@ export const assetSyncResponse = z.object({
227
232
  * field is additive; adding an enum member is not.
228
233
  */
229
234
  /**
230
- * **The upload ceiling both sides read.**
235
+ * **How much asset storage a robot has: one number, the same for every robot.**
231
236
  *
232
- * It is stated once, here, rather than once in the producer and once in the
233
- * cloud. A limit the sender guesses and the receiver enforces is not a limit;
234
- * it is two numbers that agree until one of them changes.
237
+ * It replaces two dials that answered neither question well — a per-file
238
+ * ceiling, which refused a single large mesh while saying nothing about the
239
+ * robot's total, and a per-organisation quota, which said nothing about any
240
+ * one robot. A developer syncing a robot asks *will this robot's description
241
+ * fit*, and only this number answers it.
235
242
  *
236
- * The bridge reads it **before** it reads a file into memory, and the cloud
237
- * enforces it. Without a shared number a producer cannot refuse an oversized
238
- * mesh without first buffering the whole of it.
243
+ * **Content addressing still stores a shared blob once, and each robot's
244
+ * counter still carries it.** Two robots referencing the same mesh cost one
245
+ * object and count against both stores, so a robot's number never depends on
246
+ * another robot's — which is the only way "used of 1 GB" means anything on a
247
+ * page about one robot.
239
248
  *
240
- * **It applies per file, not per sync.** Eight meshes of 30 MiB each pass; one
241
- * file of 65 MiB does not. Reading it as a ceiling on a whole transfer means
242
- * planning against a bound that does not exist — the total of a sync counts
243
- * against the organisation's storage quota, which is a different number in a
244
- * different place.
249
+ * Stated here, and in `constants.json`, because the bridge cannot import this
250
+ * package and the cloud enforces the check: a limit the sender guesses and
251
+ * the receiver enforces is two numbers that agree until one of them moves.
252
+ */
253
+ export const ROBOT_ASSET_STORE_BYTES = 1_000_000_000;
254
+ /**
255
+ * What a full store tells the caller — the same discipline as `job_queue_full`
256
+ * and `publisher_busy`: a refusal that names a state and no number leaves the
257
+ * caller unable to decide anything.
245
258
  *
246
- * A robot whose meshes exceed this is not a contract question but a question
247
- * about storage, transfer time and quota, and it is answered by raising the
248
- * number here, in one place, for both sides.
259
+ * Three numbers, because two of them answer different questions. `store_bytes`
260
+ * and `used_bytes` say how much room there is; `size_bytes` says what did not
261
+ * fit. Without the pair a developer cannot tell whether to delete something or
262
+ * to shrink the mesh, and "the store is full" answers neither.
249
263
  */
250
- export const ASSET_UPLOAD_MAX_BYTES = 64 * 1024 * 1024;
251
- export const assetTooLargeDetails = z.object({
252
- limit_bytes: z.number().int().positive().meta({
253
- description: 'The upload ceiling, in bytes.',
264
+ export const assetStoreRefusedDetails = z.object({
265
+ store_bytes: z.number().int().positive().meta({
266
+ description: 'The robot\'s store, in bytes.',
267
+ }),
268
+ used_bytes: z.number().int().nonnegative().meta({
269
+ description: 'Bytes the robot\'s assets occupy before this upload.',
254
270
  }),
255
271
  size_bytes: z.number().int().positive().meta({
256
- description: 'How large the refused file is, in bytes. With `limit_bytes` beside it a developer can tell whether to shrink the mesh or raise the limit; "too large" alone answers neither.',
272
+ description: 'The refused upload, in bytes.',
257
273
  }),
258
274
  });
259
275
  /**
@@ -269,15 +285,22 @@ export const assetTooLargeDetails = z.object({
269
285
  * reconciliation may treat as gone.
270
286
  * - **`upload_failed`** — the bytes exist and the transfer did not succeed.
271
287
  * **Transient.** The asset is still wanted; a later sync will carry it.
272
- * - **`refused`** — never attempted, because a producer-side ceiling was hit
273
- * (for example a `.dae` carrying more internal references than one file or
274
- * one sync will report). **Transient in the same sense**: nothing is known
275
- * to be missing, only unexamined.
288
+ * - **`refused`** — never attempted. Either the robot's asset store had no
289
+ * room for it, in which case `details` carries the three numbers, or a
290
+ * producer-side ceiling was hit (a `.dae` carrying more internal references
291
+ * than one file or one sync will report), in which case it does not.
292
+ * **Transient in the same sense**: nothing is known to be missing, only
293
+ * unexamined.
294
+ *
295
+ * There was a fourth, `too_large`, for a file over a per-file ceiling. That
296
+ * ceiling is gone — a robot has one store and nothing is refused for its own
297
+ * size — so the kind had no producer left and one fewer thing to branch on is
298
+ * the whole of the gain.
276
299
  *
277
300
  * A consumer that cannot act on the distinction may still print `reference`
278
301
  * alone and lose nothing it had before.
279
302
  */
280
- export const assetFailureKind = z.enum(['unresolvable', 'upload_failed', 'refused', 'too_large']);
303
+ export const assetFailureKind = z.enum(['unresolvable', 'upload_failed', 'refused']);
281
304
  export const assetFailure = z.object({
282
305
  /**
283
306
  * What could not be provided, verbatim — the same string `asset.name` would
@@ -290,35 +313,30 @@ export const assetFailure = z.object({
290
313
  description: 'What could not be provided, verbatim — the same string the asset would have been stored under, so a developer can match it against their own workspace by eye. For a failed URDF upload it is `robot_description`, which is **not** a mesh URI: a consumer must not assume every entry is one.',
291
314
  }),
292
315
  kind: assetFailureKind.meta({
293
- description: 'Why it failed. `unresolvable` means the reference names nothing the producer can find or may read, and is **permanent** — the only kind reconciliation may treat as gone. `upload_failed` means the bytes exist and the transfer did not succeed, `refused` means it was never attempted because a producer-side ceiling was hit, and `too_large` means it exceeds the upload limit and carries both numbers in `details`.',
316
+ description: 'Why it failed. `unresolvable` means the reference names nothing the producer can find or may read, and is **permanent** — the only kind reconciliation may treat as gone. `upload_failed` means the bytes exist and the transfer did not succeed, and `refused` means it was never attempted, either because the robot\'s asset store had no room — then `details` carries the three numbers — or because a producer-side ceiling was hit.',
294
317
  }),
295
318
  /**
296
- * **The two numbers, and why `too_large` is a kind of its own.**
319
+ * **The three numbers behind a full store.**
297
320
  *
298
- * `refused` means *never attempted, because a producer-side ceiling was
299
- * hit*. That fits a file skipped for its size **and** the single collective
300
- * entry a sync emits when it stops naming individual failures. Filing both
301
- * under one kind puts two facts on one key, each overwriting the other.
321
+ * A reason without numbers is not one a caller can act on. *"Refused"* does
322
+ * not answer whether to delete an old sync or shrink the mesh;
323
+ * `store_bytes`, `used_bytes` and `size_bytes` do.
302
324
  *
303
- * A reason without numbers is not one a caller can act on. *"Too large"*
304
- * does not answer whether to shrink the mesh or raise the limit;
305
- * `limit_bytes` and `size_bytes` do.
306
- *
307
- * Absent for every other kind — a forced `details: null` on every
308
- * `unresolvable` buys nothing. The pairing is **enforced** below, not merely
309
- * described: a field whose rule lives only in a comment is a request.
325
+ * **Present only on `refused`, and not on every `refused`.** The other half
326
+ * of that kind is the single collective entry a sync emits when it stops
327
+ * naming individual failures, and no store number describes it — requiring
328
+ * details there would mean inventing them. So the enforcement below is the
329
+ * half that can be enforced: details belong to `refused` and to nothing
330
+ * else. A forced `details: null` on every `unresolvable` buys nothing.
310
331
  */
311
- details: assetTooLargeDetails.nullish().meta({
312
- description: 'The two numbers behind a `too_large` failure, and absent for every other kind — a forced `null` on every `unresolvable` entry buys nothing. The pairing is enforced, not merely described.',
332
+ details: assetStoreRefusedDetails.nullish().meta({
333
+ description: 'The three numbers behind a `refused` entry the robot\'s store had no room for, and absent for every other kind — a forced `null` on every `unresolvable` entry buys nothing. A `refused` entry may also carry no details: the producer\'s own ceiling is the other half of that kind, and no store number describes it.',
313
334
  }),
314
335
  }).superRefine((f, ctx) => {
315
336
  // Enforced here, not merely described above — a rule that lives only in a
316
337
  // comment gets filled with something else.
317
- if (f.kind === 'too_large' && f.details == null) {
318
- ctx.addIssue({ code: 'custom', path: ['details'], message: '`too_large` without limit_bytes/size_bytes says nothing a developer can act on' });
319
- }
320
- if (f.kind !== 'too_large' && f.details != null) {
321
- ctx.addIssue({ code: 'custom', path: ['details'], message: 'size details belong to `too_large` only' });
338
+ if (f.kind !== 'refused' && f.details != null) {
339
+ ctx.addIssue({ code: 'custom', path: ['details'], message: 'store details belong to `refused` only' });
322
340
  }
323
341
  });
324
342
  export const assetSyncState = z.enum(['running', 'succeeded', 'failed']);
@@ -364,6 +382,36 @@ export const assetSyncStatus = z.object({
364
382
  reason: z.string().min(1).nullable().meta({
365
383
  description: 'Why the sync ended as it did, when that is not a per-reference fact. `null` when `failed` already says everything there is to say.',
366
384
  }),
385
+ /**
386
+ * **What the receiver counted, next to what the producer claimed.**
387
+ *
388
+ * `state` is the bridge's own terminal frame and nothing else. A dev stack
389
+ * with no object store answered `500` to every upload and the sync still
390
+ * read `succeeded` — the producer had genuinely sent every file, and no
391
+ * one had asked the store. These two numbers are the cloud's own count,
392
+ * taken after the terminal frame: how many of the announced files
393
+ * (`assets_available`'s URDF and mesh list) its store actually holds.
394
+ *
395
+ * They are a pair because neither alone answers anything. `stored` without
396
+ * `announced` cannot say whether four files is all of them or a tenth of
397
+ * them, and `announced` alone is what the producer said it had, which is
398
+ * the claim under examination.
399
+ *
400
+ * A partial store is still `succeeded`: some meshes were never going to
401
+ * resolve, and the per-reference `failed` entries say which. An empty one
402
+ * under a `succeeded` frame is `failed`, because no transport succeeds at
403
+ * nothing.
404
+ *
405
+ * **`stored` is `null` while the sync is still running.** The count is
406
+ * taken once, after the robot's terminal frame; a `0` before then would
407
+ * say the store is empty when nobody has looked.
408
+ */
409
+ stored: z.number().int().nonnegative().nullable().meta({
410
+ description: 'How many of the announced files the cloud\'s store actually holds. Counted once, after the robot reports the sync done, and `null` until then — nobody has looked yet. Read it against `announced`: `state` is what the robot reported, this is what arrived.',
411
+ }),
412
+ announced: z.number().int().nonnegative().meta({
413
+ description: 'How many files the robot announced for this sync — the URDF, if it has one, plus every mesh URI its description references. `0` when the robot announced nothing, and also `0` until it has answered at all: read it beside `stored`, which stays `null` until the terminal frame.',
414
+ }),
367
415
  started_at: z.iso.datetime().meta({
368
416
  description: 'When the sync started, as an ISO 8601 timestamp.',
369
417
  }),
@@ -408,6 +456,53 @@ export const assetListResponse = z.object({
408
456
  urdf_available: z.boolean().nullable().meta({
409
457
  description: 'What the connected bridge says it *could* transfer — deliberately separate from what has been transferred. `null` when no bridge is connected, distinct from `false`: "no robot is online to ask" and "the robot has no URDF" send a developer to different places. After a publisher is killed rather than shut down this can read `true` for some seconds, on the underlying DDS liveliness timeout rather than on any check made here.',
410
458
  }),
459
+ /**
460
+ * **How full this robot's store is, on the list that already names what is
461
+ * in it.** A page showing assets is the page where "will the next sync fit"
462
+ * is asked, and a second round trip to some quota endpoint would answer it
463
+ * about the organisation instead — which is a different number about a
464
+ * different thing.
465
+ */
466
+ store: z.object({
467
+ bytes: z.number().int().positive().meta({
468
+ description: 'The robot\'s asset store, `ROBOT_ASSET_STORE_BYTES`.',
469
+ }),
470
+ used_bytes: z.number().int().nonnegative().meta({
471
+ description: 'Bytes its assets occupy.',
472
+ }),
473
+ }).meta({ description: 'How full this robot\'s store is.' }),
474
+ /**
475
+ * The datapoint that moves the joints in a renderer, chosen by a developer
476
+ * and stored on the robot. It rides on this list because a client that has
477
+ * just fetched the URDF and the meshes needs exactly one more thing to
478
+ * animate them, and asking a second endpoint for one slug is a round trip
479
+ * that buys nothing.
480
+ *
481
+ * `null` is an ordinary answer: none was ever chosen, or a publish removed
482
+ * the datapoint it named and the cloud cleared the mapping rather than
483
+ * leave it pointing at something that no longer qualifies.
484
+ */
485
+ joint_state_slug: slug.nullable().meta({
486
+ description: 'The whole-message `sensor_msgs/msg/JointState` datapoint that drives the console\'s URDF viewer; null when none is chosen or a publish removed it. Set through `PUT /api/robots/:id/urdf/joint-state`.',
487
+ }),
488
+ });
489
+ /**
490
+ * What `DELETE /api/robots/:id/assets` answers — the store's escape hatch.
491
+ *
492
+ * A full store is never a dead end: the URDF upload is exempt from the gate,
493
+ * reconcile after every sync already frees what the new URDF stopped
494
+ * referencing, and this route is the third leg — an Owner can empty the
495
+ * store outright and let the next sync refill it. `deleted` and
496
+ * `bytes_freed` are what changed, not the store's state afterward — that is
497
+ * `0` by construction and not worth a field of its own.
498
+ */
499
+ export const assetsClearResponse = z.object({
500
+ deleted: z.number().int().nonnegative().meta({
501
+ description: 'How many assets — URDF, meshes and textures together — were removed.',
502
+ }),
503
+ bytes_freed: z.number().int().nonnegative().meta({
504
+ description: 'The bytes the robot\'s store got back.',
505
+ }),
411
506
  });
412
507
  /**
413
508
  * The query of `GET /api/robots/:id/assets/missing`, the placeholder a
@@ -426,11 +521,6 @@ export const missingAssetQuery = z
426
521
  }),
427
522
  })
428
523
  .meta({ description: 'The one optional parameter of the missing-asset placeholder; it names the reference in the refusal.' });
429
- /**
430
- * What an `asset_too_large` refusal tells the caller — the same discipline as
431
- * `publisher_busy` and `job_queue_full`: a refusal that names a state and no
432
- * number leaves the caller unable to decide anything.
433
- */
434
524
  /**
435
525
  * What a `busy` refusal on an asset sync has to carry.
436
526
  *
package/dist/audit.d.ts CHANGED
@@ -121,6 +121,6 @@ export declare const AUDIT_CSV_COLUMNS: readonly ["seq", "at", "actor_kind", "ac
121
121
  * The audit log is kept for **90 days**.
122
122
  *
123
123
  * A constant here so no consumer derives it a second time — the same reasoning
124
- * as `ASSET_UPLOAD_MAX_BYTES`.
124
+ * as `ROBOT_ASSET_STORE_BYTES`.
125
125
  */
126
126
  export declare const AUDIT_RETENTION_DAYS = 90;
package/dist/audit.js CHANGED
@@ -203,6 +203,6 @@ export const AUDIT_CSV_COLUMNS = ['seq', 'at', 'actor_kind', 'actor_id', 'action
203
203
  * The audit log is kept for **90 days**.
204
204
  *
205
205
  * A constant here so no consumer derives it a second time — the same reasoning
206
- * as `ASSET_UPLOAD_MAX_BYTES`.
206
+ * as `ROBOT_ASSET_STORE_BYTES`.
207
207
  */
208
208
  export const AUDIT_RETENTION_DAYS = 90;