@apollo-annotation/shared 0.3.7 → 0.3.8

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 (45) hide show
  1. package/dist/Changes/AddAssemblyAndFeaturesFromFileChange.js +1 -1
  2. package/dist/Changes/AddAssemblyAndFeaturesFromFileChange.js.map +1 -1
  3. package/dist/Changes/AddAssemblyFromFileChange.js +1 -1
  4. package/dist/Changes/AddAssemblyFromFileChange.js.map +1 -1
  5. package/dist/Changes/AddRefSeqAliasesChange.js +2 -4
  6. package/dist/Changes/AddRefSeqAliasesChange.js.map +1 -1
  7. package/dist/Changes/ImportJBrowseConfigChange.js +1 -1
  8. package/dist/Changes/ImportJBrowseConfigChange.js.map +1 -1
  9. package/dist/Changes/MergeExonsChange.d.ts +0 -1
  10. package/dist/Changes/MergeExonsChange.js +14 -30
  11. package/dist/Changes/MergeExonsChange.js.map +1 -1
  12. package/dist/Changes/MergeTranscriptsChange.d.ts +1 -3
  13. package/dist/Changes/MergeTranscriptsChange.js +55 -56
  14. package/dist/Changes/MergeTranscriptsChange.js.map +1 -1
  15. package/dist/Checks/CDSCheck.d.ts +1 -1
  16. package/dist/Checks/CDSCheck.js +8 -9
  17. package/dist/Checks/CDSCheck.js.map +1 -1
  18. package/dist/Checks/TranscriptCheck.d.ts +9 -0
  19. package/dist/Checks/TranscriptCheck.js +109 -0
  20. package/dist/Checks/TranscriptCheck.js.map +1 -0
  21. package/dist/Checks/index.d.ts +1 -0
  22. package/dist/Checks/index.js +1 -0
  23. package/dist/Checks/index.js.map +1 -1
  24. package/dist/GFF3/gff3ToAnnotationFeature.test.js +1 -1
  25. package/dist/GFF3/gff3ToAnnotationFeature.test.js.map +1 -1
  26. package/dist/tsconfig.tsbuildinfo +1 -1
  27. package/dist/util.d.ts +3 -0
  28. package/dist/util.js +33 -0
  29. package/dist/util.js.map +1 -1
  30. package/package.json +7 -7
  31. package/src/Changes/AddAssemblyAndFeaturesFromFileChange.ts +2 -1
  32. package/src/Changes/AddAssemblyFromFileChange.ts +1 -1
  33. package/src/Changes/AddRefSeqAliasesChange.ts +2 -4
  34. package/src/Changes/ImportJBrowseConfigChange.ts +1 -1
  35. package/src/Changes/MergeExonsChange.ts +52 -36
  36. package/src/Changes/MergeTranscriptsChange.ts +82 -67
  37. package/src/Checks/CDSCheck.ts +10 -10
  38. package/src/Checks/TranscriptCheck.ts +139 -0
  39. package/src/Checks/index.ts +1 -0
  40. package/src/GFF3/gff3ToAnnotationFeature.test.ts +1 -1
  41. package/src/util.ts +37 -0
  42. package/test_data/example01.json +20 -20
  43. package/test_data/example02.json +38 -38
  44. package/test_data/example04.json +57 -57
  45. package/test_data/gene_representations.gff3 +1054 -276
package/dist/util.d.ts CHANGED
@@ -1,3 +1,6 @@
1
1
  import { type Feature } from '@apollo-annotation/schemas';
2
+ import { type IKeyValueMap } from 'mobx';
2
3
  export declare function splitStringIntoChunks(input: string, chunkSize: number): string[];
3
4
  export declare function getPrintableId(feature: Feature): string;
5
+ export declare function attributesToRecords(attributes: IKeyValueMap<readonly string[] | undefined> | undefined): Record<string, string[] | undefined>;
6
+ export declare function stringifyAttributes(attributes: Record<string, string[] | undefined> | undefined): string;
package/dist/util.js CHANGED
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.splitStringIntoChunks = splitStringIntoChunks;
4
4
  exports.getPrintableId = getPrintableId;
