linked-rolls 0.31.0 → 0.33.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.
@@ -32,8 +32,8 @@ export declare class EditionView {
32
32
  getPath(anyId: string): Path | undefined;
33
33
  linksTo(anyId: string): Path[];
34
34
  travelUp(versionId: string, callback: (version: Readonly<Version>) => void): void;
35
- /** The version and its ancestors, from the version up to the root. */
36
- private lineageOf;
35
+ /** The version and its ancestors along the principal line, from the version up to the root. */
36
+ lineageOf(versionId: string): Readonly<Version>[];
37
37
  carriersOf(symbol: AnySymbol): Readonly<AnyFeature>[];
38
38
  /** The copy a feature sits on, a patch and everything it bears included. */
39
39
  copyOf(featureId: string): Readonly<RollCopy> | undefined;
@@ -143,7 +143,7 @@ export class EditionView {
143
143
  this.travelUp(idOf(principal), callback);
144
144
  }
145
145
  }
146
- /** The version and its ancestors, from the version up to the root. */
146
+ /** The version and its ancestors along the principal line, from the version up to the root. */
147
147
  lineageOf(versionId) {
148
148
  const lineage = [];
149
149
  this.travelUp(versionId, version => lineage.push(version));
package/lib/Version.d.ts CHANGED
@@ -59,21 +59,21 @@ export interface VersionCreation {
59
59
  procedure?: Concept;
60
60
  }
61
61
  /**
62
- * A version is defined by the sum of edits applied
63
- * to the version it is based on. For simple identification,
64
- * a siglum is given to each version.
62
+ * A version is defined by the sum of edits applied to the version it is
63
+ * based on. What it is called is not stored with it: `siglaOf` reads the
64
+ * sigla off the stemma, so that a label never outlives the arrangement
65
+ * it describes.
65
66
  * @see lrmoo:F2 Expression
66
67
  */
67
68
  export interface Version extends WithId, WithType<'Version'> {
68
69
  /**
69
- * A short siglum to identify the version, e.g. "A" or "B2". A siglum
70
- * is given once and not changed: its letter names the line the version
71
- * was first recognised in, its number counts the versions admitted to
72
- * that line. Where later work places the version elsewhere in the
73
- * stemma, `basedOn` changes and the siglum stays.
70
+ * A label a document carries from before the sigla were computed.
71
+ * Nothing should write one here and nothing should show one: what a
72
+ * version is called comes from `siglaOf`.
73
+ * @deprecated
74
74
  * @see reo:siglum
75
75
  */
76
- siglum: string;
76
+ siglum?: string;
77
77
  /**
78
78
  * The reproducing system this version is coded for. One roll was
79
79
  * often issued for several of them, and a version is a reading in
@@ -21,7 +21,7 @@ export type EditionOp = (draft: Draft<Edition>) => void;
21
21
  * version is a reading in that system's words, so it is coded for the
22
22
  * system the copy was cut for.
23
23
  */
24
- export declare const createVersion: (siglum: string, copy: RollCopy) => EditionOp;
24
+ export declare const createVersion: (copy: RollCopy) => EditionOp;
25
25
  /**
26
26
  * Puts the copy into the edition without a version of its own, as for a
27
27
  * copy whose features are not read into symbols: one known only from a
package/lib/editionOps.js CHANGED
@@ -75,13 +75,12 @@ const dropInsertions = (version, symbolIds) => {
75
75
  * version is a reading in that system's words, so it is coded for the
76
76
  * system the copy was cut for.
77
77
  */
78
- export const createVersion = (siglum, copy) => draft => {
78
+ export const createVersion = (copy) => draft => {
79
79
  const bar = barOf(copy);
80
80
  draft.copies.push(copy);
81
81
  draft.versions.push({
82
82
  type: 'Version',
83
83
  id: v4(),
84
- siglum,
85
84
  system: systemOf(bar),
86
85
  edits: asSymbols(copy.features, bar).map(insertion),
87
86
  motivations: []
@@ -637,19 +636,6 @@ export const removeVersion = (view, versionId) => {
637
636
  };
638
637
  /** Takes the symbols out of the version's own insertions, and the edits that had nothing else. */
639
638
  export const removeSymbols = (versionId, symbolIds) => onVersion(versionId, version => dropInsertions(version, new Set(symbolIds)));
640
- /**
641
- * The siglum for a version derived from this one: the line the siglum
642
- * names, and the next number free in it. A number already given stays
643
- * given, whatever the stemma does later.
644
- */
645
- const nextInLine = (versions, siglum) => {
646
- const [line] = siglum.match(/^\D*/) ?? [siglum];
647
- const numbers = versions.flatMap(other => {
648
- const match = other.siglum.match(/^(\D*)(\d*)$/);
649
- return match && match[1] === line ? [Number(match[2] || 1)] : [];
650
- });
651
- return `${line}${Math.max(1, ...numbers) + 1}`;
652
- };
653
639
  /** Moves the edits into a new version based on this one. */
654
640
  export const deriveVersion = (versionId, editIds) => onVersion(versionId, (version, draft) => {
655
641
  const chosen = new Set(editIds);
@@ -659,7 +645,6 @@ export const deriveVersion = (versionId, editIds) => onVersion(versionId, (versi
659
645
  draft.versions.push({
660
646
  type: 'Version',
661
647
  id: v4(),
662
- siglum: nextInLine(draft.versions, version.siglum),
663
648
  system: stateOf(version).system,
664
649
  basedOn: [assignReference(versionId)],
665
650
  edits: moved,
package/lib/index.d.ts CHANGED
@@ -28,6 +28,8 @@ export * from './validate';
28
28
  export * from './constraints';
29
29
  export * from './substitution';
30
30
  export * from './context';
31
+ export * from './sigla';
32
+ export * from './notes';
31
33
  export * from './asJsonLd';
32
34
  export * from './importJsonLd';
33
35
  export * from './migrate';
package/lib/index.js CHANGED
@@ -28,6 +28,8 @@ export * from './validate';
28
28
  export * from './constraints';
29
29
  export * from './substitution';
30
30
  export * from './context';
31
+ export * from './sigla';
32
+ export * from './notes';
31
33
  export * from './asJsonLd';
32
34
  export * from './importJsonLd';
33
35
  export * from './migrate';
package/lib/notes.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ export type NotePart = {
2
+ type: 'text';
3
+ text: string;
4
+ } | {
5
+ type: 'reference';
6
+ id: string;
7
+ };
8
+ /** The note in the pieces it is shown in: stretches of text and what they refer to. */
9
+ export declare const partsOfNote: (note: string) => NotePart[];
10
+ /** The ids a note refers to, in the order it names them. */
11
+ export declare const referencesInNote: (note: string) => string[];
12
+ /**
13
+ * The note as it reads, with every reference resolved to the name the
14
+ * edition gives that entity. A reference nothing answers to keeps its id,
15
+ * so that the gap is visible rather than silent.
16
+ */
17
+ export declare const resolveNote: (note: string, nameOf: (id: string) => string | undefined) => string;
package/lib/notes.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * A note refers to a version or a copy instead of naming it, so that a
3
+ * label the stemma decides is never written into the prose. A reference
4
+ * reads `{{<id>}}`, where the id is the entity's, and is resolved when
5
+ * the note is shown.
6
+ */
7
+ const reference = /\{\{\s*([^{}\s]+)\s*\}\}/g;
8
+ /** The note in the pieces it is shown in: stretches of text and what they refer to. */
9
+ export const partsOfNote = (note) => {
10
+ const parts = [];
11
+ let read = 0;
12
+ for (const match of note.matchAll(reference)) {
13
+ const at = match.index ?? 0;
14
+ if (at > read)
15
+ parts.push({ type: 'text', text: note.slice(read, at) });
16
+ parts.push({ type: 'reference', id: match[1] });
17
+ read = at + match[0].length;
18
+ }
19
+ if (read < note.length)
20
+ parts.push({ type: 'text', text: note.slice(read) });
21
+ return parts;
22
+ };
23
+ /** The ids a note refers to, in the order it names them. */
24
+ export const referencesInNote = (note) => partsOfNote(note).flatMap(part => part.type === 'reference' ? [part.id] : []);
25
+ /**
26
+ * The note as it reads, with every reference resolved to the name the
27
+ * edition gives that entity. A reference nothing answers to keeps its id,
28
+ * so that the gap is visible rather than silent.
29
+ */
30
+ export const resolveNote = (note, nameOf) => partsOfNote(note)
31
+ .map(part => part.type === 'text' ? part.text : nameOf(part.id) ?? part.id)
32
+ .join('');
package/lib/schema.json CHANGED
@@ -2482,7 +2482,7 @@
2482
2482
  "type": "object"
2483
2483
  },
2484
2484
  "Version": {
2485
- "description": "A version is defined by the sum of edits applied to the version it is based on. For simple identification, a siglum is given to each version.",
2485
+ "description": "A version is defined by the sum of edits applied to the version it is based on. What it is called is not stored with it: `siglaOf` reads the sigla off the stemma, so that a label never outlives the arrangement it describes.",
2486
2486
  "properties": {
2487
2487
  "basedOn": {
2488
2488
  "description": "The versions this one is held to derive from, each under the belief it rests on. The text is read against the principal one (`principalDerivationOf`); the others stand as hypotheses, such as a contamination. A version that names none represents the mother roll.",
@@ -2513,7 +2513,8 @@
2513
2513
  "type": "array"
2514
2514
  },
2515
2515
  "siglum": {
2516
- "description": "A short siglum to identify the version, e.g. \"A\" or \"B2\". A siglum is given once and not changed: its letter names the line the version was first recognised in, its number counts the versions admitted to that line. Where later work places the version elsewhere in the stemma, `basedOn` changes and the siglum stays.",
2516
+ "deprecated": true,
2517
+ "description": "A label a document carries from before the sigla were computed. Nothing should write one here and nothing should show one: what a version is called comes from `siglaOf`.",
2517
2518
  "type": "string",
2518
2519
  "ontology": "reo:siglum"
2519
2520
  },
@@ -2536,7 +2537,6 @@
2536
2537
  "required": [
2537
2538
  "@id",
2538
2539
  "motivations",
2539
- "siglum",
2540
2540
  "system",
2541
2541
  "@type"
2542
2542
  ],
package/lib/sigla.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { Edition } from './Edition';
2
+ /**
3
+ * The siglum of every version, as the stemma stands. The letter names the
4
+ * reproducing system a version is coded for, the number counts the
5
+ * generations within that system, and a number after a dot marks a branch
6
+ * that leaves a generation. A version coded for another system than the
7
+ * one it derives from starts that system's next number, since nothing in
8
+ * that system precedes it.
9
+ *
10
+ * The main line runs through the child that has descendants of its own;
11
+ * where several have, the one with the most, and where none has, the line
12
+ * ends and the children hang off it as branches. So a siglum says where a
13
+ * version sits in this stemma, and it is computed anew when the stemma
14
+ * changes. Nothing should cite one without saying which state it belongs
15
+ * to.
16
+ */
17
+ export declare const siglaOf: (edition: Pick<Edition, 'versions'>) => ReadonlyMap<string, string>;
18
+ /** The siglum of one version as the stemma stands, or nothing where the edition holds no such version. */
19
+ export declare const siglumOf: (edition: Pick<Edition, 'versions'>, versionId: string) => string | undefined;
package/lib/sigla.js ADDED
@@ -0,0 +1,98 @@
1
+ import { idOf } from './Assumption';
2
+ import { systemIdOf } from './TrackerBar';
3
+ import { trackerBarOf } from './systems';
4
+ import { principalDerivationOf } from './Version';
5
+ /** The letter the versions of a system are labelled with. */
6
+ const letters = new Map([
7
+ ['welte-t100', 'R'],
8
+ ['welte-green', 'G'],
9
+ ['welte-licensee', 'L']
10
+ ]);
11
+ /** The letter of a version's system, from the systems the library knows, else from the system's name. */
12
+ const letterOf = (version) => {
13
+ const bar = trackerBarOf(version.system);
14
+ const known = bar && letters.get(bar.id);
15
+ if (known)
16
+ return known;
17
+ const name = (bar?.name ?? version.system?.name ?? '').replace(/[^A-Za-z]/g, '');
18
+ return (name.charAt(0) || 'X').toUpperCase();
19
+ };
20
+ /**
21
+ * The siglum of every version, as the stemma stands. The letter names the
22
+ * reproducing system a version is coded for, the number counts the
23
+ * generations within that system, and a number after a dot marks a branch
24
+ * that leaves a generation. A version coded for another system than the
25
+ * one it derives from starts that system's next number, since nothing in
26
+ * that system precedes it.
27
+ *
28
+ * The main line runs through the child that has descendants of its own;
29
+ * where several have, the one with the most, and where none has, the line
30
+ * ends and the children hang off it as branches. So a siglum says where a
31
+ * version sits in this stemma, and it is computed anew when the stemma
32
+ * changes. Nothing should cite one without saying which state it belongs
33
+ * to.
34
+ */
35
+ export const siglaOf = (edition) => {
36
+ const versions = edition.versions;
37
+ const byId = new Map(versions.map(version => [version.id, version]));
38
+ const parentOf = (version) => {
39
+ const principal = principalDerivationOf(version);
40
+ const parent = principal && byId.get(idOf(principal));
41
+ return parent === version ? undefined : parent;
42
+ };
43
+ const children = new Map();
44
+ const roots = [];
45
+ for (const version of versions) {
46
+ const parent = parentOf(version);
47
+ if (!parent)
48
+ roots.push(version);
49
+ else
50
+ children.set(parent.id, [...(children.get(parent.id) ?? []), version]);
51
+ }
52
+ const counted = new Map();
53
+ const descendants = (version) => {
54
+ const known = counted.get(version.id);
55
+ if (known !== undefined)
56
+ return known;
57
+ counted.set(version.id, 0);
58
+ const count = (children.get(version.id) ?? []).reduce((sum, child) => sum + 1 + descendants(child), 0);
59
+ counted.set(version.id, count);
60
+ return count;
61
+ };
62
+ /** The child the main line runs through: the one of the same system carrying most of the stemma. */
63
+ const mainChildOf = (version) => (children.get(version.id) ?? [])
64
+ .filter(child => systemIdOf(child.system) === systemIdOf(version.system))
65
+ .sort((one, other) => descendants(other) - descendants(one))[0];
66
+ const sigla = new Map();
67
+ const taken = new Map();
68
+ const branches = new Map();
69
+ const nextIn = (letter) => {
70
+ const number = (taken.get(letter) ?? 0) + 1;
71
+ taken.set(letter, number);
72
+ return `${letter}${number}`;
73
+ };
74
+ const branchOf = (version) => {
75
+ const number = (branches.get(version.id) ?? 0) + 1;
76
+ branches.set(version.id, number);
77
+ return `${sigla.get(version.id)}.${number}`;
78
+ };
79
+ const queue = roots.map(version => {
80
+ sigla.set(version.id, nextIn(letterOf(version)));
81
+ return { version, onMainLine: true };
82
+ });
83
+ while (queue.length > 0) {
84
+ const { version, onMainLine } = queue.shift();
85
+ const main = mainChildOf(version);
86
+ for (const child of children.get(version.id) ?? []) {
87
+ if (sigla.has(child.id))
88
+ continue;
89
+ const entersSystem = systemIdOf(child.system) !== systemIdOf(version.system);
90
+ const continuesLine = onMainLine && child === main;
91
+ sigla.set(child.id, entersSystem || continuesLine ? nextIn(letterOf(child)) : branchOf(version));
92
+ queue.push({ version: child, onMainLine: continuesLine });
93
+ }
94
+ }
95
+ return sigla;
96
+ };
97
+ /** The siglum of one version as the stemma stands, or nothing where the edition holds no such version. */
98
+ export const siglumOf = (edition, versionId) => siglaOf(edition).get(versionId);
@@ -18,11 +18,19 @@ export type { WelteT98Instrument, WelteT98InstrumentName } from "welte-mignon-em
18
18
  * difference between the two, in the printed ordinate both scales share, is how
19
19
  * far the transfer of a red reading onto the green mechanism succeeded.
20
20
  *
21
- * Neither has been fitted yet, so both groups are empty and what a playback runs
22
- * on until then is the third group: the **unfitted** starting values, arithmetic
23
- * from Welte's regulation controls and the T-100 consensus with no green roll
24
- * behind any of it. A curve produced with them says so in its own `instrument`
25
- * field, and nothing should be published from them.
21
+ * Both are now fitted, on `welte-mignon-emulator` 1.1.0: five genuine
22
+ * instruments, one per lined green roll that could be read, a consensus across
23
+ * them, and the derived instrument for the one recording issued on both scales.
24
+ * The **unfitted** starting values remain as a third group, arithmetic from
25
+ * Welte's regulation controls with no green roll behind any of it, and a curve
26
+ * produced with them says so in its own `instrument` field.
27
+ *
28
+ * **Prefer a roll's own instrument where the roll is one of the five.** The
29
+ * green instruments disagree with one another far more than the red ones do:
30
+ * the consensus is held out at 0.121 on the bass where the per-roll instruments
31
+ * are 0.029 to 0.066, because a shared mechanism describes none of them well.
32
+ * That spread is a property of the instruments rather than of the fit, and
33
+ * Gottschewski's finding that they were out of regulation is visible in it.
26
34
  */
27
35
  export declare const instruments: {
28
36
  genuine: Readonly<Partial<Record<"consensus" | import("welte-mignon-emulator/t98").GenuineRoll, import("welte-mignon-emulator/t98").WelteT98Instrument>>>;
@@ -112,8 +120,9 @@ export declare const secondsAt: (spool: Spool, place: Millimeters) => Seconds;
112
120
  * permanently open bore 100, and a long perforation on the bass sforzando-piano
113
121
  * line sends the roll back.
114
122
  *
115
- * Its constants are **not fitted**. `instruments.genuine` and
116
- * `instruments.derived` are empty until their fits run, and what a playback runs
117
- * on until then is the unfitted starting values, which every curve says.
123
+ * Its constants are fitted to the drawn nuance lines of five green rolls, and a
124
+ * playback runs on the consensus across them unless the caller names another
125
+ * instrument. Where the roll being played is one of the five, its own instrument
126
+ * is the better choice and `instruments.genuine` carries it.
118
127
  */
119
128
  export declare const welteT98System: ReproducingSystem<WelteT98Options>;
@@ -17,11 +17,19 @@ import { defaultVelocityMap, velocityOf } from "../velocity";
17
17
  * difference between the two, in the printed ordinate both scales share, is how
18
18
  * far the transfer of a red reading onto the green mechanism succeeded.
19
19
  *
20
- * Neither has been fitted yet, so both groups are empty and what a playback runs
21
- * on until then is the third group: the **unfitted** starting values, arithmetic
22
- * from Welte's regulation controls and the T-100 consensus with no green roll
23
- * behind any of it. A curve produced with them says so in its own `instrument`
24
- * field, and nothing should be published from them.
20
+ * Both are now fitted, on `welte-mignon-emulator` 1.1.0: five genuine
21
+ * instruments, one per lined green roll that could be read, a consensus across
22
+ * them, and the derived instrument for the one recording issued on both scales.
23
+ * The **unfitted** starting values remain as a third group, arithmetic from
24
+ * Welte's regulation controls with no green roll behind any of it, and a curve
25
+ * produced with them says so in its own `instrument` field.
26
+ *
27
+ * **Prefer a roll's own instrument where the roll is one of the five.** The
28
+ * green instruments disagree with one another far more than the red ones do:
29
+ * the consensus is held out at 0.121 on the bass where the per-roll instruments
30
+ * are 0.029 to 0.066, because a shared mechanism describes none of them well.
31
+ * That spread is a property of the instruments rather than of the fit, and
32
+ * Gottschewski's finding that they were out of regulation is visible in it.
25
33
  */
26
34
  export const instruments = { genuine: GENUINE, derived: DERIVED, unfitted: { 'starting-values': STARTING_VALUES } };
27
35
  export const instrumentNames = [
@@ -51,8 +59,8 @@ export const pedalPresets = {
51
59
  export const pedalPresetOf = (pedals) => Object.keys(pedalPresets).find(name => sameParameters(pedalPresets[name], pedals));
52
60
  export const defaultWelteT98Options = {
53
61
  spool: WELTE_T98_SPOOL,
54
- nuance: nuanceOf(STARTING_VALUES),
55
- instrument: { unfitted: 'starting-values' },
62
+ nuance: nuanceOf(GENUINE.consensus),
63
+ instrument: { genuine: 'consensus' },
56
64
  pedals: pedalPresets.damping,
57
65
  velocity: defaultVelocityMap,
58
66
  pedalMode: 'continuous',
@@ -233,9 +241,10 @@ const perform = (events, options, roll) => {
233
241
  * permanently open bore 100, and a long perforation on the bass sforzando-piano
234
242
  * line sends the roll back.
235
243
  *
236
- * Its constants are **not fitted**. `instruments.genuine` and
237
- * `instruments.derived` are empty until their fits run, and what a playback runs
238
- * on until then is the unfitted starting values, which every curve says.
244
+ * Its constants are fitted to the drawn nuance lines of five green rolls, and a
245
+ * playback runs on the consensus across them unless the caller names another
246
+ * instrument. Where the roll being played is one of the five, its own instrument
247
+ * is the better choice and `instruments.genuine` carries it.
239
248
  */
240
249
  export const welteT98System = {
241
250
  name: 'Welte-Mignon T98',
@@ -7,6 +7,13 @@ export interface Witness {
7
7
  copy: string;
8
8
  /** Whether the copy's features carry what the version inserts, or the copy states that it carries the version. */
9
9
  by: WitnessBy;
10
+ /**
11
+ * The later version this copy reaches the version through, where its
12
+ * features carry what a version derived from this one inserts. It then
13
+ * attests the text only as that later version passed it on. A copy
14
+ * that bears witness at first hand names none.
15
+ */
16
+ through?: string;
10
17
  /** How certainly a statement is held. Carriage through features is no statement and holds none. */
11
18
  certainty?: Certainty;
12
19
  /** The belief a statement rests on, where it states one. */
@@ -18,8 +25,17 @@ export interface Witness {
18
25
  * it, with the certainty each statement is held with.
19
26
  *
20
27
  * What a version inherits is carried by nearly every copy of the roll, so
21
- * only what it inserts tells its witnesses apart. A version whose edits
22
- * are not stated is witnessed by statement alone.
28
+ * only what it inserts tells its witnesses apart from its ancestors'. Its
29
+ * insertions are in turn inherited downwards, and the copies of the
30
+ * versions derived from it carry them as well. Those bear witness to it
31
+ * only through the latest state they carry, which `through` names; a copy
32
+ * carrying nothing a later version inserts speaks for the version at first
33
+ * hand. A version no copy attests at first hand survives, as a lost state
34
+ * does, in what its descendants passed on.
35
+ *
36
+ * A statement is the editor's own about the version it names and is
37
+ * reported as it stands. A version whose edits are not stated inserts
38
+ * nothing and is witnessed by statement alone.
23
39
  */
24
40
  export declare const witnessesOf: (view: EditionView, versionId: string) => Witness[];
25
41
  /**
package/lib/witnesses.js CHANGED
@@ -7,25 +7,36 @@ const copiesCarrying = (view, symbols) => new Set(symbols
7
7
  const copy = view.copyOf(id);
8
8
  return copy ? [copy.id] : [];
9
9
  }));
10
- /**
11
- * The copies that bear witness to the version: those whose features carry
12
- * a symbol the version's own edits insert, and those that state they carry
13
- * it, with the certainty each statement is held with.
14
- *
15
- * What a version inherits is carried by nearly every copy of the roll, so
16
- * only what it inserts tells its witnesses apart. A version whose edits
17
- * are not stated is witnessed by statement alone.
18
- */
19
- export const witnessesOf = (view, versionId) => {
20
- const version = view.get(versionId);
21
- if (!version)
22
- return [];
10
+ /** Every copy carrying what a version inserts, version by version, the copies in the order the edition lists them. */
11
+ const carriagesIn = (view) => view.edition.versions.flatMap(version => {
23
12
  const carrying = copiesCarrying(view, insertedBy(version));
24
- const byCarriers = view.edition.copies
13
+ const depth = view.lineageOf(version.id).length;
14
+ return view.edition.copies
25
15
  .filter(copy => carrying.has(copy.id))
26
- .map((copy) => ({ copy: copy.id, by: 'carriers' }));
16
+ .map((copy) => ({ copy: copy.id, version: version.id, depth }));
17
+ });
18
+ const carriersIn = (view) => {
19
+ const carriages = carriagesIn(view);
20
+ return {
21
+ copiesOf: new Map(view.edition.versions.map(version => [
22
+ version.id,
23
+ carriages.filter(carriage => carriage.version === version.id).map(carriage => carriage.copy)
24
+ ])),
25
+ // Written in order of depth, so a copy's latest carriage is set last and stands.
26
+ latestOf: new Map([...carriages]
27
+ .sort((one, other) => one.depth - other.depth)
28
+ .map(carriage => [carriage.copy, carriage.version]))
29
+ };
30
+ };
31
+ const witnessesIn = (carriers, view, versionId) => {
32
+ const carrying = carriers.copiesOf.get(versionId) ?? [];
33
+ const byCarriers = carrying.map((copy) => {
34
+ const latest = carriers.latestOf.get(copy);
35
+ return { copy, by: 'carriers', ...(latest !== undefined && latest !== versionId && { through: latest }) };
36
+ });
37
+ const carryingIt = new Set(carrying);
27
38
  const byStatement = view.edition.copies
28
- .filter(copy => !carrying.has(copy.id))
39
+ .filter(copy => !carryingIt.has(copy.id))
29
40
  .flatMap(copy => (copy.carries ?? [])
30
41
  .filter(statement => idOf(statement) === versionId)
31
42
  .map((statement) => {
@@ -34,13 +45,35 @@ export const witnessesOf = (view, versionId) => {
34
45
  }));
35
46
  return [...byCarriers, ...byStatement];
36
47
  };
48
+ /**
49
+ * The copies that bear witness to the version: those whose features carry
50
+ * a symbol the version's own edits insert, and those that state they carry
51
+ * it, with the certainty each statement is held with.
52
+ *
53
+ * What a version inherits is carried by nearly every copy of the roll, so
54
+ * only what it inserts tells its witnesses apart from its ancestors'. Its
55
+ * insertions are in turn inherited downwards, and the copies of the
56
+ * versions derived from it carry them as well. Those bear witness to it
57
+ * only through the latest state they carry, which `through` names; a copy
58
+ * carrying nothing a later version inserts speaks for the version at first
59
+ * hand. A version no copy attests at first hand survives, as a lost state
60
+ * does, in what its descendants passed on.
61
+ *
62
+ * A statement is the editor's own about the version it names and is
63
+ * reported as it stands. A version whose edits are not stated inserts
64
+ * nothing and is witnessed by statement alone.
65
+ */
66
+ export const witnessesOf = (view, versionId) => view.get(versionId) ? witnessesIn(carriersIn(view), view, versionId) : [];
37
67
  /**
38
68
  * The versions the copy bears witness to, each with how, in the order the
39
69
  * edition lists its versions.
40
70
  */
41
- export const versionsWitnessedBy = (view, copyId) => view.edition.versions.flatMap(version => witnessesOf(view, version.id)
42
- .filter(witness => witness.copy === copyId)
43
- .map(witness => ({ ...witness, version: version.id })));
71
+ export const versionsWitnessedBy = (view, copyId) => {
72
+ const carriers = carriersIn(view);
73
+ return view.edition.versions.flatMap(version => witnessesIn(carriers, view, version.id)
74
+ .filter(witness => witness.copy === copyId)
75
+ .map(witness => ({ ...witness, version: version.id })));
76
+ };
44
77
  /**
45
78
  * Where a copy's statement that it carries a version cannot stand: one
46
79
  * made although the copy's features carry symbols, which say by
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linked-rolls",
3
- "version": "0.31.0",
3
+ "version": "0.33.0",
4
4
  "description": "Digital editions of piano rolls: import, collation, editorial assumptions, JSON-LD export, and emulation through a reproducing system",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -51,10 +51,10 @@
51
51
  "typescript": "^7.0.2",
52
52
  "vite": "^8.2.2",
53
53
  "vitest": "^5.0.0",
54
- "welte-mignon-emulator": "^1.0.0"
54
+ "welte-mignon-emulator": "^1.1.0"
55
55
  },
56
56
  "peerDependencies": {
57
- "welte-mignon-emulator": "^1.0.0"
57
+ "welte-mignon-emulator": "^1.1.0"
58
58
  },
59
59
  "peerDependenciesMeta": {
60
60
  "welte-mignon-emulator": {