linked-rolls 0.36.0 → 0.38.0

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.
@@ -4,10 +4,10 @@ import { Edition } from "./Edition";
4
4
  import { AnySymbol, PlacementRelation } from "./Symbol";
5
5
  import { CollationTolerance } from "./Collation";
6
6
  import { Edit } from "./Edit";
7
- import { GeneralRollCondition, RollCopy, ScaleReading, Shift } from "./RollCopy";
7
+ import { GeneralRollCondition, ModificationPurpose, RollCopy, ScaleReading, Shift } from "./RollCopy";
8
8
  import { FeatureSource } from "./FeatureSource";
9
9
  import { AnyArgumentation, Belief, Certainty, ObjectAssumption } from "./Assumption";
10
- import { AnyFeature, FeatureConditionAssignment } from "./Feature";
10
+ import { FeatureConditionAssignment, FeatureOrPatch, NestedFeature } from "./Feature";
11
11
  /**
12
12
  * A change to an edition, written onto an immer draft of it. One
13
13
  * operation is one undo step, so an operation that has to read the
@@ -70,11 +70,40 @@ export declare const addGeneralCondition: (copyId: string, condition: ObjectAssu
70
70
  export declare const stateFeatureCondition: (copyId: string, featureId: string, condition: FeatureConditionAssignment) => EditionOp;
71
71
  /** The symbols of the versions that no other copy carries. */
72
72
  export declare const symbolsCarriedOnlyBy: (edition: Edition, copyId: string) => AnySymbol[];
73
+ /**
74
+ * The act a feature an editor states belongs to: the punching of the
75
+ * copy, the act that brought another feature about, or, where neither
76
+ * is said, an act of its own for the purpose given. A patch is never
77
+ * punched, being glued on, so `punched` says nothing of one.
78
+ */
79
+ export interface FeatureAct {
80
+ /** The copy was punched with it, which is where a reading of a scan puts every hole. */
81
+ punched?: boolean;
82
+ /** The act that brought this feature about brought the new one about as well. */
83
+ beside?: string;
84
+ /** What the act was for, where a new one is made for the feature. */
85
+ purpose?: ModificationPurpose;
86
+ }
87
+ /**
88
+ * States that the copy bears the feature, in the act that brought it
89
+ * about: a patch was glued on by an attachment, anything else was
90
+ * produced, by the punching or by a later act. Which act, `FeatureAct`
91
+ * says; a `beside` that names nothing the copy bears is passed over and
92
+ * the feature goes into an act of its own.
93
+ */
94
+ export declare const addFeature: (copyId: string, feature: FeatureOrPatch, act?: FeatureAct) => EditionOp;
95
+ /**
96
+ * States that the patch bears the feature. What a patch bears came onto
97
+ * the copy with the patch, so it belongs to no act of its own and
98
+ * states no place: it stands where the patch stands.
99
+ */
100
+ export declare const addBorneFeature: (copyId: string, patchId: string, feature: NestedFeature) => EditionOp;
73
101
  /**
74
102
  * Takes the features off the copy, and out of the versions with what
75
103
  * only they carried. A feature may be named wherever the copy bears it:
76
104
  * a patch goes with everything glued onto it, and a feature of a patch
77
- * may be taken back on its own, the patch staying where it is.
105
+ * may be taken back on its own, the patch staying where it is. An act
106
+ * left having produced or added nothing goes as well.
78
107
  */
79
108
  export declare const removeFeatures: (copyId: string, featureIds: readonly string[]) => EditionOp;
80
109
  /**
@@ -84,7 +113,7 @@ export declare const removeFeatures: (copyId: string, featureIds: readonly strin
84
113
  */
85
114
  export declare const removeCopy: (copyId: string) => EditionOp;
86
115
  /** Why several features cannot be replaced by one. */
