@fleetless/contracts 1.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.
- package/CHANGELOG.md +26 -0
- package/LICENSE +202 -0
- package/NOTICE +17 -0
- package/README.md +88 -0
- package/artifacts/constants.json +24 -0
- package/artifacts/openapi.json +17219 -0
- package/artifacts/routes.json +4605 -0
- package/artifacts/schema/accept-team-invite-request.schema.json +22 -0
- package/artifacts/schema/action-config.schema.json +198 -0
- package/artifacts/schema/alert-list-response.schema.json +172 -0
- package/artifacts/schema/api-error.schema.json +20 -0
- package/artifacts/schema/app-auth-config.schema.json +106 -0
- package/artifacts/schema/app-invitation-list-response.schema.json +57 -0
- package/artifacts/schema/app-invitation.schema.json +69 -0
- package/artifacts/schema/app-list-response.schema.json +82 -0
- package/artifacts/schema/app-mail-template-list-response.schema.json +68 -0
- package/artifacts/schema/app-mail-template.schema.json +54 -0
- package/artifacts/schema/app-oidc-provider-list-response.schema.json +93 -0
- package/artifacts/schema/app-oidc-provider.schema.json +80 -0
- package/artifacts/schema/app-user-list-response.schema.json +111 -0
- package/artifacts/schema/app-user.schema.json +98 -0
- package/artifacts/schema/app.schema.json +69 -0
- package/artifacts/schema/apply-error.schema.json +41 -0
- package/artifacts/schema/asset-list-response.schema.json +288 -0
- package/artifacts/schema/asset-sync-request.schema.json +17 -0
- package/artifacts/schema/asset-sync-response.schema.json +16 -0
- package/artifacts/schema/asset-sync-status.schema.json +136 -0
- package/artifacts/schema/asset.schema.json +68 -0
- package/artifacts/schema/audit-actor.schema.json +32 -0
- package/artifacts/schema/audit-event.schema.json +119 -0
- package/artifacts/schema/audit-list-response.schema.json +144 -0
- package/artifacts/schema/audit-query.schema.json +79 -0
- package/artifacts/schema/auth-error.schema.json +23 -0
- package/artifacts/schema/auth-me-response.schema.json +99 -0
- package/artifacts/schema/auth-ok.schema.json +115 -0
- package/artifacts/schema/authorization-server-metadata.schema.json +80 -0
- package/artifacts/schema/bridge-asset-progress.schema.json +99 -0
- package/artifacts/schema/bridge-assets-available.schema.json +25 -0
- package/artifacts/schema/bridge-camera-state.schema.json +78 -0
- package/artifacts/schema/bridge-config-applied.schema.json +67 -0
- package/artifacts/schema/bridge-hello.schema.json +65 -0
- package/artifacts/schema/bridge-introspect.schema.json +114 -0
- package/artifacts/schema/bridge-job-lost.schema.json +22 -0
- package/artifacts/schema/bridge-job-update.schema.json +100 -0
- package/artifacts/schema/bridge-pong.schema.json +19 -0
- package/artifacts/schema/bridge-pressure.schema.json +292 -0
- package/artifacts/schema/bridge-state.schema.json +24 -0
- package/artifacts/schema/bridge-type-definitions.schema.json +169 -0
- package/artifacts/schema/busy-details.schema.json +115 -0
- package/artifacts/schema/camera-descriptor.schema.json +45 -0
- package/artifacts/schema/camera-list-response.schema.json +58 -0
- package/artifacts/schema/camera-source.schema.json +240 -0
- package/artifacts/schema/cancel-request.schema.json +20 -0
- package/artifacts/schema/client-accept-invitation-request.schema.json +35 -0
- package/artifacts/schema/client-auth.schema.json +18 -0
- package/artifacts/schema/client-cancel.schema.json +45 -0
- package/artifacts/schema/client-identity.schema.json +103 -0
- package/artifacts/schema/client-invoke.schema.json +45 -0
- package/artifacts/schema/client-login-request.schema.json +29 -0
- package/artifacts/schema/client-logout-request.schema.json +14 -0
- package/artifacts/schema/client-mcp-interaction-decision-response.schema.json +15 -0
- package/artifacts/schema/client-mcp-interaction.schema.json +59 -0
- package/artifacts/schema/client-oidc-callback-query.schema.json +28 -0
- package/artifacts/schema/client-oidc-exchange-request.schema.json +21 -0
- package/artifacts/schema/client-oidc-start-query.schema.json +36 -0
- package/artifacts/schema/client-password-reset-confirm-request.schema.json +22 -0
- package/artifacts/schema/client-password-reset-request.schema.json +24 -0
- package/artifacts/schema/client-provider-list-query.schema.json +17 -0
- package/artifacts/schema/client-provider-list-response.schema.json +34 -0
- package/artifacts/schema/client-publish.schema.json +40 -0
- package/artifacts/schema/client-refresh-request.schema.json +14 -0
- package/artifacts/schema/client-register-request.schema.json +44 -0
- package/artifacts/schema/client-resend-verification-request.schema.json +24 -0
- package/artifacts/schema/client-subscribe.schema.json +43 -0
- package/artifacts/schema/client-unsubscribe.schema.json +26 -0
- package/artifacts/schema/client-verify-email-request.schema.json +15 -0
- package/artifacts/schema/cloud-asset-request.schema.json +37 -0
- package/artifacts/schema/cloud-camera-start.schema.json +41 -0
- package/artifacts/schema/cloud-camera-stop.schema.json +26 -0
- package/artifacts/schema/cloud-cancel.schema.json +33 -0
- package/artifacts/schema/cloud-config.schema.json +1635 -0
- package/artifacts/schema/cloud-hello-error.schema.json +23 -0
- package/artifacts/schema/cloud-hello-ok.schema.json +19 -0
- package/artifacts/schema/cloud-introspect-request.schema.json +19 -0
- package/artifacts/schema/cloud-invoke.schema.json +40 -0
- package/artifacts/schema/cloud-ping.schema.json +19 -0
- package/artifacts/schema/cloud-publish.schema.json +28 -0
- package/artifacts/schema/cloud-type-request.schema.json +30 -0
- package/artifacts/schema/command-result.schema.json +175 -0
- package/artifacts/schema/config-draft-response.schema.json +1695 -0
- package/artifacts/schema/config-state.schema.json +124 -0
- package/artifacts/schema/config-version-response.schema.json +1641 -0
- package/artifacts/schema/config-versions-response.schema.json +33 -0
- package/artifacts/schema/create-app-invitation-request.schema.json +40 -0
- package/artifacts/schema/create-app-oidc-provider-request.schema.json +70 -0
- package/artifacts/schema/create-app-request.schema.json +30 -0
- package/artifacts/schema/create-app-user-request.schema.json +42 -0
- package/artifacts/schema/create-robot-request.schema.json +14 -0
- package/artifacts/schema/create-robot-response.schema.json +44 -0
- package/artifacts/schema/create-server-key-response.schema.json +65 -0
- package/artifacts/schema/create-team-invite-request.schema.json +43 -0
- package/artifacts/schema/datapoint-alert-row.schema.json +160 -0
- package/artifacts/schema/datapoint-config.schema.json +366 -0
- package/artifacts/schema/datapoint-display.schema.json +31 -0
- package/artifacts/schema/datapoint-event.schema.json +34 -0
- package/artifacts/schema/datapoint-frame.schema.json +28 -0
- package/artifacts/schema/datapoint-list-response.schema.json +61 -0
- package/artifacts/schema/datapoint-value.schema.json +28 -0
- package/artifacts/schema/developer-login-request.schema.json +19 -0
- package/artifacts/schema/dynamic-client-registration-request.schema.json +60 -0
- package/artifacts/schema/dynamic-client-registration-response.schema.json +68 -0
- package/artifacts/schema/error-frame.schema.json +23 -0
- package/artifacts/schema/exposure-counts.schema.json +39 -0
- package/artifacts/schema/exposure-list-response.schema.json +43 -0
- package/artifacts/schema/fetch-types-request.schema.json +19 -0
- package/artifacts/schema/fetch-types-response.schema.json +163 -0
- package/artifacts/schema/fleetless-user-list-response.schema.json +73 -0
- package/artifacts/schema/fleetless-user.schema.json +60 -0
- package/artifacts/schema/history-buckets-response.schema.json +79 -0
- package/artifacts/schema/history-query.schema.json +58 -0
- package/artifacts/schema/history-response.schema.json +150 -0
- package/artifacts/schema/history-samples-response.schema.json +68 -0
- package/artifacts/schema/introspection-response.schema.json +118 -0
- package/artifacts/schema/invoke-or-service-response.schema.json +141 -0
- package/artifacts/schema/invoke-request.schema.json +23 -0
- package/artifacts/schema/invoke-response.schema.json +125 -0
- package/artifacts/schema/job-actor.schema.json +34 -0
- package/artifacts/schema/job-event.schema.json +158 -0
- package/artifacts/schema/job-response.schema.json +123 -0
- package/artifacts/schema/job-run-list-response.schema.json +222 -0
- package/artifacts/schema/job-run-query.schema.json +95 -0
- package/artifacts/schema/job-run-summary-query.schema.json +23 -0
- package/artifacts/schema/job-run-summary.schema.json +33 -0
- package/artifacts/schema/job-run.schema.json +195 -0
- package/artifacts/schema/job-state.schema.json +11 -0
- package/artifacts/schema/job.schema.json +106 -0
- package/artifacts/schema/latency-bucket.schema.json +63 -0
- package/artifacts/schema/live-session-response.schema.json +41 -0
- package/artifacts/schema/mail-outcome.schema.json +20 -0
- package/artifacts/schema/mail-template-preview-request.schema.json +35 -0
- package/artifacts/schema/mail-template-preview-response.schema.json +31 -0
- package/artifacts/schema/mail-template-problem-details.schema.json +24 -0
- package/artifacts/schema/mcp-consent-grant-list-response.schema.json +52 -0
- package/artifacts/schema/mcp-consent-grant.schema.json +39 -0
- package/artifacts/schema/mcp-robot-datasheet.schema.json +115 -0
- package/artifacts/schema/mcp-role-preview-response.schema.json +134 -0
- package/artifacts/schema/missing-asset-query.schema.json +11 -0
- package/artifacts/schema/oauth-authorize-query.schema.json +47 -0
- package/artifacts/schema/oauth-redirect-response.schema.json +15 -0
- package/artifacts/schema/oauth-token-request.schema.json +47 -0
- package/artifacts/schema/oauth-token-response.schema.json +38 -0
- package/artifacts/schema/org-alerts-query.schema.json +15 -0
- package/artifacts/schema/org-event-dropped.schema.json +26 -0
- package/artifacts/schema/org-event-replay.schema.json +97 -0
- package/artifacts/schema/org-event-subscribe.schema.json +14 -0
- package/artifacts/schema/org-event-unsubscribe.schema.json +14 -0
- package/artifacts/schema/org-event.schema.json +75 -0
- package/artifacts/schema/org-firing-alerts-response.schema.json +178 -0
- package/artifacts/schema/org-health-query.schema.json +13 -0
- package/artifacts/schema/org-latency-query.schema.json +42 -0
- package/artifacts/schema/org-latency-response.schema.json +124 -0
- package/artifacts/schema/org-quota-usage-counts.schema.json +42 -0
- package/artifacts/schema/org-quota-usage.schema.json +102 -0
- package/artifacts/schema/org-quotas.schema.json +51 -0
- package/artifacts/schema/org-usage-query.schema.json +19 -0
- package/artifacts/schema/org-usage-response.schema.json +77 -0
- package/artifacts/schema/org.schema.json +30 -0
- package/artifacts/schema/parameter-invalid-details.schema.json +37 -0
- package/artifacts/schema/parameter-spec.schema.json +120 -0
- package/artifacts/schema/parameter-violation.schema.json +24 -0
- package/artifacts/schema/password-change-request.schema.json +21 -0
- package/artifacts/schema/password-reset-confirm.schema.json +19 -0
- package/artifacts/schema/password-reset-request.schema.json +14 -0
- package/artifacts/schema/patch-app-oidc-provider-request.schema.json +50 -0
- package/artifacts/schema/patch-app-user-request.schema.json +34 -0
- package/artifacts/schema/patch-auth-me-request.schema.json +22 -0
- package/artifacts/schema/patch-fleetless-user-request.schema.json +20 -0
- package/artifacts/schema/patch-org-request.schema.json +15 -0
- package/artifacts/schema/patch-org-response.schema.json +40 -0
- package/artifacts/schema/patch-robot-request.schema.json +15 -0
- package/artifacts/schema/patch-robot-response.schema.json +40 -0
- package/artifacts/schema/pending-team-invite-list-response.schema.json +52 -0
- package/artifacts/schema/pending-team-invite.schema.json +39 -0
- package/artifacts/schema/protected-resource-metadata.schema.json +41 -0
- package/artifacts/schema/publish-config-response.schema.json +21 -0
- package/artifacts/schema/publish-request.schema.json +17 -0
- package/artifacts/schema/publisher-config.schema.json +285 -0
- package/artifacts/schema/put-app-auth-config-request.schema.json +93 -0
- package/artifacts/schema/put-app-mail-template-request.schema.json +35 -0
- package/artifacts/schema/put-config-draft-request.schema.json +13 -0
- package/artifacts/schema/put-datapoint-display-request.schema.json +31 -0
- package/artifacts/schema/put-robot-details-request.schema.json +41 -0
- package/artifacts/schema/put-robot-details-response.schema.json +43 -0
- package/artifacts/schema/rate-limit-details.schema.json +15 -0
- package/artifacts/schema/refresh-request.schema.json +13 -0
- package/artifacts/schema/release-live-query.schema.json +13 -0
- package/artifacts/schema/rename-slug-request.schema.json +23 -0
- package/artifacts/schema/rename-slug-response.schema.json +24 -0
- package/artifacts/schema/resource-health-event.schema.json +72 -0
- package/artifacts/schema/resource-health-list-response.schema.json +80 -0
- package/artifacts/schema/resource-health-state.schema.json +68 -0
- package/artifacts/schema/robot-config-doc.schema.json +1616 -0
- package/artifacts/schema/robot-delete-query.schema.json +12 -0
- package/artifacts/schema/robot-deletion-summary.schema.json +63 -0
- package/artifacts/schema/robot-detail-response.schema.json +262 -0
- package/artifacts/schema/robot-details-doc.schema.json +33 -0
- package/artifacts/schema/robot-jobs-response.schema.json +119 -0
- package/artifacts/schema/robot-latency-series.schema.json +81 -0
- package/artifacts/schema/robot-list-item.schema.json +94 -0
- package/artifacts/schema/robot-list-response.schema.json +106 -0
- package/artifacts/schema/robot.schema.json +30 -0
- package/artifacts/schema/role-list-response.schema.json +48 -0
- package/artifacts/schema/role-permissions.schema.json +61 -0
- package/artifacts/schema/role.schema.json +35 -0
- package/artifacts/schema/ros-graph.schema.json +99 -0
- package/artifacts/schema/server-key-list-response.schema.json +64 -0
- package/artifacts/schema/server-key.schema.json +51 -0
- package/artifacts/schema/service-call-response.schema.json +13 -0
- package/artifacts/schema/service-config.schema.json +198 -0
- package/artifacts/schema/session-tokens.schema.json +28 -0
- package/artifacts/schema/sign-up-request.schema.json +26 -0
- package/artifacts/schema/sign-up-response.schema.json +127 -0
- package/artifacts/schema/slug-usage-response.schema.json +32 -0
- package/artifacts/schema/snapshot-header.schema.json +44 -0
- package/artifacts/schema/snapshot-meta-response.schema.json +85 -0
- package/artifacts/schema/subscribe-error.schema.json +31 -0
- package/artifacts/schema/team-invite.schema.json +57 -0
- package/artifacts/schema/tier-change-request.schema.json +17 -0
- package/artifacts/schema/type-definition.schema.json +144 -0
- package/artifacts/schema/types-response.schema.json +156 -0
- package/artifacts/schema/update-app-request.schema.json +32 -0
- package/artifacts/schema/urdf-completeness.schema.json +50 -0
- package/artifacts/schema/validation-issue.schema.json +43 -0
- package/artifacts/schema/waitlist-request.schema.json +15 -0
- package/artifacts/schema-outgoing/bridge-asset-progress.schema.json +102 -0
- package/artifacts/schema-outgoing/bridge-assets-available.schema.json +26 -0
- package/artifacts/schema-outgoing/bridge-camera-state.schema.json +80 -0
- package/artifacts/schema-outgoing/bridge-config-applied.schema.json +69 -0
- package/artifacts/schema-outgoing/bridge-hello.schema.json +68 -0
- package/artifacts/schema-outgoing/bridge-introspect.schema.json +119 -0
- package/artifacts/schema-outgoing/bridge-job-lost.schema.json +23 -0
- package/artifacts/schema-outgoing/bridge-job-update.schema.json +102 -0
- package/artifacts/schema-outgoing/bridge-pong.schema.json +20 -0
- package/artifacts/schema-outgoing/bridge-type-definitions.schema.json +174 -0
- package/artifacts/schema-outgoing/datapoint-frame.schema.json +29 -0
- package/artifacts/schema-outgoing/snapshot-header.schema.json +45 -0
- package/dist/alerts.d.ts +255 -0
- package/dist/alerts.js +193 -0
- package/dist/app-users.d.ts +606 -0
- package/dist/app-users.js +696 -0
- package/dist/apps.d.ts +175 -0
- package/dist/apps.js +267 -0
- package/dist/assets.d.ts +434 -0
- package/dist/assets.js +546 -0
- package/dist/audit.d.ts +129 -0
- package/dist/audit.js +238 -0
- package/dist/client-auth.d.ts +409 -0
- package/dist/client-auth.js +487 -0
- package/dist/common.d.ts +186 -0
- package/dist/common.js +199 -0
- package/dist/config-issues.d.ts +175 -0
- package/dist/config-issues.js +339 -0
- package/dist/config.d.ts +862 -0
- package/dist/config.js +1988 -0
- package/dist/errors.d.ts +52 -0
- package/dist/errors.js +786 -0
- package/dist/identity.d.ts +549 -0
- package/dist/identity.js +503 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +51 -0
- package/dist/introspection.d.ts +99 -0
- package/dist/introspection.js +97 -0
- package/dist/jobs.d.ts +334 -0
- package/dist/jobs.js +345 -0
- package/dist/mcp.d.ts +239 -0
- package/dist/mcp.js +153 -0
- package/dist/oauth.d.ts +344 -0
- package/dist/oauth.js +488 -0
- package/dist/protocol.d.ts +781 -0
- package/dist/protocol.js +715 -0
- package/dist/realtime.d.ts +494 -0
- package/dist/realtime.js +512 -0
- package/dist/rest.d.ts +1989 -0
- package/dist/rest.js +1963 -0
- package/dist/routes.d.ts +94 -0
- package/dist/routes.js +2298 -0
- package/package.json +61 -0
package/dist/assets.js
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
/**
|
|
4
|
+
* The asset store (spec §4.6, W7).
|
|
5
|
+
*
|
|
6
|
+
* An **asset is an immutable file belonging to a robot**. It has a uuid and is
|
|
7
|
+
* fetched by it. There are no org-level assets and no public retrieval: every
|
|
8
|
+
* read is authenticated and role-checked (`assets`, §3.3).
|
|
9
|
+
*
|
|
10
|
+
* **One authorization model — the `Authorization` header.** Not a signed URL,
|
|
11
|
+
* not a cookie, not a token in a query string. The reason is not ergonomics
|
|
12
|
+
* but the number of mechanisms: a second credential would carry its own
|
|
13
|
+
* lifetime, its own renewal, its own rotation, and in every log the question
|
|
14
|
+
* of which token that was. Directus solves the same problem with a cookie and
|
|
15
|
+
* an `access_token` query parameter, and the cookie half does not transfer —
|
|
16
|
+
* Fleetless has no interface of its own (§1), so the consumers of a robot's
|
|
17
|
+
* assets sit on other origins.
|
|
18
|
+
*
|
|
19
|
+
* **The consequence, said out loud: this is not a CDN.** A shared cache must
|
|
20
|
+
* not store a response to a request carrying `Authorization`. Assets are
|
|
21
|
+
* immutable, so `Cache-Control: private, immutable` is correct and the
|
|
22
|
+
* browser's own cache works fully; nothing edge-caches. Calling it a CDN would
|
|
23
|
+
* promise something that does not happen. Signed URLs stay **additive** later —
|
|
24
|
+
* a second endpoint, not a migration — precisely because assets are immutable
|
|
25
|
+
* and uuid-addressed.
|
|
26
|
+
*
|
|
27
|
+
* ---
|
|
28
|
+
*
|
|
29
|
+
* **`texture` is its own member of `assetKind` and not `other` (W7a, D2).**
|
|
30
|
+
*
|
|
31
|
+
* Filing textures under `other` would be the catch-all this project has
|
|
32
|
+
* already split five times, and it costs a real capability: a client that
|
|
33
|
+
* renders a robot must know, from the asset list alone and before fetching
|
|
34
|
+
* anything, which bytes it has to pre-fetch. Every load in the browser goes
|
|
35
|
+
* through the SDK with the bearer token — there is no lazy second fetch a
|
|
36
|
+
* renderer can make on its own account — so "what must be in memory before
|
|
37
|
+
* anything renders" is a question the list has to be able to answer.
|
|
38
|
+
*
|
|
39
|
+
* A `texture` is an image referenced by the URDF's own `<material><texture>`
|
|
40
|
+
* **or** by a mesh file internally (a `.dae`'s `<init_from>`). Both are
|
|
41
|
+
* surfaces; neither is geometry; both must be resolvable by name.
|
|
42
|
+
*/
|
|
43
|
+
export const assetKind = z.enum(['urdf', 'mesh', 'texture', 'other']);
|
|
44
|
+
/**
|
|
45
|
+
* The `name` a URDF asset carries.
|
|
46
|
+
*
|
|
47
|
+
* A mesh names itself — its `package://` URI is the only string anyone can
|
|
48
|
+
* match against a workspace. A URDF has no such natural name, so producers
|
|
49
|
+
* agree on one: `robot_description`, after the topic it comes from.
|
|
50
|
+
*
|
|
51
|
+
* **Its job changed once the store stopped overwriting it.** It was pinned
|
|
52
|
+
* because the bridge and the cloud had settled on a value in conversation, and
|
|
53
|
+
* conversations drift — then the gate found the cloud renaming every URDF to
|
|
54
|
+
* `robot.urdf` regardless of what arrived, so the pinned value was decorative
|
|
55
|
+
* and the contract looked authoritative while being ignored. The store now
|
|
56
|
+
* keeps `asset.name` verbatim, which is what `asset.name`'s own comment
|
|
57
|
+
* already promised.
|
|
58
|
+
*
|
|
59
|
+
* So this is now a **convention among producers**, not an agreement the store
|
|
60
|
+
* enforces: it exists so two different bridges do not name the same thing two
|
|
61
|
+
* ways and leave a developer looking at an inconsistent fleet. Nothing
|
|
62
|
+
* validates it, and that is deliberate — the store's job is to record what it
|
|
63
|
+
* was told.
|
|
64
|
+
*
|
|
65
|
+
* Consumers must not match on it. `kind === 'urdf'` is the reliable test; the
|
|
66
|
+
* cloud identifies a robot's URDF row by kind alone, so a producer that sends
|
|
67
|
+
* a different string updates the same singleton rather than orphaning a second.
|
|
68
|
+
*/
|
|
69
|
+
export const URDF_ASSET_NAME = 'robot_description';
|
|
70
|
+
export const asset = z.object({
|
|
71
|
+
id: z.uuid().meta({ description: 'The asset\'s id in the store.' }),
|
|
72
|
+
robot_id: z.uuid().meta({ description: 'The robot this asset belongs to.' }),
|
|
73
|
+
kind: assetKind.meta({
|
|
74
|
+
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 it has to pre-fetch.',
|
|
75
|
+
}),
|
|
76
|
+
/**
|
|
77
|
+
* What the robot called it — for a mesh, the `package://` URI the URDF
|
|
78
|
+
* references, verbatim. That is the only string a developer can match
|
|
79
|
+
* against their own workspace, and matching is the whole job when a sync
|
|
80
|
+
* comes back incomplete.
|
|
81
|
+
*
|
|
82
|
+
* **The naming rule for a file nothing in the URDF names (W7a, D2).** A
|
|
83
|
+
* `.dae` carries its own image references — `<init_from>textures/skin.png`
|
|
84
|
+
* — resolved by the renderer against *the `.dae`'s own directory*, and no
|
|
85
|
+
* `package://` URI for them appears anywhere in the URDF. The rule is:
|
|
86
|
+
*
|
|
87
|
+
* name = the .dae's package:// URI, directory part,
|
|
88
|
+
* joined with the internal reference, normalized.
|
|
89
|
+
*
|
|
90
|
+
* So `package://rx1_description/meshes/arm.dae` referencing
|
|
91
|
+
* `textures/skin.png` uploads as
|
|
92
|
+
* `package://rx1_description/meshes/textures/skin.png`.
|
|
93
|
+
*
|
|
94
|
+
* **This is the design's single point of failure and it is stated before
|
|
95
|
+
* anything is built against it.** three.js resolves that internal reference
|
|
96
|
+
* relative to wherever it loaded the `.dae` from and asks the loading
|
|
97
|
+
* manager for the result; the client can only answer if the asset's name
|
|
98
|
+
* still carries the same **relative tail** (`textures/skin.png`) that the
|
|
99
|
+
* `.dae` asked for. Normalizing into a `package://` URI preserves that tail
|
|
100
|
+
* exactly, keeps every name in one namespace a developer already reads, and
|
|
101
|
+
* keeps `urdfCompleteness.missing` meaningful for files the URDF never
|
|
102
|
+
* mentioned.
|
|
103
|
+
*
|
|
104
|
+
* A reference that escapes its package (`../../etc/passwd`) is **not**
|
|
105
|
+
* renamed into something harmless — it is refused at the producer, by the
|
|
106
|
+
* same containment check W7's K2 fix applied to `package://` resolution.
|
|
107
|
+
* Two identical rules, one of which is enforced and one of which is
|
|
108
|
+
* documented, is how W7's traversal happened in the first place.
|
|
109
|
+
*/
|
|
110
|
+
name: z.string().min(1).max(500).meta({
|
|
111
|
+
description: 'What the robot called it — for a mesh, the `package://` URI the URDF references, verbatim, which is the only string a developer can match against their own workspace. A file the URDF never names (an image a `.dae` loads for itself) is named by joining the mesh\'s own directory with that internal reference.',
|
|
112
|
+
}),
|
|
113
|
+
media_type: z.string().min(1).max(120).meta({
|
|
114
|
+
description: 'The media type of the stored bytes, as the producer reported it.',
|
|
115
|
+
}),
|
|
116
|
+
size_bytes: z.number().int().nonnegative().meta({
|
|
117
|
+
description: 'How large the stored file is, in bytes.',
|
|
118
|
+
}),
|
|
119
|
+
/**
|
|
120
|
+
* The content hash, and the reason two robots sharing a mesh cost one copy.
|
|
121
|
+
*
|
|
122
|
+
* Exposed rather than kept internal because it is the only way a client can
|
|
123
|
+
* tell "this is the same mesh I already have" across robots — and a 3D view
|
|
124
|
+
* that re-downloads an identical arm for every robot in a fleet is the
|
|
125
|
+
* predictable failure of a store that hides it.
|
|
126
|
+
*/
|
|
127
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/).meta({
|
|
128
|
+
description: 'The content hash, lowercase hex, and the reason two robots sharing a mesh cost one copy. It is exposed because it is the only way a client can tell "this is the same mesh I already have" across robots.',
|
|
129
|
+
}),
|
|
130
|
+
created_at: z.iso.datetime().meta({
|
|
131
|
+
description: 'When the asset was first stored, as an ISO 8601 timestamp.',
|
|
132
|
+
}),
|
|
133
|
+
});
|
|
134
|
+
/**
|
|
135
|
+
* Whether a URDF can actually be rendered, which is not the same as whether it
|
|
136
|
+
* was uploaded.
|
|
137
|
+
*
|
|
138
|
+
* `missing` carries **the reference, verbatim, that no asset answers** — for
|
|
139
|
+
* a `package://` mesh the URI the bridge could not resolve in the workspace,
|
|
140
|
+
* and since W7's security fix also the absolute paths and bare relative paths
|
|
141
|
+
* a URDF may carry, which the extractor sees and the sync deliberately never
|
|
142
|
+
* offers. The sentence used to say "the `package://` URIs" and the field
|
|
143
|
+
* carried three kinds of string (Momus-W7); it is widened here rather than
|
|
144
|
+
* narrowed, because a developer whose URDF names `/opt/meshes/arm.stl` is
|
|
145
|
+
* entitled to be told that nothing will ever fetch it.
|
|
146
|
+
*
|
|
147
|
+
* The spec's example is "2 Meshes fehlen" and that number alone is a dead
|
|
148
|
+
* end: it tells a developer to go looking through a workspace by hand. The
|
|
149
|
+
* references are what they can act on, so the references travel.
|
|
150
|
+
*
|
|
151
|
+
* **Every entry must be actionable, and that is a constraint on the
|
|
152
|
+
* producers, not on this field (W7a).** An entry a developer cannot make
|
|
153
|
+
* disappear by fixing what it names is a defect in whoever put it there: for
|
|
154
|
+
* a whole wave `<texture>` references were listed here and no sync would ever
|
|
155
|
+
* offer them, so the honest instruction behind the list was "fix this, it
|
|
156
|
+
* will not help".
|
|
157
|
+
*/
|
|
158
|
+
export const urdfCompleteness = z.object({
|
|
159
|
+
present: z.boolean().meta({
|
|
160
|
+
description: 'Whether a URDF has been synced at all. Whether one *could* be synced is a different question, answered by `urdf_available`.',
|
|
161
|
+
}),
|
|
162
|
+
mesh_count: z.number().int().nonnegative().meta({
|
|
163
|
+
description: 'How many distinct meshes the URDF references.',
|
|
164
|
+
}),
|
|
165
|
+
/**
|
|
166
|
+
* **Was fehlt, und wovon (W9b, DEF-081).**
|
|
167
|
+
*
|
|
168
|
+
* Vorher ein blankes `string[]`. Die Console meldete daraufhin *„N meshes
|
|
169
|
+
* missing from the workspace"* — auch für eine fehlende **Textur**, während
|
|
170
|
+
* `mesh_count` daneben eine andere Zahl nannte: zwei Angaben über denselben
|
|
171
|
+
* Gegenstand, die einander widersprechen.
|
|
172
|
+
*
|
|
173
|
+
* Die Cloud wusste es die ganze Zeit: `extractReferencesByElement` markiert
|
|
174
|
+
* jede Referenz mit ihrem Element und `buildAssetListResponse` warf die
|
|
175
|
+
* Markierung wieder weg. **Die Antwort im Client zu raten wäre genau die
|
|
176
|
+
* „neue Kopie", die die Registerzeile ausdrücklich ablehnt** — eine zweite
|
|
177
|
+
* Herleitung derselben Tatsache, die von der ersten abweichen kann.
|
|
178
|
+
*/
|
|
179
|
+
missing: z.array(z.object({
|
|
180
|
+
uri: z.string().min(1).max(500).meta({
|
|
181
|
+
description: 'The reference, verbatim, that no stored asset answers — a `package://` URI the workspace does not hold, or an absolute or bare relative path nothing will ever fetch. A developer whose URDF names one of the latter is entitled to be told so.',
|
|
182
|
+
}),
|
|
183
|
+
element: z.enum(['mesh', 'texture']).meta({
|
|
184
|
+
description: 'Which kind of reference it was: geometry the URDF names as a `mesh`, or a `texture` a surface paints with. Without it a client reports a missing texture as a missing mesh, contradicting `mesh_count` beside it.',
|
|
185
|
+
}),
|
|
186
|
+
})).meta({
|
|
187
|
+
description: 'The references nothing in the store answers, each with the element that asked for it. A bare count is a dead end that sends a developer hunting through a workspace by hand; the references are what they can act on, so the references travel.',
|
|
188
|
+
}),
|
|
189
|
+
});
|
|
190
|
+
/**
|
|
191
|
+
* A sync is long-running and is therefore answered with something to watch,
|
|
192
|
+
* never with a status that was true at the moment of asking.
|
|
193
|
+
*
|
|
194
|
+
* **`source` has one value, and that is deliberate.** §4.6 also names a manual
|
|
195
|
+
* zip upload, and the first version of this shape had `'upload'` in the enum —
|
|
196
|
+
* with **no body defined for the bytes**. An enum value with no producer and
|
|
197
|
+
* no payload invites every consumer to guess a shape, and each guesses
|
|
198
|
+
* differently; that is a defect this project has deliberately refused to
|
|
199
|
+
* introduce before, when an error code was proposed whose payload had moved.
|
|
200
|
+
* The zip path stays a condition rather than sitting in the wire as a
|
|
201
|
+
* promise.
|
|
202
|
+
*
|
|
203
|
+
* A single-member enum rather than dropping the field: the second source is a
|
|
204
|
+
* question of when, not whether, and a caller that already names its source
|
|
205
|
+
* does not change shape when the second one arrives.
|
|
206
|
+
*
|
|
207
|
+
* Caught by Eve-W7 asking what body `'upload'` takes, rather than building
|
|
208
|
+
* against a guess.
|
|
209
|
+
*/
|
|
210
|
+
export const assetSyncRequest = z.object({
|
|
211
|
+
source: z.enum(['bridge']).meta({
|
|
212
|
+
description: 'Where the bytes come from. `bridge` is the only value today: the connected bridge reads them from the robot\'s own workspace. It is validated rather than ignored, so a caller naming a source that does not exist yet learns that instead of silently getting a bridge sync.',
|
|
213
|
+
}),
|
|
214
|
+
}).strict();
|
|
215
|
+
export const assetSyncResponse = z.object({
|
|
216
|
+
sync_id: z.uuid().meta({
|
|
217
|
+
description: 'The sync that has just started. A sync is long-running, so the answer is something to watch rather than a status that was true at the moment of asking.',
|
|
218
|
+
}),
|
|
219
|
+
});
|
|
220
|
+
/**
|
|
221
|
+
* Progress of one sync.
|
|
222
|
+
*
|
|
223
|
+
* `failed` names the URIs that could not be resolved, and it is required
|
|
224
|
+
* rather than optional: a sync that drops three meshes and reports success is
|
|
225
|
+
* worse than one that fails outright, because the failure surfaces later, in a
|
|
226
|
+
* renderer, as a robot with missing limbs and no explanation.
|
|
227
|
+
*
|
|
228
|
+
* **`failed` carries per-reference facts and `reason` carries the sync's own,
|
|
229
|
+
* and that split is what M7 bought.** Three different kinds of string used to
|
|
230
|
+
* reach `failed`: unresolvable `package://` URIs (the documented meaning), the
|
|
231
|
+
* literal `robot_description` when a URDF *upload* failed, and English
|
|
232
|
+
* sentences written by the cloud — "the robot disconnected mid-sync". The
|
|
233
|
+
* console printed the array under *"these meshes could not be resolved"*, so a
|
|
234
|
+
* developer whose robot dropped was told to go find a mesh named *the robot
|
|
235
|
+
* disconnected mid-sync* (Momus-W7, M7).
|
|
236
|
+
*
|
|
237
|
+
* **Two of those three were defects and the third was not, which this
|
|
238
|
+
* paragraph used to get wrong** (Argus-W7a, W7a review, reading the file top to
|
|
239
|
+
* bottom). The cloud's English sentences do not belong here — that is what
|
|
240
|
+
* `reason` is for. But `robot_description` is a **legitimate** entry: the URDF
|
|
241
|
+
* is an asset that can fail to upload like any other, and W7a made that
|
|
242
|
+
* explicit rather than removing it — see `assetFailure.reference`, which says
|
|
243
|
+
* in as many words that not every entry is a mesh URI and a consumer must not
|
|
244
|
+
* assume one. Read together with the old sentence *"`failed` is URIs and
|
|
245
|
+
* nothing else"*, this file told a consumer the same value was both a defect
|
|
246
|
+
* and a documented case.
|
|
247
|
+
*
|
|
248
|
+
* So the rule is: **anything that is not about one specific reference goes in
|
|
249
|
+
* `reason`** — one human-readable sentence
|
|
250
|
+
* about why the sync ended as it did, `null` when the outcome speaks for
|
|
251
|
+
* itself. It also carries the distinction `bridgeAssetProgress.state` makes
|
|
252
|
+
* and this shape could not — a sync **refused** because another was in flight
|
|
253
|
+
* is not a sync that tried and failed.
|
|
254
|
+
*
|
|
255
|
+
* **`assetSyncState` was deliberately not widened to carry that.** Consumers
|
|
256
|
+
* switch on it, a new member silently changes what every existing switch
|
|
257
|
+
* covers, and "refused" is a *reason* for a terminal outcome rather than a
|
|
258
|
+
* different one. Adding a field is additive; adding an enum member is not.
|
|
259
|
+
*/
|
|
260
|
+
/**
|
|
261
|
+
* **Der Deckel, den beide Seiten kennen müssen (W9b).**
|
|
262
|
+
*
|
|
263
|
+
* Bis hierher hatte die Bridge eine eigene Zahl und die Cloud eine eigene, und
|
|
264
|
+
* die Registerzeile dazu nannte die der Bridge beim Namen: *„a guess … chosen
|
|
265
|
+
* as a starting number with no measurement behind it"* (DEF-127). Eine Grenze,
|
|
266
|
+
* die der Sender rät und der Empfänger durchsetzt, ist keine Grenze — sie ist
|
|
267
|
+
* zwei Zahlen, die zufällig übereinstimmen, bis eine von beiden sich ändert.
|
|
268
|
+
*
|
|
269
|
+
* Hier steht sie einmal. Die Bridge liest sie, **bevor** sie eine Datei in den
|
|
270
|
+
* Speicher liest; die Cloud setzt sie durch. Ohne das kann die Bridge gar nicht
|
|
271
|
+
* ablehnen, ohne 194 MB zu puffern — was am 2026-08-18 auf rx1 genau so passiert
|
|
272
|
+
* ist (DEF-148).
|
|
273
|
+
*
|
|
274
|
+
* **Sie gilt je Datei, nicht je Sync**, und das steht hier, weil DEF-127 es
|
|
275
|
+
* ausdrücklich verlangt hat: acht Meshes zu je 30 MiB passen durch, eine Datei
|
|
276
|
+
* zu 65 MiB nicht. Wer sie für eine Obergrenze der Übertragung hält, rechnet
|
|
277
|
+
* mit einer Schranke, die es nicht gibt — die Summe eines Syncs bindet das
|
|
278
|
+
* Speicherkontingent der Organisation, und das ist eine andere Zahl an einer
|
|
279
|
+
* anderen Stelle.
|
|
280
|
+
*
|
|
281
|
+
* **Die Zahl ist bewusst unverändert, und die Begründung hat zwei Fassungen
|
|
282
|
+
* gebraucht — die Korrektur ist hier mehr wert als das Ergebnis.**
|
|
283
|
+
*
|
|
284
|
+
* Zuerst stand hier: *„rx1s echte Meshes sind gemessen — `base.dae`
|
|
285
|
+
* 193.886.766 Bytes, also das 2,9-fache"*, im Präsens, als stünde das über
|
|
286
|
+
* der heute laufenden Beschreibung. Gemessen am 2026-08-19 gegen die
|
|
287
|
+
* **laufende** `robot_description`: acht `package://`-Referenzen, zusammen
|
|
288
|
+
* 89.379.096 Bytes, die größte `RX1.dae` mit 38.229.621 — **keine über dem
|
|
289
|
+
* Deckel.**
|
|
290
|
+
*
|
|
291
|
+
* Daraus habe ich dann geschlossen, `base.dae` werde *von keiner* rx1-URDF
|
|
292
|
+
* referenziert. **Auch das war falsch, und zwar weil ich nur den aktuellen
|
|
293
|
+
* Workspace geprüft hatte.** `src.old-20260730/rx1` und `.../rx1_linac`
|
|
294
|
+
* referenzieren beide `base.dae` **und** `base.stl` und kennen `RX1.dae`
|
|
295
|
+
* nicht — das ist die Beschreibung, die rx1 am 2026-08-18 lief, als DEF-148
|
|
296
|
+
* gemessen wurde. Die Beobachtung von damals war korrekt und ihre Erklärung
|
|
297
|
+
* auch.
|
|
298
|
+
*
|
|
299
|
+
* Was heute gilt: **welche Beschreibung rx1 fährt, entscheidet, ob der Deckel
|
|
300
|
+
* reicht** — die aktuelle passt mit Abstand hinein, die vorherige um das
|
|
301
|
+
* 2,9-fache nicht. Das ist keine Vertragsfrage, sondern eine über Speicher,
|
|
302
|
+
* Übertragungszeit und Kontingente, und sie liegt bei André. Sie blockiert
|
|
303
|
+
* nichts: W9bs Gate-Schritt 6 ist gegen die heute laufende Beschreibung
|
|
304
|
+
* erreichbar, ohne dass jemand eine Zahl anfasst.
|
|
305
|
+
*/
|
|
306
|
+
export const ASSET_UPLOAD_MAX_BYTES = 64 * 1024 * 1024;
|
|
307
|
+
export const assetTooLargeDetails = z.object({
|
|
308
|
+
limit_bytes: z.number().int().positive().meta({
|
|
309
|
+
description: 'The upload ceiling, in bytes.',
|
|
310
|
+
}),
|
|
311
|
+
size_bytes: z.number().int().positive().meta({
|
|
312
|
+
description: 'How large the refused file actually 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.',
|
|
313
|
+
}),
|
|
314
|
+
});
|
|
315
|
+
/**
|
|
316
|
+
* Why one reference did not make it into the store.
|
|
317
|
+
*
|
|
318
|
+
* Three kinds because three things were already happening and only one word
|
|
319
|
+
* was available for them:
|
|
320
|
+
*
|
|
321
|
+
* - **`unresolvable`** — the reference names nothing the producer can find, or
|
|
322
|
+
* nothing it is allowed to read (a `package://` URI absent from the
|
|
323
|
+
* workspace, an absolute path, a `.dae`-internal reference escaping its own
|
|
324
|
+
* package). **Permanent.** No retry changes it, and it is the only kind a
|
|
325
|
+
* reconciliation may treat as gone.
|
|
326
|
+
* - **`upload_failed`** — the bytes exist and the transfer did not succeed.
|
|
327
|
+
* **Transient.** The asset is still wanted; a later sync will carry it.
|
|
328
|
+
* - **`refused`** — never attempted, because a producer-side ceiling was hit
|
|
329
|
+
* (R9: a `.dae` with more internal references than one file or one sync will
|
|
330
|
+
* report). **Transient in the same sense**: nothing is known to be missing,
|
|
331
|
+
* only unexamined.
|
|
332
|
+
*
|
|
333
|
+
* A consumer that cannot act on the distinction may still print `reference`
|
|
334
|
+
* alone and lose nothing it had before.
|
|
335
|
+
*/
|
|
336
|
+
export const assetFailureKind = z.enum(['unresolvable', 'upload_failed', 'refused', 'too_large']);
|
|
337
|
+
export const assetFailure = z.object({
|
|
338
|
+
/**
|
|
339
|
+
* What could not be provided, verbatim — the same string `asset.name` would
|
|
340
|
+
* have stored and `urdfCompleteness.missing` reports, so a developer can
|
|
341
|
+
* match it against their own workspace by eye. For a URDF upload failure it
|
|
342
|
+
* is `URDF_ASSET_NAME`, which is **not** a mesh URI: a consumer rendering
|
|
343
|
+
* this list must not assume every entry is one.
|
|
344
|
+
*/
|
|
345
|
+
reference: z.string().min(1).max(500).meta({
|
|
346
|
+
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.',
|
|
347
|
+
}),
|
|
348
|
+
kind: assetFailureKind.meta({
|
|
349
|
+
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`.',
|
|
350
|
+
}),
|
|
351
|
+
/**
|
|
352
|
+
* **Die zwei Zahlen, und warum `too_large` eine eigene Art ist (W9b).**
|
|
353
|
+
*
|
|
354
|
+
* `refused` bedeutet *„nie versucht, weil eine Decke des Erzeugers erreicht
|
|
355
|
+
* wurde"* — das passt auf eine Datei, die wegen ihrer Größe gar nicht erst
|
|
356
|
+
* gelesen wurde, **und ebenso auf die Sammel-Sentinel**, mit der ein Sync
|
|
357
|
+
* aufhört, einzelne Fehler zu benennen. Beides unter eine Art zu legen wäre
|
|
358
|
+
* derselbe Fehler, den W9a eine Welle zuvor ausgeräumt hat: zwei Fakten auf
|
|
359
|
+
* einem Schlüssel, von denen jeder den anderen überschreibt.
|
|
360
|
+
*
|
|
361
|
+
* Und ein Grund ohne Zahlen ist kein Grund, mit dem jemand etwas anfangen
|
|
362
|
+
* kann. *„Zu groß"* beantwortet nicht, ob das Mesh zu verkleinern ist oder
|
|
363
|
+
* die Grenze zu heben — `limit_bytes` und `size_bytes` tun es.
|
|
364
|
+
*
|
|
365
|
+
* Abwesend für jede andere Art — ein erzwungenes `details: null` auf jedem
|
|
366
|
+
* `unresolvable` kauft nichts. Die Paarung ist unten **erzwungen**, nicht
|
|
367
|
+
* beschrieben: ein Feld, dessen Regel nur im Kommentar steht, ist eine
|
|
368
|
+
* Bitte.
|
|
369
|
+
*/
|
|
370
|
+
details: assetTooLargeDetails.nullish().meta({
|
|
371
|
+
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.',
|
|
372
|
+
}),
|
|
373
|
+
}).superRefine((f, ctx) => {
|
|
374
|
+
// **Erzwungen, nicht beschrieben.** Eine Regel, die nur im Kommentar steht,
|
|
375
|
+
// ist eine Bitte — und dieses Projekt hat mehrfach erlebt, dass ein Feld,
|
|
376
|
+
// dessen Bedeutung nur daneben stand, mit etwas anderem gefüllt wurde.
|
|
377
|
+
if (f.kind === 'too_large' && f.details == null) {
|
|
378
|
+
ctx.addIssue({ code: 'custom', path: ['details'], message: '`too_large` without limit_bytes/size_bytes says nothing a developer can act on' });
|
|
379
|
+
}
|
|
380
|
+
if (f.kind !== 'too_large' && f.details != null) {
|
|
381
|
+
ctx.addIssue({ code: 'custom', path: ['details'], message: 'size details belong to `too_large` only' });
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
export const assetSyncState = z.enum(['running', 'succeeded', 'failed']);
|
|
385
|
+
export const assetSyncStatus = z.object({
|
|
386
|
+
sync_id: z.uuid().meta({ description: 'The sync this status describes.' }),
|
|
387
|
+
robot_id: z.uuid().meta({ description: 'The robot whose assets are being synced.' }),
|
|
388
|
+
state: assetSyncState.meta({
|
|
389
|
+
description: 'Whether the sync is still `running`, or ended `succeeded` or `failed`. It ends `succeeded` only when nothing was left behind: a single entry in `failed` makes the whole sync `failed`.',
|
|
390
|
+
}),
|
|
391
|
+
done: z.number().int().nonnegative().meta({
|
|
392
|
+
description: 'How many files have been transferred so far.',
|
|
393
|
+
}),
|
|
394
|
+
total: z.number().int().nonnegative().meta({
|
|
395
|
+
description: 'How many files this sync set out to transfer. It is `0` until the producer has finished working out what there is.',
|
|
396
|
+
}),
|
|
397
|
+
/**
|
|
398
|
+
* **Every entry says *why*, because reconciliation could not work without
|
|
399
|
+
* it and a developer could not read it without it** (W7a review, André's
|
|
400
|
+
* decision to fix rather than defer).
|
|
401
|
+
*
|
|
402
|
+
* It was a flat `string[]`, and **six producers wrote three different facts
|
|
403
|
+
* into it indistinguishably**: a reference that resolves to nothing in the
|
|
404
|
+
* workspace, a file that exists and whose transfer failed, and — since R9's
|
|
405
|
+
* ceiling — one that was never attempted at all. The cost was paid twice
|
|
406
|
+
* over. Reconciliation cannot tell *"no longer referenced"* from
|
|
407
|
+
* *"referenced and not delivered"*, so N14 had to decline reconciling **any**
|
|
408
|
+
* partial sync, leaving legitimately-removed assets stored and charged until
|
|
409
|
+
* the next clean one. And the console prints the whole array under *"these
|
|
410
|
+
* meshes could not be resolved"*, so a URDF upload failure — which arrives
|
|
411
|
+
* as the literal `robot_description` — is shown to a developer as a mesh
|
|
412
|
+
* they should go and find.
|
|
413
|
+
*
|
|
414
|
+
* `unresolvable` is the only kind reconciliation may drop: it is the only
|
|
415
|
+
* one that means *this will not come back*. `upload_failed` and `refused`
|
|
416
|
+
* both mean *we meant to provide this and did not*, which is the distinction
|
|
417
|
+
* the union needs and the field could not carry.
|
|
418
|
+
*
|
|
419
|
+
* **Bounded, and the bound is a rule this file already wrote down one field
|
|
420
|
+
* over** (Kassandra-W7a, W7a review). `asset.name` is `max(500)`; the same
|
|
421
|
+
* names travelling here had no per-entry cap and no array cap at all.
|
|
422
|
+
*
|
|
423
|
+
* Why it matters became reachable in W7a. Before R6 an unresolvable
|
|
424
|
+
* reference was silently dropped, so this array could only grow with files
|
|
425
|
+
* that existed and failed to upload — bounded by the workspace. Once a
|
|
426
|
+
* `.dae`'s internal references are reported, **one mesh reference expands
|
|
427
|
+
* into a list bounded only by that file's own text.** Measured: a
|
|
428
|
+
* 2,120,745-byte `.dae` with 17,331 unresolvable `<init_from>` refs produces
|
|
429
|
+
* a terminal frame of 2,097,184 bytes — **32 bytes over
|
|
430
|
+
* `MAX_WS_PAYLOAD_BYTES`** — and `ws` enforces `maxPayload` before the frame
|
|
431
|
+
* is delivered, so the outcome is not a dropped frame but **the robot's
|
|
432
|
+
* socket closed, mid-sync, by a file in its own workspace.**
|
|
433
|
+
*
|
|
434
|
+
* A producer that hits its own ceiling reports **one** entry saying so
|
|
435
|
+
* rather than growing the list — the discipline gate step 5 already demands
|
|
436
|
+
* of the upload rate limit: *fail naming the limit, rather than silently
|
|
437
|
+
* reporting resolvable meshes as missing.*
|
|
438
|
+
*
|
|
439
|
+
* 1000 x 500 bytes is ~0.5 MiB of names, comfortably inside a 2 MiB frame.
|
|
440
|
+
*/
|
|
441
|
+
failed: z.array(assetFailure).max(1000).meta({
|
|
442
|
+
description: 'What could not be provided, one entry per reference, each saying why. Required rather than optional: a sync that quietly drops three meshes and reports success moves the failure into somebody\'s renderer, where it shows up as a robot with missing limbs and no cause. At most `1000` entries — a producer at its own ceiling reports one entry saying so rather than growing the list.',
|
|
443
|
+
}),
|
|
444
|
+
reason: z.string().min(1).nullable().meta({
|
|
445
|
+
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.',
|
|
446
|
+
}),
|
|
447
|
+
started_at: z.iso.datetime().meta({
|
|
448
|
+
description: 'When the sync started, as an ISO 8601 timestamp.',
|
|
449
|
+
}),
|
|
450
|
+
updated_at: z.iso.datetime().meta({
|
|
451
|
+
description: 'When this status last changed, as an ISO 8601 timestamp. A sync that stops moving is visible here rather than only in `state`.',
|
|
452
|
+
}),
|
|
453
|
+
});
|
|
454
|
+
export const assetListResponse = z.object({
|
|
455
|
+
assets: z.array(asset).meta({
|
|
456
|
+
description: 'Every asset stored for this robot: the URDF, the meshes it references, and the textures those paint with.',
|
|
457
|
+
}),
|
|
458
|
+
/**
|
|
459
|
+
* Der gerade laufende Sync, oder `null` (W9b, DEF-147).
|
|
460
|
+
*
|
|
461
|
+
* **Der Fall, für den das hier steht, ist der Neuladen-Fall.** Die Console
|
|
462
|
+
* hielt die `sync_id` nur im Speicher; ein Reload verlor die Fortschritts-
|
|
463
|
+
* anzeige, und der Zustand war serverseitig da, über
|
|
464
|
+
* `GET .../assets/sync/<id>` abfragbar — nur erreichte ihn niemand mehr, der
|
|
465
|
+
* die id nicht aufgehoben hatte. Eine Seite, die frisch lädt, drückt keinen
|
|
466
|
+
* Knopf; sie fragt diese Liste. Also muss die Liste es sagen.
|
|
467
|
+
*/
|
|
468
|
+
active_sync: assetSyncStatus.nullable().meta({
|
|
469
|
+
description: 'The sync running right now, or `null`. It is on this list so a page that reloads and has lost the sync id can still show progress — a freshly loaded page presses no button, it asks this list.',
|
|
470
|
+
}),
|
|
471
|
+
urdf: urdfCompleteness.meta({
|
|
472
|
+
description: 'Whether the stored URDF can actually be rendered, and what it is still missing. Not the same question as whether one was uploaded.',
|
|
473
|
+
}),
|
|
474
|
+
/**
|
|
475
|
+
* What the connected bridge says it *could* transfer, which is deliberately
|
|
476
|
+
* separate from what has been transferred (§4.6: the bridge "meldet nur
|
|
477
|
+
* Verfügbarkeit"). `null` when no bridge is connected — distinct from
|
|
478
|
+
* `false`, because "no robot is online to ask" and "the robot has no URDF"
|
|
479
|
+
* send a developer to two different places.
|
|
480
|
+
*
|
|
481
|
+
* **All three states are reachable as of W7a (R7).** They were not: the bridge
|
|
482
|
+
* used to report availability from a subscription callback, which fires only
|
|
483
|
+
* when a publisher *sends* something, so it could notice presence and never
|
|
484
|
+
* absence — a robot that lost its URDF left the cloud holding the last thing
|
|
485
|
+
* it heard, forever, and `true` was sticky. The fix is an **active**
|
|
486
|
+
* `count_publishers` query on the bridge's own timer.
|
|
487
|
+
*
|
|
488
|
+
* **What a consumer still needs to know is the clock, not the gap.** An
|
|
489
|
+
* ungraceful loss — the publisher process killed rather than shut down — is
|
|
490
|
+
* noticed on **DDS's liveliness timeout**, not on the bridge's check
|
|
491
|
+
* interval. Measured against a real bridge: ~1.6 s when the publisher calls
|
|
492
|
+
* `destroy_node()`, **~19 s when it is `SIGKILL`ed**. So `true` can outlive
|
|
493
|
+
* the truth by some seconds after a crash, and no amount of polling on our
|
|
494
|
+
* side shortens it.
|
|
495
|
+
*
|
|
496
|
+
* The sticky-`true` gap was found by Rosie-W7 checking her own work against
|
|
497
|
+
* the camera-health row of identical shape; the DDS clock was measured by
|
|
498
|
+
* Rosie-W7a closing it, and this comment was still describing the gap a wave
|
|
499
|
+
* after it was fixed (Momus-W7a, W7a review).
|
|
500
|
+
*/
|
|
501
|
+
urdf_available: z.boolean().nullable().meta({
|
|
502
|
+
description: 'What the connected bridge says it *could* transfer, which is deliberately separate from what has been transferred. `null` when no bridge is connected — distinct from `false`, because "no robot is online to ask" and "the robot has no URDF" send a developer to two 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.',
|
|
503
|
+
}),
|
|
504
|
+
});
|
|
505
|
+
/**
|
|
506
|
+
* The query of `GET /api/robots/:id/assets/missing`, the placeholder a
|
|
507
|
+
* rewritten URDF points at for a mesh Fleetless does not hold.
|
|
508
|
+
*
|
|
509
|
+
* **The route answers `404` either way** — this parameter changes the sentence,
|
|
510
|
+
* never the outcome. It is echoed back into the refusal message so a developer
|
|
511
|
+
* reading a failed mesh load learns *which* reference did not resolve; absent,
|
|
512
|
+
* the message says `unknown`. Echoing it discloses nothing, since it is what
|
|
513
|
+
* the caller itself sent.
|
|
514
|
+
*/
|
|
515
|
+
export const missingAssetQuery = z
|
|
516
|
+
.object({
|
|
517
|
+
name: z.string().optional().meta({
|
|
518
|
+
description: 'The unresolved reference, as the URDF spelled it, echoed into the `404 asset_missing` message. Omitted, the message names `unknown` instead. It never changes the status.',
|
|
519
|
+
}),
|
|
520
|
+
})
|
|
521
|
+
.meta({ description: 'The one optional parameter of the missing-asset placeholder; it names the reference in the refusal.' });
|
|
522
|
+
/**
|
|
523
|
+
* What an `asset_too_large` refusal tells the caller — the same discipline as
|
|
524
|
+
* `publisher_busy` and `job_queue_full`: a refusal that names a state and no
|
|
525
|
+
* number leaves the caller unable to decide anything.
|
|
526
|
+
*/
|
|
527
|
+
/**
|
|
528
|
+
* Was eine `busy`-Absage beim Asset-Sync mitgeben muss (W9b, DEF-147).
|
|
529
|
+
*
|
|
530
|
+
* Vorher antwortete die Cloud *„Robot <id> already has a sync in progress"* —
|
|
531
|
+
* eine Absage, die einen **Zustand** benennt, aber nicht das **Ding** in diesem
|
|
532
|
+
* Zustand. Der laufende Sync ist serverseitig beobachtbar und über
|
|
533
|
+
* `GET .../assets/sync/<id>` abfragbar, nur erreichte ihn niemand mehr, der die
|
|
534
|
+
* id nicht aufgehoben hatte. Genau die Form, die W6b eine ganze Welle lang
|
|
535
|
+
* ausgeräumt hat: ein Abbruch ohne Job-Id, eine Freigabe ohne Session-Id.
|
|
536
|
+
*
|
|
537
|
+
* **Das allein genügt nicht**, und deshalb steht daneben `activeSync` auf der
|
|
538
|
+
* Asset-Liste: Diese Details helfen nur dem, der den Knopf noch einmal drückt.
|
|
539
|
+
* Wer die Seite neu lädt — der Fall, den André am 2026-08-18 hatte —, drückt
|
|
540
|
+
* gar nichts und braucht den laufenden Sync im ersten `GET`.
|
|
541
|
+
*/
|
|
542
|
+
export const assetSyncBusyDetails = z.object({
|
|
543
|
+
sync_id: z.uuid(),
|
|
544
|
+
/** Wann er begann — damit „läuft noch" von „hängt seit einer Stunde" unterscheidbar ist. */
|
|
545
|
+
started_at_ms: z.number().int().nonnegative(),
|
|
546
|
+
});
|