@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +68 -2
  2. package/CODE_OF_CONDUCT.md +83 -0
  3. package/CONTRIBUTING.md +136 -0
  4. package/README.md +49 -13
  5. package/SECURITY.md +55 -0
  6. package/artifacts/openapi.json +1 -1
  7. package/artifacts/routes.json +1 -1
  8. package/dist/alerts.d.ts +19 -24
  9. package/dist/alerts.js +18 -24
  10. package/dist/app-users.d.ts +7 -6
  11. package/dist/app-users.js +6 -6
  12. package/dist/apps.d.ts +21 -25
  13. package/dist/apps.js +40 -51
  14. package/dist/assets.d.ts +70 -132
  15. package/dist/assets.js +130 -223
  16. package/dist/audit.d.ts +11 -11
  17. package/dist/audit.js +25 -51
  18. package/dist/client-auth.d.ts +4 -4
  19. package/dist/client-auth.js +3 -4
  20. package/dist/common.d.ts +27 -35
  21. package/dist/common.js +26 -35
  22. package/dist/config-issues.d.ts +4 -3
  23. package/dist/config-issues.js +7 -6
  24. package/dist/config.d.ts +31 -37
  25. package/dist/config.js +81 -110
  26. package/dist/errors.d.ts +4 -3
  27. package/dist/errors.js +61 -87
  28. package/dist/identity.d.ts +18 -21
  29. package/dist/identity.js +17 -21
  30. package/dist/index.d.ts +4 -4
  31. package/dist/index.js +12 -13
  32. package/dist/introspection.d.ts +7 -6
  33. package/dist/introspection.js +6 -6
  34. package/dist/jobs.d.ts +12 -12
  35. package/dist/jobs.js +20 -25
  36. package/dist/mcp.d.ts +11 -12
  37. package/dist/mcp.js +10 -12
  38. package/dist/oauth.d.ts +13 -18
  39. package/dist/oauth.js +13 -19
  40. package/dist/protocol.d.ts +51 -62
  41. package/dist/protocol.js +107 -139
  42. package/dist/realtime.d.ts +53 -68
  43. package/dist/realtime.js +77 -103
  44. package/dist/rest.d.ts +182 -243
  45. package/dist/rest.js +301 -395
  46. package/dist/routes.d.ts +4 -3
  47. package/dist/routes.js +3 -2
  48. 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
