linked-rolls 0.63.0 → 0.65.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 (67) hide show
  1. package/README.md +13 -5
  2. package/lib/analysis/constraints.d.ts +2 -2
  3. package/lib/analysis/constraints.js +38 -32
  4. package/lib/analysis/ownPaper.d.ts +3 -3
  5. package/lib/analysis/ownPaper.js +3 -3
  6. package/lib/analysis/reservations.d.ts +2 -2
  7. package/lib/analysis/reservations.js +6 -6
  8. package/lib/analysis/sigla.d.ts +4 -5
  9. package/lib/analysis/sigla.js +4 -4
  10. package/lib/analysis/stemma.d.ts +22 -0
  11. package/lib/analysis/stemma.js +38 -0
  12. package/lib/analysis/text.d.ts +31 -0
  13. package/lib/analysis/text.js +62 -0
  14. package/lib/analysis/witnesses.d.ts +5 -5
  15. package/lib/analysis/witnesses.js +27 -22
  16. package/lib/collation/alignment.js +55 -20
  17. package/lib/collation/scatter.d.ts +6 -6
  18. package/lib/collation/scatter.js +15 -13
  19. package/lib/emulation/Emulation.d.ts +7 -7
  20. package/lib/emulation/Emulation.js +29 -27
  21. package/lib/emulation/negotiation.d.ts +2 -2
  22. package/lib/emulation/negotiation.js +3 -2
  23. package/lib/index.d.ts +4 -1
  24. package/lib/index.js +4 -1
  25. package/lib/io/asJsonLd.js +43 -38
  26. package/lib/io/importJsonLd.js +11 -1
  27. package/lib/io/migrate.js +20 -26
  28. package/lib/lookup/lookup.d.ts +32 -0
  29. package/lib/lookup/lookup.js +40 -0
  30. package/lib/lookup/paths.d.ts +11 -0
  31. package/lib/lookup/paths.js +78 -0
  32. package/lib/lookup/perState.d.ts +32 -0
  33. package/lib/lookup/perState.js +63 -0
  34. package/lib/model/Edit.d.ts +4 -0
  35. package/lib/model/Edition.d.ts +0 -13
  36. package/lib/model/Version.d.ts +7 -1
  37. package/lib/ops/beliefs.d.ts +1 -1
  38. package/lib/ops/beliefs.js +1 -1
  39. package/lib/ops/collation.d.ts +3 -4
  40. package/lib/ops/collation.js +38 -32
  41. package/lib/ops/commands.d.ts +4 -5
  42. package/lib/ops/commands.js +10 -10
  43. package/lib/ops/copies.js +11 -3
  44. package/lib/ops/draft.d.ts +12 -12
  45. package/lib/ops/draft.js +11 -13
  46. package/lib/ops/edits.d.ts +1 -2
  47. package/lib/ops/edits.js +13 -10
  48. package/lib/ops/features.d.ts +2 -2
  49. package/lib/ops/features.js +3 -2
  50. package/lib/ops/immutable.d.ts +7 -1
  51. package/lib/ops/immutable.js +22 -8
  52. package/lib/ops/index.d.ts +1 -1
  53. package/lib/ops/index.js +6 -0
  54. package/lib/ops/versions.d.ts +2 -3
  55. package/lib/ops/versions.js +6 -5
  56. package/lib/readers/phillipsEroll.js +1 -8
  57. package/lib/readers/stanfordAton.js +2 -6
  58. package/lib/schema.json +2 -33
  59. package/lib/systems/welte.d.ts +17 -0
  60. package/lib/systems/welte.js +17 -1
  61. package/lib/systems/welteT100/system.d.ts +1 -16
  62. package/lib/systems/welteT100/system.js +3 -17
  63. package/lib/systems/welteT98/system.d.ts +1 -12
  64. package/lib/systems/welteT98/system.js +3 -13
  65. package/package.json +1 -1
  66. package/lib/view/EditionView.d.ts +0 -103
  67. package/lib/view/EditionView.js +0 -329
@@ -1,6 +1,7 @@
1
1
  import { featuresOf, isTear, tearsOf } from "../model/RollCopy.js";
2
2
  import { defaultTrackerBar } from "../systems/index.js";
3
3
  import { add, mm, subtract, track } from "../model/Quantity.js";
4
+ import { partitionPoint } from "../shared/sorted.js";
4
5
  /** A place on the copy's own paper, on the edition's axis. */
5
6
  export const toAxis = (alignment) => (place) => alignment ? mm((place + alignment.shift.horizontal) * alignment.scale) : place;
6
7
  /** A place on the edition's axis, on the copy's own paper. */
@@ -217,12 +218,41 @@ const noteOnsets = (features, bar) => features
217
218
  .map(meaning => ({ pitch: meaning.pitch, at: feature.horizontal.from }));
218
219
  })
219
220
  .sort(byPlace);
