wicked-crew 0.16.11 → 0.16.13

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 (341) hide show
  1. package/README.md +28 -8
  2. package/dist/api/artifact-outline.d.ts +64 -0
  3. package/dist/api/artifact-outline.d.ts.map +1 -0
  4. package/dist/api/artifact-outline.js +254 -0
  5. package/dist/api/artifact-outline.js.map +1 -0
  6. package/dist/api/beneath.d.ts +76 -0
  7. package/dist/api/beneath.d.ts.map +1 -0
  8. package/dist/api/beneath.js +214 -0
  9. package/dist/api/beneath.js.map +1 -0
  10. package/dist/api/capture.d.ts +2 -2
  11. package/dist/api/chat-citations.d.ts +25 -1
  12. package/dist/api/chat-citations.d.ts.map +1 -1
  13. package/dist/api/chat-citations.js +186 -61
  14. package/dist/api/chat-citations.js.map +1 -1
  15. package/dist/api/chat-context.d.ts +236 -0
  16. package/dist/api/chat-context.d.ts.map +1 -0
  17. package/dist/api/chat-context.js +808 -0
  18. package/dist/api/chat-context.js.map +1 -0
  19. package/dist/api/chat-history.d.ts +20 -0
  20. package/dist/api/chat-history.d.ts.map +1 -1
  21. package/dist/api/chat-history.js +46 -1
  22. package/dist/api/chat-history.js.map +1 -1
  23. package/dist/api/chat-proposals.d.ts +534 -0
  24. package/dist/api/chat-proposals.d.ts.map +1 -0
  25. package/dist/api/chat-proposals.js +1562 -0
  26. package/dist/api/chat-proposals.js.map +1 -0
  27. package/dist/api/chat-scope.d.ts +41 -0
  28. package/dist/api/chat-scope.d.ts.map +1 -1
  29. package/dist/api/chat-scope.js +87 -15
  30. package/dist/api/chat-scope.js.map +1 -1
  31. package/dist/api/chat-transcripts.d.ts +27 -2
  32. package/dist/api/chat-transcripts.d.ts.map +1 -1
  33. package/dist/api/chat-transcripts.js +105 -4
  34. package/dist/api/chat-transcripts.js.map +1 -1
  35. package/dist/api/diagnostics.d.ts +15 -0
  36. package/dist/api/diagnostics.d.ts.map +1 -1
  37. package/dist/api/diagnostics.js +56 -0
  38. package/dist/api/diagnostics.js.map +1 -1
  39. package/dist/api/elicitation-cache.d.ts +13 -0
  40. package/dist/api/elicitation-cache.d.ts.map +1 -1
  41. package/dist/api/elicitation-cache.js +12 -0
  42. package/dist/api/elicitation-cache.js.map +1 -1
  43. package/dist/api/eval-sample.d.ts +12 -12
  44. package/dist/api/frame-guard.d.ts +62 -0
  45. package/dist/api/frame-guard.d.ts.map +1 -0
  46. package/dist/api/frame-guard.js +97 -0
  47. package/dist/api/frame-guard.js.map +1 -0
  48. package/dist/api/governance-steering.d.ts +4 -4
  49. package/dist/api/multiscope.d.ts +3 -0
  50. package/dist/api/multiscope.d.ts.map +1 -1
  51. package/dist/api/multiscope.js +13 -0
  52. package/dist/api/multiscope.js.map +1 -1
  53. package/dist/api/needs.d.ts +6 -0
  54. package/dist/api/needs.d.ts.map +1 -1
  55. package/dist/api/needs.js +31 -4
  56. package/dist/api/needs.js.map +1 -1
  57. package/dist/api/product.d.ts.map +1 -1
  58. package/dist/api/product.js +13 -2
  59. package/dist/api/product.js.map +1 -1
  60. package/dist/api/project-aggregates.d.ts +6 -6
  61. package/dist/api/project-aggregates.d.ts.map +1 -1
  62. package/dist/api/project-aggregates.js +16 -24
  63. package/dist/api/project-aggregates.js.map +1 -1
  64. package/dist/api/requirements.d.ts +261 -6
  65. package/dist/api/requirements.d.ts.map +1 -1
  66. package/dist/api/requirements.js +344 -64
  67. package/dist/api/requirements.js.map +1 -1
  68. package/dist/api/routes.d.ts +156 -4
  69. package/dist/api/routes.d.ts.map +1 -1
  70. package/dist/api/routes.js +851 -78
  71. package/dist/api/routes.js.map +1 -1
  72. package/dist/api/run-brief.d.ts +78 -0
  73. package/dist/api/run-brief.d.ts.map +1 -0
  74. package/dist/api/run-brief.js +318 -0
  75. package/dist/api/run-brief.js.map +1 -0
  76. package/dist/api/run-files.d.ts.map +1 -1
  77. package/dist/api/run-files.js +5 -1
  78. package/dist/api/run-files.js.map +1 -1
  79. package/dist/api/run-timing-index.d.ts +27 -0
  80. package/dist/api/run-timing-index.d.ts.map +1 -1
  81. package/dist/api/run-timing-index.js +65 -0
  82. package/dist/api/run-timing-index.js.map +1 -1
  83. package/dist/api/seat-images.d.ts +102 -0
  84. package/dist/api/seat-images.d.ts.map +1 -0
  85. package/dist/api/seat-images.js +333 -0
  86. package/dist/api/seat-images.js.map +1 -0
  87. package/dist/api/server.d.ts +3 -0
  88. package/dist/api/server.d.ts.map +1 -1
  89. package/dist/api/server.js +265 -2
  90. package/dist/api/server.js.map +1 -1
  91. package/dist/api/steering-landing.d.ts +48 -1
  92. package/dist/api/steering-landing.d.ts.map +1 -1
  93. package/dist/api/steering-landing.js +116 -42
  94. package/dist/api/steering-landing.js.map +1 -1
  95. package/dist/api/system-snapshot.d.ts +51 -2
  96. package/dist/api/system-snapshot.d.ts.map +1 -1
  97. package/dist/api/system-snapshot.js +151 -15
  98. package/dist/api/system-snapshot.js.map +1 -1
  99. package/dist/api/testing.d.ts +4 -4
  100. package/dist/api/testing.js +2 -2
  101. package/dist/api/testing.js.map +1 -1
  102. package/dist/apps/spec.d.ts +3009 -0
  103. package/dist/apps/spec.d.ts.map +1 -0
  104. package/dist/apps/spec.js +870 -0
  105. package/dist/apps/spec.js.map +1 -0
  106. package/dist/campaigns/routes.js +1 -1
  107. package/dist/campaigns/routes.js.map +1 -1
  108. package/dist/cli/index.js +3 -3
  109. package/dist/cli/index.js.map +1 -1
  110. package/dist/core/adapter.d.ts +12 -0
  111. package/dist/core/adapter.d.ts.map +1 -1
  112. package/dist/core/adapter.js +36 -1
  113. package/dist/core/adapter.js.map +1 -1
  114. package/dist/core/deliver-text.d.ts.map +1 -1
  115. package/dist/core/deliver-text.js +22 -10
  116. package/dist/core/deliver-text.js.map +1 -1
  117. package/dist/core/estate-mcp-client.d.ts +7 -1
  118. package/dist/core/estate-mcp-client.d.ts.map +1 -1
  119. package/dist/core/estate-mcp-client.js +10 -2
  120. package/dist/core/estate-mcp-client.js.map +1 -1
  121. package/dist/core/exec.d.ts +12 -0
  122. package/dist/core/exec.d.ts.map +1 -1
  123. package/dist/core/exec.js +23 -0
  124. package/dist/core/exec.js.map +1 -1
  125. package/dist/core/reassign-refusal.d.ts.map +1 -1
  126. package/dist/core/reassign-refusal.js +23 -3
  127. package/dist/core/reassign-refusal.js.map +1 -1
  128. package/dist/core/repoPaths.d.ts +32 -7
  129. package/dist/core/repoPaths.d.ts.map +1 -1
  130. package/dist/core/repoPaths.js +56 -9
  131. package/dist/core/repoPaths.js.map +1 -1
  132. package/dist/core/requirements-graph-setting.d.ts +32 -0
  133. package/dist/core/requirements-graph-setting.d.ts.map +1 -0
  134. package/dist/core/requirements-graph-setting.js +84 -0
  135. package/dist/core/requirements-graph-setting.js.map +1 -0
  136. package/dist/core/run-boot-resume.d.ts +4 -1
  137. package/dist/core/run-boot-resume.d.ts.map +1 -1
  138. package/dist/core/run-boot-resume.js +6 -1
  139. package/dist/core/run-boot-resume.js.map +1 -1
  140. package/dist/core/run-identity.d.ts.map +1 -1
  141. package/dist/core/run-identity.js +6 -0
  142. package/dist/core/run-identity.js.map +1 -1
  143. package/dist/core/types.d.ts +14 -0
  144. package/dist/core/types.d.ts.map +1 -1
  145. package/dist/core/types.js.map +1 -1
  146. package/dist/decisions/chat-recorder.d.ts +14 -3
  147. package/dist/decisions/chat-recorder.d.ts.map +1 -1
  148. package/dist/decisions/chat-recorder.js +54 -9
  149. package/dist/decisions/chat-recorder.js.map +1 -1
  150. package/dist/editors/csp.d.ts +24 -4
  151. package/dist/editors/csp.d.ts.map +1 -1
  152. package/dist/editors/csp.js +35 -8
  153. package/dist/editors/csp.js.map +1 -1
  154. package/dist/editors/registry.d.ts +46 -21
  155. package/dist/editors/registry.d.ts.map +1 -1
  156. package/dist/editors/registry.js +13 -2
  157. package/dist/editors/registry.js.map +1 -1
  158. package/dist/editors/routes.d.ts +4 -1
  159. package/dist/editors/routes.d.ts.map +1 -1
  160. package/dist/editors/routes.js +30 -3
  161. package/dist/editors/routes.js.map +1 -1
  162. package/dist/editors/visuals.d.ts +51 -0
  163. package/dist/editors/visuals.d.ts.map +1 -0
  164. package/dist/editors/visuals.js +164 -0
  165. package/dist/editors/visuals.js.map +1 -0
  166. package/dist/editors/webrtc-guard.d.ts +94 -0
  167. package/dist/editors/webrtc-guard.d.ts.map +1 -0
  168. package/dist/editors/webrtc-guard.js +506 -0
  169. package/dist/editors/webrtc-guard.js.map +1 -0
  170. package/dist/inspect/reading.d.ts +110 -0
  171. package/dist/inspect/reading.d.ts.map +1 -0
  172. package/dist/inspect/reading.js +325 -0
  173. package/dist/inspect/reading.js.map +1 -0
  174. package/dist/inspect/routes.d.ts +14 -0
  175. package/dist/inspect/routes.d.ts.map +1 -0
  176. package/dist/inspect/routes.js +44 -0
  177. package/dist/inspect/routes.js.map +1 -0
  178. package/dist/inspect/service.d.ts +161 -0
  179. package/dist/inspect/service.d.ts.map +1 -0
  180. package/dist/inspect/service.js +466 -0
  181. package/dist/inspect/service.js.map +1 -0
  182. package/dist/interactive/bridge-pool.d.ts +11 -2
  183. package/dist/interactive/bridge-pool.d.ts.map +1 -1
  184. package/dist/interactive/bridge-pool.js +10 -1
  185. package/dist/interactive/bridge-pool.js.map +1 -1
  186. package/dist/interactive/doc-binding.d.ts +156 -0
  187. package/dist/interactive/doc-binding.d.ts.map +1 -0
  188. package/dist/interactive/doc-binding.js +277 -0
  189. package/dist/interactive/doc-binding.js.map +1 -0
  190. package/dist/interactive/doc-delete-routes.d.ts +3 -0
  191. package/dist/interactive/doc-delete-routes.d.ts.map +1 -1
  192. package/dist/interactive/doc-delete-routes.js +4 -0
  193. package/dist/interactive/doc-delete-routes.js.map +1 -1
  194. package/dist/interactive/draft-events.d.ts +89 -3
  195. package/dist/interactive/draft-events.d.ts.map +1 -1
  196. package/dist/interactive/draft-events.js +441 -30
  197. package/dist/interactive/draft-events.js.map +1 -1
  198. package/dist/interactive/draft-skill.d.ts +7 -0
  199. package/dist/interactive/draft-skill.d.ts.map +1 -1
  200. package/dist/interactive/draft-skill.js +10 -3
  201. package/dist/interactive/draft-skill.js.map +1 -1
  202. package/dist/interactive/edit-events.d.ts +2 -0
  203. package/dist/interactive/edit-events.d.ts.map +1 -1
  204. package/dist/interactive/edit-events.js +11 -0
  205. package/dist/interactive/edit-events.js.map +1 -1
  206. package/dist/interactive/proposal-create.d.ts +143 -0
  207. package/dist/interactive/proposal-create.d.ts.map +1 -0
  208. package/dist/interactive/proposal-create.js +318 -0
  209. package/dist/interactive/proposal-create.js.map +1 -0
  210. package/dist/interactive/proxy-routes.d.ts +60 -3
  211. package/dist/interactive/proxy-routes.d.ts.map +1 -1
  212. package/dist/interactive/proxy-routes.js +217 -3
  213. package/dist/interactive/proxy-routes.js.map +1 -1
  214. package/dist/interactive/source-packet.d.ts +126 -0
  215. package/dist/interactive/source-packet.d.ts.map +1 -0
  216. package/dist/interactive/source-packet.js +482 -0
  217. package/dist/interactive/source-packet.js.map +1 -0
  218. package/dist/learnings/estate.d.ts +16 -0
  219. package/dist/learnings/estate.d.ts.map +1 -0
  220. package/dist/learnings/estate.js +68 -0
  221. package/dist/learnings/estate.js.map +1 -0
  222. package/dist/learnings/extract.d.ts +56 -0
  223. package/dist/learnings/extract.d.ts.map +1 -0
  224. package/dist/learnings/extract.js +252 -0
  225. package/dist/learnings/extract.js.map +1 -0
  226. package/dist/learnings/recall.d.ts +43 -0
  227. package/dist/learnings/recall.d.ts.map +1 -0
  228. package/dist/learnings/recall.js +104 -0
  229. package/dist/learnings/recall.js.map +1 -0
  230. package/dist/learnings/service.d.ts +183 -0
  231. package/dist/learnings/service.d.ts.map +1 -0
  232. package/dist/learnings/service.js +571 -0
  233. package/dist/learnings/service.js.map +1 -0
  234. package/dist/mcp/ask-server.d.ts.map +1 -1
  235. package/dist/mcp/ask-server.js +37 -4
  236. package/dist/mcp/ask-server.js.map +1 -1
  237. package/dist/mcp/broker.d.ts +12 -1
  238. package/dist/mcp/broker.d.ts.map +1 -1
  239. package/dist/mcp/broker.js +24 -5
  240. package/dist/mcp/broker.js.map +1 -1
  241. package/dist/mcp/invoke.d.ts +26 -2
  242. package/dist/mcp/invoke.d.ts.map +1 -1
  243. package/dist/mcp/invoke.js +51 -5
  244. package/dist/mcp/invoke.js.map +1 -1
  245. package/dist/persona/routes.d.ts +92 -0
  246. package/dist/persona/routes.d.ts.map +1 -0
  247. package/dist/persona/routes.js +69 -0
  248. package/dist/persona/routes.js.map +1 -0
  249. package/dist/persona/store.d.ts +104 -0
  250. package/dist/persona/store.d.ts.map +1 -0
  251. package/dist/persona/store.js +176 -0
  252. package/dist/persona/store.js.map +1 -0
  253. package/dist/persona/voice.d.ts +4 -0
  254. package/dist/persona/voice.d.ts.map +1 -0
  255. package/dist/persona/voice.js +58 -0
  256. package/dist/persona/voice.js.map +1 -0
  257. package/dist/projects/routes.d.ts +2 -2
  258. package/dist/setup/known.d.ts +56 -0
  259. package/dist/setup/known.d.ts.map +1 -0
  260. package/dist/setup/known.js +159 -0
  261. package/dist/setup/known.js.map +1 -0
  262. package/dist/setup/routes.d.ts +13 -0
  263. package/dist/setup/routes.d.ts.map +1 -0
  264. package/dist/setup/routes.js +20 -0
  265. package/dist/setup/routes.js.map +1 -0
  266. package/dist/standing-orders/history.d.ts +2 -0
  267. package/dist/standing-orders/history.d.ts.map +1 -1
  268. package/dist/standing-orders/history.js +1 -0
  269. package/dist/standing-orders/history.js.map +1 -1
  270. package/dist/studio/assets/WickedSans-Regular-fb698rcD.woff2 +0 -0
  271. package/dist/studio/assets/WickedSans-SemiBold-CD53n1AC.woff2 +0 -0
  272. package/dist/studio/assets/WickedScript-Regular-CY5juO6I.woff2 +0 -0
  273. package/dist/studio/assets/WickedSerif-Regular-mdPEjaVZ.woff2 +0 -0
  274. package/dist/studio/assets/index-BUZ_-Syk.css +32 -0
  275. package/dist/studio/assets/index-u4O_7N-b.js +986 -0
  276. package/dist/studio/editors/wicked-deck/editor.json +6 -1
  277. package/dist/studio/editors/wicked-deck/icon.svg +1 -0
  278. package/dist/studio/editors/wicked-deck/index.html +1 -1
  279. package/dist/studio/editors/wicked-doc/editor.json +6 -1
  280. package/dist/studio/editors/wicked-doc/icon.svg +1 -0
  281. package/dist/studio/editors/wicked-doc/index.html +1 -1
  282. package/dist/studio/editors/wicked-insights/editor.json +6 -1
  283. package/dist/studio/editors/wicked-insights/icon.svg +1 -0
  284. package/dist/studio/editors/wicked-insights/index.html +1 -1
  285. package/dist/studio/editors/wicked-page/editor.json +6 -1
  286. package/dist/studio/editors/wicked-page/icon.svg +1 -0
  287. package/dist/studio/editors/wicked-page/index.html +1 -1
  288. package/dist/studio/editors/wicked-video/editor.json +6 -1
  289. package/dist/studio/editors/wicked-video/icon.svg +1 -0
  290. package/dist/studio/editors/wicked-video/index.html +1 -1
  291. package/dist/studio/index.html +2 -2
  292. package/dist/studio/testid-inventory.json +5155 -2707
  293. package/dist/voice/dashboard.d.ts +376 -0
  294. package/dist/voice/dashboard.d.ts.map +1 -0
  295. package/dist/voice/dashboard.js +92 -0
  296. package/dist/voice/dashboard.js.map +1 -0
  297. package/dist/voice/inject-input.d.ts +51 -0
  298. package/dist/voice/inject-input.d.ts.map +1 -0
  299. package/dist/voice/inject-input.js +126 -0
  300. package/dist/voice/inject-input.js.map +1 -0
  301. package/dist/voice/routes.d.ts +41 -0
  302. package/dist/voice/routes.d.ts.map +1 -0
  303. package/dist/voice/routes.js +116 -0
  304. package/dist/voice/routes.js.map +1 -0
  305. package/dist/voice/service.d.ts +177 -0
  306. package/dist/voice/service.d.ts.map +1 -0
  307. package/dist/voice/service.js +400 -0
  308. package/dist/voice/service.js.map +1 -0
  309. package/dist/workflow-catalog/build.d.ts +35 -0
  310. package/dist/workflow-catalog/build.d.ts.map +1 -0
  311. package/dist/workflow-catalog/build.js +229 -0
  312. package/dist/workflow-catalog/build.js.map +1 -0
  313. package/dist/workflow-catalog/catalog-data.d.ts +36 -0
  314. package/dist/workflow-catalog/catalog-data.d.ts.map +1 -0
  315. package/dist/workflow-catalog/catalog-data.js +430 -0
  316. package/dist/workflow-catalog/catalog-data.js.map +1 -0
  317. package/dist/working-doc/event-lines.d.ts +76 -0
  318. package/dist/working-doc/event-lines.d.ts.map +1 -0
  319. package/dist/working-doc/event-lines.js +247 -0
  320. package/dist/working-doc/event-lines.js.map +1 -0
  321. package/dist/working-doc/routes.d.ts +1 -1
  322. package/dist/working-doc/routes.d.ts.map +1 -1
  323. package/dist/working-doc/routes.js +2 -1
  324. package/dist/working-doc/routes.js.map +1 -1
  325. package/dist/working-doc/sections.d.ts +3 -1
  326. package/dist/working-doc/sections.d.ts.map +1 -1
  327. package/dist/working-doc/sections.js +4 -2
  328. package/dist/working-doc/sections.js.map +1 -1
  329. package/dist/working-doc/service.d.ts +66 -1
  330. package/dist/working-doc/service.d.ts.map +1 -1
  331. package/dist/working-doc/service.js +298 -22
  332. package/dist/working-doc/service.js.map +1 -1
  333. package/dist/working-doc/store.d.ts +8 -2
  334. package/dist/working-doc/store.d.ts.map +1 -1
  335. package/dist/working-doc/store.js.map +1 -1
  336. package/endpoint-manifest.json +205 -2
  337. package/package.json +5 -4
  338. package/workflows/distill-learnings.json +27 -0
  339. package/workflows/inspect-unit.json +27 -0
  340. package/dist/studio/assets/index-B-f_VEL2.css +0 -32
  341. package/dist/studio/assets/index-DhkxIK-0.js +0 -845
