linked-rolls 0.70.0 → 0.72.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 +106 -5
- package/lib/analysis/constraints.d.ts +7 -0
- package/lib/analysis/constraints.js +1 -0
- package/lib/analysis/ownPaper.d.ts +5 -1
- package/lib/analysis/ownPaper.js +5 -1
- package/lib/analysis/paper.d.ts +22 -4
- package/lib/analysis/paper.js +6 -0
- package/lib/analysis/reservations.d.ts +24 -1
- package/lib/analysis/reservations.js +15 -1
- package/lib/analysis/sigla.d.ts +6 -1
- package/lib/analysis/sigla.js +5 -1
- package/lib/analysis/stemma.d.ts +6 -1
- package/lib/analysis/stemma.js +6 -1
- package/lib/analysis/text.d.ts +16 -2
- package/lib/analysis/text.js +16 -2
- package/lib/analysis/witnesses.d.ts +20 -1
- package/lib/analysis/witnesses.js +4 -0
- package/lib/collation/Collation.d.ts +47 -8
- package/lib/collation/Collation.js +29 -5
- package/lib/collation/alignment.d.ts +16 -3
- package/lib/collation/alignment.js +15 -3
- package/lib/collation/scatter.d.ts +50 -9
- package/lib/collation/scatter.js +11 -1
- package/lib/collation/statistics.d.ts +39 -9
- package/lib/collation/statistics.js +27 -6
- package/lib/collation/substitution.d.ts +5 -0
- package/lib/collation/substitution.js +4 -0
- package/lib/emulation/Emulation.d.ts +16 -1
- package/lib/emulation/Emulation.js +10 -0
- package/lib/emulation/negotiation.d.ts +1 -0
- package/lib/emulation/negotiation.js +1 -0
- package/lib/filiation/adoption.d.ts +47 -0
- package/lib/filiation/adoption.js +190 -0
- package/lib/filiation/changes.d.ts +33 -0
- package/lib/filiation/changes.js +38 -0
- package/lib/filiation/index.d.ts +12 -0
- package/lib/filiation/index.js +12 -0
- package/lib/filiation/presence.d.ts +50 -0
- package/lib/filiation/presence.js +122 -0
- package/lib/filiation/proposal.d.ts +132 -0
- package/lib/filiation/proposal.js +376 -0
- package/lib/filiation/readings.d.ts +108 -0
- package/lib/filiation/readings.js +281 -0
- package/lib/filiation/search.d.ts +113 -0
- package/lib/filiation/search.js +268 -0
- package/lib/filiation/systems.d.ts +21 -0
- package/lib/filiation/systems.js +39 -0
- package/lib/filiation/trees.d.ts +20 -0
- package/lib/filiation/trees.js +69 -0
- package/lib/index.d.ts +34 -2
- package/lib/index.js +3 -1
- package/lib/io/asJsonLd.d.ts +10 -0
- package/lib/io/asJsonLd.js +10 -0
- package/lib/io/context.d.ts +16 -2
- package/lib/io/context.js +16 -2
- package/lib/io/importJsonLd.d.ts +1 -0
- package/lib/io/importJsonLd.js +1 -0
- package/lib/io/migrate.d.ts +8 -0
- package/lib/io/migrate.js +8 -0
- package/lib/lookup/lookup.d.ts +29 -5
- package/lib/lookup/lookup.js +29 -5
- package/lib/lookup/paths.d.ts +23 -3
- package/lib/lookup/paths.js +17 -3
- package/lib/model/Agent.d.ts +28 -2
- package/lib/model/Agent.js +15 -1
- package/lib/model/Assumption.d.ts +96 -7
- package/lib/model/Assumption.js +65 -7
- package/lib/model/ConditionState.d.ts +1 -0
- package/lib/model/Edit.d.ts +10 -0
- package/lib/model/Edit.js +8 -0
- package/lib/model/Edition.d.ts +11 -0
- package/lib/model/Edition.js +1 -0
- package/lib/model/Feature.d.ts +74 -7
- package/lib/model/Feature.js +41 -3
- package/lib/model/FeatureSource.d.ts +14 -1
- package/lib/model/FeatureSource.js +7 -1
- package/lib/model/Perforator.d.ts +10 -0
- package/lib/model/Perforator.js +1 -0
- package/lib/model/Quantity.d.ts +160 -13
- package/lib/model/Quantity.js +110 -6
- package/lib/model/RollCopy.d.ts +74 -12
- package/lib/model/RollCopy.js +53 -10
- package/lib/model/Symbol.d.ts +38 -1
- package/lib/model/Symbol.js +16 -0
- package/lib/model/TrackCalibration.d.ts +10 -2
- package/lib/model/TrackCalibration.js +9 -2
- package/lib/model/Version.d.ts +24 -4
- package/lib/model/Version.js +19 -4
- package/lib/model/notes.d.ts +15 -2
- package/lib/model/notes.js +9 -2
- package/lib/model/procedures.d.ts +5 -1
- package/lib/model/procedures.js +5 -1
- package/lib/model/vocabulary.d.ts +6 -1
- package/lib/model/vocabulary.js +6 -1
- package/lib/ops/beliefs.d.ts +23 -1
- package/lib/ops/beliefs.js +23 -1
- package/lib/ops/collation.d.ts +3 -0
- package/lib/ops/collation.js +3 -0
- package/lib/ops/commands.d.ts +17 -2
- package/lib/ops/commands.js +17 -2
- package/lib/ops/copies.d.ts +37 -6
- package/lib/ops/copies.js +37 -6
- package/lib/ops/draft.d.ts +2 -0
- package/lib/ops/draft.js +1 -0
- package/lib/ops/edits.d.ts +5 -1
- package/lib/ops/edits.js +5 -1
- package/lib/ops/features.d.ts +12 -1
- package/lib/ops/features.js +7 -0
- package/lib/ops/versions.d.ts +15 -3
- package/lib/ops/versions.js +15 -3
- package/lib/readers/phillipsEroll.d.ts +12 -1
- package/lib/readers/phillipsEroll.js +6 -1
- package/lib/readers/spencerBar.d.ts +12 -1
- package/lib/readers/spencerBar.js +7 -1
- package/lib/readers/stanfordAton.d.ts +15 -0
- package/lib/readers/stanfordAton.js +9 -0
- package/lib/schema.json +10 -0
- package/lib/shared/utils.d.ts +12 -0
- package/lib/spec/context.json +4 -0
- package/lib/systems/ReproducingSystem.d.ts +46 -3
- package/lib/systems/TrackerBar.d.ts +45 -4
- package/lib/systems/TrackerBar.js +20 -3
- package/lib/systems/index.d.ts +9 -2
- package/lib/systems/index.js +9 -2
- package/lib/systems/velocity.d.ts +1 -0
- package/lib/systems/velocity.js +1 -0
- package/lib/systems/welte.d.ts +18 -3
- package/lib/systems/welte.js +9 -2
- package/lib/systems/welteLicensee/bar.d.ts +1 -0
- package/lib/systems/welteLicensee/bar.js +1 -0
- package/lib/systems/welteLicensee/system.d.ts +18 -0
- package/lib/systems/welteLicensee/system.js +17 -0
- package/lib/systems/welteT100/bar.d.ts +8 -0
- package/lib/systems/welteT100/bar.js +4 -0
- package/lib/systems/welteT100/system.d.ts +46 -5
- package/lib/systems/welteT100/system.js +29 -3
- package/lib/systems/welteT98/bar.d.ts +6 -0
- package/lib/systems/welteT98/bar.js +2 -0
- package/lib/systems/welteT98/system.d.ts +58 -5
- package/lib/systems/welteT98/system.js +42 -3
- package/lib/validate.d.ts +16 -0
- package/lib/validate.js +15 -0
- package/package.json +2 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Edition } from "../model/Edition.js";
|
|
2
|
+
import { HorizontalSpan } from "../model/Feature.js";
|
|
3
|
+
import { RollCopy } from "../model/RollCopy.js";
|
|
4
|
+
import { Millimeters } from "../model/Quantity.js";
|
|
5
|
+
import { TrackerBar, TrackMeaning } from "../systems/TrackerBar.js";
|
|
6
|
+
import { CollationTolerance } from "../collation/Collation.js";
|
|
7
|
+
/**
|
|
8
|
+
* One copy's reading of one feature: what the copy's bar reads off it,
|
|
9
|
+
* and where.
|
|
10
|
+
* @category Filiation
|
|
11
|
+
*/
|
|
12
|
+
export interface CopyReading {
|
|
13
|
+
/** The feature and what it is read as, since a feature across two positions reads as two commands. */
|
|
14
|
+
id: string;
|
|
15
|
+
copy: string;
|
|
16
|
+
feature: string;
|
|
17
|
+
meaning: TrackMeaning;
|
|
18
|
+
key: string;
|
|
19
|
+
place: HorizontalSpan;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A reading the copies share: the readings of one meaning that lie at
|
|
23
|
+
* about the same place, at most one of each copy.
|
|
24
|
+
* @category Filiation
|
|
25
|
+
*/
|
|
26
|
+
export interface CollatedReading {
|
|
27
|
+
/**
|
|
28
|
+
* A name for the reading, the same for the same meaning on the same
|
|
29
|
+
* features, so that collating again gives it again.
|
|
30
|
+
*/
|
|
31
|
+
id: string;
|
|
32
|
+
meaning: TrackMeaning;
|
|
33
|
+
key: string;
|
|
34
|
+
/** The features carrying it, each on another copy. */
|
|
35
|
+
carriers: string[];
|
|
36
|
+
/** The copies carrying it, in the order the collation was given them. */
|
|
37
|
+
copies: string[];
|
|
38
|
+
/** Where it lies: the median of its carriers at either end. */
|
|
39
|
+
place: HorizontalSpan;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* How far two copies may put one reading apart, and where that window
|
|
43
|
+
* is centred: the systematic difference between the two and the scatter
|
|
44
|
+
* about it, as `scatter.ts` measures a derivation's.
|
|
45
|
+
* @category Filiation
|
|
46
|
+
*/
|
|
47
|
+
export interface PairWindow extends CollationTolerance {
|
|
48
|
+
/** How many readings the window was measured on; none where it fell back on the default. */
|
|
49
|
+
n: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A command one copy holds where another latches, as a transfer into a
|
|
53
|
+
* system with held commands spells it: the readings of the one standing
|
|
54
|
+
* for the readings of the other.
|
|
55
|
+
* @category Filiation
|
|
56
|
+
*/
|
|
57
|
+
export interface Recoding {
|
|
58
|
+
/** The collated readings of the latched pair. */
|
|
59
|
+
replaced: string[];
|
|
60
|
+
/** The collated readings of the held command. */
|
|
61
|
+
by: string[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The collation of a set of copies.
|
|
65
|
+
* @category Filiation
|
|
66
|
+
*/
|
|
67
|
+
export interface CopyCollation {
|
|
68
|
+
copies: string[];
|
|
69
|
+
readings: CollatedReading[];
|
|
70
|
+
/** By pair of copies (`a|b|group`, the pair in the order the copies were given), the window used. */
|
|
71
|
+
windows: Record<string, PairWindow>;
|
|
72
|
+
recodings: Recoding[];
|
|
73
|
+
/** By copy, where its text begins and ends. */
|
|
74
|
+
extent: Record<string, {
|
|
75
|
+
from: Millimeters;
|
|
76
|
+
to: Millimeters;
|
|
77
|
+
}>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* What the copy's own punching put on the paper and its bar reads as
|
|
81
|
+
* text, in order of onset.
|
|
82
|
+
*
|
|
83
|
+
* A later alteration of the copy is not part of what was punched from
|
|
84
|
+
* the version, so only the features of the production count. Where the
|
|
85
|
+
* bar finds the roll's end, reading stops, and the end itself is not
|
|
86
|
+
* read. A command no other scale has a word for (the motor, the rewind,
|
|
87
|
+
* the electric cut-off) runs the instrument rather than the music and
|
|
88
|
+
* is no reading either.
|
|
89
|
+
* @category Filiation
|
|
90
|
+
*/
|
|
91
|
+
export declare const readingsOfCopy: (copy: Readonly<RollCopy>) => CopyReading[];
|
|
92
|
+
/**
|
|
93
|
+
* Collates the copies all at once. Every reading is compared with every
|
|
94
|
+
* reading of the same meaning on the other copies, and two are taken to
|
|
95
|
+
* be one where they lie within the window of their pair of copies at
|
|
96
|
+
* both ends. The readings are then joined nearest pair first, and never
|
|
97
|
+
* so that one copy would read a joined reading twice.
|
|
98
|
+
*
|
|
99
|
+
* Collating one copy after another against what was collated before, as
|
|
100
|
+
* connecting versions does, depends on the order: six copies collated in
|
|
101
|
+
* their 720 orders give hundreds of different results. Compared all with
|
|
102
|
+
* all, the result is the same whichever order the copies are given in,
|
|
103
|
+
* and only the copies themselves decide it.
|
|
104
|
+
* @category Filiation
|
|
105
|
+
*/
|
|
106
|
+
export declare const collateCopies: (edition: Pick<Edition, 'copies'>, copyIds: readonly string[]) => CopyCollation;
|
|
107
|
+
/** Whether the bar has a word for the meaning at all. */
|
|
108
|
+
export declare const reads: (bar: TrackerBar, meaning: TrackMeaning) => boolean;
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The readings of a set of copies, collated all at once: every copy
|
|
3
|
+
* against every other, so that what counts as one reading does not
|
|
4
|
+
* depend on the order the copies are taken in.
|
|
5
|
+
*/
|
|
6
|
+
import { v5 } from "uuid";
|
|
7
|
+
import { asSymbols, barOf } from "../model/RollCopy.js";
|
|
8
|
+
import { idOf } from "../model/Assumption.js";
|
|
9
|
+
import { mm } from "../model/Quantity.js";
|
|
10
|
+
import { keyOf } from "../systems/TrackerBar.js";
|
|
11
|
+
import { medianOf, spreadOf } from "../collation/statistics.js";
|
|
12
|
+
import { departureThreshold } from "../collation/scatter.js";
|
|
13
|
+
import { substitutionsBetween } from "../collation/substitution.js";
|
|
14
|
+
import { transfersOf } from "./systems.js";
|
|
15
|
+
/** The namespace the names of collated readings are minted in, so that the same carriers always give the same name. */
|
|
16
|
+
const READINGS = '0b7a3f5e-5d0c-4a59-9d55-4b2b6c1f6a70';
|
|
17
|
+
/**
|
|
18
|
+
* How far apart the first estimate takes two readings to be the same:
|
|
19
|
+
* far beyond any scatter, near enough to stay off the next command of
|
|
20
|
+
* the same kind.
|
|
21
|
+
*/
|
|
22
|
+
const PROVISIONAL = mm(8);
|
|
23
|
+
/** The window between a pair too seldom read together for a scatter to be read off. */
|
|
24
|
+
const FALLBACK = { n: 0, toleranceStart: mm(5), toleranceEnd: mm(5) };
|
|
25
|
+
/** Below this many shared readings no scatter is read off a pair. */
|
|
26
|
+
const FEWEST = 10;
|
|
27
|
+
/** Two readings further apart than this are never compared. */
|
|
28
|
+
const REACH = mm(40);
|
|
29
|
+
/** How far a held command may lie from the latched pair it stands for. */
|
|
30
|
+
const RECODING_TOLERANCE = { toleranceStart: mm(5), toleranceEnd: mm(5) };
|
|
31
|
+
const groupOf = (meaning) => meaning.type;
|
|
32
|
+
/**
|
|
33
|
+
* What the copy's own punching put on the paper and its bar reads as
|
|
34
|
+
* text, in order of onset.
|
|
35
|
+
*
|
|
36
|
+
* A later alteration of the copy is not part of what was punched from
|
|
37
|
+
* the version, so only the features of the production count. Where the
|
|
38
|
+
* bar finds the roll's end, reading stops, and the end itself is not
|
|
39
|
+
* read. A command no other scale has a word for (the motor, the rewind,
|
|
40
|
+
* the electric cut-off) runs the instrument rather than the music and
|
|
41
|
+
* is no reading either.
|
|
42
|
+
* @category Filiation
|
|
43
|
+
*/
|
|
44
|
+
export const readingsOfCopy = (copy) => {
|
|
45
|
+
const bar = barOf(copy);
|
|
46
|
+
const chains = (copy.production?.produced ?? []).filter((feature) => feature.type === 'HoleChain');
|
|
47
|
+
const end = bar.endsAt(chains);
|
|
48
|
+
const place = new Map(chains.map(chain => [chain.id, chain.horizontal]));
|
|
49
|
+
return asSymbols(chains, bar)
|
|
50
|
+
.filter(symbol => symbol.type !== 'expression' || bar.operationOf(symbol.expressionType) !== undefined)
|
|
51
|
+
.flatMap((symbol) => {
|
|
52
|
+
if (symbol.type === 'text')
|
|
53
|
+
return [];
|
|
54
|
+
const feature = idOf(symbol.carriers[0]);
|
|
55
|
+
const horizontal = place.get(feature);
|
|
56
|
+
if (end !== undefined && horizontal.from >= end.at)
|
|
57
|
+
return [];
|
|
58
|
+
const meaning = symbol.type === 'note'
|
|
59
|
+
? { type: 'note', pitch: symbol.pitch }
|
|
60
|
+
: { type: 'expression', expressionType: symbol.expressionType, scope: symbol.scope };
|
|
61
|
+
const key = keyOf(meaning);
|
|
62
|
+
return [{ id: `${feature}|${key}`, copy: copy.id, feature, meaning, key, place: { ...horizontal } }];
|
|
63
|
+
})
|
|
64
|
+
.sort((a, b) => a.place.from - b.place.from || compare(a.id, b.id));
|
|
65
|
+
};
|
|
66
|
+
const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
67
|
+
const groupBy = (items, key) => {
|
|
68
|
+
const groups = new Map();
|
|
69
|
+
for (const item of items) {
|
|
70
|
+
const k = key(item);
|
|
71
|
+
const group = groups.get(k);
|
|
72
|
+
if (group)
|
|
73
|
+
group.push(item);
|
|
74
|
+
else
|
|
75
|
+
groups.set(k, [item]);
|
|
76
|
+
}
|
|
77
|
+
return groups;
|
|
78
|
+
};
|
|
79
|
+
const pairKey = (a, b, group) => `${a}|${b}|${group}`;
|
|
80
|
+
/** The reading of the other copy nearest in onset, within the provisional window, ties to the smaller id. */
|
|
81
|
+
const nearestOf = (reading, others) => {
|
|
82
|
+
let best;
|
|
83
|
+
let distance = Infinity;
|
|
84
|
+
for (const other of others) {
|
|
85
|
+
const d = Math.abs(other.place.from - reading.place.from);
|
|
86
|
+
if (d < distance || (d === distance && best && other.id < best.id)) {
|
|
87
|
+
best = other;
|
|
88
|
+
distance = d;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return distance <= PROVISIONAL ? best : undefined;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* The window of every pair of copies, read off the readings they share
|
|
95
|
+
* at first sight: for each meaning, the mutually nearest onsets within a
|
|
96
|
+
* wide provisional window. Their displacements, the later copy less the
|
|
97
|
+
* earlier, give the centre (the median) and the scatter about it (the
|
|
98
|
+
* median absolute deviation as a standard deviation), and the window is
|
|
99
|
+
* the scatter times the threshold beyond which fewer than one reading of
|
|
100
|
+
* a sample of that size would fall by chance.
|
|
101
|
+
*/
|
|
102
|
+
const windowsOf = (copies, byKey) => {
|
|
103
|
+
const samples = new Map();
|
|
104
|
+
for (const readings of byKey.values()) {
|
|
105
|
+
const group = groupOf(readings[0].meaning);
|
|
106
|
+
const byCopy = groupBy(readings, reading => reading.copy);
|
|
107
|
+
for (let i = 0; i < copies.length; i++) {
|
|
108
|
+
for (let j = i + 1; j < copies.length; j++) {
|
|
109
|
+
const one = byCopy.get(copies[i]) ?? [];
|
|
110
|
+
const other = byCopy.get(copies[j]) ?? [];
|
|
111
|
+
for (const a of one) {
|
|
112
|
+
const b = nearestOf(a, other);
|
|
113
|
+
if (!b || nearestOf(b, one) !== a)
|
|
114
|
+
continue;
|
|
115
|
+
const key = pairKey(copies[i], copies[j], group);
|
|
116
|
+
const sample = samples.get(key) ?? { from: [], to: [] };
|
|
117
|
+
sample.from.push(mm(b.place.from - a.place.from));
|
|
118
|
+
sample.to.push(mm(b.place.to - a.place.to));
|
|
119
|
+
samples.set(key, sample);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const windows = {};
|
|
125
|
+
for (const [key, sample] of samples) {
|
|
126
|
+
const start = spreadOf(sample.from);
|
|
127
|
+
const end = spreadOf(sample.to);
|
|
128
|
+
if (!start || !end || sample.from.length < FEWEST || start.sigma === 0 || end.sigma === 0)
|
|
129
|
+
continue;
|
|
130
|
+
const k = departureThreshold(sample.from.length);
|
|
131
|
+
windows[key] = {
|
|
132
|
+
n: sample.from.length,
|
|
133
|
+
offsetStart: start.median,
|
|
134
|
+
offsetEnd: end.median,
|
|
135
|
+
toleranceStart: mm(k * start.sigma),
|
|
136
|
+
toleranceEnd: mm(k * end.sigma)
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return windows;
|
|
140
|
+
};
|
|
141
|
+
/** How far apart two readings lie, in units of the window of their copies; above one they are not the same. */
|
|
142
|
+
const distanceIn = (windows, order, x, y) => {
|
|
143
|
+
const [a, b] = order.get(x.copy) < order.get(y.copy) ? [x, y] : [y, x];
|
|
144
|
+
const window = windows[pairKey(a.copy, b.copy, groupOf(a.meaning))] ?? FALLBACK;
|
|
145
|
+
const start = Math.abs(b.place.from - a.place.from - (window.offsetStart ?? 0)) / window.toleranceStart;
|
|
146
|
+
const end = Math.abs(b.place.to - a.place.to - (window.offsetEnd ?? 0)) / window.toleranceEnd;
|
|
147
|
+
return Math.max(start, end);
|
|
148
|
+
};
|
|
149
|
+
/** The readings in clusters: nearest pairs first, never two readings of one copy in one cluster. */
|
|
150
|
+
const clustered = (readings, edges) => {
|
|
151
|
+
const parent = new Map(readings.map(reading => [reading.id, reading.id]));
|
|
152
|
+
const members = new Map(readings.map(reading => [reading.id, [reading]]));
|
|
153
|
+
const find = (id) => {
|
|
154
|
+
let root = id;
|
|
155
|
+
while (parent.get(root) !== root)
|
|
156
|
+
root = parent.get(root);
|
|
157
|
+
parent.set(id, root);
|
|
158
|
+
return root;
|
|
159
|
+
};
|
|
160
|
+
for (const { x, y } of edges) {
|
|
161
|
+
const a = find(x.id);
|
|
162
|
+
const b = find(y.id);
|
|
163
|
+
if (a === b)
|
|
164
|
+
continue;
|
|
165
|
+
const one = members.get(a);
|
|
166
|
+
const other = members.get(b);
|
|
167
|
+
const copies = new Set(one.map(reading => reading.copy));
|
|
168
|
+
if (other.some(reading => copies.has(reading.copy)))
|
|
169
|
+
continue;
|
|
170
|
+
const [root, child] = a < b ? [a, b] : [b, a];
|
|
171
|
+
parent.set(child, root);
|
|
172
|
+
members.set(root, [...members.get(root), ...members.get(child)]);
|
|
173
|
+
members.delete(child);
|
|
174
|
+
}
|
|
175
|
+
return [...members.values()];
|
|
176
|
+
};
|
|
177
|
+
const readingId = (key, carriers) => `symbol_${v5(`${key}|${[...carriers].sort().join(',')}`, READINGS)}`;
|
|
178
|
+
const asCollated = (members, order) => {
|
|
179
|
+
const sorted = [...members].sort((a, b) => order.get(a.copy) - order.get(b.copy));
|
|
180
|
+
const carriers = sorted.map(reading => reading.feature);
|
|
181
|
+
return {
|
|
182
|
+
id: readingId(sorted[0].key, carriers),
|
|
183
|
+
meaning: sorted[0].meaning,
|
|
184
|
+
key: sorted[0].key,
|
|
185
|
+
carriers,
|
|
186
|
+
copies: sorted.map(reading => reading.copy),
|
|
187
|
+
place: {
|
|
188
|
+
unit: 'mm',
|
|
189
|
+
from: medianOf(sorted.map(reading => reading.place.from)),
|
|
190
|
+
to: medianOf(sorted.map(reading => reading.place.to))
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* Where the copies of a system re-cut from another hold what the other
|
|
196
|
+
* latches: for each such pair of copies, the held commands that stand
|
|
197
|
+
* for a latched pair, as `substitutionsBetween` finds them. Each is
|
|
198
|
+
* stated once, however many copies of the older system it was found
|
|
199
|
+
* against.
|
|
200
|
+
*/
|
|
201
|
+
const recodingsOf = (edition, copies, readingsOf, collatedOf) => {
|
|
202
|
+
const systemOf = new Map(copies.map(id => [id, barOf(edition.copies.find(copy => copy.id === id)).id]));
|
|
203
|
+
const transfers = transfersOf();
|
|
204
|
+
const asExpression = (reading) => ({ type: 'expression', id: reading.id, carriers: [], ...reading.meaning });
|
|
205
|
+
const placeOf = new Map([...readingsOf.values()].flat().map(reading => [reading.id, reading.place]));
|
|
206
|
+
const locate = (symbol) => placeOf.get(symbol.id);
|
|
207
|
+
const found = new Map();
|
|
208
|
+
for (const newer of copies) {
|
|
209
|
+
for (const older of copies) {
|
|
210
|
+
if (!transfers.get(systemOf.get(older))?.has(systemOf.get(newer)))
|
|
211
|
+
continue;
|
|
212
|
+
const own = readingsOf.get(newer).filter(reading => reading.meaning.type === 'expression').map(asExpression);
|
|
213
|
+
const inherited = readingsOf.get(older).filter(reading => reading.meaning.type === 'expression').map(asExpression);
|
|
214
|
+
for (const substitution of substitutionsBetween(own, inherited, locate, RECODING_TOLERANCE)) {
|
|
215
|
+
const by = [...new Set(substitution.by.map(symbol => collatedOf.get(symbol.id)))].sort();
|
|
216
|
+
const replaced = [...new Set(substitution.replaced.map(symbol => collatedOf.get(symbol.id)))].sort();
|
|
217
|
+
const key = `${by.join(',')}>${replaced.join(',')}`;
|
|
218
|
+
if (!found.has(key))
|
|
219
|
+
found.set(key, { replaced, by });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return [...found.values()].sort((a, b) => compare(a.by[0], b.by[0]));
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* Collates the copies all at once. Every reading is compared with every
|
|
227
|
+
* reading of the same meaning on the other copies, and two are taken to
|
|
228
|
+
* be one where they lie within the window of their pair of copies at
|
|
229
|
+
* both ends. The readings are then joined nearest pair first, and never
|
|
230
|
+
* so that one copy would read a joined reading twice.
|
|
231
|
+
*
|
|
232
|
+
* Collating one copy after another against what was collated before, as
|
|
233
|
+
* connecting versions does, depends on the order: six copies collated in
|
|
234
|
+
* their 720 orders give hundreds of different results. Compared all with
|
|
235
|
+
* all, the result is the same whichever order the copies are given in,
|
|
236
|
+
* and only the copies themselves decide it.
|
|
237
|
+
* @category Filiation
|
|
238
|
+
*/
|
|
239
|
+
export const collateCopies = (edition, copyIds) => {
|
|
240
|
+
const copies = copyIds.filter(id => edition.copies.some(copy => copy.id === id));
|
|
241
|
+
const order = new Map(copies.map((id, index) => [id, index]));
|
|
242
|
+
const readingsOf = new Map(copies.map(id => [id, readingsOfCopy(edition.copies.find(copy => copy.id === id))]));
|
|
243
|
+
const all = [...readingsOf.values()].flat();
|
|
244
|
+
const byKey = groupBy(all, reading => reading.key);
|
|
245
|
+
const windows = windowsOf(copies, byKey);
|
|
246
|
+
const edges = [];
|
|
247
|
+
for (const readings of byKey.values()) {
|
|
248
|
+
const sorted = [...readings].sort((a, b) => a.place.from - b.place.from || compare(a.id, b.id));
|
|
249
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
250
|
+
for (let j = i + 1; j < sorted.length && sorted[j].place.from - sorted[i].place.from <= REACH; j++) {
|
|
251
|
+
const x = sorted[i];
|
|
252
|
+
const y = sorted[j];
|
|
253
|
+
if (x.copy === y.copy)
|
|
254
|
+
continue;
|
|
255
|
+
const d = distanceIn(windows, order, x, y);
|
|
256
|
+
if (d > 1 + 1e-9)
|
|
257
|
+
continue;
|
|
258
|
+
const [p, q] = x.id < y.id ? [x, y] : [y, x];
|
|
259
|
+
edges.push({ x: p, y: q, d });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
edges.sort((a, b) => a.d - b.d || compare(a.x.id, b.x.id) || compare(a.y.id, b.y.id));
|
|
264
|
+
const readings = clustered(all, edges)
|
|
265
|
+
.map(members => asCollated(members, order))
|
|
266
|
+
.sort((a, b) => a.place.from - b.place.from || compare(a.key, b.key) || compare(a.id, b.id));
|
|
267
|
+
const collatedOf = new Map();
|
|
268
|
+
for (const reading of readings) {
|
|
269
|
+
reading.carriers.forEach(feature => collatedOf.set(`${feature}|${reading.key}`, reading.id));
|
|
270
|
+
}
|
|
271
|
+
const extent = Object.fromEntries(copies.map(id => {
|
|
272
|
+
const own = readingsOf.get(id);
|
|
273
|
+
return [id, {
|
|
274
|
+
from: mm(Math.min(...own.map(reading => reading.place.from))),
|
|
275
|
+
to: mm(Math.max(...own.map(reading => reading.place.from)))
|
|
276
|
+
}];
|
|
277
|
+
}));
|
|
278
|
+
return { copies, readings, windows, recodings: recodingsOf(edition, copies, readingsOf, collatedOf), extent };
|
|
279
|
+
};
|
|
280
|
+
/** Whether the bar has a word for the meaning at all. */
|
|
281
|
+
export const reads = (bar, meaning) => bar.positionOf(meaning) !== undefined;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stemma that explains the presence of the readings at the least
|
|
3
|
+
* cost, searched among all of them, and what each node of it holds.
|
|
4
|
+
*/
|
|
5
|
+
import { Presence } from "./presence.js";
|
|
6
|
+
/**
|
|
7
|
+
* What the search counts against a stemma. A reading arising costs
|
|
8
|
+
* `gain`, and a reading the base text holds arose too. A reading lost
|
|
9
|
+
* costs 1, or less where the editorial principles say a loss is weak
|
|
10
|
+
* evidence.
|
|
11
|
+
* @category Filiation
|
|
12
|
+
*/
|
|
13
|
+
export interface Costs {
|
|
14
|
+
/**
|
|
15
|
+
* A reading arising. Ten times a loss, since an addition at exactly
|
|
16
|
+
* the same place is very unlikely to be made twice (shared addition),
|
|
17
|
+
* while a perforation is easily struck.
|
|
18
|
+
*/
|
|
19
|
+
gain: number;
|
|
20
|
+
/**
|
|
21
|
+
* A redundant reading lost, for the share of a loss: the removal of
|
|
22
|
+
* redundancies is expected and happens in two branches independently
|
|
23
|
+
* (independent cleanup).
|
|
24
|
+
*/
|
|
25
|
+
redundantLoss: number;
|
|
26
|
+
/**
|
|
27
|
+
* A reading lost on the way into another system, for the share of a
|
|
28
|
+
* loss: a re-cut was an editorial pass of its own, and what it lacks
|
|
29
|
+
* says little about where it was cut from.
|
|
30
|
+
*/
|
|
31
|
+
transferLoss: number;
|
|
32
|
+
/**
|
|
33
|
+
* The copy carrying a version lacking one of its readings, since a
|
|
34
|
+
* copy may fail to show what its version holds. Where it is
|
|
35
|
+
* `Infinity`, a copy carries its version exactly.
|
|
36
|
+
*/
|
|
37
|
+
copyDeviation: number;
|
|
38
|
+
/** A lost state, so that one is posited only where shared additions require it. */
|
|
39
|
+
lostState: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The costs the edition of WM 225 is reproduced with: a gain 10, a lost
|
|
43
|
+
* redundancy 0.25, a loss on a transfer 0.25, a deviation of a copy 1
|
|
44
|
+
* and a lost state 4. They were found by searching for values that give
|
|
45
|
+
* that stemma, so they say that it is consistent with such rules, not
|
|
46
|
+
* that the rules confirm it.
|
|
47
|
+
* @category Filiation
|
|
48
|
+
*/
|
|
49
|
+
export declare const defaultCosts: Costs;
|
|
50
|
+
/**
|
|
51
|
+
* A stemma laid out for counting: its nodes children first, the root
|
|
52
|
+
* last, each with the copy it carries, its parent, its system, whether
|
|
53
|
+
* it was re-cut from its parent, and the copies at and below it.
|
|
54
|
+
*/
|
|
55
|
+
export interface LaidOut {
|
|
56
|
+
copy: Int16Array;
|
|
57
|
+
parent: Int16Array;
|
|
58
|
+
transfer: Uint8Array;
|
|
59
|
+
system: string[];
|
|
60
|
+
below: Int32Array;
|
|
61
|
+
lost: number;
|
|
62
|
+
notation: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A stemma the search scored, in a notation naming the copies: a lost
|
|
66
|
+
* state as `*`, the versions derived from a node in brackets after it.
|
|
67
|
+
* @category Filiation
|
|
68
|
+
*/
|
|
69
|
+
export interface Scored {
|
|
70
|
+
notation: string;
|
|
71
|
+
cost: number;
|
|
72
|
+
lost: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* How much more the best stemma without one of the best one's groupings
|
|
76
|
+
* costs: the margin by which the copies decide it.
|
|
77
|
+
* @category Filiation
|
|
78
|
+
*/
|
|
79
|
+
export interface Support {
|
|
80
|
+
/** The copies the grouping holds, as the copies of a node and those below it. */
|
|
81
|
+
copies: string[];
|
|
82
|
+
/** Whether it is the grouping of a copy carrying a version above others, rather than of the copies below a node. */
|
|
83
|
+
ancestor?: string;
|
|
84
|
+
margin: number;
|
|
85
|
+
}
|
|
86
|
+
/** The outcome of the search. */
|
|
87
|
+
export interface Search {
|
|
88
|
+
best: LaidOut;
|
|
89
|
+
cost: number;
|
|
90
|
+
scored: number;
|
|
91
|
+
alternatives: Scored[];
|
|
92
|
+
supports: Support[];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Scores every stemma of the copies and keeps the best, the runners-up
|
|
96
|
+
* and, for each grouping of the best, the best stemma without it. Ties
|
|
97
|
+
* go to fewer lost states, then to the notation, so that the outcome
|
|
98
|
+
* depends on nothing but the copies and the costs.
|
|
99
|
+
*/
|
|
100
|
+
export declare const searchStemmata: (copies: readonly string[], names: readonly string[], systems: readonly string[], rows: readonly {
|
|
101
|
+
presence: readonly Presence[];
|
|
102
|
+
redundant: boolean;
|
|
103
|
+
}[], costs: Costs, transfers: ReadonlyMap<string, ReadonlySet<string>>, keep: number) => Search;
|
|
104
|
+
/**
|
|
105
|
+
* What every node holds of one reading under the least costly account
|
|
106
|
+
* of it: children first, as `LaidOut` orders the nodes. A node may be
|
|
107
|
+
* held to a state (`fixed`), as an edit the editor wrote holds a
|
|
108
|
+
* version to what it inserts or deletes. Where two accounts cost the
|
|
109
|
+
* same, the one with the node lacking the reading is taken, so that a
|
|
110
|
+
* reading arises as late as the copies allow. Nothing where the fixed
|
|
111
|
+
* states cannot be met.
|
|
112
|
+
*/
|
|
113
|
+
export declare const statesOf: (tree: LaidOut, presence: readonly Presence[], redundant: boolean, costs: Costs, fixed?: ReadonlyMap<number, boolean>) => boolean[] | undefined;
|