lorenz 0.1.10 → 0.1.12

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 (76) hide show
  1. package/README.md +1 -1
  2. package/RELEASE-MANIFEST.json +1 -1
  3. package/node_modules/@agentclientprotocol/claude-agent-acp/dist/bundle.js +1 -1
  4. package/node_modules/@lorenz/config/dist/parse.d.ts.map +1 -1
  5. package/node_modules/@lorenz/config/dist/parse.js +88 -21
  6. package/node_modules/@lorenz/config/dist/parse.js.map +1 -1
  7. package/node_modules/@lorenz/dashboard/dist/assets/index-CInuwNxv.js +227 -0
  8. package/node_modules/@lorenz/dashboard/dist/index.html +1 -1
  9. package/node_modules/@lorenz/domain/dist/index.d.ts +6 -0
  10. package/node_modules/@lorenz/domain/dist/index.d.ts.map +1 -1
  11. package/node_modules/@lorenz/domain/dist/index.js +83 -0
  12. package/node_modules/@lorenz/domain/dist/index.js.map +1 -1
  13. package/node_modules/@lorenz/jira-tracker/dist/client.d.ts +13 -2
  14. package/node_modules/@lorenz/jira-tracker/dist/client.d.ts.map +1 -1
  15. package/node_modules/@lorenz/jira-tracker/dist/client.js +90 -34
  16. package/node_modules/@lorenz/jira-tracker/dist/client.js.map +1 -1
  17. package/node_modules/@lorenz/jira-tracker/dist/tools.d.ts.map +1 -1
  18. package/node_modules/@lorenz/jira-tracker/dist/tools.js +2 -2
  19. package/node_modules/@lorenz/jira-tracker/dist/tools.js.map +1 -1
  20. package/node_modules/@lorenz/linear-tracker/dist/client.d.ts +22 -0
  21. package/node_modules/@lorenz/linear-tracker/dist/client.d.ts.map +1 -1
  22. package/node_modules/@lorenz/linear-tracker/dist/client.js +357 -107
  23. package/node_modules/@lorenz/linear-tracker/dist/client.js.map +1 -1
  24. package/node_modules/@lorenz/linear-tracker/dist/diagnostics.d.ts +3 -0
  25. package/node_modules/@lorenz/linear-tracker/dist/diagnostics.d.ts.map +1 -0
  26. package/node_modules/@lorenz/linear-tracker/dist/diagnostics.js +30 -0
  27. package/node_modules/@lorenz/linear-tracker/dist/diagnostics.js.map +1 -0
  28. package/node_modules/@lorenz/linear-tracker/dist/tools.d.ts.map +1 -1
  29. package/node_modules/@lorenz/linear-tracker/dist/tools.js +8 -33
  30. package/node_modules/@lorenz/linear-tracker/dist/tools.js.map +1 -1
  31. package/node_modules/@lorenz/orchestrator/dist/codec.js +2 -0
  32. package/node_modules/@lorenz/orchestrator/dist/codec.js.map +1 -1
  33. package/node_modules/@lorenz/orchestrator/dist/index.d.ts.map +1 -1
  34. package/node_modules/@lorenz/orchestrator/dist/index.js +15 -0
  35. package/node_modules/@lorenz/orchestrator/dist/index.js.map +1 -1
  36. package/node_modules/@lorenz/presenter/dist/index.d.ts.map +1 -1
  37. package/node_modules/@lorenz/presenter/dist/index.js +7 -7
  38. package/node_modules/@lorenz/presenter/dist/index.js.map +1 -1
  39. package/node_modules/@lorenz/projections/dist/index.d.ts.map +1 -1
  40. package/node_modules/@lorenz/projections/dist/index.js +1 -0
  41. package/node_modules/@lorenz/projections/dist/index.js.map +1 -1
  42. package/node_modules/@lorenz/runtime/dist/index.d.ts +1 -0
  43. package/node_modules/@lorenz/runtime/dist/index.d.ts.map +1 -1
  44. package/node_modules/@lorenz/runtime/dist/index.js +28 -15
  45. package/node_modules/@lorenz/runtime/dist/index.js.map +1 -1
  46. package/node_modules/@lorenz/runtime-events/dist/index.d.ts +3 -0
  47. package/node_modules/@lorenz/runtime-events/dist/index.d.ts.map +1 -1
  48. package/node_modules/@lorenz/traceviz-emitter/dist/index.d.ts +1 -1
  49. package/node_modules/@lorenz/traceviz-emitter/dist/index.d.ts.map +1 -1
  50. package/node_modules/@lorenz/traceviz-emitter/dist/index.js +3 -2
  51. package/node_modules/@lorenz/traceviz-emitter/dist/index.js.map +1 -1
  52. package/node_modules/@lorenz/traceviz-server/dist/parser.js +2 -2
  53. package/node_modules/@lorenz/traceviz-server/dist/parser.js.map +1 -1
  54. package/node_modules/@lorenz/tracker-sdk/dist/index.d.ts +1 -0
  55. package/node_modules/@lorenz/tracker-sdk/dist/index.d.ts.map +1 -1
  56. package/node_modules/@lorenz/tracker-sdk/dist/index.js +1 -0
  57. package/node_modules/@lorenz/tracker-sdk/dist/index.js.map +1 -1
  58. package/node_modules/@lorenz/tracker-sdk/dist/pagination.d.ts +27 -0
  59. package/node_modules/@lorenz/tracker-sdk/dist/pagination.d.ts.map +1 -0
  60. package/node_modules/@lorenz/tracker-sdk/dist/pagination.js +58 -0
  61. package/node_modules/@lorenz/tracker-sdk/dist/pagination.js.map +1 -0
  62. package/node_modules/@lorenz/tui/dist/index.d.ts +3 -1
  63. package/node_modules/@lorenz/tui/dist/index.d.ts.map +1 -1
  64. package/node_modules/@lorenz/tui/dist/index.js +21 -24
  65. package/node_modules/@lorenz/tui/dist/index.js.map +1 -1
  66. package/node_modules/@lorenz/worker-pool/dist/pool.d.ts.map +1 -1
  67. package/node_modules/@lorenz/worker-pool/dist/pool.js +15 -0
  68. package/node_modules/@lorenz/worker-pool/dist/pool.js.map +1 -1
  69. package/node_modules/@lorenz/worker-pool/dist/reaper.d.ts +10 -0
  70. package/node_modules/@lorenz/worker-pool/dist/reaper.d.ts.map +1 -1
  71. package/node_modules/@lorenz/worker-pool/dist/reaper.js +11 -1
  72. package/node_modules/@lorenz/worker-pool/dist/reaper.js.map +1 -1
  73. package/node_modules/@lorenz/worker-pool/dist/types.d.ts +6 -4
  74. package/node_modules/@lorenz/worker-pool/dist/types.d.ts.map +1 -1
  75. package/package.json +1 -1
  76. package/node_modules/@lorenz/dashboard/dist/assets/index-DD4yVBDF.js +0 -227
