@relayfile/adapter-linear 0.1.21 → 0.1.22

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 (70) hide show
  1. package/discovery/linear/.adapter.md +71 -0
  2. package/discovery/linear/issues/.create.example.json +6 -0
  3. package/discovery/linear/issues/.schema.json +145 -0
  4. package/discovery/linear/issues/{issueId}/comments/.create.example.json +3 -0
  5. package/discovery/linear/issues/{issueId}/comments/.schema.json +91 -0
  6. package/dist/__tests__/aliases.test.d.ts +2 -0
  7. package/dist/__tests__/aliases.test.d.ts.map +1 -0
  8. package/dist/__tests__/aliases.test.js +184 -0
  9. package/dist/__tests__/aliases.test.js.map +1 -0
  10. package/dist/__tests__/by-state.test.d.ts +2 -0
  11. package/dist/__tests__/by-state.test.d.ts.map +1 -0
  12. package/dist/__tests__/by-state.test.js +282 -0
  13. package/dist/__tests__/by-state.test.js.map +1 -0
  14. package/dist/__tests__/index-emission.test.d.ts +2 -0
  15. package/dist/__tests__/index-emission.test.d.ts.map +1 -0
  16. package/dist/__tests__/index-emission.test.js +118 -0
  17. package/dist/__tests__/index-emission.test.js.map +1 -0
  18. package/dist/__tests__/layout-prompt.test.d.ts +2 -0
  19. package/dist/__tests__/layout-prompt.test.d.ts.map +1 -0
  20. package/dist/__tests__/layout-prompt.test.js +14 -0
  21. package/dist/__tests__/layout-prompt.test.js.map +1 -0
  22. package/dist/__tests__/linear-adapter.test.js +241 -7
  23. package/dist/__tests__/linear-adapter.test.js.map +1 -1
  24. package/dist/__tests__/name-id-convention.test.d.ts +2 -0
  25. package/dist/__tests__/name-id-convention.test.d.ts.map +1 -0
  26. package/dist/__tests__/name-id-convention.test.js +50 -0
  27. package/dist/__tests__/name-id-convention.test.js.map +1 -0
  28. package/dist/__tests__/path-mapper.test.js +5 -1
  29. package/dist/__tests__/path-mapper.test.js.map +1 -1
  30. package/dist/alias-slug.d.ts +3 -0
  31. package/dist/alias-slug.d.ts.map +1 -0
  32. package/dist/alias-slug.js +17 -0
  33. package/dist/alias-slug.js.map +1 -0
  34. package/dist/index-emitter.d.ts +10 -0
  35. package/dist/index-emitter.d.ts.map +1 -0
  36. package/dist/index-emitter.js +30 -0
  37. package/dist/index-emitter.js.map +1 -0
  38. package/dist/index.d.ts +3 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +3 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/layout-prompt.d.ts +7 -0
  43. package/dist/layout-prompt.d.ts.map +1 -0
  44. package/dist/layout-prompt.js +45 -0
  45. package/dist/layout-prompt.js.map +1 -0
  46. package/dist/linear-adapter.d.ts +11 -0
  47. package/dist/linear-adapter.d.ts.map +1 -1
  48. package/dist/linear-adapter.js +403 -33
  49. package/dist/linear-adapter.js.map +1 -1
  50. package/dist/path-mapper.d.ts +22 -3
  51. package/dist/path-mapper.d.ts.map +1 -1
  52. package/dist/path-mapper.js +133 -21
  53. package/dist/path-mapper.js.map +1 -1
  54. package/dist/queries.d.ts +24 -0
  55. package/dist/queries.d.ts.map +1 -1
  56. package/dist/queries.js +50 -0
  57. package/dist/queries.js.map +1 -1
  58. package/dist/resources.d.ts +25 -0
  59. package/dist/resources.d.ts.map +1 -0
  60. package/dist/resources.js +23 -0
  61. package/dist/resources.js.map +1 -0
  62. package/dist/types.d.ts +2 -0
  63. package/dist/types.d.ts.map +1 -1
  64. package/dist/writeback.d.ts +7 -3
  65. package/dist/writeback.d.ts.map +1 -1
  66. package/dist/writeback.js +52 -14
  67. package/dist/writeback.js.map +1 -1
  68. package/dist/writeback.test.js +15 -43
  69. package/dist/writeback.test.js.map +1 -1
  70. package/package.json +3 -2
