linked-rolls 0.19.1 → 0.20.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.
@@ -13,9 +13,7 @@ export interface CollationTolerance {
13
13
  }
14
14
  export declare const defaultCollationTolerance: CollationTolerance;
15
15
  /** Where a symbol lies along the roll, as its carriers put it, or nothing for a symbol without a place. */
16
- export type Locate = (symbol: AnySymbol) => Readonly<{
17
- horizontal: HorizontalSpan;
18
- }> | undefined;
16
+ export type Locate = (symbol: AnySymbol) => Readonly<HorizontalSpan> | undefined;
19
17
  /**
20
18
  * Two symbols collate when they are of one kind, say the same thing
21
19
  * (pitch, or expression type and scope), and lie at about the same
package/lib/Collation.js CHANGED
@@ -1,17 +1,15 @@
1
+ import { keyOf } from "./TrackerBar";
1
2
  import { distance, mm } from "./Quantity";
2
3
  import { partitionPoint } from "./sorted";
3
4
  export const defaultCollationTolerance = { toleranceStart: mm(5), toleranceEnd: mm(5) };
4
5
  /**
5
6
  * What a symbol says, as a key: the pitch of a note, the type and scope
6
- * of an expression. Symbols collate within one key only.
7
+ * of an expression. Symbols collate within one key only. This is the
8
+ * same key the tracker bars are indexed by, so two symbols collate
9
+ * exactly where two bars would read them as the same thing, which is
10
+ * what carries a note across a transfer between systems.
7
11
  */
8
- const kindOf = (symbol) => {
9
- switch (symbol.type) {
10
- case 'note': return `note ${symbol.pitch}`;
11
- case 'expression': return `expression ${symbol.scope} ${symbol.expressionType}`;
12
- case 'text': return 'text';
13
- }
14
- };
12
+ const kindOf = (symbol) => symbol.type === 'text' ? 'text' : keyOf(symbol);
15
13
  const nearby = (here, there, tolerance) => distance(here.from, there.from) <= tolerance.toleranceStart
16
14
  && distance(here.to, there.to) <= tolerance.toleranceEnd;
17
15
  /**
@@ -22,12 +20,12 @@ const nearby = (here, there, tolerance) => distance(here.from, there.from) <= to
22
20
  export const isCollatable = (a, b, locate, tolerance = defaultCollationTolerance) => {
23
21
  if (kindOf(a) !== kindOf(b))
24
22
  return false;
25
- const here = locate(a)?.horizontal;
26
- const there = locate(b)?.horizontal;
23
+ const here = locate(a);
24
+ const there = locate(b);
27
25
  return here !== undefined && there !== undefined && nearby(here, there, tolerance);
28
26
  };
29
27
  const placed = (symbols, locate) => symbols.flatMap((symbol, index) => {
30
- const horizontal = locate(symbol)?.horizontal;
28
+ const horizontal = locate(symbol);
31
29
  return horizontal ? [{ symbol, index, horizontal }] : [];
32
30
  });
33
31
  const groupBy = (items, keyOf) => items.reduce((groups, item) => {
package/lib/Edition.d.ts CHANGED
@@ -2,7 +2,7 @@ import { RollCopy } from "./RollCopy";
2
2
  import { Version } from "./Version";
3
3
  import { CollationTolerance } from "./Collation";
4
4
  import { DateAssignment, ObjectAssumption } from "./Assumption";
5
- import { Concept, Editor, Person, Place } from "./Agent";
5
+ import { Editor, Person, Place } from "./Agent";
6
6
  import { RollTempo } from "./ReproducingSystem";
7
7
  /**
8
8
  * This type describes the creation of an edition,
@@ -91,14 +91,6 @@ export interface Roll {
91
91
  * @see dcterms:identifier
92
92
  */
93
93
  catalogueNumber: string;
94
- /**
95
- * The reproducing system the roll was cut for. A system the
96
- * type vocabulary knows carries the IRI of its concept as `id`,
97
- * from which the export takes the system's own context, so that
98
- * the expression types are read as that system's.
99
- * @see crm:P2 has type
100
- */
101
- system: Concept;
102
94
  /**
103
95
  * @see lrmoo:R19i was realised through
104
96
  */
