@pome-sh/cli 0.21.4 → 0.21.6

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "package": "pome-sh",
3
- "version": "0.21.4",
4
- "git_sha": "94294d2f36dfd3204a980ba5c55e7a0fdcb5580a",
5
- "build_time": "2026-08-05T21:09:37.703Z"
3
+ "version": "0.21.6",
4
+ "git_sha": "ba15fcb28538fcf7b6e0717a9ec1b6ccf7a9bd10",
5
+ "build_time": "2026-08-06T00:27:44.532Z"
6
6
  }
@@ -183,7 +183,7 @@ async function checkTwinReachable(_configDir) {
183
183
  });
184
184
  let harness;
185
185
  try {
186
- const { bootTwin } = await import('./twinHarness-YTU4427G.js');
186
+ const { bootTwin } = await import('./twinHarness-KFZ2ZNAX.js');
187
187
  harness = await bootTwin({
188
188
  twin: "github",
189
189
  seedState: void 0,
@@ -1,8 +1,8 @@
1
1
  import { criterionSchema, finalizeResponseSchema, HostedDiscardRefusedError, HostedOrchError, HostedAuthError, HostedQuotaError, submitResultResponseSchema, createEvalSessionResponseSchema, createSessionResponseSchema, sessionPublicSchema } from './chunk-PQYIAA6K.js';
2
- import { seedSchema, parseSeed, defaultSeedState as defaultSeedState$2 } from './chunk-OKDJRKJV.js';
3
- import { gmailSeedSchema, defaultSeedState } from './chunk-WVWV7DLA.js';
4
- import { linearSeedSchema, defaultSeedState as defaultSeedState$1 } from './chunk-APAS34RJ.js';
5
- import { isTwinName, TWIN_REGISTRY } from './chunk-MBM7LY7E.js';
2
+ import { seedSchema, parseSeed, defaultSeedState as defaultSeedState$2 } from './chunk-SGDUD7KK.js';
3
+ import { gmailSeedSchema, defaultSeedState } from './chunk-NJ246QPJ.js';
4
+ import { linearSeedSchema, defaultSeedState as defaultSeedState$1 } from './chunk-ZKID2HS3.js';
5
+ import { isTwinName, TWIN_REGISTRY } from './chunk-A3IDZXJY.js';
6
6
  import { toTwinHttpEventRow } from './chunk-LZIPGCQJ.js';
7
7
  import { redactEvent, redactSecrets } from './chunk-SG6ZTIMT.js';
8
8
  import { mkdir, appendFile, writeFile, readFile } from 'node:fs/promises';
@@ -0,0 +1,590 @@
1
+ import { statePath, defineCheck, VACUITY_SENTINEL, childStatePath, VACUITY_SENTINEL_NUMBER } from './chunk-NY55QTVQ.js';
2
+
3
+ // ../packages/twin-gmail/dist/src/check-params.js
4
+ var messageId = {
5
+ name: "message",
6
+ pattern: "[A-Za-z0-9_-]{1,128}",
7
+ example: "msg_support",
8
+ render: (value) => value,
9
+ parse: (raw) => raw
10
+ };
11
+ var labelRef = {
12
+ name: "label",
13
+ pattern: "[A-Za-z0-9_-]{1,128}",
14
+ example: "STARRED",
15
+ render: (value) => value,
16
+ parse: (raw) => raw
17
+ };
18
+ var labelName = {
19
+ name: "label",
20
+ pattern: "[A-Za-z0-9][A-Za-z0-9 _-]{0,127}",
21
+ example: "Parity Complete",
22
+ render: (value) => value,
23
+ parse: (raw) => raw
24
+ };
25
+ var emailAddress = {
26
+ name: "email",
27
+ pattern: "[^\\s@\"'`]+@[^\\s@\"'`]+",
28
+ example: "alice@example.com",
29
+ render: (value) => value,
30
+ parse: (raw) => raw
31
+ };
32
+ var mailboxRef = {
33
+ name: "mailbox",
34
+ pattern: "[^\\s@\"'`]+@[^\\s@\"'`]+",
35
+ example: "pome-agent@pome-twin.test",
36
+ render: (value) => value,
37
+ parse: (raw) => raw
38
+ };
39
+ var exactCount = {
40
+ name: "count",
41
+ pattern: "\\d{1,9}",
42
+ example: "5",
43
+ render: (value) => value,
44
+ parse: (raw) => raw
45
+ };
46
+ var countWord = {
47
+ name: "count",
48
+ pattern: "(?:\\d{1,9}|zero|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)",
49
+ example: "two",
50
+ render: (value) => value,
51
+ parse: (raw) => raw
52
+ };
53
+ var NUMBER_WORDS = {
54
+ zero: 0,
55
+ one: 1,
56
+ two: 2,
57
+ three: 3,
58
+ four: 4,
59
+ five: 5,
60
+ six: 6,
61
+ seven: 7,
62
+ eight: 8,
63
+ nine: 9,
64
+ ten: 10,
65
+ eleven: 11,
66
+ twelve: 12
67
+ };
68
+ function parseCount(raw) {
69
+ const trimmed = raw.trim().toLowerCase();
70
+ if (/^\d+$/.test(trimmed))
71
+ return Number(trimmed);
72
+ return NUMBER_WORDS[trimmed] ?? null;
73
+ }
74
+
75
+ // ../packages/twin-gmail/dist/src/check-state.js
76
+ var MESSAGES_PATH = statePath("messages");
77
+ var DRAFTS_PATH = statePath("drafts");
78
+ var LABELS_PATH = statePath("labels");
79
+ var MESSAGE_LABELS_PATH = statePath("messageLabels");
80
+ function missSkip(miss) {
81
+ const outcome = { passed: false, status: "skipped", reason: miss.missing };
82
+ if (miss.searched === void 0)
83
+ return outcome;
84
+ return { ...outcome, evidenceStatePaths: [miss.searched] };
85
+ }
86
+ var lower = (value) => (value ?? "").toLowerCase();
87
+ function isTruncated(state, collection) {
88
+ return (state.exportBounds?.truncatedCollections ?? []).includes(collection);
89
+ }
90
+ function resolveMessage(state, id) {
91
+ if (state.messages == null)
92
+ return { missing: "state_incomplete" };
93
+ const indices = state.messages.map((message2, index) => lower(message2.id) === lower(id) ? index : -1).filter((index) => index >= 0);
94
+ if (indices.length > 1) {
95
+ return {
96
+ missing: `message_ambiguous ("${id}" exists in ${indices.length} mailboxes)`,
97
+ searched: MESSAGES_PATH
98
+ };
99
+ }
100
+ if (indices.length === 1) {
101
+ const index = indices[0];
102
+ return { found: state.messages[index], path: childStatePath(MESSAGES_PATH, index) };
103
+ }
104
+ if (isTruncated(state, "messages")) {
105
+ return { missing: 'collection_truncated ("messages")', searched: MESSAGES_PATH };
106
+ }
107
+ return { missing: `message_not_found ("${id}")`, searched: MESSAGES_PATH };
108
+ }
109
+ function labelIdsFor(state, wanted) {
110
+ const ids = /* @__PURE__ */ new Set([lower(wanted)]);
111
+ for (const label of state.labels ?? []) {
112
+ const matches = lower(label.name) === lower(wanted) || lower(label.id) === lower(wanted);
113
+ if (matches && label.id != null)
114
+ ids.add(lower(label.id));
115
+ }
116
+ return ids;
117
+ }
118
+ function messageCarriesLabel(state, messageId2, wanted) {
119
+ if (state.messageLabels == null)
120
+ return { missing: "state_incomplete" };
121
+ const ids = labelIdsFor(state, wanted);
122
+ const carried = state.messageLabels.some((row) => lower(row.messageId) === lower(messageId2) && ids.has(lower(row.labelId)));
123
+ if (!carried && isTruncated(state, "messageLabels")) {
124
+ return { missing: 'collection_truncated ("messageLabels")', searched: MESSAGE_LABELS_PATH };
125
+ }
126
+ return { found: carried, path: MESSAGE_LABELS_PATH };
127
+ }
128
+ function resolveLabelByName(state, name) {
129
+ if (state.labels == null)
130
+ return { missing: "state_incomplete" };
131
+ const index = state.labels.findIndex((label) => lower(label.name) === lower(name));
132
+ if (index >= 0) {
133
+ return { found: state.labels[index], path: childStatePath(LABELS_PATH, index) };
134
+ }
135
+ if (isTruncated(state, "labels")) {
136
+ return { missing: 'collection_truncated ("labels")', searched: LABELS_PATH };
137
+ }
138
+ return { missing: `label_not_found ("${name}")`, searched: LABELS_PATH };
139
+ }
140
+ function draftRecipients(state, draft2) {
141
+ const message2 = (state.messages ?? []).find((candidate) => lower(candidate.id) === lower(draft2.messageId));
142
+ if (!message2)
143
+ return [];
144
+ return [...message2.to ?? [], ...message2.cc ?? []];
145
+ }
146
+
147
+ // ../packages/twin-gmail/dist/src/check-worlds.js
148
+ var FIXTURE_MAILBOX = "pome-agent@pome-twin.test";
149
+ function gmailState(parts = {}) {
150
+ return {
151
+ mailboxes: [{ email: FIXTURE_MAILBOX }],
152
+ messages: [],
153
+ drafts: [],
154
+ labels: [],
155
+ messageLabels: [],
156
+ exportBounds: { messageBodiesOmitted: true, largeMailbox: false, truncatedCollections: [] },
157
+ ...parts
158
+ };
159
+ }
160
+ function finalWorld(final) {
161
+ return { seed: null, final, tape: null };
162
+ }
163
+ function tapeWorld(tape) {
164
+ return { seed: null, final: gmailState(), tape };
165
+ }
166
+ function message(id, parts = {}) {
167
+ return { mailboxEmail: FIXTURE_MAILBOX, id, to: [FIXTURE_MAILBOX], bodyOmitted: true, ...parts };
168
+ }
169
+ function systemLabel(name) {
170
+ return { mailboxEmail: FIXTURE_MAILBOX, id: name, name, type: "system" };
171
+ }
172
+ function userLabel(id, name) {
173
+ return { mailboxEmail: FIXTURE_MAILBOX, id, name, type: "user" };
174
+ }
175
+ function messageLabel(messageId2, labelId) {
176
+ return { mailboxEmail: FIXTURE_MAILBOX, messageId: messageId2, labelId };
177
+ }
178
+ function draft(id, messageId2) {
179
+ return { mailboxEmail: FIXTURE_MAILBOX, id, messageId: messageId2 };
180
+ }
181
+ function draftAddressedTo(id, recipients) {
182
+ const messageId2 = `${id}_message`;
183
+ return {
184
+ draft: draft(id, messageId2),
185
+ message: message(messageId2, { to: recipients })
186
+ };
187
+ }
188
+
189
+ // ../packages/twin-gmail/dist/src/check-drafts.js
190
+ var draftAddressedTo2 = defineCheck({
191
+ id: "gmail.draft-addressed-to",
192
+ 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.",
193
+ template: "A draft addressed to {email} exists",
194
+ params: { email: emailAddress },
195
+ substrate: "final",
196
+ // An achievement: task 22's seed ships one draft addressed to bob, so an
197
+ // examinee that does nothing does not satisfy this.
198
+ polarity: () => "positive",
199
+ //
200
+ // THE LOAD-BEARING DECLARATION ON THIS CHECK.
201
+ //
202
+ // pome-cloud's `corpus.ts` has carried a prediction about this exact criterion
203
+ // since F-1028: `22-gmail-inbox-triage`'s `alice@example.com` "becomes visible
204
+ // here as 'unguarded' the day a gmail draft-recipient predicate lands without a
205
+ // `subject`, which is precisely when it starts to matter." An email address is
206
+ // squarely inside what a team's redaction config may destroy. Without this
207
+ // field the evaluator has no way to turn an impossible comparison into an
208
+ // honest skip, so the criterion scores a vacuous verdict at both doors
209
+ // instead — and the count of unguarded hazards must stay at zero.
210
+ subject: ({ email }) => email,
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 }) => {
217
+ const wanted = draftAddressedTo("draft_target", [email]);
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 }, { final }) {
225
+ if (final.drafts == null || final.messages == null) {
226
+ return { passed: false, status: "skipped", reason: "state_incomplete" };
227
+ }
228
+ const wanted = email.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 ${email}`,
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 (F-1197).
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 ${email} (${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 this check is the second entry in pome-cloud's
274
+ // `NUMERIC_SENTINEL_ALLOWED`.
275
+ vacuityMutant: (args) => ({ ...args, count: String(VACUITY_SENTINEL_NUMBER) }),
276
+ discriminatingWorlds: ({ count }) => {
277
+ const wanted = parseCount(count) ?? 1;
278
+ const build = (n) => {
279
+ const pairs = Array.from({ length: n }, (_, i) => draftAddressedTo(`draft_${i}`, ["a@example.com"]));
280
+ return gmailState({
281
+ drafts: pairs.map((p) => p.draft),
282
+ messages: pairs.map((p) => p.message)
283
+ });
284
+ };
285
+ return { passing: finalWorld(build(wanted)), failing: finalWorld(build(Math.max(0, wanted - 1))) };
286
+ },
287
+ evaluate({ count }, { final }) {
288
+ const wanted = parseCount(count);
289
+ if (wanted === null) {
290
+ return { passed: false, status: "skipped", reason: `uncountable ("${count}")` };
291
+ }
292
+ if (final.drafts == null)
293
+ return { passed: false, status: "skipped", reason: "state_incomplete" };
294
+ const total = final.drafts.length;
295
+ if (total < wanted && isTruncated(final, "drafts")) {
296
+ return { passed: false, status: "skipped", reason: 'collection_truncated ("drafts")' };
297
+ }
298
+ return {
299
+ passed: total >= wanted,
300
+ reason: `${total} draft(s) exist (wanted at least ${wanted})`,
301
+ // The collection whose LENGTH is the assertion. There is no narrower
302
+ // address for a cardinality.
303
+ evidenceStatePaths: [DRAFTS_PATH]
304
+ };
305
+ }
306
+ });
307
+
308
+ // ../packages/twin-gmail/dist/src/check-labels.js
309
+ var labelExists = defineCheck({
310
+ id: "gmail.label-exists",
311
+ 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.",
312
+ // The name slot admits spaces, which is what the corpus needs (`Parity
313
+ // Complete`) and what separates this template from `gmail.message-has-label`'s
314
+ // id slot. The two cannot claim one sentence: this one says "A label named",
315
+ // that one says "Message ... has label".
316
+ template: "A label named {label} exists",
317
+ params: { label: labelName },
318
+ substrate: "final",
319
+ // An achievement — task 23's seed defines no `Parity Complete`, so the
320
+ // examinee has to create it.
321
+ polarity: () => "positive",
322
+ // The name is a caller-supplied literal compared against state.
323
+ subject: ({ label }) => label,
324
+ vacuityMutant: (args) => ({ ...args, label: VACUITY_SENTINEL }),
325
+ discriminatingWorlds: ({ label }) => ({
326
+ // Both worlds carry a non-empty `labels` collection, so the failing world
327
+ // fails on the ASSERTION (`label_not_found`) rather than reproducing the
328
+ // `state_incomplete` an empty world gives — the degenerate arm the probe
329
+ // rejects.
330
+ passing: finalWorld(gmailState({ labels: [systemLabel("INBOX"), userLabel("Label_1", label)] })),
331
+ failing: finalWorld(gmailState({ labels: [systemLabel("INBOX")] }))
332
+ }),
333
+ evaluate({ label }, { final }) {
334
+ const found = resolveLabelByName(final, label);
335
+ if ("missing" in found) {
336
+ if (found.missing.startsWith("label_not_found")) {
337
+ return {
338
+ passed: false,
339
+ reason: `no label named "${label}" exists`,
340
+ // The label COLLECTION — the honest citation for a lookup that found
341
+ // nothing, and the arm a reader most wants to open: see for yourself
342
+ // that the name is not in it (F-1197).
343
+ ...found.searched === void 0 ? {} : { evidenceStatePaths: [found.searched] }
344
+ };
345
+ }
346
+ return missSkip(found);
347
+ }
348
+ return {
349
+ passed: true,
350
+ reason: `label "${label}" exists (id ${found.found.id ?? "?"})`,
351
+ evidenceStatePaths: [found.path]
352
+ };
353
+ }
354
+ });
355
+
356
+ // ../packages/twin-gmail/dist/src/check-messages.js
357
+ var messageHasLabel = defineCheck({
358
+ id: "gmail.message-has-label",
359
+ 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.",
360
+ template: "Message {message} has label {label}",
361
+ params: { message: messageId, label: labelRef },
362
+ substrate: "final",
363
+ // An achievement. The seed leaves `msg_support` on INBOX/UNREAD only, so the
364
+ // examinee has to act for this to hold.
365
+ polarity: () => "positive",
366
+ // The label is a caller-supplied literal compared against state, so it is
367
+ // declared. No first-party redactor pattern touches a Gmail label id, which is
368
+ // exactly why declaring it keeps that a verified fact rather than an
369
+ // assumption — a team's own redaction config is not first-party.
370
+ subject: ({ label }) => label,
371
+ // The message id only SELECTS; falsifying it moves the verdict through the
372
+ // lookup rather than the assertion. The label is the scanned literal.
373
+ vacuityMutant: (args) => ({ ...args, label: VACUITY_SENTINEL }),
374
+ discriminatingWorlds: ({ message: id, label }) => {
375
+ const labels = [systemLabel("INBOX"), userLabel(label, label)];
376
+ const base = { messages: [message(id)], labels };
377
+ return {
378
+ passing: finalWorld(gmailState({ ...base, messageLabels: [messageLabel(id, "INBOX"), messageLabel(id, label)] })),
379
+ failing: finalWorld(gmailState({ ...base, messageLabels: [messageLabel(id, "INBOX")] }))
380
+ };
381
+ },
382
+ evaluate({ message: id, label }, { final }) {
383
+ const found = resolveMessage(final, id);
384
+ if ("missing" in found)
385
+ return missSkip(found);
386
+ const carried = messageCarriesLabel(final, id, label);
387
+ if ("missing" in carried)
388
+ return missSkip(carried);
389
+ return {
390
+ passed: carried.found,
391
+ reason: carried.found ? `message ${id} carries label ${label}` : `message ${id} does not carry label ${label}`,
392
+ // BOTH halves of the lookup, because the predicate really does read two
393
+ // places: the message row it resolved by id, and the join table it then
394
+ // questioned. Labels are not nested under their message in this export —
395
+ // that is the whole reason `messageCarriesLabel` exists — so one pointer
396
+ // could not say where this verdict came from (F-1197).
397
+ evidenceStatePaths: [found.path, carried.path]
398
+ };
399
+ }
400
+ });
401
+ var mailboxLabelCount = defineCheck({
402
+ id: "gmail.mailbox-label-count",
403
+ 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.",
404
+ template: "The mailbox `{mailbox}` has exactly {count} messages labeled {label}",
405
+ params: { mailbox: mailboxRef, count: exactCount, label: labelRef },
406
+ substrate: "final",
407
+ // The count carries the direction: asserting zero is a prohibition, any
408
+ // other count is work the examinee must do. Preserved from the legacy rule,
409
+ // which computed the same thing per match.
410
+ polarity: ({ count }) => Number(count) === 0 ? "negative" : "positive",
411
+ subject: ({ label }) => label,
412
+ // The mailbox resolves and the count is compared to a derived total; the
413
+ // label is the literal actually scanned in state. Pointing the mutant at the
414
+ // count would falsify a threshold rather than a lookup — see
415
+ // `gmail.draft-count-at-least`, where the count IS the only slot.
416
+ vacuityMutant: (args) => ({ ...args, label: VACUITY_SENTINEL }),
417
+ discriminatingWorlds: ({ mailbox, count, label }) => {
418
+ const wanted = Number(count);
419
+ const world = (n) => {
420
+ const messages = Array.from({ length: n }, (_, i) => message(`msg_${i}`, { mailboxEmail: mailbox }));
421
+ return gmailState({
422
+ mailboxes: [{ email: mailbox }],
423
+ messages,
424
+ labels: [systemLabel(label)],
425
+ messageLabels: messages.map((m) => ({ mailboxEmail: mailbox, messageId: m.id, labelId: label }))
426
+ });
427
+ };
428
+ return { passing: finalWorld(world(wanted)), failing: finalWorld(world(wanted + 1)) };
429
+ },
430
+ evaluate({ mailbox, count, label }, { final }) {
431
+ const wanted = Number(count);
432
+ if (final.messages == null || final.messageLabels == null) {
433
+ return { passed: false, status: "skipped", reason: "state_incomplete" };
434
+ }
435
+ if (isTruncated(final, "messages") || isTruncated(final, "messageLabels")) {
436
+ return { passed: false, status: "skipped", reason: "collection_truncated" };
437
+ }
438
+ if (final.mailboxes != null && !final.mailboxes.some((mb) => (mb.email ?? "").toLowerCase() === mailbox.toLowerCase())) {
439
+ return { passed: false, status: "skipped", reason: `mailbox_not_found ("${mailbox}")` };
440
+ }
441
+ const ids = labelIdsFor(final, label);
442
+ const labeled = new Set(final.messageLabels.filter((row) => ids.has((row.labelId ?? "").toLowerCase())).map((row) => (row.messageId ?? "").toLowerCase()));
443
+ const total = final.messages.filter((msg) => (msg.mailboxEmail ?? "").toLowerCase() === mailbox.toLowerCase() && msg.id != null && labeled.has(msg.id.toLowerCase())).length;
444
+ return {
445
+ passed: total === wanted,
446
+ reason: `mailbox "${mailbox}" has ${total} message(s) labeled ${label} (wanted ${wanted})`,
447
+ // The two collections the count is computed FROM. The count itself lives
448
+ // nowhere in the tree, so there is no narrower honest address — and the
449
+ // guards above already established that both collections are present and
450
+ // un-capped, which is what makes these pointers resolve.
451
+ evidenceStatePaths: [MESSAGES_PATH, MESSAGE_LABELS_PATH]
452
+ };
453
+ }
454
+ });
455
+ var oneMessagePerRecipient = defineCheck({
456
+ id: "gmail.one-message-per-recipient",
457
+ 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.",
458
+ template: "Exactly one {label} message is addressed to each of the {count} recipients",
459
+ params: { label: labelRef, count: countWord },
460
+ substrate: "final",
461
+ polarity: () => "positive",
462
+ subject: ({ label }) => label,
463
+ // The label is scanned; the count is a declared arity compared to a derived
464
+ // one. Same argument as `mailboxLabelCount`.
465
+ vacuityMutant: (args) => ({ ...args, label: VACUITY_SENTINEL }),
466
+ discriminatingWorlds: ({ label, count }) => {
467
+ const wanted = parseCount(count) ?? 1;
468
+ const recipients = Array.from({ length: wanted }, (_, i) => `user${i}@example.com`);
469
+ const build = (addressees) => {
470
+ const messages = addressees.map((to, i) => message(`msg_${i}`, { to }));
471
+ return finalWorld(gmailState({
472
+ messages,
473
+ labels: [systemLabel(label)],
474
+ messageLabels: messages.map((m) => messageLabel(m.id, label))
475
+ }));
476
+ };
477
+ return {
478
+ passing: build(recipients.map((r) => [r])),
479
+ // The DUPLICATE SEND, not a missing one: the same number of messages, one
480
+ // recipient served twice and another not at all. A world with fewer
481
+ // messages would fail on arity, which the exact-count check already covers.
482
+ failing: build([[recipients[0]], ...recipients.slice(2).map((r) => [r]), [recipients[0]]])
483
+ };
484
+ },
485
+ evaluate({ label, count }, { final }) {
486
+ const wanted = parseCount(count);
487
+ if (final.messages == null || final.messageLabels == null) {
488
+ return { passed: false, status: "skipped", reason: "state_incomplete" };
489
+ }
490
+ if (isTruncated(final, "messages") || isTruncated(final, "messageLabels")) {
491
+ return { passed: false, status: "skipped", reason: "collection_truncated" };
492
+ }
493
+ const ids = labelIdsFor(final, label);
494
+ const labeled = new Set(final.messageLabels.filter((row) => ids.has((row.labelId ?? "").toLowerCase())).map((row) => (row.messageId ?? "").toLowerCase()));
495
+ const sent = final.messages.filter((msg) => msg.id != null && labeled.has(msg.id.toLowerCase()));
496
+ const addressees = sent.flatMap((msg) => (msg.to ?? []).map((to) => to.toLowerCase()));
497
+ const distinct = new Set(addressees);
498
+ const noDuplicates = addressees.length === distinct.size;
499
+ const arityOk = wanted == null ? sent.length === distinct.size : sent.length === wanted && distinct.size === wanted && addressees.length === wanted;
500
+ const passed = noDuplicates && arityOk;
501
+ return {
502
+ passed,
503
+ 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"})`,
504
+ // Same two collections, same reason: the duplicate this check exists to
505
+ // catch is a property of the addressee lists across `messages`, filtered
506
+ // by `messageLabels`, and neither the flattened list nor its distinct set
507
+ // is anything the tree holds.
508
+ evidenceStatePaths: [MESSAGES_PATH, MESSAGE_LABELS_PATH]
509
+ };
510
+ }
511
+ });
512
+
513
+ // ../packages/twin-gmail/dist/src/check-tape.js
514
+ var noUnsupportedEndpoint = defineCheck({
515
+ id: "gmail.no-unsupported-endpoint",
516
+ 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 task 27 runs gmail and github together.',
517
+ // No slots, and no twin word. The legacy cloud rule accepted "No unsupported
518
+ // Gmail endpoint was called" alongside the bare form, plus plural and `were`
519
+ // variants, because an author typed English. Under position 2 an author PICKS
520
+ // the check, so those variants are retired rather than ported — the same
521
+ // decision `github.no-unsupported-endpoint` made, and this is now the same
522
+ // sentence on both twins, resolved per-twin by the engine.
523
+ template: "No unsupported endpoint was called",
524
+ params: {},
525
+ substrate: "tape",
526
+ // A prohibition. Nothing is required to happen; only the examinee reaching for
527
+ // an unimplemented route can break it.
528
+ polarity: () => "negative",
529
+ // No caller-supplied literal is hunted for in any substrate, so there is
530
+ // nothing a redactor could silently delete out from under this check.
531
+ subject: () => null,
532
+ // No slots, so the sentence carries no literal to falsify. The trigger is "a
533
+ // call with fidelity=unsupported exists", which lives on the tape and not in
534
+ // the sentence. Reported as `no_trigger`, never as clean.
535
+ vacuityMutant: () => null,
536
+ discriminatingWorlds: () => ({
537
+ passing: tapeWorld([
538
+ {
539
+ twin: "gmail",
540
+ method: "GET",
541
+ path: "/gmail/v1/users/me/messages",
542
+ status: 200,
543
+ fidelity: "semantic",
544
+ event_id: "evt_ok"
545
+ }
546
+ ]),
547
+ failing: tapeWorld([
548
+ {
549
+ twin: "gmail",
550
+ method: "POST",
551
+ path: "/gmail/v1/users/me/watch",
552
+ status: 501,
553
+ fidelity: "unsupported",
554
+ event_id: "evt_bad"
555
+ }
556
+ ])
557
+ }),
558
+ evaluate(_args, { tape }) {
559
+ if (tape === null)
560
+ return { passed: false, reason: "tape_missing", status: "skipped" };
561
+ const unsupported = tape.filter((event) => event.fidelity === "unsupported");
562
+ if (unsupported.length === 0) {
563
+ return {
564
+ passed: true,
565
+ reason: `no unsupported Gmail endpoint was called (${tape.length} call(s) inspected)`
566
+ };
567
+ }
568
+ const evidenceEventIds = unsupported.map((event) => event.event_id).filter((id) => typeof id === "string" && id !== "");
569
+ const outcome = {
570
+ passed: false,
571
+ reason: `${unsupported.length} unsupported Gmail call(s): [${unsupported.map((event) => event.path ?? "?").join(", ")}]`
572
+ };
573
+ return evidenceEventIds.length > 0 ? { ...outcome, evidenceEventIds } : outcome;
574
+ }
575
+ });
576
+
577
+ // ../packages/twin-gmail/dist/src/checks.js
578
+ var GMAIL_CHECKS = [
579
+ messageHasLabel,
580
+ labelExists,
581
+ draftAddressedTo2,
582
+ draftCountAtLeast,
583
+ mailboxLabelCount,
584
+ oneMessagePerRecipient,
585
+ // Last, because these are the only ones that assert about the RUN rather than
586
+ // the world it left behind — twin-github orders its tape checks the same way.
587
+ noUnsupportedEndpoint
588
+ ];
589
+
590
+ export { GMAIL_CHECKS };