@pome-sh/cli 0.21.5 → 0.21.7

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.
@@ -0,0 +1,537 @@
1
+ import { statePath, defineCheck, VACUITY_SENTINEL, VACUITY_SENTINEL_NUMBER, childStatePath } from './chunk-NY55QTVQ.js';
2
+
3
+ // ../packages/twin-linear/dist/src/check-params.js
4
+ var issueTitle = {
5
+ name: "title",
6
+ pattern: '[^"\\n]+',
7
+ example: "Orders 500 after deploy",
8
+ render: (value) => value,
9
+ parse: (raw) => raw
10
+ };
11
+ var teamKey = {
12
+ name: "team",
13
+ pattern: "[A-Z][A-Z0-9]*",
14
+ example: "ENG",
15
+ render: (value) => value,
16
+ parse: (raw) => raw
17
+ };
18
+ var workflowStateName = {
19
+ name: "state",
20
+ pattern: '[^"\\n]+',
21
+ example: "In Progress",
22
+ render: (value) => value,
23
+ parse: (raw) => raw
24
+ };
25
+ var labelName = {
26
+ name: "label",
27
+ pattern: '[^"\\n]+',
28
+ example: "Agent",
29
+ render: (value) => value,
30
+ parse: (raw) => raw
31
+ };
32
+ var estimatePoints = {
33
+ name: "estimate",
34
+ pattern: "0|[1-9][0-9]*",
35
+ example: "2",
36
+ render: (value) => value,
37
+ parse: (raw) => raw
38
+ };
39
+ var userRef = {
40
+ name: "user",
41
+ pattern: "[^`\\n]+",
42
+ example: "dev@pome-twin.test",
43
+ render: (value) => value,
44
+ parse: (raw) => raw
45
+ };
46
+ var commentNeedle = {
47
+ name: "needle",
48
+ pattern: '[^"\\n]+',
49
+ example: "triage",
50
+ render: (value) => value,
51
+ parse: (raw) => raw
52
+ };
53
+
54
+ // ../packages/twin-linear/dist/src/check-state.js
55
+ var ISSUES_PATH = statePath("issues");
56
+ var COMMENTS_PATH = statePath("comments");
57
+ var LABELS_PATH = statePath("labels");
58
+ var WORKFLOW_STATES_PATH = statePath("workflowStates");
59
+ var USERS_PATH = statePath("users");
60
+ function fieldPath(base, row, key) {
61
+ return Object.prototype.hasOwnProperty.call(row, key) ? childStatePath(base, key) : base;
62
+ }
63
+ function unresolved(r) {
64
+ const outcome = r.skip ? { passed: false, status: "skipped", reason: r.missing } : { passed: false, reason: r.missing };
65
+ if (r.searched === void 0)
66
+ return outcome;
67
+ return { ...outcome, evidenceStatePaths: [r.searched] };
68
+ }
69
+ function isTruncated(state, collection) {
70
+ return (state.exportBounds?.truncatedCollections ?? []).includes(collection);
71
+ }
72
+ function resolveIssue(state, teamKey2, title) {
73
+ if (state.teams == null)
74
+ return { missing: "state_incomplete", skip: true };
75
+ const team = state.teams.find((t) => t.key === teamKey2);
76
+ if (team?.id == null) {
77
+ return {
78
+ missing: `team \`${teamKey2}\` not found in state_final`,
79
+ skip: false,
80
+ searched: statePath("teams")
81
+ };
82
+ }
83
+ if (state.issues == null)
84
+ return { missing: "state_incomplete", skip: true };
85
+ const indices = state.issues.map((issue, index) => issue.teamId === team.id && issue.title === title && (issue.archivedAt ?? null) === null ? index : -1).filter((index) => index >= 0);
86
+ if (indices.length === 1) {
87
+ const index = indices[0];
88
+ return { found: state.issues[index], path: childStatePath(ISSUES_PATH, index) };
89
+ }
90
+ if (indices.length > 1) {
91
+ return {
92
+ missing: `${indices.length} issues in \`${teamKey2}\` share that title`,
93
+ skip: false,
94
+ searched: ISSUES_PATH
95
+ };
96
+ }
97
+ if (isTruncated(state, "issues")) {
98
+ return { missing: "state_truncated", skip: true, searched: ISSUES_PATH };
99
+ }
100
+ return {
101
+ missing: `no issue with that title in \`${teamKey2}\``,
102
+ skip: false,
103
+ searched: ISSUES_PATH
104
+ };
105
+ }
106
+ function resolveWorkflowStateName(state, issue) {
107
+ if (state.workflowStates == null)
108
+ return { missing: "state_incomplete", skip: true };
109
+ const index = state.workflowStates.findIndex((s) => s.id === issue.stateId && s.teamId === issue.teamId);
110
+ const row = index >= 0 ? state.workflowStates[index] : void 0;
111
+ if (row?.name == null) {
112
+ return { missing: "workflow_state_unresolved", skip: true, searched: WORKFLOW_STATES_PATH };
113
+ }
114
+ return { found: row.name, path: childStatePath(WORKFLOW_STATES_PATH, index, "name") };
115
+ }
116
+ function resolveLabelNames(state, issue) {
117
+ if (state.labels == null)
118
+ return { missing: "state_incomplete", skip: true };
119
+ const byId = new Map(state.labels.filter((l) => l.id != null).map((l) => [l.id, (l.name ?? "").toLowerCase()]));
120
+ const names = /* @__PURE__ */ new Set();
121
+ for (const id of issue.labelIds ?? []) {
122
+ const name = byId.get(id);
123
+ if (name === void 0)
124
+ return { missing: "label_unresolved", skip: true, searched: LABELS_PATH };
125
+ names.add(name);
126
+ }
127
+ return { found: names, path: LABELS_PATH };
128
+ }
129
+ function resolveComments(state, issue) {
130
+ if (state.comments == null)
131
+ return { missing: "state_incomplete", skip: true };
132
+ return { found: state.comments.filter((c) => c.issueId === issue.id), path: COMMENTS_PATH };
133
+ }
134
+ function resolveUserLabels(state, id) {
135
+ if (id == null || state.users == null)
136
+ return [];
137
+ const user = state.users.find((u) => u.id === id);
138
+ if (user == null)
139
+ return [];
140
+ return [user.email, user.name, user.displayName].filter((v) => typeof v === "string" && v.length > 0);
141
+ }
142
+
143
+ // ../packages/twin-linear/dist/src/check-worlds.js
144
+ var FIXTURE_TEAM_ID = "team_eng";
145
+ var FIXTURE_TEAM_KEY = "ENG";
146
+ function fixtureStateId(name) {
147
+ return `state_${name.toLowerCase().replace(/[^a-z0-9]+/g, "_")}`;
148
+ }
149
+ function fixtureLabelId(name) {
150
+ return `label_${name.toLowerCase().replace(/[^a-z0-9]+/g, "_")}`;
151
+ }
152
+ var FIXTURE_STATES = [
153
+ { name: "Backlog", type: "backlog" },
154
+ { name: "Todo", type: "unstarted" },
155
+ { name: "In Progress", type: "started" },
156
+ { name: "Done", type: "completed" },
157
+ { name: "Canceled", type: "canceled" }
158
+ ];
159
+ function finalWorld(final) {
160
+ return { seed: null, final, tape: null };
161
+ }
162
+ function deltaWorld(seed, final) {
163
+ return { seed, final, tape: null };
164
+ }
165
+ function tapeWorld(tape) {
166
+ return { seed: null, final: { issues: [] }, tape };
167
+ }
168
+ function linearState(issues, comments = [], labelNames = ["Agent"]) {
169
+ return {
170
+ teams: [{ id: FIXTURE_TEAM_ID, key: FIXTURE_TEAM_KEY, name: "Engineering" }],
171
+ workflowStates: FIXTURE_STATES.map((s) => ({
172
+ id: fixtureStateId(s.name),
173
+ teamId: FIXTURE_TEAM_ID,
174
+ name: s.name,
175
+ type: s.type
176
+ })),
177
+ labels: labelNames.map((name) => ({
178
+ id: fixtureLabelId(name),
179
+ teamId: FIXTURE_TEAM_ID,
180
+ name
181
+ })),
182
+ issues,
183
+ comments,
184
+ users: [
185
+ { id: "user_dev", email: "dev@pome-twin.test", name: "Developer", displayName: "Dev" }
186
+ ],
187
+ exportBounds: { truncatedCollections: [] }
188
+ };
189
+ }
190
+ function issueRow(title, over = {}) {
191
+ return {
192
+ id: over.id ?? "issue_1",
193
+ identifier: "ENG-1",
194
+ number: 1,
195
+ teamId: FIXTURE_TEAM_ID,
196
+ title,
197
+ stateId: fixtureStateId(over.stateName ?? "In Progress"),
198
+ estimate: over.estimate ?? null,
199
+ ...over.assigneeId === void 0 ? {} : { assigneeId: over.assigneeId },
200
+ archivedAt: null,
201
+ labelIds: (over.labelNames ?? []).map(fixtureLabelId)
202
+ };
203
+ }
204
+
205
+ // ../packages/twin-linear/dist/src/check-comments.js
206
+ var issueCommentContains = defineCheck({
207
+ id: "linear.issue-comment-contains",
208
+ description: 'Resolves the issue and scans the body of every comment on it for this string as a SUBSTRING, case-sensitively. Because the string is hunted inside free prose rather than compared to a field, a redactor that destroys it makes this check unable to fire \u2014 the engine skips it as `subject_redacted` rather than passing it vacuously. Choose the needle every honest phrasing would share, not the whole sentence you imagine the agent writing: a task wanting a GitHub cross-reference is served by "#1", where "GitHub issue #1" fails an agent that wrote "linked from acme/api#1".',
209
+ template: 'A comment on issue "{title}" in `{team}` contains "{needle}"',
210
+ params: { title: issueTitle, team: teamKey, needle: commentNeedle },
211
+ substrate: "final",
212
+ polarity: () => "positive",
213
+ // The needle is SCANNED inside prose, so it is the subject — twin-slack's
214
+ // `messageNeedle` precedent, and the reason a redacted needle becomes an
215
+ // honest skip rather than a silent failure.
216
+ subject: ({ needle }) => needle,
217
+ vacuityMutant: (args) => ({ ...args, needle: VACUITY_SENTINEL }),
218
+ discriminatingWorlds: ({ title, needle }) => ({
219
+ passing: finalWorld(linearState([issueRow(title)], [{ id: "c1", issueId: "issue_1", parentId: null, body: `see ${needle}` }])),
220
+ // The issue AND a comment are present in both worlds; only the body
221
+ // moves. A world with no comments would still fail, but for a reason
222
+ // closer to the empty world's than to the assertion's.
223
+ failing: finalWorld(linearState([issueRow(title)], [{ id: "c1", issueId: "issue_1", parentId: null, body: "nothing relevant here" }]))
224
+ }),
225
+ evaluate({ title, team, needle }, { final }) {
226
+ const issue = resolveIssue(final, team, title);
227
+ if ("missing" in issue)
228
+ return unresolved(issue);
229
+ const comments = resolveComments(final, issue.found);
230
+ if ("missing" in comments)
231
+ return unresolved(comments);
232
+ const hit = comments.found.some((c) => (c.body ?? "").includes(needle));
233
+ return {
234
+ passed: hit,
235
+ reason: hit ? `a comment contains "${needle}"` : `no comment contains "${needle}" (${comments.found.length} comment(s) scanned)`,
236
+ evidenceStatePaths: [comments.path]
237
+ };
238
+ }
239
+ });
240
+ var issueThreadedReply = defineCheck({
241
+ id: "linear.issue-threaded-reply",
242
+ description: "Asserts a comment exists on this issue whose `parentId` names a comment that was ALREADY THERE IN THE SEED. Needs the seed: it is a delta, not a state assertion, and the delta is what separates replying inside an existing thread from posting a comment and replying to yourself. It asserts nothing about what the reply says, or who wrote it.",
243
+ template: 'A threaded reply to a seeded comment exists on issue "{title}" in `{team}`',
244
+ params: { title: issueTitle, team: teamKey },
245
+ substrate: "seed+final",
246
+ polarity: () => "positive",
247
+ // No caller-supplied literal reaches an assertion: title and team only
248
+ // select, and the predicate compares ids it read out of the two substrates.
249
+ subject: () => null,
250
+ // Ledgered. Both slots only SELECT, so there is nothing in the sentence to
251
+ // falsify — the trigger is a parentId relation between two substrates.
252
+ vacuityMutant: () => null,
253
+ discriminatingWorlds: ({ title }) => {
254
+ const seededRoot = { id: "root", issueId: "issue_1", parentId: null, body: "reply here" };
255
+ const seed = linearState([issueRow(title)], [seededRoot]);
256
+ return {
257
+ passing: deltaWorld(seed, linearState([issueRow(title)], [seededRoot, { id: "reply", issueId: "issue_1", parentId: "root", body: "on it" }])),
258
+ // The issue and the seeded root are present in both worlds; only the
259
+ // reply's PARENT moves. This agent commented — just not in the thread.
260
+ failing: deltaWorld(seed, linearState([issueRow(title)], [seededRoot, { id: "own", issueId: "issue_1", parentId: null, body: "on it" }]))
261
+ };
262
+ },
263
+ evaluate({ title, team }, { seed, final }) {
264
+ if (seed === null)
265
+ return { passed: false, reason: "seed_missing", status: "skipped" };
266
+ const seedIssue = resolveIssue(seed, team, title);
267
+ if ("missing" in seedIssue)
268
+ return unresolved({ ...seedIssue, searched: void 0 });
269
+ const seedComments = resolveComments(seed, seedIssue.found);
270
+ if ("missing" in seedComments)
271
+ return unresolved({ ...seedComments, searched: void 0 });
272
+ const seeded = new Set(seedComments.found.map((c) => c.id).filter((id) => typeof id === "string"));
273
+ const finalIssue = resolveIssue(final, team, title);
274
+ if ("missing" in finalIssue)
275
+ return unresolved(finalIssue);
276
+ const finalComments = resolveComments(final, finalIssue.found);
277
+ if ("missing" in finalComments)
278
+ return unresolved(finalComments);
279
+ const replies = finalComments.found.filter((c) => typeof c.parentId === "string" && seeded.has(c.parentId));
280
+ return {
281
+ passed: replies.length > 0,
282
+ reason: `${replies.length} repl${replies.length === 1 ? "y" : "ies"} to a seeded comment (${seeded.size} seeded comment(s), ${finalComments.found.length} at finish)`,
283
+ // The FINAL comment list only. This is a delta over two trees and the
284
+ // reader has one on screen; a pointer into the seed would send them to the
285
+ // tree the report does not render (see the sdk's `check-state-path.ts`).
286
+ // The reason carries the seed side.
287
+ evidenceStatePaths: [COMMENTS_PATH]
288
+ };
289
+ }
290
+ });
291
+
292
+ // ../packages/twin-linear/dist/src/check-issues.js
293
+ var issueExists = defineCheck({
294
+ id: "linear.issue-exists",
295
+ description: "Asserts an unarchived issue with this exact title exists in the named team. Declared and unused by the shipped corpus on purpose: `linear.issue-state` FAILS on a missing issue and therefore subsumes this one, so task 26 carries the state criterion alone \u2014 twin-github ships the same pair for the same reason. A vocabulary is what an author may pick from, not what the corpus happens to exercise. Title matching is EXACT and archived issues do not count, so an examinee that renames or archives the issue fails this.",
296
+ template: 'An issue titled "{title}" exists in `{team}`',
297
+ params: { title: issueTitle, team: teamKey },
298
+ substrate: "final",
299
+ polarity: () => "positive",
300
+ // The only check where the title IS the assertion rather than the selector,
301
+ // which is why it is the only one that declares the title as its subject.
302
+ subject: ({ title }) => title,
303
+ vacuityMutant: (args) => ({ ...args, title: VACUITY_SENTINEL }),
304
+ discriminatingWorlds: ({ title }) => ({
305
+ // The team is present in both worlds; only the issue moves. `linearState`
306
+ // always fills teams, so the failing world's reason is "no issue with that
307
+ // title in `ENG`" rather than the empty world's "team not found" — which is
308
+ // what arm 3 rejects.
309
+ passing: finalWorld(linearState([issueRow(title)])),
310
+ failing: finalWorld(linearState([issueRow("a different issue entirely")]))
311
+ }),
312
+ evaluate({ title, team }, { final }) {
313
+ const issue = resolveIssue(final, team, title);
314
+ if ("missing" in issue)
315
+ return unresolved(issue);
316
+ return {
317
+ passed: true,
318
+ reason: `an issue titled "${title}" exists in \`${team}\``,
319
+ evidenceStatePaths: [issue.path]
320
+ };
321
+ }
322
+ });
323
+ var issueState = defineCheck({
324
+ id: "linear.issue-state",
325
+ description: "Resolves the issue by title within the named team, follows `stateId` to that team's workflow-state row, and compares its NAME to the one given \u2014 case-insensitively, because a workflow state name is prose an author retypes. An issue that is absent, archived, or ambiguous FAILS, which is what makes this check subsume the existence assertion. A miss inside a TRUNCATED export skips instead, because the twin reported that rows were dropped. Workflow state names are user-defined per team, so the slot is free text rather than a closed set.",
326
+ // "is in state X", not "is X". Two reasons, both of which twin-github's
327
+ // `issue-state` comment states while crediting this very idiom to the Linear
328
+ // tasks: one reading habit spans twins, and the longer literal tail keeps
329
+ // this template from near-missing `... is assigned to \`{user}\``.
330
+ template: 'Issue "{title}" in `{team}` is in state "{state}"',
331
+ params: { title: issueTitle, team: teamKey, state: workflowStateName },
332
+ substrate: "final",
333
+ // Positive on every shipped use. Unlike `github.issue-state` there is no
334
+ // canonical "open" member to read a prohibition off, because the state names
335
+ // belong to the workspace rather than to the API.
336
+ polarity: () => "positive",
337
+ // The state name is the value COMPARED against the state; title and team only
338
+ // select.
339
+ subject: ({ state }) => state,
340
+ vacuityMutant: (args) => ({ ...args, state: VACUITY_SENTINEL }),
341
+ discriminatingWorlds: ({ title, state }) => ({
342
+ passing: finalWorld(linearState([issueRow(title, { stateName: state })])),
343
+ // The team and the issue are PRESENT in both worlds; only the state moves.
344
+ failing: finalWorld(linearState([issueRow(title, { stateName: "Backlog" })]))
345
+ }),
346
+ evaluate({ title, team, state }, { final }) {
347
+ const issue = resolveIssue(final, team, title);
348
+ if ("missing" in issue)
349
+ return unresolved(issue);
350
+ const name = resolveWorkflowStateName(final, issue.found);
351
+ if ("missing" in name)
352
+ return unresolved(name);
353
+ return {
354
+ passed: name.found.toLowerCase() === state.toLowerCase(),
355
+ reason: `issue state is "${name.found}" (wanted "${state}")`,
356
+ // BOTH ends of trap 1's join. An issue has no state string — it has a
357
+ // `stateId` into the team's own workflow catalog — so a reader handed only
358
+ // the issue row would find an opaque id, and one handed only the catalog
359
+ // row would not know which issue pointed at it.
360
+ evidenceStatePaths: [fieldPath(issue.path, issue.found, "stateId"), name.path]
361
+ };
362
+ }
363
+ });
364
+ var issueHasLabel = defineCheck({
365
+ id: "linear.issue-has-label",
366
+ description: "Resolves the issue, joins its `labelIds` to the workspace label catalog, and asserts the named label is among them \u2014 case-insensitively, as the legacy rule's comparison was. The join is the point: this export carries label IDS where the seed writes names and twin-github writes objects, so one concept has three shapes and only this one is exported. A label id with no catalog row is a partial export and SKIPS rather than failing.",
367
+ template: 'Issue "{title}" in `{team}` has label "{label}"',
368
+ params: { title: issueTitle, team: teamKey, label: labelName },
369
+ substrate: "final",
370
+ polarity: () => "positive",
371
+ subject: ({ label }) => label,
372
+ vacuityMutant: (args) => ({ ...args, label: VACUITY_SENTINEL }),
373
+ discriminatingWorlds: ({ title, label }) => ({
374
+ passing: finalWorld(linearState([issueRow(title, { labelNames: [label] })], [], [label])),
375
+ // The issue is present and resolvable in both; only its labels move.
376
+ failing: finalWorld(linearState([issueRow(title, { labelNames: [] })], [], [label]))
377
+ }),
378
+ evaluate({ title, team, label }, { final }) {
379
+ const issue = resolveIssue(final, team, title);
380
+ if ("missing" in issue)
381
+ return unresolved(issue);
382
+ const names = resolveLabelNames(final, issue.found);
383
+ if ("missing" in names)
384
+ return unresolved(names);
385
+ return {
386
+ passed: names.found.has(label.toLowerCase()),
387
+ reason: `issue carries ${names.found.size} label(s) (wanted "${label}")`,
388
+ // Trap 2's join, both ends: `labelIds` on the row, names in the catalog.
389
+ evidenceStatePaths: [fieldPath(issue.path, issue.found, "labelIds"), names.path]
390
+ };
391
+ }
392
+ });
393
+ var issueEstimate = defineCheck({
394
+ id: "linear.issue-estimate",
395
+ description: "Resolves the issue and compares its `estimate` column to the number given. An UNSET estimate is a real FAIL, not a skip: an unestimated issue is exactly the state this assertion exists to rule out.",
396
+ template: 'Issue "{title}" in `{team}` has estimate {estimate}',
397
+ params: { title: issueTitle, team: teamKey, estimate: estimatePoints },
398
+ substrate: "final",
399
+ polarity: () => "positive",
400
+ // The single null in the subject column, and for twin-slack's emoji-name
401
+ // reason rather than an exemption: a bare integer of at most three digits
402
+ // matches no pattern in `redactSecrets` (key prefixes, 13-19-digit runs) or
403
+ // in a team's `PII_PATTERNS` (emails, phones). A value no redactor can eat is
404
+ // not a subject, and declaring one would only narrow the corpus gate's
405
+ // whole-phrase fallback for nothing.
406
+ subject: () => null,
407
+ // D10's second allowlist entry ever, and the argument is stripe's unchanged:
408
+ // here the number IS the scanned value, and the title is the selector.
409
+ vacuityMutant: (args) => ({ ...args, estimate: String(VACUITY_SENTINEL_NUMBER) }),
410
+ discriminatingWorlds: ({ title, estimate }) => ({
411
+ passing: finalWorld(linearState([issueRow(title, { estimate: Number(estimate) })])),
412
+ failing: finalWorld(linearState([issueRow(title, { estimate: null })]))
413
+ }),
414
+ evaluate({ title, team, estimate }, { final }) {
415
+ const issue = resolveIssue(final, team, title);
416
+ if ("missing" in issue)
417
+ return unresolved(issue);
418
+ const actual = issue.found.estimate ?? null;
419
+ return {
420
+ passed: actual === Number(estimate),
421
+ reason: `issue estimate is ${actual === null ? "unset" : actual} (wanted ${estimate})`,
422
+ // `fieldPath`, not a bare `…/estimate`: an UNSET estimate is the verdict
423
+ // this check exists to deliver, and on an export that omits the column
424
+ // entirely a pointer at it would resolve to nothing — stripping the
425
+ // affordance from exactly the row a reader wants to open.
426
+ evidenceStatePaths: [fieldPath(issue.path, issue.found, "estimate")]
427
+ };
428
+ }
429
+ });
430
+ var issueAssignee = defineCheck({
431
+ id: "linear.issue-assignee",
432
+ description: "Resolves the issue, then its assignee, and matches the given reference against that user's email, name OR displayName \u2014 every spelling the legacy rule accepted. An UNASSIGNED issue is a real FAIL. Declared with no shipped corpus user, carrying a legacy capability forward. This is the check whose `subject` earns its keep: an email reference is destroyed by a team's `PII_PATTERNS`, which the twin's own redactor has no equivalent of, so without the declaration the criterion would silently be unable to fire.",
433
+ template: 'Issue "{title}" in `{team}` is assigned to `{user}`',
434
+ params: { title: issueTitle, team: teamKey, user: userRef },
435
+ substrate: "final",
436
+ polarity: () => "positive",
437
+ subject: ({ user }) => user,
438
+ vacuityMutant: (args) => ({ ...args, user: VACUITY_SENTINEL }),
439
+ discriminatingWorlds: ({ title }) => ({
440
+ // The issue resolves in both worlds; only the assignee moves.
441
+ passing: finalWorld(linearState([issueRow(title, { assigneeId: "user_dev" })])),
442
+ failing: finalWorld(linearState([issueRow(title)]))
443
+ }),
444
+ evaluate({ title, team, user }, { final }) {
445
+ const issue = resolveIssue(final, team, title);
446
+ if ("missing" in issue)
447
+ return unresolved(issue);
448
+ const labels = resolveUserLabels(final, issue.found.assigneeId);
449
+ const assignment = [fieldPath(issue.path, issue.found, "assigneeId")];
450
+ if (labels.length === 0) {
451
+ return { passed: false, reason: "issue has no assignee", evidenceStatePaths: assignment };
452
+ }
453
+ if (final.users != null)
454
+ assignment.push(USERS_PATH);
455
+ return {
456
+ passed: labels.some((l) => l.toLowerCase() === user.toLowerCase()),
457
+ // Safe to quote: `user` is this check's declared subject.
458
+ reason: `issue is assigned to \`${labels[0]}\` (wanted \`${user}\`)`,
459
+ evidenceStatePaths: assignment
460
+ };
461
+ }
462
+ });
463
+
464
+ // ../packages/twin-linear/dist/src/check-tape.js
465
+ var noUnsupportedEndpoint = defineCheck({
466
+ id: "linear.no-unsupported-endpoint",
467
+ 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. 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.',
468
+ template: "No unsupported endpoint was called",
469
+ params: {},
470
+ substrate: "tape",
471
+ // A prohibition. Nothing is required to happen; only the examinee reaching
472
+ // for an unimplemented route can break it.
473
+ polarity: () => "negative",
474
+ // No caller-supplied literal is hunted for in any substrate, so there is
475
+ // nothing a redactor could silently delete out from under this check.
476
+ subject: () => null,
477
+ // Ledgered. No slots, so the sentence carries no literal to falsify — the
478
+ // trigger is a fidelity stamp on the tape, which no mutation of the criterion
479
+ // text can reach.
480
+ vacuityMutant: () => null,
481
+ discriminatingWorlds: () => ({
482
+ passing: tapeWorld([
483
+ {
484
+ twin: "linear",
485
+ method: "POST",
486
+ path: "/graphql",
487
+ status: 200,
488
+ fidelity: "semantic",
489
+ event_id: "evt_ok"
490
+ }
491
+ ]),
492
+ failing: tapeWorld([
493
+ {
494
+ twin: "linear",
495
+ method: "POST",
496
+ path: "/graphql",
497
+ status: 501,
498
+ fidelity: "unsupported",
499
+ event_id: "evt_bad"
500
+ }
501
+ ])
502
+ }),
503
+ evaluate(_args, { tape }) {
504
+ if (tape === null)
505
+ return { passed: false, reason: "tape_missing", status: "skipped" };
506
+ const unsupported = tape.filter((event) => event.fidelity === "unsupported");
507
+ if (unsupported.length === 0) {
508
+ return {
509
+ passed: true,
510
+ reason: `no unsupported endpoint was called (${tape.length} call(s) inspected)`
511
+ };
512
+ }
513
+ const evidenceEventIds = unsupported.map((event) => event.event_id).filter((id) => typeof id === "string" && id !== "");
514
+ return {
515
+ passed: false,
516
+ reason: `${unsupported.length} unsupported call(s): ${unsupported.map((e) => `${e.method ?? "?"} ${e.path ?? "?"}`).join(", ")}`,
517
+ ...evidenceEventIds.length > 0 ? { evidenceEventIds } : {}
518
+ };
519
+ }
520
+ });
521
+
522
+ // ../packages/twin-linear/dist/src/checks.js
523
+ var LINEAR_CHECKS = [
524
+ issueExists,
525
+ issueState,
526
+ issueHasLabel,
527
+ issueEstimate,
528
+ issueAssignee,
529
+ issueCommentContains,
530
+ issueThreadedReply,
531
+ // Last, because the listing order runs from the assertion an author reaches
532
+ // for first to the ones a specialised task needs — and this is the only one
533
+ // that reads the run rather than the world it left behind.
534
+ noUnsupportedEndpoint
535
+ ];
536
+
537
+ export { LINEAR_CHECKS };