@fleetless/contracts 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +17 -0
  4. package/README.md +88 -0
  5. package/artifacts/constants.json +24 -0
  6. package/artifacts/openapi.json +17219 -0
  7. package/artifacts/routes.json +4605 -0
  8. package/artifacts/schema/accept-team-invite-request.schema.json +22 -0
  9. package/artifacts/schema/action-config.schema.json +198 -0
  10. package/artifacts/schema/alert-list-response.schema.json +172 -0
  11. package/artifacts/schema/api-error.schema.json +20 -0
  12. package/artifacts/schema/app-auth-config.schema.json +106 -0
  13. package/artifacts/schema/app-invitation-list-response.schema.json +57 -0
  14. package/artifacts/schema/app-invitation.schema.json +69 -0
  15. package/artifacts/schema/app-list-response.schema.json +82 -0
  16. package/artifacts/schema/app-mail-template-list-response.schema.json +68 -0
  17. package/artifacts/schema/app-mail-template.schema.json +54 -0
  18. package/artifacts/schema/app-oidc-provider-list-response.schema.json +93 -0
  19. package/artifacts/schema/app-oidc-provider.schema.json +80 -0
  20. package/artifacts/schema/app-user-list-response.schema.json +111 -0
  21. package/artifacts/schema/app-user.schema.json +98 -0
  22. package/artifacts/schema/app.schema.json +69 -0
  23. package/artifacts/schema/apply-error.schema.json +41 -0
  24. package/artifacts/schema/asset-list-response.schema.json +288 -0
  25. package/artifacts/schema/asset-sync-request.schema.json +17 -0
  26. package/artifacts/schema/asset-sync-response.schema.json +16 -0
  27. package/artifacts/schema/asset-sync-status.schema.json +136 -0
  28. package/artifacts/schema/asset.schema.json +68 -0
  29. package/artifacts/schema/audit-actor.schema.json +32 -0
  30. package/artifacts/schema/audit-event.schema.json +119 -0
  31. package/artifacts/schema/audit-list-response.schema.json +144 -0
  32. package/artifacts/schema/audit-query.schema.json +79 -0
  33. package/artifacts/schema/auth-error.schema.json +23 -0
  34. package/artifacts/schema/auth-me-response.schema.json +99 -0
  35. package/artifacts/schema/auth-ok.schema.json +115 -0
  36. package/artifacts/schema/authorization-server-metadata.schema.json +80 -0
  37. package/artifacts/schema/bridge-asset-progress.schema.json +99 -0
  38. package/artifacts/schema/bridge-assets-available.schema.json +25 -0
  39. package/artifacts/schema/bridge-camera-state.schema.json +78 -0
  40. package/artifacts/schema/bridge-config-applied.schema.json +67 -0
  41. package/artifacts/schema/bridge-hello.schema.json +65 -0
  42. package/artifacts/schema/bridge-introspect.schema.json +114 -0
  43. package/artifacts/schema/bridge-job-lost.schema.json +22 -0
  44. package/artifacts/schema/bridge-job-update.schema.json +100 -0
  45. package/artifacts/schema/bridge-pong.schema.json +19 -0
  46. package/artifacts/schema/bridge-pressure.schema.json +292 -0
  47. package/artifacts/schema/bridge-state.schema.json +24 -0
  48. package/artifacts/schema/bridge-type-definitions.schema.json +169 -0
  49. package/artifacts/schema/busy-details.schema.json +115 -0
  50. package/artifacts/schema/camera-descriptor.schema.json +45 -0
  51. package/artifacts/schema/camera-list-response.schema.json +58 -0
  52. package/artifacts/schema/camera-source.schema.json +240 -0
  53. package/artifacts/schema/cancel-request.schema.json +20 -0
  54. package/artifacts/schema/client-accept-invitation-request.schema.json +35 -0
  55. package/artifacts/schema/client-auth.schema.json +18 -0
  56. package/artifacts/schema/client-cancel.schema.json +45 -0
  57. package/artifacts/schema/client-identity.schema.json +103 -0
  58. package/artifacts/schema/client-invoke.schema.json +45 -0
  59. package/artifacts/schema/client-login-request.schema.json +29 -0
  60. package/artifacts/schema/client-logout-request.schema.json +14 -0
  61. package/artifacts/schema/client-mcp-interaction-decision-response.schema.json +15 -0
  62. package/artifacts/schema/client-mcp-interaction.schema.json +59 -0
  63. package/artifacts/schema/client-oidc-callback-query.schema.json +28 -0
  64. package/artifacts/schema/client-oidc-exchange-request.schema.json +21 -0
  65. package/artifacts/schema/client-oidc-start-query.schema.json +36 -0
  66. package/artifacts/schema/client-password-reset-confirm-request.schema.json +22 -0
  67. package/artifacts/schema/client-password-reset-request.schema.json +24 -0
  68. package/artifacts/schema/client-provider-list-query.schema.json +17 -0
  69. package/artifacts/schema/client-provider-list-response.schema.json +34 -0
  70. package/artifacts/schema/client-publish.schema.json +40 -0
  71. package/artifacts/schema/client-refresh-request.schema.json +14 -0
  72. package/artifacts/schema/client-register-request.schema.json +44 -0
  73. package/artifacts/schema/client-resend-verification-request.schema.json +24 -0
  74. package/artifacts/schema/client-subscribe.schema.json +43 -0
  75. package/artifacts/schema/client-unsubscribe.schema.json +26 -0
  76. package/artifacts/schema/client-verify-email-request.schema.json +15 -0
  77. package/artifacts/schema/cloud-asset-request.schema.json +37 -0
  78. package/artifacts/schema/cloud-camera-start.schema.json +41 -0
  79. package/artifacts/schema/cloud-camera-stop.schema.json +26 -0
  80. package/artifacts/schema/cloud-cancel.schema.json +33 -0
  81. package/artifacts/schema/cloud-config.schema.json +1635 -0
  82. package/artifacts/schema/cloud-hello-error.schema.json +23 -0
  83. package/artifacts/schema/cloud-hello-ok.schema.json +19 -0
  84. package/artifacts/schema/cloud-introspect-request.schema.json +19 -0
  85. package/artifacts/schema/cloud-invoke.schema.json +40 -0
  86. package/artifacts/schema/cloud-ping.schema.json +19 -0
  87. package/artifacts/schema/cloud-publish.schema.json +28 -0
  88. package/artifacts/schema/cloud-type-request.schema.json +30 -0
  89. package/artifacts/schema/command-result.schema.json +175 -0
  90. package/artifacts/schema/config-draft-response.schema.json +1695 -0
  91. package/artifacts/schema/config-state.schema.json +124 -0
  92. package/artifacts/schema/config-version-response.schema.json +1641 -0
  93. package/artifacts/schema/config-versions-response.schema.json +33 -0
  94. package/artifacts/schema/create-app-invitation-request.schema.json +40 -0
  95. package/artifacts/schema/create-app-oidc-provider-request.schema.json +70 -0
  96. package/artifacts/schema/create-app-request.schema.json +30 -0
  97. package/artifacts/schema/create-app-user-request.schema.json +42 -0
  98. package/artifacts/schema/create-robot-request.schema.json +14 -0
  99. package/artifacts/schema/create-robot-response.schema.json +44 -0
  100. package/artifacts/schema/create-server-key-response.schema.json +65 -0
  101. package/artifacts/schema/create-team-invite-request.schema.json +43 -0
  102. package/artifacts/schema/datapoint-alert-row.schema.json +160 -0
  103. package/artifacts/schema/datapoint-config.schema.json +366 -0
  104. package/artifacts/schema/datapoint-display.schema.json +31 -0
  105. package/artifacts/schema/datapoint-event.schema.json +34 -0
  106. package/artifacts/schema/datapoint-frame.schema.json +28 -0
  107. package/artifacts/schema/datapoint-list-response.schema.json +61 -0
  108. package/artifacts/schema/datapoint-value.schema.json +28 -0
  109. package/artifacts/schema/developer-login-request.schema.json +19 -0
  110. package/artifacts/schema/dynamic-client-registration-request.schema.json +60 -0
  111. package/artifacts/schema/dynamic-client-registration-response.schema.json +68 -0
  112. package/artifacts/schema/error-frame.schema.json +23 -0
  113. package/artifacts/schema/exposure-counts.schema.json +39 -0
  114. package/artifacts/schema/exposure-list-response.schema.json +43 -0
  115. package/artifacts/schema/fetch-types-request.schema.json +19 -0
  116. package/artifacts/schema/fetch-types-response.schema.json +163 -0
  117. package/artifacts/schema/fleetless-user-list-response.schema.json +73 -0
  118. package/artifacts/schema/fleetless-user.schema.json +60 -0
  119. package/artifacts/schema/history-buckets-response.schema.json +79 -0
  120. package/artifacts/schema/history-query.schema.json +58 -0
  121. package/artifacts/schema/history-response.schema.json +150 -0
  122. package/artifacts/schema/history-samples-response.schema.json +68 -0
  123. package/artifacts/schema/introspection-response.schema.json +118 -0
  124. package/artifacts/schema/invoke-or-service-response.schema.json +141 -0
  125. package/artifacts/schema/invoke-request.schema.json +23 -0
  126. package/artifacts/schema/invoke-response.schema.json +125 -0
  127. package/artifacts/schema/job-actor.schema.json +34 -0
  128. package/artifacts/schema/job-event.schema.json +158 -0
  129. package/artifacts/schema/job-response.schema.json +123 -0
  130. package/artifacts/schema/job-run-list-response.schema.json +222 -0
  131. package/artifacts/schema/job-run-query.schema.json +95 -0
  132. package/artifacts/schema/job-run-summary-query.schema.json +23 -0
  133. package/artifacts/schema/job-run-summary.schema.json +33 -0
  134. package/artifacts/schema/job-run.schema.json +195 -0
  135. package/artifacts/schema/job-state.schema.json +11 -0
  136. package/artifacts/schema/job.schema.json +106 -0
  137. package/artifacts/schema/latency-bucket.schema.json +63 -0
  138. package/artifacts/schema/live-session-response.schema.json +41 -0
  139. package/artifacts/schema/mail-outcome.schema.json +20 -0
  140. package/artifacts/schema/mail-template-preview-request.schema.json +35 -0
  141. package/artifacts/schema/mail-template-preview-response.schema.json +31 -0
  142. package/artifacts/schema/mail-template-problem-details.schema.json +24 -0
  143. package/artifacts/schema/mcp-consent-grant-list-response.schema.json +52 -0
  144. package/artifacts/schema/mcp-consent-grant.schema.json +39 -0
  145. package/artifacts/schema/mcp-robot-datasheet.schema.json +115 -0
  146. package/artifacts/schema/mcp-role-preview-response.schema.json +134 -0
  147. package/artifacts/schema/missing-asset-query.schema.json +11 -0
  148. package/artifacts/schema/oauth-authorize-query.schema.json +47 -0
  149. package/artifacts/schema/oauth-redirect-response.schema.json +15 -0
  150. package/artifacts/schema/oauth-token-request.schema.json +47 -0
  151. package/artifacts/schema/oauth-token-response.schema.json +38 -0
  152. package/artifacts/schema/org-alerts-query.schema.json +15 -0
  153. package/artifacts/schema/org-event-dropped.schema.json +26 -0
  154. package/artifacts/schema/org-event-replay.schema.json +97 -0
  155. package/artifacts/schema/org-event-subscribe.schema.json +14 -0
  156. package/artifacts/schema/org-event-unsubscribe.schema.json +14 -0
  157. package/artifacts/schema/org-event.schema.json +75 -0
  158. package/artifacts/schema/org-firing-alerts-response.schema.json +178 -0
  159. package/artifacts/schema/org-health-query.schema.json +13 -0
  160. package/artifacts/schema/org-latency-query.schema.json +42 -0
  161. package/artifacts/schema/org-latency-response.schema.json +124 -0
  162. package/artifacts/schema/org-quota-usage-counts.schema.json +42 -0
  163. package/artifacts/schema/org-quota-usage.schema.json +102 -0
  164. package/artifacts/schema/org-quotas.schema.json +51 -0
  165. package/artifacts/schema/org-usage-query.schema.json +19 -0
  166. package/artifacts/schema/org-usage-response.schema.json +77 -0
  167. package/artifacts/schema/org.schema.json +30 -0
  168. package/artifacts/schema/parameter-invalid-details.schema.json +37 -0
  169. package/artifacts/schema/parameter-spec.schema.json +120 -0
  170. package/artifacts/schema/parameter-violation.schema.json +24 -0
  171. package/artifacts/schema/password-change-request.schema.json +21 -0
  172. package/artifacts/schema/password-reset-confirm.schema.json +19 -0
  173. package/artifacts/schema/password-reset-request.schema.json +14 -0
  174. package/artifacts/schema/patch-app-oidc-provider-request.schema.json +50 -0
  175. package/artifacts/schema/patch-app-user-request.schema.json +34 -0
  176. package/artifacts/schema/patch-auth-me-request.schema.json +22 -0
  177. package/artifacts/schema/patch-fleetless-user-request.schema.json +20 -0
  178. package/artifacts/schema/patch-org-request.schema.json +15 -0
  179. package/artifacts/schema/patch-org-response.schema.json +40 -0
  180. package/artifacts/schema/patch-robot-request.schema.json +15 -0
  181. package/artifacts/schema/patch-robot-response.schema.json +40 -0
  182. package/artifacts/schema/pending-team-invite-list-response.schema.json +52 -0
  183. package/artifacts/schema/pending-team-invite.schema.json +39 -0
  184. package/artifacts/schema/protected-resource-metadata.schema.json +41 -0
  185. package/artifacts/schema/publish-config-response.schema.json +21 -0
  186. package/artifacts/schema/publish-request.schema.json +17 -0
  187. package/artifacts/schema/publisher-config.schema.json +285 -0
  188. package/artifacts/schema/put-app-auth-config-request.schema.json +93 -0
  189. package/artifacts/schema/put-app-mail-template-request.schema.json +35 -0
  190. package/artifacts/schema/put-config-draft-request.schema.json +13 -0
  191. package/artifacts/schema/put-datapoint-display-request.schema.json +31 -0
  192. package/artifacts/schema/put-robot-details-request.schema.json +41 -0
  193. package/artifacts/schema/put-robot-details-response.schema.json +43 -0
  194. package/artifacts/schema/rate-limit-details.schema.json +15 -0
  195. package/artifacts/schema/refresh-request.schema.json +13 -0
  196. package/artifacts/schema/release-live-query.schema.json +13 -0
  197. package/artifacts/schema/rename-slug-request.schema.json +23 -0
  198. package/artifacts/schema/rename-slug-response.schema.json +24 -0
  199. package/artifacts/schema/resource-health-event.schema.json +72 -0
  200. package/artifacts/schema/resource-health-list-response.schema.json +80 -0
  201. package/artifacts/schema/resource-health-state.schema.json +68 -0
  202. package/artifacts/schema/robot-config-doc.schema.json +1616 -0
  203. package/artifacts/schema/robot-delete-query.schema.json +12 -0
  204. package/artifacts/schema/robot-deletion-summary.schema.json +63 -0
  205. package/artifacts/schema/robot-detail-response.schema.json +262 -0
  206. package/artifacts/schema/robot-details-doc.schema.json +33 -0
  207. package/artifacts/schema/robot-jobs-response.schema.json +119 -0
  208. package/artifacts/schema/robot-latency-series.schema.json +81 -0
  209. package/artifacts/schema/robot-list-item.schema.json +94 -0
  210. package/artifacts/schema/robot-list-response.schema.json +106 -0
  211. package/artifacts/schema/robot.schema.json +30 -0
  212. package/artifacts/schema/role-list-response.schema.json +48 -0
  213. package/artifacts/schema/role-permissions.schema.json +61 -0
  214. package/artifacts/schema/role.schema.json +35 -0
  215. package/artifacts/schema/ros-graph.schema.json +99 -0
  216. package/artifacts/schema/server-key-list-response.schema.json +64 -0
  217. package/artifacts/schema/server-key.schema.json +51 -0
  218. package/artifacts/schema/service-call-response.schema.json +13 -0
  219. package/artifacts/schema/service-config.schema.json +198 -0
  220. package/artifacts/schema/session-tokens.schema.json +28 -0
  221. package/artifacts/schema/sign-up-request.schema.json +26 -0
  222. package/artifacts/schema/sign-up-response.schema.json +127 -0
  223. package/artifacts/schema/slug-usage-response.schema.json +32 -0
  224. package/artifacts/schema/snapshot-header.schema.json +44 -0
  225. package/artifacts/schema/snapshot-meta-response.schema.json +85 -0
  226. package/artifacts/schema/subscribe-error.schema.json +31 -0
  227. package/artifacts/schema/team-invite.schema.json +57 -0
  228. package/artifacts/schema/tier-change-request.schema.json +17 -0
  229. package/artifacts/schema/type-definition.schema.json +144 -0
  230. package/artifacts/schema/types-response.schema.json +156 -0
  231. package/artifacts/schema/update-app-request.schema.json +32 -0
  232. package/artifacts/schema/urdf-completeness.schema.json +50 -0
  233. package/artifacts/schema/validation-issue.schema.json +43 -0
  234. package/artifacts/schema/waitlist-request.schema.json +15 -0
  235. package/artifacts/schema-outgoing/bridge-asset-progress.schema.json +102 -0
  236. package/artifacts/schema-outgoing/bridge-assets-available.schema.json +26 -0
  237. package/artifacts/schema-outgoing/bridge-camera-state.schema.json +80 -0
  238. package/artifacts/schema-outgoing/bridge-config-applied.schema.json +69 -0
  239. package/artifacts/schema-outgoing/bridge-hello.schema.json +68 -0
  240. package/artifacts/schema-outgoing/bridge-introspect.schema.json +119 -0
  241. package/artifacts/schema-outgoing/bridge-job-lost.schema.json +23 -0
  242. package/artifacts/schema-outgoing/bridge-job-update.schema.json +102 -0
  243. package/artifacts/schema-outgoing/bridge-pong.schema.json +20 -0
  244. package/artifacts/schema-outgoing/bridge-type-definitions.schema.json +174 -0
  245. package/artifacts/schema-outgoing/datapoint-frame.schema.json +29 -0
  246. package/artifacts/schema-outgoing/snapshot-header.schema.json +45 -0
  247. package/dist/alerts.d.ts +255 -0
  248. package/dist/alerts.js +193 -0
  249. package/dist/app-users.d.ts +606 -0
  250. package/dist/app-users.js +696 -0
  251. package/dist/apps.d.ts +175 -0
  252. package/dist/apps.js +267 -0
  253. package/dist/assets.d.ts +434 -0
  254. package/dist/assets.js +546 -0
  255. package/dist/audit.d.ts +129 -0
  256. package/dist/audit.js +238 -0
  257. package/dist/client-auth.d.ts +409 -0
  258. package/dist/client-auth.js +487 -0
  259. package/dist/common.d.ts +186 -0
  260. package/dist/common.js +199 -0
  261. package/dist/config-issues.d.ts +175 -0
  262. package/dist/config-issues.js +339 -0
  263. package/dist/config.d.ts +862 -0
  264. package/dist/config.js +1988 -0
  265. package/dist/errors.d.ts +52 -0
  266. package/dist/errors.js +786 -0
  267. package/dist/identity.d.ts +549 -0
  268. package/dist/identity.js +503 -0
  269. package/dist/index.d.ts +51 -0
  270. package/dist/index.js +51 -0
  271. package/dist/introspection.d.ts +99 -0
  272. package/dist/introspection.js +97 -0
  273. package/dist/jobs.d.ts +334 -0
  274. package/dist/jobs.js +345 -0
  275. package/dist/mcp.d.ts +239 -0
  276. package/dist/mcp.js +153 -0
  277. package/dist/oauth.d.ts +344 -0
  278. package/dist/oauth.js +488 -0
  279. package/dist/protocol.d.ts +781 -0
  280. package/dist/protocol.js +715 -0
  281. package/dist/realtime.d.ts +494 -0
  282. package/dist/realtime.js +512 -0
  283. package/dist/rest.d.ts +1989 -0
  284. package/dist/rest.js +1963 -0
  285. package/dist/routes.d.ts +94 -0
  286. package/dist/routes.js +2298 -0
  287. package/package.json +61 -0
