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.
Files changed (143) hide show
  1. package/README.md +106 -5
  2. package/lib/analysis/constraints.d.ts +7 -0
  3. package/lib/analysis/constraints.js +1 -0
  4. package/lib/analysis/ownPaper.d.ts +5 -1
  5. package/lib/analysis/ownPaper.js +5 -1
  6. package/lib/analysis/paper.d.ts +22 -4
  7. package/lib/analysis/paper.js +6 -0
  8. package/lib/analysis/reservations.d.ts +24 -1
  9. package/lib/analysis/reservations.js +15 -1
  10. package/lib/analysis/sigla.d.ts +6 -1
  11. package/lib/analysis/sigla.js +5 -1
  12. package/lib/analysis/stemma.d.ts +6 -1
  13. package/lib/analysis/stemma.js +6 -1
  14. package/lib/analysis/text.d.ts +16 -2
  15. package/lib/analysis/text.js +16 -2
  16. package/lib/analysis/witnesses.d.ts +20 -1
  17. package/lib/analysis/witnesses.js +4 -0
  18. package/lib/collation/Collation.d.ts +47 -8
  19. package/lib/collation/Collation.js +29 -5
  20. package/lib/collation/alignment.d.ts +16 -3
  21. package/lib/collation/alignment.js +15 -3
  22. package/lib/collation/scatter.d.ts +50 -9
  23. package/lib/collation/scatter.js +11 -1
  24. package/lib/collation/statistics.d.ts +39 -9
  25. package/lib/collation/statistics.js +27 -6
  26. package/lib/collation/substitution.d.ts +5 -0
  27. package/lib/collation/substitution.js +4 -0
  28. package/lib/emulation/Emulation.d.ts +16 -1
  29. package/lib/emulation/Emulation.js +10 -0
  30. package/lib/emulation/negotiation.d.ts +1 -0
  31. package/lib/emulation/negotiation.js +1 -0
  32. package/lib/filiation/adoption.d.ts +47 -0
  33. package/lib/filiation/adoption.js +190 -0
  34. package/lib/filiation/changes.d.ts +33 -0
  35. package/lib/filiation/changes.js +38 -0
  36. package/lib/filiation/index.d.ts +12 -0
  37. package/lib/filiation/index.js +12 -0
  38. package/lib/filiation/presence.d.ts +50 -0
  39. package/lib/filiation/presence.js +122 -0
  40. package/lib/filiation/proposal.d.ts +132 -0
  41. package/lib/filiation/proposal.js +376 -0
  42. package/lib/filiation/readings.d.ts +108 -0
  43. package/lib/filiation/readings.js +281 -0
  44. package/lib/filiation/search.d.ts +113 -0
  45. package/lib/filiation/search.js +268 -0
  46. package/lib/filiation/systems.d.ts +21 -0
  47. package/lib/filiation/systems.js +39 -0
  48. package/lib/filiation/trees.d.ts +20 -0
  49. package/lib/filiation/trees.js +69 -0
  50. package/lib/index.d.ts +34 -2
  51. package/lib/index.js +3 -1
  52. package/lib/io/asJsonLd.d.ts +10 -0
  53. package/lib/io/asJsonLd.js +10 -0
  54. package/lib/io/context.d.ts +16 -2
  55. package/lib/io/context.js +16 -2
  56. package/lib/io/importJsonLd.d.ts +1 -0
  57. package/lib/io/importJsonLd.js +1 -0
  58. package/lib/io/migrate.d.ts +8 -0
  59. package/lib/io/migrate.js +8 -0
  60. package/lib/lookup/lookup.d.ts +29 -5
  61. package/lib/lookup/lookup.js +29 -5
  62. package/lib/lookup/paths.d.ts +23 -3
  63. package/lib/lookup/paths.js +17 -3
  64. package/lib/model/Agent.d.ts +28 -2
  65. package/lib/model/Agent.js +15 -1
  66. package/lib/model/Assumption.d.ts +96 -7
  67. package/lib/model/Assumption.js +65 -7
  68. package/lib/model/ConditionState.d.ts +1 -0
  69. package/lib/model/Edit.d.ts +10 -0
  70. package/lib/model/Edit.js +8 -0
  71. package/lib/model/Edition.d.ts +11 -0
  72. package/lib/model/Edition.js +1 -0
  73. package/lib/model/Feature.d.ts +74 -7
  74. package/lib/model/Feature.js +41 -3
  75. package/lib/model/FeatureSource.d.ts +14 -1
  76. package/lib/model/FeatureSource.js +7 -1
  77. package/lib/model/Perforator.d.ts +10 -0
  78. package/lib/model/Perforator.js +1 -0
  79. package/lib/model/Quantity.d.ts +160 -13
  80. package/lib/model/Quantity.js +110 -6
  81. package/lib/model/RollCopy.d.ts +74 -12
  82. package/lib/model/RollCopy.js +53 -10
  83. package/lib/model/Symbol.d.ts +38 -1
  84. package/lib/model/Symbol.js +16 -0
  85. package/lib/model/TrackCalibration.d.ts +10 -2
  86. package/lib/model/TrackCalibration.js +9 -2
  87. package/lib/model/Version.d.ts +24 -4
  88. package/lib/model/Version.js +19 -4
  89. package/lib/model/notes.d.ts +15 -2
  90. package/lib/model/notes.js +9 -2
  91. package/lib/model/procedures.d.ts +5 -1
  92. package/lib/model/procedures.js +5 -1
  93. package/lib/model/vocabulary.d.ts +6 -1
  94. package/lib/model/vocabulary.js +6 -1
  95. package/lib/ops/beliefs.d.ts +23 -1
  96. package/lib/ops/beliefs.js +23 -1
  97. package/lib/ops/collation.d.ts +3 -0
  98. package/lib/ops/collation.js +3 -0
  99. package/lib/ops/commands.d.ts +17 -2
  100. package/lib/ops/commands.js +17 -2
  101. package/lib/ops/copies.d.ts +37 -6
  102. package/lib/ops/copies.js +37 -6
  103. package/lib/ops/draft.d.ts +2 -0
  104. package/lib/ops/draft.js +1 -0
  105. package/lib/ops/edits.d.ts +5 -1
  106. package/lib/ops/edits.js +5 -1
  107. package/lib/ops/features.d.ts +12 -1
  108. package/lib/ops/features.js +7 -0
  109. package/lib/ops/versions.d.ts +15 -3
  110. package/lib/ops/versions.js +15 -3
  111. package/lib/readers/phillipsEroll.d.ts +12 -1
  112. package/lib/readers/phillipsEroll.js +6 -1
  113. package/lib/readers/spencerBar.d.ts +12 -1
  114. package/lib/readers/spencerBar.js +7 -1
  115. package/lib/readers/stanfordAton.d.ts +15 -0
  116. package/lib/readers/stanfordAton.js +9 -0
  117. package/lib/schema.json +10 -0
  118. package/lib/shared/utils.d.ts +12 -0
  119. package/lib/spec/context.json +4 -0
  120. package/lib/systems/ReproducingSystem.d.ts +46 -3
  121. package/lib/systems/TrackerBar.d.ts +45 -4
  122. package/lib/systems/TrackerBar.js +20 -3
  123. package/lib/systems/index.d.ts +9 -2
  124. package/lib/systems/index.js +9 -2
  125. package/lib/systems/velocity.d.ts +1 -0
  126. package/lib/systems/velocity.js +1 -0
  127. package/lib/systems/welte.d.ts +18 -3
  128. package/lib/systems/welte.js +9 -2
  129. package/lib/systems/welteLicensee/bar.d.ts +1 -0
  130. package/lib/systems/welteLicensee/bar.js +1 -0
  131. package/lib/systems/welteLicensee/system.d.ts +18 -0
  132. package/lib/systems/welteLicensee/system.js +17 -0
  133. package/lib/systems/welteT100/bar.d.ts +8 -0
  134. package/lib/systems/welteT100/bar.js +4 -0
  135. package/lib/systems/welteT100/system.d.ts +46 -5
  136. package/lib/systems/welteT100/system.js +29 -3
  137. package/lib/systems/welteT98/bar.d.ts +6 -0
  138. package/lib/systems/welteT98/bar.js +2 -0
  139. package/lib/systems/welteT98/system.d.ts +58 -5
  140. package/lib/systems/welteT98/system.js +42 -3
  141. package/lib/validate.d.ts +16 -0
  142. package/lib/validate.js +15 -0
  143. package/package.json +2 -1