@@ -1,10 +1,12 @@
1
1
  import { LinearGraphQLClient } from "@linear/sdk";
2
2
  export { LinearGraphQLClient } from "@linear/sdk";
3
3
  import { normalizeIssue } from "@lorenz/issue";
4
- import { errorMessage, isRecord, normalizeStateType, } from "@lorenz/domain";
4
+ import { createTrackerPaginationGuard } from "@lorenz/tracker-sdk";
5
+ import { errorMessage, isRecord, normalizeStateType, redactDiagnosticText, } from "@lorenz/domain";
6
+ import { linearErrorContext } from "./diagnostics.js";
5
7
  import { linearEndpoint, linearTrackerOptions } from "./options.js";
6
8
  const LINEAR_REQUEST_TIMEOUT_MS = 30_000;
7
- const MAX_ERROR_BODY_LOG_BYTES = 1000;
9
+ const LINEAR_CONNECTION_PAGE_SIZE = 50;
8
10
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
9
11
  const issueFields = `
10
12
  id
@@ -42,6 +44,7 @@ export class LinearClient {
42
44
  settings;
43
45
  fetchImpl;
44
46
  logger;
47
+ paginationLimits;
45
48
  constructor(settings, fetchImplOrDeps, retryOptions = {}) {
46
49
  this.settings = settings;
47
50
  this.retryOptions = {
@@ -58,6 +61,7 @@ export class LinearClient {
58
61
  warn: (message) => console.warn(message),
59
62
  error: (message) => console.error(message),
60
63
  };
64
+ this.paginationLimits = deps.paginationLimits;
61
65
  if (deps.graphqlClient) {
62
66
  this.gqlClient = deps.graphqlClient;
63
67
  }
@@ -106,9 +110,11 @@ export class LinearClient {
106
110
  catch (error) {
107
111
  if (!response.ok) {
108
112
  this.logStatusError(query, response.status, errorBodyText ?? errorMessage(error));
109
- throw new Error(`linear api status ${response.status}`, { cause: error });
113
+ // eslint-disable-next-line preserve-caught-error -- Secret-boundary rethrows must not retain provider error objects.
114
+ throw new Error(`linear api status ${response.status}`);
110
115
  }
111
- throw new Error(`linear_invalid_json: ${errorMessage(error)}`, { cause: error });
116
+ // eslint-disable-next-line preserve-caught-error -- Secret-boundary rethrows must not retain provider error objects.
117
+ throw new Error(`linear_invalid_json: ${redactDiagnosticText(errorMessage(error))}`);
112
118
  }
113
119
  if (response.status === 429) {
114
120
  this.logStatusError(query, response.status, body);
@@ -116,7 +122,7 @@ export class LinearClient {
116
122
  }
117
123
  if (isRecord(body) && Array.isArray(body.errors) && body.errors.length > 0) {
118
124
  this.logStatusError(query, response.status, body);
119
- throw new Error(`linear_graphql_errors: ${JSON.stringify(body.errors)}`);
125
+ throw new Error(`linear_graphql_errors: ${redactDiagnosticText(JSON.stringify(body.errors))}`);
120
126
  }
121
127
  if (!response.ok) {
122
128
  this.logStatusError(query, response.status, body);
@@ -144,7 +150,8 @@ export class LinearClient {
144
150
  }
145
151
  catch (error) {
146
152
  this.logRequestError(query, error);
147
- throw error;
153
+ // eslint-disable-next-line preserve-caught-error -- Secret-boundary rethrows must not retain provider error objects.
154
+ throw new Error(redactDiagnosticText(errorMessage(error)));
148
155
  }
149
156
  if (response.status !== 429 || retryCount >= this.retryOptions.maxRetries)
150
157
  return response;
@@ -182,7 +189,7 @@ export class LinearClient {
182
189
  const project = data.projects.nodes[0];
183
190
  if (!project)
184
191
  throw new Error(`linear project not found: ${projectSlug}`);
185
- return parseProject(project);
192
+ return this.parseProject(project);
186
193
  }
187
194
  async fetchCandidateIssues() {
188
195
  return this.fetchIssuesByStates(this.settings.tracker.activeStates);
@@ -195,7 +202,13 @@ export class LinearClient {
195
202
  const issues = [];
196
203
  const assignee = await this.assigneeFilterValue();
197
204
  const projectSlugs = await this.resolveProjectSlugs();
205
+ const pagination = createTrackerPaginationGuard({
206
+ tracker: "linear",
207
+ resource: "issues",
208
+ limits: this.paginationLimits,
209
+ });
198
210
  for (;;) {
211
+ pagination.recordPage();
199
212
  const data = await this.graphql(`query LorenzTsPoll($projectSlugs: [String!]!, $stateNames: [String!]!, $first: Int!, $after: String) {
200
213
  issues(filter: {project: {slugId: {in: $projectSlugs}}, state: {name: {in: $stateNames}}}, first: $first, after: $after) {
201
214
  nodes { ${issueFields} }
@@ -207,18 +220,22 @@ export class LinearClient {
207
220
  first: 50,
208
221
  after,
209
222
  });
210
- appendNormalizedIssues(issues, data.issues.nodes, assignee);
223
+ pagination.recordItems(data.issues.nodes.length);
224
+ await this.appendNormalizedIssues(issues, data.issues.nodes, assignee);
211
225
  if (!data.issues.pageInfo.hasNextPage)
212
226
  return issues;
213
- if (!data.issues.pageInfo.endCursor)
214
- throw new Error("linear_missing_end_cursor");
215
- after = data.issues.pageInfo.endCursor;
227
+ after = pagination.nextCursor(data.issues.pageInfo.endCursor, "endCursor");
216
228
  }