@@ -0,0 +1,339 @@
1
+ /** What a path with no segments at all is called, since `path` may not be empty. */
2
+ export const DOCUMENT_ROOT_PATH = '(document)';
3
+ /**
4
+ * The five sections whose keys are slugs — one namespace across all of them,
5
+ * which is what lets a role grant say `{robot, slug}` without naming a kind.
6
+ * `messages:` is deliberately not among them: its names are their own
7
+ * namespace.
8
+ *
9
+ * `cloud/src/config-sections.ts` re-exports this constant and drives the
10
+ * cloud's iteration over sections from it; the console reads it directly
11
+ * (`useConfigRepairs.ts`). It was spelled out separately in all three until
12
+ * wave 2 task 8 (cloud `a307e18`, 2026-09-03) — this is the only spelling
13
+ * since.
14
+ */
15
+ export const EXPOSURE_SECTIONS = ['datapoints', 'actions', 'services', 'publishers', 'cameras'];
16
+ /**
17
+ * The refusals `robotConfigDoc` already made, reported as validation issues
18
+ * with their FL-002 codes.
19
+ *
20
+ * **This maps; it does not re-decide.** Seven of the thirteen codes are
21
+ * answered by the schema before a document ever becomes a `RobotConfigDoc`,
22
+ * and `config.ts` attaches `params: { code }` at each site for exactly this —
23
+ * its header lists which codes it decides and which it defers. Reading
24
+ * `params.code` is also the only stable join: the prose of a message is not a
25
+ * contract and matching on it is a join nobody notices breaking.
26
+ *
27
+ * Two refusals carry no `params.code` and are recognised by zod's own issue
28
+ * code instead, which the same header says consumers should do:
29
+ *
30
+ * - `unrecognized_keys` is `unknown_key`. One issue per key, so the path
31
+ * names the offending key rather than its parent.
32
+ * - `invalid_type` **where the value at that path is `null`** is
33
+ * `explicit_null`. The condition is checked against the parsed value and
34
+ * not against the message, which says "received null" — see above. Zod 4
35
+ * does not carry the input on the issue, so the value is navigated to. The
36
+ * sentence differs at the document root, where there is no key to remove:
37
+ * see `EMPTY_DOCUMENT_MESSAGE`.
38
+ *
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
41
+ * key that is not a slug — and inventing a fourteenth code for them would put
42
+ * a code on the wire that no table documents.
43
+ */
44
+ export function schemaIssues(value, issues) {
45
+ return issues.flatMap((issue) => {
46
+ if (issue.code === 'unrecognized_keys') {
47
+ return (issue.keys ?? []).map((key) => refusal([...issue.path, key], 'unknown_key', `'${key}' is not a key this format defines.`));
48
+ }
49
+ // `'params' in issue` rather than `issue.code === 'custom'`: the cloud's
50
+ // version read `params` off any issue that carried one, and narrowing by
51
+ // code here would be a quieter rule than the one being moved. Zod only
52
+ // declares `params` on the custom issue, so the `in` check is also what
53
+ // types it.
54
+ const declared = 'params' in issue ? issue.params?.['code'] : undefined;
55
+ if (typeof declared === 'string')
56
+ return [refusal(issue.path, declared, issue.message)];
57
+ if (issue.code === 'invalid_type' && valueAt(value, issue.path) === null) {
58
+ return [refusal(issue.path, 'explicit_null', issue.path.length === 0 ? EMPTY_DOCUMENT_MESSAGE : NULL_KEY_MESSAGE)];
59
+ }
60
+ return [refusal(issue.path, issue.code, issue.message)];
61
+ });
62
+ }
63
+ const NULL_KEY_MESSAGE = 'This key is null. Omission is the only spelling of "not set" in this format — remove the key instead.';
64
+ /**
65
+ * The same refusal at the document root, where **there is no key**.
66
+ *
67
+ * The whole document is the null: the file is empty, holds nothing but
68
+ * comments, or says `null` / `~` outright. All four reach `robotConfigDoc` as
69
+ * a genuine `invalid_type` on `null` at the empty path, so the code is right —
70
+ * but the sentence for a null *key* told the developer to remove a key that
71
+ * 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.
75
+ *
76
+ * It names the smallest legal document rather than only saying what is wrong,
77
+ * because at this path there is no line to jump to and no repair to offer —
78
+ * `repairsFor`'s `explicit_null` branch looks the path up in the text and
79
+ * finds nothing, correctly. The sentence is the entire remedy the developer
80
+ * gets.
81
+ */
82
+ const EMPTY_DOCUMENT_MESSAGE = 'There is no document in this file — it is empty, holds only comments, or is an explicit null. A fleetless configuration is a mapping, and the smallest one is the single line "fleetless: 1".';
83
+ function refusal(path, code, message) {
84
+ return { path: formatPath(path), slug: slugOf(path), code, message, severity: 'error' };
85
+ }
86
+ const EXPOSURE_SECTION_NAMES = new Set(EXPOSURE_SECTIONS);
87
+ /**
88
+ * The entry a path belongs to, for the console's "jump to it" link. `null`
89
+ * for anything outside the five exposure sections — `messages:` most of all,
90
+ * whose names are their own namespace.
91
+ */
92
+ function slugOf(path) {
93
+ const [section, slug] = path;
94
+ if (typeof section !== 'string' || !EXPOSURE_SECTION_NAMES.has(section))
95
+ return null;
96
+ return typeof slug === 'string' ? slug : null;
97
+ }
98
+ /**
99
+ * `['datapoints','a','enum',0]` -> `datapoints.a.enum[0]`, the spelling every
100
+ * other path here uses.
101
+ *
102
+ * **A segment that would render as nothing is written quoted instead.** The
103
+ * last segment of an `unrecognized_keys` or `invalid_key` path is a key the
104
+ * *developer* wrote, and YAML lets that key be empty (`"": 3`), nothing but
105
+ * whitespace, or — see `isBlank` — nothing but characters that occupy no
106
+ * width. Rendered bare, such a key produced a path a reader cannot act
107
+ * on — and at the root it produced the empty string, which
108
+ * `validationIssue.path` (`z.string().min(1)`) refuses. That was the cloud
109
+ * publishing a finding that fails the cloud's own contract for findings, and
110
+ * after D2 stored the draft it cost the whole `configDraftResponse`, not one
111
+ * issue: the console's `safeParse` dropped the response and handed the editor
112
+ * nothing, for two characters typed.
113
+ *
114
+ * The quoted spelling is the segment's JSON string literal, and that is the
115
+ * whole of the reason for choosing it: JSON's string syntax is a subset of
116
+ * YAML's double-quoted scalar syntax, so `""`, `" "` and `"\t"` are each a
117
+ * valid YAML spelling of exactly the key being complained about. The path is
118
+ * therefore text the developer can search their own file for — which is the
119
+ * bar this has to clear. It is also the same move `DOCUMENT_ROOT_PATH` makes
120
+ * for the no-segments case, one level down: give the invisible thing a name.
121
+ *
122
+ * **Only blank segments are quoted.** A segment containing `.` or `[` is
123
+ * still written bare, so it still cannot be read back — see
124
+ * `splitFormatPath`, which documents why escaping those was rejected. That
125
+ * decision is unchanged here on purpose: those paths are wrong for one
126
+ * console lookup, these were wrong on the wire.
127
+ */
128
+ export function formatPath(path) {
129
+ if (path.length === 0)
130
+ return DOCUMENT_ROOT_PATH;
131
+ return path.reduce((acc, segment, index) => {
132
+ if (typeof segment === 'number')
133
+ return `${acc}[${segment}]`;
134
+ const written = isBlank(segment) ? quoteBlank(segment) : String(segment);
135
+ // Indexed rather than `acc === ''`: "first segment" used to be detected as
136
+ // "nothing written yet", which is how an empty first segment came to be
137
+ // dropped entirely — `formatPath(['', 'a'])` was `'a'`, a path naming a
138
+ // key the document does not have. Quoting means no segment writes nothing
139
+ // any more, but a guard that holds only because of what another line
140
+ // happens to produce is the shape this file exists to avoid.
141
+ return index === 0 ? written : `${acc}.${written}`;
142
+ }, '');
143
+ }
144
+ /**
145
+ * The quoted spelling of a blank segment: its JSON string literal, with every
146
+ * zero-width character written as a `\uXXXX` escape.
147
+ *
148
+ * `JSON.stringify` escapes the C0 controls and nothing else, so a zero-width
149
+ * space came back as itself and `"\u200b"` rendered as two quote marks with
150
+ * nothing between them — visible as *a* blank key, but indistinguishable from
151
+ * `""`, and so not findable. The bar this function's caller set itself is that
152
+ * the developer can search their own file for the path, and `\uXXXX` is a JSON
153
+ * escape *and* a YAML double-quoted escape, so the quoted form stays a valid
154
+ * YAML spelling of exactly the key complained about while naming which
155
+ * invisible character it is. Astral format characters are left as
156
+ * `JSON.stringify` wrote them: `\uXXXX` cannot spell them and their surrogate
157
+ * pair already round-trips.
158
+ */
159
+ function quoteBlank(segment) {
160
+ return JSON.stringify(segment).replace(/\p{Cf}/gu, (char) => {
161
+ const code = char.codePointAt(0);
162
+ return code > 0xffff ? char : `\\u${code.toString(16).padStart(4, '0')}`;
163
+ });
164
+ }
165
+ /**
166
+ * A name with nothing in it to read: empty, whitespace all the way through, or
167
+ * made of characters that occupy no width.
168
+ *
169
+ * `trim()` alone is not the test, and that gap was real rather than
170
+ * theoretical: `trim()` removes Unicode `White_Space`, and a zero-width space
171
+ * (`U+200B`) is not white space — it is a format character (`Cf`), as are
172
+ * `U+200C`–`U+200F`, the word joiner `U+2060` and a stray BOM `U+FEFF`. A key
173
+ * spelled with one of those rendered bare and therefore rendered as nothing,
174
+ * which is the exact defect quoting exists to close, one character class over.
175
+ * Format characters are stripped before the trim so both classes, and any
176
+ * mixture of them, reach the same answer.
177
+ *
178
+ * This is the **only** spelling of "blank" in this file. `unquoteBlank` asks
179
+ * the same question on the way back and must get the same answer, or a path
180
+ * `formatPath` quoted stops round-tripping.
181
+ */
182
+ function isBlank(segment) {
183
+ return typeof segment === 'string' && segment.replace(/\p{Cf}/gu, '').trim() === '';
184
+ }
185
+ /**
186
+ * `formatPath` read back — `datapoints.a.enum[0]` -> `['datapoints','a','enum',0]`.
187
+ *
188
+ * It exists because two console call sites split an issue path on `.` alone
189
+ * while the cloud writes sequence indices in brackets, so `ranges[0]` reached
190
+ * a document lookup as one segment that matches no key.
191
+ *
192
+ * **It is not the inverse of `formatPath`, and must not be read as one.**
193
+ * `formatPath` writes `.` and `[n]` as structure and escapes nothing, so a
194
+ * name that contains either is indistinguishable afterwards from the
195
+ * structure it looks like. This is reachable, not theoretical: an
196
+ * `unrecognized_keys` path ends in a key the **developer** chose, and YAML
197
+ * lets that key be `a.b` or `ranges[0]`.
198
+ *
199
+ * Escaping on the way out was the alternative and was rejected: `path` is a
200
+ * wire field (`validationIssue.path`), it is rendered to developers as-is,
201
+ * and every recorded expectation in this repo and the cloud's spells it
202
+ * unescaped. Changing what the server says about every document to make one
203
+ * console lookup total is the larger of the two costs.
204
+ *
205
+ * So the property this has, and the one its test asserts, is the narrow one:
206
+ * **a path round-trips when no string segment contains `.` or `[`, and the
207
+ * path is not the single segment `(document)`.** Outside that, the split is a
208
+ * best guess. What it costs is bounded — the console uses the result to find
209
+ * a line to put a marker on, so a wrong split finds no line and the marker is
210
+ * not placed. It never makes the console assert something false about the
211
+ * document.
212
+ *
213
+ * A **blank** segment is inside that property rather than outside it, and
214
+ * that is new. `formatPath` used to drop an empty first segment entirely
215
+ * (`formatPath(['', 'a'])` was `'a'`, a path naming a different key) and to
216
+ * write a nested one as a trailing `.`; at the root it produced the empty
217
+ * string, which `validationIssue.path`'s `min(1)` refuses outright. It now
218
+ * quotes blank segments, and `unquoteBlank` reads them back, so `['']`,
219
+ * `[' ']` and `['datapoints', 'battery_soc', '']` all round-trip. The single
220
+ * new non-round-trip that buys is a key literally spelled with quote marks
221
+ * around whitespace.
222
+ */
223
+ export function splitFormatPath(path) {
224
+ if (path === DOCUMENT_ROOT_PATH)
225
+ return [];
226
+ const segments = [];
227
+ for (const chunk of path.split('.')) {
228
+ // A blank segment left `formatPath` quoted, so read it back. Narrowed to
229
+ // *blank* content on purpose: it is the only content `formatPath` quotes,
230
+ // so this cannot misread `"x"`, and the one key it does misread — a key
231
+ // literally spelled with quote marks around whitespace — is the same
232
+ // bounded cost as the `.` and `[` cases below.
233
+ const unquoted = unquoteBlank(chunk);
234
+ if (unquoted !== null) {
235
+ segments.push(unquoted);
236
+ continue;
237
+ }
238
+ const match = /^([^[\]]*)((?:\[\d+\])+)$/.exec(chunk);
239
+ if (match === null) {
240
+ segments.push(chunk);
241
+ continue;
242
+ }
243
+ // A chunk is `name[0][1]` or a bare `[0]`; the name is absent only when
244
+ // the whole path starts with an index, which `formatPath` does write.
245
+ const [, name, indices] = match;
246
+ if (name !== '')
247
+ segments.push(name);
248
+ for (const index of indices.slice(1, -1).split(']['))
249
+ segments.push(Number(index));
250
+ }
251
+ return segments;
252
+ }
253
+ /** The blank string a chunk quotes, or `null` if it does not quote one. */
254
+ function unquoteBlank(chunk) {
255
+ if (chunk.length < 2 || !chunk.startsWith('"') || !chunk.endsWith('"'))
256
+ return null;
257
+ let value;
258
+ try {
259
+ value = JSON.parse(chunk);
260
+ }
261
+ catch {
262
+ return null;
263
+ }
264
+ return typeof value === 'string' && isBlank(value) ? value : null;
265
+ }
266
+ /**
267
+ * The value a zod issue's path points at in the document that was parsed.
268
+ *
269
+ * `Object.hasOwn`, not a bare index, for the reason `sectionGet` gives: the
270
+ * value came out of a YAML parse and carries `Object.prototype`, so a path
271
+ * segment like `constructor` would otherwise read a function off the
272
+ * prototype and answer a question about a key the document never had.
273
+ */
274
+ function valueAt(root, path) {
275
+ let cursor = root;
276
+ for (const segment of path) {
277
+ if (cursor === null || typeof cursor !== 'object')
278
+ return undefined;
279
+ if (Array.isArray(cursor)) {
280
+ if (typeof segment !== 'number')
281
+ return undefined;
282
+ cursor = cursor[segment];
283
+ continue;
284
+ }
285
+ const key = String(segment);
286
+ if (!Object.hasOwn(cursor, key))
287
+ return undefined;
288
+ cursor = cursor[key];
289
+ }
290
+ return cursor;
291
+ }
292
+ /**
293
+ * A stable hash of a schema object, for asking *is the thing running the one
294
+ * I think it is?*
295
+ *
296
+ * Wave 5's browser sweep enumerates positions against a schema it holds and
297
+ * has to know that the editor is running the same one; the manifest that
298
+ * makes a schema-side change announce itself uses the same number as its
299
+ * baseline. Both are the same question, so there is one implementation of it:
300
+ * a second one on the sweep side would drift, and the gate would then go red
301
+ * for the drift rather than for the schema.
302
+ *
303
+ * Canonical JSON first — object keys sorted at every depth, so a re-ordered
304
+ * `meta()` block is not a change — then FNV-1a over the result, 64 bits as
305
+ * 16 hex characters. Sorting is done through the `JSON.stringify` replacer,
306
+ * which also means a cyclic input throws the engine's own "converting
307
+ * circular structure" TypeError rather than hanging.
308
+ *
309
+ * **Named residual: this is a change detector, not a digest.** FNV-1a is not
310
+ * a cryptographic hash and a collision can be constructed on purpose. It is
311
+ * asked *did this object change since the baseline was recorded*, by the
312
+ * people who wrote both; nothing here defends against someone choosing the
313
+ * input. `crypto.subtle` would be the answer to the other question and is
314
+ * async, which a `data-` attribute rendered during setup cannot be.
315
+ */
316
+ export function configSchemaHash(schema) {
317
+ return fnv1a64(canonicalJson(schema));
318
+ }
319
+ function canonicalJson(value) {
320
+ return (JSON.stringify(value, (_key, inner) => inner !== null && typeof inner === 'object' && !Array.isArray(inner)
321
+ ? Object.fromEntries(Object.keys(inner)
322
+ .sort()
323
+ .map((key) => [key, inner[key]]))
324
+ : inner) ??
325
+ // `JSON.stringify` answers `undefined`, not a string, for `undefined` and
326
+ // for a function. Hashing the word keeps this function total; a caller
327
+ // that passed one by accident gets a hash that matches no baseline, which
328
+ // is the outcome it wants anyway.
329
+ 'undefined');
330
+ }
331
+ function fnv1a64(text) {
332
+ const PRIME = 0x100000001b3n;
333
+ const MASK = 0xffffffffffffffffn;
334
+ let hash = 0xcbf29ce484222325n;
335
+ for (let i = 0; i < text.length; i++) {
336
+ hash = ((hash ^ BigInt(text.charCodeAt(i))) * PRIME) & MASK;
337
+ }
338
+ return hash.toString(16).padStart(16, '0');
339
+ }