@pome-sh/checks 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +88 -0
  3. package/dist/_types/sdk/check-discrimination.d.ts +7 -0
  4. package/dist/_types/sdk/check-state-path.d.ts +71 -0
  5. package/dist/_types/sdk/checks.d.ts +93 -0
  6. package/dist/_types/sdk/db.d.ts +44 -0
  7. package/dist/_types/sdk/failure-injection-rules.d.ts +32 -0
  8. package/dist/_types/twin-github/src/check-kind.d.ts +3 -0
  9. package/dist/_types/twin-github/src/check-state.d.ts +77 -0
  10. package/dist/_types/twin-github/src/checks.d.ts +50 -0
  11. package/dist/_types/twin-github/src/seed.d.ts +92 -0
  12. package/dist/_types/twin-github/src/types.d.ts +244 -0
  13. package/dist/_types/twin-gmail/src/check-kind.d.ts +3 -0
  14. package/dist/_types/twin-gmail/src/check-state.d.ts +138 -0
  15. package/dist/_types/twin-gmail/src/checks.d.ts +19 -0
  16. package/dist/_types/twin-gmail/src/faults.d.ts +21 -0
  17. package/dist/_types/twin-gmail/src/seed.d.ts +247 -0
  18. package/dist/_types/twin-gmail/src/types.d.ts +150 -0
  19. package/dist/_types/twin-linear/src/check-kind.d.ts +3 -0
  20. package/dist/_types/twin-linear/src/check-state.d.ts +123 -0
  21. package/dist/_types/twin-linear/src/checks.d.ts +29 -0
  22. package/dist/_types/twin-linear/src/seed.d.ts +166 -0
  23. package/dist/_types/twin-linear/src/types.d.ts +366 -0
  24. package/dist/_types/twin-slack/src/check-kind.d.ts +3 -0
  25. package/dist/_types/twin-slack/src/check-state.d.ts +107 -0
  26. package/dist/_types/twin-slack/src/checks.d.ts +14 -0
  27. package/dist/_types/twin-slack/src/seed.d.ts +62 -0
  28. package/dist/_types/twin-slack/src/types.d.ts +182 -0
  29. package/dist/_types/twin-stripe/src/check-kind.d.ts +3 -0
  30. package/dist/_types/twin-stripe/src/check-state.d.ts +107 -0
  31. package/dist/_types/twin-stripe/src/checks.d.ts +22 -0
  32. package/dist/_types/twin-stripe/src/seed.d.ts +213 -0
  33. package/dist/_types/twin-stripe/src/types.d.ts +252 -0
  34. package/dist/_types/wire/index.d.ts +23 -0
  35. package/dist/_types/wire/otel/event-schema.d.ts +424 -0
  36. package/dist/_types/wire/otel/index.d.ts +25 -0
  37. package/dist/_types/wire/otel/legacy-shim.d.ts +105 -0
  38. package/dist/_types/wire/otel/map-span.d.ts +64 -0
  39. package/dist/_types/wire/otel/nano.d.ts +26 -0
  40. package/dist/_types/wire/otel/project.d.ts +42 -0
  41. package/dist/_types/wire/otel/semconv.d.ts +57 -0
  42. package/dist/_types/wire/otel/span-event.d.ts +198 -0
  43. package/dist/_types/wire/recorder-events.d.ts +526 -0
  44. package/dist/_types/wire/redaction.d.ts +2 -0
  45. package/dist/chunk-4WXX5VPA.js +238 -0
  46. package/dist/chunk-5SJ4PVO5.js +981 -0
  47. package/dist/chunk-GYFGMULG.js +1138 -0
  48. package/dist/chunk-IVENH4KX.js +814 -0
  49. package/dist/chunk-NORTPYDQ.js +1015 -0
  50. package/dist/chunk-SJ6SVRAA.js +432 -0
  51. package/dist/chunk-ZXE6LAM3.js +1 -0
  52. package/dist/dsl.d.ts +11 -0
  53. package/dist/dsl.js +2 -0
  54. package/dist/github.d.ts +2 -0
  55. package/dist/github.js +2 -0
  56. package/dist/gmail.d.ts +2 -0
  57. package/dist/gmail.js +2 -0
  58. package/dist/index.d.ts +163 -0
  59. package/dist/index.js +24 -0
  60. package/dist/linear.d.ts +2 -0
  61. package/dist/linear.js +2 -0
  62. package/dist/slack.d.ts +2 -0
  63. package/dist/slack.js +2 -0
  64. package/dist/stripe.d.ts +2 -0
  65. package/dist/stripe.js +2 -0
  66. package/package.json +93 -0