- * Audit (spec §16). Every state-changing interaction is recorded and **every
6
- * entry carries its actor — never anonymous** (§16.2). Reads are not audited.
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
- * W3 writes the events that exist once identities do: logins, failed logins,
9
- * end-user management, config publishes, bridge connect/disconnect. The view
10
- * with filters, CSV export and the 90-day retention window is W6 (André,
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. No cursor parameter exists on `GET /api/audit` yet —
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 (W9d, DEF-078 and DEF-123).**
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
- * Same shape as DEF-059's `historyQuery.limit`: a union whose input branch
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 (Argus-W9, W9 review).** This was
166
- * `z.string().min(1).max(200)`, so any non-uuid value reached Postgres as a
167
- * uuid parameter and threw: `?actor_id=not-a-uuid` answered **500
168
- * `internal_error`**, on the list route and the export alike.
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
- * The place is the part worth keeping: **this same wave pulled
175
- * `refuseIfNotUuid` through ~15 call sites** so a typo could be told from a
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 (DEF-062).
164
+ * same rule the history shapes follow.
186
165
  *
187
- * **Bounded to years 1..9999, and the bound is borrowed rather than
188
- * invented.** `nonnegative()` alone let `253402300800000` (year 10000)
189
- * through, where the Postgres bind path has no representation and the route
190
- * answered 500 — measured either side of the edge: `253402300799000` → 200,
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 (DEF-123, spec §16.3).**
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
- * Spec §16.3: the audit log is kept for **90 days**.
207
+ * The audit log is kept for **90 days**.
233
208
  *
234
- * A constant here so the cloud does not derive it a second time — the same
235
- * reasoning as `ASSET_UPLOAD_MAX_BYTES`, and the same register row that found
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;
@@ -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** (D2). The developer's own UI owns
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 the design's, not a preference** (§4):
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
@@ -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** (D2). The developer's own UI owns
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 the design's, not a preference** (§4):
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 (spec §4.1).
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. Under FL-005 D3 nothing will consume `patternErrorMessage` at runtime
22
- * **once wave 3 lands** — `useMonacoYaml.ts` still passes `validate: true`
23
- * today, so until then this sentence IS the live diagnostic in the editor and
24
- * zod's is the live one on the server. Either way it is a second spelling of a
25
- * live rule, and an unwatched one would drift word for word,
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
- * **The blast radius of putting the sentence here was measured, and it is
39
- * zero artifacts.** A `.meta()` on `slug` would reach 42 of the 159 published
40
- * schema artifacts, the bridge's vendored protocol frames among them — which is
41
- * why `mapKey` in `config.ts` carries the annotation and `slug` does not. A
42
- * message on a `.regex()` check is a different thing: zod renders no error
43
- * message into JSON Schema at all, so every artifact is byte-identical either
44
- * way (measured across all 278 barrel schemas under both `io` modes,
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 (spec §4.1) — renaming a
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`. W2 resolves field trees for `msg` only (§4.5); the
71
- * other two are listed by the introspection browser and get their trees in
72
- * W4, where action and service parameters exist.
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* (spec §4.2: one field or one whole topic — never several topics).
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"*); this grammar said otherwise until FL-004, so a hand-written or
88
- * AI-generated document could pass the cloud and then fail at the robot as a
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 (DEF-059).
99
- *
100
- * The year bound is borrowed rather than invented: `nonnegative()` alone let
101
- * `253402300800000` through, where the Postgres bind path has no representation
102
- * and the route answered 500 — measured either side of the edge,
103
- * `253402300799000` -> 200 and `253402300800000` -> 500 (Argus-W9). This moved
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 (spec §4.1).
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. Under FL-005 D3 nothing will consume `patternErrorMessage` at runtime
23
- * **once wave 3 lands** — `useMonacoYaml.ts` still passes `validate: true`
24
- * today, so until then this sentence IS the live diagnostic in the editor and
25
- * zod's is the live one on the server. Either way it is a second spelling of a
26
- * live rule, and an unwatched one would drift word for word,
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
- * **The blast radius of putting the sentence here was measured, and it is
40
- * zero artifacts.** A `.meta()` on `slug` would reach 42 of the 159 published
41
- * schema artifacts, the bridge's vendored protocol frames among them — which is
42
- * why `mapKey` in `config.ts` carries the annotation and `slug` does not. A
43
- * message on a `.regex()` check is a different thing: zod renders no error
44
- * message into JSON Schema at all, so every artifact is byte-identical either
45
- * way (measured across all 278 barrel schemas under both `io` modes,
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 (spec §4.1) — renaming a
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`. W2 resolves field trees for `msg` only (§4.5); the
79
- * other two are listed by the introspection browser and get their trees in
80
- * W4, where action and service parameters exist.
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* (spec §4.2: one field or one whole topic — never several topics).
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"*); this grammar said otherwise until FL-004, so a hand-written or
99
- * AI-generated document could pass the cloud and then fail at the robot as a
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 (DEF-059).
113
- *
114
- * The year bound is borrowed rather than invented: `nonnegative()` alone let
115
- * `253402300800000` through, where the Postgres bind path has no representation
116
- * and the route answered 500 — measured either side of the edge,
117
- * `253402300799000` -> 200 and `253402300800000` -> 500 (Argus-W9). This moved
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.
@@ -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 their FL-002 codes.
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 with no FL-002
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
  */
@@ -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 their FL-002 codes.
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 with no FL-002
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. Since FL-005 D2 stores the draft rather than refusing it, that
73
- * sentence is what the FINDINGS panel shows persistently for an emptied
74
- * editor, where it used to ride a one-shot 422 nobody read.
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 —