@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,981 @@
1
+ import { oneOf, defineCheck, repoRef, VACUITY_SENTINEL_NUMBER, childStatePath, VACUITY_SENTINEL, statePath } from './chunk-4WXX5VPA.js';
2
+ import { z } from 'zod';
3
+
4
+ // ../twin-github/dist/src/tape-assertable-tools.js
5
+ var TAPE_ASSERTABLE_TOOLS = ["create_commit_status", "create_check_run"];
6
+
7
+ // ../twin-github/dist/src/check-params.js
8
+ var entityNumber = (name) => ({
9
+ name,
10
+ pattern: "[1-9][0-9]*",
11
+ example: "1",
12
+ render: (value) => value,
13
+ parse: (raw) => raw
14
+ });
15
+ var issueNumber = entityNumber("issue");
16
+ var prNumber = entityNumber("pr");
17
+ var labelName = {
18
+ name: "label",
19
+ pattern: "[^`\\n]+",
20
+ example: "bug",
21
+ render: (value) => value,
22
+ parse: (raw) => raw
23
+ };
24
+ var login = {
25
+ name: "login",
26
+ pattern: "[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?",
27
+ example: "alice",
28
+ render: (value) => value,
29
+ parse: (raw) => raw
30
+ };
31
+ var filePath = {
32
+ name: "path",
33
+ pattern: "[^`\\n]+",
34
+ example: "src/index.ts",
35
+ render: (value) => value,
36
+ parse: (raw) => raw
37
+ };
38
+ var commentNeedle = {
39
+ name: "needle",
40
+ pattern: '[^"\\n]+',
41
+ example: "Deploy blocked",
42
+ render: (value) => value,
43
+ parse: (raw) => raw
44
+ };
45
+ var statusContext = {
46
+ name: "context",
47
+ pattern: '[^"\\n]+',
48
+ example: "ci/build",
49
+ render: (value) => value,
50
+ parse: (raw) => raw
51
+ };
52
+ var issueState = oneOf("state", ["open", "closed"]);
53
+ var pullRequestState = oneOf("state", ["merged", "not merged", "open", "closed"]);
54
+ var reviewState = oneOf("review", ["APPROVED", "CHANGES_REQUESTED", "COMMENTED"], "APPROVED");
55
+ var commitStatusState = oneOf("state", ["success", "failure", "pending", "error"], "success");
56
+ var toolActionName = oneOf("tool", [...TAPE_ASSERTABLE_TOOLS], TAPE_ASSERTABLE_TOOLS[0]);
57
+
58
+ // ../twin-github/dist/src/check-state.js
59
+ function findRepo(state, ref) {
60
+ const repositories = state.repositories ?? [];
61
+ for (const [index, repo] of repositories.entries()) {
62
+ if (repo.full_name === ref)
63
+ return { repo, index };
64
+ if (repo.owner != null && repo.name != null && `${repo.owner}/${repo.name}` === ref) {
65
+ return { repo, index };
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+ function issueNumbers(repo) {
71
+ const numbers = /* @__PURE__ */ new Set();
72
+ for (const issue of repo.issues ?? []) {
73
+ if (typeof issue.number === "number" && Number.isFinite(issue.number))
74
+ numbers.add(issue.number);
75
+ }
76
+ return numbers;
77
+ }
78
+ function labelNames(repo) {
79
+ const names = /* @__PURE__ */ new Set();
80
+ for (const label of repo.labels ?? []) {
81
+ if (typeof label.name === "string" && label.name.length > 0)
82
+ names.add(label.name);
83
+ }
84
+ return names;
85
+ }
86
+ function missOutcome(miss) {
87
+ if (miss.searched === void 0)
88
+ return { passed: false, reason: miss.missing };
89
+ return { passed: false, reason: miss.missing, evidenceStatePaths: [miss.searched] };
90
+ }
91
+ function resolveRepo(state, ref, where) {
92
+ const hit = findRepo(state, ref);
93
+ if (hit)
94
+ return { found: hit.repo, path: statePath("repositories", hit.index) };
95
+ return {
96
+ missing: `repo ${ref} not found in ${where}`,
97
+ // `undefined` — the key absent from the export — is the only case with
98
+ // nothing to point at. A `null` repository list is a citable fact: it says
99
+ // the export carried the field and it was empty.
100
+ searched: state.repositories === void 0 ? void 0 : statePath("repositories")
101
+ };
102
+ }
103
+ function resolveIssue(state, ref, number) {
104
+ const repo = resolveRepo(state, ref, "state_final");
105
+ if ("missing" in repo)
106
+ return repo;
107
+ const issues = repo.found.issues ?? [];
108
+ const index = issues.findIndex((candidate) => candidate.number === Number(number));
109
+ if (index >= 0) {
110
+ return { found: issues[index], path: childStatePath(repo.path, "issues", index) };
111
+ }
112
+ return {
113
+ missing: `issue #${number} not found in ${ref}`,
114
+ searched: repo.found.issues === void 0 ? void 0 : childStatePath(repo.path, "issues")
115
+ };
116
+ }
117
+ function resolvePullRequest(state, ref, number) {
118
+ const repo = resolveRepo(state, ref, "state_final");
119
+ if ("missing" in repo)
120
+ return repo;
121
+ const pulls = repo.found.pull_requests ?? [];
122
+ const index = pulls.findIndex((candidate) => candidate.number === Number(number));
123
+ if (index >= 0) {
124
+ return { found: pulls[index], path: childStatePath(repo.path, "pull_requests", index) };
125
+ }
126
+ return {
127
+ missing: `pull request #${number} not found in ${ref}`,
128
+ searched: repo.found.pull_requests === void 0 ? void 0 : childStatePath(repo.path, "pull_requests")
129
+ };
130
+ }
131
+ function isMerged(pull) {
132
+ return pull.merged === 1 || pull.merged === true;
133
+ }
134
+ function sameLabel(a, b) {
135
+ return a.toLowerCase() === b.toLowerCase();
136
+ }
137
+ function appliedLabelNames(issue) {
138
+ return (issue.labels ?? []).map((label) => label.name).filter((name) => typeof name === "string");
139
+ }
140
+
141
+ // ../twin-github/dist/src/check-worlds.js
142
+ function finalWorld(final) {
143
+ return { seed: null, final, tape: null };
144
+ }
145
+ function deltaWorld(seed, final) {
146
+ return { seed, final, tape: null };
147
+ }
148
+ function tapeWorld(tape) {
149
+ return { seed: null, final: { repositories: [] }, tape };
150
+ }
151
+ function repoState(repo = {}) {
152
+ return { repositories: [{ owner: "acme", name: "api", full_name: "acme/api", ...repo }] };
153
+ }
154
+
155
+ // ../twin-github/dist/src/check-issues.js
156
+ var issueExists = defineCheck({
157
+ id: "github.issue-exists",
158
+ description: "Asserts an issue with this number is present in the repository's final state. It says NOTHING about the issue's content, state, labels or assignee \u2014 pair it with those checks when they matter. Its natural use is a task whose examinee must CREATE the issue; asserting the existence of a seeded issue is trivially true and grades nothing.",
159
+ template: "Issue #{issue} exists in `{repo}`",
160
+ params: { issue: issueNumber, repo: repoRef },
161
+ substrate: "final",
162
+ polarity: () => "positive",
163
+ // A number, not a string hunted for inside free text — no redactor can
164
+ // delete it out from under the lookup.
165
+ subject: () => null,
166
+ // The ONE check where the issue number is the SCANNED literal rather than a
167
+ // selector. Everywhere else `resolveIssue` early-returns "not found" before
168
+ // the real comparison runs, so falsifying the number would move the verdict
169
+ // for a reason that never reaches the assertion. Here the lookup IS the
170
+ // assertion, so falsifying it is exactly right.
171
+ vacuityMutant: (args) => ({ ...args, issue: String(VACUITY_SENTINEL_NUMBER) }),
172
+ // The repo is PRESENT in both worlds; only the issue moves. A world without
173
+ // the repo would fail the way an EMPTY world does, which arm 3 rejects.
174
+ discriminatingWorlds: ({ issue }) => ({
175
+ passing: finalWorld(repoState({ issues: [{ number: Number(issue) }] })),
176
+ failing: finalWorld(repoState({ issues: [{ number: Number(issue) + 1 }] }))
177
+ }),
178
+ evaluate({ issue, repo }, { final }) {
179
+ const found = resolveIssue(final, repo, issue);
180
+ if ("missing" in found)
181
+ return missOutcome(found);
182
+ return {
183
+ passed: true,
184
+ reason: `issue #${issue} exists in ${repo}`,
185
+ evidenceStatePaths: [found.path]
186
+ };
187
+ }
188
+ });
189
+ var issueStateCheck = defineCheck({
190
+ id: "github.issue-state",
191
+ description: "Compares the issue row's `state` column against the named state. A missing issue FAILS; an issue whose export carries no state at all is SKIPPED rather than judged, because absent is not the same as open. The `open` form is a prohibition \u2014 it asks the examinee NOT to close the issue \u2014 which is why polarity is read from the state word.",
192
+ // "is in state X", not "is X", for two reasons. It borrows the idiom the
193
+ // Linear tasks already use (`Issue "…" is in state In Progress`), so one
194
+ // reading habit spans twins. And it keeps this template's literal tail from
195
+ // swallowing `… is assigned to \`{login}\``: near-miss patterns open every
196
+ // slot to `.+?`, so `Issue #{issue} in \`{repo}\` is {state}` would resemble
197
+ // the assignee sentence, and a corrupted assignee would be reported as a
198
+ // corrupted STATE check — pointing an author at a check they never picked.
199
+ // The contract test asserts this directly; it is the reason this wording is
200
+ // not "is {state}".
201
+ //
202
+ // `github.pr-state` keeps the shorter "is {state}" deliberately: half its set
203
+ // (`merged`/`not merged`) is a different column from `state`, so "is in
204
+ // state merged" would name the wrong field.
205
+ template: "Issue #{issue} in `{repo}` is in state {state}",
206
+ params: { issue: issueNumber, repo: repoRef, state: issueState },
207
+ substrate: "final",
208
+ // Per-arg. "is closed" asks the examinee to close it; "is open" is the twin's
209
+ // "the agent did not close issue #1" — a prohibition wearing a state word.
210
+ polarity: ({ state }) => state === "open" ? "negative" : "positive",
211
+ subject: () => null,
212
+ // No falsifiable trigger. The state word comes from a CLOSED SET, so there is
213
+ // no value guaranteed to be false — every member might legitimately be true
214
+ // of the state. The issue number only resolves: mutating it early-returns
215
+ // "not found", which moves the verdict on every seed for a reason unrelated
216
+ // to the trigger clause. A mutant guaranteed to move is a check that measures
217
+ // nothing, so this admits the blind spot as `no_trigger` instead of buying a
218
+ // false clean bill.
219
+ vacuityMutant: () => null,
220
+ // The closed set costs the vacuity mutant but not this: a world can simply
221
+ // hold the other member.
222
+ discriminatingWorlds: ({ issue, state }) => ({
223
+ passing: finalWorld(repoState({ issues: [{ number: Number(issue), state }] })),
224
+ failing: finalWorld(repoState({ issues: [{ number: Number(issue), state: state === "closed" ? "open" : "closed" }] }))
225
+ }),
226
+ evaluate({ issue, repo, state }, { final }) {
227
+ const found = resolveIssue(final, repo, issue);
228
+ if ("missing" in found)
229
+ return missOutcome(found);
230
+ if (found.found.state == null) {
231
+ return {
232
+ passed: false,
233
+ status: "skipped",
234
+ reason: `issue #${issue} has no state in state_final (state_incomplete)`,
235
+ // The ROW, not `…/state` — the field this branch exists for is the one
236
+ // that is absent, and a pointer at it would not resolve. Pointing at the
237
+ // row is what lets a reader see the gap for themselves instead of taking
238
+ // the reason's word for it.
239
+ evidenceStatePaths: [found.path]
240
+ };
241
+ }
242
+ const actual = found.found.state.toLowerCase();
243
+ return {
244
+ passed: actual === state,
245
+ reason: `issue #${issue} state is "${found.found.state}" (wanted "${state}")`,
246
+ evidenceStatePaths: [childStatePath(found.path, "state")]
247
+ };
248
+ }
249
+ });
250
+ var issueHasLabel = defineCheck({
251
+ id: "github.issue-has-label",
252
+ description: "Asserts the label is among those APPLIED to the issue \u2014 it does not assert the issue carries only that one. An agent that applies the right label alongside three wrong ones passes this check; `github.issue-exactly-one-label` is the assertion that catches that. The comparison is case-insensitive, because GitHub creates label names case-insensitively while preserving the caller's display casing.",
253
+ template: "Issue #{issue} in `{repo}` has the `{label}` label applied",
254
+ params: { issue: issueNumber, repo: repoRef, label: labelName },
255
+ substrate: "final",
256
+ polarity: () => "positive",
257
+ // The label is a caller-supplied literal compared against the state tree, so
258
+ // a redactor that destroys it makes this check unable to fire.
259
+ subject: ({ label }) => label,
260
+ // The label is what the scan ranges over; the issue number only resolves.
261
+ vacuityMutant: (args) => ({ ...args, label: VACUITY_SENTINEL }),
262
+ // `labels` are ROW OBJECTS, not strings. The failing world carries a DIFFERENT
263
+ // label rather than none, so its reason names a non-empty applied set and
264
+ // cannot be mistaken for a missing issue.
265
+ discriminatingWorlds: ({ issue, label }) => ({
266
+ passing: finalWorld(repoState({ issues: [{ number: Number(issue), labels: [{ name: label }] }] })),
267
+ failing: finalWorld(repoState({ issues: [{ number: Number(issue), labels: [{ name: "unrelated" }] }] }))
268
+ }),
269
+ evaluate({ issue, repo, label }, { final }) {
270
+ const found = resolveIssue(final, repo, issue);
271
+ if ("missing" in found)
272
+ return missOutcome(found);
273
+ const applied = appliedLabelNames(found.found);
274
+ const passed = applied.some((name) => sameLabel(name, label));
275
+ return {
276
+ passed,
277
+ reason: passed ? `issue #${issue} has label "${label}"` : `issue #${issue} labels are [${applied.join(", ")}], missing "${label}"`,
278
+ // The APPLIED set, which is the set this check scanned — not the repo's
279
+ // label definitions, which is the neighbouring set `no-new-labels` reads
280
+ // and the one this check's description exists to keep it distinct from.
281
+ evidenceStatePaths: [childStatePath(found.path, "labels")]
282
+ };
283
+ }
284
+ });
285
+ var issueExactlyOneLabel = defineCheck({
286
+ id: "github.issue-exactly-one-label",
287
+ description: "Asserts the issue carries EXACTLY ONE applied label and that it is this one. Strictly stronger than `github.issue-has-label`: it fails an agent that piles a correct label on top of an incorrect one, which is the defect a triage task usually exists to catch. It counts every applied label, not only ones a human would call a classification.",
288
+ template: "Issue #{issue} in `{repo}` has exactly one classification label, and it is `{label}`",
289
+ params: { issue: issueNumber, repo: repoRef, label: labelName },
290
+ substrate: "final",
291
+ polarity: () => "positive",
292
+ subject: ({ label }) => label,
293
+ vacuityMutant: (args) => ({ ...args, label: VACUITY_SENTINEL }),
294
+ // The failing world holds the RIGHT label plus one more — the defect this
295
+ // check exists to catch, and precisely the one `issue-has-label` passes.
296
+ discriminatingWorlds: ({ issue, label }) => ({
297
+ passing: finalWorld(repoState({ issues: [{ number: Number(issue), labels: [{ name: label }] }] })),
298
+ failing: finalWorld(repoState({ issues: [{ number: Number(issue), labels: [{ name: label }, { name: "extra" }] }] }))
299
+ }),
300
+ evaluate({ issue, repo, label }, { final }) {
301
+ const found = resolveIssue(final, repo, issue);
302
+ if ("missing" in found)
303
+ return missOutcome(found);
304
+ const applied = appliedLabelNames(found.found);
305
+ const passed = applied.length === 1 && sameLabel(applied[0], label);
306
+ return {
307
+ passed,
308
+ reason: passed ? `issue #${issue} has exactly one label ("${label}")` : `issue #${issue} has labels [${applied.join(", ")}], expected exactly one label "${label}"`,
309
+ // The same pointer `issue-has-label` cites, because both read the same
310
+ // field — the difference between them is the assertion, not the address.
311
+ evidenceStatePaths: [childStatePath(found.path, "labels")]
312
+ };
313
+ }
314
+ });
315
+ var issueAssignee = defineCheck({
316
+ id: "github.issue-assignee",
317
+ description: "Asserts this login is among the issue's assignees. GitHub issues can carry several, so this does not assert sole ownership. It compares LOGINS exactly and case-sensitively, not display names \u2014 `alice` matches the collaborator `alice`, and `Alice Smith` matches nothing.",
318
+ template: "Issue #{issue} in `{repo}` is assigned to `{login}`",
319
+ params: { issue: issueNumber, repo: repoRef, login },
320
+ substrate: "final",
321
+ polarity: () => "positive",
322
+ subject: (args) => args.login,
323
+ vacuityMutant: (args) => ({ ...args, login: VACUITY_SENTINEL }),
324
+ // `assignees` really is `string[]` of logins here, while its label neighbours
325
+ // are row objects — the domain resolves it through a separate join.
326
+ discriminatingWorlds: ({ issue, login: wanted }) => ({
327
+ passing: finalWorld(repoState({ issues: [{ number: Number(issue), assignees: [wanted] }] })),
328
+ failing: finalWorld(repoState({ issues: [{ number: Number(issue), assignees: ["someone-else"] }] }))
329
+ }),
330
+ evaluate(args, { final }) {
331
+ const found = resolveIssue(final, args.repo, args.issue);
332
+ if ("missing" in found)
333
+ return missOutcome(found);
334
+ const assignees = found.found.assignees ?? [];
335
+ const passed = assignees.includes(args.login);
336
+ return {
337
+ passed,
338
+ reason: passed ? `issue #${args.issue} is assigned to "${args.login}"` : `issue #${args.issue} assignees are [${assignees.join(", ")}], missing "${args.login}"`,
339
+ evidenceStatePaths: [childStatePath(found.path, "assignees")]
340
+ };
341
+ }
342
+ });
343
+ var issueCommentContains = defineCheck({
344
+ id: "github.issue-comment-contains",
345
+ description: "Scans the bodies of every comment on the issue for this text as a SUBSTRING, case-sensitively. It does not assert who commented, how many did, or where in the body the text sits. Because the text is hunted inside free prose rather than compared to a field, a redaction rule that destroys it makes this check unable to fire \u2014 the engine skips it as `subject_redacted` rather than passing it vacuously.",
346
+ template: 'A comment containing "{needle}" exists on issue #{issue} in `{repo}`',
347
+ params: { needle: commentNeedle, issue: issueNumber, repo: repoRef },
348
+ substrate: "final",
349
+ polarity: () => "positive",
350
+ subject: ({ needle }) => needle,
351
+ // The issue RESOLVES, the needle is SCANNED within it. Falsifying the issue
352
+ // number instead would early-return "not found" and move the verdict for
353
+ // every seed, for a reason that never reaches the comment scan.
354
+ vacuityMutant: (args) => ({ ...args, needle: VACUITY_SENTINEL }),
355
+ // A comment that does not contain the needle, rather than zero comments: the
356
+ // reason then states a scanned count, which distinguishes it from an
357
+ // unresolvable issue.
358
+ discriminatingWorlds: ({ issue, needle }) => ({
359
+ passing: finalWorld(repoState({ issues: [{ number: Number(issue), comments: [{ body: `left pad ${needle} right pad` }] }] })),
360
+ failing: finalWorld(repoState({ issues: [{ number: Number(issue), comments: [{ body: "unrelated chatter" }] }] }))
361
+ }),
362
+ evaluate({ needle, issue, repo }, { final }) {
363
+ const found = resolveIssue(final, repo, issue);
364
+ if ("missing" in found)
365
+ return missOutcome(found);
366
+ const comments = found.found.comments ?? [];
367
+ const passed = comments.some((comment) => (comment.body ?? "").includes(needle));
368
+ return {
369
+ passed,
370
+ reason: passed ? `issue #${issue} has a comment containing "${needle}"` : `issue #${issue} has no comment containing "${needle}" (${comments.length} comment(s) scanned)`,
371
+ // The whole comment list, not the matching comment. The check scans every
372
+ // body, so the list is what it read — and on the FAILING side there is no
373
+ // matching row to point at, which would leave the citation present on a
374
+ // pass and absent on a fail. A pointer that appears only when the verdict
375
+ // is good is worse than none: its absence would read as a verdict class.
376
+ evidenceStatePaths: [childStatePath(found.path, "comments")]
377
+ };
378
+ }
379
+ });
380
+
381
+ // ../twin-github/dist/src/check-pulls.js
382
+ var pullRequestStateCheck = defineCheck({
383
+ id: "github.pr-state",
384
+ description: "Reads the pull request's `merged` flag for `merged`/`not merged`, and its `state` column for `open`/`closed`. These are DIFFERENT fields and a PR can be closed without being merged, so the two pairs do not imply each other. Whichever field the sentence turns on must be present: an export missing it is SKIPPED, because defaulting it to false would let `is not merged` pass against a world we cannot see.",
385
+ template: "Pull request #{pr} in `{repo}` is {state}",
386
+ params: { pr: prNumber, repo: repoRef, state: pullRequestState },
387
+ substrate: "final",
388
+ // Per-arg, and the reason polarity takes the args at all: one shipped task asserts
389
+ // "PR #1 is merged" and "PR #2 is not merged" through this one template.
390
+ // "open" is the same prohibition as the issue check's.
391
+ polarity: ({ state }) => state === "not merged" || state === "open" ? "negative" : "positive",
392
+ subject: () => null,
393
+ // No falsifiable trigger, same shape as issue-state: a closed set with no
394
+ // guaranteed-false member, and a PR number that only resolves.
395
+ vacuityMutant: () => null,
396
+ // `merged` is a SQLite integer boolean, and BOTH worlds set the field the
397
+ // assertion turns on: `merged == null` makes this check SKIP, and a skip
398
+ // satisfies neither arm.
399
+ discriminatingWorlds: ({ pr, state }) => {
400
+ const onMergeFlag = state === "merged" || state === "not merged";
401
+ if (onMergeFlag) {
402
+ const wantMerged = state === "merged";
403
+ return {
404
+ passing: finalWorld(repoState({ pull_requests: [{ number: Number(pr), merged: wantMerged ? 1 : 0 }] })),
405
+ failing: finalWorld(repoState({ pull_requests: [{ number: Number(pr), merged: wantMerged ? 0 : 1 }] }))
406
+ };
407
+ }
408
+ return {
409
+ passing: finalWorld(repoState({ pull_requests: [{ number: Number(pr), state }] })),
410
+ failing: finalWorld(repoState({
411
+ pull_requests: [{ number: Number(pr), state: state === "open" ? "closed" : "open" }]
412
+ }))
413
+ };
414
+ },
415
+ evaluate({ pr, repo, state }, { final }) {
416
+ const found = resolvePullRequest(final, repo, pr);
417
+ if ("missing" in found)
418
+ return missOutcome(found);
419
+ const pull = found.found;
420
+ const onMergeFlag = state === "merged" || state === "not merged";
421
+ const turnsOn = onMergeFlag ? pull.merged : pull.state;
422
+ if (turnsOn == null) {
423
+ return {
424
+ passed: false,
425
+ status: "skipped",
426
+ reason: `pull request #${pr} has no ${onMergeFlag ? "merged" : "state"} field in state_final (state_incomplete)`,
427
+ // The ROW: the field this branch exists for is the absent one, so a
428
+ // pointer at it would resolve to nothing.
429
+ evidenceStatePaths: [found.path]
430
+ };
431
+ }
432
+ if (onMergeFlag) {
433
+ const merged = isMerged(pull);
434
+ return {
435
+ passed: state === "merged" ? merged : !merged,
436
+ reason: `pull request #${pr}: merged=${merged} (wanted "${state}")`,
437
+ // The SAME discipline the reason above already follows: cite only the
438
+ // field the assertion turned on. `merged` and `state` are different
439
+ // columns, and a citation naming both would offer a reader an absent
440
+ // field as evidence for a verdict that never read it.
441
+ evidenceStatePaths: [childStatePath(found.path, "merged")]
442
+ };
443
+ }
444
+ return {
445
+ passed: pull.state === state,
446
+ reason: `pull request #${pr}: state="${pull.state}" (wanted "${state}")`,
447
+ evidenceStatePaths: [childStatePath(found.path, "state")]
448
+ };
449
+ }
450
+ });
451
+ var pullRequestCommentExists = defineCheck({
452
+ id: "github.pr-comment-exists",
453
+ description: "Asserts the pull request's CONVERSATION timeline carries at least one comment \u2014 the surface GitHub's issue-comment endpoints write to. It is not the other two things a reader may call a comment on a PR: a review's body is not one (assert that with `github.pr-review-exists`), and an inline review comment anchored to a file and line is not one either. It says nothing about who commented, how many did, or what any of them say \u2014 and no declaration reads the TEXT of a pull request's comment yet. `github.issue-comment-contains` is the issue-side counterpart and does NOT reach a pull request: it resolves its subject among the repository's issues, so pointing it at a PR number fails as `issue #N not found`. A pull request whose export carries no comments section at all is SKIPPED, because absent is not the same as none.",
454
+ template: "Pull request #{pr} in `{repo}` has at least one comment",
455
+ params: { pr: prNumber, repo: repoRef },
456
+ substrate: "final",
457
+ polarity: () => "positive",
458
+ // A count, not a literal hunted inside prose: no redaction rule can destroy
459
+ // the thing this check compares, so there is no subject to declare.
460
+ subject: () => null,
461
+ // No falsifiable trigger. The PR number only RESOLVES — mutating it
462
+ // early-returns "not found", which moves the verdict on every seed for a reason
463
+ // that never reaches the comment count — and there is no second slot to
464
+ // falsify. Same shape as `pr-review-exists`, and admitted as `no_trigger`
465
+ // rather than buying a false clean bill.
466
+ vacuityMutant: () => null,
467
+ // The failing world names `comments: []`, NOT an absent section: absent SKIPS,
468
+ // because absent is not none — and a skip satisfies no arm.
469
+ discriminatingWorlds: ({ pr }) => ({
470
+ passing: finalWorld(repoState({
471
+ pull_requests: [{ number: Number(pr), comments: [{ body: "Summary: adds an optional discount." }] }]
472
+ })),
473
+ failing: finalWorld(repoState({ pull_requests: [{ number: Number(pr), comments: [] }] }))
474
+ }),
475
+ evaluate({ pr, repo }, { final }) {
476
+ const found = resolvePullRequest(final, repo, pr);
477
+ if ("missing" in found)
478
+ return missOutcome(found);
479
+ if (found.found.comments == null) {
480
+ return {
481
+ passed: false,
482
+ status: "skipped",
483
+ reason: `pull request #${pr} has no comments section in state_final (state_incomplete)`,
484
+ evidenceStatePaths: [found.path]
485
+ };
486
+ }
487
+ const count = found.found.comments.length;
488
+ return {
489
+ passed: count > 0,
490
+ // Names the surface, so a failure cannot be misread as "the agent left a
491
+ // review body and this check refused to see it" — which is exactly the
492
+ // wrong-match complaint the three readings invite.
493
+ reason: count > 0 ? `pull request #${pr} has ${count} conversation comment(s)` : `pull request #${pr} has no conversation comments (reviews and inline review comments are not counted)`,
494
+ // `comments`, and this pointer is the disambiguation the description
495
+ // spends a paragraph on. Three fields on a PR can be called "a comment";
496
+ // the citation names which one was counted, in a form a reader can open.
497
+ evidenceStatePaths: [childStatePath(found.path, "comments")]
498
+ };
499
+ }
500
+ });
501
+ var pullRequestReviewExists = defineCheck({
502
+ id: "github.pr-review-exists",
503
+ description: "Asserts at least one submitted review on the pull request carries this state. It does not assert who reviewed, how recently, or that no other review disagrees \u2014 an APPROVED review alongside a CHANGES_REQUESTED one satisfies both. A pull request whose export carries no reviews section at all is SKIPPED, because absent is not the same as none.",
504
+ template: "A {review} review exists on pull request #{pr} in `{repo}`",
505
+ params: { review: reviewState, pr: prNumber, repo: repoRef },
506
+ substrate: "final",
507
+ polarity: () => "positive",
508
+ subject: () => null,
509
+ // A closed set with no guaranteed-false member, and a PR number that only
510
+ // resolves. Same reasoning as issue-state.
511
+ vacuityMutant: () => null,
512
+ // The failing world names `reviews: []`, NOT an absent section: absent SKIPS,
513
+ // because absent is not none — and a skip satisfies no arm.
514
+ discriminatingWorlds: ({ pr, review }) => ({
515
+ passing: finalWorld(repoState({ pull_requests: [{ number: Number(pr), reviews: [{ state: review }] }] })),
516
+ failing: finalWorld(repoState({ pull_requests: [{ number: Number(pr), reviews: [] }] }))
517
+ }),
518
+ evaluate({ review, pr, repo }, { final }) {
519
+ const found = resolvePullRequest(final, repo, pr);
520
+ if ("missing" in found)
521
+ return missOutcome(found);
522
+ if (found.found.reviews == null) {
523
+ return {
524
+ passed: false,
525
+ status: "skipped",
526
+ reason: `pull request #${pr} has no reviews section in state_final (state_incomplete)`,
527
+ evidenceStatePaths: [found.path]
528
+ };
529
+ }
530
+ const states = found.found.reviews.map((row) => row.state ?? "");
531
+ const passed = states.includes(review);
532
+ return {
533
+ passed,
534
+ reason: passed ? `pull request #${pr} has a ${review} review` : `pull request #${pr} reviews are [${states.join(", ")}], missing a ${review} review`,
535
+ evidenceStatePaths: [childStatePath(found.path, "reviews")]
536
+ };
537
+ }
538
+ });
539
+
540
+ // ../twin-github/dist/src/check-repos.js
541
+ var noNewLabels = defineCheck({
542
+ id: "github.no-new-labels",
543
+ // The same explanation the comment below carries, in the one place
544
+ // an author can actually reach it.
545
+ description: "Compares the repository's label DEFINITIONS in the seed against the final state. Applying an ALREADY-DEFINED label to an issue PASSES this check \u2014 only creating a label the repo did not already define fails it. `addIssueLabels` rejects an undefined label, so an examinee cannot apply a new one without creating it first, which is what makes this tight. Needs the seed: it is a delta, not a state assertion.",
546
+ // The repo is named on purpose. Without it the sentence reads as an
547
+ // issue-level claim — "the agent did not add labels to the issue" — which is
548
+ // WIDER than what this predicate compares. `create_label` is repo-scoped in
549
+ // GitHub's own vocabulary and `add_issue_labels` is the issue-scoped one, so
550
+ // naming the repo is what tells a reader which of the two is being asserted.
551
+ //
552
+ // An agent that applies an ALREADY-DEFINED label to an issue passes this
553
+ // check, correctly. The assertion that catches that is
554
+ // `Issue #N … has exactly one classification label`.
555
+ template: "No new labels were created in `{repo}`",
556
+ params: { repo: repoRef },
557
+ // The whole point: this is a delta, and it is unanswerable without the seed.
558
+ substrate: "seed+final",
559
+ // It should PASS on the untouched seed and can only be broken by the
560
+ // examinee acting.
561
+ polarity: () => "negative",
562
+ // Nothing here is a caller-supplied literal hunted for inside the state, so
563
+ // there is nothing a redactor could silently delete out from under it.
564
+ subject: () => null,
565
+ // The repo is a SELECTOR, not a scanned value. Falsifying it would move the
566
+ // verdict ("repo not found") for a reason that never reaches the assertion —
567
+ // a clean bill this check did not earn. Reported as `no_trigger`.
568
+ vacuityMutant: () => null,
569
+ // The only seed+final check, so its worlds are the only ones naming a seed.
570
+ // The failing world's final gains a definition the seed did not have, which is
571
+ // the delta itself.
572
+ discriminatingWorlds: () => ({
573
+ passing: deltaWorld(repoState({ labels: [{ name: "bug" }] }), repoState({ labels: [{ name: "bug" }] })),
574
+ failing: deltaWorld(repoState({ labels: [{ name: "bug" }] }), repoState({ labels: [{ name: "bug" }, { name: "invented" }] }))
575
+ }),
576
+ evaluate({ repo }, { seed, final }) {
577
+ if (seed === null)
578
+ return { passed: false, reason: "seed_missing", status: "skipped" };
579
+ const seedRepo = resolveRepo(seed, repo, "the seed state");
580
+ if ("missing" in seedRepo)
581
+ return missOutcome({ ...seedRepo, searched: void 0 });
582
+ const finalRepo = resolveRepo(final, repo, "state_final");
583
+ if ("missing" in finalRepo)
584
+ return missOutcome(finalRepo);
585
+ const before = labelNames(seedRepo.found);
586
+ const created = [...labelNames(finalRepo.found)].filter((name) => !before.has(name)).sort();
587
+ const definitions = [childStatePath(finalRepo.path, "labels")];
588
+ if (created.length === 0) {
589
+ return {
590
+ passed: true,
591
+ reason: `no labels were created in ${repo} (${before.size} defined at seed, unchanged at finish)`,
592
+ evidenceStatePaths: definitions
593
+ };
594
+ }
595
+ return {
596
+ passed: false,
597
+ reason: `labels created in ${repo}: ${created.map((name) => `\`${name}\``).join(", ")}`,
598
+ evidenceStatePaths: definitions
599
+ };
600
+ }
601
+ });
602
+ var noNewIssues = defineCheck({
603
+ id: "github.no-new-issues",
604
+ description: "Compares the issue NUMBERS present in the seed against the final state, and fails when finish carries one the seed did not. Numbers, not titles: a duplicate issue usually carries the same title as the one it duplicates. It says nothing about what happened to the seeded issues \u2014 closing, relabelling or commenting on one all PASS this check, so pair it with `github.issue-state` or `github.issue-comment-contains` when those matter. Its natural use is the inverse of `github.issue-exists`: a task whose examinee must recognise that an issue already exists and NOT open another. Needs the seed: it is a delta, not a state assertion.",
605
+ // The repo is named for `noNewLabels`'s reason, one step stronger: without it
606
+ // the sentence reads as a claim about the whole world, and issue numbers are
607
+ // per-repository — `#2` in one repo has nothing to do with `#2` in another.
608
+ template: "No new issues were created in `{repo}`",
609
+ params: { repo: repoRef },
610
+ substrate: "seed+final",
611
+ // Passes on the untouched seed; only the examinee acting can break it.
612
+ polarity: () => "negative",
613
+ // Nothing is hunted inside free text, so there is nothing a redactor could
614
+ // delete out from under this check.
615
+ subject: () => null,
616
+ // Same admission `noNewLabels` makes: the repo SELECTS, it is not a scanned
617
+ // literal. Falsifying it moves the verdict to "repo not found" for a reason
618
+ // that never reaches the comparison — a clean bill this check did not earn.
619
+ vacuityMutant: () => null,
620
+ discriminatingWorlds: () => ({
621
+ passing: deltaWorld(repoState({ issues: [{ number: 1 }] }), repoState({ issues: [{ number: 1 }] })),
622
+ failing: deltaWorld(repoState({ issues: [{ number: 1 }] }), repoState({ issues: [{ number: 1 }, { number: 2 }] }))
623
+ }),
624
+ evaluate({ repo }, { seed, final }) {
625
+ if (seed === null)
626
+ return { passed: false, reason: "seed_missing", status: "skipped" };
627
+ const seedRepo = resolveRepo(seed, repo, "the seed state");
628
+ if ("missing" in seedRepo)
629
+ return missOutcome({ ...seedRepo, searched: void 0 });
630
+ const finalRepo = resolveRepo(final, repo, "state_final");
631
+ if ("missing" in finalRepo)
632
+ return missOutcome(finalRepo);
633
+ const before = issueNumbers(seedRepo.found);
634
+ const created = [...issueNumbers(finalRepo.found)].filter((number) => !before.has(number)).sort((a, b) => a - b);
635
+ const issues = [childStatePath(finalRepo.path, "issues")];
636
+ if (created.length === 0) {
637
+ return {
638
+ passed: true,
639
+ reason: `no issues were created in ${repo} (${before.size} at seed, unchanged at finish)`,
640
+ evidenceStatePaths: issues
641
+ };
642
+ }
643
+ return {
644
+ passed: false,
645
+ reason: `issues created in ${repo}: ${created.map((number) => `#${number}`).join(", ")}`,
646
+ evidenceStatePaths: issues
647
+ };
648
+ }
649
+ });
650
+ var fileExists = defineCheck({
651
+ id: "github.file-exists",
652
+ description: "Asserts a file with this exact path exists in the repository on ANY branch \u2014 the twin exports files per branch and this check does not distinguish them, so a file committed only to a side branch satisfies it. The path is compared exactly and case-sensitively; it asserts nothing about the file's contents.",
653
+ template: "File `{path}` exists in `{repo}`",
654
+ params: { path: filePath, repo: repoRef },
655
+ substrate: "final",
656
+ polarity: () => "positive",
657
+ subject: ({ path }) => path,
658
+ // The path IS the scanned literal.
659
+ vacuityMutant: (args) => ({ ...args, path: `${VACUITY_SENTINEL}.txt` }),
660
+ discriminatingWorlds: ({ path }) => ({
661
+ passing: finalWorld(repoState({ files: [{ path }] })),
662
+ failing: finalWorld(repoState({ files: [{ path: "some/other/file.ts" }] }))
663
+ }),
664
+ evaluate({ path, repo }, { final }) {
665
+ const found = resolveRepo(final, repo, "state_final");
666
+ if ("missing" in found)
667
+ return missOutcome(found);
668
+ const files = found.found.files ?? [];
669
+ const passed = files.some((file) => file.path === path);
670
+ return {
671
+ passed,
672
+ reason: passed ? `file exists at "${path}" in ${repo}` : `no file found at "${path}" in ${repo} (${files.length} file(s) exported)`,
673
+ evidenceStatePaths: [childStatePath(found.path, "files")]
674
+ };
675
+ }
676
+ });
677
+ var commitStatus = defineCheck({
678
+ id: "github.commit-status",
679
+ description: "Asserts at least one commit status reported under this context carries this state. It does not say WHICH commit: the twin exports every status row for the repo and this check scans them all, so a green status on an old commit satisfies it. Nor does it assert the status is the latest one for that context.",
680
+ template: 'Commit status "{context}" in `{repo}` is {state}',
681
+ params: { context: statusContext, repo: repoRef, state: commitStatusState },
682
+ substrate: "final",
683
+ polarity: () => "positive",
684
+ subject: ({ context }) => context,
685
+ // No falsifiable trigger, and this one is a DELIBERATE loss against the regex
686
+ // it replaces. The legacy rule mutated the state word, which worked only
687
+ // because `(\w+)` would accept an invented sentinel. Typing the state as a
688
+ // closed set is the right call — an author picks from four real values — but
689
+ // a closed set has no guaranteed-false member, so a mutant could assert a
690
+ // different state that happens to also be true and report a clean bill this
691
+ // check did not earn. Mutating the context instead would empty the candidate
692
+ // set and move the verdict without the state comparison ranging over
693
+ // anything, which is the same false clean bill one step earlier. `no_trigger`
694
+ // is the honest answer.
695
+ vacuityMutant: () => null,
696
+ // The failing world keeps the CONTEXT and moves the STATE. Dropping the
697
+ // context instead would empty the candidate set and produce a reason about
698
+ // nothing having been found, which is the shape arm 3 distrusts.
699
+ discriminatingWorlds: ({ context, state }) => ({
700
+ passing: finalWorld(repoState({ commit_statuses: [{ context, state }] })),
701
+ failing: finalWorld(repoState({ commit_statuses: [{ context, state: state === "failure" ? "success" : "failure" }] }))
702
+ }),
703
+ evaluate({ context, repo, state }, { final }) {
704
+ const found = resolveRepo(final, repo, "state_final");
705
+ if ("missing" in found)
706
+ return missOutcome(found);
707
+ const candidates = (found.found.commit_statuses ?? []).filter((row) => row.context === context);
708
+ const passed = candidates.some((row) => (row.state ?? "").toLowerCase() === state);
709
+ return {
710
+ passed,
711
+ reason: passed ? `commit status "${context}" is "${state}" in ${repo}` : `no commit status "${context}" with state "${state}" in ${repo} (found: [${candidates.map((row) => row.state).join(", ")}])`,
712
+ // Every status row, not the filtered candidates. The filter is part of the
713
+ // assertion — "which commit" is explicitly not asserted — and a pointer at
714
+ // a set this check computed rather than a set the tree holds would address
715
+ // nothing a reader could open.
716
+ evidenceStatePaths: [childStatePath(found.path, "commit_statuses")]
717
+ };
718
+ }
719
+ });
720
+
721
+ // ../twin-github/dist/src/check-tape.js
722
+ var noUnsupportedEndpoint = defineCheck({
723
+ id: "github.no-unsupported-endpoint",
724
+ 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.',
725
+ // No slots. The corpus says this exact sentence in all ten places it appears,
726
+ // and under position 2 an author PICKS the check rather than typing it — so
727
+ // the legacy regex's optional twin word ("No unsupported GitHub endpoint was
728
+ // called") and its plural/`were` variants are retired rather than ported,
729
+ // exactly as the generic `issue-has-label` phrasing was retired.
730
+ template: "No unsupported endpoint was called",
731
+ params: {},
732
+ substrate: "tape",
733
+ // A prohibition. Nothing is required to happen; only the examinee reaching
734
+ // for an unimplemented route can break it.
735
+ polarity: () => "negative",
736
+ // No caller-supplied literal is hunted for in any substrate, so there is
737
+ // nothing a redactor could silently delete out from under this check.
738
+ subject: () => null,
739
+ // No capture groups, so the sentence carries no literal to falsify. The
740
+ // trigger is "a call with fidelity=unsupported exists", which lives on the
741
+ // tape and not in the sentence. Reported as `no_trigger`, never as clean.
742
+ vacuityMutant: () => null,
743
+ // Both of these already shipped as tests; this is the same pair,
744
+ // promoted to the declaration.
745
+ discriminatingWorlds: () => ({
746
+ passing: tapeWorld([
747
+ { twin: "github", method: "GET", path: "/repos/acme/api", status: 200, fidelity: "semantic", event_id: "evt_ok" }
748
+ ]),
749
+ failing: tapeWorld([
750
+ { twin: "github", method: "POST", path: "/repos/acme/api/hooks", status: 501, fidelity: "unsupported", event_id: "evt_bad" }
751
+ ])
752
+ }),
753
+ evaluate(_args, { tape }) {
754
+ if (tape === null)
755
+ return { passed: false, reason: "tape_missing", status: "skipped" };
756
+ const unsupported = tape.filter((event) => event.fidelity === "unsupported");
757
+ if (unsupported.length === 0) {
758
+ return {
759
+ passed: true,
760
+ reason: `no unsupported GitHub endpoint was called (${tape.length} call(s) inspected)`
761
+ };
762
+ }
763
+ const evidenceEventIds = unsupported.map((event) => event.event_id).filter((id) => typeof id === "string" && id !== "");
764
+ const outcome = {
765
+ passed: false,
766
+ reason: `${unsupported.length} unsupported GitHub call(s): [${unsupported.map((event) => event.path ?? "?").join(", ")}]`
767
+ };
768
+ return evidenceEventIds.length > 0 ? { ...outcome, evidenceEventIds } : outcome;
769
+ }
770
+ });
771
+ var toolNeverCalled = defineCheck({
772
+ id: "github.tool-never-called",
773
+ description: "Scans the recorded call tape for any request that invoked the named twin action, and fails if one did. The action is matched on the recorded `tool` field, which the runtime stamps identically for an MCP `tools/call` and for the REST route that performs the same thing \u2014 so it asserts about the ACTION, not about the transport the examinee chose. It counts an ATTEMPT: a call the twin rejected (bad arguments, 4xx) still called the action, because the question is what the examinee reached for. It asserts nothing about the resulting state, and nothing about other actions. Rows recorded before the `tool` field existed carry no action name and are not read as a match. The tape is scoped to this twin by the engine before the check sees it.",
774
+ template: "`{tool}` was never called",
775
+ params: { tool: toolActionName },
776
+ substrate: "tape",
777
+ // A prohibition: nothing is required to happen, and only the examinee invoking
778
+ // the named action can break it.
779
+ polarity: () => "negative",
780
+ // The action name IS a caller-supplied literal hunted for in a substrate, so
781
+ // it is declared — unlike `noUnsupportedEndpoint`, which scans a fidelity
782
+ // stamp. No redactor pattern touches a snake_case tool name, so the check will
783
+ // never be skipped as `subject_redacted`; declaring it anyway is what keeps
784
+ // that a verified fact rather than an assumption.
785
+ subject: (args) => args.tool,
786
+ // Null, and admitted in `HONEST_NULL_MUTANTS`. The only slot is a closed set,
787
+ // so there is no value guaranteed to be false: a mutant naming the OTHER
788
+ // assertable action asserts something that may well also be true, and a value
789
+ // outside the set does not re-bind at all — which reads as "the verdict moved"
790
+ // and would bless the very criterion the probe exists to catch.
791
+ vacuityMutant: () => null,
792
+ // A NEGATIVE check, so the failing world is the one where the action WAS
793
+ // called. Both tapes are non-empty: `[]` is a real pass (an agent that called
794
+ // nothing called nothing forbidden) and `null` is a skip, so neither is the
795
+ // world this assertion turns on.
796
+ discriminatingWorlds: ({ tool }) => ({
797
+ passing: tapeWorld([
798
+ { twin: "github", method: "GET", path: "/repos/acme/api", status: 200, tool: "list_issues", event_id: "evt_ok" }
799
+ ]),
800
+ failing: tapeWorld([
801
+ { twin: "github", method: "POST", path: "/repos/acme/api/statuses/abc", status: 201, tool, event_id: "evt_bad" }
802
+ ])
803
+ }),
804
+ evaluate(args, { tape }) {
805
+ if (tape === null)
806
+ return { passed: false, reason: "tape_missing", status: "skipped" };
807
+ const calls = tape.filter((event) => event.tool === args.tool);
808
+ if (calls.length === 0) {
809
+ return {
810
+ passed: true,
811
+ reason: `\`${args.tool}\` was never called (${tape.length} call(s) inspected)`
812
+ };
813
+ }
814
+ const evidenceEventIds = calls.map((event) => event.event_id).filter((id) => typeof id === "string" && id !== "");
815
+ const outcome = {
816
+ passed: false,
817
+ reason: `${calls.length} call(s) to \`${args.tool}\`: [${calls.map((event) => `${event.method ?? "?"} ${event.path ?? "?"}`).join(", ")}]`
818
+ };
819
+ return evidenceEventIds.length > 0 ? { ...outcome, evidenceEventIds } : outcome;
820
+ }
821
+ });
822
+
823
+ // ../twin-github/dist/src/checks.js
824
+ var GITHUB_CHECKS = [
825
+ issueExists,
826
+ issueStateCheck,
827
+ issueHasLabel,
828
+ issueExactlyOneLabel,
829
+ issueAssignee,
830
+ issueCommentContains,
831
+ // The two repo-scoped deltas sit together, right after the issue assertions
832
+ // they are the negatives of: `no-new-issues` is what `issue-exists` cannot
833
+ // say, and an author reaching for one usually wants to see the other.
834
+ noNewIssues,
835
+ noNewLabels,
836
+ pullRequestStateCheck,
837
+ pullRequestCommentExists,
838
+ pullRequestReviewExists,
839
+ fileExists,
840
+ commitStatus,
841
+ // Last, because the listing order runs from the assertion an author reaches
842
+ // for first to the ones a specialised task needs — and these are the only ones
843
+ // that assert about the RUN rather than the world it left behind.
844
+ noUnsupportedEndpoint,
845
+ toolNeverCalled
846
+ ];
847
+ var seedSchema = z.object({
848
+ users: z.array(z.object({
849
+ login: z.string().min(1),
850
+ type: z.enum(["User", "Organization"]).default("User"),
851
+ name: z.string().default("")
852
+ })).default([]),
853
+ repositories: z.array(z.object({
854
+ owner: z.string().min(1),
855
+ name: z.string().min(1),
856
+ description: z.string().default(""),
857
+ private: z.boolean().default(false),
858
+ default_branch: z.string().min(1).default("main"),
859
+ collaborators: z.array(z.string().min(1)).default([]),
860
+ labels: z.array(z.object({
861
+ name: z.string().min(1),
862
+ color: z.string().default("ededed"),
863
+ description: z.string().default("")
864
+ })).default([]),
865
+ files: z.array(z.object({ path: z.string().min(1), content: z.string(), branch: z.string().optional() })).default([]),
866
+ issues: z.array(z.object({
867
+ number: z.number().int().positive().optional(),
868
+ title: z.string().min(1),
869
+ body: z.string().default(""),
870
+ state: z.enum(["open", "closed"]).default("open"),
871
+ labels: z.array(z.string().min(1)).default([]),
872
+ assignees: z.array(z.string().min(1)).default([])
873
+ })).default([]),
874
+ pull_requests: z.array(z.object({
875
+ number: z.number().int().positive().optional(),
876
+ title: z.string().min(1),
877
+ body: z.string().default(""),
878
+ head: z.string().min(1),
879
+ base: z.string().min(1).default("main"),
880
+ state: z.enum(["open", "closed"]).default("open"),
881
+ author: z.string().min(1).optional(),
882
+ // Reviews seeded on this PR. `state` mirrors GitHub's review
883
+ // state enum; `author` must exist in the user/collaborator set.
884
+ reviews: z.array(z.object({
885
+ author: z.string().min(1),
886
+ state: z.enum(["APPROVED", "CHANGES_REQUESTED", "COMMENTED"]).default("APPROVED"),
887
+ body: z.string().default("")
888
+ })).default([]),
889
+ // Commit statuses applied to this PR's head SHA. Wired into the
890
+ // commit_statuses table so get_pull_request_status and the merge
891
+ // path see them without needing a separate setup call.
892
+ statuses: z.array(z.object({
893
+ context: z.string().min(1).default("ci/build"),
894
+ state: z.enum(["error", "failure", "pending", "success"]).default("success"),
895
+ description: z.string().default("")
896
+ })).default([])
897
+ })).default([])
898
+ }))
899
+ });
900
+ function parseSeed(input) {
901
+ const seed = seedSchema.parse(normalizeLegacyGitHubSeed(input));
902
+ if (seed.repositories.length === 0) {
903
+ throw new Error("GitHub seed must contain at least one repository");
904
+ }
905
+ return seed;
906
+ }
907
+ function normalizeLegacyGitHubSeed(input) {
908
+ if (!input || typeof input !== "object" || Array.isArray(input))
909
+ return input;
910
+ const seed = input;
911
+ if (!Array.isArray(seed.repositories))
912
+ return input;
913
+ return {
914
+ ...seed,
915
+ repositories: seed.repositories.map((repo) => {
916
+ if (!repo || typeof repo !== "object" || Array.isArray(repo))
917
+ return repo;
918
+ const record = repo;
919
+ if (!Array.isArray(record.issues))
920
+ return repo;
921
+ return {
922
+ ...record,
923
+ issues: record.issues.map((issue) => normalizeLegacyIssueAssignee(issue))
924
+ };
925
+ })
926
+ };
927
+ }
928
+ function normalizeLegacyIssueAssignee(issue) {
929
+ if (!issue || typeof issue !== "object" || Array.isArray(issue))
930
+ return issue;
931
+ const record = issue;
932
+ if (!("assignee" in record) || "assignees" in record)
933
+ return issue;
934
+ const { assignee, ...rest } = record;
935
+ if (assignee === null || assignee === void 0 || assignee === "") {
936
+ return { ...rest, assignees: [] };
937
+ }
938
+ if (typeof assignee === "string") {
939
+ return { ...rest, assignees: [assignee] };
940
+ }
941
+ return issue;
942
+ }
943
+ function defaultSeedState() {
944
+ return {
945
+ users: [
946
+ { login: "acme", type: "Organization", name: "Acme" },
947
+ { login: "alice", type: "User", name: "Alice" },
948
+ { login: "bob", type: "User", name: "Bob" },
949
+ { login: "pome-agent", type: "User", name: "Pome Agent" }
950
+ ],
951
+ repositories: [
952
+ {
953
+ owner: "acme",
954
+ name: "api",
955
+ description: "Example API service used by GitHub twin tests.",
956
+ default_branch: "main",
957
+ collaborators: ["alice", "bob", "pome-agent"],
958
+ labels: [
959
+ { name: "bug", color: "d73a4a", description: "Something is not working" },
960
+ { name: "feature", color: "a2eeef", description: "New feature or request" },
961
+ { name: "question", color: "d876e3", description: "More information needed" }
962
+ ],
963
+ files: [
964
+ { path: "README.md", content: "# Acme API\n\nA seeded repository for local GitHub twin tests.\n" },
965
+ { path: "src/index.ts", content: "export function handler() {\n return 'ok';\n}\n" }
966
+ ],
967
+ issues: [
968
+ {
969
+ number: 1,
970
+ title: "500 error on POST /orders after deploy",
971
+ body: "Started failing right after the 14:00 deploy. Stack trace points to OrderController#create.",
972
+ labels: ["bug"],
973
+ assignees: []
974
+ }
975
+ ]
976
+ }
977
+ ]
978
+ };
979
+ }
980
+
981
+ export { GITHUB_CHECKS, defaultSeedState, parseSeed, seedSchema };