@pome-sh/cli 0.21.5 → 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.
- package/dist/build-info.json +3 -3
- package/dist/{checks-2AIHHQC2.js → checks-4GJB73YX.js} +1 -1
- package/dist/{chunk-CLGRZY53.js → chunk-4ILBTPO4.js} +4 -4
- package/dist/chunk-5ETJZJR6.js +590 -0
- package/dist/{chunk-MBM7LY7E.js → chunk-A3IDZXJY.js} +9 -9
- package/dist/chunk-FKZZWWYC.js +4 -0
- package/dist/{chunk-OMVRPIBP.js → chunk-JULH5ECU.js} +1 -1
- package/dist/{chunk-JQQPZQOJ.js → chunk-M3X6OT4N.js} +1 -1
- package/dist/chunk-NJ246QPJ.js +683 -0
- package/dist/chunk-SGDUD7KK.js +138 -0
- package/dist/{chunk-NWXONLFF.js → chunk-WR7KP3LE.js} +2 -2
- package/dist/chunk-XS66ZRBJ.js +537 -0
- package/dist/chunk-ZKID2HS3.js +628 -0
- package/dist/{runDemo-M3GEOJLT.js → runDemo-BCB6EDDN.js} +7 -8
- package/dist/{runTrialGroup-QQNDVHMR.js → runTrialGroup-YCSGW56C.js} +6 -7
- package/dist/src/cli/main.js +20 -16
- package/dist/{chunk-WVWV7DLA.js → src-FLTYWM6P.js} +500 -1761
- package/dist/{chunk-APAS34RJ.js → src-K3KTJTMG.js} +154 -1308
- package/dist/{chunk-OKDJRKJV.js → src-RVUEXCKN.js} +6 -135
- package/dist/twinHarness-KFZ2ZNAX.js +5 -0
- package/dist/{twinStart-E42H5CYD.js → twinStart-4OGP2GBY.js} +2 -2
- package/package.json +1 -1
- package/dist/src-JGOW5WYI.js +0 -5
- package/dist/src-KIHABGI5.js +0 -5
- package/dist/src-TUX5L2RC.js +0 -4
- package/dist/twinHarness-YTU4427G.js +0 -5
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "pome-sh",
|
|
3
|
-
"version": "0.21.
|
|
4
|
-
"git_sha": "
|
|
5
|
-
"build_time": "2026-08-
|
|
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-
|
|
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-
|
|
3
|
-
import { gmailSeedSchema, defaultSeedState } from './chunk-
|
|
4
|
-
import { linearSeedSchema, defaultSeedState as defaultSeedState$1 } from './chunk-
|
|
5
|
-
import { isTwinName, TWIN_REGISTRY } from './chunk-
|
|
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 };
|