@@ -0,0 +1,1138 @@
1
+ import { statePath, defineCheck, VACUITY_SENTINEL, childStatePath, VACUITY_SENTINEL_NUMBER } from './chunk-4WXX5VPA.js';
2
+ import { z } from 'zod';
3
+
4
+ // ../twin-gmail/dist/src/check-params.js
5
+ var messageId = {
6
+ name: "message",
7
+ pattern: "[A-Za-z0-9_-]{1,128}",
8
+ example: "msg_support",
9
+ render: (value) => value,
10
+ parse: (raw) => raw
11
+ };
12
+ var labelRef = {
13
+ name: "label",
14
+ pattern: "[A-Za-z0-9_-]{1,128}",
15
+ example: "STARRED",
16
+ render: (value) => value,
17
+ parse: (raw) => raw
18
+ };
19
+ var labelName = {
20
+ name: "label",
21
+ pattern: "[A-Za-z0-9][A-Za-z0-9 _-]{0,127}",
22
+ example: "Parity Complete",
23
+ render: (value) => value,
24
+ parse: (raw) => raw
25
+ };
26
+ var emailAddress = {
27
+ name: "email",
28
+ pattern: "[^\\s@\"'`]+@[^\\s@\"'`]+",
29
+ example: "alice@example.com",
30
+ render: (value) => value,
31
+ parse: (raw) => raw
32
+ };
33
+ var mailboxRef = {
34
+ name: "mailbox",
35
+ pattern: "[^\\s@\"'`]+@[^\\s@\"'`]+",
36
+ example: "pome-agent@pome-twin.test",
37
+ render: (value) => value,
38
+ parse: (raw) => raw
39
+ };
40
+ var exactCount = {
41
+ name: "count",
42
+ pattern: "\\d{1,9}",
43
+ example: "5",
44
+ render: (value) => value,
45
+ parse: (raw) => raw
46
+ };
47
+ var countWord = {
48
+ name: "count",
49
+ pattern: "(?:\\d{1,9}|zero|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)",
50
+ example: "two",
51
+ render: (value) => value,
52
+ parse: (raw) => raw
53
+ };
54
+ var NUMBER_WORDS = {
55
+ zero: 0,
56
+ one: 1,
57
+ two: 2,
58
+ three: 3,
59
+ four: 4,
60
+ five: 5,
61
+ six: 6,
62
+ seven: 7,
63
+ eight: 8,
64
+ nine: 9,
65
+ ten: 10,
66
+ eleven: 11,
67
+ twelve: 12
68
+ };
69
+ function parseCount(raw) {
70
+ const trimmed = raw.trim().toLowerCase();
71
+ if (/^\d+$/.test(trimmed))
72
+ return Number(trimmed);
73
+ return NUMBER_WORDS[trimmed] ?? null;
74
+ }
75
+
76
+ // ../twin-gmail/dist/src/check-state.js
77
+ var MESSAGES_PATH = statePath("messages");
78
+ var DRAFTS_PATH = statePath("drafts");
79
+ var LABELS_PATH = statePath("labels");
80
+ var MESSAGE_LABELS_PATH = statePath("messageLabels");
81
+ function missSkip(miss) {
82
+ const outcome = { passed: false, status: "skipped", reason: miss.missing };
83
+ if (miss.searched === void 0)
84
+ return outcome;
85
+ return { ...outcome, evidenceStatePaths: [miss.searched] };
86
+ }
87
+ var lower = (value) => (value ?? "").toLowerCase();
88
+ function isTruncated(state, collection) {
89
+ return (state.exportBounds?.truncatedCollections ?? []).includes(collection);
90
+ }
91
+ function resolveMessage(state, id2) {
92
+ if (state.messages == null)
93
+ return { missing: "state_incomplete" };
94
+ const indices = state.messages.map((message2, index) => lower(message2.id) === lower(id2) ? index : -1).filter((index) => index >= 0);
95
+ if (indices.length > 1) {
96
+ return {
97
+ missing: `message_ambiguous ("${id2}" exists in ${indices.length} mailboxes)`,
98
+ searched: MESSAGES_PATH
99
+ };
100
+ }
101
+ if (indices.length === 1) {
102
+ const index = indices[0];
103
+ return { found: state.messages[index], path: childStatePath(MESSAGES_PATH, index) };
104
+ }
105
+ if (isTruncated(state, "messages")) {
106
+ return { missing: 'collection_truncated ("messages")', searched: MESSAGES_PATH };
107
+ }
108
+ return { missing: `message_not_found ("${id2}")`, searched: MESSAGES_PATH };
109
+ }
110
+ function labelIdsFor(state, wanted) {
111
+ const ids = /* @__PURE__ */ new Set([lower(wanted)]);
112
+ for (const label of state.labels ?? []) {
113
+ const matches = lower(label.name) === lower(wanted) || lower(label.id) === lower(wanted);
114
+ if (matches && label.id != null)
115
+ ids.add(lower(label.id));
116
+ }
117
+ return ids;
118
+ }
119
+ function messageCarriesLabel(state, messageId2, wanted) {
120
+ if (state.messageLabels == null)
121
+ return { missing: "state_incomplete" };
122
+ const ids = labelIdsFor(state, wanted);
123
+ const carried = state.messageLabels.some((row) => lower(row.messageId) === lower(messageId2) && ids.has(lower(row.labelId)));
124
+ if (!carried && isTruncated(state, "messageLabels")) {
125
+ return { missing: 'collection_truncated ("messageLabels")', searched: MESSAGE_LABELS_PATH };
126
+ }
127
+ return { found: carried, path: MESSAGE_LABELS_PATH };
128
+ }
129
+ function resolveLabelByName(state, name) {
130
+ if (state.labels == null)
131
+ return { missing: "state_incomplete" };
132
+ const index = state.labels.findIndex((label) => lower(label.name) === lower(name));
133
+ if (index >= 0) {
134
+ return { found: state.labels[index], path: childStatePath(LABELS_PATH, index) };
135
+ }
136
+ if (isTruncated(state, "labels")) {
137
+ return { missing: 'collection_truncated ("labels")', searched: LABELS_PATH };
138
+ }
139
+ return { missing: `label_not_found ("${name}")`, searched: LABELS_PATH };
140
+ }
141
+ function draftRecipients(state, draft2) {
142
+ const message2 = (state.messages ?? []).find((candidate) => lower(candidate.id) === lower(draft2.messageId));
143
+ if (!message2)
144
+ return [];
145
+ return [...message2.to ?? [], ...message2.cc ?? []];
146
+ }
147
+
148
+ // ../twin-gmail/dist/src/check-worlds.js
149
+ var FIXTURE_MAILBOX = "pome-agent@pome-twin.test";
150
+ function gmailState(parts = {}) {
151
+ return {
152
+ mailboxes: [{ email: FIXTURE_MAILBOX }],
153
+ messages: [],
154
+ drafts: [],
155
+ labels: [],
156
+ messageLabels: [],
157
+ exportBounds: { messageBodiesOmitted: true, largeMailbox: false, truncatedCollections: [] },
158
+ ...parts
159
+ };
160
+ }
161
+ function finalWorld(final) {
162
+ return { seed: null, final, tape: null };
163
+ }
164
+ function tapeWorld(tape) {
165
+ return { seed: null, final: gmailState(), tape };
166
+ }
167
+ function message(id2, parts = {}) {
168
+ return { mailboxEmail: FIXTURE_MAILBOX, id: id2, to: [FIXTURE_MAILBOX], bodyOmitted: true, ...parts };
169
+ }
170
+ function systemLabel(name) {
171
+ return { mailboxEmail: FIXTURE_MAILBOX, id: name, name, type: "system" };
172
+ }
173
+ function userLabel(id2, name) {
174
+ return { mailboxEmail: FIXTURE_MAILBOX, id: id2, name, type: "user" };
175
+ }
176
+ function messageLabel(messageId2, labelId) {
177
+ return { mailboxEmail: FIXTURE_MAILBOX, messageId: messageId2, labelId };
178
+ }
179
+ function draft(id2, messageId2) {
180
+ return { mailboxEmail: FIXTURE_MAILBOX, id: id2, messageId: messageId2 };
181
+ }
182
+ function draftAddressedTo(id2, recipients) {
183
+ const messageId2 = `${id2}_message`;
184
+ return {
185
+ draft: draft(id2, messageId2),
186
+ message: message(messageId2, { to: recipients })
187
+ };
188
+ }
189
+
190
+ // ../twin-gmail/dist/src/check-drafts.js
191
+ var draftAddressedTo2 = defineCheck({
192
+ id: "gmail.draft-addressed-to",
193
+ description: "Joins every draft to its backing message and asks whether any of them addresses this recipient in `to` or `cc`, compared case-insensitively as an EXACT address rather than a substring. It asserts nothing about the draft's body \u2014 message bodies are digested out of the state export unconditionally, so no check on this twin can read one \u2014 and nothing about whether the draft was left unsent, which is a separate claim. A draft whose backing message did not survive the export contributes no recipients rather than throwing.",
194
+ template: "A draft addressed to {email} exists",
195
+ params: { email: emailAddress },
196
+ substrate: "final",
197
+ // An achievement: the seeded mailbox already contains a draft with a
198
+ // recipient, so an examinee that does nothing does not satisfy this.
199
+ polarity: () => "positive",
200
+ //
201
+ // THE LOAD-BEARING DECLARATION ON THIS CHECK.
202
+ //
203
+ // `subject` names the value the assertion is actually about, and on this check
204
+ // that value is an email address — squarely inside what a redaction config may
205
+ // legitimately destroy before an evaluator ever sees the state. Declaring it
206
+ // is what lets a consumer detect that the comparison has become impossible and
207
+ // record an honest skip. Without it there is nothing to detect: the address is
208
+ // simply absent, the comparison silently cannot match, and the criterion
209
+ // reports a vacuous verdict that reads like a real one.
210
+ subject: ({ email: email2 }) => email2,
211
+ // The address is the scanned literal, so the mutant points at it. It stays
212
+ // email-shaped so it re-binds to this same check: a mutant that stops matching
213
+ // evaluates to `unmatched`, which reads as "the verdict moved -> healthy" and
214
+ // hands the criterion a clean bill it did not earn.
215
+ vacuityMutant: (args) => ({ ...args, email: `${VACUITY_SENTINEL}@example.invalid` }),
216
+ discriminatingWorlds: ({ email: email2 }) => {
217
+ const wanted = draftAddressedTo("draft_target", [email2]);
218
+ const other = draftAddressedTo("draft_other", ["someone-else@example.com"]);
219
+ return {
220
+ passing: finalWorld(gmailState({ drafts: [wanted.draft], messages: [wanted.message] })),
221
+ failing: finalWorld(gmailState({ drafts: [other.draft], messages: [other.message] }))
222
+ };
223
+ },
224
+ evaluate({ email: email2 }, { final }) {
225
+ if (final.drafts == null || final.messages == null) {
226
+ return { passed: false, status: "skipped", reason: "state_incomplete" };
227
+ }
228
+ const wanted = email2.toLowerCase();
229
+ const index = final.drafts.findIndex((entry) => draftRecipients(final, entry).some((to) => to.toLowerCase() === wanted));
230
+ if (index >= 0) {
231
+ const hit = final.drafts[index];
232
+ return {
233
+ passed: true,
234
+ reason: `draft ${hit.id ?? "?"} is addressed to ${email2}`,
235
+ // The DRAFT row, not the backing message that actually holds the
236
+ // address. A reader following this pointer lands on the thing the
237
+ // sentence is about — "a draft addressed to X" — and the join to the
238
+ // message is an implementation detail of how the twin exports, not the
239
+ // claim.
240
+ evidenceStatePaths: [childStatePath(DRAFTS_PATH, index)]
241
+ };
242
+ }
243
+ if (isTruncated(final, "drafts") || isTruncated(final, "messages")) {
244
+ return { passed: false, status: "skipped", reason: "collection_truncated" };
245
+ }
246
+ return {
247
+ passed: false,
248
+ reason: `no draft is addressed to ${email2} (${final.drafts.length} draft(s) inspected)`,
249
+ evidenceStatePaths: [DRAFTS_PATH]
250
+ };
251
+ }
252
+ });
253
+ var draftCountAtLeast = defineCheck({
254
+ id: "gmail.draft-count-at-least",
255
+ description: "Counts the rows in the exported `drafts` collection and asserts there are AT LEAST this many \u2014 a lower bound, so a mailbox with more drafts than asked still passes. It counts drafts the SEED placed there as well as any the examinee created, which means a criterion whose number the seed already satisfies can be passed by an agent that does nothing. That is a property of the task, not of this check, and `measure-criterion-discrimination` is where it surfaces.",
256
+ template: "At least {count} drafts exist",
257
+ params: { count: countWord },
258
+ substrate: "final",
259
+ polarity: () => "positive",
260
+ // A threshold, not a literal hunted for inside state — there is nothing here a
261
+ // redactor could silently delete.
262
+ subject: () => null,
263
+ //
264
+ // The NUMERIC sentinel, and the argument D10 demands before it may be used.
265
+ //
266
+ // In every other pattern a numeric capture is a SELECTOR — an issue number, a
267
+ // PR number — that the predicate RESOLVES before it scans anything, so
268
+ // falsifying it early-returns "not found" and moves the verdict for a reason
269
+ // that never reaches the assertion. Here the count is the ONLY slot and there
270
+ // is nothing to resolve: it is compared directly against a cardinality the
271
+ // predicate computes, so falsifying it moves the verdict THROUGH the
272
+ // assertion. That is the same argument `stripe.payment-intent-amount` makes,
273
+ // and it is why a numeric sentinel is admissible on this check and on very few
274
+ // others: an evaluator that allowlists the numeric sentinel per check should
275
+ // expect these two.
276
+ vacuityMutant: (args) => ({ ...args, count: String(VACUITY_SENTINEL_NUMBER) }),
277
+ discriminatingWorlds: ({ count }) => {
278
+ const wanted = parseCount(count) ?? 1;
279
+ const build = (n) => {
280
+ const pairs = Array.from({ length: n }, (_, i) => draftAddressedTo(`draft_${i}`, ["a@example.com"]));
281
+ return gmailState({
282
+ drafts: pairs.map((p) => p.draft),
283
+ messages: pairs.map((p) => p.message)
284
+ });
285
+ };
286
+ return { passing: finalWorld(build(wanted)), failing: finalWorld(build(Math.max(0, wanted - 1))) };
287
+ },
288
+ evaluate({ count }, { final }) {
289
+ const wanted = parseCount(count);
290
+ if (wanted === null) {
291
+ return { passed: false, status: "skipped", reason: `uncountable ("${count}")` };
292
+ }
293
+ if (final.drafts == null)
294
+ return { passed: false, status: "skipped", reason: "state_incomplete" };
295
+ const total = final.drafts.length;
296
+ if (total < wanted && isTruncated(final, "drafts")) {
297
+ return { passed: false, status: "skipped", reason: 'collection_truncated ("drafts")' };
298
+ }
299
+ return {
300
+ passed: total >= wanted,
301
+ reason: `${total} draft(s) exist (wanted at least ${wanted})`,
302
+ // The collection whose LENGTH is the assertion. There is no narrower
303
+ // address for a cardinality.
304
+ evidenceStatePaths: [DRAFTS_PATH]
305
+ };
306
+ }
307
+ });
308
+
309
+ // ../twin-gmail/dist/src/check-labels.js
310
+ var labelExists = defineCheck({
311
+ id: "gmail.label-exists",
312
+ description: "Asks whether the mailbox defines a label with this DISPLAY NAME, compared case-insensitively the way the twin's own seeder keys its uniqueness lookup. It reads the `labels` collection only \u2014 a label that exists but has been applied to nothing still passes, and a message carrying a label is `gmail.message-has-label`'s question, not this one. It deliberately does NOT match on the minted id: `Label_follow_up` is an id and `Follow Up` is the name of that same label, and letting one sentence mean both would make the assertion unreadable.",
313
+ // The name slot admits spaces, which is what the corpus needs (`Parity
314
+ // Complete`) and what separates this template from `gmail.message-has-label`'s
315
+ // id slot. The two cannot claim one sentence: this one says "A label named",
316
+ // that one says "Message ... has label".
317
+ template: "A label named {label} exists",
318
+ params: { label: labelName },
319
+ substrate: "final",
320
+ // An achievement — the seed defines no `Parity Complete`, so the
321
+ // examinee has to create it.
322
+ polarity: () => "positive",
323
+ // The name is a caller-supplied literal compared against state.
324
+ subject: ({ label }) => label,
325
+ vacuityMutant: (args) => ({ ...args, label: VACUITY_SENTINEL }),
326
+ discriminatingWorlds: ({ label }) => ({
327
+ // Both worlds carry a non-empty `labels` collection, so the failing world
328
+ // fails on the ASSERTION (`label_not_found`) rather than reproducing the
329
+ // `state_incomplete` an empty world gives — the degenerate arm the probe
330
+ // rejects.
331
+ passing: finalWorld(gmailState({ labels: [systemLabel("INBOX"), userLabel("Label_1", label)] })),
332
+ failing: finalWorld(gmailState({ labels: [systemLabel("INBOX")] }))
333
+ }),
334
+ evaluate({ label }, { final }) {
335
+ const found = resolveLabelByName(final, label);
336
+ if ("missing" in found) {
337
+ if (found.missing.startsWith("label_not_found")) {
338
+ return {
339
+ passed: false,
340
+ reason: `no label named "${label}" exists`,
341
+ // The label COLLECTION — the honest citation for a lookup that found
342
+ // nothing, and the arm a reader most wants to open: see for yourself
343
+ // that the name is not in it.
344
+ ...found.searched === void 0 ? {} : { evidenceStatePaths: [found.searched] }
345
+ };
346
+ }
347
+ return missSkip(found);
348
+ }
349
+ return {
350
+ passed: true,
351
+ reason: `label "${label}" exists (id ${found.found.id ?? "?"})`,
352
+ evidenceStatePaths: [found.path]
353
+ };
354
+ }
355
+ });
356
+
357
+ // ../twin-gmail/dist/src/check-messages.js
358
+ var messageHasLabel = defineCheck({
359
+ id: "gmail.message-has-label",
360
+ description: "Looks the message up by id, then asks the `messageLabels` JOIN whether any row links it to the named label. The label may be given as its minted id (`Label_follow_up`) or as the display name of the same label (`Follow Up`); a system label carries both as one string (`INBOX`, `STARRED`). It asserts nothing about the message's OTHER labels \u2014 a message carrying the named label plus five more passes. A message the export does not carry is a SKIP, not a fail, and so is a label collection the export truncated: absent from a capped list is not absent.",
361
+ template: "Message {message} has label {label}",
362
+ params: { message: messageId, label: labelRef },
363
+ substrate: "final",
364
+ // An achievement. The seed leaves `msg_support` on INBOX/UNREAD only, so the
365
+ // examinee has to act for this to hold.
366
+ polarity: () => "positive",
367
+ // The label is a caller-supplied literal compared against state, so it is
368
+ // declared. No first-party redactor pattern touches a Gmail label id, which is
369
+ // exactly why declaring it keeps that a verified fact rather than an
370
+ // assumption — a team's own redaction config is not first-party.
371
+ subject: ({ label }) => label,
372
+ // The message id only SELECTS; falsifying it moves the verdict through the
373
+ // lookup rather than the assertion. The label is the scanned literal.
374
+ vacuityMutant: (args) => ({ ...args, label: VACUITY_SENTINEL }),
375
+ discriminatingWorlds: ({ message: id2, label }) => {
376
+ const labels = [systemLabel("INBOX"), userLabel(label, label)];
377
+ const base = { messages: [message(id2)], labels };
378
+ return {
379
+ passing: finalWorld(gmailState({ ...base, messageLabels: [messageLabel(id2, "INBOX"), messageLabel(id2, label)] })),
380
+ failing: finalWorld(gmailState({ ...base, messageLabels: [messageLabel(id2, "INBOX")] }))
381
+ };
382
+ },
383
+ evaluate({ message: id2, label }, { final }) {
384
+ const found = resolveMessage(final, id2);
385
+ if ("missing" in found)
386
+ return missSkip(found);
387
+ const carried = messageCarriesLabel(final, id2, label);
388
+ if ("missing" in carried)
389
+ return missSkip(carried);
390
+ return {
391
+ passed: carried.found,
392
+ reason: carried.found ? `message ${id2} carries label ${label}` : `message ${id2} does not carry label ${label}`,
393
+ // BOTH halves of the lookup, because the predicate really does read two
394
+ // places: the message row it resolved by id, and the join table it then
395
+ // questioned. Labels are not nested under their message in this export —
396
+ // that is the whole reason `messageCarriesLabel` exists — so one pointer
397
+ // could not say where this verdict came from.
398
+ evidenceStatePaths: [found.path, carried.path]
399
+ };
400
+ }
401
+ });
402
+ var mailboxLabelCount = defineCheck({
403
+ id: "gmail.mailbox-label-count",
404
+ description: "Counts the messages in the named mailbox that the `messageLabels` JOIN links to the named label, and asserts the total is EXACTLY the number given. Not 'at least' \u2014 a mailbox with six SENT messages fails a criterion asking for five, which is what makes it able to catch a duplicate send. A mailbox the export lists but does not contain is a SKIP; so is a truncated collection.",
405
+ template: "The mailbox `{mailbox}` has exactly {count} messages labeled {label}",
406
+ params: { mailbox: mailboxRef, count: exactCount, label: labelRef },
407
+ substrate: "final",
408
+ // The count carries the direction: asserting zero is a prohibition, any
409
+ // other count is work the examinee must do. Preserved from the legacy rule,
410
+ // which computed the same thing per match.
411
+ polarity: ({ count }) => Number(count) === 0 ? "negative" : "positive",
412
+ subject: ({ label }) => label,
413
+ // The mailbox resolves and the count is compared to a derived total; the
414
+ // label is the literal actually scanned in state. Pointing the mutant at the
415
+ // count would falsify a threshold rather than a lookup — see
416
+ // `gmail.draft-count-at-least`, where the count IS the only slot.
417
+ vacuityMutant: (args) => ({ ...args, label: VACUITY_SENTINEL }),
418
+ discriminatingWorlds: ({ mailbox, count, label }) => {
419
+ const wanted = Number(count);
420
+ const world = (n) => {
421
+ const messages = Array.from({ length: n }, (_, i) => message(`msg_${i}`, { mailboxEmail: mailbox }));
422
+ return gmailState({
423
+ mailboxes: [{ email: mailbox }],
424
+ messages,
425
+ labels: [systemLabel(label)],
426
+ messageLabels: messages.map((m) => ({ mailboxEmail: mailbox, messageId: m.id, labelId: label }))
427
+ });
428
+ };
429
+ return { passing: finalWorld(world(wanted)), failing: finalWorld(world(wanted + 1)) };
430
+ },
431
+ evaluate({ mailbox, count, label }, { final }) {
432
+ const wanted = Number(count);
433
+ if (final.messages == null || final.messageLabels == null) {
434
+ return { passed: false, status: "skipped", reason: "state_incomplete" };
435
+ }
436
+ if (isTruncated(final, "messages") || isTruncated(final, "messageLabels")) {
437
+ return { passed: false, status: "skipped", reason: "collection_truncated" };
438
+ }
439
+ if (final.mailboxes != null && !final.mailboxes.some((mb) => (mb.email ?? "").toLowerCase() === mailbox.toLowerCase())) {
440
+ return { passed: false, status: "skipped", reason: `mailbox_not_found ("${mailbox}")` };
441
+ }
442
+ const ids = labelIdsFor(final, label);
443
+ const labeled = new Set(final.messageLabels.filter((row) => ids.has((row.labelId ?? "").toLowerCase())).map((row) => (row.messageId ?? "").toLowerCase()));
444
+ const total = final.messages.filter((msg) => (msg.mailboxEmail ?? "").toLowerCase() === mailbox.toLowerCase() && msg.id != null && labeled.has(msg.id.toLowerCase())).length;
445
+ return {
446
+ passed: total === wanted,
447
+ reason: `mailbox "${mailbox}" has ${total} message(s) labeled ${label} (wanted ${wanted})`,
448
+ // The two collections the count is computed FROM. The count itself lives
449
+ // nowhere in the tree, so there is no narrower honest address — and the
450
+ // guards above already established that both collections are present and
451
+ // un-capped, which is what makes these pointers resolve.
452
+ evidenceStatePaths: [MESSAGES_PATH, MESSAGE_LABELS_PATH]
453
+ };
454
+ }
455
+ });
456
+ var oneMessagePerRecipient = defineCheck({
457
+ id: "gmail.one-message-per-recipient",
458
+ description: "Flattens every addressee across the messages carrying the named label and asserts each address appears EXACTLY ONCE \u2014 and, when a count is named, that there are that many messages, that many distinct recipients, and no more addressee slots than that. It is the assertion that separates 'sent to everyone' from 'sent to everyone, some of them twice', which an exact total alone cannot do.",
459
+ template: "Exactly one {label} message is addressed to each of the {count} recipients",
460
+ params: { label: labelRef, count: countWord },
461
+ substrate: "final",
462
+ polarity: () => "positive",
463
+ subject: ({ label }) => label,
464
+ // The label is scanned; the count is a declared arity compared to a derived
465
+ // one. Same argument as `mailboxLabelCount`.
466
+ vacuityMutant: (args) => ({ ...args, label: VACUITY_SENTINEL }),
467
+ discriminatingWorlds: ({ label, count }) => {
468
+ const wanted = parseCount(count) ?? 1;
469
+ const recipients = Array.from({ length: wanted }, (_, i) => `user${i}@example.com`);
470
+ const build = (addressees) => {
471
+ const messages = addressees.map((to, i) => message(`msg_${i}`, { to }));
472
+ return finalWorld(gmailState({
473
+ messages,
474
+ labels: [systemLabel(label)],
475
+ messageLabels: messages.map((m) => messageLabel(m.id, label))
476
+ }));
477
+ };
478
+ return {
479
+ passing: build(recipients.map((r) => [r])),
480
+ // The DUPLICATE SEND, not a missing one: the same number of messages, one
481
+ // recipient served twice and another not at all. A world with fewer
482
+ // messages would fail on arity, which the exact-count check already covers.
483
+ failing: build([[recipients[0]], ...recipients.slice(2).map((r) => [r]), [recipients[0]]])
484
+ };
485
+ },
486
+ evaluate({ label, count }, { final }) {
487
+ const wanted = parseCount(count);
488
+ if (final.messages == null || final.messageLabels == null) {
489
+ return { passed: false, status: "skipped", reason: "state_incomplete" };
490
+ }
491
+ if (isTruncated(final, "messages") || isTruncated(final, "messageLabels")) {
492
+ return { passed: false, status: "skipped", reason: "collection_truncated" };
493
+ }
494
+ const ids = labelIdsFor(final, label);
495
+ const labeled = new Set(final.messageLabels.filter((row) => ids.has((row.labelId ?? "").toLowerCase())).map((row) => (row.messageId ?? "").toLowerCase()));
496
+ const sent = final.messages.filter((msg) => msg.id != null && labeled.has(msg.id.toLowerCase()));
497
+ const addressees = sent.flatMap((msg) => (msg.to ?? []).map((to) => to.toLowerCase()));
498
+ const distinct = new Set(addressees);
499
+ const noDuplicates = addressees.length === distinct.size;
500
+ const arityOk = wanted == null ? sent.length === distinct.size : sent.length === wanted && distinct.size === wanted && addressees.length === wanted;
501
+ const passed = noDuplicates && arityOk;
502
+ return {
503
+ passed,
504
+ reason: passed ? `${sent.length} ${label} message(s), one per distinct recipient, no duplicates` : `${sent.length} ${label} message(s) to ${distinct.size} distinct recipient(s) (${addressees.length} addressee slot(s)${wanted == null ? "" : `, wanted ${wanted}`}${noDuplicates ? "" : ", duplicate send detected"})`,
505
+ // Same two collections, same reason: the duplicate this check exists to
506
+ // catch is a property of the addressee lists across `messages`, filtered
507
+ // by `messageLabels`, and neither the flattened list nor its distinct set
508
+ // is anything the tree holds.
509
+ evidenceStatePaths: [MESSAGES_PATH, MESSAGE_LABELS_PATH]
510
+ };
511
+ }
512
+ });
513
+
514
+ // ../twin-gmail/dist/src/check-tape.js
515
+ var noUnsupportedEndpoint = defineCheck({
516
+ id: "gmail.no-unsupported-endpoint",
517
+ description: 'Scans the recorded call tape for any request the twin answered with fidelity "unsupported" \u2014 a route it does not implement, answered 501. It asserts nothing about whether the run SUCCEEDED, and nothing about calls that were merely rejected: a 404 or a 422 from a route the twin does implement is a semantic answer and passes this check. The tape is scoped to this twin by the engine before the check sees it, so an unsupported call to a DIFFERENT twin in a multi-twin session cannot fail it \u2014 which matters here, because a single task may run gmail and github together.',
518
+ // No slots, and no twin word. The legacy cloud rule accepted "No unsupported
519
+ // Gmail endpoint was called" alongside the bare form, plus plural and `were`
520
+ // variants, because an author typed English. Under position 2 an author PICKS
521
+ // the check, so those variants are retired rather than ported — the same
522
+ // decision `github.no-unsupported-endpoint` made, and this is now the same
523
+ // sentence on both twins, resolved per-twin by the engine.
524
+ template: "No unsupported endpoint was called",
525
+ params: {},
526
+ substrate: "tape",
527
+ // A prohibition. Nothing is required to happen; only the examinee reaching for
528
+ // an unimplemented route can break it.
529
+ polarity: () => "negative",
530
+ // No caller-supplied literal is hunted for in any substrate, so there is
531
+ // nothing a redactor could silently delete out from under this check.
532
+ subject: () => null,
533
+ // No slots, so the sentence carries no literal to falsify. The trigger is "a
534
+ // call with fidelity=unsupported exists", which lives on the tape and not in
535
+ // the sentence. Reported as `no_trigger`, never as clean.
536
+ vacuityMutant: () => null,
537
+ discriminatingWorlds: () => ({
538
+ passing: tapeWorld([
539
+ {
540
+ twin: "gmail",
541
+ method: "GET",
542
+ path: "/gmail/v1/users/me/messages",
543
+ status: 200,
544
+ fidelity: "semantic",
545
+ event_id: "evt_ok"
546
+ }
547
+ ]),
548
+ failing: tapeWorld([
549
+ {
550
+ twin: "gmail",
551
+ method: "POST",
552
+ path: "/gmail/v1/users/me/watch",
553
+ status: 501,
554
+ fidelity: "unsupported",
555
+ event_id: "evt_bad"
556
+ }
557
+ ])
558
+ }),
559
+ evaluate(_args, { tape }) {
560
+ if (tape === null)
561
+ return { passed: false, reason: "tape_missing", status: "skipped" };
562
+ const unsupported = tape.filter((event) => event.fidelity === "unsupported");
563
+ if (unsupported.length === 0) {
564
+ return {
565
+ passed: true,
566
+ reason: `no unsupported Gmail endpoint was called (${tape.length} call(s) inspected)`
567
+ };
568
+ }
569
+ const evidenceEventIds = unsupported.map((event) => event.event_id).filter((id2) => typeof id2 === "string" && id2 !== "");
570
+ const outcome = {
571
+ passed: false,
572
+ reason: `${unsupported.length} unsupported Gmail call(s): [${unsupported.map((event) => event.path ?? "?").join(", ")}]`
573
+ };
574
+ return evidenceEventIds.length > 0 ? { ...outcome, evidenceEventIds } : outcome;
575
+ }
576
+ });
577
+
578
+ // ../twin-gmail/dist/src/checks.js
579
+ var GMAIL_CHECKS = [
580
+ messageHasLabel,
581
+ labelExists,
582
+ draftAddressedTo2,
583
+ draftCountAtLeast,
584
+ mailboxLabelCount,
585
+ oneMessagePerRecipient,
586
+ // Last, because these are the only ones that assert about the RUN rather than
587
+ // the world it left behind — twin-github orders its tape checks the same way.
588
+ noUnsupportedEndpoint
589
+ ];
590
+ var GmailError = class extends Error {
591
+ status;
592
+ reason;
593
+ constructor(status, reason, message2) {
594
+ super(message2);
595
+ this.status = status;
596
+ this.reason = reason;
597
+ this.name = "GmailError";
598
+ }
599
+ };
600
+ function invalidArgument(message2) {
601
+ throw new GmailError(400, "invalidArgument", message2);
602
+ }
603
+
604
+ // ../twin-gmail/dist/src/faults.js
605
+ var KNOWN_FAULT_NAMES = ["rate-limited"];
606
+ var gmailFaultSchema = z.object({
607
+ name: z.enum(KNOWN_FAULT_NAMES),
608
+ target: z.string().min(1).max(128).default("messages.send"),
609
+ succeedFirst: z.number().int().nonnegative().max(1e3).default(2),
610
+ throttleFor: z.number().int().positive().max(1e3).default(3),
611
+ retryAfterSeconds: z.number().int().positive().max(3600).default(1)
612
+ }).strict();
613
+
614
+ // ../twin-gmail/dist/src/search-parse.js
615
+ var MAX_QUERY_BYTES = 4096;
616
+ var MAX_TOKENS = 256;
617
+ var MAX_DEPTH = 20;
618
+ var MAX_BRANCHES = 256;
619
+ var KNOWN_FIELDS = /* @__PURE__ */ new Set([
620
+ "from",
621
+ "to",
622
+ "cc",
623
+ "bcc",
624
+ "deliveredto",
625
+ "list",
626
+ "subject",
627
+ "rfc822msgid",
628
+ "filename",
629
+ "after",
630
+ "newer",
631
+ "before",
632
+ "older",
633
+ "newer_than",
634
+ "older_than",
635
+ "size",
636
+ "larger",
637
+ "smaller",
638
+ "label",
639
+ "category",
640
+ "in",
641
+ "is",
642
+ "has"
643
+ ]);
644
+ var CATEGORY_LABELS = {
645
+ primary: "CATEGORY_PERSONAL",
646
+ personal: "CATEGORY_PERSONAL",
647
+ social: "CATEGORY_SOCIAL",
648
+ promotions: "CATEGORY_PROMOTIONS",
649
+ updates: "CATEGORY_UPDATES",
650
+ forums: "CATEGORY_FORUMS"
651
+ };
652
+ function validateSearchQuery(query) {
653
+ const node = parseSearchQuery(query);
654
+ const walk = (current) => {
655
+ switch (current.type) {
656
+ case "and":
657
+ case "or":
658
+ for (const child of current.children)
659
+ walk(child);
660
+ return;
661
+ case "not":
662
+ walk(current.child);
663
+ return;
664
+ case "around":
665
+ return;
666
+ case "term":
667
+ assertSearchTerm(current.field, current.value);
668
+ return;
669
+ }
670
+ };
671
+ walk(node);
672
+ return node;
673
+ }
674
+ function assertSearchTerm(field, value) {
675
+ if (!field)
676
+ return;
677
+ if (!KNOWN_FIELDS.has(field)) {
678
+ invalidArgument(`Unsupported search operator: ${field}`);
679
+ }
680
+ if (field === "category") {
681
+ if (!CATEGORY_LABELS[value.toLocaleLowerCase("en-US")]) {
682
+ invalidArgument(`Unsupported search category: ${value}`);
683
+ }
684
+ return;
685
+ }
686
+ if (field === "after" || field === "newer" || field === "before" || field === "older") {
687
+ parseDate(value);
688
+ return;
689
+ }
690
+ if (field === "newer_than" || field === "older_than") {
691
+ parseDuration(value);
692
+ return;
693
+ }
694
+ if (field === "size" || field === "larger" || field === "smaller") {
695
+ parseSize(value);
696
+ return;
697
+ }
698
+ if (field === "has") {
699
+ assertHasOperator(value);
700
+ }
701
+ }
702
+ function parseSearchQuery(query) {
703
+ if (Buffer.byteLength(query) > MAX_QUERY_BYTES)
704
+ invalidArgument("Search query exceeds limit");
705
+ const tokens = tokenize(query);
706
+ if (tokens.length > MAX_TOKENS)
707
+ invalidArgument("Search query has too many tokens");
708
+ if (tokens.length === 0)
709
+ return { type: "and", children: [] };
710
+ let position = 0;
711
+ const parseExpression = (depth, stop) => {
712
+ if (depth > MAX_DEPTH)
713
+ invalidArgument("Search query nesting exceeds limit");
714
+ const alternatives = [];
715
+ let conjunction = [];
716
+ const flush = () => {
717
+ alternatives.push(conjunction.length === 1 ? conjunction[0] : { type: "and", children: conjunction });
718
+ conjunction = [];
719
+ };
720
+ while (position < tokens.length) {
721
+ const token = tokens[position];
722
+ if (stop && token.value === stop)
723
+ break;
724
+ if (token.value.toUpperCase() === "OR") {
725
+ position++;
726
+ if (!conjunction.length)
727
+ invalidArgument("OR requires a left expression");
728
+ flush();
729
+ continue;
730
+ }
731
+ if (token.value.toUpperCase() === "AND") {
732
+ position++;
733
+ continue;
734
+ }
735
+ conjunction.push(parsePrimary(depth + 1));
736
+ }
737
+ if (conjunction.length)
738
+ flush();
739
+ if (!alternatives.length)
740
+ return { type: "and", children: [] };
741
+ return alternatives.length === 1 ? alternatives[0] : { type: "or", children: alternatives };
742
+ };
743
+ const parsePrimary = (depth) => {
744
+ const token = tokens[position++];
745
+ if (token.value === "(" || token.value === "{") {
746
+ const end = token.value === "(" ? ")" : "}";
747
+ const child = parseExpression(depth, end);
748
+ if (tokens[position]?.value !== end)
749
+ invalidArgument(`Unclosed ${token.value}`);
750
+ position++;
751
+ return token.value === "{" ? makeImplicitOr(child) : child;
752
+ }
753
+ if (token.value === ")" || token.value === "}")
754
+ invalidArgument(`Unexpected ${token.value}`);
755
+ let value = token.value;
756
+ let negate = false;
757
+ if (value === "-") {
758
+ negate = true;
759
+ const next = tokens[position++];
760
+ if (!next)
761
+ invalidArgument("Negation requires an expression");
762
+ position--;
763
+ const child = parsePrimary(depth + 1);
764
+ return { type: "not", child };
765
+ }
766
+ if (value.startsWith("-") && value.length > 1) {
767
+ negate = true;
768
+ value = value.slice(1);
769
+ }
770
+ const around = tokens[position]?.value.toUpperCase() === "AROUND";
771
+ if (around) {
772
+ position++;
773
+ const distance = Number(tokens[position++]?.value);
774
+ const right = tokens[position++]?.value;
775
+ if (!Number.isInteger(distance) || distance < 1 || distance > 100 || !right) {
776
+ invalidArgument("AROUND requires a distance and right term");
777
+ }
778
+ const node2 = { type: "around", left: value, right, distance };
779
+ return negate ? { type: "not", child: node2 } : node2;
780
+ }
781
+ const colon = value.indexOf(":");
782
+ let node;
783
+ if (colon > 0) {
784
+ const field = value.slice(0, colon).toLowerCase();
785
+ let fieldValue = value.slice(colon + 1);
786
+ if (!fieldValue && (tokens[position]?.value === "(" || tokens[position]?.value === "{")) {
787
+ node = parseFieldGroup(field, depth + 1);
788
+ return negate ? { type: "not", child: node } : node;
789
+ }
790
+ if (!fieldValue && tokens[position] && !["AND", "OR", ")", "}"].includes(tokens[position].value.toUpperCase())) {
791
+ fieldValue = tokens[position++].value;
792
+ }
793
+ node = { type: "term", field, value: fieldValue, exact: token.quoted };
794
+ } else {
795
+ node = { type: "term", value: value.startsWith("+") ? value.slice(1) : value, exact: token.quoted || value.startsWith("+") };
796
+ }
797
+ return negate ? { type: "not", child: node } : node;
798
+ };
799
+ const parseFieldGroup = (field, depth) => {
800
+ if (depth > MAX_DEPTH)
801
+ invalidArgument("Search query nesting exceeds limit");
802
+ const opening = tokens[position++].value;
803
+ const end = opening === "(" ? ")" : "}";
804
+ const groups = [[]];
805
+ while (position < tokens.length && tokens[position].value !== end) {
806
+ const token = tokens[position++];
807
+ if (token.value.toUpperCase() === "OR") {
808
+ groups.push([]);
809
+ continue;
810
+ }
811
+ if (token.value.toUpperCase() === "AND")
812
+ continue;
813
+ if (["(", "{", ")", "}"].includes(token.value))
814
+ invalidArgument("Nested field groups are unsupported");
815
+ let value = token.value;
816
+ let negate = false;
817
+ if (value.startsWith("-")) {
818
+ negate = true;
819
+ value = value.slice(1);
820
+ }
821
+ const term = { type: "term", field, value, exact: token.quoted };
822
+ groups.at(-1).push(negate ? { type: "not", child: term } : term);
823
+ }
824
+ if (tokens[position]?.value !== end)
825
+ invalidArgument(`Unclosed ${opening}`);
826
+ position++;
827
+ const nodes = groups.map((children) => children.length === 1 ? children[0] : { type: "and", children });
828
+ const useOr = opening === "{" || nodes.length > 1;
829
+ return useOr ? { type: "or", children: nodes } : nodes[0] ?? { type: "and", children: [] };
830
+ };
831
+ const root = parseExpression(0);
832
+ if (position !== tokens.length)
833
+ invalidArgument("Unexpected search token");
834
+ if (countBranches(root) > MAX_BRANCHES)
835
+ invalidArgument("Search query has too many branches");
836
+ return root;
837
+ }
838
+ function assertHasOperator(value) {
839
+ const normalized = value.toLocaleLowerCase("en-US");
840
+ if (normalized.endsWith("-star")) {
841
+ invalidArgument(`Unsupported colored-star operator: has:${value}; twin maps only STARRED via is:starred`);
842
+ }
843
+ }
844
+ function parseDate(value) {
845
+ const normalized = /^\d{4}\/\d{1,2}\/\d{1,2}$/.test(value) ? value.replaceAll("/", "-") : value;
846
+ const date = Date.parse(`${normalized}${/^\d{4}-\d/.test(normalized) ? "T00:00:00Z" : ""}`);
847
+ if (Number.isNaN(date))
848
+ invalidArgument(`Invalid search date: ${value}`);
849
+ return date;
850
+ }
851
+ function parseDuration(value) {
852
+ const match = value.match(/^(\d+)([dmy])$/i);
853
+ if (!match)
854
+ invalidArgument(`Invalid search duration: ${value}`);
855
+ const units = { d: 864e5, m: 30 * 864e5, y: 365 * 864e5 };
856
+ return Number(match[1]) * units[match[2].toLowerCase()];
857
+ }
858
+ function parseSize(value) {
859
+ const match = value.match(/^(\d+(?:\.\d+)?)([kmg])?$/i);
860
+ if (!match)
861
+ invalidArgument(`Invalid search size: ${value}`);
862
+ const scale = { k: 1024, m: 1024 ** 2, g: 1024 ** 3 };
863
+ return Math.floor(Number(match[1]) * (match[2] ? scale[match[2].toLowerCase()] : 1));
864
+ }
865
+ function countBranches(node) {
866
+ switch (node.type) {
867
+ case "and":
868
+ case "or":
869
+ return node.children.reduce((sum, child) => sum + countBranches(child), 0);
870
+ case "not":
871
+ return countBranches(node.child);
872
+ case "around":
873
+ case "term":
874
+ return 1;
875
+ }
876
+ }
877
+ function tokenize(query) {
878
+ const out = [];
879
+ let index = 0;
880
+ while (index < query.length) {
881
+ if (/\s/.test(query[index])) {
882
+ index++;
883
+ continue;
884
+ }
885
+ const char = query[index];
886
+ if ("(){}".includes(char)) {
887
+ out.push({ value: char });
888
+ index++;
889
+ continue;
890
+ }
891
+ let value = "";
892
+ let quoted = false;
893
+ while (index < query.length && !/\s/.test(query[index]) && !"(){}".includes(query[index])) {
894
+ if (query[index] === '"') {
895
+ quoted = true;
896
+ index++;
897
+ while (index < query.length && query[index] !== '"') {
898
+ if (query[index] === "\\" && index + 1 < query.length)
899
+ index++;
900
+ value += query[index++];
901
+ }
902
+ if (query[index] !== '"')
903
+ invalidArgument("Unclosed search quote");
904
+ index++;
905
+ } else {
906
+ value += query[index++];
907
+ }
908
+ }
909
+ if (value)
910
+ out.push({ value, quoted });
911
+ }
912
+ return out;
913
+ }
914
+ function makeImplicitOr(node) {
915
+ return node.type === "and" ? { type: "or", children: node.children } : node;
916
+ }
917
+
918
+ // ../twin-gmail/dist/src/seed.js
919
+ var email = z.string().trim().email().transform((value) => value.toLowerCase());
920
+ var id = z.string().min(1).max(128).regex(/^[A-Za-z0-9_-]+$/);
921
+ var attachmentSchema = z.object({
922
+ filename: z.string().max(512),
923
+ mimeType: z.string().min(1).max(255).default("application/octet-stream"),
924
+ disposition: z.enum(["attachment", "inline"]).default("attachment"),
925
+ contentId: z.string().max(998).optional(),
926
+ data: z.string().max(5e7)
927
+ }).strict();
928
+ var messageFields = {
929
+ id: id.optional(),
930
+ threadId: id.optional(),
931
+ raw: z.string().max(5e7).optional(),
932
+ from: email.optional(),
933
+ to: z.array(email).max(500).default([]),
934
+ cc: z.array(email).max(500).default([]),
935
+ bcc: z.array(email).max(500).default([]),
936
+ subject: z.string().max(998).default(""),
937
+ text: z.string().max(25e6).default(""),
938
+ html: z.string().max(25e6).default(""),
939
+ date: z.string().datetime({ offset: true }).optional(),
940
+ messageId: z.string().min(3).max(998).optional(),
941
+ inReplyTo: z.string().max(998).optional(),
942
+ references: z.array(z.string().max(998)).max(100).default([]),
943
+ attachments: z.array(attachmentSchema).max(100).default([])
944
+ };
945
+ var messageSchema = z.object({
946
+ ...messageFields,
947
+ labels: z.array(z.string().min(1).max(255)).max(100).default([])
948
+ }).strict();
949
+ var draftSchema = z.object(messageFields).strict();
950
+ var labelSchema = z.object({
951
+ id: id.optional(),
952
+ name: z.string().trim().min(1).max(225),
953
+ color: z.object({
954
+ textColor: z.string().max(32).optional(),
955
+ backgroundColor: z.string().max(32).optional()
956
+ }).strict().optional()
957
+ }).strict();
958
+ var filterSchema = z.object({
959
+ id: id.optional(),
960
+ criteria: z.object({
961
+ from: z.string().max(998).optional(),
962
+ to: z.string().max(998).optional(),
963
+ subject: z.string().max(998).optional(),
964
+ query: z.string().max(4096).optional(),
965
+ negatedQuery: z.string().max(4096).optional(),
966
+ hasAttachment: z.boolean().optional(),
967
+ excludeChats: z.boolean().optional(),
968
+ size: z.number().int().nonnegative().optional(),
969
+ sizeComparison: z.enum(["larger", "smaller"]).optional()
970
+ }).strict().default({}),
971
+ action: z.object({
972
+ addLabelIds: z.array(z.string().min(1)).max(100).default([]),
973
+ removeLabelIds: z.array(z.string().min(1)).max(100).default([]),
974
+ forward: email.optional()
975
+ }).strict().default({ addLabelIds: [], removeLabelIds: [] })
976
+ }).strict();
977
+ var sendAsSchema = z.object({
978
+ sendAsEmail: email,
979
+ displayName: z.string().max(256).default(""),
980
+ replyToAddress: email.optional(),
981
+ isPrimary: z.boolean().default(false),
982
+ isDefault: z.boolean().default(false),
983
+ verificationStatus: z.enum(["accepted", "pending"]).default("accepted")
984
+ }).strict();
985
+ var mailboxSchema = z.object({
986
+ email,
987
+ displayName: z.string().max(256).default(""),
988
+ labels: z.array(labelSchema).max(5e3).default([]),
989
+ messages: z.array(messageSchema).max(1e4).default([]),
990
+ drafts: z.array(draftSchema).max(5e3).default([]),
991
+ filters: z.array(filterSchema).max(1e3).default([]),
992
+ forwardingAddresses: z.array(z.object({
993
+ forwardingEmail: email,
994
+ verificationStatus: z.enum(["accepted", "pending"]).default("pending")
995
+ }).strict()).max(1e3).default([]),
996
+ sendAs: z.array(sendAsSchema).max(1e3).default([])
997
+ }).strict();
998
+ var gmailSeedSchema = z.object({
999
+ primaryMailbox: mailboxSchema,
1000
+ mailboxes: z.array(mailboxSchema).max(100).default([]),
1001
+ deliveryMode: z.enum(["sender-only", "seeded-mailboxes"]).default("sender-only"),
1002
+ clock: z.string().datetime({ offset: true }).default("2025-01-01T00:00:00.000Z"),
1003
+ faults: z.array(gmailFaultSchema).max(50).default([])
1004
+ }).strict().superRefine((seed, ctx) => {
1005
+ const emails = [seed.primaryMailbox.email, ...seed.mailboxes.map((mailbox) => mailbox.email)];
1006
+ const seen = /* @__PURE__ */ new Set();
1007
+ for (const mailboxEmail of emails) {
1008
+ if (seen.has(mailboxEmail)) {
1009
+ ctx.addIssue({ code: "custom", message: `Duplicate mailbox: ${mailboxEmail}` });
1010
+ }
1011
+ seen.add(mailboxEmail);
1012
+ }
1013
+ for (const mailbox of [seed.primaryMailbox, ...seed.mailboxes]) {
1014
+ const labelNames = /* @__PURE__ */ new Set();
1015
+ for (const label of mailbox.labels) {
1016
+ const key = label.name.toLowerCase();
1017
+ if (labelNames.has(key)) {
1018
+ ctx.addIssue({ code: "custom", message: `Duplicate label in ${mailbox.email}: ${label.name}` });
1019
+ }
1020
+ labelNames.add(key);
1021
+ }
1022
+ for (const filter of mailbox.filters) {
1023
+ if (filter.action.forward) {
1024
+ ctx.addIssue({
1025
+ code: "custom",
1026
+ message: `Filter forwarding is unsupported: ${mailbox.email}`
1027
+ });
1028
+ }
1029
+ for (const key of ["query", "negatedQuery"]) {
1030
+ const value = filter.criteria[key];
1031
+ if (!value)
1032
+ continue;
1033
+ try {
1034
+ validateSearchQuery(value);
1035
+ } catch (error) {
1036
+ ctx.addIssue({
1037
+ code: "custom",
1038
+ message: `Invalid filter ${key} in ${mailbox.email}: ${error.message}`
1039
+ });
1040
+ }
1041
+ }
1042
+ }
1043
+ }
1044
+ });
1045
+ function parseSeed(input) {
1046
+ return gmailSeedSchema.parse(input);
1047
+ }
1048
+ var DEFAULT_GMAIL_AGENT_EMAIL = "pome-agent@pome-twin.test";
1049
+ function agentPathInboxMailbox(email2 = DEFAULT_GMAIL_AGENT_EMAIL) {
1050
+ return {
1051
+ email: email2,
1052
+ displayName: "Pome Agent",
1053
+ labels: [
1054
+ { id: "Label_follow_up", name: "Follow Up" },
1055
+ { id: "Label_build", name: "Build" }
1056
+ ],
1057
+ messages: [
1058
+ {
1059
+ id: "msg_welcome",
1060
+ threadId: "thread_welcome",
1061
+ from: "welcome@pome-twin.test",
1062
+ to: [email2],
1063
+ subject: "Welcome to your Pome Gmail twin",
1064
+ text: "Your deterministic inbox is ready for agent testing.",
1065
+ html: "<p>Your deterministic inbox is ready for agent testing.</p>",
1066
+ date: "2026-07-18T09:00:00.000Z",
1067
+ messageId: "welcome@pome-twin.test",
1068
+ labels: ["INBOX"]
1069
+ },
1070
+ {
1071
+ id: "msg_build",
1072
+ threadId: "thread_build",
1073
+ from: "ci@example.com",
1074
+ to: [email2],
1075
+ subject: "Build failed on main",
1076
+ text: "The nightly build failed. See the attached log.",
1077
+ date: "2026-07-19T10:00:00.000Z",
1078
+ messageId: "build-001@example.com",
1079
+ labels: ["INBOX", "UNREAD", "Build"],
1080
+ attachments: [
1081
+ {
1082
+ filename: "build.log",
1083
+ mimeType: "text/plain",
1084
+ data: Buffer.from("BUILD FAILED step=test\n", "utf8").toString("base64")
1085
+ }
1086
+ ]
1087
+ },
1088
+ {
1089
+ id: "msg_build_reply",
1090
+ threadId: "thread_build",
1091
+ from: email2,
1092
+ to: ["ci@example.com"],
1093
+ subject: "Re: Build failed on main",
1094
+ text: "Looking into the failure now.",
1095
+ date: "2026-07-19T11:00:00.000Z",
1096
+ messageId: "build-reply@pome-twin.test",
1097
+ inReplyTo: "build-001@example.com",
1098
+ references: ["build-001@example.com"],
1099
+ labels: ["SENT"]
1100
+ },
1101
+ {
1102
+ id: "msg_support",
1103
+ threadId: "thread_support",
1104
+ from: "alice@example.com",
1105
+ to: [email2],
1106
+ subject: "Production export is stuck",
1107
+ text: "Our production export has been stuck for an hour. Can you investigate?",
1108
+ date: "2026-07-19T12:00:00.000Z",
1109
+ messageId: "support-001@example.com",
1110
+ labels: ["INBOX", "UNREAD"]
1111
+ }
1112
+ ],
1113
+ drafts: [
1114
+ {
1115
+ id: "draft_ack",
1116
+ threadId: "thread_draft_ack",
1117
+ to: ["bob@example.com"],
1118
+ subject: "Draft acknowledgment",
1119
+ text: "Thanks \u2014 I'll follow up shortly.",
1120
+ date: "2026-07-19T13:00:00.000Z",
1121
+ messageId: "draft-ack@pome-twin.test"
1122
+ }
1123
+ ],
1124
+ filters: [],
1125
+ forwardingAddresses: [],
1126
+ sendAs: []
1127
+ };
1128
+ }
1129
+ function defaultSeedState() {
1130
+ return {
1131
+ primaryMailbox: agentPathInboxMailbox(),
1132
+ mailboxes: [],
1133
+ deliveryMode: "sender-only",
1134
+ clock: "2026-07-20T00:00:00.000Z"
1135
+ };
1136
+ }
1137
+
1138
+ export { GMAIL_CHECKS, defaultSeedState, gmailSeedSchema, parseSeed };