linked-rolls 0.71.0 → 0.73.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,39 @@
1
+ /** Which reproducing system a version of the stemma is coded for, and which systems a roll was re-cut into. */
2
+ import { procedures } from "../model/procedures.js";
3
+ import { trackerBars } from "../systems/index.js";
4
+ const TRANSFER = /\/umstanzung\/(.+)-to-(.+)$/;
5
+ /**
6
+ * By system, the systems a roll of it was re-cut for, as the procedures
7
+ * of the vocabulary name them: a red roll was re-punched for the
8
+ * Licensee system and for the green one, and never the other way round.
9
+ * @category Filiation
10
+ */
11
+ export const transfersOf = () => {
12
+ const transfers = new Map();
13
+ for (const procedure of procedures) {
14
+ const match = TRANSFER.exec(procedure.id ?? "");
15
+ if (!match)
16
+ continue;
17
+ const [from, to] = [match[1], match[2]];
18
+ transfers.set(from, new Set([...(transfers.get(from) ?? []), to]));
19
+ }
20
+ return transfers;
21
+ };
22
+ /** Whether a version of the one system can stand above a version of the other: the same system, or one re-cut from it. */
23
+ export const leadsTo = (transfers, from, to) => from === to || (transfers.get(from)?.has(to) ?? false);
24
+ /**
25
+ * The system of a lost version above versions of the given systems: the
26
+ * one they share, or else the one they were all re-cut from. A lost
27
+ * state above a red and a Licensee version is red, since a Licensee
28
+ * version is never re-cut into red. Where no system leads to all of
29
+ * them, nothing.
30
+ * @category Filiation
31
+ */
32
+ export const systemAbove = (transfers, systems) => {
33
+ if (systems.size === 1)
34
+ return [...systems][0];
35
+ const candidates = new Set([...systems, ...transfers.keys()]);
36
+ return [...candidates].sort().find(candidate => [...systems].every(system => leadsTo(transfers, candidate, system)));
37
+ };
38
+ /** The bar of a system the library knows, by its identifier. */
39
+ export const barNamed = (id) => trackerBars.find(bar => bar.id === id);
@@ -0,0 +1,54 @@
1
+ /** Which copies carry the same text, and so one version between them. */
2
+ import { PresenceMatrix } from "./presence.js";
3
+ /**
4
+ * Copies taken to carry one text: copies of one system, punched from one
5
+ * master, that differ in no more readings than a transcription and the
6
+ * wear of a copy explain.
7
+ * @category Filiation
8
+ */
9
+ export interface SharedText {
10
+ /** The copies, in the order the proposal was given them. */
11
+ copies: string[];
12
+ /** The most readings any two of them differ in, where both can tell. */
13
+ differences: number;
14
+ /** Whether the edition says so: the copies carry one version at first hand there. */
15
+ stated: boolean;
16
+ }
17
+ /**
18
+ * Two copies of one system the proposal keeps apart, and how many
19
+ * readings they differ in, for the closest pairs: what was near being
20
+ * taken for one text.
21
+ * @category Filiation
22
+ */
23
+ export interface KeptApart {
24
+ copies: [string, string];
25
+ differences: number;
26
+ }
27
+ /**
28
+ * How many readings two copies of one system may differ in and still be
29
+ * taken to carry the same text, where nothing else is said. A version
30
+ * can differ from the one it derives from by a handful of edits: WM 225's
31
+ * L2 differs from R4 in seven readings. Copies punched from one master,
32
+ * transcribed with care, differ in fewer.
33
+ * @category Filiation
34
+ */
35
+ export declare const defaultSameText = 3;
36
+ /** In how many readings the two copies differ where both can tell: one has it, the other lacks it. */
37
+ export declare const differencesBetween: (matrix: PresenceMatrix, one: string, other: string) => number;
38
+ /**
39
+ * The copies in groups that carry the same text.
40
+ *
41
+ * Copies of different systems never carry one text, since a version is a
42
+ * reading in one system's words. Copies the edition gives one version at
43
+ * first hand stay together (`stated`). Beyond that, the two groups whose
44
+ * copies differ least are joined first, as long as every copy of the one
45
+ * differs from every copy of the other in no more than `within` readings
46
+ * where both can tell, so that a chain of small differences never joins
47
+ * copies far apart. Ties go to the order the copies were given in.
48
+ * `within` below zero joins only what the edition states.
49
+ * @category Filiation
50
+ */
51
+ export declare const sharedTexts: (matrix: PresenceMatrix, systems: readonly string[], within: number, stated?: readonly (readonly string[])[]) => {
52
+ texts: SharedText[];
53
+ apart: KeptApart[];
54
+ };
@@ -0,0 +1,96 @@
1
+ /**
2
+ * How many readings two copies of one system may differ in and still be
3
+ * taken to carry the same text, where nothing else is said. A version
4
+ * can differ from the one it derives from by a handful of edits: WM 225's
5
+ * L2 differs from R4 in seven readings. Copies punched from one master,
6
+ * transcribed with care, differ in fewer.
7
+ * @category Filiation
8
+ */
9
+ export const defaultSameText = 3;
10
+ /** In how many readings the two copies differ where both can tell: one has it, the other lacks it. */
11
+ export const differencesBetween = (matrix, one, other) => {
12
+ let count = 0;
13
+ for (const presence of Object.values(matrix.presence)) {
14
+ const a = presence[one];
15
+ const b = presence[other];
16
+ if ((a === 'present' && b === 'absent') || (a === 'absent' && b === 'present'))
17
+ count++;
18
+ }
19
+ return count;
20
+ };
21
+ /**
22
+ * The copies in groups that carry the same text.
23
+ *
24
+ * Copies of different systems never carry one text, since a version is a
25
+ * reading in one system's words. Copies the edition gives one version at
26
+ * first hand stay together (`stated`). Beyond that, the two groups whose
27
+ * copies differ least are joined first, as long as every copy of the one
28
+ * differs from every copy of the other in no more than `within` readings
29
+ * where both can tell, so that a chain of small differences never joins
30
+ * copies far apart. Ties go to the order the copies were given in.
31
+ * `within` below zero joins only what the edition states.
32
+ * @category Filiation
33
+ */
34
+ export const sharedTexts = (matrix, systems, within, stated = []) => {
35
+ const copies = matrix.copies;
36
+ const index = new Map(copies.map((copy, i) => [copy, i]));
37
+ const distance = copies.map((one, i) => copies.map((other, j) => i === j ? 0 : j < i ? NaN : differencesBetween(matrix, one, other)));
38
+ const between = (i, j) => i < j ? distance[i][j] : distance[j][i];
39
+ const groupOf = copies.map((_, i) => i);
40
+ const members = new Map(copies.map((_, i) => [i, [i]]));
41
+ const statedGroups = new Set();
42
+ const join = (a, b) => {
43
+ const [keep, drop] = a < b ? [a, b] : [b, a];
44
+ for (const i of members.get(drop))
45
+ groupOf[i] = keep;
46
+ members.set(keep, [...members.get(keep), ...members.get(drop)].sort((x, y) => x - y));
47
+ members.delete(drop);
48
+ };
49
+ for (const group of stated) {
50
+ const known = group.map(copy => index.get(copy)).filter((i) => i !== undefined);
51
+ for (const i of known.slice(1)) {
52
+ if (systems[i] !== systems[known[0]] || groupOf[i] === groupOf[known[0]])
53
+ continue;
54
+ join(groupOf[i], groupOf[known[0]]);
55
+ }
56
+ if (known.length > 1)
57
+ statedGroups.add(groupOf[known[0]]);
58
+ }
59
+ const pairs = [];
60
+ for (let i = 0; i < copies.length; i++) {
61
+ for (let j = i + 1; j < copies.length; j++) {
62
+ if (systems[i] === systems[j])
63
+ pairs.push({ i, j, d: between(i, j) });
64
+ }
65
+ }
66
+ pairs.sort((a, b) => a.d - b.d || a.i - b.i || a.j - b.j);
67
+ if (within >= 0) {
68
+ for (const { i, j, d } of pairs) {
69
+ if (d > within)
70
+ break;
71
+ const a = groupOf[i];
72
+ const b = groupOf[j];
73
+ if (a === b)
74
+ continue;
75
+ const close = members.get(a).every(x => members.get(b).every(y => between(x, y) <= within));
76
+ if (!close)
77
+ continue;
78
+ const wasStated = statedGroups.has(a) || statedGroups.has(b);
79
+ join(a, b);
80
+ if (wasStated)
81
+ statedGroups.add(Math.min(a, b));
82
+ }
83
+ }
84
+ const texts = [...members.entries()]
85
+ .sort(([a], [b]) => a - b)
86
+ .map(([group, list]) => ({
87
+ copies: list.map(i => copies[i]),
88
+ differences: Math.max(0, ...list.flatMap(x => list.filter(y => y > x).map(y => between(x, y)))),
89
+ stated: statedGroups.has(group)
90
+ }));
91
+ const apart = pairs
92
+ .filter(({ i, j }) => groupOf[i] !== groupOf[j])
93
+ .slice(0, 5)
94
+ .map(({ i, j, d }) => ({ copies: [copies[i], copies[j]], differences: d }));
95
+ return { texts, apart };
96
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Every stemma a handful of copies can stand in: rooted trees whose
3
+ * nodes are versions, a copy carrying any of them, a lost one above
4
+ * any two or more.
5
+ */
6
+ /**
7
+ * A node of a stemma: the copy carrying it at first hand, by its number,
8
+ * or none for a lost state, and the nodes derived from it.
9
+ */
10
+ export interface StemmaNode {
11
+ readonly copy?: number;
12
+ readonly children: readonly StemmaNode[];
13
+ }
14
+ /**
15
+ * Every stemma of n copies: 3 for two copies, 91,984 for six. They are
16
+ * produced one by one, so that only the leaf trees, far fewer, are held.
17
+ */
18
+ export declare function stemmataOf(n: number): Generator<StemmaNode>;
19
+ /** How many stemmata the exhaustive search scores, at most. */
20
+ export declare const MAX_COPIES = 7;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Every stemma a handful of copies can stand in: rooted trees whose
3
+ * nodes are versions, a copy carrying any of them, a lost one above
4
+ * any two or more.
5
+ */
6
+ const leaf = (copy) => ({ copy, children: [] });
7
+ /** Every way to add the copy to the tree: as a further child of a lost node, or on an edge, above any subtree. */
8
+ function* insertions(tree, copy) {
9
+ yield { children: [tree, leaf(copy)] };
10
+ if (tree.children.length === 0)
11
+ return;
12
+ yield { ...tree, children: [...tree.children, leaf(copy)] };
13
+ for (let i = 0; i < tree.children.length; i++) {
14
+ for (const child of insertions(tree.children[i], copy)) {
15
+ yield { ...tree, children: [...tree.children.slice(0, i), child, ...tree.children.slice(i + 1)] };
16
+ }
17
+ }
18
+ }
19
+ /** The trees whose leaves are the copies 0 to n − 1 and whose inner nodes are all lost, each once. */
20
+ const leafTrees = (n) => {
21
+ if (n === 1)
22
+ return [leaf(0)];
23
+ let trees = [{ children: [leaf(0), leaf(1)] }];
24
+ for (let copy = 2; copy < n; copy++) {
25
+ trees = trees.flatMap(tree => [...insertions(tree, copy)]);
26
+ }
27
+ return trees;
28
+ };
29
+ const product = (lists) => lists.reduce((combos, list) => combos.flatMap(combo => list.map(item => [...combo, item])), [[]]);
30
+ /**
31
+ * The tree with as many of its copies moved up as it allows: at each
32
+ * lost node, at most one copy among its leaf children may come to carry
33
+ * it instead, making that copy's text the ancestor of the others'.
34
+ * Every stemma arises from exactly one leaf tree in exactly one way.
35
+ */
36
+ function* promotions(tree) {
37
+ if (tree.children.length === 0) {
38
+ yield tree;
39
+ return;
40
+ }
41
+ const options = tree.children.map(child => [...promotions(child)]);
42
+ const leaves = tree.children.flatMap((child, index) => child.children.length === 0 ? [index] : []);
43
+ for (const combo of product(options)) {
44
+ yield { children: combo };
45
+ for (const index of leaves) {
46
+ yield { copy: tree.children[index].copy, children: combo.filter((_, i) => i !== index) };
47
+ }
48
+ }
49
+ }
50
+ const cache = new Map();
51
+ /**
52
+ * Every stemma of n copies: 3 for two copies, 91,984 for six. They are
53
+ * produced one by one, so that only the leaf trees, far fewer, are held.
54
+ */
55
+ export function* stemmataOf(n) {
56
+ let trees = cache.get(n);
57
+ if (!trees) {
58
+ trees = leafTrees(n);
59
+ cache.set(n, trees);
60
+ }
61
+ if (n === 1) {
62
+ yield trees[0];
63
+ return;
64
+ }
65
+ for (const tree of trees)
66
+ yield* promotions(tree);
67
+ }
68
+ /** How many stemmata the exhaustive search scores, at most. */
69
+ export const MAX_COPIES = 7;
package/lib/index.d.ts CHANGED
@@ -76,6 +76,7 @@ export { validate, type ValidateEdition } from './validate.js';
76
76
  export * from './analysis/constraints.js';
77
77
  export * from './collation/substitution.js';
78
78
  export * from './io/context.js';
79
+ export * from './filiation/index.js';
79
80
  export * from './analysis/sigla.js';
80
81
  export * from './model/notes.js';
81
82
  export { asJsonLd } from './io/asJsonLd.js';
package/lib/index.js CHANGED
@@ -39,6 +39,7 @@ export { validate } from './validate.js';
39
39
  export * from './analysis/constraints.js';
40
40
  export * from './collation/substitution.js';
41
41
  export * from './io/context.js';
42
+ export * from './filiation/index.js';
42
43
  export * from './analysis/sigla.js';
43
44
  export * from './model/notes.js';
44
45
  export { asJsonLd } from './io/asJsonLd.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linked-rolls",
3
- "version": "0.71.0",
3
+ "version": "0.73.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": {