@yolk-sdk/emulators 0.1.0-canary.96

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 (333) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2135 -0
  3. package/dist/anthropic.d.mts +269 -0
  4. package/dist/anthropic.d.mts.map +1 -0
  5. package/dist/anthropic.mjs +177 -0
  6. package/dist/anthropic.mjs.map +1 -0
  7. package/dist/chat-completions.d.mts +288 -0
  8. package/dist/chat-completions.d.mts.map +1 -0
  9. package/dist/chat-completions.mjs +451 -0
  10. package/dist/chat-completions.mjs.map +1 -0
  11. package/dist/codex.d.mts +265 -0
  12. package/dist/codex.d.mts.map +1 -0
  13. package/dist/codex.mjs +199 -0
  14. package/dist/codex.mjs.map +1 -0
  15. package/dist/dropbox/api.d.mts +63 -0
  16. package/dist/dropbox/api.d.mts.map +1 -0
  17. package/dist/dropbox/api.mjs +565 -0
  18. package/dist/dropbox/api.mjs.map +1 -0
  19. package/dist/dropbox/state.d.mts +136 -0
  20. package/dist/dropbox/state.d.mts.map +1 -0
  21. package/dist/dropbox/state.mjs +209 -0
  22. package/dist/dropbox/state.mjs.map +1 -0
  23. package/dist/dropbox.d.mts +79 -0
  24. package/dist/dropbox.d.mts.map +1 -0
  25. package/dist/dropbox.mjs +124 -0
  26. package/dist/dropbox.mjs.map +1 -0
  27. package/dist/email-fixtures.d.mts +36 -0
  28. package/dist/email-fixtures.d.mts.map +1 -0
  29. package/dist/email-fixtures.mjs +1080 -0
  30. package/dist/email-fixtures.mjs.map +1 -0
  31. package/dist/email.d.mts +160 -0
  32. package/dist/email.d.mts.map +1 -0
  33. package/dist/email.mjs +608 -0
  34. package/dist/email.mjs.map +1 -0
  35. package/dist/emulator-compose.d.mts +40 -0
  36. package/dist/emulator-compose.d.mts.map +1 -0
  37. package/dist/emulator-compose.mjs +91 -0
  38. package/dist/emulator-compose.mjs.map +1 -0
  39. package/dist/emulator-http.d.mts +53 -0
  40. package/dist/emulator-http.d.mts.map +1 -0
  41. package/dist/emulator-http.mjs +117 -0
  42. package/dist/emulator-http.mjs.map +1 -0
  43. package/dist/emulator-kernel.d.mts +176 -0
  44. package/dist/emulator-kernel.d.mts.map +1 -0
  45. package/dist/emulator-kernel.mjs +413 -0
  46. package/dist/emulator-kernel.mjs.map +1 -0
  47. package/dist/fixture-route.d.mts +129 -0
  48. package/dist/fixture-route.d.mts.map +1 -0
  49. package/dist/fixture-route.mjs +257 -0
  50. package/dist/fixture-route.mjs.map +1 -0
  51. package/dist/fortnox/api.d.mts +92 -0
  52. package/dist/fortnox/api.d.mts.map +1 -0
  53. package/dist/fortnox/api.mjs +750 -0
  54. package/dist/fortnox/api.mjs.map +1 -0
  55. package/dist/fortnox/state.d.mts +533 -0
  56. package/dist/fortnox/state.d.mts.map +1 -0
  57. package/dist/fortnox/state.mjs +612 -0
  58. package/dist/fortnox/state.mjs.map +1 -0
  59. package/dist/fortnox.d.mts +128 -0
  60. package/dist/fortnox.d.mts.map +1 -0
  61. package/dist/fortnox.mjs +403 -0
  62. package/dist/fortnox.mjs.map +1 -0
  63. package/dist/gateway-evaluate-recordings.d.mts +12 -0
  64. package/dist/gateway-evaluate-recordings.d.mts.map +1 -0
  65. package/dist/gateway-evaluate-recordings.mjs +128 -0
  66. package/dist/gateway-evaluate-recordings.mjs.map +1 -0
  67. package/dist/gateway.d.mts +272 -0
  68. package/dist/gateway.d.mts.map +1 -0
  69. package/dist/gateway.mjs +400 -0
  70. package/dist/gateway.mjs.map +1 -0
  71. package/dist/github/api.d.mts +63 -0
  72. package/dist/github/api.d.mts.map +1 -0
  73. package/dist/github/api.mjs +527 -0
  74. package/dist/github/api.mjs.map +1 -0
  75. package/dist/github/state.d.mts +272 -0
  76. package/dist/github/state.d.mts.map +1 -0
  77. package/dist/github/state.mjs +303 -0
  78. package/dist/github/state.mjs.map +1 -0
  79. package/dist/github.d.mts +77 -0
  80. package/dist/github.d.mts.map +1 -0
  81. package/dist/github.mjs +180 -0
  82. package/dist/github.mjs.map +1 -0
  83. package/dist/google/calendar.d.mts +10 -0
  84. package/dist/google/calendar.d.mts.map +1 -0
  85. package/dist/google/calendar.mjs +252 -0
  86. package/dist/google/calendar.mjs.map +1 -0
  87. package/dist/google/drive.d.mts +8 -0
  88. package/dist/google/drive.d.mts.map +1 -0
  89. package/dist/google/drive.mjs +252 -0
  90. package/dist/google/drive.mjs.map +1 -0
  91. package/dist/google/gmail.d.mts +10 -0
  92. package/dist/google/gmail.d.mts.map +1 -0
  93. package/dist/google/gmail.mjs +591 -0
  94. package/dist/google/gmail.mjs.map +1 -0
  95. package/dist/google/shared.d.mts +107 -0
  96. package/dist/google/shared.d.mts.map +1 -0
  97. package/dist/google/shared.mjs +146 -0
  98. package/dist/google/shared.mjs.map +1 -0
  99. package/dist/google/state.d.mts +385 -0
  100. package/dist/google/state.d.mts.map +1 -0
  101. package/dist/google/state.mjs +496 -0
  102. package/dist/google/state.mjs.map +1 -0
  103. package/dist/google.d.mts +75 -0
  104. package/dist/google.d.mts.map +1 -0
  105. package/dist/google.mjs +210 -0
  106. package/dist/google.mjs.map +1 -0
  107. package/dist/linkedin-search/api.d.mts +45 -0
  108. package/dist/linkedin-search/api.d.mts.map +1 -0
  109. package/dist/linkedin-search/api.mjs +181 -0
  110. package/dist/linkedin-search/api.mjs.map +1 -0
  111. package/dist/linkedin-search/state.d.mts +123 -0
  112. package/dist/linkedin-search/state.d.mts.map +1 -0
  113. package/dist/linkedin-search/state.mjs +261 -0
  114. package/dist/linkedin-search/state.mjs.map +1 -0
  115. package/dist/linkedin-search.d.mts +69 -0
  116. package/dist/linkedin-search.d.mts.map +1 -0
  117. package/dist/linkedin-search.mjs +158 -0
  118. package/dist/linkedin-search.mjs.map +1 -0
  119. package/dist/mcp/api.d.mts +44 -0
  120. package/dist/mcp/api.d.mts.map +1 -0
  121. package/dist/mcp/api.mjs +557 -0
  122. package/dist/mcp/api.mjs.map +1 -0
  123. package/dist/mcp/recordings.d.mts +40 -0
  124. package/dist/mcp/recordings.d.mts.map +1 -0
  125. package/dist/mcp/recordings.mjs +2390 -0
  126. package/dist/mcp/recordings.mjs.map +1 -0
  127. package/dist/mcp/state.d.mts +71 -0
  128. package/dist/mcp/state.d.mts.map +1 -0
  129. package/dist/mcp/state.mjs +83 -0
  130. package/dist/mcp/state.mjs.map +1 -0
  131. package/dist/mcp.d.mts +84 -0
  132. package/dist/mcp.d.mts.map +1 -0
  133. package/dist/mcp.mjs +241 -0
  134. package/dist/mcp.mjs.map +1 -0
  135. package/dist/messages.d.mts +183 -0
  136. package/dist/messages.d.mts.map +1 -0
  137. package/dist/messages.mjs +532 -0
  138. package/dist/messages.mjs.map +1 -0
  139. package/dist/microsoft/api.d.mts +47 -0
  140. package/dist/microsoft/api.d.mts.map +1 -0
  141. package/dist/microsoft/api.mjs +178 -0
  142. package/dist/microsoft/api.mjs.map +1 -0
  143. package/dist/microsoft/calendar.d.mts +30 -0
  144. package/dist/microsoft/calendar.d.mts.map +1 -0
  145. package/dist/microsoft/calendar.mjs +271 -0
  146. package/dist/microsoft/calendar.mjs.map +1 -0
  147. package/dist/microsoft/drive.d.mts +36 -0
  148. package/dist/microsoft/drive.d.mts.map +1 -0
  149. package/dist/microsoft/drive.mjs +298 -0
  150. package/dist/microsoft/drive.mjs.map +1 -0
  151. package/dist/microsoft/graph.d.mts +198 -0
  152. package/dist/microsoft/graph.d.mts.map +1 -0
  153. package/dist/microsoft/graph.mjs +264 -0
  154. package/dist/microsoft/graph.mjs.map +1 -0
  155. package/dist/microsoft/mail.d.mts +48 -0
  156. package/dist/microsoft/mail.d.mts.map +1 -0
  157. package/dist/microsoft/mail.mjs +488 -0
  158. package/dist/microsoft/mail.mjs.map +1 -0
  159. package/dist/microsoft/state.d.mts +480 -0
  160. package/dist/microsoft/state.d.mts.map +1 -0
  161. package/dist/microsoft/state.mjs +625 -0
  162. package/dist/microsoft/state.mjs.map +1 -0
  163. package/dist/microsoft.d.mts +168 -0
  164. package/dist/microsoft.d.mts.map +1 -0
  165. package/dist/microsoft.mjs +483 -0
  166. package/dist/microsoft.mjs.map +1 -0
  167. package/dist/node.d.mts +47 -0
  168. package/dist/node.d.mts.map +1 -0
  169. package/dist/node.mjs +178 -0
  170. package/dist/node.mjs.map +1 -0
  171. package/dist/notion/api.d.mts +51 -0
  172. package/dist/notion/api.d.mts.map +1 -0
  173. package/dist/notion/api.mjs +507 -0
  174. package/dist/notion/api.mjs.map +1 -0
  175. package/dist/notion/state.d.mts +329 -0
  176. package/dist/notion/state.d.mts.map +1 -0
  177. package/dist/notion/state.mjs +432 -0
  178. package/dist/notion/state.mjs.map +1 -0
  179. package/dist/notion.d.mts +72 -0
  180. package/dist/notion.d.mts.map +1 -0
  181. package/dist/notion.mjs +127 -0
  182. package/dist/notion.mjs.map +1 -0
  183. package/dist/openai.d.mts +165 -0
  184. package/dist/openai.d.mts.map +1 -0
  185. package/dist/openai.mjs +141 -0
  186. package/dist/openai.mjs.map +1 -0
  187. package/dist/opencode-recordings.d.mts +7 -0
  188. package/dist/opencode-recordings.d.mts.map +1 -0
  189. package/dist/opencode-recordings.mjs +222 -0
  190. package/dist/opencode-recordings.mjs.map +1 -0
  191. package/dist/opencode.d.mts +70 -0
  192. package/dist/opencode.d.mts.map +1 -0
  193. package/dist/opencode.mjs +217 -0
  194. package/dist/opencode.mjs.map +1 -0
  195. package/dist/r2-fixtures.d.mts +36 -0
  196. package/dist/r2-fixtures.d.mts.map +1 -0
  197. package/dist/r2-fixtures.mjs +245 -0
  198. package/dist/r2-fixtures.mjs.map +1 -0
  199. package/dist/r2-guard.d.mts +43 -0
  200. package/dist/r2-guard.d.mts.map +1 -0
  201. package/dist/r2-guard.mjs +215 -0
  202. package/dist/r2-guard.mjs.map +1 -0
  203. package/dist/r2.d.mts +166 -0
  204. package/dist/r2.d.mts.map +1 -0
  205. package/dist/r2.mjs +517 -0
  206. package/dist/r2.mjs.map +1 -0
  207. package/dist/responses.d.mts +231 -0
  208. package/dist/responses.d.mts.map +1 -0
  209. package/dist/responses.mjs +556 -0
  210. package/dist/responses.mjs.map +1 -0
  211. package/dist/route-evidence.d.mts +45 -0
  212. package/dist/route-evidence.d.mts.map +1 -0
  213. package/dist/route-evidence.mjs +56 -0
  214. package/dist/route-evidence.mjs.map +1 -0
  215. package/dist/router.d.mts +93 -0
  216. package/dist/router.d.mts.map +1 -0
  217. package/dist/router.mjs +259 -0
  218. package/dist/router.mjs.map +1 -0
  219. package/dist/stateful-core.d.mts +17 -0
  220. package/dist/stateful-core.d.mts.map +1 -0
  221. package/dist/stateful-core.mjs +41 -0
  222. package/dist/stateful-core.mjs.map +1 -0
  223. package/dist/stateful-emulator.d.mts +565 -0
  224. package/dist/stateful-emulator.d.mts.map +1 -0
  225. package/dist/stateful-emulator.mjs +1228 -0
  226. package/dist/stateful-emulator.mjs.map +1 -0
  227. package/dist/stateful-secrets.d.mts +84 -0
  228. package/dist/stateful-secrets.d.mts.map +1 -0
  229. package/dist/stateful-secrets.mjs +216 -0
  230. package/dist/stateful-secrets.mjs.map +1 -0
  231. package/dist/subscription-usage-recordings.d.mts +9 -0
  232. package/dist/subscription-usage-recordings.d.mts.map +1 -0
  233. package/dist/subscription-usage-recordings.mjs +53 -0
  234. package/dist/subscription-usage-recordings.mjs.map +1 -0
  235. package/dist/subscription-usage.d.mts +53 -0
  236. package/dist/subscription-usage.d.mts.map +1 -0
  237. package/dist/subscription-usage.mjs +21 -0
  238. package/dist/subscription-usage.mjs.map +1 -0
  239. package/dist/telegram/api.d.mts +34 -0
  240. package/dist/telegram/api.d.mts.map +1 -0
  241. package/dist/telegram/api.mjs +257 -0
  242. package/dist/telegram/api.mjs.map +1 -0
  243. package/dist/telegram/state.d.mts +128 -0
  244. package/dist/telegram/state.d.mts.map +1 -0
  245. package/dist/telegram/state.mjs +154 -0
  246. package/dist/telegram/state.mjs.map +1 -0
  247. package/dist/telegram.d.mts +62 -0
  248. package/dist/telegram.d.mts.map +1 -0
  249. package/dist/telegram.mjs +127 -0
  250. package/dist/telegram.mjs.map +1 -0
  251. package/dist/todoist/api.d.mts +55 -0
  252. package/dist/todoist/api.d.mts.map +1 -0
  253. package/dist/todoist/api.mjs +415 -0
  254. package/dist/todoist/api.mjs.map +1 -0
  255. package/dist/todoist/state.d.mts +275 -0
  256. package/dist/todoist/state.d.mts.map +1 -0
  257. package/dist/todoist/state.mjs +345 -0
  258. package/dist/todoist/state.mjs.map +1 -0
  259. package/dist/todoist.d.mts +68 -0
  260. package/dist/todoist.d.mts.map +1 -0
  261. package/dist/todoist.mjs +181 -0
  262. package/dist/todoist.mjs.map +1 -0
  263. package/dist/xai.d.mts +267 -0
  264. package/dist/xai.d.mts.map +1 -0
  265. package/dist/xai.mjs +252 -0
  266. package/dist/xai.mjs.map +1 -0
  267. package/package.json +157 -0
  268. package/src/anthropic.ts +289 -0
  269. package/src/chat-completions.ts +924 -0
  270. package/src/codex.ts +296 -0
  271. package/src/dropbox/api.ts +1084 -0
  272. package/src/dropbox/state.ts +364 -0
  273. package/src/dropbox.ts +203 -0
  274. package/src/email-fixtures.ts +1297 -0
  275. package/src/email.ts +1108 -0
  276. package/src/emulator-compose.ts +147 -0
  277. package/src/emulator-http.ts +184 -0
  278. package/src/emulator-kernel.ts +844 -0
  279. package/src/fixture-route.ts +561 -0
  280. package/src/fortnox/api.ts +1352 -0
  281. package/src/fortnox/state.ts +798 -0
  282. package/src/fortnox.ts +801 -0
  283. package/src/gateway-evaluate-recordings.ts +153 -0
  284. package/src/gateway.ts +530 -0
  285. package/src/github/api.ts +986 -0
  286. package/src/github/state.ts +439 -0
  287. package/src/github.ts +271 -0
  288. package/src/google/calendar.ts +486 -0
  289. package/src/google/drive.ts +471 -0
  290. package/src/google/gmail.ts +1011 -0
  291. package/src/google/shared.ts +321 -0
  292. package/src/google/state.ts +686 -0
  293. package/src/google.ts +298 -0
  294. package/src/linkedin-search/api.ts +363 -0
  295. package/src/linkedin-search/state.ts +373 -0
  296. package/src/linkedin-search.ts +241 -0
  297. package/src/mcp/api.ts +995 -0
  298. package/src/mcp/recordings.ts +2665 -0
  299. package/src/mcp/state.ts +125 -0
  300. package/src/mcp.ts +319 -0
  301. package/src/messages.ts +844 -0
  302. package/src/microsoft/api.ts +426 -0
  303. package/src/microsoft/calendar.ts +491 -0
  304. package/src/microsoft/drive.ts +541 -0
  305. package/src/microsoft/graph.ts +550 -0
  306. package/src/microsoft/mail.ts +833 -0
  307. package/src/microsoft/state.ts +822 -0
  308. package/src/microsoft.ts +982 -0
  309. package/src/node.ts +293 -0
  310. package/src/notion/api.ts +976 -0
  311. package/src/notion/state.ts +512 -0
  312. package/src/notion.ts +210 -0
  313. package/src/openai.ts +198 -0
  314. package/src/opencode-recordings.ts +257 -0
  315. package/src/opencode.ts +271 -0
  316. package/src/r2-fixtures.ts +295 -0
  317. package/src/r2-guard.ts +323 -0
  318. package/src/r2.ts +890 -0
  319. package/src/responses.ts +901 -0
  320. package/src/route-evidence.ts +90 -0
  321. package/src/router.ts +490 -0
  322. package/src/stateful-core.ts +70 -0
  323. package/src/stateful-emulator.ts +2571 -0
  324. package/src/stateful-secrets.ts +299 -0
  325. package/src/subscription-usage-recordings.ts +77 -0
  326. package/src/subscription-usage.ts +68 -0
  327. package/src/telegram/api.ts +437 -0
  328. package/src/telegram/state.ts +229 -0
  329. package/src/telegram.ts +227 -0
  330. package/src/todoist/api.ts +736 -0
  331. package/src/todoist/state.ts +456 -0
  332. package/src/todoist.ts +316 -0
  333. package/src/xai.ts +341 -0