5
+ exports.attributesToRecords = attributesToRecords;
6
+ exports.stringifyAttributes = stringifyAttributes;
5
7
  function splitStringIntoChunks(input, chunkSize) {
6
8
  const chunks = [];
7
9
  for (let i = 0; i < input.length; i += chunkSize) {
@@ -22,4 +24,35 @@ function getPrintableId(feature) {
22
24
  }
23
25
  return `_id: ${feature._id.toString()}`;
24
26
  }
27
+ function attributesToRecords(attributes) {
28
+ const records = {};
29
+ if (!attributes) {
30
+ return records;
31
+ }
32
+ for (const [key, value] of Object.entries(attributes)) {
33
+ records[key] = value?.slice();
34
+ }
35
+ return records;
36
+ }
37
+ function stringifyAttributes(attributes) {
38
+ if (!attributes) {
39
+ return '';
40
+ }
41
+ const str = [];
42
+ for (const [key, value] of Object.entries(attributes)) {
43
+ let attributeName = key;
44
+ if (attributeName.startsWith('gff_')) {
45
+ attributeName = attributeName.slice(4);
46
+ attributeName =
47
+ attributeName.charAt(0).toUpperCase() + attributeName.slice(1);
48
+ }
49
+ if (value) {
50
+ str.push(`${attributeName}=${value.join(',')}`);
51
+ }
52
+ else {
53
+ str.push(attributeName);
54
+ }
55
+ }
56
+ return encodeURIComponent(str.join(';'));
57
+ }
25
58
  //# sourceMappingURL=util.js.map
package/dist/util.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":";;AAGA,sDAUC;AAED,wCAWC;AAvBD,SAAgB,qBAAqB,CACnC,KAAa,EACb,SAAiB;IAEjB,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,SAAS,EAAE,CAAC;QACjD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,CAAA;QAC3C,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACpB,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED,SAAgB,cAAc,CAAC,OAAgB;IAC7C,MAAM,EAAE,GAAG,OAAuC,CAAA;IAClD,MAAM,MAAM,GAAG,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAA;IACrD,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,MAAM,MAAM,UAAU,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAA;IACxD,CAAC;IACD,MAAM,QAAQ,GAAG,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAA;IACzD,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,QAAQ,QAAQ,UAAU,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAA;IAC5D,CAAC;IACD,OAAO,QAAQ,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAA;AACzC,CAAC"}
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":";;AAIA,sDAUC;AAED,wCAWC;AAED,kDAWC;AAED,kDAqBC;AA3DD,SAAgB,qBAAqB,CACnC,KAAa,EACb,SAAiB;IAEjB,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,SAAS,EAAE,CAAC;QACjD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,CAAA;QAC3C,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACpB,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED,SAAgB,cAAc,CAAC,OAAgB;IAC7C,MAAM,EAAE,GAAG,OAAuC,CAAA;IAClD,MAAM,MAAM,GAAG,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAA;IACrD,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,MAAM,MAAM,UAAU,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAA;IACxD,CAAC;IACD,MAAM,QAAQ,GAAG,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAA;IACzD,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,QAAQ,QAAQ,UAAU,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAA;IAC5D,CAAC;IACD,OAAO,QAAQ,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAA;AACzC,CAAC;AAED,SAAgB,mBAAmB,CACjC,UAAmE;IAEnE,MAAM,OAAO,GAAyC,EAAE,CAAA;IACxD,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,OAAO,CAAA;IAChB,CAAC;IACD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;QACtD,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,EAAE,KAAK,EAAE,CAAA;IAC/B,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,SAAgB,mBAAmB,CACjC,UAA4D;IAE5D,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,EAAE,CAAA;IACX,CAAC;IACD,MAAM,GAAG,GAAG,EAAE,CAAA;IACd,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;QACtD,IAAI,aAAa,GAAG,GAAG,CAAA;QACvB,IAAI,aAAa,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YACrC,aAAa,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;YACtC,aAAa;gBACX,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;QAClE,CAAC;QACD,IAAI,KAAK,EAAE,CAAC;YACV,GAAG,CAAC,IAAI,CAAC,GAAG,aAAa,IAAI,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACjD,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;QACzB,CAAC;IACH,CAAC;IACD,OAAO,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;AAC1C,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apollo-annotation/shared",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "main": "./dist/index.js",
5
5
  "scripts": {
6
6
  "build": "yarn clean && tsc --build",
@@ -10,12 +10,12 @@
10
10
  "test:ci": "NODE_V8_COVERAGE=./coverage glob -c \"tsx --test --test-reporter spec --experimental-test-coverage \" \"**/*.test.ts\""
11
11
  },
12
12
  "dependencies": {
13
- "@apollo-annotation/common": "^0.3.7",
14
- "@apollo-annotation/mst": "^0.3.7",
15
- "@apollo-annotation/schemas": "^0.3.7",
13
+ "@apollo-annotation/common": "^0.3.8",
14
+ "@apollo-annotation/mst": "^0.3.8",
15
+ "@apollo-annotation/schemas": "^0.3.8",
16
16
  "@gmod/gff": "1.2.0",
17
17
  "@gmod/indexedfasta": "^2.0.4",
18
- "@jbrowse/core": "^3.0.1",
18
+ "@jbrowse/core": "^3.6.5",
19
19
  "bson-objectid": "^2.0.4",
20
20
  "generic-filehandle": "^3.0.0",
21
21
  "jwt-decode": "^3.1.2",
@@ -39,8 +39,8 @@
39
39
  "typescript": "^5.5.3"
40
40
  },
41
41
  "peerDependencies": {
42
- "@mui/material": "^6.0.0",
43
- "@mui/x-data-grid": "^7.0.0",
42
+ "@mui/material": "^7.0.0",
43
+ "@mui/x-data-grid": "^8.0.0",
44
44
  "mobx": "^6.6.1",
45
45
  "mobx-react": "^7.2.1",
46
46
  "mobx-state-tree": "^5.4.0",
@@ -90,7 +90,8 @@ export class AddAssemblyAndFeaturesFromFileChange extends FromFileBaseChange {
90
90
  throw new Error(`Assembly "${assemblyName}" already exists`)
91
91
  }
92
92
  // get checks
93
- const checkDocs = await checkModel.find({ default: true }).exec()
93
+ const checkDocs = await checkModel.find({ isDefault: true }).exec()
94
+
94
95
  const checks = checkDocs.map((checkDoc) => checkDoc._id.toHexString())
95
96
  // Add assembly
96
97
  const [newAssemblyDoc] = await assemblyModel.create([
@@ -128,7 +128,7 @@ export class AddAssemblyFromFileChange extends FromFileBaseChange {
128
128
  if (assemblyDoc) {
129
129
  throw new Error(`Assembly "${assemblyName}" already exists`)
130
130
  }
131
- const checkDocs = await checkModel.find({ default: true }).exec()
131
+ const checkDocs = await checkModel.find({ isDefault: true }).exec()
132
132
  const checks = checkDocs.map((checkDoc) => checkDoc._id.toHexString())
133
133
  const [newAssemblyDoc] = await assemblyModel.create([
134
134
  {
@@ -36,9 +36,8 @@ export class AddRefSeqAliasesChange extends AssemblySpecificChange {
36
36
  throw new Error(`assembly ${this.assembly} not found`)
37
37
  }
38
38
  const sessionAliases = assembly.refNameAliases
39
- const sessionLCAliases = assembly.lowerCaseRefNameAliases
40
39
 
41
- if (!sessionAliases || !sessionLCAliases) {
40
+ if (!sessionAliases) {
42
41
  throw new Error('Session refNameAliases not found in assembly')
43
42
  }
44
43
 
@@ -46,10 +45,9 @@ export class AddRefSeqAliasesChange extends AssemblySpecificChange {
46
45
  const { aliases, refName } = refSeqAlias
47
46
  for (const alias of aliases) {
48
47
  sessionAliases[alias] = refName
49
- sessionLCAliases[alias.toLowerCase()] = refName
50
48
  }
51
49
  }
52
- assembly.setRefNameAliases(sessionAliases, sessionLCAliases)
50
+ assembly.setRefNameAliases(sessionAliases)
53
51
  return Promise.resolve()
54
52
  }
55
53
 
@@ -75,7 +75,7 @@ export function filterJBrowseConfig(config: JBrowseConfig): JBrowseConfig {
75
75
  filteredConfig.plugins = plugins.filter((p) => p.name !== 'Apollo')
76
76
  }
77
77
  if (tracks) {
78
- filteredConfig.trackss = tracks.filter((t) => t.type !== 'ApolloTrack')
78
+ filteredConfig.tracks = tracks.filter((t) => t.type !== 'ApolloTrack')
79
79
  }
80
80
  return filteredConfig
81
81
  }
@@ -1,3 +1,4 @@
1
+ /* eslint-disable unicorn/prefer-structured-clone */
1
2
  /* eslint-disable @typescript-eslint/require-await */
2
3
  /* eslint-disable @typescript-eslint/no-unsafe-assignment */
3
4
 
@@ -11,6 +12,8 @@ import {
11
12
  } from '@apollo-annotation/common'
12
13
  import { type AnnotationFeatureSnapshot } from '@apollo-annotation/mst'
13
14
 
15
+ import { attributesToRecords, stringifyAttributes } from '../util'
16
+
14
17
  import { findAndDeleteChildFeature } from './DeleteFeatureChange'
15
18
  import { UndoMergeExonsChange } from './UndoMergeExonsChange'
16
19
 
@@ -89,7 +92,15 @@ export class MergeExonsChange extends FeatureChange {
89
92
  }
90
93
  mergedExon.min = Math.min(firstExon.min, secondExon.min)
91
94
  mergedExon.max = Math.max(firstExon.max, secondExon.max)
92
- mergedExon.attributes = this.mergeAttributes(firstExon, secondExon)
95
+
96
+ const mergedAttributes: Record<string, string[]> = mergedExon.attributes
97
+ ? JSON.parse(JSON.stringify(mergedExon.attributes))
98
+ : {}
99
+ mergedAttributes.merged_with = [
100
+ stringifyAttributes(attributesToRecords(secondExon.attributes)),
101
+ ]
102
+ mergedExon.attributes = mergedAttributes
103
+
93
104
  const deletedIds = findAndDeleteChildFeature(
94
105
  topLevelFeature,
95
106
  secondExon._id,
@@ -123,11 +134,16 @@ export class MergeExonsChange extends FeatureChange {
123
134
  }
124
135
  mergedExon.setMin(Math.min(firstExon.min, secondExon.min))
125
136
  mergedExon.setMax(Math.max(firstExon.max, secondExon.max))
126
- const mergedAttrs = this.mergeAttributes(firstExon, secondExon)
127
- mergedExon.setAttributes(new Map())
128
- for (const key of Object.keys(mergedAttrs)) {
129
- mergedExon.setAttribute(key, mergedAttrs[key])
137
+
138
+ const mrg = mergedExon.attributes.get('merged_with')?.slice() ?? []
139
+ const mergedWith = stringifyAttributes(
140
+ attributesToRecords(secondExon.attributes),
141
+ )
142
+ if (!mrg.includes(mergedWith)) {
143
+ mrg.push(mergedWith)
130
144
  }
145
+ mergedExon.setAttribute('merged_with', mrg)
146
+
131
147
  mergedExon.parent?.deleteChild(secondExon._id)
132
148
  }
133
149
  }
@@ -151,35 +167,35 @@ export class MergeExonsChange extends FeatureChange {
151
167
  )
152
168
  }
153
169
 
154
- mergeAttributes(
155
- firstExon: AnnotationFeatureSnapshot,
156
- secondExon: AnnotationFeatureSnapshot,
157
- ): Record<string, string[]> {
158
- let mergedAttrs: Record<string, string[]> = {}
159
- if (firstExon.attributes) {
160
- // eslint-disable-next-line unicorn/prefer-structured-clone
161
- mergedAttrs = JSON.parse(JSON.stringify(firstExon.attributes))
162
- }
163
-
164
- if (secondExon.attributes) {
165
- // eslint-disable-next-line unicorn/prefer-structured-clone
166
- const attrs: Record<string, string[]> = JSON.parse(
167
- JSON.stringify(secondExon.attributes),
168
- )
169
- for (const key of Object.keys(attrs)) {
170
- if (key === '_id' || key === 'gff_id') {
171
- continue
172
- }
173
- if (!Object.keys(mergedAttrs).includes(key)) {
174
- mergedAttrs[key] = []
175
- }
176
- attrs[key].map((x) => {
177
- if (!mergedAttrs[key].includes(x)) {
178
- mergedAttrs[key].push(x)
179
- }
180
- })
181
- }
182
- }
183
- return mergedAttrs
184
- }
170
+ // mergeAttributes(
171
+ // firstExon: AnnotationFeatureSnapshot,
172
+ // secondExon: AnnotationFeatureSnapshot,
173
+ // ): Record<string, string[]> {
174
+ // let mergedAttrs: Record<string, string[]> = {}
175
+ // if (firstExon.attributes) {
176
+ // // eslint-disable-next-line unicorn/prefer-structured-clone
177
+ // mergedAttrs = JSON.parse(JSON.stringify(firstExon.attributes))
178
+ // }
179
+
180
+ // if (secondExon.attributes) {
181
+ // // eslint-disable-next-line unicorn/prefer-structured-clone
182
+ // const attrs: Record<string, string[]> = JSON.parse(
183
+ // JSON.stringify(secondExon.attributes),
184
+ // )
185
+ // for (const key of Object.keys(attrs)) {
186
+ // if (key === '_id' || key === 'gff_id') {
187
+ // continue
188
+ // }
189
+ // if (!Object.keys(mergedAttrs).includes(key)) {
190
+ // mergedAttrs[key] = []
191
+ // }
192
+ // attrs[key].map((x) => {
193
+ // if (!mergedAttrs[key].includes(x)) {
194
+ // mergedAttrs[key].push(x)
195
+ // }
196
+ // })
197
+ // }
198
+ // }
199
+ // return mergedAttrs
200
+ // }
185
201
  }
@@ -16,6 +16,9 @@ import {
16
16
  } from '@apollo-annotation/mst'
17
17
  import { type Feature } from '@apollo-annotation/schemas'
18
18
  import { doesIntersect2 } from '@jbrowse/core/util'
19
+ import { getSnapshot } from 'mobx-state-tree'
20
+
21
+ import { attributesToRecords, stringifyAttributes } from '../util'
19
22
 
20
23
  import { findAndDeleteChildFeature } from './DeleteFeatureChange'
21
24
  import { UndoMergeTranscriptsChange } from './UndoMergeTranscriptsChange'
@@ -42,6 +45,7 @@ interface SerializedMergeTranscriptsChangeMultiple
42
45
  export type SerializedMergeTranscriptsChange =
43
46
  | SerializedMergeTranscriptsChangeSingle
44
47
  | SerializedMergeTranscriptsChangeMultiple
48
+
45
49
  export class MergeTranscriptsChange extends FeatureChange {
46
50
  typeName = 'MergeTranscriptsChange' as const
47
51
  changes: MergeTranscriptsChangeDetails[]
@@ -50,6 +54,10 @@ export class MergeTranscriptsChange extends FeatureChange {
50
54
  super(json, options)
51
55
  this.changes = 'changes' in json ? json.changes : [json]
52
56
  }
57
+ // eslint-disable-next-line @typescript-eslint/class-literal-property-style
58
+ get notification() {
59
+ return 'Transcripts successfully merged'
60
+ }
53
61
 
54
62
  toJSON(): SerializedMergeTranscriptsChange {
55
63
  const { assembly, changedIds, changes, typeName } = this
@@ -111,7 +119,21 @@ export class MergeTranscriptsChange extends FeatureChange {
111
119
  ) {
112
120
  firstTranscript.min = Math.min(firstTranscript.min, secondTranscript.min)
113
121
  firstTranscript.max = Math.max(firstTranscript.max, secondTranscript.max)
114
- this.mergeTranscriptAttributes(firstTranscript, secondTranscript)
122
+
123
+ const mergedAttributes: Record<string, string[]> =
124
+ firstTranscript.attributes
125
+ ? JSON.parse(JSON.stringify(firstTranscript.attributes))
126
+ : {}
127
+ if (secondTranscript.attributes) {
128
+ if (!Object.keys(mergedAttributes).includes('merged_with')) {
129
+ mergedAttributes.merged_with = []
130
+ }
131
+ mergedAttributes.merged_with.push(
132
+ stringifyAttributes(attributesToRecords(secondTranscript.attributes)),
133
+ )
134
+ }
135
+ firstTranscript.attributes = mergedAttributes
136
+
115
137
  if (secondTranscript.children) {
116
138
  for (const [, secondFeatureChild] of Object.entries(
117
139
  secondTranscript.children,
@@ -133,9 +155,13 @@ export class MergeTranscriptsChange extends FeatureChange {
133
155
  }
134
156
  let merged = false
135
157
  let mrgChild: Feature | undefined
136
- for (const [fKey, firstFeatureChild] of firstTranscript.children) {
158
+ let toDelete
159
+ for (const [, firstFeatureChild] of firstTranscript.children) {
137
160
  if (!merged || !mrgChild) {
161
+ toDelete = false
138
162
  mrgChild = firstFeatureChild
163
+ } else {
164
+ toDelete = true
139
165
  }
140
166
  if (
141
167
  mrgChild.type === secondFeatureChild.type &&
@@ -163,9 +189,36 @@ export class MergeTranscriptsChange extends FeatureChange {
163
189
  mrgChild.max,
164
190
  firstFeatureChild.max,
165
191
  )
166
- const mergedAttrs = this.mergeAttributes(mrgChild, secondFeatureChild)
167
- mrgChild.attributes = mergedAttrs
168
- firstTranscript.children.delete(fKey)
192
+
193
+ if (!mrgChild.attributes) {
194
+ mrgChild.attributes = {}
195
+ }
196
+
197
+ const mrgChildAttr: Record<string, string[]> = JSON.parse(
198
+ JSON.stringify(mrgChild.attributes),
199
+ )
200
+
201
+ if (!Object.keys(mrgChildAttr).includes('merged_with')) {
202
+ mrgChildAttr.merged_with = []
203
+ }
204
+ const mergedWithAttributes = mrgChildAttr.merged_with
205
+ mergedWithAttributes.push(
206
+ stringifyAttributes(
207
+ attributesToRecords(secondFeatureChild.attributes),
208
+ ),
209
+ )
210
+
211
+ if (toDelete) {
212
+ const recs: Record<string, string[] | undefined> =
213
+ firstFeatureChild.attributes
214
+ ? JSON.parse(JSON.stringify(firstFeatureChild.attributes))
215
+ : undefined
216
+ mergedWithAttributes.push(stringifyAttributes(recs))
217
+ firstTranscript.children.delete(firstFeatureChild._id.toString())
218
+ }
219
+
220
+ mrgChildAttr.merged_with = [...new Set(mergedWithAttributes)]
221
+ mrgChild.attributes = mrgChildAttr
169
222
  merged = true
170
223
  }
171
224
  }
@@ -215,7 +268,16 @@ export class MergeTranscriptsChange extends FeatureChange {
215
268
  firstTranscript.setMin(Math.min(firstTranscript.min, secondTranscript.min))
216
269
  firstTranscript.setMax(Math.max(firstTranscript.max, secondTranscript.max))
217
270
 
218
- this.mergeTranscriptAttributes(firstTranscript, secondTranscript)
271
+ const mrg = firstTranscript.attributes.get('merged_with')?.slice() ?? []
272
+ const mergedWith = stringifyAttributes(
273
+ attributesToRecords(secondTranscript.attributes),
274
+ )
275
+
276
+ if (!mrg.includes(mergedWith)) {
277
+ // executeOnClient runs twice (?!) so avoid adding this key again
278
+ mrg.push(mergedWith)
279
+ }
280
+ firstTranscript.setAttribute('merged_with', mrg)
219
281
 
220
282
  if (secondTranscript.children) {
221
283
  for (const [, secondFeatureChild] of Object.entries(
@@ -269,15 +331,20 @@ export class MergeTranscriptsChange extends FeatureChange {
269
331
  Math.max(secondFeatureChild.max, mrgChild.max, firstFeatureChild.max),
270
332
  )
271
333
 
272
- const mergedAttrs = this.mergeAttributes(mrgChild, secondFeatureChild)
273
- Object.entries(mergedAttrs).map(([key, value]) => {
274
- if (mrgChild) {
275
- mrgChild.setAttribute(key, value)
276
- }
277
- })
334
+ const mergedWithAttributes =
335
+ mrgChild.attributes.get('merged_with')?.slice() ?? []
336
+ mergedWithAttributes.push(
337
+ stringifyAttributes(
338
+ attributesToRecords(secondFeatureChild.attributes),
339
+ ),
340
+ )
278
341
  if (toDelete) {
342
+ mergedWithAttributes.push(
343
+ stringifyAttributes(getSnapshot(firstFeatureChild).attributes),
344
+ )
279
345
  firstTranscript.deleteChild(firstFeatureChild._id)
280
346
  }
347
+ mrgChild.setAttribute('merged_with', [...new Set(mergedWithAttributes)])
281
348
  merged = true
282
349
  }
283
350
  }
@@ -288,71 +355,19 @@ export class MergeTranscriptsChange extends FeatureChange {
288
355
  })
289
356
  }
290
357
 
291
- if (!(merged && mrgChild)) {
358
+ if (merged && mrgChild) {
359
+ firstTranscript.addChild(getSnapshot(mrgChild))
360
+ } else {
292
361
  // This secondFeatureChild has no overlap with any feature in the
293
362
  // receiving transcript so we add it as it is to the receiving transcript
294
363
  firstTranscript.addChild(secondFeatureChild)
295
364
  }
296
365
  }
297
366
 
298
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
299
- isAnnotationFeature(obj: any): obj is AnnotationFeature {
300
- return (
301
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
302
- typeof obj.setMin === 'function' &&
303
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
304
- typeof obj.setMax === 'function' &&
305
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
306
- typeof obj.addChild === 'function' &&
307
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
308
- typeof obj.deleteChild === 'function'
309
- )
310
- }
311
-
312
367
  async executeOnLocalGFF3(_backend: LocalGFF3DataStore) {
313
368
  throw new Error('executeOnLocalGFF3 not implemented')
314
369
  }
315
370
 
316
- /* Merge attributes from source into destination */
317
- mergeAttributes(
318
- destination: Feature | AnnotationFeature,
319
- source: AnnotationFeatureSnapshot,
320
- ): Record<string, string[]> {
321
- const destAttrs: Record<string, string[]> = destination.attributes
322
- ? JSON.parse(JSON.stringify(destination.attributes))
323
- : {}
324
- if (source.attributes) {
325
- const sourceAttrs: Record<string, string[]> = JSON.parse(
326
- JSON.stringify(source.attributes),
327
- )
328
- Object.entries(sourceAttrs).map(([key, value]) => {
329
- if (!(key in destAttrs)) {
330
- destAttrs[key] = []
331
- }
332
- value.map((x) => {
333
- if (!destAttrs[key].includes(x)) {
334
- destAttrs[key].push(x)
335
- }
336
- })
337
- })
338
- }
339
- return destAttrs
340
- }
341
-
342
- mergeTranscriptAttributes(
343
- firstTranscript: Feature | AnnotationFeature,
344
- secondTranscript: AnnotationFeatureSnapshot,
345
- ) {
346
- const txAttrs = this.mergeAttributes(firstTranscript, secondTranscript)
347
- if (this.isAnnotationFeature(firstTranscript)) {
348
- Object.entries(txAttrs).map(([key, value]) => {
349
- firstTranscript.setAttribute(key, value)
350
- })
351
- } else {
352
- firstTranscript.attributes = txAttrs
353
- }
354
- }
355
-
356
371
  getInverse() {
357
372
  const { assembly, changedIds, changes, logger } = this
358
373
  const inverseChangedIds = [...changedIds].reverse()
@@ -22,7 +22,6 @@ enum CAUSES {
22
22
  'InternalStopCodon',
23
23
  'MissingStartCodon',
24
24
  'MissingStopCodon',
25
- 'MultipleOfThree',
26
25
  }
27
26
 
28
27
  const iupacComplements: Record<string, string | undefined> = {
@@ -156,6 +155,10 @@ async function checkMRNA(
156
155
  for (const cdsLocation of cdsLocations) {
157
156
  const sequence = await getCDSSequence(cdsLocation, strand, getSequence)
158
157
  const codons = splitSequenceInCodons(sequence)
158
+ const cdsEnd =
159
+ strand === -1
160
+ ? cdsLocation.at(0)?.min ?? min
161
+ : cdsLocation.at(-1)?.max ?? max
159
162
  if (sequence.length % 3 === 0) {
160
163
  const start_codon = codons.at(0)
161
164
  if (start_codon && !(start_codon.toUpperCase() in START_CODONS)) {
@@ -175,12 +178,9 @@ async function checkMRNA(
175
178
  message: `Unexpected start codon in feature "${_id}": ${start_codon}`,
176
179
  })
177
180
  }
181
+
178
182
  const lastCodon = codons.at(-1) // Last codon is supposed to be a stop
179
183
  if (lastCodon && !(lastCodon.toUpperCase() in STOP_CODONS)) {
180
- const cdsEnd =
181
- strand === -1
182
- ? cdsLocation.at(0)?.min ?? min
183
- : cdsLocation.at(-1)?.max ?? max
184
184
  checkResults.push({
185
185
  _id: new ObjectID().toHexString(),
186
186
  name: CHECK_NAME,
@@ -196,12 +196,12 @@ async function checkMRNA(
196
196
  checkResults.push({
197
197
  _id: new ObjectID().toHexString(),
198
198
  name: CHECK_NAME,
199
- cause: CAUSES[CAUSES.MultipleOfThree],
199
+ cause: CAUSES[CAUSES.MissingStopCodon],
200
200
  ids,
201
201
  refSeq: refSeq.toString(),
202
- start: min,
203
- end: max,
204
- message: `The coding sequence for feature "${_id}" is not a multiple of three`,
202
+ start: cdsEnd,
203
+ end: cdsEnd,
204
+ message: `Missing stop codon: The coding sequence for feature "${_id}" is not a multiple of three`,
205
205
  })
206
206
  }
207
207
  for (const [idx, codon] of codons.entries()) {
@@ -285,7 +285,7 @@ export class CDSCheck extends Check {
285
285
  name = CHECK_NAME
286
286
  causes = getCauses()
287
287
  version = 1
288
- default = true
288
+ isDefault = true
289
289
 
290
290
  async checkFeature(
291
291
  feature: AnnotationFeatureSnapshot,