217
229
  }
218
230
  async fetchIssuesByIds(ids) {
219
231
  const uniqueIds = [...new Set(ids)];
220
232
  if (uniqueIds.length === 0)
221
233
  return [];
234
+ createTrackerPaginationGuard({
235
+ tracker: "linear",
236
+ resource: "issuesByIds",
237
+ limits: this.paginationLimits,
238
+ }).recordItems(uniqueIds.length);
222
239
  const assignee = await this.assigneeFilterValue();
223
240
  const issueOrder = new Map(uniqueIds.map((id, index) => [id, index]));
224
241
  const issues = [];
@@ -230,7 +247,7 @@ export class LinearClient {
230
247
  nodes { ${issueFields} }
231
248
  }
232
249
  }`, { ids: batchIds, first: batchIds.length });
233
- appendNormalizedIssues(issues, data.issues.nodes, assignee);
250
+ await this.appendNormalizedIssues(issues, data.issues.nodes, assignee);
234
251
  }
235
252
  catch (error) {
236
253
  // Identifier-shaped inputs ("MT-32" from workspace directory names) can make
@@ -255,7 +272,7 @@ export class LinearClient {
255
272
  }
256
273
  }`, { id });
257
274
  if (data.issue)
258
- appendNormalizedIssues(issues, [data.issue], assignee);
275
+ await this.appendNormalizedIssues(issues, [data.issue], assignee);
259
276
  }