@@ -0,0 +1,870 @@
1
+ /**
2
+ * `apps.json`, the app spec (DES-APPS-001 §4, rev 11): the runtime schema crew checks every tree
3
+ * against, mirroring the wire contract's `AppSpec` (`wicked-crew-api-types`; the drift guard in
4
+ * `tests/wire-contract.test.ts` holds the two equal in both directions).
5
+ *
6
+ * Two layers, both pure:
7
+ * - {@link AppSpecSchema}: the shape and every per-field limit, strict at every level, so a key crew
8
+ * does not know is refused rather than dropped (an agent writes this file: what crew cannot read, it
9
+ * does not accept).
10
+ * - {@link checkSpec}: what a shape cannot say. Ids are unique; every binding names a query, action,
11
+ * filter, form field, row column or page that exists; a viewer-bound parameter is never bound by a
12
+ * component; a link leaves only for a listed host over https; a pie has one series.
13
+ *
14
+ * Both answer findings (`AppCheckFinding`, stage `spec`) with a JSON path into the document, so the
15
+ * staged check, the builder and `apps_check` all name the same place.
16
+ */
17
+ import { z } from 'zod';
18
+ import { MCP_SERVER_NAME_RE } from '../mcp/classify.js';
19
+ /** Page and component ids: unique across the app. */
20
+ export const APP_ELEMENT_ID_RE = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
21
+ /** Query and action ids. A query id is a file name (`queries/<id>.sql`), so lower case only. */
22
+ export const APP_NAMED_ID_RE = /^[a-z][a-z0-9_]{0,63}$/;
23
+ /** Output column and SQL identifier names. */
24
+ const COLUMN_RE = /^[A-Za-z_][A-Za-z0-9_-]{0,127}$/;
25
+ const PARAM_RE = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
26
+ const FIELD_RE = /^[a-z][a-z0-9_]{0,63}$/;
27
+ const SQL_IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;
28
+ const TABLE_RE = /^[A-Za-z_][A-Za-z0-9_]{0,62}\.[A-Za-z_][A-Za-z0-9_]{0,62}$/;
29
+ const TOOL_RE = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
30
+ const COLOR_RE = /^#[0-9a-fA-F]{6}$/;
31
+ const HOST_RE = /^(?=.{1,253}$)[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/;
32
+ /** A JSONPath subset: `$`, then `.name`, `[*]` or `[n]` steps. */
33
+ const JSON_PATH_RE = /^\$(\.[A-Za-z_][A-Za-z0-9_]*|\[\*\]|\[[0-9]{1,6}\])*$/;
34
+ /** `filter.<id>` (`.from`/`.to` on a date range), `form.<field>`, `row.<column>`, `url.<name>`. */
35
+ const BINDING_RE = /^(filter\.[A-Za-z][A-Za-z0-9_-]{0,63}(\.(from|to))?|form\.[a-z][a-z0-9_]{0,63}|row\.[A-Za-z_][A-Za-z0-9_-]{0,127}|url\.[A-Za-z_][A-Za-z0-9_]{0,63})$/;
36
+ /** 256 KB decoded: the base64 payload of at most 262,144 bytes. */
37
+ const IMAGE_MAX_BYTES = 262_144;
38
+ const IMAGE_MAX_BASE64 = Math.ceil(IMAGE_MAX_BYTES / 3) * 4;
39
+ /** The decoded size of a whole base64 payload (`null` when it is not whole quartets). */
40
+ function base64Bytes(payload) {
41
+ if (payload.length % 4 !== 0)
42
+ return null;
43
+ const pad = payload.endsWith('==') ? 2 : payload.endsWith('=') ? 1 : 0;
44
+ return (payload.length / 4) * 3 - pad;
45
+ }
46
+ const IMAGE_RE = /^data:image\/(png|jpeg|gif|webp);base64,[A-Za-z0-9+/]+={0,2}$/;
47
+ const id = z.string().regex(APP_ELEMENT_ID_RE, 'an id is 1-64 of A-Z a-z 0-9 _ -, starting with a letter');
48
+ const namedId = z.string().regex(APP_NAMED_ID_RE, 'a query or action id is 1-64 of a-z 0-9 _, starting with a letter');
49
+ const sourceName = z.string().regex(MCP_SERVER_NAME_RE, 'a source name is 1-63 of a-z 0-9 _ -, starting with a letter or digit');
50
+ const column = z.string().regex(COLUMN_RE, 'a column is 1-128 of A-Z a-z 0-9 _ -, starting with a letter or _');
51
+ const color = z.string().regex(COLOR_RE, 'a colour is #rrggbb');
52
+ const text = (max) => z.string().min(1).max(max);
53
+ const binding = z.custom((v) => typeof v === 'string' && BINDING_RE.test(v), {
54
+ message: 'a binding is filter.<id>, form.<field>, row.<column> or url.<name>',
55
+ });
56
+ const bindings = z.record(z.string().regex(PARAM_RE), binding).refine((r) => Object.keys(r).length <= 30, 'at most 30 bindings');
57
+ const BoxSchema = z
58
+ .object({
59
+ x: z.number().int().min(0).max(11),
60
+ y: z.number().int().min(0).max(999),
61
+ w: z.number().int().min(1).max(12),
62
+ h: z.number().int().min(1).max(50),
63
+ })
64
+ .strict()
65
+ .refine((b) => b.x + b.w <= 12, { message: 'x + w must be at most 12', path: ['w'] });
66
+ const ThemeSchema = z
67
+ .object({
68
+ mode: z.enum(['light', 'dark', 'auto']).optional(),
69
+ accent: color.optional(),
70
+ palette: z.array(color).max(12).optional(),
71
+ density: z.enum(['comfortable', 'compact']).optional(),
72
+ })
73
+ .strict();
74
+ const FormatSchema = z.union([
75
+ z
76
+ .object({
77
+ kind: z.literal('currency'),
78
+ currency: z.string().regex(/^[A-Z]{3}$/, 'a currency is an ISO 4217 code'),
79
+ decimals: z.number().int().min(0).max(10).optional(),
80
+ })
81
+ .strict(),
82
+ z
83
+ .object({
84
+ kind: z.enum(['text', 'number', 'percent', 'date', 'datetime', 'duration']),
85
+ decimals: z.number().int().min(0).max(10).optional(),
86
+ })
87
+ .strict(),
88
+ ]);
89
+ const QueryBindingSchema = z.object({ query: namedId, params: bindings.optional() }).strict();
90
+ const ActionBindingSchema = z.object({ label: text(60), action: namedId, params: bindings.optional() }).strict();
91
+ const OptionSchema = z.object({ value: z.string().max(200), label: z.string().max(200).optional() }).strict();
92
+ const base = {
93
+ id,
94
+ box: BoxSchema,
95
+ title: z.string().max(120).optional(),
96
+ hideOnPhone: z.boolean().optional(),
97
+ };
98
+ const TableSchema = z
99
+ .object({
100
+ ...base,
101
+ type: z.literal('table'),
102
+ data: QueryBindingSchema,
103
+ columns: z
104
+ .array(z.object({ column, label: z.string().max(120).optional(), format: FormatSchema.optional() }).strict())
105
+ .min(1)
106
+ .max(200)
107
+ .optional(),
108
+ rowActions: z.array(ActionBindingSchema).max(4).optional(),
109
+ pageSize: z.number().int().min(5).max(500).optional(),
110
+ })
111
+ .strict();
112
+ const ChartSchema = z
113
+ .object({
114
+ ...base,
115
+ type: z.literal('chart'),
116
+ chart: z.enum(['line', 'bar', 'area', 'pie']),
117
+ data: QueryBindingSchema,
118
+ x: column,
119
+ series: z.array(z.object({ column, label: z.string().max(120).optional(), color: color.optional() }).strict()).min(1).max(12),
120
+ stacked: z.boolean().optional(),
121
+ })
122
+ .strict();
123
+ const KpiSchema = z
124
+ .object({
125
+ ...base,
126
+ type: z.literal('kpi'),
127
+ data: QueryBindingSchema,
128
+ value: column,
129
+ format: FormatSchema.optional(),
130
+ delta: column.optional(),
131
+ caption: z.string().max(120).optional(),
132
+ })
133
+ .strict();
134
+ const TextSchema = z
135
+ .object({ ...base, type: z.literal('text'), text: text(4000), style: z.enum(['heading', 'body', 'caption']).optional() })
136
+ .strict();
137
+ const FilterSchema = z
138
+ .object({
139
+ ...base,
140
+ type: z.literal('filter'),
141
+ filter: z.enum(['select', 'date-range', 'search']),
142
+ label: z.string().max(120).optional(),
143
+ options: z
144
+ .discriminatedUnion('kind', [
145
+ z.object({ kind: z.literal('query'), query: QueryBindingSchema, value: column, label: column.optional() }).strict(),
146
+ z.object({ kind: z.literal('values'), values: z.array(OptionSchema).min(1).max(200) }).strict(),
147
+ ])
148
+ .optional(),
149
+ default: z.union([z.string().max(200), z.object({ lastDays: z.number().int().min(1).max(3660) }).strict()]).optional(),
150
+ })
151
+ .strict();
152
+ const FormSchema = z
153
+ .object({
154
+ ...base,
155
+ type: z.literal('form'),
156
+ fields: z
157
+ .array(z
158
+ .object({
159
+ name: z.string().regex(FIELD_RE, 'a field name is 1-64 of a-z 0-9 _, starting with a letter'),
160
+ label: z.string().max(120).optional(),
161
+ input: z.enum(['text', 'textarea', 'number', 'date', 'checkbox', 'select']),
162
+ required: z.boolean().optional(),
163
+ options: z.array(OptionSchema).min(1).max(200).optional(),
164
+ })
165
+ .strict())
166
+ .min(1)
167
+ .max(30),
168
+ submit: ActionBindingSchema,
169
+ })
170
+ .strict();
171
+ const ButtonSchema = z.object({ ...base, type: z.literal('button'), action: ActionBindingSchema }).strict();
172
+ const ImageSchema = z
173
+ .object({
174
+ ...base,
175
+ type: z.literal('image'),
176
+ src: z
177
+ .string()
178
+ .max(IMAGE_MAX_BASE64 + 32)
179
+ .regex(IMAGE_RE, 'an image is a data:image/(png|jpeg|gif|webp);base64 URI')
180
+ .refine((s) => {
181
+ const n = base64Bytes(s.slice(s.indexOf(',') + 1));
182
+ return n !== null && n <= IMAGE_MAX_BYTES;
183
+ }, 'an image is whole base64 of at most 256 KB'),
184
+ alt: z.string().max(300),
185
+ })
186
+ .strict();
187
+ const LinkSchema = z
188
+ .object({
189
+ ...base,
190
+ type: z.literal('link'),
191
+ label: text(120),
192
+ to: z.discriminatedUnion('kind', [
193
+ z.object({ kind: z.literal('page'), page: id, params: bindings.optional() }).strict(),
194
+ z.object({ kind: z.literal('href'), href: z.string().max(2048) }).strict(),
195
+ ]),
196
+ })
197
+ .strict();
198
+ const ComponentSchema = z.discriminatedUnion('type', [
199
+ TableSchema,
200
+ ChartSchema,
201
+ KpiSchema,
202
+ TextSchema,
203
+ FilterSchema,
204
+ FormSchema,
205
+ ButtonSchema,
206
+ ImageSchema,
207
+ LinkSchema,
208
+ ]);
209
+ const PageSchema = z.object({ id, title: text(80), components: z.array(ComponentSchema).max(100) }).strict();
210
+ const scalar = z.union([z.string().max(1000), z.number().finite(), z.boolean()]);
211
+ const ParamSchema = z
212
+ .object({
213
+ type: z.enum(['string', 'number', 'integer', 'boolean', 'array']),
214
+ items: z.object({ type: z.enum(['string', 'number', 'integer']) }).strict().optional(),
215
+ format: z.enum(['date', 'date-time', 'uuid', 'email']).optional(),
216
+ enum: z.array(z.union([z.string().max(200), z.number().finite()])).min(1).max(200).optional(),
217
+ minimum: z.number().finite().optional(),
218
+ maximum: z.number().finite().optional(),
219
+ minLength: z.number().int().min(0).max(100_000).optional(),
220
+ maxLength: z.number().int().min(0).max(100_000).optional(),
221
+ default: scalar.optional(),
222
+ description: z.string().max(500).optional(),
223
+ from: z.enum(['viewer.email', 'viewer.name', 'viewer.id', 'viewer.groups']).optional(),
224
+ })
225
+ .strict();
226
+ const ParamsSchema = z
227
+ .object({
228
+ type: z.literal('object'),
229
+ properties: z.record(z.string().regex(PARAM_RE, 'a parameter name is 1-64 of A-Z a-z 0-9 _'), ParamSchema),
230
+ required: z.array(z.string()).max(30).optional(),
231
+ })
232
+ .strict()
233
+ .refine((p) => Object.keys(p.properties).length <= 30, 'at most 30 parameters');
234
+ const RefreshSchema = z.union([
235
+ z.object({ mode: z.literal('load') }).strict(),
236
+ z.object({ mode: z.literal('every'), seconds: z.number().int().min(15).max(86_400) }).strict(),
237
+ z.object({ mode: z.literal('events') }).strict(),
238
+ ]);
239
+ const QuerySchema = z
240
+ .object({
241
+ id: namedId,
242
+ source: sourceName,
243
+ kind: z.enum(['sql', 'graphql', 'rest', 'mcp', 'crew']),
244
+ description: z.string().max(500).optional(),
245
+ params: ParamsSchema.optional(),
246
+ output: z.array(z.object({ name: column, type: z.enum(['string', 'number', 'integer', 'boolean', 'date', 'datetime']), nullable: z.boolean().optional() }).strict()).min(1).max(200),
247
+ rowPath: z.string().max(256).regex(JSON_PATH_RE, 'a row path is $ then .name, [*] or [n] steps').optional(),
248
+ morePath: z.string().max(256).regex(JSON_PATH_RE, 'a more path is $ then .name, [*] or [n] steps').optional(),
249
+ refresh: RefreshSchema.optional(),
250
+ public: z.boolean().optional(),
251
+ })
252
+ .strict();
253
+ const ParamValueSchema = z.object({ kind: z.literal('param'), name: z.string().regex(PARAM_RE) }).strict();
254
+ const ValueSchema = z.discriminatedUnion('kind', [
255
+ ParamValueSchema,
256
+ z.object({ kind: z.literal('literal'), value: z.union([z.string().max(1000), z.number().finite(), z.boolean(), z.null()]) }).strict(),
257
+ z.object({ kind: z.literal('viewer'), claim: z.enum(['email', 'name', 'id']) }).strict(),
258
+ z.object({ kind: z.literal('crew'), value: z.enum(['now', 'uuid']) }).strict(),
259
+ ]);
260
+ const ApiActionSchema = z
261
+ .object({
262
+ id: namedId,
263
+ kind: z.literal('api'),
264
+ source: sourceName,
265
+ call: z.discriminatedUnion('kind', [
266
+ z.object({ kind: z.literal('tool'), tool: z.string().regex(TOOL_RE, 'a tool name is 1-128 of A-Z a-z 0-9 _ . -') }).strict(),
267
+ z.object({
268
+ kind: z.literal('mutation'),
269
+ mutation: text(16_384).refine((m) => Buffer.byteLength(m, 'utf8') <= 16_384, 'a mutation is at most 16 KB of UTF-8'),
270
+ }).strict(),
271
+ ]),
272
+ args: z.record(z.string().min(1).max(128), ValueSchema).refine((r) => Object.keys(r).length <= 50, 'at most 50 arguments'),
273
+ params: ParamsSchema.optional(),
274
+ description: z.string().max(500).optional(),
275
+ })
276
+ .strict();
277
+ const RowWriteActionSchema = z
278
+ .object({
279
+ id: namedId,
280
+ kind: z.literal('row-write'),
281
+ source: sourceName,
282
+ table: z.string().regex(TABLE_RE, 'a table is schema.table'),
283
+ op: z.enum(['insert', 'update', 'delete']),
284
+ set: z
285
+ .record(z.string().regex(SQL_IDENT_RE, 'a column is a plain SQL identifier'), ValueSchema)
286
+ .refine((r) => Object.keys(r).length <= 200, 'at most 200 columns')
287
+ .optional(),
288
+ key: z
289
+ .record(z.string().regex(SQL_IDENT_RE, 'a key column is a plain SQL identifier'), ParamValueSchema)
290
+ .refine((k) => Object.keys(k).length >= 1 && Object.keys(k).length <= 16, 'a key names 1-16 columns'),
291
+ params: ParamsSchema.optional(),
292
+ description: z.string().max(500).optional(),
293
+ })
294
+ .strict();
295
+ const ActionSchema = z.discriminatedUnion('kind', [ApiActionSchema, RowWriteActionSchema]);
296
+ /** The whole `apps.json`. */
297
+ export const AppSpecSchema = z
298
+ .object({
299
+ schema: z.literal('wicked.apps/v1'),
300
+ name: text(120),
301
+ description: z.string().max(2000).optional(),
302
+ theme: ThemeSchema.optional(),
303
+ pages: z.array(PageSchema).min(1).max(20),
304
+ queries: z.array(QuerySchema).max(100),
305
+ actions: z.array(ActionSchema).max(50),
306
+ linkHosts: z.array(z.string().regex(HOST_RE, 'a link host is a lower-case host name, no scheme or port')).max(50),
307
+ })
308
+ .strict();
309
+ /** The largest `apps.json` crew reads (the spec, images included). */
310
+ export const APP_SPEC_MAX_BYTES = 2 * 1024 * 1024;
311
+ /**
312
+ * Bounds on the JSON's structure, checked BEFORE the schema sees it: zod records an over-long array and
313
+ * still parses every element, so a 2 MB document of a million empty objects would cost a million
314
+ * findings. Every legal spec is inside these: its longest list is 200, its deepest object 7 levels down,
315
+ * and the node budget is above any legal spec's node count (it also bounds an edit body, which may be
316
+ * larger than 2 MB, before the size check). Published with the contract (`AppSpec`).
317
+ */
318
+ export const APP_SPEC_LIMITS = { depth: 16, arrayLength: 200, objectKeys: 256, nodes: 1_000_000 };
319
+ /**
320
+ * Each list's own limit, by its path with indices and record keys as `*` (codex r3): the walk refuses a
321
+ * list longer than the schema allows before zod parses its elements, so no list can make zod expand
322
+ * more items than a legal spec holds. A list not named here is held to {@link APP_SPEC_LIMITS.arrayLength},
323
+ * the longest any legal list is.
324
+ */
325
+ const LIST_LIMITS = new Map([
326
+ ['pages', 20],
327
+ ['queries', 100],
328
+ ['actions', 50],
329
+ ['linkHosts', 50],
330
+ ['theme.palette', 12],
331
+ ['pages.*.components', 100],
332
+ ['pages.*.components.*.rowActions', 4],
333
+ ['pages.*.components.*.series', 12],
334
+ ['pages.*.components.*.fields', 30],
335
+ ['queries.*.params.required', 30],
336
+ ['actions.*.params.required', 30],
337
+ ]);
338
+ /**
339
+ * Each record's own key limit, by the same normalised path (codex r5): zod checks a record's size only
340
+ * after parsing every value, so the walk holds it first. Every binding map on a component (`…params`) is
341
+ * held to 30; any other object to {@link APP_SPEC_LIMITS.objectKeys}. With these and the list limits, one
342
+ * component, query or action can yield a few thousand diagnostics at most.
343
+ */
344
+ const RECORD_LIMITS = new Map([
345
+ ['queries.*.params.properties', 30],
346
+ ['actions.*.params.properties', 30],
347
+ ['actions.*.args', 50],
348
+ ['actions.*.set', 200],
349
+ ['actions.*.key', 16],
350
+ ]);
351
+ const BINDING_MAP_PATH = /^pages\.\*\.components\.\*\.(.+\.)?params$/;
352
+ /** A walk path with list indices and parameter names as `*`. */
353
+ const normalise = (path) => path.map((p) => (typeof p === 'number' ? '*' : p)).join('.').replace(/\.properties\.[^.]+/g, '.properties.*');
354
+ const own = (o, k) => Object.hasOwn(o, k);
355
+ function pathOf(path) {
356
+ let out = '$';
357
+ for (const p of path)
358
+ out += typeof p === 'number' ? `[${p}]` : /^[A-Za-z_][A-Za-z0-9_]*$/.test(p) ? `.${p}` : `[${JSON.stringify(p)}]`;
359
+ return out;
360
+ }
361
+ function finding(code, path, message) {
362
+ return { stage: 'spec', code, path: pathOf(path), message };
363
+ }
364
+ /**
365
+ * The structure walk: depth, list length, key count and node count within {@link APP_SPEC_LIMITS}, and no
366
+ * `__proto__` key (JSON makes it an own key, but a parsed record drops it, so crew would check a document
367
+ * other than the one it was given). Iterative: depth is what it bounds.
368
+ */
369
+ export function structureFinding(value) {
370
+ const stack = [{ v: value, path: [] }];
371
+ let nodes = 0;
372
+ while (stack.length > 0) {
373
+ const { v, path } = stack.pop();
374
+ if (++nodes > APP_SPEC_LIMITS.nodes)
375
+ return finding('too_complex', [], `apps.json holds more than ${APP_SPEC_LIMITS.nodes} values`);
376
+ if (v === null || typeof v !== 'object')
377
+ continue;
378
+ if (path.length >= APP_SPEC_LIMITS.depth)
379
+ return finding('too_complex', path, `apps.json nests deeper than ${APP_SPEC_LIMITS.depth}`);
380
+ if (Array.isArray(v)) {
381
+ const limit = LIST_LIMITS.get(normalise(path)) ?? APP_SPEC_LIMITS.arrayLength;
382
+ if (v.length > limit)
383
+ return finding('too_complex', path, `a list here holds at most ${limit} items`);
384
+ v.forEach((x, i) => stack.push({ v: x, path: [...path, i] }));
385
+ continue;
386
+ }
387
+ const keys = Object.keys(v);
388
+ const norm = normalise(path);
389
+ const keyLimit = RECORD_LIMITS.get(norm) ?? (BINDING_MAP_PATH.test(norm) ? 30 : APP_SPEC_LIMITS.objectKeys);
390
+ if (keys.length > keyLimit)
391
+ return finding('too_complex', path, `an object here holds at most ${keyLimit} keys`);
392
+ for (const k of keys) {
393
+ if (k === '__proto__')
394
+ return finding('bad_key', [...path, k], '__proto__ is not a key apps.json may use');
395
+ stack.push({ v: v[k], path: [...path, k] });
396
+ }
397
+ }
398
+ return null;
399
+ }
400
+ /** The most findings a check answers; the element-wise pass stops once it has this many. */
401
+ const MAX_FINDINGS = 50;
402
+ /**
403
+ * The document with its three big lists (components, queries, actions) unparsed: cheap, and what the
404
+ * element-wise pass walks.
405
+ */
406
+ const SkeletonSchema = AppSpecSchema.extend({
407
+ pages: z.array(PageSchema.extend({ components: z.array(z.unknown()).max(100) })).min(1).max(20),
408
+ queries: z.array(z.unknown()).max(100),
409
+ actions: z.array(z.unknown()).max(50),
410
+ });
411
+ /** (tests) What the last {@link parseSpec} parsed: big-list elements, and whether the whole schema ran. */
412
+ export const parseWork = { elements: 0, whole: false };
413
+ /**
414
+ * Schema findings with a budget (codex r4): the skeleton first, then each component, query and action on
415
+ * its own, stopping once {@link MAX_FINDINGS} findings are in hand, so a malformed document costs at most
416
+ * the elements up to that point, never zod's diagnostics for every element of every list.
417
+ */
418
+ function budgetedFindings(value) {
419
+ const out = [];
420
+ const add = (issues, prefix) => {
421
+ for (const i of issues) {
422
+ if (out.length >= MAX_FINDINGS)
423
+ return;
424
+ out.push(finding('schema', [...prefix, ...i.path], i.message));
425
+ }
426
+ };
427
+ const skeleton = SkeletonSchema.safeParse(value);
428
+ if (!skeleton.success)
429
+ add(skeleton.error.issues, []);
430
+ // A root that is not an object (null, a list, a scalar) has no pieces to walk (codex r5).
431
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
432
+ return out;
433
+ const doc = value;
434
+ const lists = [];
435
+ if (Array.isArray(doc.pages)) {
436
+ doc.pages.forEach((page, pi) => {
437
+ const comps = page?.components;
438
+ if (Array.isArray(comps))
439
+ lists.push([comps, ComponentSchema, (i) => ['pages', pi, 'components', i]]);
440
+ });
441
+ }
442
+ if (Array.isArray(doc.queries))
443
+ lists.push([doc.queries, QuerySchema, (i) => ['queries', i]]);
444
+ if (Array.isArray(doc.actions))
445
+ lists.push([doc.actions, ActionSchema, (i) => ['actions', i]]);
446
+ for (const [items, schema, at] of lists) {
447
+ for (const [i, item] of items.entries()) {
448
+ if (out.length >= MAX_FINDINGS)
449
+ return out;
450
+ parseWork.elements++;
451
+ const r = schema.safeParse(item);
452
+ if (!r.success)
453
+ add(r.error.issues, at(i));
454
+ }
455
+ }
456
+ return out;
457
+ }
458
+ /** The shape alone: the parsed spec, or the findings (at most 50). */
459
+ export function parseSpec(value) {
460
+ parseWork.elements = 0;
461
+ parseWork.whole = false;
462
+ const structure = structureFinding(value);
463
+ if (structure !== null)
464
+ return { spec: null, findings: [structure] };
465
+ const findings = budgetedFindings(value);
466
+ if (findings.length > 0)
467
+ return { spec: null, findings };
468
+ // Every piece passed on its own; the whole schema now types the document (and has nothing to say
469
+ // beyond what the pieces said: a valid document costs zod no diagnostics).
470
+ parseWork.whole = true;
471
+ const parsed = AppSpecSchema.safeParse(value);
472
+ if (parsed.success)
473
+ return { spec: parsed.data, findings: [] };
474
+ return { spec: null, findings: parsed.error.issues.slice(0, MAX_FINDINGS).map((i) => finding('schema', i.path, i.message)) };
475
+ }
476
+ /** `apps.json` bytes → the parsed spec or findings: size, JSON, then {@link parseSpec} and {@link checkSpec}. */
477
+ export function readSpec(bytes) {
478
+ const size = typeof bytes === 'string' ? Buffer.byteLength(bytes) : bytes.length;
479
+ if (size > APP_SPEC_MAX_BYTES)
480
+ return { spec: null, findings: [finding('too_large', [], `apps.json is ${size} bytes; the limit is ${APP_SPEC_MAX_BYTES}`)] };
481
+ let json;
482
+ try {
483
+ json = JSON.parse(typeof bytes === 'string' ? bytes : bytes.toString('utf8'));
484
+ }
485
+ catch (err) {
486
+ return { spec: null, findings: [finding('not_json', [], `apps.json is not JSON: ${err instanceof Error ? err.message : String(err)}`)] };
487
+ }
488
+ const parsed = parseSpec(json);
489
+ if (parsed.spec === null)
490
+ return parsed;
491
+ const findings = checkSpec(parsed.spec);
492
+ return findings.length === 0 ? parsed : { spec: null, findings };
493
+ }
494
+ /** A real calendar date `YYYY-MM-DD`, years 0001-9999 (proleptic Gregorian). */
495
+ function isDate(v) {
496
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(v);
497
+ if (m === null)
498
+ return false;
499
+ const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];
500
+ if (y < 1 || mo < 1 || mo > 12 || d < 1)
501
+ return false;
502
+ const leap = (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;
503
+ const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][mo - 1];
504
+ return d <= days;
505
+ }
506
+ /**
507
+ * The `date-time` crew accepts: RFC 3339 with seconds, `YYYY-MM-DDTHH:MM:SS[.fraction](Z|±HH:MM)`, a real
508
+ * date, seconds 00-59 (no leap second: no source stores one), any number of fraction digits.
509
+ */
510
+ function isDateTime(v) {
511
+ const m = /^(\d{4}-\d{2}-\d{2})[Tt](\d{2}):(\d{2}):(\d{2})(\.\d+)?([Zz]|[+-](\d{2}):(\d{2}))$/.exec(v);
512
+ if (m === null || !isDate(m[1]))
513
+ return false;
514
+ const [h, mi, se] = [Number(m[2]), Number(m[3]), Number(m[4])];
515
+ if (h > 23 || mi > 59 || se > 59)
516
+ return false;
517
+ return m[7] === undefined || (Number(m[7]) <= 23 && Number(m[8]) <= 59);
518
+ }
519
+ /** A string's length as JSON Schema counts it: Unicode code points, not UTF-16 units. */
520
+ const codePoints = (v) => Array.from(v).length;
521
+ const FORMAT_OK = {
522
+ date: isDate,
523
+ 'date-time': isDateTime,
524
+ uuid: (v) => /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(v),
525
+ email: (v) => /^[^\s@]{1,64}@[^\s@]{1,255}$/.test(v),
526
+ };
527
+ /**
528
+ * Why `value` does not satisfy the SCALAR declaration `decl`, or `null` when it does: type (integer
529
+ * included), enum, numeric and length bounds, format. An `array` parameter is not a scalar (its values
530
+ * are judged item by item by whoever validates a request).
531
+ */
532
+ export function paramValueProblem(decl, value) {
533
+ switch (decl.type) {
534
+ case 'string':
535
+ if (typeof value !== 'string')
536
+ return 'is not a string';
537
+ if (decl.minLength !== undefined && codePoints(value) < decl.minLength)
538
+ return `is shorter than ${decl.minLength}`;
539
+ if (decl.maxLength !== undefined && codePoints(value) > decl.maxLength)
540
+ return `is longer than ${decl.maxLength}`;
541
+ if (decl.format !== undefined && !FORMAT_OK[decl.format](value))
542
+ return `is not a ${decl.format}`;
543
+ break;
544
+ case 'number':
545
+ case 'integer':
546
+ if (typeof value !== 'number' || !Number.isFinite(value))
547
+ return 'is not a number';
548
+ if (decl.type === 'integer' && !Number.isSafeInteger(value))
549
+ return 'is not an integer';
550
+ if (decl.minimum !== undefined && value < decl.minimum)
551
+ return `is below ${decl.minimum}`;
552
+ if (decl.maximum !== undefined && value > decl.maximum)
553
+ return `is above ${decl.maximum}`;
554
+ break;
555
+ case 'boolean':
556
+ if (typeof value !== 'boolean')
557
+ return 'is not a boolean';
558
+ break;
559
+ default:
560
+ return 'is not a scalar';
561
+ }
562
+ if (decl.enum !== undefined && !decl.enum.some((e) => e === value))
563
+ return 'is not one of its enum values';
564
+ return null;
565
+ }
566
+ /**
567
+ * What the schema cannot say (DES §6.2 "Spec schema, bindings exist, link hosts listed"). Empty when the
568
+ * spec is consistent. Every name lookup is an OWN-property lookup: `constructor` or `toString` is a name
569
+ * like any other, never something an object inherits.
570
+ */
571
+ export function checkSpec(spec) {
572
+ const out = [];
573
+ const push = (code, path, message) => {
574
+ if (out.length < 50)
575
+ out.push(finding(code, path, message));
576
+ };
577
+ const queries = new Map(spec.queries.map((q) => [q.id, q]));
578
+ const actions = new Map(spec.actions.map((a) => [a.id, a]));
579
+ const pages = new Set(spec.pages.map((p) => p.id));
580
+ const hosts = new Set(spec.linkHosts);
581
+ const dup = (ids, what, path) => {
582
+ const seen = new Set();
583
+ ids.forEach((v, i) => {
584
+ if (seen.has(v))
585
+ push('duplicate_id', path(i), `${what} id ${JSON.stringify(v)} is used twice`);
586
+ seen.add(v);
587
+ });
588
+ };
589
+ dup(spec.queries.map((q) => q.id), 'query', (i) => ['queries', i, 'id']);
590
+ dup(spec.actions.map((a) => a.id), 'action', (i) => ['actions', i, 'id']);
591
+ const elementIds = [];
592
+ const elementPaths = [];
593
+ spec.pages.forEach((p, pi) => {
594
+ elementIds.push(p.id);
595
+ elementPaths.push(['pages', pi, 'id']);
596
+ p.components.forEach((c, ci) => {
597
+ elementIds.push(c.id);
598
+ elementPaths.push(['pages', pi, 'components', ci, 'id']);
599
+ });
600
+ });
601
+ dup(elementIds, 'page or component', (i) => elementPaths[i]);
602
+ // Parameters: `required` names declared ones; `from` fits the type; `items` only on arrays; bounds in
603
+ // order; a default and every enum value satisfy the declaration.
604
+ const checkParams = (params, path) => {
605
+ if (params === undefined)
606
+ return;
607
+ const required = params.required ?? [];
608
+ for (const r of required) {
609
+ if (!own(params.properties, r))
610
+ push('unknown_param', [...path, 'required'], `required parameter ${JSON.stringify(r)} is not declared`);
611
+ }
612
+ if (new Set(required).size !== required.length)
613
+ push('bad_param', [...path, 'required'], 'a parameter is listed as required twice');
614
+ for (const [name, p] of Object.entries(params.properties)) {
615
+ const at = [...path, 'properties', name];
616
+ if ((p.type === 'array') !== (p.items !== undefined))
617
+ push('bad_param', at, 'an array parameter declares items, and only an array does');
618
+ if (p.from === 'viewer.groups' && !(p.type === 'array' && p.items?.type === 'string'))
619
+ push('bad_param', at, 'viewer.groups is an array of strings');
620
+ if (p.from !== undefined && p.from !== 'viewer.groups' && p.type !== 'string')
621
+ push('bad_param', at, `${p.from} is a string`);
622
+ if (p.from !== undefined && p.default !== undefined)
623
+ push('bad_param', at, 'a viewer-bound parameter has no default');
624
+ if (p.minimum !== undefined && p.maximum !== undefined && p.minimum > p.maximum)
625
+ push('bad_param', at, 'minimum is above maximum');
626
+ if (p.minLength !== undefined && p.maxLength !== undefined && p.minLength > p.maxLength)
627
+ push('bad_param', at, 'minLength is above maxLength');
628
+ // Constraints apply to their own type only.
629
+ const isString = p.type === 'string';
630
+ const isNumber = p.type === 'number' || p.type === 'integer';
631
+ if (!isString && (p.format !== undefined || p.minLength !== undefined || p.maxLength !== undefined))
632
+ push('bad_param', at, 'format, minLength and maxLength apply to a string');
633
+ if (!isNumber && (p.minimum !== undefined || p.maximum !== undefined))
634
+ push('bad_param', at, 'minimum and maximum apply to a number');
635
+ if (p.type === 'array' && p.enum !== undefined)
636
+ push('bad_param', [...at, 'enum'], 'an array has no enum');
637
+ if (p.type === 'array' && p.default !== undefined)
638
+ push('bad_param', [...at, 'default'], 'an array has no default');
639
+ if (p.type !== 'array') {
640
+ // Every enum value satisfies the rest of the declaration; the default satisfies all of it, so a
641
+ // default must also be one of the enum values.
642
+ const { enum: values, default: dflt, description: _d, from: _f, items: _i, ...decl } = p;
643
+ void _d;
644
+ void _f;
645
+ void _i;
646
+ for (const [i, e] of (values ?? []).entries()) {
647
+ const problem = paramValueProblem(decl, e);
648
+ if (problem !== null)
649
+ push('bad_param', [...at, 'enum', i], `enum value ${JSON.stringify(e)} ${problem}`);
650
+ }
651
+ if (dflt !== undefined) {
652
+ const problem = paramValueProblem(p, dflt);
653
+ if (problem !== null)
654
+ push('bad_param', [...at, 'default'], `the default ${problem}`);
655
+ }
656
+ }
657
+ }
658
+ };
659
+ /** A required parameter nobody fills: no `from`, no default. */
660
+ const mustBind = (params) => [...new Set(params?.required ?? [])].filter((r) => own(params.properties, r) && params.properties[r].from === undefined && params.properties[r].default === undefined);
661
+ spec.queries.forEach((q, i) => {
662
+ checkParams(q.params, ['queries', i, 'params']);
663
+ if (q.refresh?.mode === 'events' && q.source !== 'crew')
664
+ push('bad_refresh', ['queries', i, 'refresh'], 'refresh on events is for the built-in crew source only');
665
+ if (q.kind === 'crew' && q.source !== 'crew')
666
+ push('bad_source', ['queries', i, 'source'], 'a crew query reads the built-in crew source');
667
+ if (q.kind !== 'crew' && q.source === 'crew')
668
+ push('bad_source', ['queries', i, 'kind'], 'the built-in crew source is read by a crew query');
669
+ if (q.kind === 'sql' && (q.rowPath !== undefined || q.morePath !== undefined))
670
+ push('bad_path', ['queries', i], 'a SQL query has no row or more path');
671
+ dup(q.output.map((c) => c.name), 'output column', (ci) => ['queries', i, 'output', ci, 'name']);
672
+ });
673
+ spec.actions.forEach((a, i) => {
674
+ const path = ['actions', i];
675
+ checkParams(a.params, [...path, 'params']);
676
+ const declared = a.params?.properties ?? {};
677
+ const values = [];
678
+ if (a.kind === 'api') {
679
+ for (const [k, v] of Object.entries(a.args))
680
+ values.push([[...path, 'args', k], v]);
681
+ }
682
+ else {
683
+ for (const [k, v] of Object.entries(a.set ?? {}))
684
+ values.push([[...path, 'set', k], v]);
685
+ for (const [k, v] of Object.entries(a.key))
686
+ values.push([[...path, 'key', k], v]);
687
+ if (a.op === 'delete' && a.set !== undefined)
688
+ push('bad_row_write', [...path, 'set'], 'a delete sets no column');
689
+ if (a.op !== 'delete' && (a.set === undefined || Object.keys(a.set).length === 0))
690
+ push('bad_row_write', [...path, 'set'], `an ${a.op} sets at least one column`);
691
+ for (const k of Object.keys(a.set ?? {})) {
692
+ if (a.op === 'update' && own(a.key, k))
693
+ push('bad_row_write', [...path, 'set', k], 'an update never changes a primary-key column');
694
+ }
695
+ // The row's identity always comes from the request: every key parameter is required (codex r2).
696
+ const required = new Set(a.params?.required ?? []);
697
+ for (const [k, v] of Object.entries(a.key)) {
698
+ if (own(declared, v.name) && !required.has(v.name))
699
+ push('bad_row_write', [...path, 'key', k], `key parameter ${JSON.stringify(v.name)} must be required`);
700
+ // An insert sets each key column from its own key parameter, never a second value (DES §7.4).
701
+ if (a.op === 'insert') {
702
+ const set = a.set !== undefined && own(a.set, k) ? a.set[k] : undefined;
703
+ if (set === undefined || set.kind !== 'param' || set.name !== v.name) {
704
+ push('bad_row_write', [...path, 'set', k], `an insert sets key column ${k} from its key parameter ${JSON.stringify(v.name)}`);
705
+ }
706
+ }
707
+ }
708
+ }
709
+ for (const [at, v] of values) {
710
+ if (v.kind === 'param' && !own(declared, v.name))
711
+ push('unknown_param', at, `parameter ${JSON.stringify(v.name)} is not declared`);
712
+ }
713
+ });
714
+ // Bindings.
715
+ const sites = [];
716
+ spec.pages.forEach((page, pi) => {
717
+ const filters = new Map();
718
+ for (const c of page.components)
719
+ if (c.type === 'filter')
720
+ filters.set(c.id, c.filter);
721
+ page.components.forEach((c, ci) => {
722
+ const at = ['pages', pi, 'components', ci];
723
+ const dataQuery = 'data' in c ? c.data.query : null;
724
+ if ('data' in c)
725
+ sites.push({ path: [...at, 'data'], target: { query: c.data.query }, params: c.data.params ?? {}, rowQuery: null, formFields: null });
726
+ if (c.type === 'table') {
727
+ (c.rowActions ?? []).forEach((ra, ri) => sites.push({ path: [...at, 'rowActions', ri], target: { action: ra.action }, params: ra.params ?? {}, rowQuery: c.data.query, formFields: null }));
728
+ }
729
+ if (c.type === 'filter' && c.options?.kind === 'query') {
730
+ sites.push({ path: [...at, 'options', 'query'], target: { query: c.options.query.query }, params: c.options.query.params ?? {}, rowQuery: null, formFields: null });
731
+ const q = queries.get(c.options.query.query);
732
+ if (q !== undefined) {
733
+ for (const [key, col] of [['value', c.options.value], ['label', c.options.label]]) {
734
+ if (col !== undefined && !q.output.some((o) => o.name === col))
735
+ push('unknown_column', [...at, 'options', key], `query ${q.id} declares no output column ${JSON.stringify(col)}`);
736
+ }
737
+ }
738
+ }
739
+ if (c.type === 'filter' && c.options !== undefined && c.filter !== 'select')
740
+ push('bad_filter', [...at, 'options'], 'only a select filter has options');
741
+ if (c.type === 'filter' && c.default !== undefined && (typeof c.default === 'object') !== (c.filter === 'date-range')) {
742
+ push('bad_filter', [...at, 'default'], c.filter === 'date-range' ? 'a date range defaults to { lastDays }' : 'only a date range defaults to { lastDays }');
743
+ }
744
+ if (c.type === 'form') {
745
+ dup(c.fields.map((f) => f.name), 'form field', (fi) => [...at, 'fields', fi, 'name']);
746
+ c.fields.forEach((f, fi) => {
747
+ if ((f.input === 'select') !== (f.options !== undefined))
748
+ push('bad_field', [...at, 'fields', fi], 'a select field has options, and only a select field does');
749
+ });
750
+ sites.push({ path: [...at, 'submit'], target: { action: c.submit.action }, params: c.submit.params ?? {}, rowQuery: null, formFields: new Set(c.fields.map((f) => f.name)) });
751
+ }
752
+ if (c.type === 'button')
753
+ sites.push({ path: [...at, 'action'], target: { action: c.action.action }, params: c.action.params ?? {}, rowQuery: null, formFields: null });
754
+ if (c.type === 'link') {
755
+ if (c.to.kind === 'page')
756
+ sites.push({ path: [...at, 'to'], target: { page: c.to.page }, params: c.to.params ?? {}, rowQuery: null, formFields: null });
757
+ else {
758
+ let url = null;
759
+ try {
760
+ url = new URL(c.to.href);
761
+ }
762
+ catch {
763
+ url = null;
764
+ }
765
+ if (url === null || url.protocol !== 'https:' || url.username !== '' || url.password !== '' || url.port !== '') {
766
+ push('bad_link', [...at, 'to', 'href'], 'a link leaves over https, with no credentials or port');
767
+ }
768
+ else if (!hosts.has(url.hostname)) {
769
+ push('unlisted_host', [...at, 'to', 'href'], `host ${url.hostname} is not in linkHosts`);
770
+ }
771
+ }
772
+ }
773
+ // A component's columns are columns of its own query.
774
+ const q = dataQuery === null ? undefined : queries.get(dataQuery);
775
+ if (q !== undefined) {
776
+ const cols = new Set(q.output.map((o) => o.name));
777
+ const need = [];
778
+ if (c.type === 'table')
779
+ (c.columns ?? []).forEach((tc, i) => need.push([tc.column, [...at, 'columns', i, 'column']]));
780
+ if (c.type === 'chart') {
781
+ need.push([c.x, [...at, 'x']]);
782
+ c.series.forEach((s, i) => need.push([s.column, [...at, 'series', i, 'column']]));
783
+ }
784
+ if (c.type === 'kpi') {
785
+ need.push([c.value, [...at, 'value']]);
786
+ if (c.delta !== undefined)
787
+ need.push([c.delta, [...at, 'delta']]);
788
+ }
789
+ for (const [col, p] of need)
790
+ if (!cols.has(col))
791
+ push('unknown_column', p, `query ${q.id} declares no output column ${JSON.stringify(col)}`);
792
+ }
793
+ if (c.type === 'chart' && c.chart === 'pie' && c.series.length !== 1)
794
+ push('bad_chart', [...at, 'series'], 'a pie has exactly one series');
795
+ });
796
+ // Each binding on this page: the target exists, every parameter it binds is declared (an OWN key) and
797
+ // not viewer-bound, every required parameter nobody else fills is bound, and the bound value's source
798
+ // exists where it is bound.
799
+ for (const site of sites.splice(0)) {
800
+ let params;
801
+ if ('query' in site.target) {
802
+ const q = queries.get(site.target.query);
803
+ if (q === undefined) {
804
+ push('unknown_query', [...site.path, 'query'], `no query ${JSON.stringify(site.target.query)}`);
805
+ continue;
806
+ }
807
+ params = q.params;
808
+ }
809
+ else if ('action' in site.target) {
810
+ const a = actions.get(site.target.action);
811
+ if (a === undefined) {
812
+ push('unknown_action', [...site.path, 'action'], `no action ${JSON.stringify(site.target.action)}`);
813
+ continue;
814
+ }
815
+ params = a.params;
816
+ }
817
+ else {
818
+ if (!pages.has(site.target.page))
819
+ push('unknown_page', [...site.path, 'page'], `no page ${JSON.stringify(site.target.page)}`);
820
+ params = undefined;
821
+ }
822
+ if (!('page' in site.target)) {
823
+ for (const r of mustBind(params)) {
824
+ if (!own(site.params, r))
825
+ push('unbound_param', [...site.path, 'params'], `required parameter ${JSON.stringify(r)} has no binding, default or viewer claim`);
826
+ }
827
+ }
828
+ for (const [name, ref] of Object.entries(site.params)) {
829
+ const at = [...site.path, 'params', name];
830
+ if (!('page' in site.target)) {
831
+ const decl = params !== undefined && own(params.properties, name) ? params.properties[name] : undefined;
832
+ if (decl === undefined) {
833
+ push('unknown_param', at, `parameter ${JSON.stringify(name)} is not declared`);
834
+ continue;
835
+ }
836
+ if (decl.from !== undefined) {
837
+ push('viewer_bound', at, `parameter ${JSON.stringify(name)} is filled from ${decl.from}; no component binds it`);
838
+ continue;
839
+ }
840
+ }
841
+ const dot = ref.indexOf('.');
842
+ const kind = ref.slice(0, dot);
843
+ const rest = ref.slice(dot + 1);
844
+ if (kind === 'filter') {
845
+ const [fid, end] = rest.split('.');
846
+ const f = filters.get(fid);
847
+ if (f === undefined)
848
+ push('unknown_filter', at, `no filter ${JSON.stringify(fid)} on this page`);
849
+ else if ((f === 'date-range') !== (end !== undefined))
850
+ push('bad_binding', at, f === 'date-range' ? 'a date range binds .from or .to' : 'only a date range binds .from or .to');
851
+ }
852
+ else if (kind === 'form') {
853
+ if (site.formFields === null)
854
+ push('bad_binding', at, 'form.<field> binds only on a form submit');
855
+ else if (!site.formFields.has(rest))
856
+ push('unknown_field', at, `the form has no field ${JSON.stringify(rest)}`);
857
+ }
858
+ else if (kind === 'row') {
859
+ const rq = site.rowQuery === null ? undefined : queries.get(site.rowQuery);
860
+ if (site.rowQuery === null)
861
+ push('bad_binding', at, "row.<column> binds only on a table's row action");
862
+ else if (rq !== undefined && !rq.output.some((o) => o.name === rest))
863
+ push('unknown_column', at, `query ${rq.id} declares no output column ${JSON.stringify(rest)}`);
864
+ }
865
+ }
866
+ }
867
+ });
868
+ return out;
869
+ }
870
+ //# sourceMappingURL=spec.js.map