linked-rolls 0.19.1 → 0.21.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 (49) hide show
  1. package/README.md +31 -6
  2. package/lib/Collation.d.ts +1 -3
  3. package/lib/Collation.js +9 -11
  4. package/lib/Edition.d.ts +1 -9
  5. package/lib/EditionView.d.ts +54 -6
  6. package/lib/EditionView.js +93 -20
  7. package/lib/Emulation.js +5 -4
  8. package/lib/ReproducingSystem.d.ts +38 -2
  9. package/lib/RollCopy.d.ts +25 -2
  10. package/lib/RollCopy.js +30 -4
  11. package/lib/TrackerBar.d.ts +57 -2
  12. package/lib/TrackerBar.js +35 -17
  13. package/lib/Version.d.ts +49 -1
  14. package/lib/asJsonLd.js +23 -8
  15. package/lib/constraints.d.ts +8 -19
  16. package/lib/constraints.js +71 -15
  17. package/lib/context.d.ts +10 -1
  18. package/lib/context.js +10 -1
  19. package/lib/editionOps.d.ts +3 -1
  20. package/lib/editionOps.js +77 -15
  21. package/lib/importJsonLd.js +4 -2
  22. package/lib/index.d.ts +1 -0
  23. package/lib/index.js +1 -0
  24. package/lib/migrate.js +85 -11
  25. package/lib/readers/phillipsEroll.d.ts +4 -6
  26. package/lib/readers/phillipsEroll.js +6 -7
  27. package/lib/readers/spencerBar.d.ts +4 -5
  28. package/lib/readers/spencerBar.js +7 -8
  29. package/lib/readers/stanfordAton.d.ts +4 -7
  30. package/lib/readers/stanfordAton.js +4 -5
  31. package/lib/reservations.d.ts +1 -1
  32. package/lib/reservations.js +24 -2
  33. package/lib/schema.json +27 -5
  34. package/lib/spec/context.json +1 -0
  35. package/lib/spec/welte-licensee.context.json +10 -0
  36. package/lib/spec/welte-t98.context.json +10 -0
  37. package/lib/substitution.d.ts +73 -0
  38. package/lib/substitution.js +175 -0
  39. package/lib/systems/velocity.d.ts +34 -0
  40. package/lib/systems/velocity.js +36 -0
  41. package/lib/systems/welteLicensee/system.d.ts +46 -0
  42. package/lib/systems/welteLicensee/system.js +60 -0
  43. package/lib/systems/welteT100/system.d.ts +19 -16
  44. package/lib/systems/welteT100/system.js +42 -47
  45. package/lib/systems/welteT98/bar.d.ts +15 -2
  46. package/lib/systems/welteT98/bar.js +31 -3
  47. package/lib/systems/welteT98/system.d.ts +119 -0
  48. package/lib/systems/welteT98/system.js +245 -0
  49. package/package.json +12 -4
package/lib/TrackerBar.js CHANGED
@@ -1,9 +1,19 @@
1
- import { track } from "./Quantity";
1
+ import { mm, track } from "./Quantity";
2
+ /**
3
+ * What a position says, as a key. Two bars read the same thing exactly
4
+ * where their keys agree, which is what lets a symbol cross from one
5
+ * system to another and what decides whether two symbols collate.
6
+ */
7
+ export const keyOf = (meaning) => meaning.type === 'note'
8
+ ? `note ${meaning.pitch}`
9
+ : `expression ${meaning.scope} ${meaning.expressionType}`;
2
10
  const SYSTEM_IRI = 'https://w3id.org/reo/type/system/';
3
11
  /** The roll system a tracker bar belongs to, as the roll metadata states it. */
4
12
  export const systemOf = (bar) => ({ id: SYSTEM_IRI + bar.id, name: bar.name, sameAs: [] });
13
+ /** The identifier of a system the type vocabulary knows, from the IRI naming it. */
14
+ export const systemIdIn = (id) => id?.startsWith(SYSTEM_IRI) ? id.slice(SYSTEM_IRI.length) : undefined;
5
15
  /** The identifier of a system the type vocabulary knows, from its concept. */