260
277
  catch {
261
278
  // Best-effort identifier resolution.
@@ -286,7 +303,10 @@ export class LinearClient {
286
303
  });
287
304
  if (!data.issueCreate.success || !data.issueCreate.issue)
288
305
  throw new Error("linear issueCreate failed");
289
- return normalizeIssue(linearIssuePayload(data.issueCreate.issue), await this.assigneeFilterValue());
306
+ const normalized = await this.normalizeLinearIssue(data.issueCreate.issue, await this.assigneeFilterValue());
307
+ if (!normalized)
308
+ throw new Error("linear issueCreate returned malformed issue");
309
+ return normalized;
290
310
  }
291
311
  async updateIssueState(issueId, stateId) {
292
312
  const data = await this.graphql(`mutation LorenzTsUpdateIssue($id: String!, $input: IssueUpdateInput!) {
@@ -297,7 +317,10 @@ export class LinearClient {
297
317
  }`, { id: issueId, input: { stateId } });
298
318
  if (!data.issueUpdate.success || !data.issueUpdate.issue)
299
319
  throw new Error("linear issueUpdate failed");
300
- return normalizeIssue(linearIssuePayload(data.issueUpdate.issue), await this.assigneeFilterValue());
320
+ const normalized = await this.normalizeLinearIssue(data.issueUpdate.issue, await this.assigneeFilterValue());
321
+ if (!normalized)
322
+ throw new Error("linear issueUpdate returned malformed issue");
323
+ return normalized;
301
324
  }
