@fleetless/contracts 1.0.0 → 1.0.2
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 +68 -2
- package/CODE_OF_CONDUCT.md +83 -0
- package/CONTRIBUTING.md +136 -0
- package/README.md +49 -13
- package/SECURITY.md +55 -0
- package/artifacts/openapi.json +1 -1
- package/artifacts/routes.json +1 -1
- package/dist/alerts.d.ts +19 -24
- package/dist/alerts.js +18 -24
- package/dist/app-users.d.ts +7 -6
- package/dist/app-users.js +6 -6
- package/dist/apps.d.ts +21 -25
- package/dist/apps.js +40 -51
- package/dist/assets.d.ts +70 -132
- package/dist/assets.js +130 -223
- package/dist/audit.d.ts +11 -11
- package/dist/audit.js +25 -51
- package/dist/client-auth.d.ts +4 -4
- package/dist/client-auth.js +3 -4
- package/dist/common.d.ts +27 -35
- package/dist/common.js +26 -35
- package/dist/config-issues.d.ts +4 -3
- package/dist/config-issues.js +7 -6
- package/dist/config.d.ts +31 -37
- package/dist/config.js +81 -110
- package/dist/errors.d.ts +4 -3
- package/dist/errors.js +61 -87
- package/dist/identity.d.ts +18 -21
- package/dist/identity.js +17 -21
- package/dist/index.d.ts +4 -4
- package/dist/index.js +12 -13
- package/dist/introspection.d.ts +7 -6
- package/dist/introspection.js +6 -6
- package/dist/jobs.d.ts +12 -12
- package/dist/jobs.js +20 -25
- package/dist/mcp.d.ts +11 -12
- package/dist/mcp.js +10 -12
- package/dist/oauth.d.ts +13 -18
- package/dist/oauth.js +13 -19
- package/dist/protocol.d.ts +51 -62
- package/dist/protocol.js +107 -139
- package/dist/realtime.d.ts +53 -68
- package/dist/realtime.js +77 -103
- package/dist/rest.d.ts +182 -243
- package/dist/rest.js +301 -395
- package/dist/routes.d.ts +4 -3
- package/dist/routes.js +3 -2
- package/package.json +12 -7
package/dist/audit.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { wireSeqCursor, wireTimestampMs } from './common.js';
|
|
4
4
|
/**
|
|
5
|
-
|
|
6
|
-
*
|
|
5
|
+
/**
|
|
6
|
+
* Audit. Every state-changing interaction is recorded and **every entry carries
|
|
7
|
+
* its actor — never anonymous**. Reads are not audited.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* 2026-08-10) — same shape of work as the history API.
|
|
9
|
+
* What is written: logins, failed logins, user management, configuration
|
|
10
|
+
* publishes, bridge connect and disconnect. The log is filterable, exportable
|
|
11
|
+
* as CSV, and kept for ninety days.
|
|
12
12
|
*/
|
|
13
13
|
/**
|
|
14
14
|
* The kinds of actor the platform knows. `label` is what a human reads in the
|
|
@@ -42,8 +42,7 @@ export const auditEvent = z.object({
|
|
|
42
42
|
org_id: z.uuid(),
|
|
43
43
|
at: z.iso.datetime(),
|
|
44
44
|
/**
|
|
45
|
-
* A monotonic counter, ascending in write order, unique across the log
|
|
46
|
-
* (W6b).
|
|
45
|
+
* A monotonic counter, ascending in write order, unique across the log.
|
|
47
46
|
*
|
|
48
47
|
* `at` is not a total order. Two events written in the same millisecond —
|
|
49
48
|
* a login and the config publish it enables, a cascade writing several
|
|
@@ -55,11 +54,7 @@ export const auditEvent = z.object({
|
|
|
55
54
|
*
|
|
56
55
|
* It is also the only correct **cursor** for paging this log, for the same
|
|
57
56
|
* reason: a cursor that is not unique either skips rows or repeats them at
|
|
58
|
-
* every page boundary.
|
|
59
|
-
* the route returns the whole log — and that is stated here rather than
|
|
60
|
-
* implied, because a contract that describes a capability the API does not
|
|
61
|
-
* have is the defect this project keeps finding. When paging is added it
|
|
62
|
-
* uses this field; nothing else in this shape can carry it.
|
|
57
|
+
* every page boundary. Nothing else in this shape can carry one.
|
|
63
58
|
*
|
|
64
59
|
* Required, not optional: an event without a sequence cannot be ordered
|
|
65
60
|
* against one that has it, and a log with two orderings has none.
|
|
@@ -94,12 +89,7 @@ export const auditEvent = z.object({
|
|
|
94
89
|
details: z.record(z.string(), z.unknown()).nullable(),
|
|
95
90
|
});
|
|
96
91
|
/**
|
|
97
|
-
* **How this log is read
|
|
98
|
-
*
|
|
99
|
-
* Until now `GET /api/audit` returned the **whole** log — no filters, no
|
|
100
|
-
* cursor. `auditEvent.seq`'s own comment has said so plainly since W6b rather
|
|
101
|
-
* than describing a capability the API does not have; this shape builds
|
|
102
|
-
* exactly what that comment announced.
|
|
92
|
+
* **How this log is read.**
|
|
103
93
|
*
|
|
104
94
|
* **The cursor is `seq`, and no other field can be.** `at` is not a total
|
|
105
95
|
* order: two events written in the same millisecond sort differently on every
|
|
@@ -109,10 +99,6 @@ export const auditEvent = z.object({
|
|
|
109
99
|
*
|
|
110
100
|
* `before_seq` rather than `after_seq`, because this log is read **newest
|
|
111
101
|
* first**: the next page is older, not newer.
|
|
112
|
-
*
|
|
113
|
-
* **Filters are part of the same work, not a later garnish.** A console view
|
|
114
|
-
* without them is a page with nothing to filter by — the register row says
|
|
115
|
-
* exactly that, which is why the two rows are one piece of work.
|
|
116
102
|
*/
|
|
117
103
|
/**
|
|
118
104
|
* A unix-millisecond bound a Postgres `timestamptz` can actually hold.
|
|
@@ -129,7 +115,7 @@ export const auditQuery = z.object({
|
|
|
129
115
|
/** Only events with a smaller `seq` — the next, older page. */
|
|
130
116
|
before_seq: wireSeqCursor.optional(),
|
|
131
117
|
/**
|
|
132
|
-
*
|
|
118
|
+
* The same shape as `historyQuery.limit`: a union whose input branch
|
|
133
119
|
* **is the wire**. A `z.coerce` cannot be published — zod renders the
|
|
134
120
|
* coercion's result in either `io` direction, so the artifact would describe
|
|
135
121
|
* a shape a query string can never carry.
|
|
@@ -162,36 +148,25 @@ export const auditQuery = z.object({
|
|
|
162
148
|
/**
|
|
163
149
|
* Only events by this actor.
|
|
164
150
|
*
|
|
165
|
-
* **`z.uuid()`, because the column is one
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* Not a SQL-injection finding — Drizzle parameterises, and `' or 1=1--`
|
|
171
|
-
* failed at the same cast. It is a **500 where a 400 belongs**, and a 500 is
|
|
172
|
-
* the answer that explains nothing.
|
|
151
|
+
* **`z.uuid()`, because the column is one.** A looser string type lets any
|
|
152
|
+
* non-uuid value reach the database as a uuid parameter, where the cast
|
|
153
|
+
* throws: `?actor_id=not-a-uuid` then answers **500 `internal_error`** rather
|
|
154
|
+
* than refusing the value.
|
|
173
155
|
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* deletion — and the brand-new filter, whose field has exactly that shape,
|
|
177
|
-
* is the one that did not get it. A rule applied to the sites in front of
|
|
178
|
-
* you is not a rule applied to the class.
|
|
156
|
+
* Not an injection question — the query is parameterised either way. It is a
|
|
157
|
+
* **500 where a 400 belongs**, and a 500 is the answer that explains nothing.
|
|
179
158
|
*/
|
|
180
159
|
actor_id: z.uuid().optional(),
|
|
181
160
|
/** Only events about this kind of target, e.g. `robot`. */
|
|
182
161
|
target_kind: z.string().min(1).max(40).optional(),
|
|
183
162
|
/**
|
|
184
163
|
* Absolute bounds in unix milliseconds, **half-open `[from, to)`** — the
|
|
185
|
-
* same rule the history shapes follow
|
|
164
|
+
* same rule the history shapes follow.
|
|
186
165
|
*
|
|
187
|
-
* **Bounded to years 1..9999
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* `253402300800000` → 500 (Argus-W9). `history-query.ts`'s `parseTimeExprMs`
|
|
192
|
-
* already carries exactly this range, with M3's reasoning for why
|
|
193
|
-
* `Number.isSafeInteger` is wider than what a timestamp can be; this is that
|
|
194
|
-
* same number, not a second one that happens to agree.
|
|
166
|
+
* **Bounded to years 1..9999.** `nonnegative()` alone admits instants a
|
|
167
|
+
* timestamp column has no representation for, and the route answers 500
|
|
168
|
+
* rather than refusing the value. `Number.isSafeInteger` is wider than what a
|
|
169
|
+
* timestamp can be, so the bound is stated rather than inherited.
|
|
195
170
|
*/
|
|
196
171
|
from_ms: auditTimestampMs.optional(),
|
|
197
172
|
to_ms: auditTimestampMs.optional(),
|
|
@@ -216,7 +191,7 @@ export const auditListResponse = z.object({
|
|
|
216
191
|
next_cursor: z.number().int().positive().nullable(),
|
|
217
192
|
});
|
|
218
193
|
/**
|
|
219
|
-
* **What a CSV export of this log looks like
|
|
194
|
+
* **What a CSV export of this log looks like.**
|
|
220
195
|
*
|
|
221
196
|
* The column order lives here because otherwise the cloud and the console
|
|
222
197
|
* would each carry their own, and nobody would notice them drifting apart
|
|
@@ -229,10 +204,9 @@ export const auditListResponse = z.object({
|
|
|
229
204
|
*/
|
|
230
205
|
export const AUDIT_CSV_COLUMNS = ['seq', 'at', 'actor_kind', 'actor_id', 'action', 'target_kind', 'target_id', 'target_label', 'details'];
|
|
231
206
|
/**
|
|
232
|
-
*
|
|
207
|
+
* The audit log is kept for **90 days**.
|
|
233
208
|
*
|
|
234
|
-
* A constant here so
|
|
235
|
-
*
|
|
236
|
-
* there is no purge touching audit rows at all.
|
|
209
|
+
* A constant here so no consumer derives it a second time — the same reasoning
|
|
210
|
+
* as `ASSET_UPLOAD_MAX_BYTES`.
|
|
237
211
|
*/
|
|
238
212
|
export const AUDIT_RETENTION_DAYS = 90;
|
package/dist/client-auth.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
/**
|
|
3
|
-
* **The client auth API: the whole of what an app user's browser talks to
|
|
4
|
-
* (spec `2026-09-05-app-user-auth`, §4).
|
|
4
|
+
* **The client auth API: the whole of what an app user's browser talks to.**
|
|
5
5
|
*
|
|
6
|
-
* Fleetless shows an app user **no page
|
|
6
|
+
* Fleetless shows an app user **no page**. The developer's own UI owns
|
|
7
7
|
* every screen — login, registration, verification, invitation acceptance,
|
|
8
8
|
* password reset, the provider buttons, the MCP consent — and calls these
|
|
9
9
|
* routes as JSON. The hosted, app-branded login and consent pages this file
|
|
@@ -23,7 +23,7 @@ import { z } from 'zod';
|
|
|
23
23
|
* anything else is parsed as a JWT. That rule is written down once, here, so
|
|
24
24
|
* the SDK and the cloud cannot drift into disagreeing about it.
|
|
25
25
|
*
|
|
26
|
-
* **The enumeration discipline is
|
|
26
|
+
* **The enumeration discipline is deliberate, not a preference:**
|
|
27
27
|
* `register`, `resend-verification` and `password/reset` answer `202` for every
|
|
28
28
|
* policy-allowed request whether or not the address exists, and `login` answers
|
|
29
29
|
* the identical `invalid_credentials` for a wrong password, a `blocked` account
|
package/dist/client-auth.js
CHANGED
|
@@ -4,10 +4,9 @@ import { appIdentifier } from './apps.js';
|
|
|
4
4
|
import { APP_USER_DISPLAY_NAME_MAX, providerSlug } from './app-users.js';
|
|
5
5
|
import { password } from './identity.js';
|
|
6
6
|
/**
|
|
7
|
-
* **The client auth API: the whole of what an app user's browser talks to
|
|
8
|
-
* (spec `2026-09-05-app-user-auth`, §4).
|
|
7
|
+
* **The client auth API: the whole of what an app user's browser talks to.**
|
|
9
8
|
*
|
|
10
|
-
* Fleetless shows an app user **no page
|
|
9
|
+
* Fleetless shows an app user **no page**. The developer's own UI owns
|
|
11
10
|
* every screen — login, registration, verification, invitation acceptance,
|
|
12
11
|
* password reset, the provider buttons, the MCP consent — and calls these
|
|
13
12
|
* routes as JSON. The hosted, app-branded login and consent pages this file
|
|
@@ -27,7 +26,7 @@ import { password } from './identity.js';
|
|
|
27
26
|
* anything else is parsed as a JWT. That rule is written down once, here, so
|
|
28
27
|
* the SDK and the cloud cannot drift into disagreeing about it.
|
|
29
28
|
*
|
|
30
|
-
* **The enumeration discipline is
|
|
29
|
+
* **The enumeration discipline is deliberate, not a preference:**
|
|
31
30
|
* `register`, `resend-verification` and `password/reset` answer `202` for every
|
|
32
31
|
* policy-allowed request whether or not the address exists, and `login` answers
|
|
33
32
|
* the identical `invalid_credentials` for a wrong password, a `blocked` account
|
package/dist/common.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
/**
|
|
3
4
|
* Names shared by every layer: the Fleetless slug and the ROS names it is
|
|
4
|
-
* deliberately decoupled from
|
|
5
|
+
* deliberately decoupled from.
|
|
5
6
|
*
|
|
6
7
|
* They live here rather than in `protocol.ts` so the exposure model
|
|
7
8
|
* (`config.ts`) and the bridge protocol can both use them without importing
|
|
@@ -18,16 +19,11 @@ import { z } from 'zod';
|
|
|
18
19
|
* Each is used **twice**: as the message zod itself produces, here, and as
|
|
19
20
|
* `patternErrorMessage` in `config.ts`'s exported JSON Schema, which is a
|
|
20
21
|
* published artifact that other tools validate against and that a person
|
|
21
|
-
* reads.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* forever and invisibly — the shape that had `buildAcceptUrl` mailing one URL
|
|
27
|
-
* three ways. They are therefore one constant with two readers rather than two
|
|
28
|
-
* strings that happen to agree, and `config-zod-messages.test.ts` asserts the
|
|
29
|
-
* two readings are the same string at all 24 pattern positions the document
|
|
30
|
-
* has.
|
|
22
|
+
* reads. Either way it is a second spelling of a live rule, and an unwatched
|
|
23
|
+
* second spelling drifts word for word, forever and invisibly. They are
|
|
24
|
+
* therefore one constant with two readers rather than two strings that happen
|
|
25
|
+
* to agree, and `config-zod-messages.test.ts` asserts the two readings are the
|
|
26
|
+
* same string at every pattern position the document has.
|
|
31
27
|
*
|
|
32
28
|
* **They are exported because the pattern and its sentence must not be able to
|
|
33
29
|
* move apart**, and the pattern is here while the schema annotation is in
|
|
@@ -35,15 +31,13 @@ import { z } from 'zod';
|
|
|
35
31
|
* document — the two URL schemes and the capture-device path — are constants in
|
|
36
32
|
* `config.ts` beside their own patterns, on the same rule.
|
|
37
33
|
*
|
|
38
|
-
* **
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* 2026-09-03). What it does reach is the sentence a *parser* produces, in every
|
|
46
|
-
* layer that parses one of these names — which is the improvement, not a cost.
|
|
34
|
+
* **Putting the sentence here changes no published artifact.** A `.meta()` on
|
|
35
|
+
* `slug` would reach dozens of the published schemas — which is why `mapKey` in
|
|
36
|
+
* `config.ts` carries the annotation and `slug` does not. A message on a
|
|
37
|
+
* `.regex()` check is a different thing: zod renders no error message into JSON
|
|
38
|
+
* Schema at all, so every artifact is byte-identical either way. What it does
|
|
39
|
+
* reach is the sentence a *parser* produces, in every layer that parses one of
|
|
40
|
+
* these names — which is the improvement, not a cost.
|
|
47
41
|
*/
|
|
48
42
|
export declare const SLUG_RULE = "A name is lower-case: it starts with a letter, continues with letters and digits, and joins further words with a single underscore \u2014 `battery_voltage`. Capitals, dashes, dots, spaces, a leading digit and a doubled or trailing underscore are all refused.";
|
|
49
43
|
/**
|
|
@@ -51,7 +45,7 @@ export declare const SLUG_RULE = "A name is lower-case: it starts with a letter,
|
|
|
51
45
|
* message name. Lowercase, underscore-separated, letter-initial, 2..63
|
|
52
46
|
* characters, no leading/trailing/doubled underscores.
|
|
53
47
|
*
|
|
54
|
-
* Names are stable and decoupled from ROS names
|
|
48
|
+
* Names are stable and decoupled from ROS names — renaming a
|
|
55
49
|
* topic on the robot must never break a client app. The reverse also holds
|
|
56
50
|
* and costs more: changing a name breaks every client, role grant and MCP
|
|
57
51
|
* tool name that uses it.
|
|
@@ -67,9 +61,9 @@ export declare const rosName: z.ZodString;
|
|
|
67
61
|
export declare const ROS_TYPE_NAME_RULE = "A ROS 2 type name has three segments: the package, then `msg`, `srv` or `action`, then the type \u2014 `sensor_msgs/msg/BatteryState`, `std_srvs/srv/Trigger`, `nav2_msgs/action/NavigateToPose`. The middle segment is the one usually left out. The package is lower-case with underscores; the type itself is letters and digits, conventionally CamelCase.";
|
|
68
62
|
/**
|
|
69
63
|
* A ROS interface type as ROS 2 spells it: `pkg/msg/Type`, `pkg/srv/Type`,
|
|
70
|
-
* `pkg/action/Type`.
|
|
71
|
-
*
|
|
72
|
-
*
|
|
64
|
+
* `pkg/action/Type`. Introspection resolves a field tree for each of the
|
|
65
|
+
* three; a message has one flat list, a service and an action have one tree
|
|
66
|
+
* per part.
|
|
73
67
|
*/
|
|
74
68
|
export declare const rosTypeName: z.ZodString;
|
|
75
69
|
export declare const FIELD_PATH_RULE = "A field path is dotted and lower-case, and each segment may index at most one array level \u2014 `voltage`, `pose.position.x`, `ranges[0]`. ROS 2 has no nested arrays, so a second index on one segment could name nothing that exists.";
|
|
@@ -77,15 +71,15 @@ export declare const FIELD_PATH_RULE = "A field path is dotted and lower-case, a
|
|
|
77
71
|
* A path into a message: dot-separated field names, each carrying **at most
|
|
78
72
|
* one** array index, e.g. `percentage`, `pose.position.x`, `ranges[0]`,
|
|
79
73
|
* `poses[0].pose.position.x`. `null` in a datapoint config means *the whole
|
|
80
|
-
* message
|
|
74
|
+
* message*: one field or one whole topic, never several topics.
|
|
81
75
|
*
|
|
82
76
|
* One index per segment is not a preference but the shape of the target: ROS 2
|
|
83
77
|
* IDL has `float64[]`, `float64[3]` and `float64[<=10]`, and no nested or
|
|
84
78
|
* multi-dimensional arrays at all. A second index on one segment — `a[0][1]` —
|
|
85
79
|
* could therefore denote nothing on any message that exists. The bridge has
|
|
86
80
|
* always refused it (`sampling.py`'s `FieldPathError`, *"ROS has no nested
|
|
87
|
-
* arrays"*)
|
|
88
|
-
* AI-generated document
|
|
81
|
+
* arrays"*). A grammar that said otherwise would let a hand-written or
|
|
82
|
+
* AI-generated document pass the cloud and then fail at the robot as a
|
|
89
83
|
* `config_applied` error — the latest and worst place to learn it.
|
|
90
84
|
*/
|
|
91
85
|
export declare const fieldPath: z.ZodString;
|
|
@@ -95,14 +89,12 @@ export declare const fieldPath: z.ZodString;
|
|
|
95
89
|
*
|
|
96
90
|
* The union's input branch **is the wire** — a `z.coerce` cannot be published,
|
|
97
91
|
* because zod renders the coercion's result in either `io` direction, so the
|
|
98
|
-
* artifact would describe a shape a query string can never carry
|
|
99
|
-
*
|
|
100
|
-
* The year bound is
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* here from `audit.ts` when `jobRunQuery` needed the same guard; a second copy
|
|
105
|
-
* would have been a second policy for one decision.
|
|
92
|
+
* artifact would describe a shape a query string can never carry.
|
|
93
|
+
*
|
|
94
|
+
* The year bound is not decorative. `nonnegative()` alone admits instants a
|
|
95
|
+
* timestamp column has no representation for, and the route answers 500 rather
|
|
96
|
+
* than refusing the value. It lives here, once, because more than one query
|
|
97
|
+
* needs it and a second copy would be a second policy for one decision.
|
|
106
98
|
*/
|
|
107
99
|
/**
|
|
108
100
|
* A `seq` cursor as a **query string** actually carries it.
|
package/dist/common.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
/**
|
|
4
4
|
* Names shared by every layer: the Fleetless slug and the ROS names it is
|
|
5
|
-
* deliberately decoupled from
|
|
5
|
+
* deliberately decoupled from.
|
|
6
6
|
*
|
|
7
7
|
* They live here rather than in `protocol.ts` so the exposure model
|
|
8
8
|
* (`config.ts`) and the bridge protocol can both use them without importing
|
|
@@ -19,16 +19,11 @@ import { z } from 'zod';
|
|
|
19
19
|
* Each is used **twice**: as the message zod itself produces, here, and as
|
|
20
20
|
* `patternErrorMessage` in `config.ts`'s exported JSON Schema, which is a
|
|
21
21
|
* published artifact that other tools validate against and that a person
|
|
22
|
-
* reads.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* forever and invisibly — the shape that had `buildAcceptUrl` mailing one URL
|
|
28
|
-
* three ways. They are therefore one constant with two readers rather than two
|
|
29
|
-
* strings that happen to agree, and `config-zod-messages.test.ts` asserts the
|
|
30
|
-
* two readings are the same string at all 24 pattern positions the document
|
|
31
|
-
* has.
|
|
22
|
+
* reads. Either way it is a second spelling of a live rule, and an unwatched
|
|
23
|
+
* second spelling drifts word for word, forever and invisibly. They are
|
|
24
|
+
* therefore one constant with two readers rather than two strings that happen
|
|
25
|
+
* to agree, and `config-zod-messages.test.ts` asserts the two readings are the
|
|
26
|
+
* same string at every pattern position the document has.
|
|
32
27
|
*
|
|
33
28
|
* **They are exported because the pattern and its sentence must not be able to
|
|
34
29
|
* move apart**, and the pattern is here while the schema annotation is in
|
|
@@ -36,15 +31,13 @@ import { z } from 'zod';
|
|
|
36
31
|
* document — the two URL schemes and the capture-device path — are constants in
|
|
37
32
|
* `config.ts` beside their own patterns, on the same rule.
|
|
38
33
|
*
|
|
39
|
-
* **
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* 2026-09-03). What it does reach is the sentence a *parser* produces, in every
|
|
47
|
-
* layer that parses one of these names — which is the improvement, not a cost.
|
|
34
|
+
* **Putting the sentence here changes no published artifact.** A `.meta()` on
|
|
35
|
+
* `slug` would reach dozens of the published schemas — which is why `mapKey` in
|
|
36
|
+
* `config.ts` carries the annotation and `slug` does not. A message on a
|
|
37
|
+
* `.regex()` check is a different thing: zod renders no error message into JSON
|
|
38
|
+
* Schema at all, so every artifact is byte-identical either way. What it does
|
|
39
|
+
* reach is the sentence a *parser* produces, in every layer that parses one of
|
|
40
|
+
* these names — which is the improvement, not a cost.
|
|
48
41
|
*/
|
|
49
42
|
export const SLUG_RULE = 'A name is lower-case: it starts with a letter, continues with letters and digits, and joins further words with a single underscore — `battery_voltage`. Capitals, dashes, dots, spaces, a leading digit and a doubled or trailing underscore are all refused.';
|
|
50
43
|
/**
|
|
@@ -52,7 +45,7 @@ export const SLUG_RULE = 'A name is lower-case: it starts with a letter, continu
|
|
|
52
45
|
* message name. Lowercase, underscore-separated, letter-initial, 2..63
|
|
53
46
|
* characters, no leading/trailing/doubled underscores.
|
|
54
47
|
*
|
|
55
|
-
* Names are stable and decoupled from ROS names
|
|
48
|
+
* Names are stable and decoupled from ROS names — renaming a
|
|
56
49
|
* topic on the robot must never break a client app. The reverse also holds
|
|
57
50
|
* and costs more: changing a name breaks every client, role grant and MCP
|
|
58
51
|
* tool name that uses it.
|
|
@@ -75,9 +68,9 @@ export const rosName = z
|
|
|
75
68
|
export const ROS_TYPE_NAME_RULE = 'A ROS 2 type name has three segments: the package, then `msg`, `srv` or `action`, then the type — `sensor_msgs/msg/BatteryState`, `std_srvs/srv/Trigger`, `nav2_msgs/action/NavigateToPose`. The middle segment is the one usually left out. The package is lower-case with underscores; the type itself is letters and digits, conventionally CamelCase.';
|
|
76
69
|
/**
|
|
77
70
|
* A ROS interface type as ROS 2 spells it: `pkg/msg/Type`, `pkg/srv/Type`,
|
|
78
|
-
* `pkg/action/Type`.
|
|
79
|
-
*
|
|
80
|
-
*
|
|
71
|
+
* `pkg/action/Type`. Introspection resolves a field tree for each of the
|
|
72
|
+
* three; a message has one flat list, a service and an action have one tree
|
|
73
|
+
* per part.
|
|
81
74
|
*/
|
|
82
75
|
export const rosTypeName = z
|
|
83
76
|
.string()
|
|
@@ -88,15 +81,15 @@ export const FIELD_PATH_RULE = 'A field path is dotted and lower-case, and each
|
|
|
88
81
|
* A path into a message: dot-separated field names, each carrying **at most
|
|
89
82
|
* one** array index, e.g. `percentage`, `pose.position.x`, `ranges[0]`,
|
|
90
83
|
* `poses[0].pose.position.x`. `null` in a datapoint config means *the whole
|
|
91
|
-
* message
|
|
84
|
+
* message*: one field or one whole topic, never several topics.
|
|
92
85
|
*
|
|
93
86
|
* One index per segment is not a preference but the shape of the target: ROS 2
|
|
94
87
|
* IDL has `float64[]`, `float64[3]` and `float64[<=10]`, and no nested or
|
|
95
88
|
* multi-dimensional arrays at all. A second index on one segment — `a[0][1]` —
|
|
96
89
|
* could therefore denote nothing on any message that exists. The bridge has
|
|
97
90
|
* always refused it (`sampling.py`'s `FieldPathError`, *"ROS has no nested
|
|
98
|
-
* arrays"*)
|
|
99
|
-
* AI-generated document
|
|
91
|
+
* arrays"*). A grammar that said otherwise would let a hand-written or
|
|
92
|
+
* AI-generated document pass the cloud and then fail at the robot as a
|
|
100
93
|
* `config_applied` error — the latest and worst place to learn it.
|
|
101
94
|
*/
|
|
102
95
|
export const fieldPath = z
|
|
@@ -109,14 +102,12 @@ export const fieldPath = z
|
|
|
109
102
|
*
|
|
110
103
|
* The union's input branch **is the wire** — a `z.coerce` cannot be published,
|
|
111
104
|
* because zod renders the coercion's result in either `io` direction, so the
|
|
112
|
-
* artifact would describe a shape a query string can never carry
|
|
113
|
-
*
|
|
114
|
-
* The year bound is
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* here from `audit.ts` when `jobRunQuery` needed the same guard; a second copy
|
|
119
|
-
* would have been a second policy for one decision.
|
|
105
|
+
* artifact would describe a shape a query string can never carry.
|
|
106
|
+
*
|
|
107
|
+
* The year bound is not decorative. `nonnegative()` alone admits instants a
|
|
108
|
+
* timestamp column has no representation for, and the route answers 500 rather
|
|
109
|
+
* than refusing the value. It lives here, once, because more than one query
|
|
110
|
+
* needs it and a second copy would be a second policy for one decision.
|
|
120
111
|
*/
|
|
121
112
|
/**
|
|
122
113
|
* A `seq` cursor as a **query string** actually carries it.
|
package/dist/config-issues.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
import type { ValidationIssue } from './config.js';
|
|
3
4
|
/**
|
|
@@ -51,7 +52,7 @@ export declare const EXPOSURE_SECTIONS: readonly ["datapoints", "actions", "serv
|
|
|
51
52
|
export type ExposureSection = (typeof EXPOSURE_SECTIONS)[number];
|
|
52
53
|
/**
|
|
53
54
|
* The refusals `robotConfigDoc` already made, reported as validation issues
|
|
54
|
-
* with
|
|
55
|
+
* with the format's own codes.
|
|
55
56
|
*
|
|
56
57
|
* **This maps; it does not re-decide.** Seven of the thirteen codes are
|
|
57
58
|
* answered by the schema before a document ever becomes a `RobotConfigDoc`,
|
|
@@ -72,8 +73,8 @@ export type ExposureSection = (typeof EXPOSURE_SECTIONS)[number];
|
|
|
72
73
|
* sentence differs at the document root, where there is no key to remove:
|
|
73
74
|
* see `EMPTY_DOCUMENT_MESSAGE`.
|
|
74
75
|
*
|
|
75
|
-
* Everything else keeps zod's own code. Those are refusals
|
|
76
|
-
* code — a reversed `min_value`/`max_value` pair, a section over its cap, a
|
|
76
|
+
* Everything else keeps zod's own code. Those are refusals the format names no
|
|
77
|
+
* code for — a reversed `min_value`/`max_value` pair, a section over its cap, a
|
|
77
78
|
* key that is not a slug — and inventing a fourteenth code for them would put
|
|
78
79
|
* a code on the wire that no table documents.
|
|
79
80
|
*/
|
package/dist/config-issues.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
2
|
/** What a path with no segments at all is called, since `path` may not be empty. */
|
|
2
3
|
export const DOCUMENT_ROOT_PATH = '(document)';
|
|
3
4
|
/**
|
|
@@ -15,7 +16,7 @@ export const DOCUMENT_ROOT_PATH = '(document)';
|
|
|
15
16
|
export const EXPOSURE_SECTIONS = ['datapoints', 'actions', 'services', 'publishers', 'cameras'];
|
|
16
17
|
/**
|
|
17
18
|
* The refusals `robotConfigDoc` already made, reported as validation issues
|
|
18
|
-
* with
|
|
19
|
+
* with the format's own codes.
|
|
19
20
|
*
|
|
20
21
|
* **This maps; it does not re-decide.** Seven of the thirteen codes are
|
|
21
22
|
* answered by the schema before a document ever becomes a `RobotConfigDoc`,
|
|
@@ -36,8 +37,8 @@ export const EXPOSURE_SECTIONS = ['datapoints', 'actions', 'services', 'publishe
|
|
|
36
37
|
* sentence differs at the document root, where there is no key to remove:
|
|
37
38
|
* see `EMPTY_DOCUMENT_MESSAGE`.
|
|
38
39
|
*
|
|
39
|
-
* Everything else keeps zod's own code. Those are refusals
|
|
40
|
-
* code — a reversed `min_value`/`max_value` pair, a section over its cap, a
|
|
40
|
+
* Everything else keeps zod's own code. Those are refusals the format names no
|
|
41
|
+
* code for — a reversed `min_value`/`max_value` pair, a section over its cap, a
|
|
41
42
|
* key that is not a slug — and inventing a fourteenth code for them would put
|
|
42
43
|
* a code on the wire that no table documents.
|
|
43
44
|
*/
|
|
@@ -69,9 +70,9 @@ const NULL_KEY_MESSAGE = 'This key is null. Omission is the only spelling of "no
|
|
|
69
70
|
* a genuine `invalid_type` on `null` at the empty path, so the code is right —
|
|
70
71
|
* but the sentence for a null *key* told the developer to remove a key that
|
|
71
72
|
* does not exist, and "select all, delete" is the commonest way anybody gets
|
|
72
|
-
* here.
|
|
73
|
-
* sentence is what
|
|
74
|
-
*
|
|
73
|
+
* here. A draft that does not parse is stored rather than refused, so this
|
|
74
|
+
* sentence is what an editor's findings panel shows persistently for an emptied
|
|
75
|
+
* document rather than a one-shot refusal nobody reads.
|
|
75
76
|
*
|
|
76
77
|
* It names the smallest legal document rather than only saying what is wrong,
|
|
77
78
|
* because at this path there is no line to jump to and no repair to offer —
|