linked-rolls 0.25.1 → 0.27.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.
package/lib/editionOps.js CHANGED
@@ -3,7 +3,7 @@ import { v4 } from "uuid";
3
3
  import { getAt } from "./EditionView";
4
4
  import { isPerforation, placementRelations } from "./Symbol";
5
5
  import { collationsOf, defaultCollationTolerance } from "./Collation";
6
- import { collationToleranceOf, insertedBy } from "./Version";
6
+ import { collationToleranceOf, editsOf, insertedBy, principalDerivationOf } from "./Version";
7
7
  import { asSymbols, barOf, isPaperStretch } from "./RollCopy";
8
8
  import { systemOf } from "./TrackerBar";
9
9
  import { substitutionsBetween } from "./substitution";
@@ -65,7 +65,9 @@ const droppingInsertions = (symbolIds) => (edit) => {
65
65
  };
66
66
  /** Takes the symbols out of the version's own insertions, and the edits that had nothing else. */
67
67
  const dropInsertions = (version, symbolIds) => {
68
- version.edits = edited(stateOf(version).edits, droppingInsertions(symbolIds));
68
+ const edits = stateOf(version).edits;
69
+ if (edits)
70
+ version.edits = edited(edits, droppingInsertions(symbolIds));
69
71
  };
70
72
  /**
71
73
  * Puts the copy into the edition with a version of its own, which
@@ -86,6 +88,14 @@ export const createVersion = (siglum, copy) => draft => {
86
88
  motivations: []
87
89
  });
88
90
  };
91
+ /**
92
+ * Puts the copy into the edition without a version of its own, as for a
93
+ * copy whose features are not read into symbols: one known only from a
94
+ * recording states instead which versions it carries.
95
+ */
96
+ export const addCopy = (copy) => draft => {
97
+ draft.copies.push(copy);
98
+ };
89
99
  /** States what the scale is put down to, in place of an earlier reading. */