302
325
  async archiveIssue(issueId) {
303
326
  const data = await this.graphql(`mutation LorenzTsArchiveIssue($id: String!) {
@@ -335,24 +358,276 @@ export class LinearClient {
335
358
  async resolveProjectSlugsByLabels(labels) {
336
359
  let after = null;
337
360
  const slugs = [];
361
+ const pagination = createTrackerPaginationGuard({
362
+ tracker: "linear",
363
+ resource: "projectsByLabels",
364
+ limits: this.paginationLimits,
365
+ });
338
366
  for (;;) {
367
+ pagination.recordPage();
339
368
  const data = await this.graphql(`query LorenzTsProjectsByLabels($labels: [String!]!, $first: Int!, $after: String) {
340
369
  projects(filter: {labels: {name: {in: $labels}}}, first: $first, after: $after) {
341
370
  nodes { slugId }
342
371
  pageInfo { hasNextPage endCursor }
343
372
  }
344
373
  }`, { labels, first: 100, after });
374
+ pagination.recordItems(data.projects.nodes.length);
345
375
  slugs.push(...data.projects.nodes.map((p) => p.slugId));
346
376
  if (!data.projects.pageInfo?.hasNextPage)
347
377
  break;
348
- if (!data.projects.pageInfo.endCursor)
349
- throw new Error("linear_missing_end_cursor: projectsByLabels");
350
- after = data.projects.pageInfo.endCursor;
378
+ after = pagination.nextCursor(data.projects.pageInfo.endCursor, "endCursor");
351
379
  }
352
380
  if (slugs.length === 0)
353
381
  throw new Error(`no linear projects found for labels: ${labels.join(", ")}`);
354
382
  return slugs;
355
383
  }
384
+ async appendNormalizedIssues(target, nodes, assignee) {
385
+ for (const issue of nodes) {
386
+ const normalized = await this.normalizeLinearIssue(issue, assignee);
387
+ if (normalized)
388
+ target.push(normalized);
389
+ }
390
+ }
391
+ async normalizeLinearIssue(issue, assignee) {
392
+ if (!isRecord(issue))
393
+ return null;
394
+ try {
395
+ return normalizeIssue(await this.linearIssuePayload(issue), assignee);
396
+ }
397
+ catch {
398
+ return null;
399
+ }
400
+ }
401
+ async linearIssuePayload(issue) {
402
+ const source = issueSource(issue);
403
+ const issueId = stringField(issue, "id");
404
+ const [labels, inverseRelations] = await Promise.all([
405
+ this.completePagedConnection({
406
+ source,
407
+ connection: "issue.labels",
408
+ initial: issue.labels,
409
+ fetchPage: async (after) => this.fetchIssueLabelsPage(issueId, after),
410
+ }),
411
+ this.completePagedConnection({
412
+ source,
413
+ connection: "issue.inverseRelations",
414
+ initial: issue.inverseRelations,
415
+ fetchPage: async (after) => this.fetchIssueInverseRelationsPage(issueId, after),
416
+ }),
417
+ ]);
418
+ const degradedConnections = [
419
+ ...labels.degradedConnections,
420
+ ...inverseRelations.degradedConnections,
421
+ ];
422
+ if (degradedConnections.length > 0)
423
+ this.logDegradedConnections(degradedConnections);
424
+ return linearIssuePayload({
425
+ ...issue,
426
+ labels: connectionFromNodes(labels.nodes),
427
+ inverseRelations: connectionFromNodes(inverseRelations.nodes),
428
+ }, degradedConnections);
429
+ }
430
+ async fetchIssueLabelsPage(issueId, after) {
431
+ const data = await this.graphql(`query LorenzTsIssueLabels($id: String!, $first: Int!, $after: String) {
432
+ issue(id: $id) {
433
+ labels(first: $first, after: $after) {
434
+ nodes { name }
435
+ pageInfo { hasNextPage endCursor }
436
+ }
437
+ }
438
+ }`, { id: issueId, first: LINEAR_CONNECTION_PAGE_SIZE, after });
439
+ if (!data.issue)
440
+ throw new Error(`linear issue not found: ${issueId}`);
441
+ return data.issue.labels;
442
+ }
443
+ async fetchIssueInverseRelationsPage(issueId, after) {
444
+ const data = await this.graphql(`query LorenzTsIssueInverseRelations($id: String!, $first: Int!, $after: String) {
445
+ issue(id: $id) {
446
+ inverseRelations(first: $first, after: $after) {
447
+ nodes {
448
+ type
449
+ issue {
450
+ id
451
+ identifier
452
+ state { name type }
453
+ }
454
+ }
455
+ pageInfo { hasNextPage endCursor }
456
+ }
457
+ }
458
+ }`, { id: issueId, first: LINEAR_CONNECTION_PAGE_SIZE, after });
459
+ if (!data.issue)
460
+ throw new Error(`linear issue not found: ${issueId}`);
461
+ return data.issue.inverseRelations;
462
+ }
463
+ async parseProject(project) {
464
+ const id = stringField(project, "id");
465
+ const name = stringField(project, "name");
466
+ const slugId = stringField(project, "slugId");
467
+ const teams = await this.completePagedConnection({
468
+ source: projectSource(project),
469
+ connection: "project.teams",
470
+ initial: project.teams,
471
+ fetchPage: async (after) => this.fetchProjectTeamsPage(id, after),
472
+ });
473
+ const parsedTeams = await Promise.all(teams.nodes.map(async (team) => this.parseTeam(asRecord(team))));
474
+ const degradedConnections = [
475
+ ...teams.degradedConnections,
476
+ ...parsedTeams.flatMap((team) => team.degradedConnections ?? []),
477
+ ];
478
+ if (degradedConnections.length > 0)
479
+ this.logDegradedConnections(degradedConnections);
480
+ return {
481
+ id,
482
+ name,
483
+ slugId,
484
+ teams: parsedTeams,
485
+ ...(degradedConnections.length > 0 ? { degradedConnections } : {}),
486
+ };
487
+ }
488
+ async fetchProjectTeamsPage(projectId, after) {
489
+ const data = await this.graphql(`query LorenzTsProjectTeams($id: String!, $first: Int!, $after: String) {
490
+ project(id: $id) {
491
+ teams(first: $first, after: $after) {
492
+ nodes {
493
+ id
494
+ key
495
+ name
496
+ states(first: $first) {
497
+ nodes { id name type }
498
+ pageInfo { hasNextPage endCursor }
499
+ }
500
+ }
501
+ pageInfo { hasNextPage endCursor }
502
+ }
503
+ }
504
+ }`, { id: projectId, first: LINEAR_CONNECTION_PAGE_SIZE, after });
505
+ if (!data.project)
506
+ throw new Error(`linear project not found: ${projectId}`);
507
+ return data.project.teams;
508
+ }
509
+ async parseTeam(team) {
510
+ const id = stringField(team, "id");
511
+ const key = stringField(team, "key");
512
+ const name = stringField(team, "name");
513
+ const states = await this.completePagedConnection({
514
+ source: teamSource(team),
515
+ connection: "project.team.states",
516
+ initial: team.states,
517
+ fetchPage: async (after) => this.fetchTeamStatesPage(id, after),
518
+ });
519
+ const degradedConnections = states.degradedConnections;
520
+ return {
521
+ id,
522
+ key,
523
+ name,
524
+ states: states.nodes.map((state) => {
525
+ const stateRecord = asRecord(state);
526
+ return {
527
+ id: stringField(stateRecord, "id"),
528
+ name: stringField(stateRecord, "name"),
529
+ type: normalizeStateType(stringField(stateRecord, "type")),
530
+ };
531
+ }),
532
+ ...(degradedConnections.length > 0 ? { degradedConnections } : {}),
533
+ };
534
+ }
535
+ async fetchTeamStatesPage(teamId, after) {
536
+ const data = await this.graphql(`query LorenzTsTeamStates($id: String!, $first: Int!, $after: String) {
537
+ team(id: $id) {
538
+ states(first: $first, after: $after) {
539
+ nodes { id name type }
540
+ pageInfo { hasNextPage endCursor }
541
+ }
542
+ }
543
+ }`, { id: teamId, first: LINEAR_CONNECTION_PAGE_SIZE, after });
544
+ if (!data.team)
545
+ throw new Error(`linear team not found: ${teamId}`);
546
+ return data.team.states;
547
+ }
548
+ async completePagedConnection(input) {
549
+ const pagination = createTrackerPaginationGuard({
550
+ tracker: "linear",
551
+ resource: input.connection,
552
+ limits: this.paginationLimits,
553
+ });
554
+ const initial = connectionSnapshot(input.initial);
555
+ const nodes = [...initial.nodes];
556
+ try {
557
+ pagination.recordPage();
558
+ pagination.recordItems(initial.nodes.length);
559
+ }
560
+ catch (error) {
561
+ return {
562
+ nodes: [],
563
+ degradedConnections: [degradedConnection(input, errorMessage(error))],
564
+ };
565
+ }
566
+ if (!initial.pageInfo?.hasNextPage)
567
+ return { nodes, degradedConnections: [] };
568
+ let after;
569
+ try {
570
+ after = pagination.nextCursor(initial.pageInfo.endCursor, "endCursor");
571
+ }
572
+ catch (error) {
573
+ return {
574
+ nodes,
575
+ degradedConnections: [
576
+ degradedConnection(input, errorMessage(error), initial.pageInfo.endCursor),
577
+ ],
578
+ };
579
+ }
580
+ for (;;) {
581
+ let page;
582
+ try {
583
+ pagination.recordPage();
584
+ page = await input.fetchPage(after);
585
+ }
586
+ catch (error) {
587
+ return {
588
+ nodes,
589
+ degradedConnections: [degradedConnection(input, errorMessage(error), after)],
590
+ };
591
+ }
592
+ const next = connectionSnapshot(page);
593
+ if (!next.isConnection) {
594
+ return {
595
+ nodes,
596
+ degradedConnections: [
597
+ degradedConnection(input, "linear_invalid_connection_payload", after),
598
+ ],
599
+ };
600
+ }
601
+ try {
602
+ pagination.recordItems(next.nodes.length);
603
+ }
604
+ catch (error) {
605
+ return {
606
+ nodes,
607
+ degradedConnections: [degradedConnection(input, errorMessage(error), after)],
608
+ };
609
+ }
610
+ nodes.push(...next.nodes);
611
+ if (!next.pageInfo?.hasNextPage)
612
+ return { nodes, degradedConnections: [] };
613
+ try {
614
+ after = pagination.nextCursor(next.pageInfo.endCursor, "endCursor");
615
+ }
616
+ catch (error) {
617
+ return {
618
+ nodes,
619
+ degradedConnections: [
620
+ degradedConnection(input, errorMessage(error), next.pageInfo.endCursor),
621
+ ],
622
+ };
623
+ }
624
+ }
625
+ }
626
+ logDegradedConnections(degradedConnections) {
627
+ for (const degraded of degradedConnections) {
628
+ this.logger.warn(`linear tracker degraded connection source=${degraded.source} connection=${degraded.connection} reason=${degraded.reason}${degraded.cursor ? ` cursor=${degraded.cursor}` : ""}`);
629
+ }
630
+ }
356
631
  requiredProjectSlug() {
357
632
  const { projectSlug } = linearTrackerOptions(this.settings);
358
633
  if (!projectSlug)
@@ -388,80 +663,48 @@ export class LinearClient {
388
663
  this.logger.error(`Linear GraphQL request failed status=${status}${linearErrorContext(query, body)}`);
389
664
  }
390
665
  logRequestError(query, error) {
391
- this.logger.error(`Linear GraphQL request failed: ${errorMessage(error)}${linearErrorContext(query)}`);
666
+ this.logger.error(`Linear GraphQL request failed: ${redactDiagnosticText(errorMessage(error))}${linearErrorContext(query)}`);
392
667
  }
393
668
  }
394
669
  function resolveDeps(fetchImplOrDeps) {
395
670
  if (!fetchImplOrDeps)
396
- return { fetchImpl: undefined, graphqlClient: undefined, logger: undefined };
671
+ return {
672
+ fetchImpl: undefined,
673
+ graphqlClient: undefined,
674
+ logger: undefined,
675
+ paginationLimits: undefined,
676
+ };
397
677
  if (typeof fetchImplOrDeps === "function")
398
- return { fetchImpl: fetchImplOrDeps, graphqlClient: undefined, logger: undefined };
678
+ return {
679
+ fetchImpl: fetchImplOrDeps,
680
+ graphqlClient: undefined,
681
+ logger: undefined,
682
+ paginationLimits: undefined,
683
+ };
399
684
  return {
400
685
  fetchImpl: fetchImplOrDeps.fetchImpl ?? undefined,
401
686
  graphqlClient: fetchImplOrDeps.graphqlClient ?? undefined,
402
687
  logger: fetchImplOrDeps.logger ?? undefined,
688
+ paginationLimits: fetchImplOrDeps.paginationLimits ?? undefined,
403
689
  };
404
690
  }
405
- function linearIssuePayload(issue) {
691
+ function linearIssuePayload(issue, degradedConnections = []) {
406
692
  return {
407
693
  ...issue,
408
694
  state: issue.state,
409
695
  state_type: isRecord(issue.state) ? issue.state.type : null,
410
696
  branch_name: issue.branchName,
411
697
  assignee_id: isRecord(issue.assignee) ? issue.assignee.id : null,
412
- labels: nodesFromConnection(issue.labels, "issue.labels"),
413
- relations: nodesFromConnection(issue.inverseRelations, "issue.inverseRelations"),
698
+ labels: connectionSnapshot(issue.labels).nodes,
699
+ relations: connectionSnapshot(issue.inverseRelations).nodes,
414
700
  created_at: issue.createdAt,
415
701
  updated_at: issue.updatedAt,
702
+ ...(degradedConnections.length > 0 ? { linear_degraded_connections: degradedConnections } : {}),
416
703
  };
417
704
  }
418
- function appendNormalizedIssues(target, nodes, assignee) {
419
- for (const issue of nodes) {
420
- const normalized = normalizeLinearIssue(issue, assignee);
421
- if (normalized)
422
- target.push(normalized);
423
- }
424
- }
425
- function normalizeLinearIssue(issue, assignee) {
426
- if (!isRecord(issue))
427
- return null;
428
- try {
429
- return normalizeIssue(linearIssuePayload(issue), assignee);
430
- }
431
- catch (error) {
432
- if (isLinearConnectionTruncatedError(error))
433
- throw error;
434
- return null;
435
- }
436
- }
437
705
  function normalizeStateNames(stateNames) {
438
706
  return [...new Set(stateNames.map((stateName) => String(stateName)))];
439
707
  }
440
- function parseProject(project) {
441
- const teams = nodesFromConnection(project.teams, "project.teams");
442
- return {
443
- id: stringField(project, "id"),
444
- name: stringField(project, "name"),
445
- slugId: stringField(project, "slugId"),
446
- teams: teams.map((team) => {
447
- const teamRecord = asRecord(team);
448
- const states = nodesFromConnection(teamRecord.states, "project.team.states");
449
- return {
450
- id: stringField(teamRecord, "id"),
451
- key: stringField(teamRecord, "key"),
452
- name: stringField(teamRecord, "name"),
453
- states: states.map((state) => {
454
- const stateRecord = asRecord(state);
455
- return {
456
- id: stringField(stateRecord, "id"),
457
- name: stringField(stateRecord, "name"),
458
- type: normalizeStateType(stringField(stateRecord, "type")),
459
- };
460
- }),
461
- };
462
- }),
463
- };
464
- }
465
708
  function isRateLimitError(error) {
466
709
  if (!error || typeof error !== "object")
467
710
  return false;
@@ -484,35 +727,61 @@ function retryDelayFromError(error, options, retryCount) {
484
727
  }
485
728
  function reclassifyError(error) {
486
729
  if (error instanceof Error) {
487
- const msg = error.message;
730
+ const msg = redactDiagnosticText(error.message);
731
+ const cause = redactedLinearCause(error);
488
732
  if (msg.includes("429"))
489
- return new Error("linear api status 429", { cause: error });
733
+ return new Error("linear api status 429", { cause });
490
734
  if (msg.toLowerCase().includes("graphql")) {
491
- return new Error(`linear_graphql_errors: ${msg}`, { cause: error });
735
+ return new Error(`linear_graphql_errors: ${msg}`, { cause });
492
736
  }
493
- return error;
737
+ return new Error(msg, { cause });
494
738
  }
495
- return new Error(String(error));
739
+ return new Error(redactDiagnosticText(String(error)));
496
740
  }
497
- class LinearConnectionTruncatedError extends Error {
498
- constructor(connectionName) {
499
- super(`linear_truncated_connection: ${connectionName}`);
500
- this.name = "LinearConnectionTruncatedError";
501
- }
741
+ function redactedLinearCause(error) {
742
+ return new Error(redactDiagnosticText(errorMessage(error)));
502
743
  }
503
- function isLinearConnectionTruncatedError(error) {
504
- return error instanceof LinearConnectionTruncatedError;
744
+ function isConnection(value) {
745
+ return isRecord(value) && Array.isArray(value.nodes);
505
746
  }
506
- function nodesFromConnection(value, connectionName) {
747
+ function connectionSnapshot(value) {
507
748
  if (!isConnection(value))
508
- return [];
509
- if (isRecord(value.pageInfo) && value.pageInfo.hasNextPage === true) {
510
- throw new LinearConnectionTruncatedError(connectionName);
511
- }
512
- return value.nodes;
749
+ return { nodes: [], pageInfo: null, isConnection: false };
750
+ return { nodes: value.nodes, pageInfo: connectionPageInfo(value.pageInfo), isConnection: true };
513
751
  }
514
- function isConnection(value) {
515
- return isRecord(value) && Array.isArray(value.nodes);
752
+ function connectionPageInfo(value) {
753
+ if (!isRecord(value) || typeof value.hasNextPage !== "boolean")
754
+ return null;
755
+ return {
756
+ hasNextPage: value.hasNextPage,
757
+ endCursor: typeof value.endCursor === "string" || value.endCursor === null ? value.endCursor : null,
758
+ };
759
+ }
760
+ function connectionFromNodes(nodes) {
761
+ return { nodes, pageInfo: { hasNextPage: false, endCursor: null } };
762
+ }
763
+ function degradedConnection(input, reason, cursor) {
764
+ return {
765
+ source: input.source,
766
+ connection: input.connection,
767
+ reason,
768
+ ...(cursor !== undefined ? { cursor } : {}),
769
+ };
770
+ }
771
+ function issueSource(issue) {
772
+ const identifier = typeof issue.identifier === "string" ? issue.identifier : "unknown";
773
+ const id = typeof issue.id === "string" ? issue.id : "unknown";
774
+ return `issue ${identifier} (${id})`;
775
+ }
776
+ function projectSource(project) {
777
+ const slug = typeof project.slugId === "string" ? project.slugId : "unknown";
778
+ const id = typeof project.id === "string" ? project.id : "unknown";
779
+ return `project ${slug} (${id})`;
780
+ }
781
+ function teamSource(team) {
782
+ const key = typeof team.key === "string" ? team.key : "unknown";
783
+ const id = typeof team.id === "string" ? team.id : "unknown";
784
+ return `team ${key} (${id})`;
516
785
  }
517
786
  function stringField(record, key) {
518
787
  const value = record[key];
@@ -525,25 +794,6 @@ function asRecord(value) {
525
794
  throw new Error("expected Linear object");
526
795
  return value;
527
796
  }
528
- function linearErrorContext(query, body) {
529
- const parts = [];
530
- const operation = operationName(query);
531
- if (operation)
532
- parts.push(`operation=${operation}`);
533
- if (body !== undefined)
534
- parts.push(`body=${summarizeErrorBody(body)}`);
535
- return parts.length === 0 ? "" : ` ${parts.join(" ")}`;
536
- }
537
- function operationName(query) {
538
- return /\b(?:query|mutation)\s+([A-Za-z_][A-Za-z0-9_]*)/.exec(query)?.[1] ?? null;
539
- }
540
- function summarizeErrorBody(body) {
541
- const text = typeof body === "string" ? body : (JSON.stringify(body) ?? String(body));
542
- const compact = text.replace(/\s+/g, " ").trim();
543
- if (compact.length <= MAX_ERROR_BODY_LOG_BYTES)
544
- return compact;
545
- return `${compact.slice(0, MAX_ERROR_BODY_LOG_BYTES)}...<truncated>`;
546
- }
547
797
  function retryAfterHeaderValueFromError(error) {
548
798
  if (!isRecord(error))
549
799
  return null;