@@ -0,0 +1,45 @@
1
+ export const LINEAR_LAYOUT_PROMPT = `# Linear Mount Layout
2
+
3
+ Always run \`ls\` before constructing a path. PR 0 standardizes human-readable leaf names to \`<sanitized-name>__<id>\`, so consumers should inspect the live directory instead of guessing a filename.
4
+
5
+ ## Tree
6
+
7
+ \`/linear/LAYOUT.md\` is this guide.
8
+ \`/linear/issues/\`, \`/linear/comments/\`, \`/linear/users/\`, and \`/linear/teams/\` each own their canonical JSON records plus a sibling \`_index.json\`.
9
+ Other integration-owned trees include \`/linear/projects/\`, \`/linear/cycles/\`, \`/linear/milestones/\`, and \`/linear/roadmaps/\`.
10
+
11
+ ## Indexes
12
+
13
+ \`/linear/issues/_index.json\` rows use:
14
+
15
+ \`\`\`json
16
+ { "id": "<id>", "title": "<human-readable>", "updated": "<iso8601>", "identifier": "<TEAM-123>", "state": "<state name>" }
17
+ \`\`\`
18
+
19
+ \`/linear/comments/_index.json\`, \`/linear/users/_index.json\`, and \`/linear/teams/_index.json\` use:
20
+
21
+ \`\`\`json
22
+ { "id": "<id>", "title": "<human-readable>", "updated": "<iso8601>" }
23
+ \`\`\`
24
+
25
+ ## JSONL And Querying
26
+
27
+ Linear does not emit JSONL in this adapter today. Comments are individual \`.json\` records rather than \`comments.jsonl\`.
28
+
29
+ Examples:
30
+
31
+ \`\`\`bash
32
+ ls /linear/issues
33
+ jq '.[0]' /linear/issues/_index.json
34
+ jq '.[] | {identifier, state, title}' /linear/issues/_index.json
35
+ grep -R "ENG-" /linear/comments
36
+ \`\`\`
37
+ `;
38
+ export function linearLayoutPromptFile() {
39
+ return {
40
+ path: '/linear/LAYOUT.md',
41
+ contentType: 'text/markdown; charset=utf-8',
42
+ content: LINEAR_LAYOUT_PROMPT.endsWith('\n') ? LINEAR_LAYOUT_PROMPT : `${LINEAR_LAYOUT_PROMPT}\n`,
43
+ };
44
+ }
45
+ //# sourceMappingURL=layout-prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout-prompt.js","sourceRoot":"","sources":["../src/layout-prompt.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoCnC,CAAC;AAEF,MAAM,UAAU,sBAAsB;IACpC,OAAO;QACL,IAAI,EAAE,mBAAmB;QACzB,WAAW,EAAE,8BAAuC;QACpD,OAAO,EAAE,oBAAoB,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,GAAG,oBAAoB,IAAI;KAClG,CAAC;AACJ,CAAC"}
@@ -42,9 +42,17 @@ export interface DeleteFileInput {
42
42
  workspaceId: string;
43
43
  path: string;
44
44
  }
45
+ export interface ReadFileInput {
46
+ workspaceId: string;
47
+ path: string;
48
+ }
49
+ export interface ReadFileResult {
50
+ content?: string;
51
+ }
45
52
  export interface RelayFileClientLike {
46
53
  writeFile(input: WriteFileInput): Promise<WriteFileResult | void>;
47
54
  deleteFile?(input: DeleteFileInput): Promise<void> | void;
55
+ readFile?(inputOrWorkspaceId: ReadFileInput | string, path?: string): Promise<ReadFileResult | string | undefined> | ReadFileResult | string | undefined;
48
56
  }