6
- export const systemIdOf = (system) => system?.id?.startsWith(SYSTEM_IRI) ? system.id.slice(SYSTEM_IRI.length) : undefined;
16
+ export const systemIdOf = (system) => systemIdIn(system?.id);
7
17
  const areasOf = ({ notes, trackCount }) => [
8
18
  { role: 'bass-expression', from: track(1), to: track(notes.from - 1) },
9
19
  { role: 'note', from: track(notes.from), to: track(notes.to) },
@@ -28,41 +38,49 @@ export const describeTrackerBar = (spec) => {
28
38
  return undefined;
29
39
  return { type: 'expression', expressionType, scope: scopeOf(role) };
30
40
  };
41
+ const positions = new Map(Array.from({ length: spec.trackCount }, (_, i) => track(i + 1))
42
+ .flatMap(position => {
43
+ const meaning = meaningOf(position);
44
+ return meaning ? [[keyOf(meaning), position]] : [];
45
+ }));
31
46
  const rewind = spec.rewindTrack
32
47
  ?? [...spec.expressions].find(([, type]) => type === 'Rewind')?.[0];
33
48
  if (rewind === undefined) {
34
49
  throw new Error(`${spec.name} declares no rewind track`);
35
50
  }
51
+ // A rewind on a line of its own is unambiguous; one sharing a line is only
52
+ // the rewind when it is far longer than that line's usual command.
53
+ const shared = spec.rewindTrack !== undefined;
54
+ const endsAt = (features) => {
55
+ const hold = spec.rewindHold ?? mm(0);
56
+ const candidates = features
57
+ .filter(feature => feature.vertical.from === rewind)
58
+ .filter(feature => !shared || feature.horizontal.to - feature.horizontal.from >= hold)
59
+ .map(feature => feature.horizontal.from);
60
+ return candidates.length ? { at: mm(Math.min(...candidates)), because: 'rewind' } : undefined;
61
+ };
36
62
  return {
37
63
  id: spec.id,
38
64
  name: spec.name,
39
65
  width: spec.width,
40
66
  trackCount: spec.trackCount,
67
+ endsAt,
41
68
  areas,
42
69
  expressionTypes: [...new Set(spec.expressions.values())],
43
70
  rewindTrack: track(rewind),
44
71
  ...(spec.paperSpeed && { paperSpeed: spec.paperSpeed }),
45
72
  meaningOf,
73
+ positionOf: meaning => positions.get(keyOf(meaning)),
46
74
  roleOf
47
75
  };
48
76
  };
49
- const keyOf = (meaning) => meaning.type === 'note' ? `note ${meaning.pitch}` : `${meaning.scope} ${meaning.expressionType}`;
50
- const positionsOf = (bar) => Array.from({ length: bar.trackCount }, (_, i) => track(i + 1));
51
77
  /**
52
78
  * Puts a position of one bar onto the position of another that reads
53
79
  * the same thing, or nowhere when the other bar does not read it. This
54
- * is how a copy cut for one system takes its place in an edition of
55
- * another, as a Licensee re-cut does in an edition of a T-100 roll.
80
+ * is how a copy read in one system's numbering is put into another's,
81
+ * as the migration does for a Licensee copy stored on T-100 tracks.
56
82
  */
57
- export const translationBetween = (from, to) => {
58
- const positions = new Map();
59
- positionsOf(to).forEach(position => {
60
- const meaning = to.meaningOf(position);
61
- if (meaning)
62
- positions.set(keyOf(meaning), position);
63
- });
64
- return position => {
65
- const meaning = from.meaningOf(position);
66
- return meaning ? positions.get(keyOf(meaning)) : undefined;
67
- };
83
+ export const translationBetween = (from, to) => (position) => {
84
+ const meaning = from.meaningOf(position);
85
+ return meaning && to.positionOf(meaning);
68
86
  };
package/lib/Version.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Edit } from "./Edit";
2
- import { ReferenceAssumption } from "./Assumption";
2
+ import { Concept } from "./Agent";
3
+ import { ActorAssignment, DateAssignment, ReferenceAssumption } from "./Assumption";
3
4
  import { CollationTolerance } from "./Collation";
4
5
  import { AnySymbol } from "./Symbol";
5
6
  import { WithId, WithNote, WithType } from "./utils";
@@ -42,6 +43,37 @@ export type Derivation = ReferenceAssumption & {
42
43
  };
43
44
  /** The tolerance the derivation was collated at, or the default where it states none. */
44
45
  export declare const collationToleranceOf: (derivation: Readonly<Derivation>) => CollationTolerance;