@@ -0,0 +1,50 @@
1
+ /** Which copies carry which collated reading, which lack it, and where a copy cannot say. */
2
+ import { Edition } from "../model/Edition.js";
3
+ import { HorizontalSpan } from "../model/Feature.js";
4
+ import { Millimeters } from "../model/Quantity.js";
5
+ import { TrackerBar, TrackMeaning } from "../systems/TrackerBar.js";
6
+ import { CopyCollation } from "./readings.js";
7
+ /**
8
+ * What a copy says of a reading: it carries it, it lacks it, or it
9
+ * cannot tell, because it is torn there, its text does not reach so
10
+ * far, or its system has no word for the reading.
11
+ * @category Filiation
12
+ */
13
+ export type Presence = 'present' | 'absent' | 'unknown';
14
+ /**
15
+ * The presence of every collated reading in every copy, and the readings
16
+ * that are ineffective in a copy carrying them.
17
+ * @category Filiation
18
+ */
19
+ export interface PresenceMatrix {
20
+ copies: string[];
21
+ /** By reading, by copy. */
22
+ presence: Record<string, Record<string, Presence>>;
23
+ /** The readings that are ineffective where a copy carries them: a latched function set again, or cancelled again. */
24
+ redundant: string[];
25
+ }
26
+ /** How far beyond the end of its text, or the edge of a tear, a copy cannot say what it lacks. */
27
+ export declare const defaultMargin: import("../index.js").Quantity<"mm">;
28
+ /**
29
+ * The readings a latched function makes no use of in the text given: a
30
+ * command setting what is set already, or cancelling what is cancelled.
31
+ * Commands at one place are taken cancel first, as the valves take them.
32
+ * @category Filiation
33
+ */
34
+ export declare const ineffectiveAmong: (bar: TrackerBar, readings: readonly {
35
+ id: string;
36
+ meaning: TrackMeaning;
37
+ place: HorizontalSpan;
38
+ }[]) => Set<string>;
39
+ /**
40
+ * The presence of every reading in every copy of the collation.
41
+ *
42
+ * A copy lacking a reading is taken to lack it only where it could show
43
+ * it: inside its text, away from its tears, and in its own system's
44
+ * words. A copy re-cut for a system that holds what another latches
45
+ * carries a latched pair where one of its held commands stands for it;
46
+ * where it holds the same function nearby without standing for the pair,
47
+ * or where the pair was ineffective anyway, it cannot say.
48
+ * @category Filiation
49
+ */
50
+ export declare const presenceIn: (edition: Pick<Edition, 'copies'>, collation: CopyCollation, margin?: Millimeters) => PresenceMatrix;
@@ -0,0 +1,122 @@
1
+ import { barOf, tearsOf } from "../model/RollCopy.js";
2
+ import { mm } from "../model/Quantity.js";
3
+ import { reads } from "./readings.js";
4
+ /** How far beyond the end of its text, or the edge of a tear, a copy cannot say what it lacks. */
5
+ export const defaultMargin = mm(3);
6
+ /** How far from a held command of the same function a copy cannot say whether it holds a latched pair. */
7
+ const HELD_REACH = mm(5);
8
+ /** The function a latching command sets or cancels, and whether it sets it, or nothing for any other. */
9
+ const latchOf = (bar, meaning) => {
10
+ if (meaning.type !== 'expression')
11
+ return undefined;
12
+ const operation = bar.operationOf(meaning.expressionType);
13
+ if (!operation || operation.spelling === 'held')
14
+ return undefined;
15
+ return {
16
+ latch: operation.sided ? `${operation.operates} ${meaning.scope}` : operation.operates,
17
+ on: operation.spelling === 'on'
18
+ };
19
+ };
20
+ /** The function a command operates, the side included where it counts, whatever its spelling. */
21
+ const functionOf = (bar, meaning) => {
22
+ if (meaning.type !== 'expression')
23
+ return undefined;
24
+ const operation = bar.operationOf(meaning.expressionType);
25
+ return operation && (operation.sided ? `${operation.operates} ${meaning.scope}` : operation.operates);
26
+ };
27
+ /**
28
+ * The readings a latched function makes no use of in the text given: a
29
+ * command setting what is set already, or cancelling what is cancelled.
30
+ * Commands at one place are taken cancel first, as the valves take them.
31
+ * @category Filiation
32
+ */
33
+ export const ineffectiveAmong = (bar, readings) => {
34
+ const byLatch = new Map();
35
+ for (const reading of readings) {
36
+ const latch = latchOf(bar, reading.meaning);
37
+ if (!latch)
38
+ continue;
39
+ const commands = byLatch.get(latch.latch) ?? [];
40
+ commands.push({ id: reading.id, at: reading.place.from, on: latch.on });
41
+ byLatch.set(latch.latch, commands);
42
+ }
43
+ const ineffective = new Set();
44
+ for (const commands of byLatch.values()) {
45
+ commands.sort((a, b) => a.at - b.at || Number(a.on) - Number(b.on));
46
+ let on = false;
47
+ for (const command of commands) {
48
+ if (command.on === on)
49
+ ineffective.add(command.id);
50
+ on = command.on;
51
+ }
52
+ }
53
+ return ineffective;
54
+ };
55
+ const within = (at, span, margin) => at >= span.from - margin && at <= span.to + margin;
56
+ /**
57
+ * The presence of every reading in every copy of the collation.
58
+ *
59
+ * A copy lacking a reading is taken to lack it only where it could show
60
+ * it: inside its text, away from its tears, and in its own system's
61
+ * words. A copy re-cut for a system that holds what another latches
62
+ * carries a latched pair where one of its held commands stands for it;
63
+ * where it holds the same function nearby without standing for the pair,
64
+ * or where the pair was ineffective anyway, it cannot say.
65
+ * @category Filiation
66
+ */
67
+ export const presenceIn = (edition, collation, margin = defaultMargin) => {
68
+ const copyOf = new Map(edition.copies.map(copy => [copy.id, copy]));
69
+ const barFor = new Map(collation.copies.map(id => [id, barOf(copyOf.get(id))]));
70
+ const placeOf = new Map();
71
+ for (const id of collation.copies) {
72
+ for (const feature of copyOf.get(id).production?.produced ?? []) {
73
+ if ('horizontal' in feature && feature.horizontal)
74
+ placeOf.set(feature.id, feature.horizontal);
75
+ }
76
+ }
77
+ const redundant = new Set();
78
+ for (const copy of collation.copies) {
79
+ const carried = collation.readings.flatMap(reading => {
80
+ const index = reading.copies.indexOf(copy);
81
+ if (index < 0)
82
+ return [];
83
+ return [{ id: reading.id, meaning: reading.meaning, place: placeOf.get(reading.carriers[index]) ?? reading.place }];
84
+ });
85
+ ineffectiveAmong(barFor.get(copy), carried).forEach(id => redundant.add(id));
86
+ }
87
+ const standsFor = new Map();
88
+ for (const recoding of collation.recodings) {
89
+ for (const replaced of recoding.replaced)
90
+ standsFor.set(replaced, [...(standsFor.get(replaced) ?? []), ...recoding.by]);
91
+ }
92
+ const copiesOf = new Map(collation.readings.map(reading => [reading.id, new Set(reading.copies)]));
93
+ const heldBy = new Map(collation.copies.map(copy => [copy, collation.readings.filter(reading => reading.meaning.type === 'expression' && copiesOf.get(reading.id).has(copy))]));
94
+ const tears = new Map(collation.copies.map(copy => [copy, tearsOf(copyOf.get(copy)).map(tear => tear.horizontal)]));
95
+ const presenceOf = (reading, copy) => {
96
+ if (copiesOf.get(reading.id).has(copy))
97
+ return 'present';
98
+ const at = reading.place.from;
99
+ if (!within(at, collation.extent[copy], margin))
100
+ return 'unknown';
101
+ if (tears.get(copy).some(tear => within(at, tear, margin)))
102
+ return 'unknown';
103
+ const bar = barFor.get(copy);
104
+ if (reads(bar, reading.meaning))
105
+ return 'absent';
106
+ if (reading.meaning.type !== 'expression')
107
+ return 'unknown';
108
+ if ((standsFor.get(reading.id) ?? []).some(by => copiesOf.get(by)?.has(copy)))
109
+ return 'present';
110
+ if (redundant.has(reading.id))
111
+ return 'unknown';
112
+ const wanted = functionOf(barFor.get(reading.copies[0]), reading.meaning);
113
+ const held = heldBy.get(copy).some(other => functionOf(bar, other.meaning) === wanted
114
+ && at >= other.place.from - HELD_REACH && at <= other.place.to + HELD_REACH);
115
+ return held ? 'unknown' : 'absent';
116
+ };
117
+ const presence = {};
118
+ for (const reading of collation.readings) {
119
+ presence[reading.id] = Object.fromEntries(collation.copies.map(copy => [copy, presenceOf(reading, copy)]));
120
+ }
121
+ return { copies: [...collation.copies], presence, redundant: [...redundant].sort() };
122
+ };
@@ -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
+ };