@@ -0,0 +1,439 @@
1
+ /**
2
+ * GitHub emulator state: the authenticated user, the practice repository, its labels, issues,
3
+ * comments, and files, the record of deleted comments, the counters, the seed input, the default
4
+ * seed, and the profiles (internal; re-exported by `src/github.ts`).
5
+ *
6
+ * Entity shapes and the default entities follow the synthetic GitHub conformance fixtures, copied
7
+ * as data (the same owner, repository, labels, work issue, file, user, and ids as the fixtures and
8
+ * `githubConformanceFixtureSeeds`), never imported from SDK code. The repository id is the one the
9
+ * paging fixture's `Link` URLs name, and the default branch is the one the contents fixture's URLs
10
+ * name.
11
+ *
12
+ * Issue numbers below `nextIssueNumber` that the state does not hold are implied (the repository
13
+ * reached them, but no fixture shows them): an answer that would render one is not emulated.
14
+ * Numbers at or above it are not reached yet and answer the not-found fixture's 404. Created issue
15
+ * numbers and comment ids come from counters that only advance; the default seed starts them at the
16
+ * fixtures' created values (issue 42, comment 9000000001), and a seed's counters must lie above
17
+ * every seeded number. Created issue `id` and `node_id` values derive from the minted number in the
18
+ * fixtures' form (`3000000000 + number`, `I_kwSynthetic<number>`), and created comment `node_id`
19
+ * values from the minted id (`IC_kwSynthetic<id>`); a seed may not use those minted forms at or
20
+ * above its counters.
21
+ *
22
+ * Comments exist only as created here (no fixture shows a seeded comment). A deleted comment is
23
+ * kept as a record (`deletedComments`): deleting it again answers the comment fixture's 404.
24
+ *
25
+ * @experimental
26
+ */
27
+ import { Result } from 'effect'
28
+ import * as Schema from 'effect/Schema'
29
+
30
+ /** A GitHub owner login (the connector's owner pattern). */
31
+ export const githubOwnerPattern = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/
32
+
33
+ /** A repository name in plain characters (no `.`/`..`, no `.git` suffix; checked below). */
34
+ export const githubRepoPattern = /^[A-Za-z0-9._-]{1,100}$/
35
+
36
+ /** A label name in plain characters (the conformance seed pattern; never percent-encoded). */
37
+ export const githubLabelNamePattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,49}$/
38
+
39
+ /**
40
+ * The source of a relative file path whose segments never start with a dot (the conformance seed
41
+ * pattern), unanchored, so the route's raw pattern can add its length bound explicitly.
42
+ */
43
+ export const githubFilePathSource = '[A-Za-z0-9_-][A-Za-z0-9._-]*(?:/[A-Za-z0-9_-][A-Za-z0-9._-]*)*'
44
+
45
+ /** A relative file path whose segments never start with a dot (the conformance seed pattern). */
46
+ export const githubFilePathPattern = new RegExp(`^${githubFilePathSource}$`)
47
+
48
+ /** A GitHub timestamp as the fixtures write it: whole seconds, UTC. */
49
+ export const githubTimestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/
50
+
51
+ const Timestamp = Schema.String.check(Schema.isPattern(githubTimestampPattern))
52
+
53
+ const PositiveInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))
54
+
55
+ /** The largest issue number the emulator addresses (ten digits, the route pattern). */
56
+ export const githubMaxIssueNumber = 9_999_999_999
57
+
58
+ const IssueNumber = PositiveInt.check(Schema.isLessThanOrEqualTo(githubMaxIssueNumber))
59
+
60
+ /** The largest comment id the emulator addresses (fifteen digits, the delete route's pattern). */
61
+ export const githubMaxCommentId = 999_999_999_999_999
62
+
63
+ const CommentId = PositiveInt.check(Schema.isLessThanOrEqualTo(githubMaxCommentId))
64
+
65
+ const LabelName = Schema.String.check(Schema.isPattern(githubLabelNamePattern))
66
+
67
+ /** A user as the fixtures render it (`login`, `id`, `type`, `site_admin`). */
68
+ export const GithubEmulatorUser = Schema.Struct({
69
+ login: Schema.String.check(Schema.isPattern(githubOwnerPattern)),
70
+ id: PositiveInt,
71
+ type: Schema.String,
72
+ siteAdmin: Schema.Boolean
73
+ })
74
+
75
+ export type GithubEmulatorUser = typeof GithubEmulatorUser.Type
76
+
77
+ /** The practice repository: its owner and name, its numeric id, and its default branch. */
78
+ export const GithubEmulatorRepository = Schema.Struct({
79
+ owner: Schema.String.check(Schema.isPattern(githubOwnerPattern)),
80
+ repo: Schema.String.check(
81
+ Schema.isPattern(githubRepoPattern),
82
+ Schema.makeFilter(
83
+ (repo: string) =>
84
+ (repo !== '.' && repo !== '..' && !repo.endsWith('.git')) ||
85
+ 'a repository name is not . or .. and does not end in .git'
86
+ )
87
+ ),
88
+ /** The id the `Link` URLs name (`/repositories/<id>/labels`). */
89
+ id: PositiveInt,
90
+ defaultBranch: Schema.String.check(Schema.isPattern(/^[A-Za-z0-9._-]{1,100}$/))
91
+ })
92
+
93
+ export type GithubEmulatorRepository = typeof GithubEmulatorRepository.Type
94
+
95
+ /** A repository label. */
96
+ export const GithubEmulatorLabel = Schema.Struct({
97
+ id: PositiveInt,
98
+ nodeId: Schema.String,
99
+ name: LabelName,
100
+ /** Six hex digits, as the fixtures write colors. */
101
+ color: Schema.String.check(Schema.isPattern(/^[0-9a-f]{6}$/)),
102
+ default: Schema.Boolean,
103
+ description: Schema.NullOr(Schema.String)
104
+ })
105
+
106
+ export type GithubEmulatorLabel = typeof GithubEmulatorLabel.Type
107
+
108
+ /** A seeded issue (its labels by name; never a pull request, never locked). */
109
+ export const GithubEmulatorSeedIssue = Schema.Struct({
110
+ number: IssueNumber,
111
+ id: PositiveInt,
112
+ nodeId: Schema.String,
113
+ title: Schema.String.check(Schema.isMinLength(1)),
114
+ body: Schema.NullOr(Schema.String),
115
+ state: Schema.Literals(['open', 'closed']),
116
+ stateReason: Schema.NullOr(Schema.String),
117
+ labels: Schema.Array(LabelName),
118
+ user: GithubEmulatorUser,
119
+ authorAssociation: Schema.String,
120
+ createdAt: Timestamp,
121
+ updatedAt: Timestamp,
122
+ closedAt: Schema.NullOr(Timestamp)
123
+ })
124
+
125
+ export type GithubEmulatorSeedIssue = typeof GithubEmulatorSeedIssue.Type
126
+
127
+ /** A stored issue: a seeded one, or one created through the recorded create flow. */
128
+ export const GithubEmulatorIssue = Schema.Struct({
129
+ ...GithubEmulatorSeedIssue.fields,
130
+ /** Created here (only such issues are renamed or closed). */
131
+ createdHere: Schema.Boolean
132
+ })
133
+
134
+ export type GithubEmulatorIssue = typeof GithubEmulatorIssue.Type
135
+
136
+ /** A comment created here on an issue. */
137
+ export const GithubEmulatorComment = Schema.Struct({
138
+ id: CommentId,
139
+ issueNumber: IssueNumber,
140
+ body: Schema.String,
141
+ user: GithubEmulatorUser,
142
+ authorAssociation: Schema.String,
143
+ createdAt: Timestamp,
144
+ updatedAt: Timestamp
145
+ })
146
+
147
+ export type GithubEmulatorComment = typeof GithubEmulatorComment.Type
148
+
149
+ /** A UTF-8 text file in the default branch. */
150
+ export const GithubEmulatorFile = Schema.Struct({
151
+ path: Schema.String.check(Schema.isPattern(githubFilePathPattern), Schema.isMaxLength(200)),
152
+ /** The blob sha (40 lower-case hex digits). */
153
+ sha: Schema.String.check(Schema.isPattern(/^[0-9a-f]{40}$/)),
154
+ text: Schema.String
155
+ })
156
+
157
+ export type GithubEmulatorFile = typeof GithubEmulatorFile.Type
158
+
159
+ const Counters = Schema.Struct({
160
+ /** The number the next created issue takes (one past the maximum when exhausted). */
161
+ nextIssueNumber: PositiveInt.check(Schema.isLessThanOrEqualTo(githubMaxIssueNumber + 1)),
162
+ /** The id the next created comment takes (one past the maximum when exhausted). */
163
+ nextCommentId: PositiveInt.check(Schema.isLessThanOrEqualTo(githubMaxCommentId + 1))
164
+ })
165
+
166
+ /** The whole emulator state (JSON-compatible; what `snapshot()` returns). */
167
+ export const GithubEmulatorStateSchema = Schema.Struct({
168
+ /** The authenticated user: the author of everything created here. */
169
+ viewer: GithubEmulatorUser,
170
+ repository: GithubEmulatorRepository,
171
+ labels: Schema.Array(GithubEmulatorLabel),
172
+ issues: Schema.Array(GithubEmulatorIssue),
173
+ comments: Schema.Array(GithubEmulatorComment),
174
+ /** Ids of comments deleted here (a second delete answers the recorded 404). */
175
+ deletedComments: Schema.Array(CommentId),
176
+ files: Schema.Array(GithubEmulatorFile),
177
+ counters: Counters
178
+ })
179
+
180
+ /**
181
+ * The emulator state. The container is mutable (routes replace whole lists); every entity is
182
+ * replaced, never edited in place.
183
+ */
184
+ export type GithubEmulatorState = {
185
+ viewer: GithubEmulatorUser
186
+ repository: GithubEmulatorRepository
187
+ labels: ReadonlyArray<GithubEmulatorLabel>
188
+ issues: ReadonlyArray<GithubEmulatorIssue>
189
+ comments: ReadonlyArray<GithubEmulatorComment>
190
+ deletedComments: ReadonlyArray<number>
191
+ files: ReadonlyArray<GithubEmulatorFile>
192
+ counters: typeof Counters.Type
193
+ }
194
+
195
+ /** Account-variance profiles for the default seed. */
196
+ export const GithubEmulatorProfile = Schema.Literals(['default', 'empty'])
197
+
198
+ export type GithubEmulatorProfile = typeof GithubEmulatorProfile.Type
199
+
200
+ /**
201
+ * A typed seed. Start from `profile` (default `'default'`, the fixture entities); every other key,
202
+ * when given, replaces that part of the profile. Counters default to the profile's, raised above
203
+ * the highest seeded issue number; a counter given at or below a seeded value is invalid.
204
+ */
205
+ export const GithubEmulatorSeed = Schema.Struct({
206
+ profile: Schema.optionalKey(GithubEmulatorProfile),
207
+ viewer: Schema.optionalKey(GithubEmulatorUser),
208
+ repository: Schema.optionalKey(GithubEmulatorRepository),
209
+ labels: Schema.optionalKey(Schema.Array(GithubEmulatorLabel)),
210
+ issues: Schema.optionalKey(Schema.Array(GithubEmulatorSeedIssue)),
211
+ files: Schema.optionalKey(Schema.Array(GithubEmulatorFile)),
212
+ nextIssueNumber: Schema.optionalKey(IssueNumber),
213
+ nextCommentId: Schema.optionalKey(CommentId)
214
+ })
215
+
216
+ export type GithubEmulatorSeed = typeof GithubEmulatorSeed.Type
217
+
218
+ const strict = { onExcessProperty: 'error' } as const
219
+
220
+ const decodeSeedInput = Schema.decodeUnknownResult(GithubEmulatorSeed, strict)
221
+
222
+ const decodeStateInput = Schema.decodeUnknownResult(GithubEmulatorStateSchema, strict)
223
+
224
+ const issueMessage = (issue: Schema.SchemaError['issue']): string =>
225
+ new Schema.SchemaError(issue).message
226
+
227
+ /** Minted issue `id` values: `3000000000 + number`, as the fixtures write them. */
228
+ export const mintedIssueIdBase = 3_000_000_000
229
+
230
+ export const mintedIssueNodeId = (number: number): string => `I_kwSynthetic${number}`
231
+
232
+ export const mintedCommentNodeId = (id: number): string => `IC_kwSynthetic${id}`
233
+
234
+ // Default entities, copied from the fixtures: the user, the practice repository (the paging
235
+ // fixture's repository id, the contents fixture's branch), the five labels of the paging fixture,
236
+ // the work issue of the label fixture, and the text file of the contents fixture.
237
+
238
+ const seededAt = '2026-09-30T12:00:00Z'
239
+
240
+ const defaultViewer: GithubEmulatorUser = {
241
+ login: 'yolk-synthetic-bot',
242
+ id: 1000001,
243
+ type: 'User',
244
+ siteAdmin: false
245
+ }
246
+
247
+ const defaultRepository: GithubEmulatorRepository = {
248
+ owner: 'yolk-synthetic',
249
+ repo: 'conformance-practice',
250
+ id: 100000001,
251
+ defaultBranch: 'main'
252
+ }
253
+
254
+ const label = (id: number, name: string, color: string): GithubEmulatorLabel => ({
255
+ id,
256
+ nodeId: `LA_kwSynthetic${id}`,
257
+ name,
258
+ color,
259
+ default: false,
260
+ description: `Synthetic ${name} label`
261
+ })
262
+
263
+ const defaultLabels: ReadonlyArray<GithubEmulatorLabel> = [
264
+ label(700000001, 'bug', 'd73a4a'),
265
+ label(700000002, 'documentation', '0075ca'),
266
+ label(700000003, 'enhancement', 'a2eeef'),
267
+ label(700000004, 'question', 'd876e3'),
268
+ label(700000005, 'synthetic-conformance', 'ededed')
269
+ ]
270
+
271
+ const workIssue: GithubEmulatorSeedIssue = {
272
+ number: 1,
273
+ id: mintedIssueIdBase + 1,
274
+ nodeId: mintedIssueNodeId(1),
275
+ title: 'Synthetic work issue for conformance runs',
276
+ body: 'Synthetic work issue: conformance cases add and remove a label and a comment here.',
277
+ state: 'open',
278
+ stateReason: null,
279
+ labels: ['bug'],
280
+ user: defaultViewer,
281
+ authorAssociation: 'OWNER',
282
+ createdAt: seededAt,
283
+ updatedAt: seededAt,
284
+ closedAt: null
285
+ }
286
+
287
+ const notesFile: GithubEmulatorFile = {
288
+ path: 'docs/synthetic-notes.txt',
289
+ sha: '5f1c0ffee5f1c0ffee5f1c0ffee5f1c0ffee0001',
290
+ text:
291
+ 'Synthetic conformance notes for the practice repository: café, naïve, façade.\n' +
292
+ 'Second synthetic line.\n'
293
+ }
294
+
295
+ type ProfileParts = {
296
+ readonly viewer: GithubEmulatorUser
297
+ readonly repository: GithubEmulatorRepository
298
+ readonly labels: ReadonlyArray<GithubEmulatorLabel>
299
+ readonly issues: ReadonlyArray<GithubEmulatorSeedIssue>
300
+ readonly files: ReadonlyArray<GithubEmulatorFile>
301
+ readonly counters: typeof Counters.Type
302
+ }
303
+
304
+ const profileParts = (profile: GithubEmulatorProfile): ProfileParts =>
305
+ profile === 'default'
306
+ ? {
307
+ viewer: defaultViewer,
308
+ repository: defaultRepository,
309
+ labels: defaultLabels,
310
+ issues: [workIssue],
311
+ files: [notesFile],
312
+ // The lifecycle fixture's created issue and the comment fixture's created comment.
313
+ counters: { nextIssueNumber: 42, nextCommentId: 9000000001 }
314
+ }
315
+ : {
316
+ viewer: defaultViewer,
317
+ repository: defaultRepository,
318
+ labels: [],
319
+ issues: [],
320
+ files: [],
321
+ counters: { nextIssueNumber: 1, nextCommentId: 1 }
322
+ }
323
+
324
+ const duplicate = <A>(values: ReadonlyArray<A>): A | undefined =>
325
+ values.find((value, index) => values.indexOf(value) !== index)
326
+
327
+ /** Integrity problems a decoded seed can still have (duplicates, dangling or minted values). */
328
+ const seedProblem = (parts: ProfileParts): string | undefined => {
329
+ const duplicates: ReadonlyArray<readonly [string, unknown]> = [
330
+ ['label name', duplicate(parts.labels.map(entry => entry.name))],
331
+ ['label id', duplicate(parts.labels.map(entry => entry.id))],
332
+ ['issue number', duplicate(parts.issues.map(issue => issue.number))],
333
+ ['issue id', duplicate(parts.issues.map(issue => issue.id))],
334
+ // Node ids are global across types (labels and issues alike).
335
+ [
336
+ 'node id',
337
+ duplicate([...parts.labels.map(entry => entry.nodeId), ...parts.issues.map(i => i.nodeId)])
338
+ ],
339
+ ['file path', duplicate(parts.files.map(file => file.path))]
340
+ ]
341
+
342
+ for (const [kind, value] of duplicates) {
343
+ if (value !== undefined) return `duplicate ${kind} ${String(value)}`
344
+ }
345
+
346
+ const labelNames = new Set(parts.labels.map(entry => entry.name))
347
+
348
+ for (const issue of parts.issues) {
349
+ const missing = issue.labels.find(name => !labelNames.has(name))
350
+
351
+ if (missing !== undefined) return `issue ${issue.number} names missing label ${missing}`
352
+
353
+ if (duplicate(issue.labels) !== undefined) return `issue ${issue.number} repeats a label`
354
+
355
+ if (issue.number >= parts.counters.nextIssueNumber) {
356
+ return `nextIssueNumber must lie above seeded issue ${issue.number}`
357
+ }
358
+
359
+ // Minted ids never collide with seeded ones.
360
+ if (issue.id >= mintedIssueIdBase + parts.counters.nextIssueNumber) {
361
+ return `issue ${issue.number} uses an id in the minted form at or above nextIssueNumber`
362
+ }
363
+ }
364
+
365
+ // Every minted node id form is reserved across all seeded node ids, of issues and labels alike.
366
+ const reserved = [...parts.labels.map(entry => entry.nodeId), ...parts.issues.map(i => i.nodeId)]
367
+ .map(nodeId => {
368
+ const issueNumber = /^I_kwSynthetic(\d+)$/.exec(nodeId)?.[1]
369
+ const commentId = /^IC_kwSynthetic(\d+)$/.exec(nodeId)?.[1]
370
+
371
+ return (issueNumber !== undefined && Number(issueNumber) >= parts.counters.nextIssueNumber) ||
372
+ (commentId !== undefined && Number(commentId) >= parts.counters.nextCommentId)
373
+ ? nodeId
374
+ : undefined
375
+ })
376
+ .find(nodeId => nodeId !== undefined)
377
+
378
+ if (reserved !== undefined) {
379
+ return `node id ${reserved} uses a minted form at or above its counter`
380
+ }
381
+
382
+ const prefix = parts.files.find(file =>
383
+ parts.files.some(other => other.path.startsWith(`${file.path}/`))
384
+ )
385
+
386
+ return prefix === undefined ? undefined : `file ${prefix.path} is also a folder`
387
+ }
388
+
389
+ /** Build the emulator state for a decoded seed; a string is an integrity problem. */
390
+ const stateFromSeed = (seed: GithubEmulatorSeed): GithubEmulatorState | string => {
391
+ const profile = profileParts(seed.profile ?? 'default')
392
+ const issues = seed.issues ?? profile.issues
393
+ const highest = Math.max(0, ...issues.map(issue => issue.number))
394
+
395
+ const parts: ProfileParts = {
396
+ viewer: seed.viewer ?? profile.viewer,
397
+ repository: seed.repository ?? profile.repository,
398
+ labels: seed.labels ?? profile.labels,
399
+ issues,
400
+ files: seed.files ?? profile.files,
401
+ counters: {
402
+ // Raised above the seeded numbers unless the seed names the counter itself (then checked).
403
+ nextIssueNumber:
404
+ seed.nextIssueNumber ?? Math.max(profile.counters.nextIssueNumber, highest + 1),
405
+ nextCommentId: seed.nextCommentId ?? profile.counters.nextCommentId
406
+ }
407
+ }
408
+
409
+ const problem = seedProblem(parts)
410
+
411
+ return (
412
+ problem ?? {
413
+ viewer: parts.viewer,
414
+ repository: parts.repository,
415
+ labels: parts.labels,
416
+ issues: parts.issues.map(issue => ({ ...issue, createdHere: false })),
417
+ comments: [],
418
+ deletedComments: [],
419
+ files: parts.files,
420
+ counters: parts.counters
421
+ }
422
+ )
423
+ }
424
+
425
+ /** Decode and build a seed; a string is the reason it is invalid. */
426
+ export const buildSeedState = (input: unknown): GithubEmulatorState | string => {
427
+ const decoded = decodeSeedInput(input)
428
+
429
+ return Result.isFailure(decoded)
430
+ ? issueMessage(decoded.failure.issue)
431
+ : stateFromSeed(decoded.success)
432
+ }
433
+
434
+ /** Decode a full state (a restored snapshot); a string is the reason it is invalid. */
435
+ export const decodeState = (input: unknown): GithubEmulatorState | string => {
436
+ const decoded = decodeStateInput(input)
437
+
438
+ return Result.isFailure(decoded) ? issueMessage(decoded.failure.issue) : { ...decoded.success }
439
+ }