46
+ /**
47
+ * How a version was made, where that is known and worth stating.
48
+ *
49
+ * A roll issued for another system was re-punched by an editor of the
50
+ * publisher's, and that was an editorial act rather than a conversion:
51
+ * Lawson names Kähle as the man who corrected second masters for the
52
+ * green system. The rule he worked by is the procedure named here, and
53
+ * the version's edits carry it out, so the mechanical part of a
54
+ * transfer is stated once instead of being spelled out per note.
55
+ * @see lrmoo:F28 Expression Creation
56
+ */
57
+ export interface VersionCreation {
58
+ /**
59
+ * Who carried the act out. An `ObjectAssumption`, so an attribution
60
+ * can carry the belief it rests on and the reasons for it.
61
+ * @see crm:P14 carried out by
62
+ */
63
+ actor?: ActorAssignment;
64
+ /**
65
+ * When it took place.
66
+ * @see dcterms:date
67
+ */
68
+ date?: DateAssignment;
69
+ /**
70
+ * The rule followed, as a term of the vocabulary: the notes stand
71
+ * at their pitch, the expression is re-spelled in the other
72
+ * system's words.
73
+ * @see crm:P33 used specific technique
74
+ */
75
+ procedure?: Concept;
76
+ }
45
77
  /**
46
78
  * A version is defined by the sum of edits applied
47
79
  * to the version it is based on. For simple identification,
@@ -54,6 +86,16 @@ export interface Version extends WithId, WithType<'Version'> {
54
86
  * @see reo:siglum
55
87
  */
56
88
  siglum: string;
89
+ /**
90
+ * The reproducing system this version is coded for. One roll was
91
+ * often issued for several of them, and a version is a reading in
92
+ * one system's words: its expression types are that system's
93
+ * vocabulary and its notes sit on that bar's positions. A system
94
+ * the type vocabulary knows carries the IRI of its concept as
95
+ * `id`, from which the export takes the system's own context.
96
+ * @see crm:P2 has type
97
+ */
98
+ system: Concept;
57
99
  /**
58
100
  * Whether the version served as a master for reproductions
59
101
  * or exists on one copy only.
@@ -65,6 +107,12 @@ export interface Version extends WithId, WithType<'Version'> {
65
107
  * @see lrmoo:R76 is derivative of
66
108
  */
67
109
  basedOn?: Derivation;
110
+ /**
111
+ * The act that made this version, where it is known: who carried it
112
+ * out, when, and by what rule.
113
+ * @see lrmoo:R17i was created by
114
+ */
115
+ creation?: VersionCreation;
68
116
  /**
69
117
  * The list of edits that, applied to the base version, produce this version.
70
118
  * @see reo:involvedEdit
package/lib/asJsonLd.js CHANGED
@@ -1,4 +1,4 @@
1
- import { systemIdOf } from "./TrackerBar";
1
+ import { systemIdIn } from "./TrackerBar";
2
2
  export const exportDate = (date) => {
3
3
  const year = date.getFullYear();
4
4
  const month = String(date.getMonth() + 1).padStart(2, "0");
@@ -39,20 +39,35 @@ const asJsonLdEntity = (obj) => {
39
39
  return result;
40
40
  };
41
41
  /**
42
- * The context of the roll's reproducing system, which reads the
43
- * expression types as that system's terms.
42
+ * Gives every version the context of its own reproducing system, which
43
+ * reads its expression types as that system's terms.
44
+ *
45
+ * It sits on the version rather than on the edition because one edition
46
+ * may hold versions of several systems, and each system's context
47
+ * defines `expressionType` with its own `@vocab`. Two of them in one
48
+ * context array would leave every expression type in the document
49
+ * reading as whichever came last. An embedded context merges with the
50
+ * active one, so the shared prefixes survive.
44
51
  */