87
- export type MergeObstacle = 'fewer-than-two' | 'different-types' | 'different-tracks' | 'differing-conditions' | 'unlike-features';
116
+ export type MergeObstacle = 'fewer-than-two' | 'different-types' | 'different-tracks' | 'differing-conditions' | 'unlike-features' | 'different-acts';
88
117
  /**
89
118
  * What stands in the way of reading the features as one, or nothing
90
119
  * where they may be merged. Features may be merged where they differ
@@ -93,7 +122,7 @@ export type MergeObstacle = 'fewer-than-two' | 'different-types' | 'different-tr
93
122
  * asked: whether a gap is a bridge of the perforator, a tear or two
94
123
  * perforations of their own is the editor's reading.
95
124
  */
96
- export declare const mergeObstacle: (features: readonly AnyFeature[]) => MergeObstacle | undefined;
125
+ export declare const mergeObstacle: (features: readonly FeatureOrPatch[]) => MergeObstacle | undefined;
97
126
  /**
98
127
  * Replaces the features of the copy with a single one covering them
99
128
  * all, where a scan has split what the editor reads as one feature.
@@ -106,7 +135,8 @@ export declare const mergeObstacle: (features: readonly AnyFeature[]) => MergeOb
106
135
  * Only the features the copy bears itself are merged. A feature of a
107
136
  * patch states no place along the roll of its own, so there is nothing
108
137
  * for a merge to span, and an id naming one is passed over as an id the
109
- * copy does not bear is.
138
+ * copy does not bear is. Features of two different acts are not merged
139
+ * either: one feature is the work of one act.
110
140
  *
111
141
  * Throws where the features cannot stand for one; `mergeObstacle`
112
142
  * says beforehand whether they can.
package/lib/editionOps.js CHANGED
@@ -4,7 +4,7 @@ import { getAt } from "./EditionView";
4
4
  import { isPerforation, placementRelations } from "./Symbol";
5
5
  import { collationsOf, defaultCollationTolerance } from "./Collation";
6
6
  import { collationToleranceOf, editsOf, insertedBy, principalDerivationOf } from "./Version";
7
- import { asSymbols, barOf, isPaperStretch } from "./RollCopy";
7
+ import { asSymbols, barOf, featuresByAct, featuresOf, isPaperStretch, statesNothing } from "./RollCopy";
8
8
  import { systemOf } from "./TrackerBar";
9
9
  import { substitutionsBetween } from "./substitution";
10
10
  import { trackerBarOf } from "./systems";
@@ -24,7 +24,7 @@ const onVersion = (versionId, op) => draft => {
24
24
  op(version, draft);
25
25
  };
26
26
  const onFeature = (copyId, featureId, op) => onCopy(copyId, copy => {
27
- const feature = copy.features.find(f => f.id === featureId);
27
+ const feature = featuresOf(copy).find(f => f.id === featureId);
28
28
  if (feature)
29
29
  op(feature);
30
30
  });
@@ -82,7 +82,7 @@ export const createVersion = (copy) => draft => {
82
82
  type: 'Version',
83
83
  id: v4(),
84
84
  system: systemOf(bar),
85
- edits: asSymbols(copy.features, bar).map(insertion),
85
+ edits: asSymbols(featuresOf(copy), bar).map(insertion),
86
86
  motivations: []
87
87
  });
88
88
  };
@@ -203,7 +203,7 @@ export const stateFeatureCondition = (copyId, featureId, condition) => onFeature
203
203
  feature.condition = condition;
204
204
  });
205
205
  /** The ids of every feature the copy bears, those a patch bears among them. */
206
- const featureIdsOf = (copy) => new Set(copy.features.flatMap(withBorneFeatures).map(feature => feature.id));
206
+ const featureIdsOf = (copy) => new Set(featuresOf(copy).flatMap(withBorneFeatures).map(feature => feature.id));
207
207
  /**
208
208
  * A symbol every carrier of which lies among the features loses its
209
209
  * evidence with them. A symbol without carriers, such as a label,
@@ -279,26 +279,27 @@ const writeInto = (draft, before, after) => Object.entries(after).forEach(([key,
279
279
  const isCopy = (record) => 'type' in record && record.type === 'RollCopy';
280
280
  const isBelief = (record) => 'type' in record && record.type === 'belief';
281
281
  const isComprehension = (reason) => reason.type === 'meaningComprehension';
282
- /** What the modification names, be it as added or as removed. */
283
- const membersOf = (modification) => modification.type === 'Addition' ? modification.added : modification.removed;
284
- const namesNothing = (modification) => membersOf(modification).length === 0;
285
282
  const comprehendsNothing = (reason) => isComprehension(reason) && reason.comprehends.length === 0;