221
+ /**
222
+ * The middle one of the values, the upper of the two where their number
223
+ * is even. It is selected rather than the values sorted (Hoare's FIND,
224
+ * as Wirth writes it): a long roll gives over a million slopes, and only
225
+ * the one in the middle is wanted. The values are reordered where they
226
+ * lie.
227
+ */
220
228
  const median = (values) => {
221
- const sorted = [...values].sort((x, y) => x - y);
222
- return sorted[Math.floor(sorted.length / 2)] ?? 0;
229
+ const middle = values.length >> 1;
230
+ let low = 0;
231
+ let high = values.length - 1;
232
+ while (low < high) {
233
+ const pivot = values[middle];
234
+ let i = low;
235
+ let j = high;
236
+ while (i <= j) {
237
+ while (values[i] < pivot)
238
+ i++;
239
+ while (pivot < values[j])
240
+ j--;
241
+ if (i <= j) {
242
+ const swapped = values[i];
243
+ values[i] = values[j];
244
+ values[j] = swapped;
245
+ i++;
246
+ j--;
247
+ }
248
+ }
249
+ if (j < middle)
250
+ low = i;
251
+ if (middle < i)
252
+ high = j;
253
+ }
254
+ return values[middle] ?? 0;
223
255
  };
224
- /** The slope of the line through two matches, or nothing where they share a place on the copy. */
225
- const slopeBetween = (m, n) => n.a === m.a ? [] : [(n.b - m.b) / (n.a - m.a)];
226
256
  /** Matches beyond this are thinned before the slopes are taken, whose number grows with the square. */
227
257
  const SLOPE_SAMPLE = 1500;
228
258
  /**
@@ -234,25 +264,27 @@ const SLOPE_SAMPLE = 1500;
234
264
  const robustLine = (matches) => {
235
265
  const step = Math.max(1, Math.ceil(matches.length / SLOPE_SAMPLE));
236
266
  const sample = matches.filter((_, i) => i % step === 0);
237
- const slopes = sample.flatMap((m, i) => sample.slice(i + 1).flatMap(n => slopeBetween(m, n)));
238
- if (slopes.length === 0)
267
+ const slopes = new Float64Array(sample.length * (sample.length - 1) / 2);
268
+ let count = 0;
269
+ sample.forEach((m, i) => {
270
+ for (let j = i + 1; j < sample.length; j++) {
271
+ const n = sample[j];
272
+ // Two matches at one place on the copy give no slope.
273
+ if (n.a !== m.a)
274
+ slopes[count++] = (n.b - m.b) / (n.a - m.a);
275
+ }
276
+ });
277
+ if (count === 0)
239
278
  return undefined;
240
- const slope = median(slopes);
241
- return { slope, intercept: median(matches.map(m => m.b - slope * m.a)) };
279
+ const slope = median(slopes.subarray(0, count));
280
+ return { slope, intercept: median(Float64Array.from(matches, m => m.b - slope * m.a)) };
242
281
  };
243
282
  const placed = (line, a) => line.slope * a + line.intercept;
244
283
  const residualOf = (line, match) => match.b - placed(line, match.a);
245
284
  /** Pitches in a row that pin down a place on a roll, provided the row occurs once. */
246
285
  const ANCHOR_LENGTH = 6;
247
286
  /** Every run of `length` pitches, keyed by the run, with the onsets it starts at. */
248
- const runsOf = (onsets, length) => {
249
- const runs = new Map();
250
- onsets.slice(0, Math.max(0, onsets.length - length + 1)).forEach((start, i) => {
251
- const key = onsets.slice(i, i + length).map(onset => onset.pitch).join(',');
252
- runs.set(key, [...(runs.get(key) ?? []), start]);
253
- });
254
- return runs;
255
- };
287
+ const runsOf = (onsets, length) => Map.groupBy(onsets.slice(0, Math.max(0, onsets.length - length + 1)), (_, i) => onsets.slice(i, i + length).map(onset => onset.pitch).join(','));
256
288
  /** The places a run of pitches found once on each roll ties together. */
257
289
  const anchors = (a, b) => {
258
290
  const runsB = runsOf(b, ANCHOR_LENGTH);
@@ -277,13 +309,16 @@ const pairedNearestFirst = (candidates) => {
277
309
  }
278
310
  return matches;
279
311
  };
312
+ /** Widens the window by a hair before it is searched, so that rounding in its bounds cannot leave out what the distance admits. */
313
+ const WINDOW_SLACK = 1e-9;
280
314
  /** Each onset of A paired with the nearest onset of B of its pitch within the window around where the line puts it. */