@@ -1,8 +1,10 @@
1
1
  import { Edition } from "./Edition";
2
- import { HorizontalSpan, VerticalSpan, AnyFeature } from "./Feature";
2
+ import { HorizontalSpan, AnyFeature } from "./Feature";
3
3
  import { AnySymbol, Expression, Note } from "./Symbol";
4
4
  import { Version } from "./Version";
5
5
  import { NegotiatedEvent } from "./ReproducingSystem";
6
+ import { TrackerBar } from "./TrackerBar";
7
+ import { RollCopy } from "./RollCopy";
6
8
  import { Millimeters } from "./Quantity";
7
9
  export type Path = (string | number)[];
8
10
  export declare const getAt: <T>(path: Path, obj: unknown) => T | undefined;
@@ -20,6 +22,8 @@ export declare class EditionView {
20
22
  * Map from id to paths where it is referenced
21
23
  */
22
24
  private readonly links;
25
+ /** Built when a copy is first asked for, the walk not recording it. */
26
+ private copiesByFeature?;
23
27
  constructor(edition: Edition);
24
28
  atPath<T>(path: Path): T | null;
25
29
  indexObjects(): void;
@@ -31,11 +35,46 @@ export declare class EditionView {
31
35
  /** The version and its ancestors, from the version up to the root. */
32
36
  private lineageOf;
33
37
  carriersOf(symbol: AnySymbol): Readonly<AnyFeature>[];
38
+ /** The copy a feature sits on, a patch and everything it bears included. */
39
+ copyOf(featureId: string): Readonly<RollCopy> | undefined;
40
+ /**
41
+ * How a place on the edition's shared axis relates to the paper of
42
+ * this version: place × factor = millimetres of its own paper.
43
+ *
44
+ * Copies cut for different systems are scaled onto one axis so that
45
+ * they can be collated at all, which leaves a version of another
46
+ * system carrying places in the axis copy's millimetres. A
47
+ * performance needs the paper the roll actually ran on, and the
48
+ * factor is the inverse of the scale `alignCopy` recorded.
49
+ *
50
+ * It is read only from the copies of the version's own system, since
51
+ * under the shared axis a green version's notes are carried by red
52
+ * copies too and those say nothing about green paper. A copy whose
53
+ * scale is put down to its own paper having stretched is left out as
54
+ * well: that is a fact about the one exemplar, not about the speed
55
+ * the system's rolls were cut at. Where what remains disagrees,
56
+ * `constraintProblems` reports it rather than averaging it away.
57
+ */
58
+ toOwnPaperOf(version: Readonly<Version>): number | undefined;
59
+ /** The scales of the version's own copies that are not their own paper stretch. */
60
+ speedScalesIn(version: Readonly<Version>): number[];
61
+ /** The copies of the version's own system that carry any of its symbols. */
62
+ copiesOwning(version: Readonly<Version>): Readonly<RollCopy>[];
34
63
  predecessorOf(versionId: string): Readonly<Version> | undefined;
35
- dimensionOf(symbol: AnySymbol): Readonly<{
36
- horizontal: HorizontalSpan;
37
- vertical: VerticalSpan;
38
- }> | undefined;
64
+ /**
65
+ * Where along the roll the symbol lies, as its carriers put it, or
66
+ * nothing for a symbol no copy carries.
67
+ *
68
+ * Only the place is measured. Which track the symbol sits on is not
69
+ * a measurement but a question for a tracker bar, since a note of
70
+ * one pitch sits on exactly one position of a given bar: ask
71
+ * `bar.positionOf`. The carriers cannot answer it, because a copy
72
+ * cut for another system numbers its tracks differently, and one
73
+ * symbol may be carried by copies of both — averaging a red carrier
74
+ * on track 47 with a green one on 45 would give 46, a legal
75
+ * position a semitone away on either bar.
76
+ */
77
+ placeOf(symbol: AnySymbol): Readonly<HorizontalSpan> | undefined;
39
78
  /** Where the symbol begins, as the mean onset of its carriers, or nothing for a symbol without a place. */
40
79
  onsetOf(symbol: AnySymbol): Millimeters | undefined;
41
80
  /** The symbols by onset, those without a place first; symbols at one place keep their order. */
@@ -51,5 +90,14 @@ export declare class EditionView {
51
90
  withGenerations(): Array<Version & {
52
91
  generation: number;
53
92
  }>;
54
- simplifySymbol(symbol: Note | Expression): NegotiatedEvent | null;
93
+ /**
94
+ * The symbol as a performance needs it: where it lies, and the
95
+ * position the performing bar reads it on.
96
+ *
97
+ * Nothing where that bar reads nothing of it, which is the case a
98
+ * transfer between systems leaves behind: a red `ForzandoOn` a
99
+ * green version still inherits cannot be performed on a green
100
+ * machine, and an edit has yet to say what took its place.
101
+ */
102
+ simplifySymbol(symbol: Note | Expression, bar: TrackerBar): NegotiatedEvent | null;
55
103
  }
@@ -1,4 +1,7 @@
1
+ import { withBorneFeatures } from "./Feature";
1
2
  import { deletedBy, insertedBy } from "./Version";
3
+ import { systemIdOf } from "./TrackerBar";
4
+ import { isPaperStretch } from "./RollCopy";
2
5
  import { idOf, idsOf } from "./Assumption";
3
6
  import { mean } from "./Quantity";
4
7
  export const getAt = (path, obj) => {
@@ -58,6 +61,13 @@ export class EditionView {
58
61
  writable: true,
59
62
  value: new Map()
60
63
  });
64
+ /** Built when a copy is first asked for, the walk not recording it. */
65
+ Object.defineProperty(this, "copiesByFeature", {
66
+ enumerable: true,
67
+ configurable: true,
68
+ writable: true,
69
+ value: void 0
70
+ });
61
71
  this.edition = edition;
62
72
  this.indexObjects();
63
73
  }
@@ -141,35 +151,87 @@ export class EditionView {
141
151
  carriersOf(symbol) {
142
152
  return this.getAll(idsOf(symbol.carriers));
143
153
  }
154
+ /** The copy a feature sits on, a patch and everything it bears included. */
155
+ copyOf(featureId) {
156
+ if (!this.copiesByFeature) {
157
+ this.copiesByFeature = new Map(this.edition.copies.flatMap(copy => copy.features
158
+ .flatMap(withBorneFeatures)
159
+ .map(feature => [feature.id, copy])));
160
+ }
161
+ return this.copiesByFeature.get(featureId);
162
+ }
163
+ /**
164
+ * How a place on the edition's shared axis relates to the paper of
165
+ * this version: place × factor = millimetres of its own paper.
166
+ *
167
+ * Copies cut for different systems are scaled onto one axis so that
168
+ * they can be collated at all, which leaves a version of another
169
+ * system carrying places in the axis copy's millimetres. A
170
+ * performance needs the paper the roll actually ran on, and the
171
+ * factor is the inverse of the scale `alignCopy` recorded.
172
+ *
173
+ * It is read only from the copies of the version's own system, since
174
+ * under the shared axis a green version's notes are carried by red
175
+ * copies too and those say nothing about green paper. A copy whose
176
+ * scale is put down to its own paper having stretched is left out as
177
+ * well: that is a fact about the one exemplar, not about the speed
178
+ * the system's rolls were cut at. Where what remains disagrees,
179
+ * `constraintProblems` reports it rather than averaging it away.
180
+ */
181
+ toOwnPaperOf(version) {
182
+ const scales = this.speedScalesIn(version);
183
+ return scales.length === 1 ? 1 / scales[0] : undefined;
184
+ }
185
+ /** The scales of the version's own copies that are not their own paper stretch. */
186
+ speedScalesIn(version) {
187
+ return [...new Set(this.copiesOwning(version)
188
+ .filter(copy => !copy.conditions.some(isPaperStretch))
189
+ .map(copy => copy.measurements.scale)
190
+ .filter((scale) => scale !== undefined && scale > 0))];
191
+ }
192
+ /** The copies of the version's own system that carry any of its symbols. */
193
+ copiesOwning(version) {
194
+ const system = systemIdOf(version.system);
195
+ const carrying = new Set(this.snapshot(version.id)
196
+ .flatMap(symbol => idsOf(symbol.carriers))
197
+ .flatMap(id => {
198
+ const copy = this.copyOf(id);
199
+ return copy ? [copy.id] : [];
200
+ }));
201
+ return this.edition.copies.filter(copy => carrying.has(copy.id) && systemIdOf(copy.production?.system) === system);
202
+ }
144
203
  predecessorOf(versionId) {
145
204
  const v = this.get(versionId);
146
205
  if (!v?.basedOn)
147
206
  return;
148
207
  return this.get(idOf(v.basedOn));
149
208
  }
150
- dimensionOf(symbol) {
209
+ /**
210
+ * Where along the roll the symbol lies, as its carriers put it, or
211
+ * nothing for a symbol no copy carries.
212
+ *
213
+ * Only the place is measured. Which track the symbol sits on is not
214
+ * a measurement but a question for a tracker bar, since a note of
215
+ * one pitch sits on exactly one position of a given bar: ask
216
+ * `bar.positionOf`. The carriers cannot answer it, because a copy
217
+ * cut for another system numbers its tracks differently, and one
218
+ * symbol may be carried by copies of both — averaging a red carrier
219
+ * on track 47 with a green one on 45 would give 46, a legal
220
+ * position a semitone away on either bar.
221
+ */
222
+ placeOf(symbol) {
151
223
  const carriers = this.getAll(idsOf(symbol.carriers));
152
- if (carriers.length === 0) {
224
+ if (carriers.length === 0)
153
225
  return;
154
- }
155
- const farEnds = carriers.flatMap(carrier => carrier.vertical.to === undefined ? [] : [carrier.vertical.to]);
156
226
  return {
157
- horizontal: {
158
- unit: 'mm',
159
- from: mean(carriers.map(carrier => carrier.horizontal.from)),
160
- to: mean(carriers.map(carrier => carrier.horizontal.to))
161
- },
162
- vertical: {
163
- unit: 'track',
164
- from: mean(carriers.map(carrier => carrier.vertical.from)),
165
- ...(farEnds.length > 0 && { to: mean(farEnds) })
166
- }
227
+ unit: 'mm',
228
+ from: mean(carriers.map(carrier => carrier.horizontal.from)),
229
+ to: mean(carriers.map(carrier => carrier.horizontal.to))
167
230
  };
168
231
  }
169
232
  /** Where the symbol begins, as the mean onset of its carriers, or nothing for a symbol without a place. */
170
233
  onsetOf(symbol) {
171
- const carriers = this.carriersOf(symbol);
172
- return carriers.length > 0 ? mean(carriers.map(carrier => carrier.horizontal.from)) : undefined;
234
+ return this.placeOf(symbol)?.from;
173
235
  }
174
236
  /** The symbols by onset, those without a place first; symbols at one place keep their order. */
175
237
  inOrderOfPlace(symbols) {
@@ -231,13 +293,24 @@ export class EditionView {
231
293
  }));
232
294
  return withGen;
233
295
  }
234
- simplifySymbol(symbol) {
235
- const dim = this.dimensionOf(symbol);
236
- if (!symbol.carriers.length || !dim)
296
+ /**
297
+ * The symbol as a performance needs it: where it lies, and the
298
+ * position the performing bar reads it on.
299
+ *
300
+ * Nothing where that bar reads nothing of it, which is the case a
301
+ * transfer between systems leaves behind: a red `ForzandoOn` a
302
+ * green version still inherits cannot be performed on a green
303
+ * machine, and an edit has yet to say what took its place.
304
+ */
305
+ simplifySymbol(symbol, bar) {
306
+ const horizontal = this.placeOf(symbol);
307
+ const position = bar.positionOf(symbol);
308
+ if (!horizontal || position === undefined)
237
309
  return null;
238
310
  return {
239
311
  ...symbol,
240
- ...dim
312
+ horizontal,
313
+ vertical: { unit: 'track', from: position }
241
314
  };
242
315
  }
243
316
  }
package/lib/Emulation.js CHANGED
@@ -154,7 +154,7 @@ export class Emulation {
154
154
  this.negotiatedEvents =
155
155
  view.snapshot(version.id)
156
156
  .filter(isPerforation)
157
- .map(symbol => view.simplifySymbol(symbol))
157
+ .map(symbol => view.simplifySymbol(symbol, this.system.trackerBar))
158
158
  .filter(event => event !== null)
159
159
  .filter(inScope);
160
160
  if (this.negotiatedEvents.length === 0) {
package/lib/RollCopy.d.ts CHANGED
@@ -301,17 +301,32 @@ export interface RollCopy extends WithType<'RollCopy'>, WithId {
301
301
  */
302
302
  readFrom?: FeatureSource;
303
303
  }
304
+ /**
305
+ * Whether the condition is the stretch or shrinkage of the paper. A
306
+ * scale put down to this belongs to the one exemplar and says nothing
307
+ * about the speed its system's rolls were cut at.
308
+ */
309
+ export declare const isPaperStretch: (condition: RollConditionAssignment) => boolean;
310
+ /**
311
+ * The bar a copy is read by, which is the one it was cut for. A copy
312
+ * that names no system falls back to the T-100, as every copy was read
313
+ * before the systems were told apart; `reservationsAbout` says so.
314
+ */
315
+ export declare const barOf: (copy: Pick<RollCopy, 'production'>) => TrackerBar;
304
316
  /**
305
317
  * Reads the features of a copy as the tracker bar would read them.
306
318
  * Holes on a position the bar does not read carry no symbol and are
307
319
  * dropped, which is what happens physically as well.
320
+ *
321
+ * The bar is named rather than defaulted: a copy is read by its own
322
+ * bar, and reading a green copy with the red one is a silent semitone.
308
323
  */
309
- export declare function asSymbols(features: AnyFeature[], bar?: TrackerBar): AnySymbol[];
324
+ export declare function asSymbols(features: AnyFeature[], bar: TrackerBar): AnySymbol[];
310
325
  /**
311
326
  * The tracks a copy carries holes on that the tracker bar does not read.
312
327
  * A non-empty result usually means the scan is calibrated wrongly.
313
328
  */
314
- export declare function unreadTracks(features: AnyFeature[], bar?: TrackerBar): Map<Track, number>;
329
+ export declare function unreadTracks(features: AnyFeature[], bar: TrackerBar): Map<Track, number>;
315
330
  /**
316
331
  * Falls back to the way the scan geometry was reconstructed before the
317
332
  * calibration was recorded: the bass hard margin stood in for the phase
package/lib/RollCopy.js CHANGED
@@ -1,17 +1,33 @@
1
1
  import { v4 } from "uuid";
2
2
  import { welteT100 } from "./systems/welteT100/bar";
3
+ import { trackerBarOf } from "./systems";
3
4
  import { assignReference } from "./Assumption";
4
5
  import { px, track } from "./Quantity";
5
6
  export const rollConditions = [
6
7
  'general',
7
8
  'paper-stretch'
8
9
  ];
10
+ /**
11
+ * Whether the condition is the stretch or shrinkage of the paper. A
12
+ * scale put down to this belongs to the one exemplar and says nothing
13
+ * about the speed its system's rolls were cut at.
14
+ */
15
+ export const isPaperStretch = (condition) => condition.conditionType === 'paper-stretch';
16
+ /**
17
+ * The bar a copy is read by, which is the one it was cut for. A copy
18
+ * that names no system falls back to the T-100, as every copy was read
19
+ * before the systems were told apart; `reservationsAbout` says so.
20
+ */
21
+ export const barOf = (copy) => trackerBarOf(copy.production?.system) ?? welteT100;
9
22
  /**
10
23
  * Reads the features of a copy as the tracker bar would read them.
11
24
  * Holes on a position the bar does not read carry no symbol and are
12
25
  * dropped, which is what happens physically as well.
26
+ *
27
+ * The bar is named rather than defaulted: a copy is read by its own
28
+ * bar, and reading a green copy with the red one is a silent semitone.
13
29
  */
14
- export function asSymbols(features, bar = welteT100) {
30
+ export function asSymbols(features, bar) {
15
31
  return features
16
32
  .filter(feature => feature.type === 'Hole')
17
33
  .flatMap((feature) => {
@@ -29,7 +45,7 @@ export function asSymbols(features, bar = welteT100) {
29
45
  * The tracks a copy carries holes on that the tracker bar does not read.
30
46
  * A non-empty result usually means the scan is calibrated wrongly.
31
47
  */
32
- export function unreadTracks(features, bar = welteT100) {
48
+ export function unreadTracks(features, bar) {
33
49
  const counts = new Map();
34
50
  features
35
51
  .filter(feature => feature.type === 'Hole')
@@ -58,11 +58,26 @@ export interface TrackerBar {
58
58
  readonly paperSpeed?: SpeedMeasure;
59
59
  /** `undefined` for a position the bar does not read. */
60
60
  meaningOf(position: Track): TrackMeaning | undefined;
61
+ /**
62
+ * `meaningOf` inverted: the position this bar reads the meaning on,
63
+ * or `undefined` where it does not read it at all. A symbol's track
64
+ * is this rather than anything measured, since a note of one pitch
65
+ * sits on exactly one position of a given bar.
66
+ */
67
+ positionOf(meaning: TrackMeaning): Track | undefined;
61
68
  /** `undefined` for a position the bar does not read. */
62
69
  roleOf(position: Track): TrackRole | undefined;
63
70
  }
71
+ /**
72
+ * What a position says, as a key. Two bars read the same thing exactly
73
+ * where their keys agree, which is what lets a symbol cross from one
74
+ * system to another and what decides whether two symbols collate.
75
+ */
76
+ export declare const keyOf: (meaning: TrackMeaning) => string;
64
77
  /** The roll system a tracker bar belongs to, as the roll metadata states it. */
65
78
  export declare const systemOf: (bar: TrackerBar) => Concept;
79
+ /** The identifier of a system the type vocabulary knows, from the IRI naming it. */
80
+ export declare const systemIdIn: (id: string | undefined) => string | undefined;
66
81
  /** The identifier of a system the type vocabulary knows, from its concept. */
67
82
  export declare const systemIdOf: (system: Concept | undefined) => string | undefined;
68
83
  /**
@@ -97,7 +112,7 @@ export declare const describeTrackerBar: (spec: TrackerBarSpec) => TrackerBar;
97
112
  /**
98
113
  * Puts a position of one bar onto the position of another that reads
99
114
  * the same thing, or nowhere when the other bar does not read it. This
100
- * is how a copy cut for one system takes its place in an edition of
101
- * another, as a Licensee re-cut does in an edition of a T-100 roll.
115
+ * is how a copy read in one system's numbering is put into another's,
116
+ * as the migration does for a Licensee copy stored on T-100 tracks.
102
117
  */
103
118
  export declare const translationBetween: (from: TrackerBar, to: TrackerBar) => (position: Track) => Track | undefined;
package/lib/TrackerBar.js CHANGED
@@ -1,9 +1,19 @@
1
1
  import { 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,6 +38,11 @@ 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) {
@@ -43,26 +58,17 @@ export const describeTrackerBar = (spec) => {
43
58
  rewindTrack: track(rewind),
44
59
  ...(spec.paperSpeed && { paperSpeed: spec.paperSpeed }),
45
60
  meaningOf,
61
+ positionOf: meaning => positions.get(keyOf(meaning)),
46
62
  roleOf
47
63
  };
48
64
  };
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
65
  /**
52
66
  * Puts a position of one bar onto the position of another that reads
53
67
  * 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.
68
+ * is how a copy read in one system's numbering is put into another's,
69
+ * as the migration does for a Licensee copy stored on T-100 tracks.
56
70
  */
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
- };
71
+ export const translationBetween = (from, to) => (position) => {
72
+ const meaning = from.meaningOf(position);
73
+ return meaning && to.positionOf(meaning);
68
74
  };
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
  }