286
- /** The modification with what it names rewritten, or the very same one where that leaves it as it was. */
287
- const renamingMembers = (rename) => (modification) => modification.type === 'Addition'
288
- ? replacing(modification, 'added', rename(modification.added))
289
- : replacing(modification, 'removed', rename(modification.removed));
283
+ /**
284
+ * The modification with what it names rewritten, or the very same one
285
+ * where that leaves it as it was. Only a removal names anything by id:
286
+ * what an act produced or glued on it states itself.
287
+ */
288
+ const renamingMembers = (rename) => (modification) => modification.type === 'Removal'
289
+ ? replacing(modification, 'removed', rename(modification.removed))
290
+ : modification;
290
291
  /** The reason with what a comprehension comprehends rewritten; any other reason names nothing of the kind. */
291
292
  const renamingComprehended = (rename) => (reason) => isComprehension(reason) ? replacing(reason, 'comprehends', rename(reason.comprehends)) : reason;
292
293
  /**
293
- * The record with the ids it names rewritten: what a copy's
294
- * modifications added or removed, and what the comprehensions among a
295
- * belief's reasons comprehend. A modification or a comprehension the
296
- * rewriting emptied goes with what it named; one that named nothing
297
- * before stays, as an edit that was empty before it does.
294
+ * The record with the ids it names rewritten: what a copy's removals
295
+ * removed, and what the comprehensions among a belief's reasons
296
+ * comprehend. A removal or a comprehension the rewriting emptied goes
297
+ * with what it named; one that named nothing before stays, as an edit
298
+ * that was empty before it does.
298
299
  */