281
315
  const nearestMatches = (a, b, line, window) => {
282
- const bByPitch = new Map();
283
- b.forEach(onset => bByPitch.set(onset.pitch, [...(bByPitch.get(onset.pitch) ?? []), onset]));
284
316
  const candidates = a.flatMap(onset => {
285
317
  const expected = placed(line, onset.at);
286
- return (bByPitch.get(onset.pitch) ?? [])
318
+ const ofPitch = b.get(onset.pitch) ?? [];
319
+ const first = partitionPoint(ofPitch, other => other.at < expected - window - WINDOW_SLACK);
320
+ const end = partitionPoint(ofPitch, other => other.at <= expected + window + WINDOW_SLACK);
321
+ return ofPitch.slice(first, end)
287
322
  .map(other => ({ distance: Math.abs(other.at - expected), a: onset, b: other }))
288
323
  .filter(candidate => candidate.distance <= window);
289
324
  });
@@ -341,5 +376,5 @@ export function alignFeatures(rollA, rollB, barA = defaultTrackerBar, barB = bar
341
376
  const coarse = robustLine(anchors(a, b));
342
377
  if (!coarse)
343
378
  return undefined;
344
- return resultOf(WINDOWS.reduce(settled(a, b), { line: coarse, matches: [] }));
379
+ return resultOf(WINDOWS.reduce(settled(a, Map.groupBy(b, onset => onset.pitch)), { line: coarse, matches: [] }));
345
380
  }
@@ -1,9 +1,9 @@
1
1
  import { Belief } from "../model/Assumption.js";
2
2
  import { BothEnds, CollationTolerance, Displacement } from "./Collation.js";
3
- import { EditionView } from "../view/EditionView.js";
4
3
  import { Millimeters } from "../model/Quantity.js";
5
4
  import { Histogram, Spread, Tail } from "./statistics.js";
6
5
  import { AnySymbol } from "../model/Symbol.js";
6
+ import { Edition } from "../model/Edition.js";
7
7
  /**
8
8
  * How far the copies of a roll disagree about where a symbol lies, and
9
9
  * the collation tolerance that follows from it.
@@ -44,7 +44,7 @@ export interface Reading {
44
44
  * A symbol only one side carries measures nothing and is passed over,
45
45
  * which leaves out exactly the insertions and the deletions.
46
46
  */
47
- export declare const readingsOf: (view: EditionView, symbols: readonly Readonly<AnySymbol>[], copies: ReadonlySet<string>) => Reading[];
47
+ export declare const readingsOf: (edition: Edition, symbols: readonly Readonly<AnySymbol>[], copies: ReadonlySet<string>) => Reading[];
48
48
  /**
49
49
  * What sample a reading belongs to. Notes and expressions are taken
50
50
  * apart by default, since in the present material they scatter
@@ -66,7 +66,7 @@ export interface ScatterOptions {
66
66
  groupOf?: Grouping;
67
67
  /** The width of the histogram's bins. */
68
68
  binWidth?: Millimeters;
69
- /** The tolerance the edition states at present, against which each departure is reported as still admitted or not. */
69
+ /** The tolerance the derivation states at present, against which each departure is reported as still admitted or not. */
70
70
  stated?: CollationTolerance;
71
71
  }
72
72
  /** A reading the calculated tolerance does not admit. */