90
100
  const readScale = (copy, reading) => {
91
101
  if (reading.cause === 'paper') {
@@ -125,6 +135,39 @@ export const stateSource = (copyId, source) => onCopy(copyId, copy => {
125
135
  export const clearSource = (copyId) => onCopy(copyId, copy => {
126
136
  copy.readFrom = undefined;
127
137
  });
138
+ /** A reference under the belief given, where one is given. */
139
+ const referenceHeld = (id, belief) => ({ ...assignReference(id), ...(belief && { '@annotation': { id: v4(), belief } }) });
140
+ /** The references less those that match, or nothing where none is left. */
141
+ const withoutReferences = (references, matches) => {
142
+ const kept = references.filter(reference => !matches(reference));
143
+ return kept.length > 0 ? kept : undefined;
144
+ };
145
+ /** Takes out the copy's statements that match, and the list itself where none is left. */
146
+ const dropStatements = (copy, matches) => {
147
+ const statements = stateOf(copy).carries;
148
+ if (!statements?.some(matches))
149
+ return;
150
+ const kept = withoutReferences(statements, matches);
151
+ if (kept)
152
+ copy.carries = kept;
153
+ else
154
+ delete copy.carries;
155
+ };
156
+ /**
157
+ * States that the copy carries the version, under the belief given. It
158
+ * is meant for a copy whose features are not read into symbols, such as
159
+ * one known only from a recording, and `carriageProblems` reports it
160
+ * where features carry symbols already. A second statement about one
161
+ * version is none.
162
+ */
163
+ export const stateCarriage = (copyId, versionId, belief) => onCopy(copyId, copy => {
164
+ const statements = stateOf(copy).carries ?? [];
165
+ if (statements.some(statement => idOf(statement) === versionId))
166
+ return;
167
+ copy.carries = [...statements, referenceHeld(versionId, belief)];
168
+ });
169
+ /** Takes back the copy's statement that it carries the version. */
170
+ export const clearCarriage = (copyId, versionId) => onCopy(copyId, copy => dropStatements(copy, statement => idOf(statement) === versionId));
128
171
  /**
129
172
  * Adds a general condition to the copy, beside whatever is stated of it
130
173
  * already. The other condition a copy may be in, a paper stretch, is
@@ -278,15 +321,17 @@ const forgetFeatures = (draft, features) => {
278
321
  const versions = stateOf(draft.versions);
279
322
  const dropped = new Set(insertedIn(versions).filter(carriedOnlyOn(features)).map(symbol => symbol.id));
280
323
  const forget = forgettingFeatures(features, dropped);
281
- const edits = versions.map(version => edited(version.edits, forget));
324
+ const edits = versions.map(version => version.edits && edited(version.edits, forget));
282
325
  const gone = new Set([
283
326
  ...features,
284
327
  ...dropped,
285
- ...versions.flatMap((version, i) => droppedIds(version.edits, edits[i]))
328
+ ...versions.flatMap((version, i) => droppedIds(editsOf(version), edits[i] ?? []))
286
329
  ]);
287
330
  renameReferences(draft, ids => without(ids, id => gone.has(id)));
288
331
  draft.versions.forEach((version, i) => {
289
- version.edits = edits[i];
332
+ const stated = edits[i];
333
+ if (stated)
334
+ version.edits = stated;
290
335
  });
291
336
  };
292
337
  /**
@@ -417,7 +462,9 @@ const carryOver = (draft, replaced, mergedId) => {
417
462
  };
418
463
  const versions = stateOf(draft.versions);
419
464
  draft.versions.forEach((version, i) => {
420
- version.edits = mapped(versions[i].edits, recarrying);
465
+ const edits = versions[i].edits;
466
+ if (edits)
467
+ version.edits = mapped(edits, recarrying);
421
468
  });
422
469
  };
423
470
  /**
@@ -463,12 +510,34 @@ const differ = (child, parent) => {
463
510
  const other = trackerBarOf(parent?.system);
464
511
  return one !== undefined && other !== undefined && one.id !== other.id;
465
512
  };
513
+ /** Whether the version's text is read against the parent, its principal derivation naming it. */
514
+ const readsAgainst = (version, parentId) => {
515
+ const principal = principalDerivationOf(version);
516
+ return principal !== undefined && idOf(principal) === parentId;
517
+ };
518
+ /** The derivations the version states beside its principal one, less any naming the parent. */
519
+ const hypothesesBeside = (version, parentId) => {
520
+ const principal = principalDerivationOf(version);
521
+ return (version.basedOn ?? []).filter(derivation => derivation !== principal && idOf(derivation) !== parentId);
522
+ };
523
+ /** Takes out the derivations that match, and the list itself where none is left. */
524
+ const dropDerivations = (version, matches) => {
525
+ const derivations = stateOf(version).basedOn;
526
+ if (!derivations?.some(matches))
527
+ return;
528
+ const kept = withoutReferences(derivations, matches);
529
+ if (kept)
530
+ version.basedOn = kept;
531
+ else
532
+ delete version.basedOn;
533
+ };
466
534
  /**
467
535
  * Bases the child on the parent. A symbol of the child that collates
468
536
  * with one the parent hands down adds its carriers to that symbol; the
469
537
  * rest become the child's insertions, and what the parent hands down
470
538
  * and the child lacks becomes its deletions. The derivation states the
471
- * tolerance it was collated at.
539
+ * tolerance it was collated at and becomes the principal one; the
540
+ * hypotheses the child stated beside its former one stay.
472
541
  */
473
542
  export const connectVersions = (view, childId, parentId, tolerance = defaultCollationTolerance) => {
474
543
  const inherited = view.snapshot(parentId);
@@ -504,7 +573,10 @@ export const connectVersions = (view, childId, parentId, tolerance = defaultColl
504
573
  return onVersion(childId, (child, draft) => {
505
574
  handOverCarriers(view, draft, collations);
506
575
  child.edits = edits;
507
- child.basedOn = { ...assignReference(parentId), collationTolerance: tolerance };
576
+ child.basedOn = [
577
+ { ...assignReference(parentId), collationTolerance: tolerance },
578
+ ...hypothesesBeside(stateOf(child), parentId)
579
+ ];
508
580
  });
509
581
  };
510
582
  /**
@@ -514,11 +586,12 @@ export const connectVersions = (view, childId, parentId, tolerance = defaultColl
514
586
  */
515
587
  export const collateSymbols = (view, versionId, symbolIds, tolerance) => {
516
588
  const version = view.get(versionId);
517
- if (!version?.basedOn)
589
+ const principal = version && principalDerivationOf(version);
590
+ if (!version || !principal)
518
591
  return noChange;
519
592
  const chosen = new Set(symbolIds);
520
593
  const own = insertedIn([version]).filter(symbol => chosen.has(symbol.id));
521
- const collations = collationsOf(own, view.snapshot(idOf(version.basedOn)), symbol => view.placeOf(symbol), tolerance ?? collationToleranceOf(version.basedOn));
594
+ const collations = collationsOf(own, view.snapshot(idOf(principal)), symbol => view.placeOf(symbol), tolerance ?? collationToleranceOf(principal));
522
595
  const collated = new Set(collations.map(({ symbol }) => symbol.id));
523
596
  return onVersion(versionId, (version, draft) => {
524
597
  handOverCarriers(view, draft, collations);
@@ -527,8 +600,8 @@ export const collateSymbols = (view, versionId, symbolIds, tolerance) => {
527
600
  };
528
601
  /**
529
602
  * Makes the version stand on its own: what it inherited becomes its
530
- * own insertions, and the link to the version it was based on goes,
531
- * with the motivations that belonged to that derivation.
603
+ * own insertions, and its derivations go, the hypotheses among them,
604
+ * with the motivations that belonged to them.
532
605
  */
533
606
  export const detachVersion = (view, versionId) => {
534
607
  const edits = view.snapshot(versionId).map(insertion);
@@ -538,13 +611,20 @@ export const detachVersion = (view, versionId) => {
538
611
  version.motivations = [];
539
612
  });
540
613
  };
541
- /** Takes the version out; whatever was based on it comes to stand on its own. */
614
+ /**
615
+ * Takes the version out. Whatever read its text against it comes to
616
+ * stand on its own, a hypothesis that something derives from it goes,
617
+ * and so does a copy's statement that it carries the version.
618
+ */
542
619
  export const removeVersion = (view, versionId) => {
543
620
  const detachments = view.edition.versions
544
- .filter(version => version.basedOn && idOf(version.basedOn) === versionId)
621
+ .filter(version => readsAgainst(version, versionId))
545
622
  .map(version => detachVersion(view, version.id));
623
+ const namesIt = (reference) => idOf(reference) === versionId;
546
624
  return draft => {
547
625
  detachments.forEach(detach => detach(draft));
626
+ draft.versions.forEach(version => dropDerivations(version, namesIt));
627
+ draft.copies.forEach(copy => dropStatements(copy, namesIt));
548
628
  draft.versions = without(draft.versions, version => version.id === versionId);
549
629
  };
550
630
  };
@@ -553,19 +633,39 @@ export const removeSymbols = (versionId, symbolIds) => onVersion(versionId, vers
553
633
  /** Moves the edits into a new version based on this one. */
554
634
  export const deriveVersion = (versionId, editIds) => onVersion(versionId, (version, draft) => {
555
635
  const chosen = new Set(editIds);
556
- const moved = version.edits.filter(edit => chosen.has(edit.id));
557
- version.edits = without(version.edits, edit => chosen.has(edit.id));
636
+ const moved = editsOf(version).filter(edit => chosen.has(edit.id));
637
+ if (version.edits)
638
+ version.edits = without(version.edits, edit => chosen.has(edit.id));
558
639
  draft.versions.push({
559
640
  type: 'Version',
560
641
  id: v4(),
561
642
  siglum: `${version.siglum}_derived`,
562
643
  system: stateOf(version).system,
563
644
  versionType: 'unicum',
564
- basedOn: assignReference(versionId),
645
+ basedOn: [assignReference(versionId)],
565
646
  edits: moved,
566
647
  motivations: []
567
648
  });
568
649
  });
650
+ /**
651
+ * States that the version may also derive from the parent, beside what
652
+ * it derives from already: a hypothesis, such as a contamination, under
653
+ * the belief given. The text stays read against the principal derivation
654
+ * unless the belief holds this one more certain. A version derives from
655
+ * itself, or twice from one parent, in no statement.
656
+ */
657
+ export const stateDerivation = (versionId, parentId, belief) => onVersion(versionId, version => {
658
+ const derivations = stateOf(version).basedOn ?? [];
659
+ if (parentId === versionId || derivations.some(derivation => idOf(derivation) === parentId))
660
+ return;
661
+ version.basedOn = [...derivations, referenceHeld(parentId, belief)];
662
+ });
663
+ /** Takes back the hypothesis that the version derives from the parent; the principal derivation goes with `detachVersion`. */
664
+ export const clearDerivation = (versionId, parentId) => onVersion(versionId, version => {
665
+ if (readsAgainst(stateOf(version), parentId))
666
+ return;
667
+ dropDerivations(version, derivation => idOf(derivation) === parentId);
668
+ });
569
669
  const sameSequence = (a, b) => a.length === b.length && a.every((value, i) => value === b[i]);
570
670
  const expressionTypesOf = (symbols) => symbols.filter((symbol) => symbol.type === 'expression').map(symbol => symbol.expressionType);
571
671
  /**
@@ -640,7 +740,7 @@ export const mergeEdits = (view, versionId, toMerge) => {
640
740
  merged.editType = guessEditType(view, versionId, merged);
641
741
  const mergedIds = new Set(toMerge.map(edit => edit.id));
642
742
  return onVersion(versionId, version => {
643
- version.edits = [...version.edits.filter(edit => !mergedIds.has(edit.id)), merged];
743
+ version.edits = [...editsOf(version).filter(edit => !mergedIds.has(edit.id)), merged];
644
744
  });
645
745
  };
646
746
  /** Replaces the edit with one edit per inserted and one per deleted symbol. */
@@ -650,7 +750,7 @@ export const splitEdit = (versionId, toSplit) => {
650
750
  ...(toSplit.delete ?? []).map(deletion)
651
751
  ];
652
752
  return onVersion(versionId, version => {
653
- version.edits = [...version.edits.filter(edit => edit.id !== toSplit.id), ...parts];
753
+ version.edits = [...editsOf(version).filter(edit => edit.id !== toSplit.id), ...parts];
654
754
  });
655
755
  };
656
756
  /**
package/lib/index.d.ts CHANGED
@@ -18,6 +18,7 @@ export * from './ReproducingSystem';
18
18
  export * from './FeatureSource';
19
19
  export * from './RollCopy';
20
20
  export * from './reservations';
21
+ export * from './witnesses';
21
22
  export * from './alignment';
22
23
  export * from './Edition';
23
24
  export * from './EditionView';
package/lib/index.js CHANGED
@@ -18,6 +18,7 @@ export * from './ReproducingSystem';
18
18
  export * from './FeatureSource';
19
19
  export * from './RollCopy';
20
20
  export * from './reservations';
21
+ export * from './witnesses';
21
22
  export * from './alignment';
22
23
  export * from './Edition';
23
24
  export * from './EditionView';
package/lib/migrate.js CHANGED
@@ -69,7 +69,27 @@ const withScale = (node) => {
69
69
  const factor = (node.conditions ?? []).find(isPaperStretch)?.factor;
70
70
  return factor === undefined ? node : { ...node, measurements: { ...node.measurements, scale: factor } };
71
71
  };
72
- const migrateNode = (node) => [withRenamedKeys, withTypology, withReferences, withKeeper, withProductionNodes, withScale]
72
+ /** A derivation written as a single one, before a version could name several. */
73
+ const isSingleDerivation = (basedOn) => basedOn !== null && typeof basedOn === 'object' && !Array.isArray(basedOn);
74
+ const withDerivationList = (node) => isSingleDerivation(node.basedOn) ? { ...node, basedOn: [node.basedOn] } : node;
75
+ /**
76
+ * A copy read on a roll reader was stated as a recording before a sound
77
+ * recording could be one. A sound recording gives no holes, so a recording
78
+ * with holes is a reading.
79
+ */
80
+ const withReadingKind = (node) => node.readFrom?.kind === 'recording' && Array.isArray(node.features)
81
+ && node.features.some((feature) => feature?.['@type'] === 'Hole')
82
+ ? { ...node, readFrom: { ...node.readFrom, kind: 'reading' } }
83
+ : node;
84
+ /** A keeper nobody could name was written as an empty one before a copy could leave it out. */
85
+ const withoutEmptyKeeper = (node) => {
86
+ if (node.keeper?.name !== '' || node.keeper.sameAs?.length)
87
+ return node;
88
+ const { keeper: _unnamed, ...rest } = node;
89
+ return rest;
90
+ };
91
+ const migrateNode = (node) => [withRenamedKeys, withTypology, withReferences, withKeeper, withoutEmptyKeeper, withProductionNodes, withScale,
92
+ withDerivationList, withReadingKind]
73
93
  .reduce((result, step) => step(result), node);
74
94
  /** The items each walked, or the very same list where the walk changed none. */
75
95
  const walked = (items) => {
@@ -181,7 +201,7 @@ const withSystems = (edition) => {
181
201
  const withEditors = (edition) => !edition.creation || edition.creation.editors
182
202
  ? edition
183
203
  : { ...edition, creation: { ...edition.creation, editors: [] } };
184
- const statesNoTolerance = (version) => version.basedOn && !version.basedOn.collationTolerance;
204
+ const statesNoTolerance = (version) => isSingleDerivation(version.basedOn) && !version.basedOn.collationTolerance;
185
205
  /**
186
206
  * The collation tolerance was the edition's before it was stated on
187
207
  * each derivation. An edition written then collated every version at
@@ -147,13 +147,12 @@ export function readFromPhillipsEroll(buffer, { system = welteT100, controlOffse
147
147
  id: v4(),
148
148
  ops: [],
149
149
  conditions: [],
150
- keeper: { name: '', sameAs: [] },
151
150
  measurements: {},
152
151
  production: { system: systemOf(system) },
153
152
  modifications: [],
154
153
  features,
155
154
  readFrom: {
156
- kind: 'recording',
155
+ kind: 'reading',
157
156
  actor: assignObject({ name: 'Phillips, Peter', sameAs: [] }),
158
157
  note: 'Read on Phillips’s pneumatic roll reader. The places are elapsed time put back onto the paper, and a hole runs as long as its switch stayed open, which is longer than the perforation.'
159
158
  }
@@ -113,7 +113,6 @@ export function readFromSpencerBar(buffer, { rowsPerInch = SPENCER_ROWS_PER_INCH
113
113
  id: v4(),
114
114
  ops: [],
115
115
  conditions: [],
116
- keeper: { name: '', sameAs: [] },
117
116
  measurements: {},
118
117
  production: { system: systemOf(system) },
119
118
  modifications: [],
@@ -157,7 +157,6 @@ export function readFromStanfordAton(atonString, { trackShift, system = welteT10
157
157
  id: v4(),
158
158
  ops: [],
159
159
  conditions: [],
160
- keeper: { name: '', sameAs: [] },
161
160
  production: { system: systemOf(system) },
162
161
  modifications: [],
163
162
  ...((scan ?? stanford) && { scan: scan ?? stanford?.scan }),
@@ -1,23 +1,30 @@
1
1
  import { RollCopy } from "./RollCopy";
2
- export declare const reservationTypes: readonly ['source-not-stated', 'source-undocumented', 'features-interpreted', 'no-physical-evidence', 'measurement-undocumented', 'not-calibrated', 'system-unknown'];
2
+ import { Version } from "./Version";
3
+ import { EditionView } from "./EditionView";
4
+ export declare const reservationTypes: readonly ['source-not-stated', 'source-undocumented', 'software-not-named', 'instrument-not-named', 'features-interpreted', 'no-physical-evidence', 'measurement-undocumented', 'not-calibrated', 'system-unknown', 'keeper-unknown'];
3
5
  export type ReservationType = typeof reservationTypes[number];
4
6
  /**
5
- * Something an edition cannot vouch for in one of its copies.
7
+ * Something an edition cannot vouch for in one of its copies or
8
+ * versions.
6
9
  *
7
- * A reservation is worked out from what the copy states about itself
8
- * and is never written into the edition: it says that knowledge of
9
- * the copy is incomplete, so filling in what is missing makes it go
10
- * away. Nothing here describes the state of the paper, which is a
10
+ * A reservation is worked out from what the copy or version states
11
+ * about itself and is never written into the edition: it says that
12
+ * knowledge of it is incomplete, so filling in what is missing makes it
13
+ * go away. Nothing here describes the state of the paper, which is a
11
14
  * condition of the copy.
12
15
  */
13
- export interface Reservation {
14
- type: ReservationType;
16
+ export interface Reservation<T extends string = ReservationType> {
17
+ type: T;
15
18
  /** What the reservation means for a reader, in one sentence. */
16
19
  note: string;
17
20
  }
18
21
  /**
19
22
  * What the edition cannot vouch for in a copy, in the order the
20
23
  * checks are listed: where its features came from first, then what
21
- * the measurement leaves open.
24
+ * the measurement leaves open, then who holds it.
22
25
  */
23
26
  export declare const reservationsAbout: (copy: RollCopy) => Reservation[];
27
+ export declare const versionReservationTypes: readonly ['text-not-stated', 'type-not-stated', 'witnessed-by-statement-only'];
28
+ export type VersionReservationType = typeof versionReservationTypes[number];
29
+ /** What the edition cannot vouch for in a version, in the order the checks are listed. */
30
+ export declare const reservationsAboutVersion: (view: EditionView, version: Readonly<Version>) => Reservation<VersionReservationType>[];
@@ -1,14 +1,18 @@
1
1
  import { bearsPhysicalEvidence, isMeasured, sourceLabels } from "./FeatureSource";
2
2
  import { calibrationOf } from "./RollCopy";
3
3
  import { trackerBarOf } from "./systems";
4
+ import { witnessesOf } from "./witnesses";
4
5
  export const reservationTypes = [
5
6
  'source-not-stated',
6
7
  'source-undocumented',
8
+ 'software-not-named',
9
+ 'instrument-not-named',
7
10
  'features-interpreted',
8
11
  'no-physical-evidence',
9
12
  'measurement-undocumented',
10
13
  'not-calibrated',
11
- 'system-unknown'
14
+ 'system-unknown',
15
+ 'keeper-unknown'
12
16
  ];
13
17
  const sourceStated = copy => copy.readFrom ? undefined : {
14
18
  type: 'source-not-stated',
@@ -23,6 +27,20 @@ const sourceDocumented = copy => {
23
27
  note: `The copy names ${sourceLabels[source.kind]} as its source, but records nobody who made it, no device and no date.`
24
28
  };
25
29
  };
30
+ /** An emulation and a recording are turned into notes by software, which says how far to trust them. */
31
+ const softwareNamed = copy => {
32
+ const source = copy.readFrom;
33
+ if (!source || isMeasured(source.kind) || source.software?.length)
34
+ return undefined;
35
+ return {
36
+ type: 'software-not-named',
37
+ note: `The copy names ${sourceLabels[source.kind]} as its source, but not the software that turned it into notes.`
38
+ };
39
+ };
40
+ const instrumentNamed = copy => copy.readFrom?.kind === 'recording' && !copy.readFrom.instrument ? {
41
+ type: 'instrument-not-named',
42
+ note: 'The copy is known from a recording, but neither the instrument it was played on nor its regulation is named.'
43
+ } : undefined;
26
44
  const featuresMeasured = copy => copy.readFrom && !isMeasured(copy.readFrom.kind) ? {
27
45
  type: 'features-interpreted',
28
46
  note: `The features come from ${sourceLabels[copy.readFrom.kind]}, in which the roll has already been read into notes and commands by somebody else.`
@@ -58,18 +76,50 @@ const systemKnown = copy => {
58
76
  note: `The copy names ${system.name || 'a reproducing system'}, which the edition has no tracker bar for, so it is read by the T-100's.`
59
77
  };
60
78
  };
79
+ const keeperKnown = copy => copy.keeper ? undefined : {
80
+ type: 'keeper-unknown',
81
+ note: 'Nobody is known to hold the copy, so its paper cannot be consulted.'
82
+ };
83
+ /** A check on what a copy's features leave open, which has nothing to say of a copy without any. */
84
+ const onFeatures = (check) => copy => copy.features.length > 0 ? check(copy) : undefined;
61
85
  const checks = [
62
86
  sourceStated,
63
87
  sourceDocumented,
64
- featuresMeasured,
65
- physicalEvidence,
66
- measurementDocumented,
67
- calibrated,
68
- systemKnown
88
+ softwareNamed,
89
+ instrumentNamed,
90
+ onFeatures(featuresMeasured),
91
+ onFeatures(physicalEvidence),
92
+ onFeatures(measurementDocumented),
93
+ onFeatures(calibrated),
94
+ onFeatures(systemKnown),
95
+ keeperKnown
69
96
  ];
70
97
  /**
71
98
  * What the edition cannot vouch for in a copy, in the order the
72
99
  * checks are listed: where its features came from first, then what
73
- * the measurement leaves open.
100
+ * the measurement leaves open, then who holds it.
74
101
  */
75
102
  export const reservationsAbout = (copy) => checks.flatMap(check => check(copy) ?? []);
103
+ export const versionReservationTypes = [
104
+ 'text-not-stated',
105
+ 'type-not-stated',
106
+ 'witnessed-by-statement-only'
107
+ ];
108
+ const textStated = (_view, version) => version.edits ? undefined : {
109
+ type: 'text-not-stated',
110
+ note: 'The version does not state its edits, so it reads as the version it derives from.'
111
+ };
112
+ const typeStated = (_view, version) => version.versionType ? undefined : {
113
+ type: 'type-not-stated',
114
+ note: 'The version does not say whether it served as a master for several copies or exists on one only.'
115
+ };
116
+ const witnessedByFeatures = (view, version) => {
117
+ const witnesses = witnessesOf(view, version.id);
118
+ return witnesses.length > 0 && witnesses.every(({ by }) => by === 'statement') ? {
119
+ type: 'witnessed-by-statement-only',
120
+ note: 'No copy\'s features carry what the version inserts; only copies that state they carry it bear witness to it.'
121
+ } : undefined;
122
+ };
123
+ const versionChecks = [textStated, typeStated, witnessedByFeatures];
124
+ /** What the edition cannot vouch for in a version, in the order the checks are listed. */
125
+ export const reservationsAboutVersion = (view, version) => versionChecks.flatMap(check => check(view, version) ?? []);