299
300
  const renaming = (rename) => (record) => {
300
301
  if (isCopy(record)) {
301
- return replacing(record, 'modifications', pruned(record.modifications, renamingMembers(rename), namesNothing));
302
+ return replacing(record, 'modifications', pruned(record.modifications, renamingMembers(rename), statesNothing));
302
303
  }
303
304
  if (isBelief(record)) {
304
305
  return replacing(record, 'reasons', pruned(record.reasons, renamingComprehended(rename), comprehendsNothing));
@@ -356,16 +357,99 @@ const withoutBorne = (feature, named) => {
356
357
  };
357
358
  /** The features without those named, wherever they lie, and without whatever those bore. */
358
359
  const withoutFeatures = (features, named) => mapped(without(features, feature => named.has(feature.id)), feature => withoutBorne(feature, named));
360
+ /** The act with its features rewritten, or the very same one where the rewriting left them. */
361
+ const rewritingAct = (rewrite) => (modification) => {
362
+ if (modification.type === 'Alteration')
363
+ return replacing(modification, 'produced', rewrite(modification.produced));
364
+ if (modification.type === 'Attachment')
365
+ return replacing(modification, 'added', rewrite(modification.added));
366
+ return modification;
367
+ };
368
+ /**
369
+ * Writes the rewriting onto the features of every act of the copy, and
370
+ * takes out an act it left with nothing to state.
371
+ */
372
+ const rewriteFeatures = (copy, rewrite) => {
373
+ const state = stateOf(copy);
374
+ const produced = state.production?.produced;
375
+ if (produced && copy.production) {
376
+ const rewritten = rewrite(produced);
377
+ if (rewritten !== produced)
378
+ copy.production.produced = rewritten;
379
+ }
380
+ copy.modifications = pruned(state.modifications, rewritingAct(rewrite), statesNothing);
381
+ };
382
+ /** The features of the act that brought the named feature about, where that act produces features. */
383
+ const producedBeside = (copy, featureId) => {
384
+ const names = (features) => features.some(feature => feature.id === featureId);
385
+ const punched = copy.production?.produced;
386
+ if (punched && names(punched))
387
+ return punched;
388
+ const act = copy.modifications.find(act => act.type === 'Alteration' && names(act.produced));
389
+ return act?.type === 'Alteration' ? act.produced : undefined;
390
+ };
391
+ /** The patches of the attachment that glued the named one on. */
392
+ const gluedBeside = (copy, patchId) => {
393
+ const act = copy.modifications.find(act => act.type === 'Attachment' && act.added.some(patch => patch.id === patchId));
394
+ return act?.type === 'Attachment' ? act.added : undefined;
395
+ };
396
+ /** The list the copy states a new feature in, making the act it belongs to where there is none. */
397
+ const actFor = (copy, patch, act) => {
398
+ const beside = act.beside !== undefined
399
+ ? (patch ? gluedBeside(copy, act.beside) : producedBeside(copy, act.beside))
400
+ : undefined;
401
+ if (beside)
402
+ return beside;
403
+ if (patch) {
404
+ const added = [];
405
+ copy.modifications.push({ type: 'Attachment', added, ...(act.purpose && { purpose: act.purpose }) });
406
+ return added;
407
+ }
408
+ if (act.punched) {
409
+ if (!copy.production)
410
+ copy.production = {};
411
+ if (!copy.production.produced)
412
+ copy.production.produced = [];
413
+ return copy.production.produced;
414
+ }
415
+ const produced = [];
416
+ copy.modifications.push({ type: 'Alteration', produced, ...(act.purpose && { purpose: act.purpose }) });
417
+ return produced;
418
+ };
419
+ /**
420
+ * States that the copy bears the feature, in the act that brought it
421
+ * about: a patch was glued on by an attachment, anything else was
422
+ * produced, by the punching or by a later act. Which act, `FeatureAct`
423
+ * says; a `beside` that names nothing the copy bears is passed over and
424
+ * the feature goes into an act of its own.
425
+ */
426
+ export const addFeature = (copyId, feature, act = {}) => onCopy(copyId, copy => {
427
+ actFor(copy, isGluedOn(feature), act).push(feature);
428
+ });
429
+ /**
430
+ * States that the patch bears the feature. What a patch bears came onto
431
+ * the copy with the patch, so it belongs to no act of its own and
432
+ * states no place: it stands where the patch stands.
433
+ */
434
+ export const addBorneFeature = (copyId, patchId, feature) => onCopy(copyId, copy => {
435
+ const patch = featuresOf(copy).flatMap(withBorneFeatures).find(borne => borne.id === patchId);
436
+ if (!patch || !isGluedOn(patch))
437
+ return;
438
+ if (!patch.features)
439
+ patch.features = [];
440
+ patch.features.push(feature);
441
+ });
359
442
  /**
360
443
  * Takes the features off the copy, and out of the versions with what
361
444
  * only they carried. A feature may be named wherever the copy bears it:
362
445
  * a patch goes with everything glued onto it, and a feature of a patch
363
- * may be taken back on its own, the patch staying where it is.
446
+ * may be taken back on its own, the patch staying where it is. An act
447
+ * left having produced or added nothing goes as well.
364
448
  */
365
449
  export const removeFeatures = (copyId, featureIds) => onCopy(copyId, (copy, draft) => {
366
450
  const named = new Set(featureIds);
367
- const features = stateOf(copy.features);
368
- copy.features = withoutFeatures(features, named);
451
+ const features = featuresOf(stateOf(copy));
452
+ rewriteFeatures(copy, (stated) => withoutFeatures(stated, named));
369
453
  forgetFeatures(draft, new Set(goneWith(features, named)));
370
454
  });
371
455
  /**
@@ -445,9 +529,9 @@ const spanning = (features) => ({
445
529
  * others being alike in all but their place. The depiction goes: a
446
530
  * region of the scan showing one part depicts no more than that part.
447
531
  */
448
- const mergedFrom = (features) => {
532
+ const mergedFrom = (features, id) => {
449
533
  const stating = features.find(feature => feature.condition) ?? features[0];
450
- const merged = { ...stating, id: v4(), horizontal: spanning(features) };
534
+ const merged = { ...stating, id, horizontal: spanning(features) };
451
535
  delete merged.depiction;
452
536
  return merged;
453
537
  };
@@ -485,24 +569,29 @@ const carryOver = (draft, replaced, mergedId) => {
485
569
  * Only the features the copy bears itself are merged. A feature of a
486
570
  * patch states no place along the roll of its own, so there is nothing
487
571
  * for a merge to span, and an id naming one is passed over as an id the
488
- * copy does not bear is.
572
+ * copy does not bear is. Features of two different acts are not merged
573
+ * either: one feature is the work of one act.
489
574
  *
490
575
  * Throws where the features cannot stand for one; `mergeObstacle`
491
576
  * says beforehand whether they can.
492
577
  */
493
578
  export const mergeFeatures = (copyId, featureIds) => onCopy(copyId, (copy, draft) => {
494
579
  const named = new Set(featureIds);
495
- const features = stateOf(copy.features);
496
- const toMerge = features.filter(feature => named.has(feature.id));
497
- const obstacle = mergeObstacle(toMerge);
580
+ const holds = (features) => features.some(feature => named.has(feature.id));
581
+ const acts = featuresByAct(stateOf(copy)).filter(holds);
582
+ const toMerge = (acts[0] ?? []).filter(feature => named.has(feature.id));
583
+ const obstacle = acts.length > 1 ? 'different-acts' : mergeObstacle(toMerge);
498
584
  if (obstacle) {
499
585
  throw new Error(`The features of copy ${copyId} cannot be merged: ${obstacle}`);
500
586
  }
501
587
  const replaced = new Set(toMerge.map(feature => feature.id));
502
- const merged = mergedFrom(toMerge);
503
- copy.features = standingFor(features, feature => replaced.has(feature.id), merged);
504
- carryOver(draft, replaced, merged.id);
505
- renameReferences(draft, ids => standingFor(ids, id => replaced.has(id), merged.id));
588
+ const mergedId = v4();
589
+ rewriteFeatures(copy, (stated) => {
590
+ const stands = (feature) => replaced.has(feature.id);
591
+ return stated.some(stands) ? standingFor(stated, stands, mergedFrom(stated.filter(stands), mergedId)) : stated;
592
+ });
593
+ carryOver(draft, replaced, mergedId);
594
+ renameReferences(draft, ids => standingFor(ids, id => replaced.has(id), mergedId));
506
595
  });
507
596
  /** The carriers of each collated symbol pass to its counterpart. */
508
597
  const handOverCarriers = (view, draft, collations) => collations.forEach(({ symbol, counterpart }) => {
@@ -1,3 +1,4 @@
1
+ import { derivedKeys } from "./asJsonLd";
1
2
  import { migrate } from "./migrate";
2
3
  import { isDateString } from "./utils";
3
4
  export const importDate = (str) => {
@@ -17,6 +18,15 @@ const fromJsonLdValue = (value) => {
17
18
  return fromJsonLdEntity(value);
18
19
  return value;
19
20
  };
21
+ /**
22
+ * A node without what the export derived from it. A copy bears the
23
+ * features its acts brought about, and the graph has to be told so.
24
+ * In the edition the act that made a feature is the one place it
25
+ * stands.
26
+ */
27
+ const asStated = (json) => Object.keys(json).some(key => derivedKeys.has(key))
28
+ ? Object.fromEntries(Object.entries(json).filter(([key]) => !derivedKeys.has(key)))
29
+ : json;
20
30
  /**
21
31
  * An entity with its keywords read as plain keys. The input is left as
22
32
  * it is. The `@type` of a value object names the datatype of the value,
@@ -25,7 +35,7 @@ const fromJsonLdValue = (value) => {
25
35
  * rather than to the edition, which states the system as data.
26
36
  */
27
37
  const fromJsonLdEntity = (json) => {
28
- const { '@type': type, '@id': id, '@context': context, ...rest } = json;
38
+ const { '@type': type, '@id': id, '@context': context, ...rest } = asStated(json);
29
39
  const entity = Object.fromEntries(Object.entries(rest).map(([key, value]) => [key, fromJsonLdValue(value)]));
30
40
  if (type !== undefined && !('@value' in json))
31
41
  entity.type = type;
package/lib/migrate.js CHANGED
@@ -1,4 +1,4 @@
1
- import { conditions } from "./Feature";
1
+ import { conditions, isFeatureType } from "./Feature";
2
2
  import { rollConditions } from "./RollCopy";
3
3
  import { systemIdIn, systemOf, translationBetween } from "./TrackerBar";
4
4
  import { trackerBars } from "./systems";
@@ -12,6 +12,14 @@ const renamedKeys = {
12
12
  classification: 'editType'
13
13
  };
14
14
  const referenceKeys = ['alignedWith', 'pairedWith', 'basedOn'];
15
+ /**
16
+ * The five type terms that were capitalised while the rest of the
17
+ * vocabulary was not, under the keys that carry them.
18
+ */
19
+ const lowerCasedTerms = {
20
+ method: { Print: 'print', Handwriting: 'handwriting', Stamp: 'stamp' },
21
+ material: { Paper: 'paper', Tape: 'tape' }
22
+ };
15
23
  const named = (name) => ({ name, sameAs: [] });
16
24
  const withRenamedKeys = (node) => Object.keys(node).some(key => Object.hasOwn(renamedKeys, key))
17
25
  ? Object.fromEntries(Object.entries(node).map(([key, value]) => [renamedKeys[key] ?? key, value]))
@@ -32,6 +40,10 @@ const withoutVersionType = (node) => {
32
40
  const { versionType: _retired, ...rest } = node;
33
41
  return rest;
34
42
  };
43
+ const withLowerCaseTerms = (node) => Object.entries(lowerCasedTerms).reduce((result, [key, terms]) => {
44
+ const lowered = terms[result[key]];
45
+ return lowered ? { ...result, [key]: lowered } : result;
46
+ }, node);
35
47
  const withReferences = (node) => referenceKeys.reduce((result, key) => {
36
48
  const reference = result[key];
37
49
  if (reference && typeof reference === 'object' && '@value' in reference) {
@@ -79,15 +91,100 @@ const withScale = (node) => {
79
91
  /** A derivation written as a single one, before a version could name several. */
80
92
  const isSingleDerivation = (basedOn) => basedOn !== null && typeof basedOn === 'object' && !Array.isArray(basedOn);
81
93
  const withDerivationList = (node) => isSingleDerivation(node.basedOn) ? { ...node, basedOn: [node.basedOn] } : node;
94
+ /** The features a copy states at a place of its own, in either shape. */
95
+ const featuresOf = (copy) => [
96
+ ...(Array.isArray(copy.features) ? copy.features : []),
97
+ ...(Array.isArray(copy.production?.produced) ? copy.production.produced : [])
98
+ ];
82
99
  /**
83
100
  * A copy read on a roll reader was stated as a recording before a sound
84
101
  * recording could be one. A sound recording gives no holes, so a recording
85
102
  * with holes is a reading.
86
103
  */
87
- const withReadingKind = (node) => node.readFrom?.kind === 'recording' && Array.isArray(node.features)
88
- && node.features.some((feature) => feature?.['@type'] === 'Hole')
104
+ const withReadingKind = (node) => node.readFrom?.kind === 'recording'
105
+ && featuresOf(node).some((feature) => feature?.['@type'] === 'Hole')
89
106
  ? { ...node, readFrom: { ...node.readFrom, kind: 'reading' } }
90
107
  : node;
108
+ /** A feature written while the kind of a feature was a type beside its class. */
109
+ const withoutFeatureKind = (node) => {
110
+ if (!isFeatureType(node['@type']) || !Object.hasOwn(node, 'kind'))
111
+ return node;
112
+ const { kind: _typed, ...rest } = node;
113
+ return rest;
114
+ };
115
+ const isPatch = (feature) => feature?.['@type'] === 'GluedOn';
116
+ /**
117
+ * A feature a patch bears was sometimes written without an id, and what
118
+ * has no id can be referred to from nowhere: neither by a reading of it
119
+ * nor by the statement that the copy bears it. It is named after the
120
+ * patch and the place it stands in.
121
+ */
122
+ const withBorneFeaturesNamed = (node) => {
123
+ const bearer = node['@id'];
124
+ if (typeof bearer !== 'string' || !Array.isArray(node.features))
125
+ return node;
126
+ if (node.features.every((feature) => typeof feature?.['@id'] === 'string'))
127
+ return node;
128
+ return {
129
+ ...node,
130
+ features: node.features.map((feature, index) => feature && typeof feature === 'object' && typeof feature['@id'] !== 'string'
131
+ ? { '@id': `${bearer}-${index + 1}`, ...feature }
132
+ : feature)
133
+ };
134
+ };
135
+ /**
136
+ * A copy held its features in one list and a modification named by id
137
+ * what it had added. Each feature now stands in the act that brought it
138
+ * about: a patch in an attachment, a feature an act names in an
139
+ * alteration, and everything else in the production of the copy, which
140
+ * is the punching. An addition naming both a patch and a feature
141
+ * becomes two acts, since gluing something on and drawing something are
142
+ * not one act.
143
+ *
144
+ * A feature a patch bears is not moved: it has no place of its own, and
145
+ * it came onto the copy with the patch.
146
+ */
147
+ const withFeaturesInActs = (node) => {
148
+ if (node['@type'] !== 'RollCopy' || !Array.isArray(node.features))
149
+ return node;
150
+ const { features, modifications, ...rest } = node;
151
+ const byId = new Map(features
152
+ .filter((feature) => typeof feature?.['@id'] === 'string')
153
+ .map((feature) => [feature['@id'], feature]));
154
+ const taken = new Set();
155
+ /** The features the modification named, each going to the first act that names it. */
156
+ const namedBy = (modification) => (Array.isArray(modification.added) ? modification.added : [])
157
+ .flatMap((id) => {
158
+ if (typeof id !== 'string' || taken.has(id) || !byId.has(id))
159
+ return [];
160
+ taken.add(id);
161
+ return [byId.get(id)];
162
+ });
163
+ const acts = (Array.isArray(modifications) ? modifications : []).flatMap((modification) => {
164
+ if (modification?.['@type'] !== 'Addition')
165
+ return [modification];
166
+ const { added: _named, ...act } = modification;
167
+ const held = namedBy(modification);
168
+ const glued = held.filter(isPatch);
169
+ const made = held.filter(feature => !isPatch(feature));
170
+ if (glued.length === 0)
171
+ return [{ ...act, '@type': 'Alteration', produced: made }];
172
+ return [
173
+ ...(made.length > 0 ? [{ ...act, '@type': 'Alteration', produced: made }] : []),
174
+ { ...act, '@type': 'Attachment', added: glued }
175
+ ];
176
+ });
177
+ const left = features.filter((feature) => !taken.has(feature?.['@id']));
178
+ const punched = left.filter((feature) => !isPatch(feature));
179
+ const loose = left.filter(isPatch);
180
+ return {
181
+ ...rest,
182
+ ...((rest.production || punched.length > 0) && {
183
+ production: { ...rest.production, ...(punched.length > 0 && { produced: punched }) }
184
+ }),
185
+ modifications: [...acts, ...(loose.length > 0 ? [{ '@type': 'Attachment', added: loose }] : [])]
186
+ };
187
+ };
91
188
  /** A keeper nobody could name was written as an empty one before a copy could leave it out. */
92
189
  const withoutEmptyKeeper = (node) => {
93
190
  if (node.keeper?.name !== '' || node.keeper.sameAs?.length)
@@ -106,8 +203,9 @@ const withTimeSpanDates = (node) => {
106
203
  const { '@value': within, '@type': _typed, ...rest } = node;
107
204
  return { ...rest, within };
108
205
  };
109
- const migrateNode = (node) => [withRenamedKeys, withTypology, withoutVersionType, withReferences, withKeeper, withoutEmptyKeeper, withProductionNodes, withScale,
110
- withDerivationList, withReadingKind, withTimeSpanDates]
206
+ const migrateNode = (node) => [withRenamedKeys, withTypology, withoutVersionType, withLowerCaseTerms, withReferences, withKeeper, withoutEmptyKeeper,
207
+ withProductionNodes, withScale, withDerivationList, withReadingKind, withTimeSpanDates, withoutFeatureKind,
208
+ withBorneFeaturesNamed, withFeaturesInActs]
111
209
  .reduce((result, step) => step(result), node);
112
210
  /** The items each walked, or the very same list where the walk changed none. */
113
211
  const walked = (items) => {
@@ -148,9 +148,8 @@ export function readFromPhillipsEroll(buffer, { system = welteT100, controlOffse
148
148
  ops: [],
149
149
  conditions: [],
150
150
  measurements: {},
151
- production: { system: systemOf(system) },
151
+ production: { system: systemOf(system), produced: features },
152
152
  modifications: [],
153
- features,
154
153
  readFrom: {
155
154
  kind: 'reading',
156
155
  actor: assignObject({ name: 'Phillips, Peter', sameAs: [] }),
@@ -114,9 +114,8 @@ export function readFromSpencerBar(buffer, { rowsPerInch = SPENCER_ROWS_PER_INCH
114
114
  ops: [],
115
115
  conditions: [],
116
116
  measurements: {},
117
- production: { system: systemOf(system) },
118
- modifications: [],
119
- features
117
+ production: { system: systemOf(system), produced: features },
118
+ modifications: []
120
119
  };
121
120
  }
122
121
  /**
@@ -157,7 +157,7 @@ export function readFromStanfordAton(atonString, { trackShift, system = welteT10
157
157
  id: v4(),
158
158
  ops: [],
159
159
  conditions: [],
160
- production: { system: systemOf(system) },
160
+ production: { system: systemOf(system), produced: features },
161
161
  modifications: [],
162
162
  ...((scan ?? stanford) && { scan: scan ?? stanford?.scan }),
163
163
  measurements: {
@@ -183,7 +183,6 @@ export function readFromStanfordAton(atonString, { trackShift, system = welteT10
183
183
  }),
184
184
  trackCalibration: calibration,
185
185
  ...(measuredBy && { measuredBy })
186
- },
187
- features
186
+ }
188
187
  };
189
188
  }
@@ -1,5 +1,5 @@
1
1
  import { bearsPhysicalEvidence, isMeasured, sourceLabels } from "./FeatureSource";
2
- import { calibrationOf } from "./RollCopy";
2
+ import { calibrationOf, featuresOf } from "./RollCopy";
3
3
  import { trackerBarOf } from "./systems";
4
4
  import { witnessesOf } from "./witnesses";
5
5
  export const reservationTypes = [
@@ -81,7 +81,7 @@ const keeperKnown = copy => copy.keeper ? undefined : {
81
81
  note: 'Nobody is known to hold the copy, so its paper cannot be consulted.'
82
82
  };
83
83
  /** A check on what a copy's features leave open, which has nothing to say of a copy without any. */
84
- const onFeatures = (check) => copy => copy.features.length > 0 ? check(copy) : undefined;
84
+ const onFeatures = (check) => copy => featuresOf(copy).length > 0 ? check(copy) : undefined;
85
85
  const checks = [
86
86
  sourceStated,
87
87
  sourceDocumented,