@@ -90,7 +90,7 @@ export interface Departure {
90
90
  */
91
91
  separatedBy: BothEnds<boolean>;
92
92
  /**
93
- * Whether the tolerance the edition states at present still admits
93
+ * Whether the tolerance the derivation states at present still admits
94
94
  * it. Absent where none was given to compare against.
95
95
  *
96
96
  * This sees one direction only. A departure is by definition a
@@ -166,7 +166,7 @@ export declare const departureThreshold: (n: number) => number;
166
166
  */
167
167
  export declare const scatterOf: (readings: readonly Reading[], options?: ScatterOptions) => Scatter[];
168
168
  /** How one copy's readings of a version's text scatter against the readings of the copies it is collated with. */
169
- export declare const scatterOfCopy: (view: EditionView, versionId: string, copyId: string, options?: ScatterOptions) => Scatter[];
169
+ export declare const scatterOfCopy: (edition: Edition, versionId: string, copyId: string, options?: ScatterOptions) => Scatter[];
170
170
  /**
171
171
  * Which side of a derivation a measurement was taken over: the version
172
172
  * derived, or the version it is read against.
@@ -256,7 +256,7 @@ export interface Sides {
256
256
  * neither side, which is the case where the edition has genuinely
257
257
  * stopped saying and an editor has to.
258
258
  */
259
- export declare const sidesOf: (view: EditionView, versionId: string) => Sides | undefined;
259
+ export declare const sidesOf: (edition: Edition, versionId: string) => Sides | undefined;
260
260
  /**
261
261
  * The belief a calculated tolerance rests on, to annotate the
262
262
  * derivation's `collationTolerance` with: an inference from the scatter
@@ -4,12 +4,14 @@ import { mean, mm } from "../model/Quantity.js";
4
4
  import { excessKurtosisOf, histogramOf, normalQuantile, spreadOf, standardise, tailOf } from "./statistics.js";
5
5
  import { groupBy } from "../shared/utils.js";
6
6
  import { deletedBy, insertedBy } from "../model/Version.js";
7
+ import { carriersOf, placedCarriersOf, snapshotOf } from "../analysis/text.js";
8
+ import { copyOfFeature, symbolsIn, versionIn } from "../lookup/lookup.js";
7
9
  const placeOf = (carriers) => ({
8
10
  from: mean(carriers.map(carrier => carrier.horizontal.from)),
9
11
  to: mean(carriers.map(carrier => carrier.horizontal.to))
10
12
  });
11
- const sitsOn = (view, copies) => (feature) => {
12
- const copy = view.copyOf(feature.id);
13
+ const sitsOn = (edition, copies) => (feature) => {
14
+ const copy = copyOfFeature(edition, feature.id);
13
15
  return copy !== undefined && copies.has(copy.id);
14
16
  };
15
17
  /**
@@ -34,10 +36,10 @@ const sitsOn = (view, copies) => (feature) => {
34
36
  * A symbol only one side carries measures nothing and is passed over,
35
37
  * which leaves out exactly the insertions and the deletions.
36
38
  */
37
- export const readingsOf = (view, symbols, copies) => {
38
- const tested = sitsOn(view, copies);
39
+ export const readingsOf = (edition, symbols, copies) => {
40
+ const tested = sitsOn(edition, copies);
39
41
  return symbols.flatMap((symbol) => {
40
- const carriers = view.placedCarriersOf(symbol);
42
+ const carriers = placedCarriersOf(edition, symbol);
41
43
  const here = carriers.filter(tested);
42
44
  const there = carriers.filter(carrier => !tested(carrier));
43
45
  if (here.length === 0 || there.length === 0)
@@ -120,7 +122,7 @@ const scatterIn = (group, sample, { binWidth = DEFAULT_BIN_WIDTH, stated }) => {
120
122
  export const scatterOf = (readings, options = {}) => [...groupBy(readings, reading => (options.groupOf ?? byWhatItIs)(reading.symbol))]
121
123
  .flatMap(([group, sample]) => scatterIn(group, sample, options));
122
124
  /** How one copy's readings of a version's text scatter against the readings of the copies it is collated with. */
123
- export const scatterOfCopy = (view, versionId, copyId, options = {}) => scatterOf(readingsOf(view, view.snapshot(versionId), new Set([copyId])), options);
125
+ export const scatterOfCopy = (edition, versionId, copyId, options = {}) => scatterOf(readingsOf(edition, snapshotOf(edition, versionId), new Set([copyId])), options);
124
126
  const covering = (windows) => {
125
127
  const lowest = Math.min(...windows.map(window => window.offset - window.tolerance));
126
128
  const highest = Math.max(...windows.map(window => window.offset + window.tolerance));
@@ -189,10 +191,10 @@ export const changesBetween = (readings, inForce, proposed) => ({
189
191
  separated: readings.filter(({ displacement }) => admits(inForce, displacement) && !admits(proposed, displacement)),
190
192
  merged: readings.filter(({ displacement }) => !admits(inForce, displacement) && admits(proposed, displacement))
191
193
  });
192
- const copiesBearing = (view, symbols) => {
194
+ const copiesBearing = (edition, symbols) => {
193
195
  const tally = symbols.reduce((counts, symbol) => {
194
- const bearers = new Set(view.carriersOf(symbol).flatMap(carrier => {
195
- const copy = view.copyOf(carrier.id);
196
+ const bearers = new Set(carriersOf(edition, symbol).flatMap(carrier => {
197
+ const copy = copyOfFeature(edition, carrier.id);
196
198
  return copy ? [copy.id] : [];
197
199
  }));
198
200
  bearers.forEach(copy => counts.set(copy, (counts.get(copy) ?? 0) + 1));
@@ -219,13 +221,13 @@ const copiesBearing = (view, symbols) => {
219
221
  * neither side, which is the case where the edition has genuinely
220
222
  * stopped saying and an editor has to.
221
223
  */
222
- export const sidesOf = (view, versionId) => {
223
- const version = view.version(versionId);
224
+ export const sidesOf = (edition, versionId) => {
225
+ const version = versionIn(edition, versionId);
224
226
  if (!version)
225
227
  return undefined;
226
228
  return {
227
- child: copiesBearing(view, insertedBy(version)),
228
- parent: copiesBearing(view, view.symbols(deletedBy(version)))
229
+ child: copiesBearing(edition, insertedBy(version)),
230
+ parent: copiesBearing(edition, symbolsIn(edition, deletedBy(version)))
229
231
  };
230
232
  };
231
233
  const inMillimetres = (value) => `${value.toFixed(2)} mm`;
@@ -1,9 +1,9 @@
1
1
  import { MidiFile } from "midifile-ts";
2
- import { EditionView } from "../view/EditionView.js";
3
2
  import type { TrackerBar } from "../systems/TrackerBar.js";
4
3
  import { Version } from "../model/Version.js";
5
4
  import { AnyPerformedRollFeature, EmulatedCurve, NegotiatedEvent, ReproducingSystem } from "../systems/ReproducingSystem.js";
6
5
  import { Millimeters } from "../model/Quantity.js";
6
+ import { Edition } from "../model/Edition.js";
7
7
  export type EmulationScope = {
8
8
  /** Only notes whose onset lies within this span of the roll are played. */
9
9
  range?: [Millimeters, Millimeters];
@@ -12,17 +12,17 @@ export type EmulationScope = {
12
12
  };
13
13
  /**
14
14
  * The events moved to where their statements put them, the events given
15
- * left as they were. The view supplies the copies, whose measurements
15
+ * left as they were. The edition supplies the copies, whose measurements
16
16
  * decide how far before or after its reference a command goes, and a
17
17
  * punch diameter, or a millimetre, where no copy agrees with a statement.
18
18
  */
19
- export declare const withPlacementsApplied: (view: EditionView, events: readonly NegotiatedEvent[]) => NegotiatedEvent[];
19
+ export declare const withPlacementsApplied: (edition: Edition, events: readonly NegotiatedEvent[]) => NegotiatedEvent[];
20
20
  /**
21
21
  * The commands of a version as the bar performs them, in order of place,
22
22
  * within the scope and with the placements applied. A note plays only
23
23
  * where its onset falls in the range; expressions play throughout.
24
24
  */
25
- export declare const negotiatedEventsOf: (view: EditionView, version: Readonly<Version>, bar: TrackerBar, { range }?: Pick<EmulationScope, 'range'>) => NegotiatedEvent[];
25
+ export declare const negotiatedEventsOf: (edition: Edition, version: Readonly<Version>, bar: TrackerBar, { range }?: Pick<EmulationScope, 'range'>) => NegotiatedEvent[];
26
26
  /** A version performed: what was negotiated, and what the system made of it. */
27
27
  export interface Emulated {
28
28
  /** The id of the version performed. */
@@ -36,7 +36,7 @@ export interface Emulated {
36
36
  * Performs a version on a reproducing system: the symbols are negotiated
37
37
  * into placed events and the system plays them. Nothing given is changed.
38
38
  */
39
- export declare const emulate: <Options extends object>(system: ReproducingSystem<Options>, version: Readonly<Version>, view: EditionView, options?: Options, { range, skipToFirstNote }?: EmulationScope) => Emulated;
39
+ export declare const emulate: <Options extends object>(system: ReproducingSystem<Options>, version: Readonly<Version>, edition: Edition, options?: Options, { range, skipToFirstNote }?: EmulationScope) => Emulated;
40
40
  /**
41
41
  * The performed events as a MIDI file, in which every note and pedal step
42
42
  * is labelled with the symbol it performs. The file names the system, the
@@ -57,8 +57,8 @@ export declare class Emulation<Options extends object> {
57
57
  source?: string;
58
58
  constructor(system: ReproducingSystem<Options>, options?: Options);
59
59
  /** Moves the negotiated events to where their statements put them; see `withPlacementsApplied`. */
60
- applyConstraints(view: EditionView): void;
61
- emulateVersion(version: Version, view: EditionView, scope?: EmulationScope): AnyPerformedRollFeature[];
60
+ applyConstraints(edition: Edition): void;
61
+ emulateVersion(version: Version, edition: Edition, scope?: EmulationScope): AnyPerformedRollFeature[];
62
62
  findEventsPerforming(id: string): AnyPerformedRollFeature[];
63
63
  asMIDI(): MidiFile;
64
64
  }
@@ -5,25 +5,27 @@ import { add, mean, mm, seconds, subtract } from "../model/Quantity.js";
5
5
  import { punchDiameterOf } from "../model/RollCopy.js";
6
6
  import { toOwnPaperOf } from "../analysis/ownPaper.js";
7
7
  import { negotiatedEventOf } from "./negotiation.js";
8
+ import { placedCarriersOf, snapshotOf } from "../analysis/text.js";
9
+ import { copyOfFeature, symbolIn } from "../lookup/lookup.js";
8
10
  /** The punch diameter the edition's copies report, where any of them does. */
9
- const meanPunchDiameterOf = (view) => {
10
- const measured = view.edition.copies
11
+ const meanPunchDiameterOf = (edition) => {
12
+ const measured = edition.copies
11
13
  .map(punchDiameterOf)
12
14
  .filter(value => value !== undefined);
13
15
  return measured.length > 0 ? mean(measured) : undefined;
14
16
  };
15
- const propertiesOf = (view, version) => ({
16
- punchDiameter: meanPunchDiameterOf(view),
17
- tempo: view.edition.tempoAdjustment,
18
- toOwnPaper: toOwnPaperOf(view, version)
17
+ const propertiesOf = (edition, version) => ({
18
+ punchDiameter: meanPunchDiameterOf(edition),
19
+ tempo: edition.tempoAdjustment,
20
+ toOwnPaper: toOwnPaperOf(edition, version)
19
21
  });
20
22
  /** The onset a symbol has on each copy carrying it, by the copy's id, as the mean of its chains there. */
21
- const onsetsByCopy = (view, symbolId) => {
22
- const symbol = view.symbol(symbolId);
23
+ const onsetsByCopy = (edition, symbolId) => {
24
+ const symbol = symbolIn(edition, symbolId);
23
25
  if (!symbol)
24
26
  return new Map();
25
- const onsets = view.placedCarriersOf(symbol).flatMap((carrier) => {
26
- const copy = view.copyOf(carrier.id);
27
+ const onsets = placedCarriersOf(edition, symbol).flatMap((carrier) => {
28
+ const copy = copyOfFeature(edition, carrier.id);
27
29
  return copy ? [[copy.id, carrier.horizontal.from]] : [];
28
30
  });
29
31
  const copies = new Set(onsets.map(([copy]) => copy));
@@ -34,9 +36,9 @@ const onsetsByCopy = (view, symbolId) => {
34
36
  * the follower on each copy carrying both, negative where the follower
35
37
  * comes first there.
36
38
  */
37
- const offsetsBetween = (view, followerId, referenceId) => {
38
- const references = onsetsByCopy(view, referenceId);
39
- return [...onsetsByCopy(view, followerId)].flatMap(([copy, onset]) => {
39
+ const offsetsBetween = (edition, followerId, referenceId) => {
40
+ const references = onsetsByCopy(edition, referenceId);
41
+ return [...onsetsByCopy(edition, followerId)].flatMap(([copy, onset]) => {
40
42
  const reference = references.get(copy);
41
43
  return reference === undefined ? [] : [subtract(onset, reference)];
42
44
  });
@@ -91,13 +93,13 @@ const displacementsOf = (events, offsetsBetween, gap) => {
91
93
  const earliestOf = (times) => times.length === 0 ? seconds(0) : times.reduce((soonest, at) => at < soonest ? at : soonest);
92
94
  /**
93
95
  * The events moved to where their statements put them, the events given
94
- * left as they were. The view supplies the copies, whose measurements
96
+ * left as they were. The edition supplies the copies, whose measurements
95
97
  * decide how far before or after its reference a command goes, and a
96
98
  * punch diameter, or a millimetre, where no copy agrees with a statement.
97
99
  */
98
- export const withPlacementsApplied = (view, events) => {
99
- const gap = meanPunchDiameterOf(view) ?? mm(1);
100
- const offsets = (follower, reference) => offsetsBetween(view, follower, reference);
100
+ export const withPlacementsApplied = (edition, events) => {
101
+ const gap = meanPunchDiameterOf(edition) ?? mm(1);
102
+ const offsets = (follower, reference) => offsetsBetween(edition, follower, reference);
101
103
  const displacements = displacementsOf(events, offsets, gap);
102
104
  return events.map(event => {
103
105
  const distance = displacements.get(event);
@@ -112,11 +114,11 @@ export const withPlacementsApplied = (view, events) => {
112
114
  * within the scope and with the placements applied. A note plays only
113
115
  * where its onset falls in the range; expressions play throughout.
114
116
  */
115
- export const negotiatedEventsOf = (view, version, bar, { range } = {}) => {
117
+ export const negotiatedEventsOf = (edition, version, bar, { range } = {}) => {
116
118
  const inScope = (event) => !range || event.type !== 'note' || (event.horizontal.from > range[0] && event.horizontal.from < range[1]);
117
- return withPlacementsApplied(view, view.snapshot(version.id)
119
+ return withPlacementsApplied(edition, snapshotOf(edition, version.id)
118
120
  .filter(isCommand)
119
- .map(symbol => negotiatedEventOf(view, symbol, bar))
121
+ .map(symbol => negotiatedEventOf(edition, symbol, bar))
120
122
  .filter(event => event !== null)
121
123
  .filter(inScope));
122
124
  };
@@ -124,11 +126,11 @@ export const negotiatedEventsOf = (view, version, bar, { range } = {}) => {
124
126
  * Performs a version on a reproducing system: the symbols are negotiated
125
127
  * into placed events and the system plays them. Nothing given is changed.
126
128
  */
127
- export const emulate = (system, version, view, options = system.defaultOptions, { range, skipToFirstNote = false } = {}) => {
128
- const negotiated = negotiatedEventsOf(view, version, system.trackerBar, { range });
129
+ export const emulate = (system, version, edition, options = system.defaultOptions, { range, skipToFirstNote = false } = {}) => {
130
+ const negotiated = negotiatedEventsOf(edition, version, system.trackerBar, { range });
129
131
  if (negotiated.length === 0)
130
132
  return { source: version.id, negotiated, events: [], curves: [] };
131
- const performance = system.perform(negotiated, options, propertiesOf(view, version));
133
+ const performance = system.perform(negotiated, options, propertiesOf(edition, version));
132
134
  const onsets = performance.events.filter(event => event.type === 'noteOn').map(event => event.at);
133
135
  const origin = skipToFirstNote ? earliestOf(onsets) : seconds(0);
134
136
  return {
@@ -262,11 +264,11 @@ export class Emulation {
262
264
  this.options = options;
263
265
  }
264
266
  /** Moves the negotiated events to where their statements put them; see `withPlacementsApplied`. */
265
- applyConstraints(view) {
266
- this.negotiatedEvents = withPlacementsApplied(view, this.negotiatedEvents);
267
+ applyConstraints(edition) {
268
+ this.negotiatedEvents = withPlacementsApplied(edition, this.negotiatedEvents);
267
269
  }
268
- emulateVersion(version, view, scope = {}) {
269
- const emulated = emulate(this.system, version, view, this.options, scope);
270
+ emulateVersion(version, edition, scope = {}) {
271
+ const emulated = emulate(this.system, version, edition, this.options, scope);
270
272
  this.source = emulated.source;
271
273
  this.negotiatedEvents = [...emulated.negotiated];
272
274
  this.curves = emulated.curves;
@@ -1,7 +1,7 @@
1
- import type { EditionView } from "../view/EditionView.js";
2
1
  import type { Expression, Note } from "../model/Symbol.js";
3
2
  import type { TrackerBar } from "../systems/TrackerBar.js";
4
3
  import type { NegotiatedEvent } from "../systems/ReproducingSystem.js";
4
+ import { Edition } from "../model/Edition.js";
5
5
  /**
6
6
  * The symbol as a performance needs it: where it lies, and the
7
7
  * position the performing bar reads it on.
@@ -11,4 +11,4 @@ import type { NegotiatedEvent } from "../systems/ReproducingSystem.js";
11
11
  * green version still inherits cannot be performed on a green
12
12
  * machine, and an edit has yet to say what took its place.
13
13
  */
14
- export declare const negotiatedEventOf: (view: EditionView, symbol: Note | Expression, bar: TrackerBar) => NegotiatedEvent | null;
14
+ export declare const negotiatedEventOf: (edition: Edition, symbol: Note | Expression, bar: TrackerBar) => NegotiatedEvent | null;
@@ -1,3 +1,4 @@
1
+ import { placeOf } from "../analysis/text.js";
1
2
  /**
2
3
  * The symbol as a performance needs it: where it lies, and the
3
4
  * position the performing bar reads it on.
@@ -7,8 +8,8 @@
7
8
  * green version still inherits cannot be performed on a green
8
9
  * machine, and an edit has yet to say what took its place.
9
10
  */
10
- export const negotiatedEventOf = (view, symbol, bar) => {
11
- const horizontal = view.placeOf(symbol);
11
+ export const negotiatedEventOf = (edition, symbol, bar) => {
12
+ const horizontal = placeOf(edition, symbol);
12
13
  const position = bar.positionOf(symbol);
13
14
  if (!horizontal || position === undefined)
14
15
  return null;
package/lib/index.d.ts CHANGED
@@ -31,7 +31,10 @@ export * from './analysis/witnesses.js';
31
31
  export { alignFeatures, ALIGNMENT_METHOD, fromAxis, ownFeaturesOf, toAxis, tooShortToShorten, type AlignmentResult } from './collation/alignment.js';
32
32
  export * from './collation/statistics.js';
33
33
  export * from './model/Edition.js';
34
- export * from './view/EditionView.js';
34
+ export * from './lookup/lookup.js';
35
+ export * from './lookup/paths.js';
36
+ export * from './analysis/stemma.js';
37
+ export * from './analysis/text.js';
35
38
  export * from './analysis/ownPaper.js';
36
39
  export * from './analysis/paper.js';
37
40
  export * from './emulation/negotiation.js';
package/lib/index.js CHANGED
@@ -24,7 +24,10 @@ export * from './analysis/witnesses.js';
24
24
  export { alignFeatures, ALIGNMENT_METHOD, fromAxis, ownFeaturesOf, toAxis, tooShortToShorten } from './collation/alignment.js';
25
25
  export * from './collation/statistics.js';
26
26
  export * from './model/Edition.js';
27
- export * from './view/EditionView.js';
27
+ export * from './lookup/lookup.js';
28
+ export * from './lookup/paths.js';
29
+ export * from './analysis/stemma.js';
30
+ export * from './analysis/text.js';
28
31
  export * from './analysis/ownPaper.js';
29
32
  export * from './analysis/paper.js';
30
33
  export * from './emulation/negotiation.js';
@@ -59,6 +59,16 @@ const withSystemContext = (version) => {
59
59
  return system ? { '@context': `https://w3id.org/reo/${system}/context.jsonld`, ...version } : version;
60
60
  };
61
61
  const withSystemContexts = (edition) => Array.isArray(edition.versions) ? { ...edition, versions: edition.versions.map(withSystemContext) } : edition;
62
+ /**
63
+ * Gives every version the act that made it, stating nothing of it where
64
+ * nothing is known. The axioms of reo.ttl order that act in time against
65
+ * the recording, the copies and the act that made the version it derives
66
+ * from, and a reasoner cannot place a node the graph lacks. An import
67
+ * drops it again where it states nothing.
68
+ */
69
+ const withCreations = (edition) => Array.isArray(edition.versions)
70
+ ? { ...edition, versions: edition.versions.map((version) => version.creation ? version : { ...version, creation: {} }) }
71
+ : edition;
62
72
  const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
63
73
  /** The act with what it changed named: an attachment augments the copy, a removal diminishes it. */
64
74
  const changing = (copyId) => (act) => {
@@ -70,23 +80,19 @@ const changing = (copyId) => (act) => {
70
80
  return { ...act, diminished: { '@id': copyId } };
71
81
  return act;
72
82
  };
83
+ /** The copy with what each of its acts changed stated. */
84
+ const withChangesOf = (copy) => isRecord(copy) && Array.isArray(copy.modifications)
85
+ ? { ...copy, modifications: copy.modifications.map(changing(copy['@id'])) }
86
+ : copy;
73
87
  /**
74
88
  * The document with what each act changed stated. Which of P110
75
89
  * augmented and P112 diminished applies depends on the class of the
76
90
  * act, which no property chain in OWL 2 RL can test, so the export
77
91
  * states it. What a copy or a patch bears is left to a reasoner (see
78
- * the entailments in reo.ttl).
92
+ * the entailments in reo.ttl). The copies are the only nodes that are
93
+ * modified, and the edition holds them in its list of copies alone.
79
94
  */
80
- const withChanges = (value) => {
81
- if (Array.isArray(value))
82
- return value.map(withChanges);
83
- if (!isRecord(value))
84
- return value;
85
- const node = Object.fromEntries(Object.entries(value).map(([key, child]) => [key, withChanges(child)]));
86
- return node['@type'] === 'RollCopy' && Array.isArray(node.modifications)
87
- ? { ...node, modifications: node.modifications.map(changing(node['@id'])) }
88
- : node;
89
- };
95
+ const withChanges = (edition) => Array.isArray(edition.copies) ? { ...edition, copies: edition.copies.map(withChangesOf) } : edition;
90
96
  /** Terms the context sets to null, which say nothing when the edition is read as RDF. */
91
97
  const silentTerms = new Set(Object.entries(context['@context'])
92
98
  .filter(([, definition]) => definition === null)
@@ -110,21 +116,24 @@ const quote = (subject, key, reference, listed) => {
110
116
  const { '@annotation': { '@id': annotation, ...about }, ...object } = reference;
111
117
  return { '@id': { '@id': subject, [key]: listed ? [object] : object }, annotation, ...about };
112
118
  };
113
- /** The value a node states under a key, less the doubted references, and those references quoted. */
114
- const quotingValue = (subject, key, value) => {
119
+ /** The value a node states under a key, less the doubted references, which go to the quoted. */
120
+ const unquoted = (subject, key, value, quoted) => {
115
121
  if (Array.isArray(value)) {
116
- return {
117
- node: value.filter(item => !isDoubtedReference(item)),
118
- quoted: value.filter(isDoubtedReference).map(item => quote(subject, key, item, true))
119
- };
122
+ return value.filter(item => {
123
+ if (!isDoubtedReference(item))
124
+ return true;
125
+ quoted.push(quote(subject, key, item, true));
126
+ return false;
127
+ });
120
128
  }
121
- return isDoubtedReference(value)
122
- ? { node: undefined, quoted: [quote(subject, key, value, false)] }
123
- : { node: value, quoted: [] };
129
+ if (!isDoubtedReference(value))
130
+ return value;
131
+ quoted.push(quote(subject, key, value, false));
132
+ return undefined;
124
133
  };
125
134
  /**
126
135
  * The document with every doubted reference taken off the node that
127
- * states it, and quoted instead.
136
+ * states it, and quoted instead, in the order the document states them.
128
137
  *
129
138
  * An `@annotation` in JSON-LD-star states the triple it annotates and
130
139
  * then says something about it, so a statement the edition holds
@@ -132,32 +141,28 @@ const quotingValue = (subject, key, value) => {
132
141
  * between nodes are quoted, since only they can be put back where they
133
142
  * stood; a doubted date or attribution stays annotated in place.
134
143
  */
135
- const withDoubtedReferencesQuoted = (value) => {
136
- if (Array.isArray(value)) {
137
- const quotings = value.map(withDoubtedReferencesQuoted);
138
- return { node: quotings.map(({ node }) => node), quoted: quotings.flatMap(({ quoted }) => quoted) };
139
- }
144
+ const withDoubtedReferencesQuoted = (value, quoted) => {
145
+ if (Array.isArray(value))
146
+ return value.map(item => withDoubtedReferencesQuoted(item, quoted));
140
147
  if (!isRecord(value))
141
- return { node: value, quoted: [] };
148
+ return value;
142
149
  const subject = value['@id'];
143
- const entries = Object.entries(value).map(([key, child]) => {
144
- const own = typeof subject === 'string' && !key.startsWith('@')
145
- ? quotingValue(subject, key, child)
146
- : { node: child, quoted: [] };
147
- const below = withDoubtedReferencesQuoted(own.node);
148
- return { key, node: below.node, quoted: [...own.quoted, ...below.quoted] };
150
+ const node = {};
151
+ Object.entries(value).forEach(([key, child]) => {
152
+ const stated = typeof subject === 'string' && !key.startsWith('@') ? unquoted(subject, key, child, quoted) : child;
153
+ const below = withDoubtedReferencesQuoted(stated, quoted);
154
+ if (below !== undefined)
155
+ node[key] = below;
149
156
  });
150
- return {
151
- node: Object.fromEntries(entries.filter(({ node }) => node !== undefined).map(({ key, node }) => [key, node])),
152
- quoted: entries.flatMap(({ quoted }) => quoted)
153
- };
157
+ return node;
154
158
  };
155
159
  /** The edition with each copy's features and tears at the copy's own places, as a document holds them. */
156
160
  const atOwnPlacesAll = (edition) => edition.copies.some(copy => copy.measurements.alignment)
157
161
  ? { ...edition, copies: edition.copies.map(atOwnPlaces) }
158
162
  : edition;
159
163
  export const asJsonLd = (edition) => {
160
- const { node, quoted } = withDoubtedReferencesQuoted(withSystemContexts(withChanges(asJsonLdEntity(atOwnPlacesAll(edition)))));
164
+ const quoted = [];
165
+ const node = withDoubtedReferencesQuoted(withSystemContexts(withCreations(withChanges(asJsonLdEntity(atOwnPlacesAll(edition))))), quoted);
161
166
  // The context is the export's own; one carried in from an import must not override it.
162
167
  const { base, '@context': carried, ...rest } = node;
163
168
  return {
@@ -71,6 +71,16 @@ const withPlainCopyIds = (json) => ({
71
71
  '@id': typeof copy['@id'] === 'string' ? plainCopyId(copy['@id']) : copy['@id']
72
72
  }))
73
73
  });
74
+ /** The document without the creations an export gave versions that state none. */
75
+ const withoutEmptyCreations = (json) => Array.isArray(json.versions)
76
+ ? {
77
+ ...json,
78
+ versions: json.versions.map((version) => {
79
+ const { creation, ...rest } = version;
80
+ return creation !== null && typeof creation === 'object' && Object.keys(creation).length === 0 ? rest : version;
81
+ })
82
+ }
83
+ : json;
74
84
  /** A statement an export quoted rather than stated: an included node whose id is a triple. */
75
85
  const isQuotedStatement = (node) => node !== null && typeof node === 'object' && node['@id'] !== null && typeof node['@id'] === 'object';
76
86
  /** The reference a quoted statement made, annotated again with the belief the export set beside it. */
@@ -125,7 +135,7 @@ const withQuotedStatementsInPlace = (edition) => {
125
135
  * its axis, which is where the copy's alignment carries them.
126
136
  */
127
137
  export const importJsonLd = (json) => {
128
- const { '@context': context, ...document } = withPlainCopyIds(migrate(withQuotedStatementsInPlace(json)));
138
+ const { '@context': context, ...document } = withoutEmptyCreations(withPlainCopyIds(migrate(withQuotedStatementsInPlace(json))));
129
139
  const edition = fromJsonLdEntity(document);
130
140
  edition.base = Array.isArray(context)
131
141
  ? context.find((c) => c['@base'])?.['@base'] || ''