45
- const systemContextOf = (edition) => {
46
- const system = systemIdOf(edition.roll?.system);
47
- return system ? [`https://w3id.org/reo/${system}/context.jsonld`] : [];
52
+ const withSystemContexts = (node) => {
53
+ if (Array.isArray(node))
54
+ return node.map(withSystemContexts);
55
+ if (node === null || typeof node !== 'object')
56
+ return node;
57
+ const walked = Object.fromEntries(Object.entries(node).map(([key, value]) => [key, withSystemContexts(value)]));
58
+ const system = node['@type'] === 'Version'
59
+ ? systemIdIn(node.system?.['@id'])
60
+ : undefined;
61
+ return system
62
+ ? { '@context': `https://w3id.org/reo/${system}/context.jsonld`, ...walked }
63
+ : walked;
48
64
  };
49
65
  export const asJsonLd = (edition) => {
50
66
  // The context is the export's own; one carried in from an import must not override it.
51
- const { base, copies, '@context': carried, ...rest } = asJsonLdEntity(edition);
67
+ const { base, copies, '@context': carried, ...rest } = withSystemContexts(asJsonLdEntity(edition));
52
68
  return {
53
69
  '@context': [
54
70
  'https://w3id.org/reo/context.jsonld',
55
- ...systemContextOf(edition),
56
71
  {
57
72
  '@base': edition.base
58
73
  }
@@ -1,27 +1,16 @@
1
1
  import { EditionView } from "./EditionView";
2
- import { TrackerBar } from "./TrackerBar";
3
2
  export type ConstraintProblem = {
4
3
  version: string;
5
4
  symbol: string;
6
- problem: 'alignment-reference-missing' | 'before-reference-missing' | 'after-reference-missing' | 'placed-relative-to-itself' | 'placed-several-ways' | 'partner-missing' | 'paired-with-itself' | 'in-several-pairs' | 'pair-placed-on-both-sides';
5
+ problem: 'alignment-reference-missing' | 'before-reference-missing' | 'after-reference-missing' | 'placed-relative-to-itself' | 'placed-several-ways' | 'partner-missing' | 'paired-with-itself' | 'in-several-pairs' | 'pair-placed-on-both-sides' | 'type-not-on-the-bar' | 'carrier-on-another-track' | 'copies-disagree-on-the-paper';
7
6
  };
8
7
  /**
9
- * Where the placements and pairings of the edition cannot hold as
10
- * stated, version by version: a reference or partner absent from the
11
- * version, a perforation placed relative to itself or in several ways
12
- * at once, one claimed by several pairs, or a pair whose members are
13
- * both placed and so cannot keep their distance and follow their
14
- * references at once.
8
+ * Where the edition cannot hold as stated, version by version: a
9
+ * placement or pairing reference absent from the version, a perforation
10
+ * placed relative to itself or in several ways at once, one claimed by
11
+ * several pairs, a pair whose members are both placed and so cannot
12
+ * keep their distance and follow their references at once, an
13
+ * expression the version's own bar cannot read, and a carrier sitting
14
+ * on a track that does not say what its symbol says.
15
15
  */
16
16
  export declare const constraintProblems: (view: EditionView) => ConstraintProblem[];
17
- export type UnknownExpressionType = {
18
- version: string;
19
- symbol: string;
20
- expressionType: string;
21
- };
22
- /**
23
- * The expressions of the edition whose type the tracker bar does not
24
- * read, version by version. The roll names its system; a type from
25
- * another system, or a misspelt one, means nothing on it.
26
- */
27
- export declare const unknownExpressionTypes: (view: EditionView, bar: TrackerBar) => UnknownExpressionType[];
@@ -1,5 +1,8 @@
1
1
  import { idOf } from "./Assumption";
2
2
  import { isPerforation, pairsAmong, placementsOf } from "./Symbol";
3
+ import { keyOf } from "./TrackerBar";
4
+ import { trackerBarOf } from "./systems";
5
+ import { barOf } from "./RollCopy";
3
6
  const missingReference = {
4
7
  alignedWith: 'alignment-reference-missing',
5
8
  before: 'before-reference-missing',
@@ -39,25 +42,78 @@ const problemsIn = (version, perforations) => {
39
42
  ...missingPartners, ...selfPaired, ...inSeveralPairs, ...placedOnBothSides
40
43
  ];
41
44
  };
42
- /**
43
- * Where the placements and pairings of the edition cannot hold as
44
- * stated, version by version: a reference or partner absent from the
45
- * version, a perforation placed relative to itself or in several ways
46
- * at once, one claimed by several pairs, or a pair whose members are
47
- * both placed and so cannot keep their distance and follow their
48
- * references at once.
49
- */
50
- export const constraintProblems = (view) => view.edition.versions.flatMap(version => problemsIn(version.id, view.snapshot(version.id).filter(isPerforation)));
51
45
  const isExpression = (symbol) => symbol.type === 'expression';
52
46
  /**
53
- * The expressions of the edition whose type the tracker bar does not
54
- * read, version by version. The roll names its system; a type from
55
- * another system, or a misspelt one, means nothing on it.
47
+ * The expressions the version's own bar cannot read, its type belonging
48
+ * to another system or misspelt.
49
+ *
50
+ * This is what says a transfer between systems is unfinished. A green
51
+ * version connected to a red one inherits every red expression, and
52
+ * none of them means anything on a green machine, so each is reported
53
+ * until an edit deletes it and says what took its place. The list
54
+ * emptying is the proof that the transfer is complete.
56
55
  */
57
- export const unknownExpressionTypes = (view, bar) => {
56
+ const typesNotOnTheBar = (version, snapshot) => {
57
+ const bar = trackerBarOf(version.system);
58
+ if (!bar)
59
+ return [];
58
60
  const known = new Set(bar.expressionTypes);
59
- return view.edition.versions.flatMap(version => view.snapshot(version.id)
61
+ return snapshot
60
62
  .filter(isExpression)
61
63
  .filter(symbol => !known.has(symbol.expressionType))
62
- .map(symbol => ({ version: version.id, symbol: symbol.id, expressionType: symbol.expressionType })));
64
+ .map(symbol => ({ version: version.id, symbol: symbol.id, problem: 'type-not-on-the-bar' }));
65
+ };
66
+ /**
67
+ * Carriers whose track does not say what the symbol they carry says.
68
+ *
69
+ * A carrier stands as evidence for its symbol, so the bar of the copy
70
+ * it sits on must read its track as the symbol's own meaning. Since a
71
+ * copy cut for another system numbers its tracks differently, this is
72
+ * what tells a transfer apart from a miscalibration: a red hole on 47
73
+ * and a green one on 45 both say pitch 60, while a green copy one track
74
+ * out says 59. Collation across systems consults only the place, so
75
+ * without this nothing checks the tracks at all.
76
+ */
77
+ const carriersOffTheirMeaning = (view, version, perforations) => {
78
+ const misread = (carrier, symbol) => {
79
+ const copy = view.copyOf(carrier.id);
80
+ if (!copy)
81
+ return false;
82
+ const meaning = barOf(copy).meaningOf(carrier.vertical.from);
83
+ return !meaning || keyOf(meaning) !== keyOf(symbol);
84
+ };
85
+ return perforations
86
+ .filter(symbol => view.carriersOf(symbol).some(carrier => misread(carrier, symbol)))
87
+ .map(symbol => ({ version, symbol: symbol.id, problem: 'carrier-on-another-track' }));
63
88
  };
89
+ /**
90
+ * Where the copies of the version's own system disagree about the scale
91
+ * that put them on the edition's shared axis.
92
+ *
93
+ * That scale is what takes a place back to the paper the version's roll
94
+ * ran on, so a performance needs one number. Copies disagreeing about it
95
+ * is evidence about the copies, and averaging it away would hide both
96
+ * the disagreement and the fact that the playback rests on a guess.
97
+ */
98
+ const paperDisagreed = (view, version) => view.speedScalesIn(version).length > 1
99
+ ? [{ version: version.id, symbol: version.id, problem: 'copies-disagree-on-the-paper' }]
100
+ : [];
101
+ /**
102
+ * Where the edition cannot hold as stated, version by version: a
103
+ * placement or pairing reference absent from the version, a perforation
104
+ * placed relative to itself or in several ways at once, one claimed by
105
+ * several pairs, a pair whose members are both placed and so cannot
106
+ * keep their distance and follow their references at once, an
107
+ * expression the version's own bar cannot read, and a carrier sitting
108
+ * on a track that does not say what its symbol says.
109
+ */
110
+ export const constraintProblems = (view) => view.edition.versions.flatMap(version => {
111
+ const snapshot = view.snapshot(version.id);
112
+ const perforations = snapshot.filter(isPerforation);
113
+ return [
114
+ ...problemsIn(version.id, perforations),
115
+ ...typesNotOnTheBar(version, snapshot),
116
+ ...carriersOffTheirMeaning(view, version.id, perforations),
117
+ ...paperDisagreed(view, version)
118
+ ];
119
+ });
package/lib/context.d.ts CHANGED
@@ -1,3 +1,12 @@
1
1
  export { default as jsonLdContext } from './spec/context.json';
2
- /** The context of the Welte-Mignon T-100, added to an edition of a T-100 roll. */
2
+ /** The context of the Welte-Mignon T-100, added to a version coded for it. */
3
3
  export { default as welteT100JsonLdContext } from './spec/welte-t100.context.json';
4
+ /**
5
+ * The context of the Welte-Mignon Licensee. Its expression terms are
6
+ * the T-100's, the two systems differing in the tracker scale rather
7
+ * than in the coding, so it reads them out of the T-100's vocabulary
8
+ * rather than minting a second set of IRIs for the same commands.
9
+ */
10
+ export { default as welteLicenseeJsonLdContext } from './spec/welte-licensee.context.json';
11
+ /** The context of the Welte-Mignon T-98, added to an edition of a green roll. */
12
+ export { default as welteT98JsonLdContext } from './spec/welte-t98.context.json';
package/lib/context.js CHANGED
@@ -1,3 +1,12 @@
1
1
  export { default as jsonLdContext } from './spec/context.json';
2
- /** The context of the Welte-Mignon T-100, added to an edition of a T-100 roll. */
2
+ /** The context of the Welte-Mignon T-100, added to a version coded for it. */
3
3
  export { default as welteT100JsonLdContext } from './spec/welte-t100.context.json';
4
+ /**
5
+ * The context of the Welte-Mignon Licensee. Its expression terms are
6
+ * the T-100's, the two systems differing in the tracker scale rather
7
+ * than in the coding, so it reads them out of the T-100's vocabulary
8
+ * rather than minting a second set of IRIs for the same commands.
9
+ */
10
+ export { default as welteLicenseeJsonLdContext } from './spec/welte-licensee.context.json';
11
+ /** The context of the Welte-Mignon T-98, added to an edition of a green roll. */
12
+ export { default as welteT98JsonLdContext } from './spec/welte-t98.context.json';
@@ -17,7 +17,9 @@ import { AnyFeature, FeatureConditionAssignment } from "./Feature";
17
17
  export type EditionOp = (draft: Draft<Edition>) => void;
18
18
  /**
19
19
  * Puts the copy into the edition with a version of its own, which
20
- * inserts every symbol the tracker bar reads on the copy.
20
+ * inserts every symbol the copy's own tracker bar reads on it. The
21
+ * version is a reading in that system's words, so it is coded for the
22
+ * system the copy was cut for.
21
23
  */
22
24
  export declare const createVersion: (siglum: string, copy: RollCopy) => EditionOp;
23
25
  /**
package/lib/editionOps.js CHANGED
@@ -4,7 +4,10 @@ import { getAt } from "./EditionView";
4
4
  import { isPerforation, placementRelations } from "./Symbol";
5
5
  import { collationsOf, defaultCollationTolerance } from "./Collation";
6
6
  import { collationToleranceOf, insertedBy } from "./Version";
7
- import { asSymbols } from "./RollCopy";
7
+ import { asSymbols, barOf, isPaperStretch } from "./RollCopy";
8
+ import { systemOf } from "./TrackerBar";
9
+ import { substitutionsBetween } from "./substitution";
10
+ import { trackerBarOf } from "./systems";
8
11
  import { applyShift, applyScale, revertShift, revertScale } from "./alignment";
9
12
  import { assignReference, idOf } from "./Assumption";
10
13
  import { conditions as conditionsAllowed, featuresBorneBy, isGluedOn, withBorneFeatures } from "./Feature";
@@ -66,20 +69,23 @@ const dropInsertions = (version, symbolIds) => {
66
69
  };
67
70
  /**
68
71
  * Puts the copy into the edition with a version of its own, which
69
- * inserts every symbol the tracker bar reads on the copy.
72
+ * inserts every symbol the copy's own tracker bar reads on it. The
73
+ * version is a reading in that system's words, so it is coded for the
74
+ * system the copy was cut for.
70
75
  */
71
76
  export const createVersion = (siglum, copy) => draft => {
77
+ const bar = barOf(copy);
72
78
  draft.copies.push(copy);
73
79
  draft.versions.push({
74
80
  type: 'Version',
75
81
  id: v4(),
76
82
  siglum,
83
+ system: systemOf(bar),
77
84
  versionType: 'edition',
78
- edits: asSymbols(copy.features).map(insertion),
85
+ edits: asSymbols(copy.features, bar).map(insertion),
79
86
  motivations: []
80
87
  });
81
88
  };
82
- const isPaperStretch = (condition) => condition.conditionType === 'paper-stretch';
83
89
  /** States what the scale is put down to, in place of an earlier reading. */
84
90
  const readScale = (copy, reading) => {
85
91
  if (reading.cause === 'paper') {
@@ -451,6 +457,12 @@ const handOverCarriers = (view, draft, collations) => collations.forEach(({ symb
451
457
  const target = path && getAt(path, draft);
452
458
  target?.carriers.push(...symbol.carriers);
453
459
  });
460
+ /** Whether the two versions are coded for different reproducing systems. */
461
+ const differ = (child, parent) => {
462
+ const one = trackerBarOf(child?.system);
463
+ const other = trackerBarOf(parent?.system);
464
+ return one !== undefined && other !== undefined && one.id !== other.id;
465
+ };
454
466
  /**
455
467
  * Bases the child on the parent. A symbol of the child that collates
456
468
  * with one the parent hands down adds its carriers to that symbol; the
@@ -461,12 +473,33 @@ const handOverCarriers = (view, draft, collations) => collations.forEach(({ symb
461
473
  export const connectVersions = (view, childId, parentId, tolerance = defaultCollationTolerance) => {
462
474
  const inherited = view.snapshot(parentId);
463
475
  const own = view.snapshot(childId);
464
- const collations = collationsOf(own, inherited, symbol => view.dimensionOf(symbol), tolerance);
476
+ const locate = (symbol) => view.placeOf(symbol);
477
+ const collations = collationsOf(own, inherited, locate, tolerance);
465
478
  const collated = new Set(collations.map(({ symbol }) => symbol.id));
466
479
  const matched = new Set(collations.map(({ counterpart }) => counterpart.id));
480
+ /**
481
+ * Where the child is coded for another system, a held perforation of
482
+ * its own often stands for a latched pair of the parent's. Saying so
483
+ * as one edit is the transfer being carried out, and leaving the two
484
+ * apart would make the apparatus a list of unexplained losses beside
485
+ * a list of unexplained gains.
486
+ */
487
+ const substituted = differ(view.get(childId), view.get(parentId))
488
+ ? substitutionsBetween(own.filter(symbol => !collated.has(symbol.id)), inherited.filter(symbol => !matched.has(symbol.id)), locate, tolerance)
489
+ : [];
490
+ const paired = new Set(substituted.flatMap(({ replaced, by }) => [...by, ...replaced].map(symbol => symbol.id)));
467
491
  const edits = [
468
- ...own.filter(symbol => !collated.has(symbol.id)).map(insertion),
469
- ...inherited.filter(symbol => !matched.has(symbol.id)).map(symbol => deletion(symbol.id))
492
+ ...substituted.map(({ replaced, by }) => ({
493
+ type: 'edit',
494
+ id: v4(),
495
+ editType: 'replace-with-equivalent',
496
+ insert: [...by],
497
+ delete: replaced.map(symbol => symbol.id)
498
+ })),
499
+ ...own.filter(symbol => !collated.has(symbol.id) && !paired.has(symbol.id)).map(insertion),
500
+ ...inherited
501
+ .filter(symbol => !matched.has(symbol.id) && !paired.has(symbol.id))
502
+ .map(symbol => deletion(symbol.id))
470
503
  ];
471
504
  return onVersion(childId, (child, draft) => {
472
505
  handOverCarriers(view, draft, collations);
@@ -485,7 +518,7 @@ export const collateSymbols = (view, versionId, symbolIds, tolerance) => {
485
518
  return noChange;
486
519
  const chosen = new Set(symbolIds);
487
520
  const own = insertedIn([version]).filter(symbol => chosen.has(symbol.id));
488
- const collations = collationsOf(own, view.snapshot(idOf(version.basedOn)), symbol => view.dimensionOf(symbol), tolerance ?? collationToleranceOf(version.basedOn));
521
+ const collations = collationsOf(own, view.snapshot(idOf(version.basedOn)), symbol => view.placeOf(symbol), tolerance ?? collationToleranceOf(version.basedOn));
489
522
  const collated = new Set(collations.map(({ symbol }) => symbol.id));
490
523
  return onVersion(versionId, (version, draft) => {
491
524
  handOverCarriers(view, draft, collations);
@@ -526,6 +559,7 @@ export const deriveVersion = (versionId, editIds) => onVersion(versionId, (versi
526
559
  type: 'Version',
527
560
  id: v4(),
528
561
  siglum: `${version.siglum}_derived`,
562
+ system: stateOf(version).system,
529
563
  versionType: 'unicum',
530
564
  basedOn: assignReference(versionId),
531
565
  edits: moved,
@@ -534,25 +568,53 @@ export const deriveVersion = (versionId, editIds) => onVersion(versionId, (versi
534
568
  });
535
569
  const sameSequence = (a, b) => a.length === b.length && a.every((value, i) => value === b[i]);
536
570
  const expressionTypesOf = (symbols) => symbols.filter((symbol) => symbol.type === 'expression').map(symbol => symbol.expressionType);
537
- const accents = [['SlowCrescendoOn', 'SlowCrescendoOff'], ['ForzandoOn', 'ForzandoOff']];
571
+ /**
572
+ * How a single added accent is spelled. The red Welte latches a valve
573
+ * on and off again, the green holds one perforation for as long as the
574
+ * accent lasts, so the spelling is the system's and not the music's.
575
+ * Which of them a bar can spell decides which ones it is offered.
576
+ */
577
+ const ACCENTS = [
578
+ ['SlowCrescendoOn', 'SlowCrescendoOff'],
579
+ ['ForzandoOn', 'ForzandoOff'],
580
+ ['Crescendo'],
581
+ ['SforzandoForte']
582
+ ];
583
+ const accentsOn = (bar) => bar ? ACCENTS.filter(accent => accent.every(type => bar.expressionTypes.includes(type))) : [];
538
584
  const lengthOf = (span) => subtract(span.to, span.from);
539
585
  /** How far apart two onsets may lie for the one symbol to count as a replacement of the other. */
540
586
  const REPLACEMENT_TOLERANCE = mm(5);
541
587
  /** Shorten or prolong, where the inserted symbol starts about where the deleted one did. */
542
588
  const replacementType = (view, inserted, deleted) => {
543
- const after = view.dimensionOf(inserted)?.horizontal;
544
- const before = view.dimensionOf(deleted)?.horizontal;
589
+ const after = view.placeOf(inserted);
590
+ const before = view.placeOf(deleted);
545
591
  if (!after || !before || distance(after.from, before.from) >= REPLACEMENT_TOLERANCE)
546
592
  return undefined;
547
593
  return lengthOf(after) < lengthOf(before) ? 'shorten' : 'prolong';
548
594
  };
549
- /** A guess at what an edit does, from the symbols it exchanges. */
550
- const guessEditType = (view, edit) => {
595
+ /**
596
+ * A guess at what an edit does, from the symbols it exchanges and from
597
+ * the systems the version and the one it is based on are coded for.
598
+ */
599
+ const guessEditType = (view, versionId, edit) => {
551
600
  const inserts = edit.insert ?? [];
552
601
  const deletes = view.getAll(edit.delete ?? []);
553
602
  const inserted = expressionTypesOf(inserts);
554
603
  const deleted = expressionTypesOf(deletes);
555
- if (deleted.length === 0 && accents.some(accent => sameSequence(inserted, accent)))
604
+ const bar = trackerBarOf(view.get(versionId)?.system);
605
+ const parentBar = trackerBarOf(view.predecessorOf(versionId)?.system);
606
+ /**
607
+ * Where the version is coded for another system than its parent, an
608
+ * exchange of expression matter is the transfer being carried out:
609
+ * a red ForzandoOn and ForzandoOff pair giving way to one held green
610
+ * SforzandoForte says the same thing in the other system's words,
611
+ * which is what 'replace-with-equivalent' is for. Calling it a
612
+ * corrected error would say the editor made a mistake.
613
+ */
614
+ if (bar && parentBar && bar.id !== parentBar.id && inserted.length > 0 && deleted.length > 0) {
615
+ return 'replace-with-equivalent';
616
+ }
617
+ if (deleted.length === 0 && accentsOn(bar).some(accent => sameSequence(inserted, accent)))
556
618
  return 'additional-accent';
557
619
  if (inserted.length > 1 && sameSequence(inserted, deleted))
558
620
  return 'shift';
@@ -575,7 +637,7 @@ export const mergeEdits = (view, versionId, toMerge) => {
575
637
  insert: toMerge.flatMap(edit => edit.insert ?? []),
576
638
  delete: toMerge.flatMap(edit => edit.delete ?? [])
577
639
  };
578
- merged.editType = guessEditType(view, merged);
640
+ merged.editType = guessEditType(view, versionId, merged);
579
641
  const mergedIds = new Set(toMerge.map(edit => edit.id));
580
642
  return onVersion(versionId, version => {
581
643
  version.edits = [...version.edits.filter(edit => !mergedIds.has(edit.id)), merged];
@@ -23,10 +23,12 @@ const fromJsonLdValue = (value) => {
23
23
  /**
24
24
  * An entity with its keywords read as plain keys. The input is left as
25
25
  * it is. The `@type` of a value object names the datatype of the value,
26
- * not a class, and is dropped.
26
+ * not a class, and is dropped. So is a context: a version carries one
27
+ * naming its system's vocabulary, and it belongs to the serialisation
28
+ * rather than to the edition, which states the system as data.
27
29
  */
28
30
  const fromJsonLdEntity = (json) => {
29
- const { '@type': type, '@id': id, ...rest } = json;
31
+ const { '@type': type, '@id': id, '@context': context, ...rest } = json;
30
32
  const entity = Object.fromEntries(Object.entries(rest).map(([key, value]) => [key, fromJsonLdValue(value)]));
31
33
  if (type !== undefined && !('@value' in json))
32
34
  entity.type = type;
package/lib/index.d.ts CHANGED
@@ -25,6 +25,7 @@ export * from './editionOps';
25
25
  export * from './Emulation';
26
26
  export * from './validate';
27
27
  export * from './constraints';
28
+ export * from './substitution';
28
29
  export * from './context';
29
30
  export * from './asJsonLd';
30
31
  export * from './importJsonLd';
package/lib/index.js CHANGED
@@ -25,6 +25,7 @@ export * from './editionOps';
25
25
  export * from './Emulation';
26
26
  export * from './validate';
27
27
  export * from './constraints';
28
+ export * from './substitution';
28
29
  export * from './context';
29
30
  export * from './asJsonLd';
30
31
  export * from './importJsonLd';