@fleetless/contracts 1.0.0 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +97 -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 +11 -11
  7. package/artifacts/routes.json +12 -12
  8. package/artifacts/schema/create-app-oidc-provider-request.schema.json +1 -1
  9. package/dist/alerts.d.ts +23 -28
  10. package/dist/alerts.js +23 -29
  11. package/dist/app-users.d.ts +18 -19
  12. package/dist/app-users.js +18 -20
  13. package/dist/apps.d.ts +21 -25
  14. package/dist/apps.js +42 -52
  15. package/dist/assets.d.ts +70 -132
  16. package/dist/assets.js +130 -223
  17. package/dist/audit.d.ts +14 -15
  18. package/dist/audit.js +28 -55
  19. package/dist/client-auth.d.ts +9 -9
  20. package/dist/client-auth.js +8 -9
  21. package/dist/common.d.ts +29 -37
  22. package/dist/common.js +28 -37
  23. package/dist/config-issues.d.ts +23 -25
  24. package/dist/config-issues.js +17 -17
  25. package/dist/config.d.ts +37 -44
  26. package/dist/config.js +145 -187
  27. package/dist/errors.d.ts +4 -3
  28. package/dist/errors.js +83 -116
  29. package/dist/identity.d.ts +24 -27
  30. package/dist/identity.js +23 -27
  31. package/dist/index.d.ts +4 -4
  32. package/dist/index.js +14 -15
  33. package/dist/introspection.d.ts +7 -6
  34. package/dist/introspection.js +6 -6
  35. package/dist/jobs.d.ts +16 -16
  36. package/dist/jobs.js +24 -29
  37. package/dist/mcp.d.ts +14 -15
  38. package/dist/mcp.js +12 -14
  39. package/dist/oauth.d.ts +21 -27
  40. package/dist/oauth.js +33 -43
  41. package/dist/protocol.d.ts +51 -62
  42. package/dist/protocol.js +107 -139
  43. package/dist/realtime.d.ts +53 -68
  44. package/dist/realtime.js +78 -104
  45. package/dist/rest.d.ts +183 -244
  46. package/dist/rest.js +305 -399
  47. package/dist/routes.d.ts +4 -3
  48. package/dist/routes.js +33 -32
  49. 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
@@ -16,7 +16,7 @@ import { wireSeqCursor, wireTimestampMs } from './common.js';
16
16
  * resolve four different id kinds to render a row.
17
17
  *
18
18
  * **`end_user` stays, and it stays for the rows already written.** The
19
- * two-space cut (2026-09-05, D1) replaced the org's one user pool with
19
+ * split into two identity spaces replaced the org's one user pool with
20
20
  * Fleetless users and per-app app users; every new row an app user writes
21
21
  * carries `app_user`. But an audit log is the one thing this platform must
22
22
  * never rewrite, and there are stored rows whose `kind` is `end_user`. Dropping
@@ -24,9 +24,8 @@ import { wireSeqCursor, wireTimestampMs } from './common.js';
24
24
  * cannot be read back is worse than one carrying a retired word.
25
25
  *
26
26
  * So this enum is deliberately **wider than what any producer emits**: nothing
27
- * writes `end_user` any more, and nothing may start again. That is the kind of
28
- * claim this repository has been wrong about before by leaving it unsaid, so it
29
- * is said here rather than inferred from a `grep` somebody runs in a year.
27
+ * writes `end_user` any more, and nothing may start again. That is said here
28
+ * rather than left to be inferred from a search somebody runs in a year.
30
29
  *
31
30
  * `developer` is a Fleetless user. It kept its name through both redesigns
32
31
  * because it was always right about what it named: the person who configures
