linked-rolls 0.42.0 → 0.44.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.
package/README.md CHANGED
@@ -75,6 +75,83 @@ at. It then reads as the version it derives from, and
75
75
  `reservationsAboutVersion` says so. A file that names a single
76
76
  derivation loads as a list of one.
77
77
 
78
+ What a version is called is not stored with it. `siglaOf` reads the
79
+ sigla off the stemma as it stands, so a label never outlives the
80
+ arrangement it describes, and nothing should cite one without saying
81
+ which state of the edition it belongs to.
82
+
83
+ ## The tolerance a derivation was collated at
84
+
85
+ `collationTolerance` on a derivation is the window two readings of one
86
+ symbol must fall in to be collated: how far apart they may lie at
87
+ either end, and, since two copies differ by a systematic offset as well
88
+ as by scatter, where that window is centred. A window naming no offset
89
+ is centred on nothing, which is what every window written before the
90
+ offset was held here means.
91
+
92
+ The number decides what counts as a reading at all, so it is worth
93
+ measuring rather than picking. `scatterOfCopy` measures how far one
94
+ copy puts each symbol from where the copies collated with it put it,
95
+ and `scatterOf` describes that sample: its median, its scatter as a
96
+ median absolute deviation scaled to a standard deviation, a histogram
97
+ with the normal curve to lay over it, and the readings the curve does
98
+ not account for. The scatter is taken from the median because the
99
+ displacements an editor is looking for are in the sample, and a
100
+ standard deviation would grow towards them until they no longer stood
101
+ out.
102
+
103
+ The tolerance that follows is the scatter times a threshold fixed by
104
+ the size of the sample, the point beyond which fewer than one reading
105
+ is expected to fall by chance (`departureThreshold`). For the few
106
+ hundred readings an edge of a stemma yields this lands a little above
107
+ three. `toleranceAcross` covers several samples with one window, since
108
+ a derivation states one tolerance while notes and expressions scatter
109
+ differently, and `inferredTolerance` builds the belief the number rests
110
+ on, to annotate `collationTolerance` with. It is held likely and not
111
+ true: the tolerance follows from the sample only as far as the sample
112
+ is normal, which is what the reported excess kurtosis and tail counts
113
+ are there to show.
114
+
115
+ Two limits are worth naming. Notes and expressions are estimated apart
116
+ by default, which is a stopgap standing in for a skew across the width
117
+ of the paper rather than a distinction in the model, so the grouping is
118
+ a parameter of the estimator and appears nowhere in the format. And the
119
+ method screens: it says which readings the curve does not account for,
120
+ and an editor says which of those are editorial acts.
121
+
122
+ A separation reports which end made it (`separatedBy`, and
123
+ `admittedAtEnds` for a collation). The two ends answer different
124
+ questions: the onset, with the kind, decides whether two copies read one
125
+ command, and the end decides whether that command was lengthened or
126
+ shortened. On welte225.org the end alone decides 24 collations of 2839
127
+ and the onset alone 39, so neither test is idle, but a difference at the
128
+ end is as often a punch measured badly as a punch genuinely prolonged,
129
+ and the two are not worth confusing.
130
+
131
+ ## Collating a derivation a second time
132
+
133
+ A collated symbol is one symbol carrying every copy that reads it, so a
134
+ collation cannot simply be run again: there is no second symbol left to
135
+ match. `separateReadings` is the inverse. It takes one copy's carriers
136
+ back off the symbols it shares, gives them to new symbols of the
137
+ version's own, and states the exchange, leaving the edition in the state
138
+ a collation would have produced had nothing joined. `connectVersions` at
139
+ a new tolerance then re-collates, joining again whatever the new window
140
+ admits.
141
+
142
+ Nothing an editor established is lost by that. `connectVersions`
143
+ rewrites only what a collation wrote (`isCollationsOwn`): a bare
144
+ insertion or deletion, or an equivalence between two systems' spellings.
145
+ An edit naming what the change is, why it was made, or what it rests on
146
+ stays, and the symbols it speaks for are left out of the collation. An
147
+ equivalence counts as the collation's own, since it is derived from the
148
+ two systems' vocabularies rather than read off the paper and freezing it
149
+ would leave the transfers uncollatable, but one drawn a second time over
150
+ the very same symbols is kept as it stands, identifier and motivation
151
+ and all. Readings already separated by hand are carried by one copy
152
+ alone, so `separateReadings` passes over them and they keep their
153
+ identifiers.
154
+
78
155
  ## What brought a feature about
79
156
 
80
157
  A copy states no list of features. Each feature stands in the act that
@@ -140,8 +217,8 @@ they carry it, a statement with the belief it rests on;
140
217
  `versionsWitnessedBy` gathers the same from the side of a copy.
