@larsgw/formica 0.8.8 → 0.9.1

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.
package/CHANGELOG.md CHANGED
@@ -1,12 +1,35 @@
1
- ## [0.8.8](https://github.com/identification-resources/formica/compare/v0.8.7...v0.8.8) (2025-12-25)
1
+ ## [0.9.1](https://github.com/identification-resources/formica/compare/v0.9.0...v0.9.1) (2026-04-21)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **resources:** fix RDF mapping of taxonomic status ([c65383e](https://github.com/identification-resources/formica/commit/c65383e4d64c674c6c3b9f69e8cae925fcdf6896))
2
7
 
3
8
 
4
9
  ### Features
5
10
 
6
- * **resources:** add 'subclass' rank ([4b10405](https://github.com/identification-resources/formica/commit/4b10405b32840c1675caf7ef24bf2bbfd821a236))
11
+ * **catalog:** add more validation for taxa ([2bbc017](https://github.com/identification-resources/formica/commit/2bbc0177f8d4a6909339cfee812c33873770f725))
7
12
 
13
+ # [0.9.0](https://github.com/identification-resources/formica/compare/v0.8.8...v0.9.0) (2026-02-04)
8
14
 
9
15
 
16
+ ### Bug Fixes
17
+
18
+ * **resources:** improve taxonomic status ([0ea8fd2](https://github.com/identification-resources/formica/commit/0ea8fd220571f5f6ff05f517069aba10148c6888))
19
+
20
+
21
+ ### Features
22
+
23
+ * **resources:** include cluster data in DwC files ([60a28ac](https://github.com/identification-resources/formica/commit/60a28ace18b0760696735ca416a2e8d014d1c928))
24
+ * **resources:** include uncertainty of synonymy in DwC if specified ([03b6734](https://github.com/identification-resources/formica/commit/03b6734851dc9c331ec88fab21e90ad3f0303c3c))
25
+
26
+ ## [0.8.8](https://github.com/identification-resources/formica/compare/v0.8.7...v0.8.8) (2025-12-25)
27
+
28
+
29
+ ### Features
30
+
31
+ * **resources:** add 'subclass' rank ([4b10405](https://github.com/identification-resources/formica/commit/4b10405b32840c1675caf7ef24bf2bbfd821a236))
32
+
10
33
  ## [0.8.7](https://github.com/identification-resources/formica/compare/v0.8.6...v0.8.7) (2025-11-01)
11
34
 
12
35
 
@@ -14,8 +37,6 @@
14
37
 
15
38
  * **resources:** fix handling of corrections ([49c05b4](https://github.com/identification-resources/formica/commit/49c05b499262e64514b832313360d1e6a4cc6378))
16
39
 
17
-
18
-
19
40
  ## [0.8.6](https://github.com/identification-resources/formica/compare/v0.8.5...v0.8.6) (2025-10-28)
20
41
 
21
42
 
@@ -29,8 +50,6 @@
29
50
 
30
51
  * **resources:** ignore errors in later-corrected names ([4c1f69e](https://github.com/identification-resources/formica/commit/4c1f69e81450664f122cdf00e68cb918248790e6)), closes [#5](https://github.com/identification-resources/formica/issues/5)
31
52
 
32
-
33
-
34
53
  ## [0.8.5](https://github.com/identification-resources/formica/compare/v0.8.4...v0.8.5) (2025-10-28)
35
54
 
36
55
 
@@ -48,8 +67,6 @@
48
67
  * **resources:** handle subgenus synonyms of genera ([4e61336](https://github.com/identification-resources/formica/commit/4e61336e36054ffd6df57ebad5479e7ceebcb235)), closes [#16](https://github.com/identification-resources/formica/issues/16)
49
68
  * **resources:** rework parsing of resources ([cff7730](https://github.com/identification-resources/formica/commit/cff773000c5b9e3f990c619cdd45a76a18ee39a9)), closes [#15](https://github.com/identification-resources/formica/issues/15) [#7](https://github.com/identification-resources/formica/issues/7)
50
69
 
51
-
52
-
53
70
  ## [0.8.4](https://github.com/identification-resources/formica/compare/v0.8.3...v0.8.4) (2025-09-19)
54
71
 
55
72
 
@@ -63,8 +80,6 @@
63
80
 
64
81
  * **catalog:** map additional scope value ([80c6d26](https://github.com/identification-resources/formica/commit/80c6d269a7d5b03600991fb353280e81afcefb0e))
65
82
 
66
-
67
-
68
83
  ## [0.8.3](https://github.com/identification-resources/formica/compare/v0.8.2...v0.8.3) (2025-07-15)
69
84
 
70
85
 
@@ -72,8 +87,6 @@
72
87
 
73
88
  * **catalog:** add catalog field key_characteristics ([6529cfa](https://github.com/identification-resources/formica/commit/6529cfab81f759f926b81bdd9d5a36e84b526284))
74
89
 
75
-
76
-
77
90
  ## [0.8.2](https://github.com/identification-resources/formica/compare/v0.8.1...v0.8.2) (2025-06-26)
78
91
 
79
92
 
@@ -82,8 +95,6 @@
82
95
  * **resources:** add support for documented flag ([6cb89e4](https://github.com/identification-resources/formica/commit/6cb89e428bde8665b7e8f03b5c7f47a44e15f58e))
83
96
  * **resources:** allow hyphens in biname pattern ([6a58a77](https://github.com/identification-resources/formica/commit/6a58a77757b8b6ae8c2d7d01440775776d183d05))
84
97
 
85
-
86
-
87
98
  ## [0.8.1](https://github.com/identification-resources/formica/compare/v0.8.0...v0.8.1) (2025-05-30)
88
99
 
89
100
 
@@ -97,8 +108,6 @@
97
108
 
98
109
  * **resources:** add support for "(sub)gen. nov" pattern ([d2598eb](https://github.com/identification-resources/formica/commit/d2598ebfd2f80d9c2bcc00ce145b9fc229316e5b))
99
110
 
100
-
101
-
102
111
  # [0.8.0](https://github.com/identification-resources/formica/compare/v0.7.3...v0.8.0) (2025-05-07)
103
112
 
104
113
 
@@ -111,8 +120,6 @@
111
120
 
112
121
  * **catalog:** implement generation of linked data ([334616e](https://github.com/identification-resources/formica/commit/334616e2e12c0d91e49f84f06bdd00ab7a238b8e))
113
122
 
114
-
115
-
116
123
  ## [0.7.3](https://github.com/identification-resources/formica/compare/v0.7.2...v0.7.3) (2025-04-17)
117
124
 
118
125
 
@@ -120,8 +127,6 @@
120
127
 
121
128
  * **catalog:** update validation for identifiers ([156a703](https://github.com/identification-resources/formica/commit/156a7036ab09c411d091bc9d0f628831a30ba3f5))
122
129
 
123
-
124
-
125
130
  ## [0.7.2](https://github.com/identification-resources/formica/compare/v0.7.1...v0.7.2) (2025-04-02)
126
131
 
127
132
 
@@ -129,8 +134,6 @@
129
134
 
130
135
  * **catalog:** add validation for taxa.csv ([59cfcf2](https://github.com/identification-resources/formica/commit/59cfcf28ebbef42e28e648e800c178a7d528f37a))
131
136
 
132
-
133
-
134
137
  ## [0.7.1](https://github.com/identification-resources/formica/compare/v0.7.0...v0.7.1) (2025-03-25)
135
138
 
136
139
 
@@ -138,8 +141,6 @@
138
141
 
139
142
  * **resources:** add 'subphylum' rank ([8b5243b](https://github.com/identification-resources/formica/commit/8b5243bfd79b6486c613ca1178f1e1c9ec3f43a4))
140
143
 
141
-
142
-
143
144
  # [0.7.0](https://github.com/identification-resources/formica/compare/v0.6.8...v0.7.0) (2025-03-14)
144
145
 
145
146
 
@@ -157,8 +158,6 @@
157
158
 
158
159
  * **resources:** "indet" lines now have to be prefixed with "[indet]"
159
160
 
160
-
161
-
162
161
  ## [0.6.8](https://github.com/identification-resources/formica/compare/v0.6.7...v0.6.8) (2025-03-13)
163
162
 
164
163
 
@@ -168,8 +167,6 @@
168
167
  * **resources:** create index of CoL identifiers ([3792903](https://github.com/identification-resources/formica/commit/37929035374bc0c67ed2403e775e06a904bdbd4d))
169
168
  * **resources:** support intergeneric hybrids ([0a6696c](https://github.com/identification-resources/formica/commit/0a6696c5fd0603a0366c80c2298b6544636ed0c6))
170
169
 
171
-
172
-
173
170
  ## [0.6.7](https://github.com/identification-resources/formica/compare/v0.6.6...v0.6.7) (2025-02-26)
174
171
 
175
172
 
@@ -184,8 +181,6 @@
184
181
 
185
182
  * **resources:** add support for completeness flags ([55b5427](https://github.com/identification-resources/formica/commit/55b5427125b4f1072191cadbc461ba31d37e6f2e))
186
183
 
187
-
188
-
189
184
  ## [0.6.6](https://github.com/identification-resources/formica/compare/v0.6.5...v0.6.6) (2025-02-16)
190
185
 
191
186
 
@@ -193,8 +188,6 @@
193
188
 
194
189
  * **resources:** allow other resources in version_of ([85947b0](https://github.com/identification-resources/formica/commit/85947b0dcfbdb4bfd6537c628b34087a6e6326f7))
195
190
 
196
-
197
-
198
191
  ## [0.6.5](https://github.com/identification-resources/formica/compare/v0.6.4...v0.6.5) (2024-12-20)
199
192
 
200
193
 
@@ -204,8 +197,6 @@
204
197
  * **resources:** improve (ICBN) author parsing ([d3e3faf](https://github.com/identification-resources/formica/commit/d3e3faf411d1a7dccd528a5ee78bd0823a5883b4)), closes [#13](https://github.com/identification-resources/formica/issues/13) [#14](https://github.com/identification-resources/formica/issues/14)
205
198
  * **resources:** support "et al." in author name ([82993bf](https://github.com/identification-resources/formica/commit/82993bffac5e7f3ed00d00c28c836f347b264a43))
206
199
 
207
-
208
-
209
200
  ## [0.6.4](https://github.com/identification-resources/formica/compare/v0.6.3...v0.6.4) (2024-12-16)
210
201
 
211
202
 
@@ -213,8 +204,6 @@
213
204
 
214
205
  * **resources:** fix matching of some ICBN names ([3c18458](https://github.com/identification-resources/formica/commit/3c184586b64e2a31f1eb7298bcd7c80c1d119069))
215
206
 
216
-
217
-
218
207
  ## [0.6.3](https://github.com/identification-resources/formica/compare/v0.6.2...v0.6.3) (2024-04-12)
219
208
 
220
209
 
@@ -222,8 +211,6 @@
222
211
 
223
212
  * **resources:** allow 'phylum' rank ([ff9be66](https://github.com/identification-resources/formica/commit/ff9be66abab79a86263764b54d6a2709833b14d4))
224
213
 
225
-
226
-
227
214
  ## [0.6.2](https://github.com/identification-resources/formica/compare/v0.6.1...v0.6.2) (2024-04-08)
228
215
 
229
216
 
@@ -236,8 +223,6 @@
236
223
 
237
224
  * **catalog:** add script to de-duplicate links ([b719348](https://github.com/identification-resources/formica/commit/b719348635e1b35ee80758c0cdd9938625d79ebf))
238
225
 
239
-
240
-
241
226
  ## [0.6.1](https://github.com/identification-resources/formica/compare/v0.6.0...v0.6.1) (2024-03-20)
242
227
 
243
228
 
@@ -245,8 +230,6 @@
245
230
 
246
231
  * **resources:** allow two-part hybrid names ([8e3b836](https://github.com/identification-resources/formica/commit/8e3b8368985d1db35b62d306d833121150e91865))
247
232
 
248
-
249
-
250
233
  # [0.6.0](https://github.com/identification-resources/formica/compare/v0.5.2...v0.6.0) (2024-01-05)
251
234
 
252
235
 
@@ -254,8 +237,6 @@
254
237
 
255
238
  * **catalog:** add entry type, key type values ([ed6e4fb](https://github.com/identification-resources/formica/commit/ed6e4fb545e09cddb5d466f602bffb8a8483f043))
256
239
 
257
-
258
-
259
240
  ## [0.5.2](https://github.com/identification-resources/formica/compare/v0.5.1...v0.5.2) (2023-11-27)
260
241
 
261
242
 
@@ -263,8 +244,6 @@
263
244
 
264
245
  * **resources:** fix GBIF index generation ([c88e2d2](https://github.com/identification-resources/formica/commit/c88e2d2b306b930ca0b00b978e3eb89b8adfb656))
265
246
 
266
-
267
-
268
247
  ## [0.5.1](https://github.com/identification-resources/formica/compare/v0.5.0...v0.5.1) (2023-11-03)
269
248
 
270
249
 
@@ -272,8 +251,6 @@
272
251
 
273
252
  * **resources:** fix file listing in processor ([009b980](https://github.com/identification-resources/formica/commit/009b98091b0f597e91213ace82252facbe3b5fed))
274
253
 
275
-
276
-
277
254
  # [0.5.0](https://github.com/identification-resources/formica/compare/v0.4.3...v0.5.0) (2023-10-24)
278
255
 
279
256
 
@@ -304,8 +281,6 @@
304
281
  does no longer pass validation. Use the "taxon_scope" and "scope" fields
305
282
  in "catalog" instead.
306
283
 
307
-
308
-
309
284
  ## [0.4.3](https://github.com/identification-resources/formica/compare/v0.4.2...v0.4.3) (2023-09-30)
310
285
 
311
286
 
@@ -313,8 +288,6 @@ in "catalog" instead.
313
288
 
314
289
  * **resource:** fix regression in taxon parsing ([098477b](https://github.com/identification-resources/formica/commit/098477b5914324a3c15ec885617d4cf30996f30d))
315
290
 
316
-
317
-
318
291
  ## [0.4.2](https://github.com/identification-resources/formica/compare/v0.4.1...v0.4.2) (2023-09-30)
319
292
 
320
293
 
@@ -324,8 +297,6 @@ in "catalog" instead.
324
297
  * **resources:** correct synonym rank determintation ([766ea2a](https://github.com/identification-resources/formica/commit/766ea2af14ff3aa64a7a2e19ed096836d38182cf))
325
298
  * **resources:** do not parse "indet." lines as taxa ([f4debf6](https://github.com/identification-resources/formica/commit/f4debf695b0cdb375bfb60ab5ce942fa2f7dee85)), closes [#8](https://github.com/identification-resources/formica/issues/8)
326
299
 
327
-
328
-
329
300
  ## [0.4.1](https://github.com/identification-resources/formica/compare/v0.4.0...v0.4.1) (2023-09-07)
330
301
 
331
302
 
@@ -333,8 +304,6 @@ in "catalog" instead.
333
304
 
334
305
  * **catalog:** fix typo in duplicate_of schema ([fbb6333](https://github.com/identification-resources/formica/commit/fbb6333d10dd6f5846856ea8de81d941ce539d21))
335
306
 
336
-
337
-
338
307
  # [0.4.0](https://github.com/identification-resources/formica/compare/v0.3.1...v0.4.0) (2023-09-07)
339
308
 
340
309
 
@@ -348,8 +317,6 @@ in "catalog" instead.
348
317
 
349
318
  * **catalog:** add duplicate_of field ([e4a8bbe](https://github.com/identification-resources/formica/commit/e4a8bbecef9c6a40d0f2cc5d4e8bb67685a453ff))
350
319
 
351
-
352
-
353
320
  ## [0.3.1](https://github.com/identification-resources/formica/compare/v0.3.0...v0.3.1) (2023-09-04)
354
321
 
355
322
 
@@ -359,8 +326,6 @@ in "catalog" instead.
359
326
  * **resources:** fix regression in c737d5b ([d4e52e9](https://github.com/identification-resources/formica/commit/d4e52e95ed1a35dabbd80b9f9aace1a824fdae98))
360
327
  * **resources:** handle corrections to synonyms ([21ed9b7](https://github.com/identification-resources/formica/commit/21ed9b79f77e1e6d0639d96ed9822d5212a85a2e)), closes [#4](https://github.com/identification-resources/formica/issues/4)
361
328
 
362
-
363
-
364
329
  # [0.3.0](https://github.com/identification-resources/formica/compare/v0.2.1...v0.3.0) (2023-08-20)
365
330
 
366
331
 
@@ -372,8 +337,6 @@ in "catalog" instead.
372
337
  * **resources:** improve taxon name matching ([78ff480](https://github.com/identification-resources/formica/commit/78ff480485ca42bd1d1f2893f230e61db6cb6be8)), closes [#2](https://github.com/identification-resources/formica/issues/2)
373
338
  * **resources:** test for rank mismatch ([0e69b7f](https://github.com/identification-resources/formica/commit/0e69b7f0a6654fa5255e797be7516cd06edb4df3)), closes [#2](https://github.com/identification-resources/formica/issues/2)
374
339
 
375
-
376
-
377
340
  ## [0.2.1](https://github.com/identification-resources/formica/compare/v0.2.0...v0.2.1) (2023-08-10)
378
341
 
379
342
 
@@ -382,8 +345,6 @@ in "catalog" instead.
382
345
  * **resources:** do not validate old versions ([1a8dd2e](https://github.com/identification-resources/formica/commit/1a8dd2e0e373489e287d1fd89dbb7443245c5214))
383
346
  * **resources:** fix check for missing leaf taxa ([035f1ad](https://github.com/identification-resources/formica/commit/035f1ad20efa80b388ec3828d4ace4518c7b344e))
384
347
 
385
-
386
-
387
348
  # [0.2.0](https://github.com/identification-resources/formica/compare/v0.1.1...v0.2.0) (2023-08-09)
388
349
 
389
350
 
@@ -392,8 +353,6 @@ in "catalog" instead.
392
353
  * **catalog:** add taxon_scope column ([6a06c37](https://github.com/identification-resources/formica/commit/6a06c37ec0640a8ffbc258ad6a4d81d84d35fee9))
393
354
  * **resources:** check for missing leaf taxa ([387c47d](https://github.com/identification-resources/formica/commit/387c47d10bb7f1fb09a68c10dfa869bb924b7633))
394
355
 
395
-
396
-
397
356
  ## [0.1.1](https://github.com/identification-resources/formica/compare/v0.1.0...v0.1.1) (2023-05-10)
398
357
 
399
358
 
@@ -401,8 +360,6 @@ in "catalog" instead.
401
360
 
402
361
  * **processor:** add Node hashbang ([51dee57](https://github.com/identification-resources/formica/commit/51dee57b8547afba449b2b846bd7d65e7927368f))
403
362
 
404
-
405
-
406
363
  # [0.1.0](https://github.com/identification-resources/formica/compare/fed91fd6f350c47bd067d221a4d0e2278a199dae...v0.1.0) (2023-03-01)
407
364
 
408
365
 
@@ -424,6 +381,3 @@ in "catalog" instead.
424
381
  * **resources:** add support for hybrids ([e49e96a](https://github.com/identification-resources/formica/commit/e49e96af9c3958bab2a3b0508b1fc8a018b80716))
425
382
  * **validate:** add catalog validation script ([4cdf785](https://github.com/identification-resources/formica/commit/4cdf785afb2c77320cd9b5b76483e3464c0f1cff))
426
383
  * **validate:** add script to validate resources ([246fb5d](https://github.com/identification-resources/formica/commit/246fb5dd8da03e736884850874cc51a11be9985c))
427
-
428
-
429
-
@@ -125,7 +125,8 @@ const GBIF_RANKS = ['kingdom', 'phylum', 'class', 'order', 'family', 'genus', 's
125
125
  const GBIF_VOCAB_RANKS = ['domain', 'kingdom', 'subkingdom', 'superphylum', 'phylum', 'subphylum', 'superclass', 'class', 'subclass', 'supercohort', 'cohort', 'subcohort', 'superorder', 'order', 'suborder', 'infraorder', 'superfamily', 'family', 'subfamily', 'tribe', 'subtribe', 'genus', 'subgenus', 'section', 'subsection', 'series', 'subseries', 'speciesAggregate', 'species', 'subspecificAggregate', 'subspecies', 'variety', 'subvariety', 'form', 'subform', 'cultivarGroup', 'cultivar', 'strain'];
126
126
  const STATUSES = {
127
127
  'accepted': 'http://rs.gbif.org/vocabulary/gbif/taxonomicStatus/accepted',
128
- 'heterotypic synonym': 'http://rs.gbif.org/vocabulary/gbif/taxonomicStatus/heterotypicSynonym',
128
+ 'misapplied': 'http://rs.gbif.org/vocabulary/gbif/taxonomicStatus/misapplied',
129
+ 'proparte synonym': 'http://rs.gbif.org/vocabulary/gbif/taxonomicStatus/proParteSynonym',
129
130
  'synonym': 'http://rs.gbif.org/vocabulary/gbif/taxonomicStatus/synonym',
130
131
  };
131
132
  function getCoveringTaxon(taxa) {
@@ -284,9 +285,12 @@ function makeLinkedDataForScientificName(name) {
284
285
  if (GBIF_VOCAB_RANKS.includes(name.taxonRank)) {
285
286
  node[DWC_FIELDS.taxonRank] = makeTaxonRankUri(name.taxonRank);
286
287
  }
287
- if (name.taxonomicStatus) {
288
+ if (name.taxonomicStatus in STATUSES) {
288
289
  node[DWC_FIELDS.taxonomicStatus] = makeTaxonomicStatusUri(name.taxonomicStatus);
289
290
  }
291
+ else {
292
+ console.error('Unmapped taxonomic status:', name.taxonomicStatus);
293
+ }
290
294
  if (name.acceptedNameUsageID) {
291
295
  node['dwc:acceptedNameUsageID'] = makeScientificNameUri(name.acceptedNameUsageID);
292
296
  }
@@ -82,6 +82,7 @@ const DWC_FIELDS = [
82
82
  'subgenus',
83
83
  'higherClassification',
84
84
  'verbatimIdentification',
85
+ 'dynamicProperties',
85
86
  'colTaxonID',
86
87
  'gbifTaxonID',
87
88
  'colAcceptedTaxonID',
@@ -46,12 +46,66 @@ Object.defineProperty(exports, "__esModule", { value: true });
46
46
  const fs_1 = require("fs");
47
47
  const path = __importStar(require("path"));
48
48
  const index_1 = require("../index");
49
+ class TaxaValidator {
50
+ constructor(taxa) {
51
+ this.taxa = Array.from(taxa).filter(taxon => taxon.has('ancestors_gbif'));
52
+ this.errors = [];
53
+ this.parentIndex = {};
54
+ }
55
+ validate() {
56
+ for (const taxon of this.taxa) {
57
+ const parents = taxon.get('ancestors_gbif');
58
+ if (parents.length > 1) {
59
+ const id = taxon.get('id');
60
+ for (let i = 1; i < parents.length; i++) {
61
+ this.addToIndex(id, 'ancestors_gbif', parents[i], parents[i - 1]);
62
+ }
63
+ }
64
+ }
65
+ for (const taxon of this.taxa) {
66
+ if (taxon.has('gbif')) {
67
+ const id = taxon.get('id');
68
+ const parents = taxon.get('ancestors_gbif');
69
+ const parent = parents[parents.length - 1];
70
+ this.addToIndex(id, 'gbif', taxon.get('gbif'), parent);
71
+ }
72
+ }
73
+ for (const taxon of this.taxa) {
74
+ if (taxon.has('children_gbif')) {
75
+ const id = taxon.get('id');
76
+ const parents = taxon.get('ancestors_gbif');
77
+ const parent = parents[parents.length - 1];
78
+ const childIds = taxon.get('children_gbif');
79
+ for (const childId of childIds) {
80
+ this.addToIndex(id, 'gbif', childId, parent);
81
+ }
82
+ }
83
+ }
84
+ return this.errors;
85
+ }
86
+ addToIndex(entity, field, childId, parentId) {
87
+ const actualParentId = this.parentIndex[childId];
88
+ if (!actualParentId) {
89
+ this.parentIndex[childId] = parentId;
90
+ }
91
+ else if (actualParentId !== parentId) {
92
+ const error = `Inconsistent ancestor of ${childId}, expected ${actualParentId} but got ${parentId}`;
93
+ this.errors.push({ entity, field, error });
94
+ }
95
+ }
96
+ }
49
97
  function validateFile(arg) {
50
98
  return __awaiter(this, void 0, void 0, function* () {
51
99
  const filePath = path.resolve(arg);
52
100
  const file = yield fs_1.promises.readFile(filePath, 'utf8');
53
101
  const sheet = path.basename(filePath, '.csv');
54
- return index_1.catalog.loadData(file, sheet).validate();
102
+ const entities = index_1.catalog.loadData(file, sheet);
103
+ const errors = entities.validate();
104
+ if (sheet === 'taxa') {
105
+ const validator = new TaxaValidator(entities);
106
+ errors.push(...validator.validate());
107
+ }
108
+ return errors;
55
109
  });
56
110
  }
57
111
  function main(args) {
@@ -43,7 +43,7 @@ exports.RANKS = [
43
43
  ];
44
44
  const TAXONOMIC_STATUS = {
45
45
  '>': 'incorrect',
46
- '+': 'heterotypic synonym',
46
+ '+': 'proparte synonym',
47
47
  '=': 'synonym'
48
48
  };
49
49
  const RANK_LABELS = {
@@ -113,6 +113,7 @@ const SUBGENUS_PATTERN = /^([A-Z]\S+) (?:\(([A-Z]\S+?)\))(?= |$)/;
113
113
  * $2c intergeneric hybrid: [A-Z][a-z]+_[a-z-]+ x [A-Z][a-z]+_[a-z-]+
114
114
  */
115
115
  const BINAME_PATTERN = /^(?:((?:x )?[A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?(x [a-z-]+|[a-z-][^\s.]+(?: x [a-z-]+)?|[A-Z][a-z]+_[a-z-]+ x [A-Z][a-z]+_[a-z-]+)(?= |$)/;
116
+ const CLUSTER_PATTERN = /^\[(_|\d+)\] /;
116
117
  function compareRanks(a, b) {
117
118
  return exports.RANKS.indexOf(a) - exports.RANKS.indexOf(b);
118
119
  }
@@ -158,15 +159,21 @@ function parseName(name, rank, parent) {
158
159
  const isSynonym = /^[+=>] /.test(name);
159
160
  if (isSynonym) {
160
161
  item.taxonomicStatus = TAXONOMIC_STATUS[name[0]];
161
- name = name.replace(/^[+=>] (\? ?)?/, '');
162
+ name = name.slice(2);
163
+ if (/^\? ?/.test(name)) {
164
+ item.dynamicProperties = '{"synonymUncertain":true}';
165
+ name = name.replace(/^\? ?/, '');
166
+ }
162
167
  rank = getSynonymRank(name, parent.taxonRank);
163
168
  }
164
169
  else {
165
170
  item.taxonomicStatus = 'accepted';
166
171
  }
167
172
  // Clusters
168
- if (/^\[(_|\d+)\] /.test(name)) {
169
- name = name.replace(/^\[(_|\d+)\] /, '');
173
+ if (CLUSTER_PATTERN.test(name)) {
174
+ const [match, cluster] = name.match(CLUSTER_PATTERN);
175
+ item.cluster = cluster;
176
+ name = name.slice(match.length);
170
177
  }
171
178
  // Set verbatim identification after subsequent syntax is removed.
172
179
  item.verbatimIdentification = name.replace(/(?<=^| )x(?=$| )/g, HYBRID_SIGN).replace(/_/g, ' ');
@@ -351,5 +358,8 @@ function parseName(name, rank, parent) {
351
358
  if (item.scientificNameAuthorship) {
352
359
  item.scientificName += ` ${item.scientificNameAuthorship}`;
353
360
  }
361
+ if (isSynonym && /^auctt?\./.test(item.taxonRemarks)) {
362
+ item.taxonomicStatus = 'misapplied';
363
+ }
354
364
  return item;
355
365
  }
@@ -153,6 +153,32 @@ function parseResource(resource) {
153
153
  const offsetLine = resource.offsetLine + (header + _).split('\n').length - 1;
154
154
  return [config, { content, offsetLine }];
155
155
  }
156
+ function getTaxonChildren(parent, taxa) {
157
+ const children = [];
158
+ for (const id in taxa) {
159
+ if (taxa[id].parentNameUsageID === parent) {
160
+ children.push(taxa[id]);
161
+ }
162
+ }
163
+ return children;
164
+ }
165
+ function processClusters(taxa) {
166
+ for (const id in taxa) {
167
+ const taxon = taxa[id];
168
+ if (taxon.taxonomicStatus !== 'accepted' || !taxon.cluster) {
169
+ continue;
170
+ }
171
+ const dynamicProperties = taxon.dynamicProperties ? JSON.parse(taxon.dynamicProperties) : {};
172
+ if (taxon.cluster === '_') {
173
+ dynamicProperties.identifiable = false;
174
+ }
175
+ else {
176
+ const siblings = getTaxonChildren(taxon.parentNameUsageID, taxa).filter(sibling => sibling.scientificNameID !== id);
177
+ dynamicProperties.indistinguishableFrom = siblings.filter(sibling => sibling.cluster === taxon.cluster).map(sibling => sibling.scientificNameID);
178
+ }
179
+ taxon.dynamicProperties = JSON.stringify(dynamicProperties);
180
+ }
181
+ }
156
182
  function parseResourceContent(content, resource, oldIds, offsetLine) {
157
183
  var _a, _b;
158
184
  const leafTaxonIndex = resource.metadata.levels.reduce((last, rank, i) => MAIN_RANKS.includes(rank) ? i : last, 0);
@@ -311,6 +337,7 @@ function parseResourceContent(content, resource, oldIds, offsetLine) {
311
337
  if (errors.length) {
312
338
  throw mergeParserErrors(errors);
313
339
  }
340
+ processClusters(data);
314
341
  return resource;
315
342
  }
316
343
  function splitResources(file) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@larsgw/formica",
3
- "version": "0.8.8",
3
+ "version": "0.9.1",
4
4
  "description": "SDK and tools for data from the Library of Identification Resources",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -34,7 +34,7 @@
34
34
  "dependencies": {
35
35
  "ietf-language-tag-regex": "^0.0.5",
36
36
  "js-yaml": "^4.1.0",
37
- "jsonld": "^8.3.3",
37
+ "jsonld": "^9.0.0",
38
38
  "n3": "^1.25.2",
39
39
  "spdx-license-list": "^6.6.0"
40
40
  },
@@ -106,7 +106,8 @@ const GBIF_RANKS = ['kingdom', 'phylum', 'class', 'order', 'family', 'genus', 's
106
106
  const GBIF_VOCAB_RANKS = ['domain', 'kingdom', 'subkingdom', 'superphylum', 'phylum', 'subphylum', 'superclass', 'class', 'subclass', 'supercohort', 'cohort', 'subcohort', 'superorder', 'order', 'suborder', 'infraorder', 'superfamily', 'family', 'subfamily', 'tribe', 'subtribe', 'genus', 'subgenus', 'section', 'subsection', 'series', 'subseries', 'speciesAggregate', 'species', 'subspecificAggregate', 'subspecies', 'variety', 'subvariety', 'form', 'subform', 'cultivarGroup', 'cultivar', 'strain']
107
107
  const STATUSES: Record<string, string> = {
108
108
  'accepted': 'http://rs.gbif.org/vocabulary/gbif/taxonomicStatus/accepted',
109
- 'heterotypic synonym': 'http://rs.gbif.org/vocabulary/gbif/taxonomicStatus/heterotypicSynonym',
109
+ 'misapplied': 'http://rs.gbif.org/vocabulary/gbif/taxonomicStatus/misapplied',
110
+ 'proparte synonym': 'http://rs.gbif.org/vocabulary/gbif/taxonomicStatus/proParteSynonym',
110
111
  'synonym': 'http://rs.gbif.org/vocabulary/gbif/taxonomicStatus/synonym',
111
112
  }
112
113
 
@@ -182,8 +183,8 @@ function makeTaxonRankUri (rank: string): NodeObject|string {
182
183
  }
183
184
  }
184
185
 
185
- function makeTaxonomicStatusUri (status: string): NodeObject {
186
- return { '@id': STATUSES[status] as string }
186
+ function makeTaxonomicStatusUri (status: keyof typeof STATUSES): NodeObject {
187
+ return { '@id': STATUSES[status] }
187
188
  }
188
189
 
189
190
  function makeLinkedDataForAuthor (author: catalog.Entity): NodeObject {
@@ -313,8 +314,10 @@ function makeLinkedDataForScientificName (name: AmendedTaxon): NodeObject {
313
314
  node[DWC_FIELDS.taxonRank] = makeTaxonRankUri(name.taxonRank)
314
315
  }
315
316
 
316
- if (name.taxonomicStatus) {
317
+ if (name.taxonomicStatus in STATUSES) {
317
318
  node[DWC_FIELDS.taxonomicStatus] = makeTaxonomicStatusUri(name.taxonomicStatus)
319
+ } else {
320
+ console.error('Unmapped taxonomic status:', name.taxonomicStatus)
318
321
  }
319
322
 
320
323
  if (name.acceptedNameUsageID) {
@@ -44,6 +44,8 @@ const DWC_FIELDS: (keyof AmendedTaxon)[] = [
44
44
  'higherClassification',
45
45
  'verbatimIdentification',
46
46
 
47
+ 'dynamicProperties',
48
+
47
49
  'colTaxonID',
48
50
  'gbifTaxonID',
49
51
  'colAcceptedTaxonID',
@@ -4,11 +4,81 @@ import { promises as fs } from 'fs'
4
4
  import * as path from 'path'
5
5
  import { catalog } from '../index'
6
6
 
7
+ class TaxaValidator {
8
+ taxa: catalog.Entity[];
9
+ errors: WorkError[];
10
+ parentIndex: Record<string, string>;
11
+
12
+ constructor (taxa: catalog.Entities) {
13
+ this.taxa = Array.from(taxa).filter(taxon => taxon.has('ancestors_gbif'))
14
+ this.errors = []
15
+ this.parentIndex = {}
16
+ }
17
+
18
+ validate (): WorkError[] {
19
+ for (const taxon of this.taxa) {
20
+ const parents = taxon.get('ancestors_gbif') as string[]
21
+
22
+ if (parents.length > 1) {
23
+ const id = taxon.get('id') as string
24
+
25
+ for (let i = 1; i < parents.length; i++) {
26
+ this.addToIndex(id, 'ancestors_gbif', parents[i], parents[i - 1])
27
+ }
28
+ }
29
+ }
30
+
31
+ for (const taxon of this.taxa) {
32
+ if (taxon.has('gbif')) {
33
+ const id = taxon.get('id') as string
34
+ const parents = taxon.get('ancestors_gbif') as string[]
35
+ const parent = parents[parents.length - 1] as string
36
+
37
+ this.addToIndex(id, 'gbif', taxon.get('gbif') as string, parent)
38
+ }
39
+ }
40
+
41
+ for (const taxon of this.taxa) {
42
+ if (taxon.has('children_gbif')) {
43
+ const id = taxon.get('id') as string
44
+ const parents = taxon.get('ancestors_gbif') as string[]
45
+ const parent = parents[parents.length - 1] as string
46
+
47
+ const childIds = taxon.get('children_gbif') as string[]
48
+ for (const childId of childIds) {
49
+ this.addToIndex(id, 'gbif', childId, parent)
50
+ }
51
+ }
52
+ }
53
+
54
+ return this.errors
55
+ }
56
+
57
+ addToIndex (entity: string, field: string, childId: string, parentId: string) {
58
+ const actualParentId = this.parentIndex[childId]
59
+
60
+ if (!actualParentId) {
61
+ this.parentIndex[childId] = parentId
62
+ } else if (actualParentId !== parentId) {
63
+ const error = `Inconsistent ancestor of ${childId}, expected ${actualParentId} but got ${parentId}`
64
+ this.errors.push({ entity, field, error })
65
+ }
66
+ }
67
+ }
68
+
7
69
  async function validateFile (arg: string): Promise<WorkError[]> {
8
70
  const filePath = path.resolve(arg)
9
71
  const file = await fs.readFile(filePath, 'utf8')
10
72
  const sheet = path.basename(filePath, '.csv')
11
- return catalog.loadData(file, sheet).validate()
73
+ const entities = catalog.loadData(file, sheet)
74
+ const errors = entities.validate()
75
+
76
+ if (sheet === 'taxa') {
77
+ const validator = new TaxaValidator(entities)
78
+ errors.push(...validator.validate())
79
+ }
80
+
81
+ return errors
12
82
  }
13
83
 
14
84
  async function main (args: string[]): Promise<void> {
package/src/module.d.ts CHANGED
@@ -56,12 +56,14 @@ interface TaxonBase {
56
56
  subgenus?: string,
57
57
  higherClassification?: string,
58
58
  verbatimIdentification?: string,
59
+ dynamicProperties?: string,
59
60
  }
60
61
 
61
62
  interface WorkingTaxon extends TaxonBase {
62
63
  // Non-standard
63
64
  scientificNameOnly?: string,
64
- incorrect?: WorkingTaxon
65
+ incorrect?: WorkingTaxon,
66
+ cluster?: string
65
67
  }
66
68
 
67
69
  interface Taxon extends TaxonBase {
@@ -42,7 +42,7 @@ export const RANKS: Rank[] = [
42
42
 
43
43
  const TAXONOMIC_STATUS: Record<string, TaxonStatus> = {
44
44
  '>': 'incorrect',
45
- '+': 'heterotypic synonym',
45
+ '+': 'proparte synonym',
46
46
  '=': 'synonym'
47
47
  }
48
48
 
@@ -123,6 +123,8 @@ const SUBGENUS_PATTERN = /^([A-Z]\S+) (?:\(([A-Z]\S+?)\))(?= |$)/
123
123
  */
124
124
  const BINAME_PATTERN = /^(?:((?:x )?[A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?(x [a-z-]+|[a-z-][^\s.]+(?: x [a-z-]+)?|[A-Z][a-z]+_[a-z-]+ x [A-Z][a-z]+_[a-z-]+)(?= |$)/
125
125
 
126
+ const CLUSTER_PATTERN = /^\[(_|\d+)\] /
127
+
126
128
  function compareRanks (a: Rank, b: Rank): number {
127
129
  return RANKS.indexOf(a) - RANKS.indexOf(b)
128
130
  }
@@ -173,15 +175,21 @@ export function parseName (name: string, rank: Rank, parent: WorkingTaxon): Work
173
175
  const isSynonym = /^[+=>] /.test(name)
174
176
  if (isSynonym) {
175
177
  item.taxonomicStatus = TAXONOMIC_STATUS[name[0]]
176
- name = name.replace(/^[+=>] (\? ?)?/, '')
178
+ name = name.slice(2)
179
+ if (/^\? ?/.test(name)) {
180
+ item.dynamicProperties = '{"synonymUncertain":true}'
181
+ name = name.replace(/^\? ?/, '')
182
+ }
177
183
  rank = getSynonymRank(name, parent.taxonRank as Rank)
178
184
  } else {
179
185
  item.taxonomicStatus = 'accepted'
180
186
  }
181
187
 
182
188
  // Clusters
183
- if (/^\[(_|\d+)\] /.test(name)) {
184
- name = name.replace(/^\[(_|\d+)\] /, '')
189
+ if (CLUSTER_PATTERN.test(name)) {
190
+ const [match, cluster] = name.match(CLUSTER_PATTERN) as string[]
191
+ item.cluster = cluster
192
+ name = name.slice(match.length)
185
193
  }
186
194
 
187
195
  // Set verbatim identification after subsequent syntax is removed.
@@ -376,5 +384,9 @@ export function parseName (name: string, rank: Rank, parent: WorkingTaxon): Work
376
384
  item.scientificName += ` ${item.scientificNameAuthorship}`
377
385
  }
378
386
 
387
+ if (isSynonym && /^auctt?\./.test(item.taxonRemarks)) {
388
+ item.taxonomicStatus = 'misapplied'
389
+ }
390
+
379
391
  return item
380
392
  }
@@ -134,6 +134,36 @@ function parseResource (resource: FilePart): [ResourceMetadata, FilePart] {
134
134
  return [config, { content, offsetLine }]
135
135
  }
136
136
 
137
+ function getTaxonChildren (parent: TaxonId|undefined, taxa: Record<TaxonId, WorkingTaxon>): WorkingTaxon[] {
138
+ const children = []
139
+ for (const id in taxa) {
140
+ if (taxa[id].parentNameUsageID === parent) {
141
+ children.push(taxa[id])
142
+ }
143
+ }
144
+ return children
145
+ }
146
+
147
+ function processClusters (taxa: Record<TaxonId, WorkingTaxon>) {
148
+ for (const id in taxa) {
149
+ const taxon = taxa[id]
150
+ if (taxon.taxonomicStatus !== 'accepted' || !taxon.cluster) {
151
+ continue
152
+ }
153
+
154
+ const dynamicProperties = taxon.dynamicProperties ? JSON.parse(taxon.dynamicProperties) : {}
155
+
156
+ if (taxon.cluster === '_') {
157
+ dynamicProperties.identifiable = false
158
+ } else {
159
+ const siblings = getTaxonChildren(taxon.parentNameUsageID, taxa).filter(sibling => sibling.scientificNameID !== id)
160
+ dynamicProperties.indistinguishableFrom = siblings.filter(sibling => sibling.cluster === taxon.cluster).map(sibling => sibling.scientificNameID)
161
+ }
162
+
163
+ taxon.dynamicProperties = JSON.stringify(dynamicProperties)
164
+ }
165
+ }
166
+
137
167
  function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds: number[], offsetLine: number): Resource {
138
168
  const leafTaxonIndex = resource.metadata.levels.reduce((last, rank, i) => MAIN_RANKS.includes(rank) ? i : last, 0)
139
169
  const data = resource.taxa as Record<TaxonId, WorkingTaxon>
@@ -307,6 +337,8 @@ function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds
307
337
  throw mergeParserErrors(errors)
308
338
  }
309
339
 
340
+ processClusters(data)
341
+
310
342
  return resource
311
343
  }
312
344
 
package/test/resources.js CHANGED
@@ -166,6 +166,27 @@ Polistes Latreille, 1802
166
166
  assert.strictEqual(resource.taxa['T1:1:2'].genericName, 'Polistes')
167
167
  })
168
168
 
169
+ test('handles clusters', () => {
170
+ const [resource] = resources.parseTextFile(`---
171
+ levels: [genus, species]
172
+ ---
173
+
174
+ Eurydema Laporte, 1833
175
+ oleracea (Linnaeus, 1758)
176
+ [1] rotundicollis (Dohrn, 1860)
177
+ [1] fieberi Schummel, 1837
178
+ [2] ornata (Linnaeus, 1758)
179
+ [2] ventralis Kolenati, 1846
180
+ [_] eckerleini Josifov, 1961
181
+ `, 'T1')
182
+ assert.strictEqual(resource.taxa['T1:1:2'].dynamicProperties, undefined)
183
+ assert.strictEqual(resource.taxa['T1:1:3'].dynamicProperties, '{"indistinguishableFrom":["T1:1:4"]}')
184
+ assert.strictEqual(resource.taxa['T1:1:4'].dynamicProperties, '{"indistinguishableFrom":["T1:1:3"]}')
185
+ assert.strictEqual(resource.taxa['T1:1:5'].dynamicProperties, '{"indistinguishableFrom":["T1:1:6"]}')
186
+ assert.strictEqual(resource.taxa['T1:1:6'].dynamicProperties, '{"indistinguishableFrom":["T1:1:5"]}')
187
+ assert.strictEqual(resource.taxa['T1:1:7'].dynamicProperties, '{"identifiable":false}')
188
+ })
189
+
169
190
  suite('leaf taxa checks', () => {
170
191
  test('errors for missing leaf taxa', () => {
171
192
  assert.throws(() => {