49
57
  export declare abstract class IntegrationAdapter {
50
58
  protected readonly client: RelayFileClientLike;
@@ -69,5 +77,8 @@ export declare class LinearAdapter extends IntegrationAdapter {
69
77
  private normalizeEvent;
70
78
  private isRemoveEvent;
71
79
  private renderContent;
80
+ private writeAuxiliaryFiles;
81
+ private readIndexRows;
82
+ private recordAuxiliaryWrite;
72
83
  }
73
84
  //# sourceMappingURL=linear-adapter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"linear-adapter.d.ts","sourceRoot":"","sources":["../src/linear-adapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"linear-adapter.d.ts","sourceRoot":"","sources":["../src/linear-adapter.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,7 @@
1
- import { computeLinearPath, linearCyclePath, linearIssuePath, linearMilestonePath, linearProjectPath, linearRoadmapPath, linearTeamPath, linearUserPath, normalizeLinearObjectType, } from './path-mapper.js';
1
+ import { computeLinearPath, LINEAR_PATH_ROOT, linearByIdAliasPath, linearByTitleAliasPath, linearCyclePath, linearIssueByStatePath, linearIssuePath, linearMilestonePath, linearProjectPath, linearRoadmapPath, linearTeamPath, linearUserPath, normalizeLinearObjectType, } from './path-mapper.js';
2
+ import { buildLinearIndexFile } from './index-emitter.js';
3
+ import { linearLayoutPromptFile } from './layout-prompt.js';
4
+ import { getLinearCommentHumanReadable, getLinearIssueHumanReadable, linearCommentIndexRow, linearIssueIndexRow, linearTeamIndexRow, linearUserIndexRow, } from './queries.js';
2
5
  import { LINEAR_WEBHOOK_OBJECT_TYPES } from './types.js';
3
6
  export class IntegrationAdapter {
4
7
  client;
@@ -29,16 +32,35 @@ export class LinearAdapter extends IntegrationAdapter {
29
32
  async ingestWebhook(workspaceId, event) {
30
33
  try {
31
34
  const normalized = this.normalizeEvent(event);
32
- const path = computeLinearPath(normalized.objectType, normalized.objectId, readIssueTitle(normalized.objectType, normalized.payload));
35
+ const path = computeLinearPath(normalized.objectType, normalized.objectId, readPathHumanReadable(normalized.objectType, normalized.payload));
36
+ const content = this.renderContent(workspaceId, normalized, false);
37
+ const semantics = this.computeSemantics(normalized.objectType, normalized.objectId, normalized.payload);
38
+ const aliasErrorPath = inferIssueStateAliasErrorPath(normalized);
33
39
  if (this.isRemoveEvent(normalized)) {
40
+ const deletePaths = [path];
41
+ let filesDeleted = 0;
42
+ let filesWritten = 0;
43
+ let filesUpdated = 0;
44
+ const aliasPath = resolveIssueStateAliasPath(normalized.payload);
45
+ const previousAliasPath = resolvePreviousIssueStateAliasPath(normalized.payload);
34
46
  if (this.client.deleteFile) {
35
47
  await this.client.deleteFile({ workspaceId, path });
48
+ filesDeleted += 1;
49
+ for (const candidatePath of uniqueStrings([aliasPath, previousAliasPath])) {
50
+ if (!candidatePath) {
51
+ continue;
52
+ }
53
+ await this.client.deleteFile({ workspaceId, path: candidatePath });
54
+ deletePaths.push(candidatePath);
55
+ filesDeleted += 1;
56
+ }
57
+ const auxiliary = await this.writeAuxiliaryFiles(workspaceId, normalized, true);
36
58
  return {
37
- filesWritten: 0,
38
- filesUpdated: 0,
39
- filesDeleted: 1,
40
- paths: [path],
41
- errors: [],
59
+ filesWritten: filesWritten + auxiliary.filesWritten,
60
+ filesUpdated: filesUpdated + auxiliary.filesUpdated,
61
+ filesDeleted,
62
+ paths: [...deletePaths, ...auxiliary.paths],
63
+ errors: auxiliary.errors,
42
64
  };
43
65
  }
44
66
  const deleteResult = await this.client.writeFile({
@@ -46,32 +68,99 @@ export class LinearAdapter extends IntegrationAdapter {
46
68
  path,
47
69
  content: this.renderContent(workspaceId, normalized, true),
48
70
  contentType: JSON_CONTENT_TYPE,
49
- semantics: this.computeSemantics(normalized.objectType, normalized.objectId, normalized.payload),
71
+ semantics,
50
72
  });
51
73
  const counts = inferWriteCounts(normalized, deleteResult, true);
74
+ filesDeleted += counts.filesDeleted;
75
+ filesWritten += counts.filesWritten;
76
+ filesUpdated += counts.filesUpdated;
77
+ for (const candidatePath of uniqueStrings([aliasPath, previousAliasPath])) {
78
+ if (!candidatePath) {
79
+ continue;
80
+ }
81
+ const aliasDeleteResult = await this.client.writeFile({
82
+ workspaceId,
83
+ path: candidatePath,
84
+ content: this.renderContent(workspaceId, normalized, true),
85
+ contentType: JSON_CONTENT_TYPE,
86
+ semantics,
87
+ });
88
+ const aliasCounts = inferWriteCounts(normalized, aliasDeleteResult, true);
89
+ deletePaths.push(candidatePath);
90
+ filesDeleted += aliasCounts.filesDeleted;
91
+ filesWritten += aliasCounts.filesWritten;
92
+ filesUpdated += aliasCounts.filesUpdated;
93
+ }
94
+ const auxiliary = await this.writeAuxiliaryFiles(workspaceId, normalized, true);
52
95
  return {
53
- filesWritten: counts.filesWritten,
54
- filesUpdated: counts.filesUpdated,
55
- filesDeleted: counts.filesDeleted,
56
- paths: [path],
57
- errors: [],
96
+ filesWritten: filesWritten + auxiliary.filesWritten,
97
+ filesUpdated: filesUpdated + auxiliary.filesUpdated,
98
+ filesDeleted,
99
+ paths: [...deletePaths, ...auxiliary.paths],
100
+ errors: auxiliary.errors,
58
101
  };
59
102
  }
60
103
  const writeResult = await this.client.writeFile({
61
104
  workspaceId,
62
105
  path,
63
- content: this.renderContent(workspaceId, normalized, false),
106
+ content,
64
107
  contentType: JSON_CONTENT_TYPE,
65
- semantics: this.computeSemantics(normalized.objectType, normalized.objectId, normalized.payload),
108
+ semantics,
66
109
  });
110
+ await writeLinearAliases(this.client, workspaceId, normalized, path, content, semantics);
67
111
  const counts = inferWriteCounts(normalized, writeResult, false);
68
- return {
69
- filesWritten: counts.filesWritten,
70
- filesUpdated: counts.filesUpdated,
112
+ const auxiliary = await this.writeAuxiliaryFiles(workspaceId, normalized, false);
113
+ const result = {
114
+ filesWritten: counts.filesWritten + auxiliary.filesWritten,
115
+ filesUpdated: counts.filesUpdated + auxiliary.filesUpdated,
71
116
  filesDeleted: 0,
72
- paths: [path],
73
- errors: [],
117
+ paths: [path, ...auxiliary.paths],
118
+ errors: auxiliary.errors,
74
119
  };
120
+ if (normalized.objectType !== 'issue') {
121
+ return result;
122
+ }
123
+ const previousAliasPath = resolvePreviousIssueStateAliasPath(normalized.payload);
124
+ const aliasPath = resolveIssueStateAliasPath(normalized.payload);
125
+ if (previousAliasPath && previousAliasPath !== aliasPath) {
126
+ if (this.client.deleteFile) {
127
+ await this.client.deleteFile({ workspaceId, path: previousAliasPath });
128
+ result.filesDeleted += 1;
129
+ }
130
+ else {
131
+ const previousDeleteResult = await this.client.writeFile({
132
+ workspaceId,
133
+ path: previousAliasPath,
134
+ content: this.renderContent(workspaceId, normalized, true),
135
+ contentType: JSON_CONTENT_TYPE,
136
+ semantics,
137
+ });
138
+ const previousCounts = inferWriteCounts(normalized, previousDeleteResult, true);
139
+ result.filesWritten += previousCounts.filesWritten;
140
+ result.filesUpdated += previousCounts.filesUpdated;
141
+ result.filesDeleted += previousCounts.filesDeleted;
142
+ }
143
+ result.paths.push(previousAliasPath);
144
+ }
145
+ if (!aliasPath) {
146
+ result.errors.push({
147
+ path: aliasErrorPath,
148
+ error: 'Linear issue is missing state_name or identifier for by-state alias emission.',
149
+ });
150
+ return result;
151
+ }
152
+ const aliasWriteResult = await this.client.writeFile({
153
+ workspaceId,
154
+ path: aliasPath,
155
+ content,
156
+ contentType: JSON_CONTENT_TYPE,
157
+ semantics,
158
+ });
159
+ const aliasCounts = inferWriteCounts(normalized, aliasWriteResult, false);
160
+ result.filesWritten += aliasCounts.filesWritten;
161
+ result.filesUpdated += aliasCounts.filesUpdated;
162
+ result.paths.push(aliasPath);
163
+ return result;
75
164
  }
76
165
  catch (error) {
77
166
  const fallbackPath = inferFallbackPath(event);
@@ -195,6 +284,69 @@ export class LinearAdapter extends IntegrationAdapter {
195
284
  payload: event.payload,
196
285
  });
197
286
  }
287
+ async writeAuxiliaryFiles(workspaceId, event, deleted) {
288
+ const result = {
289
+ filesWritten: 0,
290
+ filesUpdated: 0,
291
+ paths: [],
292
+ errors: [],
293
+ };
294
+ const layoutFile = linearLayoutPromptFile();
295
+ await this.recordAuxiliaryWrite(result, workspaceId, layoutFile.path, layoutFile.content, layoutFile.contentType);
296
+ const bucket = bucketForObjectType(event.objectType);
297
+ if (!bucket) {
298
+ return result;
299
+ }
300
+ const existingRows = await this.readIndexRows(workspaceId, buildIndexPathForBucket(bucket));
301
+ if (!existingRows) {
302
+ return result;
303
+ }
304
+ const nextRows = deleted
305
+ ? existingRows.filter((row) => row.id !== event.objectId)
306
+ : upsertLinearIndexRow(existingRows, buildIndexRow(bucket, event));
307
+ const indexFile = buildIndexFileForBucket(bucket, nextRows);
308
+ await this.recordAuxiliaryWrite(result, workspaceId, indexFile.path, indexFile.content, indexFile.contentType);
309
+ return result;
310
+ }
311
+ async readIndexRows(workspaceId, path) {
312
+ const content = await readClientFile(this.client, workspaceId, path);
313
+ if (content === READ_NOT_AVAILABLE) {
314
+ // No reader on the client — we can't reconcile, so skip the auxiliary write entirely.
315
+ return undefined;
316
+ }
317
+ if (content === undefined) {
318
+ // Reader ran but the index is missing/empty/malformed. Bootstrap with an empty
319
+ // array so the first ingest writes the index instead of getting stuck.
320
+ return [];
321
+ }
322
+ try {
323
+ const parsed = JSON.parse(content);
324
+ return Array.isArray(parsed) ? parsed : [];
325
+ }
326
+ catch {
327
+ return [];
328
+ }
329
+ }
330
+ async recordAuxiliaryWrite(result, workspaceId, path, content, contentType) {
331
+ try {
332
+ const writeResult = await this.client.writeFile({
333
+ workspaceId,
334
+ path,
335
+ content,
336
+ contentType,
337
+ });
338
+ const counts = inferAuxiliaryWriteCounts(writeResult);
339
+ result.filesWritten += counts.filesWritten;
340
+ result.filesUpdated += counts.filesUpdated;
341
+ result.paths.push(path);
342
+ }
343
+ catch (error) {
344
+ result.errors.push({
345
+ path,
346
+ error: toErrorMessage(error),
347
+ });
348
+ }
349
+ }
198
350
  }
199
351
  function applyIssueSemantics(properties, relations, payload) {
200
352
  const issue = payload;
@@ -267,12 +419,12 @@ function applyIssueSemantics(properties, relations, payload) {
267
419
  addStringProperty(properties, 'linear.cycle_name', issue.cycle.name);
268
420
  }
269
421
  if (issue.parent?.id) {
270
- relations.add(linearIssuePath(issue.parent.id));
422
+ relations.add(buildLinearIssueReferencePath(issue.parent));
271
423
  addStringProperty(properties, 'linear.parent_id', issue.parent.id);
272
424
  }
273
425
  for (const child of issue.children ?? []) {
274
426
  if (child.id) {
275
- relations.add(linearIssuePath(child.id));
427
+ relations.add(buildLinearIssueReferencePath(child));
276
428
  }
277
429
  }
278
430
  for (const relation of asRelations(issue.relations)) {
@@ -294,11 +446,15 @@ function applyIssueSemantics(properties, relations, payload) {
294
446
  addFirstStringProperty(properties, 'linear.team_key', properties['linear.team_key'], issue.team_key);
295
447
  addFirstStringProperty(properties, 'linear.team_name', properties['linear.team_name'], issue.team_name);
296
448
  }
297
- function readIssueTitle(objectType, payload) {
298
- if (normalizeLinearObjectType(objectType) !== 'issue') {
299
- return undefined;
449
+ function readPathHumanReadable(objectType, payload) {
450
+ switch (normalizeLinearObjectType(objectType)) {
451
+ case 'issue':
452
+ return getLinearIssueHumanReadable(buildLinearIssueHumanReadableInput(payload));
453
+ case 'comment':
454
+ return getLinearCommentHumanReadable(buildLinearCommentHumanReadableInput(payload));
455
+ default:
456
+ return undefined;
300
457
  }
301
- return asString(payload.title);
302
458
  }
303
459
  function applyCommentSemantics(properties, relations, comments, payload) {
304
460
  const comment = payload;
@@ -323,7 +479,7 @@ function applyCommentSemantics(properties, relations, comments, payload) {
323
479
  addFirstStringProperty(properties, 'linear.author_name', properties['linear.author_name'], comment.user_name, comment.author_name);
324
480
  addFirstStringProperty(properties, 'linear.author_email', properties['linear.author_email'], comment.user_email, comment.author_email);
325
481
  if (comment.issue?.id) {
326
- relations.add(linearIssuePath(comment.issue.id));
482
+ relations.add(buildLinearIssueReferencePath(comment.issue));
327
483
  addStringProperty(properties, 'linear.issue_id', comment.issue.id);
328
484
  addStringProperty(properties, 'linear.issue_identifier', comment.issue.identifier);
329
485
  addStringProperty(properties, 'linear.issue_title', comment.issue.title);
@@ -331,7 +487,10 @@ function applyCommentSemantics(properties, relations, comments, payload) {
331
487
  }
332
488
  const issueId = asString(comment.issue_id);
333
489
  if (issueId) {
334
- relations.add(linearIssuePath(issueId));
490
+ relations.add(linearIssuePath(issueId, getLinearIssueHumanReadable(buildLinearIssueHumanReadableInput({
491
+ identifier: comment.issue_identifier,
492
+ title: comment.issue_title,
493
+ }))));
335
494
  addStringProperty(properties, 'linear.issue_id', issueId);
336
495
  }
337
496
  addFirstStringProperty(properties, 'linear.issue_identifier', properties['linear.issue_identifier'], comment.issue_identifier);
@@ -369,6 +528,29 @@ function applyProjectSemantics(properties, relations, payload) {
369
528
  }
370
529
  }
371
530
  }
531
+ function buildLinearIssueReferencePath(issue) {
532
+ return linearIssuePath(issue.id, getLinearIssueHumanReadable(issue));
533
+ }
534
+ function buildLinearIssueHumanReadableInput(record) {
535
+ const identifier = asString(record.identifier);
536
+ const title = asString(record.title);
537
+ return {
538
+ ...(identifier ? { identifier } : {}),
539
+ ...(title ? { title } : {}),
540
+ };
541
+ }
542
+ function buildLinearCommentIssueInput(issue) {
543
+ const identifier = asString(issue?.identifier);
544
+ return identifier ? { identifier } : undefined;
545
+ }
546
+ function buildLinearCommentHumanReadableInput(record) {
547
+ const body = asString(record.body);
548
+ const issue = buildLinearCommentIssueInput(getRecord(record.issue));
549
+ return {
550
+ ...(body ? { body } : {}),
551
+ ...(issue ? { issue } : {}),
552
+ };
553
+ }
372
554
  function applyCycleSemantics(properties, payload) {
373
555
  const cycle = payload;
374
556
  addNumberProperty(properties, 'linear.number', cycle.number);
@@ -467,12 +649,90 @@ function mergeLinearPayload(event) {
467
649
  }),
468
650
  };
469
651
  }
652
+ function bucketForObjectType(objectType) {
653
+ switch (normalizeLinearObjectType(objectType)) {
654
+ case 'issue':
655
+ return 'issues';
656
+ case 'comment':
657
+ return 'comments';
658
+ case 'team':
659
+ return 'teams';
660
+ case 'user':
661
+ return 'users';
662
+ default:
663
+ return undefined;
664
+ }
665
+ }
666
+ function buildIndexRow(bucket, event) {
667
+ const payload = {
668
+ ...event.payload,
669
+ id: event.objectId,
670
+ };
671
+ switch (bucket) {
672
+ case 'issues':
673
+ return linearIssueIndexRow(payload);
674
+ case 'comments':
675
+ return linearCommentIndexRow(payload);
676
+ case 'teams':
677
+ return linearTeamIndexRow(payload);
678
+ case 'users':
679
+ return linearUserIndexRow(payload);
680
+ }
681
+ }
682
+ function buildIndexFileForBucket(bucket, rows) {
683
+ switch (bucket) {
684
+ case 'issues':
685
+ return buildLinearIndexFile('issues', rows);
686
+ case 'comments':
687
+ return buildLinearIndexFile('comments', rows);
688
+ case 'teams':
689
+ return buildLinearIndexFile('teams', rows);
690
+ case 'users':
691
+ return buildLinearIndexFile('users', rows);
692
+ }
693
+ }
694
+ function buildIndexPathForBucket(bucket) {
695
+ return buildIndexFileForBucket(bucket, []).path;
696
+ }
697
+ function upsertLinearIndexRow(rows, row) {
698
+ return [...rows.filter((existing) => existing.id !== row.id), row];
699
+ }
700
+ // `READ_NOT_AVAILABLE` is returned when the client cannot read at all (no
701
+ // `readFile` method). `undefined` means the call ran but the file is missing
702
+ // or the response was malformed. Callers use this distinction to decide
703
+ // whether to skip auxiliary writes (no readFile) or bootstrap an empty index
704
+ // (file missing on first ingest).
705
+ const READ_NOT_AVAILABLE = Symbol('readNotAvailable');
706
+ async function readClientFile(client, workspaceId, path) {
707
+ if (!client.readFile) {
708
+ return READ_NOT_AVAILABLE;
709
+ }
710
+ try {
711
+ const value = client.readFile.length >= 2
712
+ ? await client.readFile(workspaceId, path)
713
+ : await client.readFile({ workspaceId, path });
714
+ if (typeof value === 'string') {
715
+ return value;
716
+ }
717
+ if (value && typeof value === 'object' && typeof value.content === 'string') {
718
+ return value.content;
719
+ }
720
+ }
721
+ catch {
722
+ return undefined;
723
+ }
724
+ return undefined;
725
+ }
726
+ function inferAuxiliaryWriteCounts(writeResult) {
727
+ if (writeResult?.created || writeResult?.status === 'created') {
728
+ return { filesWritten: 1, filesUpdated: 0 };
729
+ }
730
+ return { filesWritten: 0, filesUpdated: 1 };
731
+ }
470
732
  function inferWriteCounts(event, writeResult, deleted) {
471
733
  if (deleted) {
472
- if (writeResult?.status === 'created' || writeResult?.created) {
473
- return { filesWritten: 1, filesUpdated: 0, filesDeleted: 0 };
474
- }
475
- return { filesWritten: 0, filesUpdated: 1, filesDeleted: 0 };
734
+ void writeResult;
735
+ return { filesWritten: 0, filesUpdated: 0, filesDeleted: 1 };
476
736
  }
477
737
  if (writeResult?.created || writeResult?.status === 'created') {
478
738
  return { filesWritten: 1, filesUpdated: 0, filesDeleted: 0 };
@@ -511,6 +771,33 @@ function inferFallbackPath(event) {
511
771
  return '';
512
772
  }
513
773
  }
774
+ function resolveIssueStateAliasPath(payload) {
775
+ const stateName = asString(payload.state_name);
776
+ const identifier = asString(payload.identifier);
777
+ if (!stateName || !identifier) {
778
+ return undefined;
779
+ }
780
+ return linearIssueByStatePath(stateName, identifier);
781
+ }
782
+ function resolvePreviousIssueStateAliasPath(payload) {
783
+ const previousData = getRecord(getRecord(payload._webhook)?.previousData);
784
+ if (!previousData) {
785
+ return undefined;
786
+ }
787
+ const stateName = asString(previousData.state_name);
788
+ const identifier = asString(previousData.identifier) ?? asString(payload.identifier);
789
+ if (!stateName || !identifier) {
790
+ return undefined;
791
+ }
792
+ return linearIssueByStatePath(stateName, identifier);
793
+ }
794
+ function inferIssueStateAliasErrorPath(event) {
795
+ const identifier = asString(event.payload.identifier);
796
+ if (identifier) {
797
+ return `/linear/issues/by-state/<missing-state>/${encodeURIComponent(identifier)}.json`;
798
+ }
799
+ return '/linear/issues/by-state/<missing-state>/<missing-identifier>.json';
800
+ }
514
801
  function extractPayloadId(value) {
515
802
  const record = getRecord(value);
516
803
  return asString(record?.id);
@@ -571,6 +858,88 @@ function sortStrings(values) {
571
858
  function stableJson(value) {
572
859
  return `${JSON.stringify(sortJson(value), null, 2)}\n`;
573
860
  }
861
+ async function writeLinearAliases(client, workspaceId, event, canonicalPath, content, semantics) {
862
+ // duplicate write — the adapter only has a file-write interface, so aliases store the canonical bytes verbatim.
863
+ const normalizedType = normalizeLinearObjectType(event.objectType);
864
+ if (normalizedType !== 'issue' && normalizedType !== 'project') {
865
+ return;
866
+ }
867
+ const scope = normalizedType === 'issue' ? `${LINEAR_PATH_ROOT}/issues` : `${LINEAR_PATH_ROOT}/projects`;
868
+ const title = normalizedType === 'issue' ? asString(event.payload.title) : asString(event.payload.name);
869
+ const byId = normalizedType === 'issue'
870
+ ? asString(event.payload.identifier) ?? event.objectId
871
+ : event.objectId;
872
+ await writeLinearIndex(client, workspaceId, scope);
873
+ await writeLinearFile(client, workspaceId, linearByIdAliasPath(scope, byId), content, semantics);
874
+ if (!title) {
875
+ return;
876
+ }
877
+ const baseAliasPath = linearByTitleAliasPath(scope, title, event.objectId);
878
+ const existingBaseContent = await readLinearFile(client, baseAliasPath);
879
+ const aliasPath = existingBaseContent !== undefined && existingBaseContent !== content
880
+ ? linearByTitleAliasPath(scope, title, event.objectId, true)
881
+ : baseAliasPath;
882
+ // TODO(issue #106): remove stale by-title aliases when a record title changes on re-ingest; this wave only writes the current alias.
883
+ await writeLinearFile(client, workspaceId, aliasPath, content, semantics);
884
+ }
885
+ async function writeLinearIndex(client, workspaceId, scope) {
886
+ const indexPath = `${scope}/_index.json`;
887
+ const rows = mergeLinearIndexRows(await readLinearFile(client, indexPath), [
888
+ { title: 'by-id', file: 'by-id/' },
889
+ { title: 'by-title', file: 'by-title/' },
890
+ ]);
891
+ await writeLinearFile(client, workspaceId, indexPath, stableJson({ rows }), undefined);
892
+ }
893
+ function mergeLinearIndexRows(existingContent, requiredRows) {
894
+ const rows = new Map();
895
+ for (const row of parseLinearIndexRows(existingContent)) {
896
+ rows.set(row.file, row);
897
+ }
898
+ for (const row of requiredRows) {
899
+ rows.set(row.file, row);
900
+ }
901
+ return [...rows.values()].sort((left, right) => left.file.localeCompare(right.file));
902
+ }
903
+ function parseLinearIndexRows(existingContent) {
904
+ if (!existingContent) {
905
+ return [];
906
+ }
907
+ try {
908
+ const parsed = JSON.parse(existingContent);
909
+ return Array.isArray(parsed.rows)
910
+ ? parsed.rows.filter((row) => typeof row?.file === 'string' && typeof row?.title === 'string')
911
+ : [];
912
+ }
913
+ catch {
914
+ return [];
915
+ }
916
+ }
917
+ async function writeLinearFile(client, workspaceId, path, content, semantics) {
918
+ await client.writeFile({
919
+ workspaceId,
920
+ path,
921
+ content,
922
+ contentType: JSON_CONTENT_TYPE,
923
+ ...(semantics ? { semantics } : {}),
924
+ });
925
+ }
926
+ async function readLinearFile(client, path) {
927
+ const reader = client.readFile;
928
+ if (typeof reader !== 'function') {
929
+ return undefined;
930
+ }
931
+ try {
932
+ const value = await reader.call(client, path);
933
+ return typeof value === 'string'
934
+ ? value
935
+ : value && typeof value === 'object' && 'content' in value && typeof value.content === 'string'
936
+ ? value.content
937
+ : undefined;
938
+ }
939
+ catch {
940
+ return undefined;
941
+ }
942
+ }
574
943
  function sortJson(value) {
575
944
  if (Array.isArray(value)) {
576
945
  return value.map(sortJson);
@@ -628,7 +997,8 @@ function asLinearReferenceIds(value) {
628
997
  .filter((entry) => entry !== undefined);
629
998
  }
630
999
  function uniqueStrings(values) {
631
- return Array.from(new Set(values)).sort((left, right) => left.localeCompare(right));
1000
+ return Array.from(new Set(values.filter((value) => Boolean(value))))
1001
+ .sort((left, right) => left.localeCompare(right));
632
1002
  }
633
1003
  function mapPriorityLabel(priority) {
634
1004
  switch (priority) {