141
218
  `carriageProblems` reports a statement made beside features that carry
142
219
  symbols already, or one naming a version the edition lacks. A copy may
143
- carry a `siglum`, as a version does; one without is named by its
144
- keeper.
220
+ carry a `siglum`, which is given by hand and not read off anything;
221
+ one without is named by its keeper.
145
222
 
146
223
  ```ts
147
224
  import { reservationsAbout, stateSource } from 'linked-rolls'
package/lib/Agent.d.ts CHANGED
@@ -67,10 +67,15 @@ export type WithActor = {
67
67
  export interface Place extends Named {
68
68
  }
69
69
  /**
70
- * A term from a vocabulary, such as a roll system or a kind of
71
- * paper. A term the type vocabulary knows carries its IRI as `id`.
70
+ * A term from a vocabulary, such as a roll system, a procedure or a
71
+ * kind of paper.
72
+ *
73
+ * A term the type vocabulary declares is named by its IRI, and what it
74
+ * is called stands in the vocabulary rather than in the edition, so it
75
+ * needs no name of its own; `nameOf` reads one either way. A term the
76
+ * vocabulary does not have is given by name, which is then all there is
77
+ * to go on.
72
78
  * @see crm:E55 Type
73
79
  */
74
- export interface Concept extends Named, Partial<WithId> {
75
- }
80
+ export type Concept = (WithId & Partial<Named>) | (Named & Partial<WithId>);
76
81
  export {};
@@ -1,17 +1,69 @@
1
+ import { Edit } from "./Edit";
1
2
  import { HorizontalSpan } from "./Feature";
2
3
  import { AnySymbol } from "./Symbol";
3
4
  import { Millimeters } from "./Quantity";
5
+ /** A value taken at each end of a feature: where it begins and where it stops. */
6
+ export interface BothEnds<T> {
7
+ from: T;
8
+ to: T;
9
+ }
10
+ /** How far one text puts a feature from where another puts it, at either end. */
11
+ export type Displacement = BothEnds<Millimeters>;
4
12
  /**
5
- * Tolerance used in collation of roll copies: the acceptable deviation
6
- * at either end when aligning features across copies.
13
+ * The window two readings of one feature must fall in to be collated:
14
+ * how far apart they may lie at either end, and where that window is
15
+ * centred.
16
+ *
17
+ * The centre is worth stating. Two copies of one roll differ by a
18
+ * systematic offset as well as by scatter, and a window centred on
19
+ * nothing has to be widened by the whole of that offset before it
20
+ * admits what the offset alone displaces. Naming the offset lets the
21
+ * tolerance stand for the scatter only. A window that names none is
22
+ * centred on nothing, which is what every window written before the
23
+ * offset was held here means.
7
24
  */
8
25
  export interface CollationTolerance {
9
- /** Tolerance at the start position of a feature. */
26
+ /** How far the two readings may lie apart at the start of a feature, measured from `offsetStart`. */
10
27
  toleranceStart: Millimeters;
11
- /** Tolerance at the end position of a feature. */
28
+ /** How far they may lie apart at the end, measured from `offsetEnd`. */
12
29
  toleranceEnd: Millimeters;
30
+ /**
31
+ * How much later the collated copy puts the start of a feature than
32
+ * the copy it is read against, where the two differ systematically.
33
+ * Nothing where they do not.
34
+ */
35
+ offsetStart?: Millimeters;
36
+ /** The same at the end of a feature. */
37
+ offsetEnd?: Millimeters;
13
38
  }
14
39
  export declare const defaultCollationTolerance: CollationTolerance;
40
+ /** Where the window is centred at the start of a feature, on nothing where it names no offset. */
41
+ export declare const offsetStartOf: (tolerance: CollationTolerance) => Millimeters;
42
+ /** Where the window is centred at the end of a feature. */
43
+ export declare const offsetEndOf: (tolerance: CollationTolerance) => Millimeters;
44
+ /**
45
+ * Whether the window admits the displacement at each end, taken apart.
46
+ *
47
+ * The two ends answer different questions. The onset, with the kind,
48
+ * decides identity: whether the two copies read one command. The end
49
+ * decides duration: whether the same command was lengthened or
50
+ * shortened. A collation joins two readings only where both admit, so a
51
+ * separation may be the work of either, and an editor looking at one
52
+ * wants to know which.
53
+ *
54
+ * A difference at the end alone has at least three causes and only the
55
+ * first is an editorial act. A punch may genuinely have been lengthened
56
+ * or shortened. A chain of punches, bridged to keep the paper strong,
57
+ * may have been read as one perforation on one copy and as repeated
58
+ * notes on the other, which Phillips names as a common error of roll
59
+ * scanning and which runs in both directions (p. 173). Or a reading may
60
+ * report something other than the punched slot, as a pneumatic reader
61
+ * reports how long a valve stayed open. Long held notes are where the
62
+ * second is likeliest, since that is where the chains are.
63
+ */
64
+ export declare const admittedAtEnds: (tolerance: CollationTolerance, displacement: Displacement) => BothEnds<boolean>;
65
+ /** Whether the window admits the displacement: within the tolerance of the offset, at both ends. */
66
+ export declare const admits: (tolerance: CollationTolerance, displacement: Displacement) => boolean;
15
67
  /** Where a symbol lies along the roll, as its carriers put it, or nothing for a symbol without a place. */
16
68
  export type Locate = (symbol: AnySymbol) => Readonly<HorizontalSpan> | undefined;
17
69
  /**
@@ -26,3 +78,21 @@ export type Collation = {
26
78
  };
27
79
  /** Each of the own symbols with every inherited symbol it collates with, both in the order given. */
28
80
  export declare const collationsOf: (own: readonly Readonly<AnySymbol>[], inherited: readonly Readonly<AnySymbol>[], locate: Locate, tolerance?: CollationTolerance) => Collation[];
81
+ /**
82
+ * Whether the edit is one a collation writes by itself: a bare
83
+ * insertion or a bare deletion saying nothing further, or an
84
+ * equivalence between two systems' spellings for one command.
85
+ *
86
+ * Collating again rewrites these and keeps the rest. An edit that says
87
+ * what the change is, or why it was made, or what it rests on, is an
88
+ * editor's reading of the difference between two texts, and collating
89
+ * the two again is no reason to discard it.
90
+ *
91
+ * An equivalence counts as the collation's own even where an editor
92
+ * added a motivation to it, because it is derived rather than read:
93
+ * freezing it would leave the transfers, where nearly every edit is
94
+ * one, unable to be collated again at all. What an editor wrote on it
95
+ * is not lost by that, since `connectVersions` keeps an equivalence it
96
+ * draws a second time over the very same symbols.
97
+ */
98
+ export declare const isCollationsOwn: (edit: Readonly<Edit>) => boolean;
package/lib/Collation.js CHANGED
@@ -1,7 +1,41 @@
1
1
  import { keyOf } from "./TrackerBar";
2
2
  import { distance, mm } from "./Quantity";
3
3
  import { partitionPoint } from "./sorted";
4
+ import { groupBy } from "./utils";
4
5
  export const defaultCollationTolerance = { toleranceStart: mm(5), toleranceEnd: mm(5) };
6
+ /** Where the window is centred at the start of a feature, on nothing where it names no offset. */
7
+ export const offsetStartOf = (tolerance) => tolerance.offsetStart ?? mm(0);
8
+ /** Where the window is centred at the end of a feature. */
9
+ export const offsetEndOf = (tolerance) => tolerance.offsetEnd ?? mm(0);
10
+ /**
11
+ * Whether the window admits the displacement at each end, taken apart.
12
+ *
13
+ * The two ends answer different questions. The onset, with the kind,
14
+ * decides identity: whether the two copies read one command. The end
15
+ * decides duration: whether the same command was lengthened or
16
+ * shortened. A collation joins two readings only where both admit, so a
17
+ * separation may be the work of either, and an editor looking at one
18
+ * wants to know which.
19
+ *
20
+ * A difference at the end alone has at least three causes and only the
21
+ * first is an editorial act. A punch may genuinely have been lengthened
22
+ * or shortened. A chain of punches, bridged to keep the paper strong,
23
+ * may have been read as one perforation on one copy and as repeated
24
+ * notes on the other, which Phillips names as a common error of roll
25
+ * scanning and which runs in both directions (p. 173). Or a reading may
26
+ * report something other than the punched slot, as a pneumatic reader
27
+ * reports how long a valve stayed open. Long held notes are where the
28
+ * second is likeliest, since that is where the chains are.
29
+ */
30
+ export const admittedAtEnds = (tolerance, displacement) => ({
31
+ from: distance(displacement.from, offsetStartOf(tolerance)) <= tolerance.toleranceStart,
32
+ to: distance(displacement.to, offsetEndOf(tolerance)) <= tolerance.toleranceEnd
33
+ });
34
+ /** Whether the window admits the displacement: within the tolerance of the offset, at both ends. */
35
+ export const admits = (tolerance, displacement) => {
36
+ const ends = admittedAtEnds(tolerance, displacement);
37
+ return ends.from && ends.to;
38
+ };
5
39
  /**
6
40
  * What a symbol says, as a key: the pitch of a note, the type and scope
7
41
  * of an expression. Symbols collate within one key only. This is the
@@ -10,8 +44,8 @@ export const defaultCollationTolerance = { toleranceStart: mm(5), toleranceEnd:
10
44
  * what carries a note across a transfer between systems.
11
45
  */
12
46
  const kindOf = (symbol) => symbol.type === 'text' ? 'text' : keyOf(symbol);
13
- const nearby = (here, there, tolerance) => distance(here.from, there.from) <= tolerance.toleranceStart
14
- && distance(here.to, there.to) <= tolerance.toleranceEnd;
47
+ const displacementBetween = (here, there) => ({ from: mm(here.from - there.from), to: mm(here.to - there.to) });
48
+ const nearby = (here, there, tolerance) => admits(tolerance, displacementBetween(here, there));
15
49
  /**
16
50
  * Two symbols collate when they are of one kind, say the same thing
17
51
  * (pitch, or expression type and scope), and lie at about the same
@@ -28,15 +62,6 @@ const placed = (symbols, locate) => symbols.flatMap((symbol, index) => {
28
62
  const horizontal = locate(symbol);
29
63
  return horizontal ? [{ symbol, index, horizontal }] : [];
30
64
  });
31
- const groupBy = (items, keyOf) => items.reduce((groups, item) => {
32
- const key = keyOf(item);
33
- const group = groups.get(key);
34
- if (group)
35
- group.push(item);
36
- else
37
- groups.set(key, [item]);
38
- return groups;
39
- }, new Map());
40
65
  /** The placed symbols by kind, each kind in order of onset. */
41
66
  const byKindInOrderOfOnset = (symbols) => {
42
67
  const groups = groupBy(symbols, ({ symbol }) => kindOf(symbol));
@@ -47,8 +72,9 @@ const byKindInOrderOfOnset = (symbols) => {
47
72
  const WINDOW_SLACK = 1e-9;
48
73
  /** The symbols of a kind whose onset lies within the start tolerance of the span. */
49
74
  const nearOnsetOf = (kind, span, tolerance) => {
50
- const lowest = span.from - tolerance.toleranceStart - WINDOW_SLACK;
51
- const highest = span.from + tolerance.toleranceStart + WINDOW_SLACK;
75
+ const centre = span.from - offsetStartOf(tolerance);
76
+ const lowest = centre - tolerance.toleranceStart - WINDOW_SLACK;
77
+ const highest = centre + tolerance.toleranceStart + WINDOW_SLACK;
52
78
  const first = partitionPoint(kind, candidate => candidate.horizontal.from < lowest);
53
79
  const end = partitionPoint(kind, candidate => candidate.horizontal.from <= highest);
54
80
  return kind.slice(first, end);
@@ -61,3 +87,24 @@ export const collationsOf = (own, inherited, locate, tolerance = defaultCollatio
61
87
  .sort((a, b) => a.index - b.index)
62
88
  .map(({ symbol: counterpart }) => ({ symbol, counterpart })));
63
89
  };
90
+ /** The edit type a collation draws by itself, from the two systems' vocabularies rather than off the paper. */
91
+ const drawnByCollation = 'replace-with-equivalent';
92
+ /**
93
+ * Whether the edit is one a collation writes by itself: a bare
94
+ * insertion or a bare deletion saying nothing further, or an
95
+ * equivalence between two systems' spellings for one command.
96
+ *
97
+ * Collating again rewrites these and keeps the rest. An edit that says
98
+ * what the change is, or why it was made, or what it rests on, is an
99
+ * editor's reading of the difference between two texts, and collating
100
+ * the two again is no reason to discard it.
101
+ *
102
+ * An equivalence counts as the collation's own even where an editor
103
+ * added a motivation to it, because it is derived rather than read:
104
+ * freezing it would leave the transfers, where nearly every edit is
105
+ * one, unable to be collated again at all. What an editor wrote on it
106
+ * is not lost by that, since `connectVersions` keeps an equivalence it
107
+ * draws a second time over the very same symbols.
108
+ */
109
+ export const isCollationsOwn = (edit) => edit.editType === drawnByCollation
110
+ || (edit.editType === undefined && edit.motivation === undefined && edit['@annotation'] === undefined);
package/lib/Edition.d.ts CHANGED
@@ -29,9 +29,15 @@ export interface EditionCreation {
29
29
  */
30
30
  publicationDate: Date;
31
31
  /**
32
- * The tolerance parameters used when collating (aligning)
33
- * the different roll copies for this edition.
32
+ * The tolerance every version of the edition was collated at,
33
+ * before each derivation stated its own. How precisely two copies
34
+ * place a symbol depends on which two they are, so the tolerance
35
+ * belongs to the derivation, `Derivation.collationTolerance`, and
36
+ * migration writes this one onto every derivation that gives none.
37
+ * Nothing else reads it. A window's offset is the displacement
38
+ * between two particular copies and so can never be stated here.
34
39
  * Not exported to RDF.
40
+ * @deprecated
35
41
  */
36
42
  collationTolerance?: CollationTolerance;
37
43
  }
package/lib/Version.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Edit } from "./Edit";
2
2
  import { Concept } from "./Agent";
3
- import { ActorAssignment, Belief, Certainty, DateAssignment, ReferenceAssumption } from "./Assumption";
3
+ import { ActorAssignment, Belief, Certainty, DateAssignment, ObjectAssumption, ReferenceAssumption } from "./Assumption";
4
4
  import { CollationTolerance } from "./Collation";
5
5
  import { AnySymbol } from "./Symbol";
6
6
  import { WithId, WithNote, WithType } from "./utils";
@@ -22,8 +22,14 @@ export type Derivation = ReferenceAssumption & {
22
22
  * The tolerance at which the derived version was collated against
23
23
  * the one it is based on. A derivation written before the tolerance
24
24
  * was held here states none. Not exported to RDF.
25
+ *
26
+ * It is an object assumption, so that a tolerance calculated from
27
+ * the scatter of the readings can carry the inference it was drawn
28
+ * by. It is the one quantity in the edition that decides what
29
+ * counts as a reading at all, and a number arrived at by a method
30
+ * should say so.
25
31
  */
26
- collationTolerance?: CollationTolerance;
32
+ collationTolerance?: ObjectAssumption<CollationTolerance>;
27
33
  };
28
34
  /** The tolerance the derivation was collated at, or the default where it states none. */
29
35
  export declare const collationToleranceOf: (derivation: Readonly<Derivation>) => CollationTolerance;
@@ -66,14 +72,6 @@ export interface VersionCreation {
66
72
  * @see lrmoo:F2 Expression
67
73
  */
68
74
  export interface Version extends WithId, WithType<'Version'> {
69
- /**
70
- * A label a document carries from before the sigla were computed.
71
- * Nothing should write one here and nothing should show one: what a
72
- * version is called comes from `siglaOf`.
73
- * @deprecated
74
- * @see reo:siglum
75
- */
76
- siglum?: string;
77
75
  /**
78
76
  * The reproducing system this version is coded for. One roll was
79
77
  * often issued for several of them, and a version is a reading in
@@ -157,14 +157,46 @@ export declare const mergeFeatures: (copyId: string, featureIds: readonly string
157
157
  * and the child lacks becomes its deletions. The derivation states the
158
158
  * tolerance it was collated at and becomes the principal one; the
159
159
  * hypotheses the child stated beside its former one stay.
160
+ *
161
+ * The edits the child already stated are rewritten only as far as a
162
+ * collation wrote them (`isCollationsOwn`). An editor's reading of the
163
+ * difference between the two texts stays, and the symbols it speaks for
164
+ * are left out of the collation, so that connecting the two again
165
+ * neither doubles them nor silently drops what somebody established by
166
+ * hand. An equivalence is the collation's own, since it is drawn from
167
+ * the two systems' vocabularies rather than read off the paper, but one
168
+ * drawn again over the very same symbols is kept as it stands, with its
169
+ * identifier and whatever was written on it.
170
+ *
171
+ * The child's own text is what it shows less what the parent hands
172
+ * down, so that connecting two versions already connected collates the
173
+ * child's symbols and not the parent's with themselves.
160
174
  */
161
- export declare const connectVersions: (view: EditionView, childId: string, parentId: string, tolerance?: CollationTolerance) => EditionOp;
175
+ export declare const connectVersions: (view: EditionView, childId: string, parentId: string, tolerance?: ObjectAssumption<CollationTolerance>) => EditionOp;
162
176
  /**
163
177
  * Folds the version's own symbols into those it inherits and collates
164
178
  * with: the carriers pass over, and the insertions go. Collates at the
165
179
  * tolerance of the derivation, where the caller names none.
166
180
  */
167
181
  export declare const collateSymbols: (view: EditionView, versionId: string, symbolIds: readonly string[], tolerance?: CollationTolerance) => EditionOp;
182
+ /**
183
+ * Takes the copy's reading of a symbol back out of the symbol it was
184
+ * collated into: the copy's carriers pass to a new symbol of the
185
+ * version's own, the other copies keep the symbol they had, and the
186
+ * version states the exchange.
187
+ *
188
+ * This is the inverse of the hand-over a collation makes, and it is
189
+ * what lets a derivation be collated a second time. A collated symbol
190
+ * is one symbol carrying every copy that reads it, so nothing else
191
+ * takes the two readings apart again, and without that a tolerance
192
+ * arrived at after the fact could never be applied.
193
+ *
194
+ * Symbols the copy alone carries are already the version's own and are
195
+ * passed over, so a reading somebody has separated by hand keeps its
196
+ * identifier and the edit that speaks for it. Named symbols narrow the
197
+ * act to those; naming none separates the copy's whole reading.
198
+ */
199
+ export declare const separateReadings: (view: EditionView, versionId: string, copyId: string, symbolIds?: readonly string[]) => EditionOp;
168
200
  /**
169
201
  * Makes the version stand on its own: what it inherited becomes its
170
202
  * own insertions, and its derivations go, the hypotheses among them,
package/lib/editionOps.js CHANGED
@@ -2,7 +2,7 @@ import { current, isDraft } from "immer";
2
2
  import { v4 } from "uuid";
3
3
  import { getAt } from "./EditionView";
4
4
  import { isCommand, placementRelations } from "./Symbol";
5
- import { collationsOf, defaultCollationTolerance } from "./Collation";
5
+ import { collationsOf, defaultCollationTolerance, isCollationsOwn } from "./Collation";
6
6
  import { collationToleranceOf, editsOf, insertedBy, principalDerivationOf } from "./Version";
7
7
  import { asSymbols, barOf, featuresByAct, featuresOf, isPaperStretch, statesNothing } from "./RollCopy";
8
8
  import { systemOf } from "./TrackerBar";
@@ -651,6 +651,20 @@ const dropDerivations = (version, matches) => {
651
651
  else
652
652
  delete version.basedOn;
653
653
  };
654
+ /** The symbols a set of edits speaks for, whether by inserting or by deleting them. */
655
+ const spokenForBy = (edits) => new Set(edits.flatMap(edit => [
656
+ ...(edit.insert ?? []).map(symbol => symbol.id),
657
+ ...(edit.delete ?? [])
658
+ ]));
659
+ /** An exchange of symbols, as a key: what it puts in against what it takes out. */
660
+ const exchangeKey = (inserted, deleted) => `${[...inserted].sort().join(',')}/${[...deleted].sort().join(',')}`;
661
+ /**
662
+ * The edits already stated, by the exchange each of them makes. An
663
+ * equivalence the collation draws a second time is the one already
664
+ * stated, and drawing it again is no reason to mint a new identifier or
665
+ * to drop the motivation somebody wrote on it.
666
+ */
667
+ const byExchange = (edits) => new Map(edits.map(edit => [exchangeKey((edit.insert ?? []).map(symbol => symbol.id), edit.delete ?? []), edit]));
654
668
  /**
655
669
  * Bases the child on the parent. A symbol of the child that collates
656
670
  * with one the parent hands down adds its carriers to that symbol; the
@@ -658,10 +672,36 @@ const dropDerivations = (version, matches) => {
658
672
  * and the child lacks becomes its deletions. The derivation states the
659
673
  * tolerance it was collated at and becomes the principal one; the
660
674
  * hypotheses the child stated beside its former one stay.
675
+ *
676
+ * The edits the child already stated are rewritten only as far as a
677
+ * collation wrote them (`isCollationsOwn`). An editor's reading of the
678
+ * difference between the two texts stays, and the symbols it speaks for
679
+ * are left out of the collation, so that connecting the two again
680
+ * neither doubles them nor silently drops what somebody established by
681
+ * hand. An equivalence is the collation's own, since it is drawn from
682
+ * the two systems' vocabularies rather than read off the paper, but one
683
+ * drawn again over the very same symbols is kept as it stands, with its
684
+ * identifier and whatever was written on it.
685
+ *
686
+ * The child's own text is what it shows less what the parent hands
687
+ * down, so that connecting two versions already connected collates the
688
+ * child's symbols and not the parent's with themselves.
661
689
  */
662
690
  export const connectVersions = (view, childId, parentId, tolerance = defaultCollationTolerance) => {
663
- const inherited = view.snapshot(parentId);
664
- const own = view.snapshot(childId);
691
+ const child = view.get(childId);
692
+ const stated = child ? editsOf(child) : [];
693
+ const established = stated.filter(edit => !isCollationsOwn(edit));
694
+ const spokenFor = spokenForBy(established);
695
+ const unspoken = (symbol) => !spokenFor.has(symbol.id);
696
+ // What the parent hands down and the child still shows passes
697
+ // through: it is neither the child's own symbol nor one it lacks.
698
+ // Without that, connecting a pair already connected would collate
699
+ // the inherited symbols with themselves and double their carriers.
700
+ const handedDown = view.snapshot(parentId).filter(unspoken);
701
+ const shown = new Set(view.snapshot(childId).map(symbol => symbol.id));
702
+ const inheritedIds = new Set(handedDown.map(symbol => symbol.id));
703
+ const inherited = handedDown.filter(symbol => !shown.has(symbol.id));
704
+ const own = view.snapshot(childId).filter(symbol => unspoken(symbol) && !inheritedIds.has(symbol.id));
665
705
  const locate = (symbol) => view.placeOf(symbol);
666
706
  const collations = collationsOf(own, inherited, locate, tolerance);
667
707
  const collated = new Set(collations.map(({ symbol }) => symbol.id));
@@ -673,18 +713,25 @@ export const connectVersions = (view, childId, parentId, tolerance = defaultColl
673
713
  * apart would make the apparatus a list of unexplained losses beside
674
714
  * a list of unexplained gains.
675
715
  */
676
- const substituted = differ(view.get(childId), view.get(parentId))
716
+ const substituted = differ(child, view.get(parentId))
677
717
  ? substitutionsBetween(own.filter(symbol => !collated.has(symbol.id)), inherited.filter(symbol => !matched.has(symbol.id)), locate, tolerance)
678
718
  : [];
679
719
  const paired = new Set(substituted.flatMap(({ replaced, by }) => [...by, ...replaced].map(symbol => symbol.id)));
680
- const edits = [
681
- ...substituted.map(({ replaced, by }) => ({
720
+ const alreadyStated = byExchange(stated);
721
+ const equivalence = ({ replaced, by }) => {
722
+ const inserted = by.map(symbol => symbol.id);
723
+ const deleted = replaced.map(symbol => symbol.id);
724
+ return alreadyStated.get(exchangeKey(inserted, deleted)) ?? {
682
725
  type: 'edit',
683
726
  id: v4(),
684
727
  editType: 'replace-with-equivalent',
685
728
  insert: [...by],
686
- delete: replaced.map(symbol => symbol.id)
687
- })),
729
+ delete: deleted
730
+ };
731
+ };
732
+ const edits = [
733
+ ...established,
734
+ ...substituted.map(equivalence),
688
735
  ...own.filter(symbol => !collated.has(symbol.id) && !paired.has(symbol.id)).map(insertion),
689
736
  ...inherited
690
737
  .filter(symbol => !matched.has(symbol.id) && !paired.has(symbol.id))
@@ -718,6 +765,72 @@ export const collateSymbols = (view, versionId, symbolIds, tolerance) => {
718
765
  dropInsertions(version, collated);
719
766
  });
720
767
  };
768
+ /** The symbol as one copy reads it: what it says, on the given carriers, standing in no relation of its own. */
769
+ const readingOf = (symbol, carriers) => {
770
+ const identity = { id: v4(), carriers };
771
+ switch (symbol.type) {
772
+ case 'note':
773
+ return { type: 'note', pitch: symbol.pitch, ...identity };
774
+ case 'expression':
775
+ return { type: 'expression', expressionType: symbol.expressionType, scope: symbol.scope, ...identity };
776
+ case 'text':
777
+ return { type: 'text', text: symbol.text, ...identity };
778
+ }
779
+ };
780
+ /** The symbol where the copy and at least one other carry it, with the carriers of each side. */
781
+ const sharedWith = (view, symbol, copyId) => {
782
+ const onCopy = (carrier) => view.copyOf(idOf(carrier))?.id === copyId;
783
+ const mine = symbol.carriers.filter(onCopy);
784
+ const theirs = symbol.carriers.filter(carrier => !onCopy(carrier));
785
+ return mine.length > 0 && theirs.length > 0 ? [{ symbol, mine, theirs }] : [];
786
+ };
787
+ /**
788
+ * Takes the copy's reading of a symbol back out of the symbol it was
789
+ * collated into: the copy's carriers pass to a new symbol of the
790
+ * version's own, the other copies keep the symbol they had, and the
791
+ * version states the exchange.
792
+ *
793
+ * This is the inverse of the hand-over a collation makes, and it is
794
+ * what lets a derivation be collated a second time. A collated symbol
795
+ * is one symbol carrying every copy that reads it, so nothing else
796
+ * takes the two readings apart again, and without that a tolerance
797
+ * arrived at after the fact could never be applied.
798
+ *
799
+ * Symbols the copy alone carries are already the version's own and are
800
+ * passed over, so a reading somebody has separated by hand keeps its
801
+ * identifier and the edit that speaks for it. Named symbols narrow the
802
+ * act to those; naming none separates the copy's whole reading.
803
+ */
804
+ export const separateReadings = (view, versionId, copyId, symbolIds) => {
805
+ const version = view.get(versionId);
806
+ if (!version)
807
+ return noChange;
808
+ const chosen = symbolIds && new Set(symbolIds);
809
+ const shared = view.snapshot(versionId)
810
+ .filter(symbol => chosen === undefined || chosen.has(symbol.id))
811
+ .flatMap(symbol => sharedWith(view, symbol, copyId));
812
+ if (shared.length === 0)
813
+ return noChange;
814
+ const inserted = new Set(insertedBy(version).map(symbol => symbol.id));
815
+ const stated = editsOf(version);
816
+ const separations = shared.map(({ symbol, mine }) => ({
817
+ type: 'edit',
818
+ id: v4(),
819
+ insert: [readingOf(symbol, [...mine])],
820
+ // A symbol the version inserts itself stays where it is, minus
821
+ // the carriers that leave it. Only one it inherits is exchanged.
822
+ ...(inserted.has(symbol.id) ? {} : { delete: [symbol.id] })
823
+ }));
824
+ return onVersion(versionId, (version, draft) => {
825
+ shared.forEach(({ symbol, theirs }) => {
826
+ const path = view.getPath(symbol.id);
827
+ const target = path && getAt(path, draft);
828
+ if (target)
829
+ target.carriers = theirs;
830
+ });
831
+ version.edits = [...stated, ...separations];
832
+ });
833
+ };
721
834
  /**
722
835
  * Makes the version stand on its own: what it inherited becomes its
723
836
  * own insertions, and its derivations go, the hypotheses among them,
package/lib/index.d.ts CHANGED
@@ -18,11 +18,14 @@ export * from './ReproducingSystem';
18
18
  export * from './FeatureSource';
19
19
  export * from './RollCopy';
20
20
  export * from './procedures';
21
+ export * from './vocabulary';
21
22
  export * from './reservations';
22
23
  export * from './witnesses';
23
24
  export * from './alignment';
25
+ export * from './statistics';
24
26
  export * from './Edition';
25
27
  export * from './EditionView';
28
+ export * from './scatter';
26
29
  export * from './editionOps';
27
30
  export * from './Emulation';
28
31
  export * from './validate';
package/lib/index.js CHANGED
@@ -18,11 +18,14 @@ export * from './ReproducingSystem';
18
18
  export * from './FeatureSource';
19
19
  export * from './RollCopy';
20
20
  export * from './procedures';
21
+ export * from './vocabulary';
21
22
  export * from './reservations';
22
23
  export * from './witnesses';
23
24
  export * from './alignment';
25
+ export * from './statistics';
24
26
  export * from './Edition';
25
27
  export * from './EditionView';
28
+ export * from './scatter';
26
29
  export * from './editionOps';
27
30
  export * from './Emulation';
28
31
  export * from './validate';
package/lib/migrate.js CHANGED
@@ -40,6 +40,17 @@ const withoutVersionType = (node) => {
40
40
  const { versionType: _retired, ...rest } = node;
41
41
  return rest;
42
42
  };
43
+ /**
44
+ * A version carried the label it was cited under, from before the sigla
45
+ * were read off the stemma. A copy keeps its siglum, which is given by
46
+ * hand and derives from nothing.
47
+ */
48
+ const withoutVersionSiglum = (node) => {
49
+ if (node['@type'] !== 'Version' || !Object.hasOwn(node, 'siglum'))
50
+ return node;
51
+ const { siglum: _labelled, ...rest } = node;
52
+ return rest;
53
+ };
43
54
  const withLowerCaseTerms = (node) => Object.entries(lowerCasedTerms).reduce((result, [key, terms]) => {
44
55
  const lowered = terms[result[key]];
45
56
  return lowered ? { ...result, [key]: lowered } : result;
@@ -221,7 +232,7 @@ const withTimeSpanDates = (node) => {
221
232
  const { '@value': within, '@type': _typed, ...rest } = node;
222
233
  return { ...rest, within };
223
234
  };
224
- const migrateNode = (node) => [withRenamedKeys, withTypology, withoutVersionType, withLowerCaseTerms, withSplitMethod, withReferences, withKeeper, withoutEmptyKeeper,
235
+ const migrateNode = (node) => [withRenamedKeys, withTypology, withoutVersionType, withoutVersionSiglum, withLowerCaseTerms, withSplitMethod, withReferences, withKeeper, withoutEmptyKeeper,
225
236
  withProductionNodes, withScale, withDerivationList, withReadingKind, withTimeSpanDates, withoutFeatureKind,
226
237
  withBorneFeaturesNamed, withFeaturesInActs]
227
238
  .reduce((result, step) => step(result), node);