linked-rolls 0.62.0 → 0.63.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.
@@ -1,56 +1,126 @@
1
- import { featuresOf } from "../model/RollCopy.js";
1
+ import { featuresOf, isTear, tearsOf } from "../model/RollCopy.js";
2
2
  import { defaultTrackerBar } from "../systems/index.js";
3
- import { add, mm, scale, subtract } from "../model/Quantity.js";
4
- /** Moves both ends of a span, the far end only where the span has one. */
5
- const move = (span, by) => {
6
- span.from = add(span.from, by);
3
+ import { add, mm, subtract, track } from "../model/Quantity.js";
4
+ /** A place on the copy's own paper, on the edition's axis. */
5
+ export const toAxis = (alignment) => (place) => alignment ? mm((place + alignment.shift.horizontal) * alignment.scale) : place;
6
+ /** A place on the edition's axis, on the copy's own paper. */
7
+ export const fromAxis = (alignment) => (place) => alignment ? mm(place / alignment.scale - alignment.shift.horizontal) : place;
8
+ const acrossToAxis = (alignment) => (position) => alignment ? track(position + alignment.shift.vertical) : position;
9
+ const acrossFromAxis = (alignment) => (position) => alignment ? track(position - alignment.shift.vertical) : position;
10
+ /** Both ends of a span taken through the mapping, the far end only where the span has one. */
11
+ const mapped = (span, through) => {
12
+ span.from = through(span.from);
7
13
  if (span.to !== undefined)
8
- span.to = add(span.to, by);
14
+ span.to = through(span.to);
9
15
  };
10
- /** Stretches both ends of a span away from the beginning of the roll. */
11
- const stretch = (span, factor) => {
12
- span.from = scale(span.from, factor);
13
- if (span.to !== undefined)
14
- span.to = scale(span.to, factor);
16
+ /**
17
+ * Where along the roll the copy states anything: its features, and its
18
+ * tears, which are measured as the features are. A tear states no track,
19
+ * so the shift across the roll leaves it where it is.
20
+ */
21
+ const spansAlong = (copy) => [...featuresOf(copy), ...tearsOf(copy)].map(placed => placed.horizontal);
22
+ /** Puts the copy's own places onto the axis in place. */
23
+ const moveOnto = (alignment, copy) => {
24
+ spansAlong(copy).forEach(span => mapped(span, toAxis(alignment)));
25
+ featuresOf(copy).forEach(feature => mapped(feature.vertical, acrossToAxis(alignment)));
15
26
  };
16
- const back = (shift) => ({ horizontal: scale(shift.horizontal, -1), vertical: scale(shift.vertical, -1) });
17
- export const applyShift = (shift, copy) => {
18
- if (copy.measurements.shift)
19
- return;
20
- featuresOf(copy).forEach(feature => {
21
- move(feature.horizontal, shift.horizontal);
22
- move(feature.vertical, shift.vertical);
23
- });
24
- copy.measurements.shift = shift;
27
+ /** Takes the copy's places on the axis back to its own paper in place. */
28
+ const moveOff = (alignment, copy) => {
29
+ spansAlong(copy).forEach(span => mapped(span, fromAxis(alignment)));
30
+ featuresOf(copy).forEach(feature => mapped(feature.vertical, acrossFromAxis(alignment)));
25
31
  };
26
- /** Scales the copy's features away from the beginning of the roll, and records the factor. */
27
- export const applyScale = (factor, copy) => {
28
- if (copy.measurements.scale !== undefined)
29
- return;
30
- featuresOf(copy).forEach(feature => stretch(feature.horizontal, factor));
31
- copy.measurements.scale = factor;
32
+ /**
33
+ * Carries the copy's features and tears from its own places onto the
34
+ * axis, and records how, in place of any alignment it had: that one is
35
+ * taken off first.
36
+ */
37
+ export const applyAlignment = (alignment, copy) => {
38
+ revertAlignment(copy);
39
+ moveOnto(alignment, copy);
40
+ copy.measurements.alignment = alignment;
32
41
  };
33
- /** Takes the shift off the copy's features again, as far as one was applied. */
34
- export const revertShift = (copy) => {
35
- const shift = copy.measurements.shift;
36
- if (!shift)
42
+ /** Takes the copy's features and tears back to its own places, as far as it was aligned. */
43
+ export const revertAlignment = (copy) => {
44
+ const alignment = copy.measurements.alignment;
45
+ if (!alignment)
37
46
  return;
38
- const reversed = back(shift);
39
- featuresOf(copy).forEach(feature => {
40
- move(feature.horizontal, reversed.horizontal);
41
- move(feature.vertical, reversed.vertical);
42
- });
43
- delete copy.measurements.shift;
47
+ moveOff(alignment, copy);
48
+ delete copy.measurements.alignment;
44
49
  };
45
- /** Takes the scale off the copy's features again, as far as one was applied. */
46
- export const revertScale = (copy) => {
47
- const factor = copy.measurements.scale;
48
- if (factor === undefined)
49
- return;
50
- featuresOf(copy).forEach(feature => stretch(feature.horizontal, 1 / factor));
51
- delete copy.measurements.scale;
50
+ /**
51
+ * Places are written to the nanometre. Carried onto the axis and back,
52
+ * a place comes back a rounding error away from where it was, and a
53
+ * document saved again would change in its last digits every time.
54
+ * A scan resolves a tenth of a millimetre at best.
55
+ */
56
+ const PLACES_PER_MM = 1e6;
57
+ const written = (place) => mm(Math.round(place * PLACES_PER_MM) / PLACES_PER_MM);
58
+ /**
59
+ * The copy with its features and tears at its own places, as a document
60
+ * holds them, and its alignment kept to say how they go onto the axis.
61
+ * The copy given is left as it is. A copy that is not aligned is the
62
+ * very same copy.
63
+ */
64
+ export const atOwnPlaces = (copy) => {
65
+ const alignment = copy.measurements.alignment;
66
+ if (!alignment)
67
+ return copy;
68
+ const along = (place) => written(fromAxis(alignment)(place));
69
+ const across = acrossFromAxis(alignment);
70
+ const ownSpan = (span, through) => {
71
+ const own = { ...span };
72
+ mapped(own, through);
73
+ return own;
74
+ };
75
+ const ownFeature = (feature) => ({ ...feature, horizontal: ownSpan(feature.horizontal, along), vertical: ownSpan(feature.vertical, across) });
76
+ return {
77
+ ...copy,
78
+ ...(copy.production && {
79
+ production: {
80
+ ...copy.production,
81
+ ...(copy.production.produced && { produced: copy.production.produced.map(ownFeature) })
82
+ }
83
+ }),
84
+ modifications: copy.modifications.map(act => {
85
+ if (act.type === 'Alteration')
86
+ return { ...act, produced: act.produced.map(ownFeature) };
87
+ if (act.type === 'Attachment')
88
+ return { ...act, added: act.added.map(ownFeature) };
89
+ return act;
90
+ }),
91
+ conditions: copy.conditions.map(condition => isTear(condition)
92
+ ? { ...condition, horizontal: ownSpan(condition.horizontal, along) }
93
+ : condition)
94
+ };
95
+ };
96
+ /**
97
+ * Puts the features and tears of a copy read from a document, which
98
+ * holds them at the copy's own places, onto the axis. The copy given is
99
+ * changed.
100
+ */
101
+ export const putOnAxis = (copy) => {
102
+ const alignment = copy.measurements.alignment;
103
+ if (alignment)
104
+ moveOnto(alignment, copy);
105
+ };
106
+ /** The copy's features at its own places, as it was read, whatever alignment it has. */
107
+ export const ownFeaturesOf = (copy) => {
108
+ const alignment = copy.measurements.alignment;
109
+ if (!alignment)
110
+ return featuresOf(copy);
111
+ return featuresOf(copy).map(feature => {
112
+ const own = { ...feature, horizontal: { ...feature.horizontal }, vertical: { ...feature.vertical } };
113
+ mapped(own.horizontal, fromAxis(alignment));
114
+ mapped(own.vertical, acrossFromAxis(alignment));
115
+ return own;
116
+ });
52
117
  };
53
118
  const chainsOf = (copy) => featuresOf(copy).filter(feature => feature.type === 'HoleChain');
119
+ /**
120
+ * The reader's extension as it lies on the axis. It is a length on the
121
+ * copy's own paper, which the alignment scales like any other.
122
+ */
123
+ const extensionOnAxis = (extension, copy) => mm(extension * (copy.measurements.alignment?.scale ?? 1));
54
124
  /**
55
125
  * Takes the extension a pneumatic reader adds off the ends of the
56
126
  * copy's chains of holes, and records how much was taken.
@@ -86,7 +156,10 @@ const chainsOf = (copy) => featuresOf(copy).filter(feature => feature.type === '
86
156
  * on the feature, a reading of the chain, a smaller extension — and
87
157
  * `shortenChains` throws rather than answer it.
88
158
  */
89
- export const tooShortToShorten = (extension, copy, leaving = new Set()) => chainsOf(copy).filter(chain => !leaving.has(chain.id) && chain.horizontal.to - chain.horizontal.from <= extension);
159
+ export const tooShortToShorten = (extension, copy, leaving = new Set()) => {
160
+ const onAxis = extensionOnAxis(extension, copy);
161
+ return chainsOf(copy).filter(chain => !leaving.has(chain.id) && chain.horizontal.to - chain.horizontal.from <= onAxis);
162
+ };
90
163
  /**
91
164
  * Takes the extension off, leaving the named chains as the reader gave
92
165
  * them. Naming a chain is an editorial act and not a repair: it says
@@ -107,10 +180,11 @@ export const shortenChains = (extension, copy, leaving = new Set()) => {
107
180
  + `chain(s) of holes of copy ${copy.id}, which are no longer than it: `
108
181
  + `${tooShort.map(chain => chain.id).join(', ')}`);
109
182
  }
183
+ const onAxis = extensionOnAxis(extension, copy);
110
184
  const left = chainsOf(copy).filter(chain => leaving.has(chain.id)).map(chain => chain.id);
111
185
  chainsOf(copy)
112
186
  .filter(chain => !leaving.has(chain.id))
113
- .forEach(chain => { chain.horizontal.to = subtract(chain.horizontal.to, extension); });
187
+ .forEach(chain => { chain.horizontal.to = subtract(chain.horizontal.to, onAxis); });
114
188
  copy.measurements.readerExtension = { length: extension, ...(left.length > 0 && { leaving: left }) };
115
189
  };
116
190
  /** Puts the reader's extension back on the chains it was taken off, as far as one was taken off. */
@@ -118,12 +192,20 @@ export const revertShortening = (copy) => {
118
192
  const taken = copy.measurements.readerExtension;
119
193
  if (taken === undefined)
120
194
  return;
195
+ const onAxis = extensionOnAxis(taken.length, copy);
121
196
  const left = new Set(taken.leaving ?? []);
122
197
  chainsOf(copy)
123
198
  .filter(chain => !left.has(chain.id))
124
- .forEach(chain => { chain.horizontal.to = add(chain.horizontal.to, taken.length); });
199
+ .forEach(chain => { chain.horizontal.to = add(chain.horizontal.to, onAxis); });
125
200
  delete copy.measurements.readerExtension;
126
201
  };
202
+ /**
203
+ * The method `alignFeatures` follows, as an alignment records it. Its
204
+ * revision changes when the method does and at no other release, so
205
+ * that an alignment found by an earlier method can be told from one
206
+ * that would be found again as it stands.
207
+ */
208
+ export const ALIGNMENT_METHOD = { software: 'linked-rolls alignFeatures', version: '1' };
127
209
  const byPlace = (x, y) => x.at - y.at || x.pitch - y.pitch;
128
210
  /** The notes the bar reads off the features, in the order they pass it. */
129
211
  const noteOnsets = (features, bar) => features
@@ -218,15 +300,23 @@ const settled = (a, b) => (fit, window) => {
218
300
  const matches = nearestMatches(a, b, fit.line, window);
219
301
  return { line: robustLine(matches) ?? fit.line, matches };
220
302
  };
303
+ /** How far the places of the matches on the copy spread, as a standard deviation. */
304
+ const spreadOf = (matches) => {
305
+ const centre = matches.reduce((total, m) => total + m.a, 0) / matches.length;
306
+ return Math.sqrt(matches.reduce((total, m) => total + (m.a - centre) ** 2, 0) / matches.length);
307
+ };
221
308
  const resultOf = ({ line, matches }) => {
222
309
  if (matches.length < 2 || line.slope <= 0)
223
310
  return undefined;
224
311
  const squares = matches.reduce((total, m) => total + residualOf(line, m) ** 2, 0);
312
+ const residual = Math.sqrt(squares / matches.length);
313
+ const spread = spreadOf(matches);
225
314
  return {
226
315
  shift: mm(line.intercept / line.slope),
227
316
  scale: line.slope,
228
317
  matched: matches.length,
229
- residual: mm(Math.sqrt(squares / matches.length))
318
+ residual: mm(residual),
319
+ scaleError: spread > 0 ? residual / (spread * Math.sqrt(matches.length)) : Infinity
230
320
  };
231
321
  };
232
322
  /**
package/lib/index.d.ts CHANGED
@@ -28,11 +28,12 @@ export * from './model/procedures.js';
28
28
  export * from './model/vocabulary.js';
29
29
  export * from './analysis/reservations.js';
30
30
  export * from './analysis/witnesses.js';
31
- export { alignFeatures, tooShortToShorten, type AlignmentResult } from './collation/alignment.js';
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
34
  export * from './view/EditionView.js';
35
35
  export * from './analysis/ownPaper.js';
36
+ export * from './analysis/paper.js';
36
37
  export * from './emulation/negotiation.js';
37
38
  export * from './collation/scatter.js';
38
39
  export * from './ops/index.js';
package/lib/index.js CHANGED
@@ -21,11 +21,12 @@ export * from './model/procedures.js';
21
21
  export * from './model/vocabulary.js';
22
22
  export * from './analysis/reservations.js';
23
23
  export * from './analysis/witnesses.js';
24
- export { alignFeatures, tooShortToShorten } from './collation/alignment.js';
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
27
  export * from './view/EditionView.js';
28
28
  export * from './analysis/ownPaper.js';
29
+ export * from './analysis/paper.js';
29
30
  export * from './emulation/negotiation.js';
30
31
  export * from './collation/scatter.js';
31
32
  export * from './ops/index.js';
@@ -1,6 +1,7 @@
1
1
  import { systemIdIn } from "../systems/TrackerBar.js";
2
2
  import { certaintyOf, isAsserted } from "../model/Assumption.js";
3
3
  import context from "../spec/context.json" with { type: 'json' };
4
+ import { atOwnPlaces } from "../collation/alignment.js";
4
5
  /**
5
6
  * The keys an export derives from the tree, which no node of the
6
7
  * edition states itself. An import reads them off again.
@@ -151,8 +152,12 @@ const withDoubtedReferencesQuoted = (value) => {
151
152
  quoted: entries.flatMap(({ quoted }) => quoted)
152
153
  };
153
154
  };
155
+ /** The edition with each copy's features and tears at the copy's own places, as a document holds them. */
156
+ const atOwnPlacesAll = (edition) => edition.copies.some(copy => copy.measurements.alignment)
157
+ ? { ...edition, copies: edition.copies.map(atOwnPlaces) }
158
+ : edition;
154
159
  export const asJsonLd = (edition) => {
155
- const { node, quoted } = withDoubtedReferencesQuoted(withSystemContexts(withChanges(asJsonLdEntity(edition))));
160
+ const { node, quoted } = withDoubtedReferencesQuoted(withSystemContexts(withChanges(asJsonLdEntity(atOwnPlacesAll(edition)))));
156
161
  // The context is the export's own; one carried in from an import must not override it.
157
162
  const { base, '@context': carried, ...rest } = node;
158
163
  return {
@@ -1,5 +1,10 @@
1
1
  import { Edition } from "../model/Edition.js";
2
2
  export declare const importDate: (str: string) => Date;
3
3
  type Json = any;
4
+ /**
5
+ * Reads an edition. A document holds each copy's features and tears at
6
+ * the copy's own places, as they were read; the edition holds them on
7
+ * its axis, which is where the copy's alignment carries them.
8
+ */
4
9
  export declare const importJsonLd: (json: Json) => Edition;
5
10
  export {};
@@ -1,6 +1,7 @@
1
1
  import { derivedKeys } from "./asJsonLd.js";
2
2
  import { migrate, plainCopyId } from "./migrate.js";
3
3
  import { isDateString } from "../shared/utils.js";
4
+ import { putOnAxis } from "../collation/alignment.js";
4
5
  import schema from "../schema.json" with { type: 'json' };
5
6
  export const importDate = (str) => {
6
7
  const [y, m, d] = str.split('-').map(s => parseInt(s, 10));
@@ -118,11 +119,17 @@ const withQuotedStatementsInPlace = (edition) => {
118
119
  const bySubject = Map.groupBy(statements.map(referenceOf), ({ subject }) => subject);
119
120
  return withReferencesOn({ ...rest, ...(others.length > 0 && { '@included': others }) }, bySubject);
120
121
  };
122
+ /**
123
+ * Reads an edition. A document holds each copy's features and tears at
124
+ * the copy's own places, as they were read; the edition holds them on
125
+ * its axis, which is where the copy's alignment carries them.
126
+ */
121
127
  export const importJsonLd = (json) => {
122
128
  const { '@context': context, ...document } = withPlainCopyIds(migrate(withQuotedStatementsInPlace(json)));
123
129
  const edition = fromJsonLdEntity(document);
124
130
  edition.base = Array.isArray(context)
125
131
  ? context.find((c) => c['@base'])?.['@base'] || ''
126
132
  : '';
133
+ (edition.copies ?? []).forEach(putOnAxis);
127
134
  return edition;
128
135
  };
package/lib/io/migrate.js CHANGED
@@ -360,6 +360,78 @@ const withFeaturesInActs = (node) => {
360
360
  modifications: [...acts, ...(loose.length > 0 ? [{ '@type': 'Attachment', added: loose }] : [])]
361
361
  };
362
362
  };
363
+ /** A place written to the nanometre, as the export writes places it has carried off the axis. */
364
+ const writtenPlace = (place) => Math.round(place * 1e6) / 1e6;
365
+ /** The span with both ends taken through the mapping, the far end only where it has one. */
366
+ const spanThrough = (span, through) => span && typeof span.from === 'number'
367
+ ? { ...span, from: through(span.from), ...(typeof span.to === 'number' && { to: through(span.to) }) }
368
+ : span;
369
+ const isLegacyAligned = (copy) => copy?.measurements?.shift !== undefined || copy?.measurements?.scale !== undefined;
370
+ const isLegacyPaperStretch = (condition) => isPaperStretch(condition) && Object.hasOwn(condition, 'factor');
371
+ const conditionTypeOf = (condition) => condition?.conditionType ?? condition?.['@type'];
372
+ /**
373
+ * A copy's features once stood where its alignment had put them, on the
374
+ * edition's axis, with the shift and the scale that had done it recorded
375
+ * beside them. They stand at the copy's own places now, as they were
376
+ * read, and the alignment says how they go onto the axis; so they are
377
+ * taken back by it, and so are the tears, which were measured as the
378
+ * features are. The features are in their acts here, `withFeaturesInActs`
379
+ * having run.
380
+ *
381
+ * The factor of a paper-stretch condition was the alignment's scale
382
+ * stated again, which is how every such condition came about. It is
383
+ * dropped: the scale stays with the alignment, and what it says about the
384
+ * paper is worked out from all the alignments together.
385
+ */
386
+ const withOwnPlaces = (node) => {
387
+ if (node['@type'] !== 'RollCopy' || !isLegacyAligned(node))
388
+ return node;
389
+ const { shift: legacyShift, scale: legacyScale, ...measurements } = node.measurements;
390
+ const shift = {
391
+ horizontal: legacyShift?.horizontal ?? 0,
392
+ vertical: legacyShift?.vertical ?? 0
393
+ };
394
+ const scale = legacyScale ?? 1;
395
+ const along = (place) => writtenPlace(place / scale - shift.horizontal);
396
+ const across = (position) => position - shift.vertical;
397
+ const ownFeature = (feature) => {
398
+ if (!feature || typeof feature !== 'object' || !feature.horizontal)
399
+ return feature;
400
+ return {
401
+ ...feature,
402
+ horizontal: spanThrough(feature.horizontal, along),
403
+ ...(feature.vertical?.unit === 'track' && shift.vertical !== 0 && { vertical: spanThrough(feature.vertical, across) })
404
+ };
405
+ };
406
+ const ownFeatures = (features) => Array.isArray(features) ? features.map(ownFeature) : features;
407
+ const production = node.production && {
408
+ ...node.production,
409
+ ...(Array.isArray(node.production.produced) && { produced: ownFeatures(node.production.produced) })
410
+ };
411
+ const modifications = Array.isArray(node.modifications)
412
+ ? node.modifications.map((act) => act && typeof act === 'object'
413
+ ? {
414
+ ...act,
415
+ ...(Array.isArray(act.produced) && { produced: ownFeatures(act.produced) }),
416
+ ...(Array.isArray(act.added) && { added: ownFeatures(act.added) })
417
+ }
418
+ : act)
419
+ : node.modifications;
420
+ const conditions = Array.isArray(node.conditions)
421
+ ? node.conditions
422
+ .filter((condition) => !isLegacyPaperStretch(condition))
423
+ .map((condition) => conditionTypeOf(condition) === 'torn' && condition.horizontal
424
+ ? { ...condition, horizontal: spanThrough(condition.horizontal, along) }
425
+ : condition)
426
+ : node.conditions;
427
+ return {
428
+ ...node,
429
+ measurements: { ...measurements, alignment: { shift, scale } },
430
+ ...(production && { production }),
431
+ ...(modifications && { modifications }),
432
+ ...(conditions && { conditions })
433
+ };
434
+ };
363
435
  /** A keeper nobody could name was written as an empty one before a copy could leave it out. */
364
436
  const withoutEmptyKeeper = (node) => {
365
437
  if (node.keeper?.name !== '' || node.keeper.sameAs?.length)
@@ -380,7 +452,7 @@ const withTimeSpanDates = (node) => {
380
452
  };
381
453
  const migrateNode = (node) => [withRenamedKeys, withRenamedEditType, withTypology, withRenamedType, withoutVersionType, withoutVersionSiglum, withLowerCaseTerms, withSplitMethod, withReferences, withKeeper, withoutEmptyKeeper,
382
454
  withPerforator, withProductionNodes, withTypedPerforator, withScale, withoutOps, withDerivationList, withReadingKind, withTimeSpanDates,
383
- withoutFeatureKind, withBorneFeaturesNamed, withFeaturesInActs, withDriveOfStaggering, withoutPattern, withoutBearings, withoutEntailedType]
455
+ withoutFeatureKind, withBorneFeaturesNamed, withFeaturesInActs, withOwnPlaces, withDriveOfStaggering, withoutPattern, withoutBearings, withoutEntailedType]
384
456
  .reduce((result, step) => step(result), node);
385
457
  /** The items each walked, or the very same list where the walk changed none. */
386
458
  const walked = (items) => {
@@ -525,5 +597,25 @@ const withoutFormatVersion = (edition) => {
525
597
  const { formatVersion: _stated, ...rest } = edition;
526
598
  return rest;
527
599
  };
528
- const editionSteps = [withoutFormatVersion, withoutEditionType, withSystems, withEditors, withDerivationTolerance];
600
+ /** Whether the copy states features, in either shape. */
601
+ const hasFeatures = (copy) => [copy?.features, copy?.production?.produced, copy?.productionEvent?.produced]
602
+ .some(features => Array.isArray(features) && features.length > 0);
603
+ const statesLegacyAlignment = (copy) => isLegacyAligned(copy)
604
+ || (Array.isArray(copy?.ops) && (copy.ops.includes('shifted') || copy.ops.includes('stretched')));
605
+ /**
606
+ * The copy whose millimetres were the edition's axis went unnamed while
607
+ * the copies aligned onto it recorded only what had been done to them.
608
+ * It was the copy nothing had been done to, and of those the first with
609
+ * features, so that is the one named.
610
+ */
611
+ const withReferenceCopy = (edition) => {
612
+ const copies = listAt(edition, 'copies');
613
+ if (edition.referenceCopy !== undefined || !copies?.some(statesLegacyAlignment))
614
+ return edition;
615
+ const reference = copies.find(copy => !statesLegacyAlignment(copy) && hasFeatures(copy));
616
+ return typeof reference?.['@id'] === 'string'
617
+ ? { ...edition, referenceCopy: plainCopyId(reference['@id']) }
618
+ : edition;
619
+ };
620
+ const editionSteps = [withoutFormatVersion, withoutEditionType, withSystems, withEditors, withDerivationTolerance, withReferenceCopy];
529
621
  export const migrate = (edition) => walk(editionSteps.reduce((result, step) => step(result), edition));
@@ -46,22 +46,35 @@ export interface Inference extends Argumentation<'inference'> {
46
46
  /**
47
47
  * References (by `@id`) to the beliefs
48
48
  * from which the conclusion is drawn.
49
+ *
50
+ * A belief the editor holds and publishes elsewhere under an IRI of
51
+ * its own, such as a bound from a catalogue of premises the editor
52
+ * keeps, is named by that IRI. Someone else's belief is not: it is
53
+ * adopted first, and the belief of the edition's own that the
54
+ * adoption concludes is named instead.
49
55
  * @see crminf:J1 used as premise
50
56
  */
51
57
  premises: string[];
52
58
  /**
53
59
  * References (by `@id`) to what the inference worked on besides
54
- * beliefs, such as the versions it compared or the analysis output
55
- * it cites.
60
+ * beliefs, such as the versions it compared, the analysis output it
61
+ * cites, or the document holding a premise named by IRI, in the
62
+ * state it was read in, since a belief published elsewhere may move
63
+ * after the edition has used it.
56
64
  * @see crm:P16 used specific object
57
65
  */
58
66
  used?: string[];
59
67
  }
60
68
  /**
61
- * A belief adoption adopts someone else's belief. This type is used
62
- * to indicate e.g. knowledge through private communication or
63
- * from secondary literature, or a belief published elsewhere under
64
- * an IRI of its own, such as a premise from a catalogue of premises.
69
+ * A belief adoption takes over someone else's belief without checking it:
70
+ * knowledge through private communication, from the secondary
71
+ * literature, or from another edition. Like every reason, it concludes
72
+ * the belief it is a reason of, which is the edition's own and holds of
73
+ * the edition's statement what the adopted belief held. An inference that
74
+ * relies on it names that belief among its premises.
75
+ *
76
+ * The editor's own beliefs need no adoption, wherever they are
77
+ * published: an inference names them directly (see `Inference`).
65
78
  * @see crminf:I7 Belief Adoption
66
79
  */
67
80
  export interface BeliefAdoption extends Argumentation<'beliefAdoption'> {
@@ -76,9 +89,8 @@ export interface BeliefAdoption extends Argumentation<'beliefAdoption'> {
76
89
  note: string;
77
90
  /**
78
91
  * The beliefs this adoption takes over, by IRI, where they are
79
- * published with one. An inference that uses such a belief names it
80
- * among its premises by the same IRI; the adoption says where it
81
- * comes from.
92
+ * published with one. They are someone else's, and stay theirs: what
93
+ * the edition holds is the belief the adoption concludes.
82
94
  * @see crminf:J6 adopted
83
95
  */
84
96
  adopted?: string[];
@@ -140,6 +140,18 @@ export interface Edition {
140
140
  * @see reo:witness
141
141
  */
142
142
  copies: RollCopy[];
143
+ /**
144
+ * The copy whose millimetres are the edition's axis, by id: every
145
+ * place the edition gives along the roll is a place on this copy's
146
+ * paper, counted from where its scan begins, and every other copy is
147
+ * aligned onto it. Choosing it is the one editorial decision the
148
+ * alignment leaves; a copy scanned whole and without tears serves
149
+ * best, since its places are what notes and readers cite. Left out,
150
+ * it is the first copy with features that is not aligned
151
+ * (`referenceCopyOf`).
152
+ * Not exported to RDF.
153
+ */
154
+ referenceCopy?: string;
143
155
  /**
144
156
  * The different versions of the roll on which
145
157
  * this edition is based.
@@ -154,3 +166,10 @@ export interface Edition {
154
166
  tempoAdjustment?: ObjectAssumption<RollTempo>;
155
167
  }
156
168
  export type EditionMetadata = Pick<Edition, 'base' | 'title' | 'license' | 'creation' | 'roll'>;
169
+ /**
170
+ * The copy whose millimetres are the edition's axis: the one it names,
171
+ * or where it names none or one it no longer has, the first copy with
172
+ * features that is not aligned, which is how an edition stood before it
173
+ * named one.
174
+ */
175
+ export declare const referenceCopyOf: (edition: Pick<Edition, 'copies' | 'referenceCopy'>) => RollCopy | undefined;
@@ -1 +1,9 @@
1
- export {};
1
+ import { featuresOf } from "./RollCopy.js";
2
+ /**
3
+ * The copy whose millimetres are the edition's axis: the one it names,
4
+ * or where it names none or one it no longer has, the first copy with
5
+ * features that is not aligned, which is how an edition stood before it
6
+ * named one.
7
+ */
8
+ export const referenceCopyOf = (edition) => edition.copies.find(copy => copy.id === edition.referenceCopy)
9
+ ?? edition.copies.find(copy => !copy.measurements.alignment && featuresOf(copy).length > 0);
@@ -11,7 +11,7 @@ declare const unit: unique symbol;
11
11
  * The units a record may state. A `Measure` is limited to these, since
12
12
  * they are what the `unit` field is allowed to say.
13
13
  */
14
- export type Unit = 'mm' | 'cm' | 'px' | 'track' | 's' | 'ms' | 'ft/min' | 'm/min' | 'px/in' | 'deg';
14
+ export type Unit = 'mm' | 'cm' | 'px' | 'track' | 's' | 'ms' | 'ft/min' | 'm/min' | 'px/in' | 'deg' | 'percent';
15
15
  export type Quantity<U extends string> = number & {
16
16
  readonly [unit]: U;
17
17
  };
@@ -30,6 +30,8 @@ export type MetersPerMinute = Quantity<'m/min'>;
30
30
  export type Resolution = Quantity<'px/in'>;
31
31
  /** An angle on the roll, turning from the line across the paper towards the end of the roll. */
32
32
  export type Degrees = Quantity<'deg'>;
33
+ /** A share of a length, such as how far the paper has stretched: dimensionless, in hundredths. */
34
+ export type Percent = Quantity<'percent'>;
33
35
  /** Names a number in a unit. Partially apply it to make a constructor. */
34
36
  export declare const quantity: <U extends string>(value: number) => Quantity<U>;
35
37
  export declare const mm: (value: number) => Quantity<"mm">;
@@ -42,6 +44,7 @@ export declare const feetPerMinute: (value: number) => Quantity<"ft/min">;
42
44
  export declare const metersPerMinute: (value: number) => Quantity<"m/min">;
43
45
  export declare const pixelsPerInch: (value: number) => Quantity<"px/in">;
44
46
  export declare const degrees: (value: number) => Quantity<"deg">;
47
+ export declare const percent: (value: number) => Quantity<"percent">;
45
48
  /**
46
49
  * A value together with the unit it was measured in, as a record
47
50
  * states it.
@@ -10,6 +10,7 @@ export const feetPerMinute = quantity;
10
10
  export const metersPerMinute = quantity;
11
11
  export const pixelsPerInch = quantity;
12
12
  export const degrees = quantity;
13
+ export const percent = quantity;
13
14
  // The second operand takes its unit from the first. Inferred from both,
14
15
  // TypeScript would unite two units rather than reject them.
15
16
  export const add = (a, b) => quantity(a + b);