linked-rolls 0.71.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.
@@ -0,0 +1,132 @@
1
+ /**
2
+ * A stemma proposed from the copies alone: which versions there are,
3
+ * lost ones included, how they derive from each other, and which
4
+ * readings each inserts and deletes.
5
+ */
6
+ import { Edition } from "../model/Edition.js";
7
+ import { Millimeters } from "../model/Quantity.js";
8
+ import { CopyCollation, Recoding } from "./readings.js";
9
+ import { PresenceMatrix } from "./presence.js";
10
+ import { Costs, Scored, Support } from "./search.js";
11
+ /**
12
+ * How near a reading of the same kind on the other side of the root
13
+ * makes a reading on the less refined side a moved one, where nothing
14
+ * else is said. Hits up to about this distance are more than chance; a
15
+ * wider window agrees with the edition of WM 225 more often, but largely
16
+ * by convention.
17
+ * @category Filiation
18
+ */
19
+ export declare const defaultMovedWithin: import("../index.js").Quantity<"mm">;
20
+ /**
21
+ * What a proposal is computed with. Every value has a default, the one
22
+ * the edition of WM 225 is reproduced with.
23
+ * @category Filiation
24
+ */
25
+ export interface FiliationOptions {
26
+ /** The copies whose features are collated; by default every copy with features of its punching. */
27
+ copies?: readonly string[];
28
+ /** What the search counts against a stemma (`defaultCosts`). */
29
+ costs?: Partial<Costs>;
30
+ /**
31
+ * How near a reading of the same kind on the other side of the root
32
+ * makes a reading on the less refined side a moved one (`defaultMovedWithin`).
33
+ */
34
+ movedWithin?: Millimeters;
35
+ /** How far beyond its text, or the edge of a tear, a copy cannot say what it lacks (3 mm). */
36
+ margin?: Millimeters;
37
+ /** Whether the edits the editor wrote hold their versions to what they insert and delete (yes). */
38
+ pin?: boolean;
39
+ /** How many runners-up to report (5). */
40
+ alternatives?: number;
41
+ }
42
+ /**
43
+ * A version the proposal posits.
44
+ * @category Filiation
45
+ */
46
+ export interface ProposedVersion {
47
+ /** A name within the proposal: the versions are numbered from the root down. */
48
+ id: string;
49
+ /** The copy carrying the version at first hand; none for a lost state. */
50
+ copy?: string;
51
+ /** The system the version is coded for, by its identifier. */
52
+ system: string;
53
+ /** The version it derives from, by its name within the proposal. */
54
+ parent?: string;
55
+ /** The version of the edition it stands for, where one does. */
56
+ version?: string;
57
+ /** The collated readings the version inserts. */
58
+ inserts: string[];
59
+ /** The collated readings the version deletes. */
60
+ deletes: string[];
61
+ /** The held commands that stand for a latched pair, among what the version inserts and deletes. */
62
+ recodings: Recoding[];
63
+ /** The readings the version holds that the copy carrying it lacks. */
64
+ deviations: string[];
65
+ /** The edits the editor wrote on `version` that the proposal keeps as they are. */
66
+ pinned: string[];
67
+ }
68
+ /**
69
+ * Why an edit the editor wrote is not kept.
70
+ * @category Filiation
71
+ */
72
+ export type Unpinned = {
73
+ edit: string;
74
+ version: string;
75
+ reason: 'version-gone' | 'reading-gone' | 'contradicts-copies';
76
+ };
77
+ /**
78
+ * A stemma proposed from the copies, with what it rests on: the
79
+ * collation, the presence of every reading in every copy, the cost of
80
+ * the best stemma and of the runners-up, and the margin by which the
81
+ * copies decide each of its groupings.
82
+ * @category Filiation
83
+ */
84
+ export interface StemmaProposal {
85
+ copies: string[];
86
+ costs: Costs;
87
+ movedWithin: Millimeters;
88
+ margin: Millimeters;
89
+ collation: CopyCollation;
90
+ matrix: PresenceMatrix;
91
+ /** From the root down. */
92
+ versions: ProposedVersion[];
93
+ /** The stemma in the notation `Scored` uses, the copies by siglum. */
94
+ notation: string;
95
+ cost: number;
96
+ /** How many admissible stemmata were scored. */
97
+ scored: number;
98
+ alternatives: Scored[];
99
+ supports: Support[];
100
+ /** The readings the stemma lets arise more than once, with the versions inserting them. */
101
+ conflicts: {
102
+ reading: string;
103
+ versions: string[];
104
+ }[];
105
+ /** The readings on the less refined side of the root taken for the base text, as moved or cleaned up by the other side. */
106
+ baseText: string[];
107
+ /** By collated reading, the symbol of the edition it stands for, where one does. */
108
+ symbols: Record<string, string>;
109
+ unpinned: Unpinned[];
110
+ }
111
+ /**
112
+ * Proposes a stemma from the copies.
113
+ *
114
+ * The copies are collated all at once (`collateCopies`), and every
115
+ * reading becomes a character that each copy has, lacks or cannot speak
116
+ * to (`presenceIn`). Every stemma of the copies that the systems admit,
117
+ * up to seven copies, is scored by what it has to let arise and be lost
118
+ * (`Costs`), and the least costly is taken, ties going to fewer lost
119
+ * states. On it, each reading arises in the latest version all copies
120
+ * carrying it descend from, and is deleted where a branch lacks it. A
121
+ * reading only the less refined side of the root carries is taken for
122
+ * the base text where the other side has it moved or cleaned up.
123
+ *
124
+ * Where the edition has versions already, a version of the proposal
125
+ * that a copy carries at first hand stands for the version that copy
126
+ * carries in the edition, and a lost one for the version inserting most
127
+ * of the same readings. An edit the editor wrote on such a version, one
128
+ * a collation would not have written, holds the version to what it
129
+ * inserts and deletes, unless the copies cannot be read so.
130
+ * @category Filiation
131
+ */
132
+ export declare const proposeStemma: (edition: Edition, options?: FiliationOptions) => StemmaProposal;
@@ -0,0 +1,376 @@
1
+ import { barOf } from "../model/RollCopy.js";
2
+ import { idOf } from "../model/Assumption.js";
3
+ import { editsOf, insertedBy } from "../model/Version.js";
4
+ import { mm } from "../model/Quantity.js";
5
+ import { keyOf } from "../systems/TrackerBar.js";
6
+ import { isCollationsOwn } from "../collation/Collation.js";
7
+ import { versionsWitnessedBy } from "../analysis/witnesses.js";
8
+ import { collateCopies, reads } from "./readings.js";
9
+ import { defaultMargin, presenceIn } from "./presence.js";
10
+ import { defaultCosts, searchStemmata, statesOf } from "./search.js";
11
+ import { barNamed, transfersOf } from "./systems.js";
12
+ import { MAX_COPIES } from "./trees.js";
13
+ /**
14
+ * How near a reading of the same kind on the other side of the root
15
+ * makes a reading on the less refined side a moved one, where nothing
16
+ * else is said. Hits up to about this distance are more than chance; a
17
+ * wider window agrees with the edition of WM 225 more often, but largely
18
+ * by convention.
19
+ * @category Filiation
20
+ */
21
+ export const defaultMovedWithin = mm(25);
22
+ /** The copies with features of their own punching, in the order the edition lists them. */
23
+ const transcribed = (edition) => edition.copies
24
+ .filter(copy => (copy.production?.produced ?? []).some(feature => feature.type === 'HoleChain'))
25
+ .map(copy => copy.id);
26
+ const keyOfSymbol = (symbol) => symbol.type === 'text' ? undefined : keyOf(symbol);
27
+ /**
28
+ * By collated reading, the symbol of the edition it stands for: the one
29
+ * of the same meaning sharing most of its carriers, each symbol given to
30
+ * one reading at most. A reading whose carriers grew or shrank keeps the
31
+ * symbol's name, so that a collation repeated does not rename what it
32
+ * finds again.
33
+ */
34
+ const symbolsFor = (edition, readings) => {
35
+ const byCarrier = new Map();
36
+ for (const reading of readings) {
37
+ for (const carrier of reading.carriers)
38
+ byCarrier.set(carrier, [...(byCarrier.get(carrier) ?? []), reading]);
39
+ }
40
+ const candidates = [];
41
+ let order = 0;
42
+ for (const version of edition.versions) {
43
+ for (const symbol of insertedBy(version)) {
44
+ order++;
45
+ const key = keyOfSymbol(symbol);
46
+ if (!key)
47
+ continue;
48
+ const carriers = symbol.carriers.map(idOf);
49
+ const counts = new Map();
50
+ for (const carrier of carriers) {
51
+ for (const reading of byCarrier.get(carrier) ?? []) {
52
+ if (reading.key === key)
53
+ counts.set(reading, (counts.get(reading) ?? 0) + 1);
54
+ }
55
+ }
56
+ for (const [reading, shared] of counts) {
57
+ const exact = shared === carriers.length && shared === reading.carriers.length;
58
+ candidates.push({ symbol: symbol.id, reading: reading.id, shared, exact, order });
59
+ }
60
+ }
61
+ }
62
+ candidates.sort((a, b) => Number(b.exact) - Number(a.exact) || b.shared - a.shared || a.order - b.order || (a.reading < b.reading ? -1 : 1));
63
+ const symbols = {};
64
+ const taken = new Set();
65
+ for (const { symbol, reading } of candidates) {
66
+ if (taken.has(symbol) || symbols[reading])
67
+ continue;
68
+ symbols[reading] = symbol;
69
+ taken.add(symbol);
70
+ }
71
+ return symbols;
72
+ };
73
+ const childrenOf = (tree) => {
74
+ const children = Array.from({ length: tree.copy.length }, () => []);
75
+ tree.parent.forEach((parent, child) => { if (parent >= 0)
76
+ children[parent].push(child); });
77
+ return children;
78
+ };
79
+ /** The nodes from the root down, each before the nodes derived from it. */
80
+ const fromRoot = (tree) => {
81
+ const children = childrenOf(tree);
82
+ const order = [];
83
+ const visit = (v) => {
84
+ order.push(v);
85
+ children[v].forEach(visit);
86
+ };
87
+ visit(tree.copy.length - 1);
88
+ return order;
89
+ };
90
+ const inserting = (tree, states, v) => states[v] && (tree.parent[v] < 0 || !states[tree.parent[v]]);
91
+ const deleting = (tree, states, v) => !states[v] && tree.parent[v] >= 0 && states[tree.parent[v]];
92
+ /**
93
+ * The readings on the less refined side of the root that the other side
94
+ * moved or cleaned up, rather than that this side added.
95
+ *
96
+ * Where the root has two branches, presence alone cannot tell a reading
97
+ * one branch added from a reading of the base text the other changed.
98
+ * Refinement orients the root: the branch whose copies carry fewer
99
+ * dynamic perforations is the earlier text. A reading only that branch
100
+ * carries is then taken for the base text where the other branch carries
101
+ * a reading of the same kind within the window and this branch does not
102
+ * (moved), or where it is ineffective (cleaned up, as redundancies are).
103
+ */
104
+ const baseTextOf = (tree, readings, matrix, placement, held, within, systems) => {
105
+ const root = tree.copy.length - 1;
106
+ const children = childrenOf(tree)[root];
107
+ if (tree.copy[root] >= 0 || children.length !== 2)
108
+ return [];
109
+ const copiesBelow = (v) => matrix.copies.filter((_, c) => tree.below[v] & (1 << c));
110
+ const rootSystem = tree.system[root];
111
+ /** How many dynamic perforations the copies of the root's system below the node carry, on average. */
112
+ const densityOf = (v) => {
113
+ const copies = matrix.copies.filter((_, c) => (tree.below[v] & (1 << c)) && systems[c] === rootSystem);
114
+ if (copies.length === 0)
115
+ return Infinity;
116
+ const counts = copies.map(copy => readings.filter(reading => reading.meaning.type === 'expression' && matrix.presence[reading.id][copy] === 'present').length);
117
+ return counts.reduce((sum, count) => sum + count, 0) / counts.length;
118
+ };
119
+ const [one, other] = children;
120
+ const [side, refined] = densityOf(one) < densityOf(other) ? [one, other] : densityOf(other) < densityOf(one) ? [other, one] : [-1, -1];
121
+ if (side < 0)
122
+ return [];
123
+ const sideCopies = copiesBelow(side);
124
+ const refinedCopies = copiesBelow(refined);
125
+ const redundant = new Set(matrix.redundant);
126
+ const byKey = new Map();
127
+ for (const reading of readings)
128
+ byKey.set(reading.key, [...(byKey.get(reading.key) ?? []), reading]);
129
+ const moved = [];
130
+ for (const reading of readings) {
131
+ if (held.has(reading.id))
132
+ continue;
133
+ const states = placement.get(reading.id);
134
+ if (!inserting(tree, states, side))
135
+ continue;
136
+ const near = (byKey.get(reading.key) ?? []).some(other => other.id !== reading.id
137
+ && Math.abs(other.place.from - reading.place.from) <= within
138
+ && refinedCopies.some(copy => matrix.presence[other.id][copy] === 'present')
139
+ && !sideCopies.some(copy => matrix.presence[other.id][copy] === 'present'));
140
+ if (!near && !redundant.has(reading.id))
141
+ continue;
142
+ states[root] = true;
143
+ moved.push(reading.id);
144
+ }
145
+ return moved;
146
+ };
147
+ /**
148
+ * Where a version is coded for a system whose bar has no word for a
149
+ * reading it inherits, the reading does not go on: the re-cut deletes
150
+ * it, and what stands for it there is a reading of its own.
151
+ */
152
+ const recode = (tree, readings, placement) => {
153
+ const order = fromRoot(tree);
154
+ for (const reading of readings) {
155
+ const states = placement.get(reading.id);
156
+ for (const v of order) {
157
+ const bar = barNamed(tree.system[v]);
158
+ if (states[v] && bar && !reads(bar, reading.meaning))
159
+ states[v] = false;
160
+ }
161
+ }
162
+ };
163
+ /**
164
+ * Proposes a stemma from the copies.
165
+ *
166
+ * The copies are collated all at once (`collateCopies`), and every
167
+ * reading becomes a character that each copy has, lacks or cannot speak
168
+ * to (`presenceIn`). Every stemma of the copies that the systems admit,
169
+ * up to seven copies, is scored by what it has to let arise and be lost
170
+ * (`Costs`), and the least costly is taken, ties going to fewer lost
171
+ * states. On it, each reading arises in the latest version all copies
172
+ * carrying it descend from, and is deleted where a branch lacks it. A
173
+ * reading only the less refined side of the root carries is taken for
174
+ * the base text where the other side has it moved or cleaned up.
175
+ *
176
+ * Where the edition has versions already, a version of the proposal
177
+ * that a copy carries at first hand stands for the version that copy
178
+ * carries in the edition, and a lost one for the version inserting most
179
+ * of the same readings. An edit the editor wrote on such a version, one
180
+ * a collation would not have written, holds the version to what it
181
+ * inserts and deletes, unless the copies cannot be read so.
182
+ * @category Filiation
183
+ */
184
+ export const proposeStemma = (edition, options = {}) => {
185
+ const copies = transcribed(edition).filter(id => !options.copies || options.copies.includes(id));
186
+ if (copies.length < 2)
187
+ throw new Error('A stemma needs at least two copies with features');
188
+ if (copies.length > MAX_COPIES)
189
+ throw new Error(`The exhaustive search covers at most ${MAX_COPIES} copies`);
190
+ const costs = { ...defaultCosts, ...options.costs };
191
+ const movedWithin = options.movedWithin ?? defaultMovedWithin;
192
+ const margin = options.margin ?? defaultMargin;
193
+ const copyOf = new Map(edition.copies.map(copy => [copy.id, copy]));
194
+ const names = copies.map(id => copyOf.get(id).siglum ?? id);
195
+ const systems = copies.map(id => barOf(copyOf.get(id)).id);
196
+ const transfers = transfersOf();
197
+ const collation = collateCopies(edition, copies);
198
+ const matrix = presenceIn(edition, collation, margin);
199
+ const redundant = new Set(matrix.redundant);
200
+ const rows = collation.readings.map(reading => ({
201
+ presence: copies.map(copy => matrix.presence[reading.id][copy]),
202
+ redundant: redundant.has(reading.id)
203
+ }));
204
+ const search = searchStemmata(copies, names, systems, rows, costs, transfers, options.alternatives ?? 5);
205
+ const tree = search.best;
206
+ const size = tree.copy.length;
207
+ const place = (fixed) => {
208
+ const placement = new Map();
209
+ const failed = new Set();
210
+ collation.readings.forEach((reading, index) => {
211
+ const pinned = fixed.get(reading.id);
212
+ let states = statesOf(tree, rows[index].presence, rows[index].redundant, costs, pinned);
213
+ if (!states) {
214
+ failed.add(reading.id);
215
+ states = statesOf(tree, rows[index].presence, rows[index].redundant, costs);
216
+ }
217
+ placement.set(reading.id, states);
218
+ });
219
+ return { placement, failed };
220
+ };
221
+ const symbols = symbolsFor(edition, collation.readings);
222
+ const readingOf = new Map(Object.entries(symbols).map(([reading, symbol]) => [symbol, reading]));
223
+ // A first placement, free of the editor's edits, tells which version a lost node stands for.
224
+ const free = place(new Map());
225
+ recode(tree, collation.readings, free.placement);
226
+ const standsFor = new Map();
227
+ const claimed = new Set();
228
+ for (let v = 0; v < size; v++) {
229
+ const copy = tree.copy[v];
230
+ if (copy < 0)
231
+ continue;
232
+ const firstHand = versionsWitnessedBy(edition, copies[copy])
233
+ .find(witness => witness.by === 'carriers' && witness.through === undefined);
234
+ if (firstHand && !claimed.has(firstHand.version)) {
235
+ standsFor.set(v, firstHand.version);
236
+ claimed.add(firstHand.version);
237
+ }
238
+ }
239
+ const insertsAt = (placement, v) => collation.readings.filter(reading => inserting(tree, placement.get(reading.id), v)).map(reading => symbols[reading.id] ?? reading.id);
240
+ const lostMatches = [];
241
+ for (let v = 0; v < size; v++) {
242
+ if (tree.copy[v] >= 0)
243
+ continue;
244
+ const proposed = new Set(insertsAt(free.placement, v));
245
+ for (const version of edition.versions) {
246
+ if (claimed.has(version.id))
247
+ continue;
248
+ const stated = insertedBy(version).map(symbol => symbol.id);
249
+ const shared = stated.filter(id => proposed.has(id)).length;
250
+ const union = new Set([...stated, ...proposed]).size;
251
+ if (shared > 0)
252
+ lostMatches.push({ v, version: version.id, overlap: shared / union });
253
+ }
254
+ }
255
+ lostMatches.sort((a, b) => b.overlap - a.overlap || a.v - b.v);
256
+ for (const { v, version } of lostMatches) {
257
+ if (standsFor.has(v) || claimed.has(version))
258
+ continue;
259
+ standsFor.set(v, version);
260
+ claimed.add(version);
261
+ }
262
+ // The editor's edits hold their versions, as far as the copies allow.
263
+ const unpinned = [];
264
+ const pins = new Map();
265
+ const fixed = new Map();
266
+ const nodeOf = new Map([...standsFor].map(([v, version]) => [version, v]));
267
+ const meaningOf = new Map(collation.readings.map(reading => [reading.id, reading.meaning]));
268
+ if (options.pin !== false) {
269
+ for (const version of edition.versions) {
270
+ const established = editsOf(version).filter(edit => !isCollationsOwn(edit));
271
+ const v = nodeOf.get(version.id);
272
+ for (const edit of established) {
273
+ if (v === undefined) {
274
+ unpinned.push({ edit: edit.id, version: version.id, reason: 'version-gone' });
275
+ continue;
276
+ }
277
+ const inserted = (edit.insert ?? []).map(symbol => readingOf.get(symbol.id));
278
+ const deleted = (edit.delete ?? []).map(id => readingOf.get(id));
279
+ if ([...inserted, ...deleted].some(reading => reading === undefined)) {
280
+ unpinned.push({ edit: edit.id, version: version.id, reason: 'reading-gone' });
281
+ continue;
282
+ }
283
+ const bar = barNamed(tree.system[v]);
284
+ const hold = (reading, node, state) => {
285
+ const states = fixed.get(reading) ?? new Map();
286
+ states.set(node, state);
287
+ fixed.set(reading, states);
288
+ };
289
+ for (const reading of inserted) {
290
+ if (bar && !reads(bar, meaningOf.get(reading)))
291
+ continue;
292
+ hold(reading, v, true);
293
+ if (tree.parent[v] >= 0)
294
+ hold(reading, tree.parent[v], false);
295
+ }
296
+ for (const reading of deleted) {
297
+ if (bar && !reads(bar, meaningOf.get(reading)))
298
+ continue;
299
+ hold(reading, v, false);
300
+ if (tree.parent[v] >= 0)
301
+ hold(reading, tree.parent[v], true);
302
+ }
303
+ pins.set(v, [...(pins.get(v) ?? []), { edit: edit.id, readings: [...inserted, ...deleted] }]);
304
+ }
305
+ }
306
+ }
307
+ const { placement, failed } = place(fixed);
308
+ const held = new Set([...fixed.keys()].filter(reading => !failed.has(reading)));
309
+ const baseText = baseTextOf(tree, collation.readings, matrix, placement, held, movedWithin, systems);
310
+ recode(tree, collation.readings, placement);
311
+ for (const [v, edits] of pins) {
312
+ for (const { edit, readings } of edits) {
313
+ if (readings.some(reading => failed.has(reading))) {
314
+ unpinned.push({ edit, version: standsFor.get(v), reason: 'contradicts-copies' });
315
+ }
316
+ }
317
+ }
318
+ const released = new Set(unpinned.map(({ edit }) => edit));
319
+ // Pre-order numbering, so that the root is p0.
320
+ const order = fromRoot(tree);
321
+ const nameOf = new Map(order.map((v, index) => [v, `p${index}`]));
322
+ const versions = order.map((v) => {
323
+ const inserts = collation.readings.filter(reading => inserting(tree, placement.get(reading.id), v)).map(reading => reading.id);
324
+ const deletes = collation.readings.filter(reading => deleting(tree, placement.get(reading.id), v)).map(reading => reading.id);
325
+ const inserted = new Set(inserts);
326
+ const deleted = new Set(deletes);
327
+ const used = new Set();
328
+ const recodings = tree.parent[v] >= 0 && tree.transfer[v]
329
+ ? collation.recodings.filter(recoding => {
330
+ const fits = recoding.by.every(id => inserted.has(id) && !used.has(id))
331
+ && recoding.replaced.every(id => deleted.has(id) && !used.has(id));
332
+ if (fits)
333
+ [...recoding.by, ...recoding.replaced].forEach(id => used.add(id));
334
+ return fits;
335
+ })
336
+ : [];
337
+ const copy = tree.copy[v];
338
+ const inner = copy >= 0 && tree.below[v] !== 1 << copy;
339
+ const deviations = inner
340
+ ? collation.readings.filter(reading => placement.get(reading.id)[v] && matrix.presence[reading.id][copies[copy]] === 'absent').map(reading => reading.id)
341
+ : [];
342
+ return {
343
+ id: nameOf.get(v),
344
+ ...(copy >= 0 && { copy: copies[copy] }),
345
+ system: tree.system[v],
346
+ ...(tree.parent[v] >= 0 && { parent: nameOf.get(tree.parent[v]) }),
347
+ ...(standsFor.has(v) && { version: standsFor.get(v) }),
348
+ inserts,
349
+ deletes,
350
+ recodings,
351
+ deviations,
352
+ pinned: (pins.get(v) ?? []).map(({ edit }) => edit).filter(edit => !released.has(edit))
353
+ };
354
+ });
355
+ const conflicts = collation.readings
356
+ .map(reading => ({ reading: reading.id, versions: versions.filter(version => version.inserts.includes(reading.id)).map(version => version.id) }))
357
+ .filter(({ versions }) => versions.length > 1);
358
+ return {
359
+ copies,
360
+ costs,
361
+ movedWithin,
362
+ margin,
363
+ collation,
364
+ matrix,
365
+ versions,
366
+ notation: tree.notation,
367
+ cost: search.cost,
368
+ scored: search.scored,
369
+ alternatives: search.alternatives,
370
+ supports: search.supports,
371
+ conflicts,
372
+ baseText,
373
+ symbols,
374
+ unpinned
375
+ };
376
+ };
@@ -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;