@@ -42,8 +41,7 @@ export const auditEvent = z.object({
42
41
  org_id: z.uuid(),
43
42
  at: z.iso.datetime(),
44
43
  /**
45
- * A monotonic counter, ascending in write order, unique across the log
46
- * (W6b).
44
+ * A monotonic counter, ascending in write order, unique across the log.
47
45
  *
48
46
  * `at` is not a total order. Two events written in the same millisecond —
49
47
  * a login and the config publish it enables, a cascade writing several
@@ -55,11 +53,7 @@ export const auditEvent = z.object({
55
53
  *
56
54
  * It is also the only correct **cursor** for paging this log, for the same
57
55
  * 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.
56
+ * every page boundary. Nothing else in this shape can carry one.
63
57
  *
64
58
  * Required, not optional: an event without a sequence cannot be ordered
65
59
  * against one that has it, and a log with two orderings has none.
@@ -94,12 +88,7 @@ export const auditEvent = z.object({
94
88
  details: z.record(z.string(), z.unknown()).nullable(),
95
89
  });
96
90
  /**
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.
91
+ * **How this log is read.**
103
92
  *
104
93
  * **The cursor is `seq`, and no other field can be.** `at` is not a total
105
94
  * order: two events written in the same millisecond sort differently on every
@@ -109,10 +98,6 @@ export const auditEvent = z.object({
109
98
  *
110
99
  * `before_seq` rather than `after_seq`, because this log is read **newest
111
100
  * 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
101
  */
117
102
  /**
118
103
  * A unix-millisecond bound a Postgres `timestamptz` can actually hold.
@@ -129,7 +114,7 @@ export const auditQuery = z.object({
129
114
  /** Only events with a smaller `seq` — the next, older page. */
130
115
  before_seq: wireSeqCursor.optional(),
131
116
  /**
132
- * Same shape as DEF-059's `historyQuery.limit`: a union whose input branch
117
+ * The same shape as `historyQuery.limit`: a union whose input branch
133
118
  * **is the wire**. A `z.coerce` cannot be published — zod renders the
134
119
  * coercion's result in either `io` direction, so the artifact would describe
135
120
  * a shape a query string can never carry.
@@ -162,36 +147,25 @@ export const auditQuery = z.object({
162
147
  /**
163
148
  * Only events by this actor.
164
149
  *
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.
150
+ * **`z.uuid()`, because the column is one.** A looser string type lets any
151
+ * non-uuid value reach the database as a uuid parameter, where the cast
152
+ * throws: `?actor_id=not-a-uuid` then answers **500 `internal_error`** rather
153
+ * than refusing the value.
173
154
  *
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.
155
+ * Not an injection question — the query is parameterised either way. It is a
156
+ * **500 where a 400 belongs**, and a 500 is the answer that explains nothing.
179
157
  */
180
158
  actor_id: z.uuid().optional(),
181
159
  /** Only events about this kind of target, e.g. `robot`. */
182
160
  target_kind: z.string().min(1).max(40).optional(),
183
161
  /**
184
162
  * Absolute bounds in unix milliseconds, **half-open `[from, to)`** — the
185
- * same rule the history shapes follow (DEF-062).
163
+ * same rule the history shapes follow.
186
164
  *
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.
165
+ * **Bounded to years 1..9999.** `nonnegative()` alone admits instants a
166
+ * timestamp column has no representation for, and the route answers 500
167
+ * rather than refusing the value. `Number.isSafeInteger` is wider than what a
168
+ * timestamp can be, so the bound is stated rather than inherited.
195
169
  */
196
170
  from_ms: auditTimestampMs.optional(),
197
171
  to_ms: auditTimestampMs.optional(),
@@ -216,7 +190,7 @@ export const auditListResponse = z.object({
216
190
  next_cursor: z.number().int().positive().nullable(),
217
191
  });
218
192
  /**
219
- * **What a CSV export of this log looks like (DEF-123, spec §16.3).**
193
+ * **What a CSV export of this log looks like.**
220
194
  *
221
195
  * The column order lives here because otherwise the cloud and the console
222
196
  * would each carry their own, and nobody would notice them drifting apart
@@ -229,10 +203,9 @@ export const auditListResponse = z.object({
229
203
  */
230
204
  export const AUDIT_CSV_COLUMNS = ['seq', 'at', 'actor_kind', 'actor_id', 'action', 'target_kind', 'target_id', 'target_label', 'details'];
231
205
  /**
232
- * Spec §16.3: the audit log is kept for **90 days**.
206
+ * The audit log is kept for **90 days**.
233
207
  *
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.
208
+ * A constant here so no consumer derives it a second time — the same reasoning
209
+ * as `ASSET_UPLOAD_MAX_BYTES`.
237
210
  */
238
211
  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
@@ -61,7 +61,7 @@ export declare const clientLogoutRequest: z.ZodObject<{
61
61
  }, z.core.$strip>;
62
62
  export type ClientLogoutRequest = z.infer<typeof clientLogoutRequest>;
63
63
  /**
64
- * **Self-registration** (D6) — and the account it creates cannot log in yet.
64
+ * **Self-registration** — and the account it creates cannot log in yet.
65
65
  *
66
66
  * `register` writes the user as `pending_verification` and mails the app's
67
67
  * `verify_url`. Without that step the domain whitelist would prove nothing:
@@ -221,7 +221,7 @@ export type ClientOidcStartQuery = z.infer<typeof clientOidcStartQuery>;
221
221
  * `state` is the one required field because it is the one Fleetless minted: it
222
222
  * resolves the `oidc_interactions` row that holds the app's `redirect_uri`,
223
223
  * and without it there is nowhere to send any answer, success or failure. That
224
- * is the single case where the cloud renders a page of its own (D2).
224
+ * is the single case where the cloud renders a page of its own.
225
225
  *
226
226
  * It exists as a schema rather than as four parameters read by hand because
227
227
  * the manifest forbids the second: a documented route whose prose names a
@@ -244,7 +244,7 @@ export type ClientOidcExchangeRequest = z.infer<typeof clientOidcExchangeRequest
244
244
  /**
245
245
  * **Why a federated sign-in ended without a session, in a code the app can
246
246
  * branch on** — carried back to the app's own `redirect_uri` as `error`, not
247
- * rendered by Fleetless (D2). The only Fleetless-rendered page in this flow is
247
+ * rendered by Fleetless. The only Fleetless-rendered page in this flow is
248
248
  * the one for a state that can no longer be resolved to a redirect URI, because
249
249
  * then there is nowhere to send the answer.
250
250
  *
@@ -293,7 +293,7 @@ export declare const clientOidcErrorCode: z.ZodEnum<{
293
293
  export type ClientOidcErrorCode = z.infer<typeof clientOidcErrorCode>;
294
294
  /**
295
295
  * **A pending MCP authorization, as the app's own consent screen reads it**
296
- * (D7). Fleetless renders no page here either: `authorize` redirects to the
296
+ * Fleetless renders no page here either: `authorize` redirects to the
297
297
  * app's `mcp_login_url` with an interaction id, the app authenticates the user
298
298
  * with its normal UI, shows this, and approves or denies through the API.
299
299
  *
@@ -388,7 +388,7 @@ export type McpConsentGrantListResponse = z.infer<typeof mcpConsentGrantListResp
388
388
  * quietest way for a cut like this to go wrong.
389
389
  *
390
390
  * **`act` is gone.** It named the org admin behind an impersonation (the RFC
391
- * 8693 pattern). Impersonation is deleted with no successor (D1), so a field
391
+ * 8693 pattern). Impersonation is deleted with no successor, so a field
392
392
  * that could still arrive would describe a delegation nothing can mint — and a
393
393
  * client rendering "you are acting as …" from it would be showing a state the
394
394
  * platform cannot enter.
@@ -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
@@ -74,7 +73,7 @@ export const clientLogoutRequest = z.object({
74
73
  });
75
74
  /* ---------------------------------------------- registration and mails -- */
76
75
  /**
77
- * **Self-registration** (D6) — and the account it creates cannot log in yet.
76
+ * **Self-registration** — and the account it creates cannot log in yet.
78
77
  *
79
78
  * `register` writes the user as `pending_verification` and mails the app's
80
79
  * `verify_url`. Without that step the domain whitelist would prove nothing:
@@ -272,7 +271,7 @@ export const clientOidcStartQuery = z.object({
272
271
  * `state` is the one required field because it is the one Fleetless minted: it
273
272
  * resolves the `oidc_interactions` row that holds the app's `redirect_uri`,
274
273
  * and without it there is nowhere to send any answer, success or failure. That
275
- * is the single case where the cloud renders a page of its own (D2).
274
+ * is the single case where the cloud renders a page of its own.
276
275
  *
277
276
  * It exists as a schema rather than as four parameters read by hand because
278
277
  * the manifest forbids the second: a documented route whose prose names a
@@ -307,7 +306,7 @@ export const clientOidcExchangeRequest = z
307
306
  /**
308
307
  * **Why a federated sign-in ended without a session, in a code the app can
309
308
  * branch on** — carried back to the app's own `redirect_uri` as `error`, not
310
- * rendered by Fleetless (D2). The only Fleetless-rendered page in this flow is
309
+ * rendered by Fleetless. The only Fleetless-rendered page in this flow is
311
310
  * the one for a state that can no longer be resolved to a redirect URI, because
312
311
  * then there is nowhere to send the answer.
313
312
  *
@@ -356,7 +355,7 @@ export const clientOidcErrorCode = z.enum([
356
355
  /* ------------------------------------------------ MCP, delegated login -- */
357
356
  /**
358
357
  * **A pending MCP authorization, as the app's own consent screen reads it**
359
- * (D7). Fleetless renders no page here either: `authorize` redirects to the
358
+ * Fleetless renders no page here either: `authorize` redirects to the
360
359
  * app's `mcp_login_url` with an interaction id, the app authenticates the user
361
360
  * with its normal UI, shows this, and approves or denies through the API.
362
361
  *
@@ -457,7 +456,7 @@ export const mcpConsentGrantListResponse = z.object({
457
456
  * quietest way for a cut like this to go wrong.
458
457
  *
459
458
  * **`act` is gone.** It named the org admin behind an impersonation (the RFC
460
- * 8693 pattern). Impersonation is deleted with no successor (D1), so a field
459
+ * 8693 pattern). Impersonation is deleted with no successor, so a field
461
460
  * that could still arrive would describe a delegation nothing can mint — and a
462
461
  * client rendering "you are acting as …" from it would be showing a state the
463
462
  * platform cannot enter.
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.
@@ -131,8 +123,8 @@ export declare const wireTimestampMs: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z
131
123
  *
132
124
  * Lives here, not in `protocol.ts`, for the same reason `slug` and friends
133
125
  * do: `config.ts`'s `configState.applied_errors` is the REST shape the
134
- * console reads this same error through (spec `2026-08-21-exposure-and-revoke-design`
135
- * D4), and `protocol.ts` already imports from `config.ts`
126
+ * console reads this same error through, and `protocol.ts` already imports
127
+ * from `config.ts`
136
128
  * (`credentialRef`, `robotConfigDoc`) — so `config.ts` importing back from
137
129
  * `protocol.ts` would be a cycle. One definition, reachable from both
138
130
  * without either importing the other.
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.
@@ -158,8 +149,8 @@ export const wireTimestampMs = z
158
149
  *
159
150
  * Lives here, not in `protocol.ts`, for the same reason `slug` and friends
160
151
  * do: `config.ts`'s `configState.applied_errors` is the REST shape the
161
- * console reads this same error through (spec `2026-08-21-exposure-and-revoke-design`
162
- * D4), and `protocol.ts` already imports from `config.ts`
152
+ * console reads this same error through, and `protocol.ts` already imports
153
+ * from `config.ts`
163
154
  * (`credentialRef`, `robotConfigDoc`) — so `config.ts` importing back from
164
155
  * `protocol.ts` would be a cycle. One definition, reachable from both
165
156
  * without either importing the other.