linked-rolls 0.25.0 → 0.26.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/README.md CHANGED
@@ -24,6 +24,16 @@ https://pfefferniels.github.io/linked-rolls/reo/; the w3id.org
24
24
  identifiers are not registered yet. `ontology/README.md` records the
25
25
  naming decisions.
26
26
 
27
+ Beliefs travel as JSON-LD-star annotations (`@annotation`), which state
28
+ the triple they annotate. A reference the edition holds possible,
29
+ unlikely or false is therefore written as an embedded node beside the
30
+ document: RDF names the statement without stating it, and
31
+ `importJsonLd` puts it back where it stood. A doubted value, such as a
32
+ date or an attribution, stays annotated in place for now. Reading the
33
+ beliefs needs a processor that implements JSON-LD-star, such as the
34
+ Ruby `json-ld` gem; jsonld.js ignores `@annotation`, and every belief
35
+ with it.
36
+
27
37
  ## Format revisions
28
38
 
29
39
  Files written by linked-rolls 0.1 load unchanged: `importJsonLd`
@@ -33,6 +43,19 @@ the keeper and the production metadata are nodes with a name and
33
43
  authority links, and the roll names its reproducing system. Exports
34
44
  are always in the current format.
35
45
 
46
+ ## What a version derives from
47
+
48
+ A version names the versions it is held to derive from in `basedOn`,
49
+ each under the belief it rests on. Its text is read against the
50
+ principal derivation, the first of those held most certain, and one
51
+ held unlikely or false never gives the text. The others stand as
52
+ hypotheses, such as a contamination: `stateDerivation` adds one and
53
+ `clearDerivation` takes it back. A version may leave out its `edits`
54
+ where nobody can state them, as for a text that only a recording hints
55
+ at. It then reads as the version it derives from, and
56
+ `reservationsAboutVersion` says so. A file that names a single
57
+ derivation loads as a list of one.
58
+
36
59
  ## Where a copy's features come from
37
60
 
38
61
  `readFrom` states what a copy's features were read from: the roll
@@ -6,6 +6,15 @@ export declare const certainties: readonly ['true', 'likely', 'possible', 'unlik
6
6
  * and some values in between.
7
7
  */
8
8
  export type Certainty = typeof certainties[number];
9
+ /**
10
+ * Whether a statement held with this certainty is stated as a fact when
11
+ * the edition is read as RDF. One held possible, unlikely or false is
12
+ * only quoted, so that a reader who leaves the beliefs aside does not
13
+ * take a doubted statement for the edition's own.
14
+ */
15
+ export declare const isAsserted: (certainty: Certainty) => boolean;
16
+ /** The certainty a statement is held with. One that carries no belief is stated plainly, and so held true. */
17
+ export declare const certaintyOf: (assumption: Readonly<Assumption>) => Certainty;
9
18
  /**
10
19
  * An argumentation provides reasons for a belief and
11
20
  * may be associated with a person carrying out that argumentation.
package/lib/Assumption.js CHANGED
@@ -5,6 +5,15 @@ export const certainties = [
5
5
  'unlikely',
6
6
  'false'
7
7
  ];
8
+ /**
9
+ * Whether a statement held with this certainty is stated as a fact when
10
+ * the edition is read as RDF. One held possible, unlikely or false is
11
+ * only quoted, so that a reader who leaves the beliefs aside does not
12
+ * take a doubted statement for the edition's own.
13
+ */
14
+ export const isAsserted = (certainty) => certainty === 'true' || certainty === 'likely';
15
+ /** The certainty a statement is held with. One that carries no belief is stated plainly, and so held true. */
16
+ export const certaintyOf = (assumption) => assumption['@annotation']?.belief.certainty ?? 'true';
8
17
  export function valueOf(assumption) {
9
18
  return assumption['@value'];
10
19
  }
@@ -60,6 +60,7 @@ export declare class EditionView {
60
60
  speedScalesIn(version: Readonly<Version>): number[];
61
61
  /** The copies of the version's own system that carry any of its symbols. */
62
62
  copiesOwning(version: Readonly<Version>): Readonly<RollCopy>[];
63
+ /** The version the given one's text is read against, by its principal derivation. */
63
64
  predecessorOf(versionId: string): Readonly<Version> | undefined;
64
65
  /**
65
66
  * Where along the roll the symbol lies, as its carriers put it, or
@@ -1,5 +1,5 @@
1
1
  import { withBorneFeatures } from "./Feature";
2
- import { deletedBy, insertedBy } from "./Version";
2
+ import { deletedBy, insertedBy, principalDerivationOf } from "./Version";
3
3
  import { systemIdOf } from "./TrackerBar";
4
4
  import { isPaperStretch } from "./RollCopy";
5
5
  import { idOf, idsOf } from "./Assumption";
@@ -138,8 +138,9 @@ export class EditionView {
138
138
  if (!v)
139
139
  return;
140
140
  callback(v);
141
- if (v.basedOn) {
142
- this.travelUp(idOf(v.basedOn), callback);
141
+ const principal = principalDerivationOf(v);
142
+ if (principal) {
143
+ this.travelUp(idOf(principal), callback);
143
144
  }
144
145
  }
145
146
  /** The version and its ancestors, from the version up to the root. */
@@ -200,11 +201,11 @@ export class EditionView {
200
201
  }));
201
202
  return this.edition.copies.filter(copy => carrying.has(copy.id) && systemIdOf(copy.production?.system) === system);
202
203
  }
204
+ /** The version the given one's text is read against, by its principal derivation. */
203
205
  predecessorOf(versionId) {
204
206
  const v = this.get(versionId);
205
- if (!v?.basedOn)
206
- return;
207
- return this.get(idOf(v.basedOn));
207
+ const principal = v && principalDerivationOf(v);
208
+ return principal && this.get(idOf(principal));
208
209
  }
209
210
  /**
210
211
  * Where along the roll the symbol lies, as its carriers put it, or
@@ -271,7 +272,8 @@ export class EditionView {
271
272
  }
272
273
  inStack.add(id);
273
274
  let gen;
274
- const basedOn = node.basedOn && idOf(node.basedOn);
275
+ const principal = principalDerivationOf(node);
276
+ const basedOn = principal && idOf(principal);
275
277
  if (basedOn === undefined) {
276
278
  gen = 0; // root
277
279
  }
package/lib/Version.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Edit } from "./Edit";
2
2
  import { Concept } from "./Agent";
3
- import { ActorAssignment, DateAssignment, ReferenceAssumption } from "./Assumption";
3
+ import { ActorAssignment, Certainty, DateAssignment, ReferenceAssumption } from "./Assumption";
4
4
  import { CollationTolerance } from "./Collation";
5
5
  import { AnySymbol } from "./Symbol";
6
6
  import { WithId, WithNote, WithType } from "./utils";
@@ -98,15 +98,20 @@ export interface Version extends WithId, WithType<'Version'> {
98
98
  system: Concept;
99
99
  /**
100
100
  * Whether the version served as a master for reproductions
101
- * or exists on one copy only.
101
+ * or exists on one copy only. Left out where that is not known,
102
+ * as for a version only a secondary witness hints at.
102
103
  * @see crm:P2 has type
103
104
  */
104
- versionType: VersionType;
105
+ versionType?: VersionType;
105
106
  /**
106
- * If no derivation is defined, it is assumed that this version represents the mother roll.
107
+ * The versions this one is held to derive from, each under the
108
+ * belief it rests on. The text is read against the principal one
109
+ * (`principalDerivationOf`); the others stand as hypotheses, such as
110
+ * a contamination. A version that names none represents the mother
111
+ * roll.
107
112
  * @see lrmoo:R76 is derivative of
108
113
  */
109
- basedOn?: Derivation;
114
+ basedOn?: Derivation[];
110
115
  /**
111
116
  * The act that made this version, where it is known: who carried it
112
117
  * out, when, and by what rule.
@@ -114,16 +119,32 @@ export interface Version extends WithId, WithType<'Version'> {
114
119
  */
115
120
  creation?: VersionCreation;
116
121
  /**
117
- * The list of edits that, applied to the base version, produce this version.
122
+ * The list of edits that, applied to the base version, produce this
123
+ * version. A hypothetical version whose changes nobody can state
124
+ * leaves it out; it then reads as the version it derives from.
118
125
  * @see reo:involvedEdit
119
126
  */
120
- edits: Edit[];
127
+ edits?: Edit[];
121
128
  /**
122
129
  * A collection of motivations used in this version's edits.
123
130
  */
124
131
  motivations: Motivation[];
125
132
  }
133
+ /** The edits the version states, none where it leaves its text unstated. */
134
+ export declare const editsOf: (version: Readonly<Version>) => Edit[];
126
135
  /** The symbols the version's edits insert. */
127
136
  export declare const insertedBy: (version: Readonly<Version>) => AnySymbol[];
128
137
  /** The ids of the symbols the version's edits delete. */
129
138
  export declare const deletedBy: (version: Readonly<Version>) => string[];
139
+ /** The parents the version names, each with the certainty its derivation is held with. */
140
+ export declare const derivationsOf: (version: Readonly<Version>) => {
141
+ parent: string;
142
+ certainty: Certainty;
143
+ }[];
144
+ /**
145
+ * The derivation the version's text is read against: the first of those
146
+ * held most certain. One held unlikely or false is a rejected hypothesis
147
+ * and gives no text. Lowering the certainty of the principal derivation
148
+ * below another's reads the version's edits against another parent.
149
+ */
150
+ export declare const principalDerivationOf: (version: Readonly<Version>) => Readonly<Derivation> | undefined;
package/lib/Version.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { certainties, certaintyOf, idOf } from "./Assumption";
1
2
  import { defaultCollationTolerance } from "./Collation";
2
3
  export const versionTypes = [
3
4
  /**
@@ -12,7 +13,21 @@ export const versionTypes = [
12
13
  ];
13
14
  /** The tolerance the derivation was collated at, or the default where it states none. */
14
15
  export const collationToleranceOf = (derivation) => derivation.collationTolerance ?? defaultCollationTolerance;
16
+ /** The edits the version states, none where it leaves its text unstated. */
17
+ export const editsOf = (version) => version.edits ?? [];
15
18
  /** The symbols the version's edits insert. */
16
- export const insertedBy = (version) => version.edits.flatMap(edit => edit.insert ?? []);
19
+ export const insertedBy = (version) => editsOf(version).flatMap(edit => edit.insert ?? []);
17
20
  /** The ids of the symbols the version's edits delete. */
18
- export const deletedBy = (version) => version.edits.flatMap(edit => edit.delete ?? []);
21
+ export const deletedBy = (version) => editsOf(version).flatMap(edit => edit.delete ?? []);
22
+ /** The parents the version names, each with the certainty its derivation is held with. */
23
+ export const derivationsOf = (version) => (version.basedOn ?? []).map(derivation => ({ parent: idOf(derivation), certainty: certaintyOf(derivation) }));
24
+ const rankOf = (derivation) => certainties.indexOf(certaintyOf(derivation));
25
+ /**
26
+ * The derivation the version's text is read against: the first of those
27
+ * held most certain. One held unlikely or false is a rejected hypothesis
28
+ * and gives no text. Lowering the certainty of the principal derivation
29
+ * below another's reads the version's edits against another parent.
30
+ */
31
+ export const principalDerivationOf = (version) => (version.basedOn ?? [])
32
+ .filter(derivation => rankOf(derivation) <= certainties.indexOf('possible'))
33
+ .reduce((principal, derivation) => principal === undefined || rankOf(derivation) < rankOf(principal) ? derivation : principal, undefined);
package/lib/asJsonLd.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { systemIdIn } from "./TrackerBar";
2
+ import { certaintyOf, isAsserted } from "./Assumption";
3
+ import context from "./spec/context.json";
2
4
  export const exportDate = (date) => {
3
5
  const year = date.getFullYear();
4
6
  const month = String(date.getMonth() + 1).padStart(2, "0");
@@ -62,9 +64,76 @@ const withSystemContexts = (node) => {
62
64
  ? { '@context': `https://w3id.org/reo/${system}/context.jsonld`, ...walked }
63
65
  : walked;
64
66
  };
67
+ const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
68
+ /** Terms the context sets to null, which say nothing when the edition is read as RDF. */
69
+ const silentTerms = new Set(Object.entries(context['@context'])
70
+ .filter(([, definition]) => definition === null)
71
+ .map(([term]) => term));
72
+ /**
73
+ * A reference its belief does not hold to be so: it names a node, states
74
+ * nothing RDF would read besides, and its belief is below likely.
75
+ */
76
+ const isDoubtedReference = (value) => isRecord(value)
77
+ && typeof value['@id'] === 'string'
78
+ && isRecord(value['@annotation'])
79
+ && !isAsserted(certaintyOf(value))
80
+ && Object.keys(value).every(key => key === '@id' || key === '@annotation' || silentTerms.has(key));
81
+ /**
82
+ * The statement a doubted reference makes, as a JSON-LD-star embedded
83
+ * node: the triple is named without being stated, and the belief is
84
+ * about it. The annotation's own id goes along under a key RDF does not
85
+ * read, so that an import can put it back.
86
+ */
87
+ const quote = (subject, key, reference, listed) => {
88
+ const { '@annotation': { '@id': annotation, ...about }, ...object } = reference;
89
+ return { '@id': { '@id': subject, [key]: listed ? [object] : object }, annotation, ...about };
90
+ };
91
+ /** The value a node states under a key, less the doubted references, and those references quoted. */
92
+ const quotingValue = (subject, key, value) => {
93
+ if (Array.isArray(value)) {
94
+ return {
95
+ node: value.filter(item => !isDoubtedReference(item)),
96
+ quoted: value.filter(isDoubtedReference).map(item => quote(subject, key, item, true))
97
+ };
98
+ }
99
+ return isDoubtedReference(value)
100
+ ? { node: undefined, quoted: [quote(subject, key, value, false)] }
101
+ : { node: value, quoted: [] };
102
+ };
103
+ /**
104
+ * The document with every doubted reference taken off the node that
105
+ * states it, and quoted instead.
106
+ *
107
+ * An `@annotation` in JSON-LD-star states the triple it annotates and
108
+ * then says something about it, so a statement the edition holds
109
+ * possible, unlikely or false would reach RDF as a fact. Only references
110
+ * between nodes are quoted, since only they can be put back where they
111
+ * stood; a doubted date or attribution stays annotated in place.
112
+ */
113
+ const withDoubtedReferencesQuoted = (value) => {
114
+ if (Array.isArray(value)) {
115
+ const quotings = value.map(withDoubtedReferencesQuoted);
116
+ return { node: quotings.map(({ node }) => node), quoted: quotings.flatMap(({ quoted }) => quoted) };
117
+ }
118
+ if (!isRecord(value))
119
+ return { node: value, quoted: [] };
120
+ const subject = value['@id'];
121
+ const entries = Object.entries(value).map(([key, child]) => {
122
+ const own = typeof subject === 'string' && !key.startsWith('@')
123
+ ? quotingValue(subject, key, child)
124
+ : { node: child, quoted: [] };
125
+ const below = withDoubtedReferencesQuoted(own.node);
126
+ return { key, node: below.node, quoted: [...own.quoted, ...below.quoted] };
127
+ });
128
+ return {
129
+ node: Object.fromEntries(entries.filter(({ node }) => node !== undefined).map(({ key, node }) => [key, node])),
130
+ quoted: entries.flatMap(({ quoted }) => quoted)
131
+ };
132
+ };
65
133
  export const asJsonLd = (edition) => {
134
+ const { node, quoted } = withDoubtedReferencesQuoted(withSystemContexts(asJsonLdEntity(edition)));
66
135
  // The context is the export's own; one carried in from an import must not override it.
67
- const { base, '@context': carried, ...rest } = withSystemContexts(asJsonLdEntity(edition));
136
+ const { base, '@context': carried, ...rest } = node;
68
137
  return {
69
138
  '@context': [
70
139
  'https://w3id.org/reo/context.jsonld',
@@ -74,6 +143,7 @@ export const asJsonLd = (edition) => {
74
143
  ],
75
144
  '@type': "Edition",
76
145
  '@id': edition.base,
77
- ...rest
146
+ ...rest,
147
+ ...(quoted.length > 0 && { '@included': quoted })
78
148
  };
79
149
  };
@@ -6,7 +6,7 @@ import { CollationTolerance } from "./Collation";
6
6
  import { Edit } from "./Edit";
7
7
  import { GeneralRollCondition, RollCopy, ScaleReading, Shift } from "./RollCopy";
8
8
  import { FeatureSource } from "./FeatureSource";
9
- import { AnyArgumentation, Certainty, ObjectAssumption } from "./Assumption";
9
+ import { AnyArgumentation, Belief, Certainty, ObjectAssumption } from "./Assumption";
10
10
  import { AnyFeature, FeatureConditionAssignment } from "./Feature";
11
11
  /**
12
12
  * A change to an edition, written onto an immer draft of it. One
@@ -99,7 +99,8 @@ export declare const mergeFeatures: (copyId: string, featureIds: readonly string
99
99
  * with one the parent hands down adds its carriers to that symbol; the
100
100
  * rest become the child's insertions, and what the parent hands down
101
101
  * and the child lacks becomes its deletions. The derivation states the
102
- * tolerance it was collated at.
102
+ * tolerance it was collated at and becomes the principal one; the
103
+ * hypotheses the child stated beside its former one stay.
103
104
  */
104
105
  export declare const connectVersions: (view: EditionView, childId: string, parentId: string, tolerance?: CollationTolerance) => EditionOp;
105
106
  /**
@@ -110,16 +111,29 @@ export declare const connectVersions: (view: EditionView, childId: string, paren
110
111
  export declare const collateSymbols: (view: EditionView, versionId: string, symbolIds: readonly string[], tolerance?: CollationTolerance) => EditionOp;
111
112
  /**
112
113
  * Makes the version stand on its own: what it inherited becomes its
113
- * own insertions, and the link to the version it was based on goes,
114
- * with the motivations that belonged to that derivation.
114
+ * own insertions, and its derivations go, the hypotheses among them,
115
+ * with the motivations that belonged to them.
115
116
  */
116
117
  export declare const detachVersion: (view: EditionView, versionId: string) => EditionOp;
117
- /** Takes the version out; whatever was based on it comes to stand on its own. */
118
+ /**
119
+ * Takes the version out. Whatever read its text against it comes to
120
+ * stand on its own, and a hypothesis that something derives from it goes.
121
+ */
118
122
  export declare const removeVersion: (view: EditionView, versionId: string) => EditionOp;
119
123
  /** Takes the symbols out of the version's own insertions, and the edits that had nothing else. */
120
124
  export declare const removeSymbols: (versionId: string, symbolIds: readonly string[]) => EditionOp;
121
125
  /** Moves the edits into a new version based on this one. */
122
126
  export declare const deriveVersion: (versionId: string, editIds: readonly string[]) => EditionOp;
127
+ /**
128
+ * States that the version may also derive from the parent, beside what
129
+ * it derives from already: a hypothesis, such as a contamination, under
130
+ * the belief given. The text stays read against the principal derivation
131
+ * unless the belief holds this one more certain. A version derives from
132
+ * itself, or twice from one parent, in no statement.
133
+ */
134
+ export declare const stateDerivation: (versionId: string, parentId: string, belief?: Belief) => EditionOp;
135
+ /** Takes back the hypothesis that the version derives from the parent; the principal derivation goes with `detachVersion`. */
136
+ export declare const clearDerivation: (versionId: string, parentId: string) => EditionOp;
123
137
  /**
124
138
  * Replaces the edits with a single one carrying all their insertions
125
139
  * and deletions, classified by a guess at what the exchange does.
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
@@ -278,15 +280,17 @@ const forgetFeatures = (draft, features) => {
278
280
  const versions = stateOf(draft.versions);
279
281
  const dropped = new Set(insertedIn(versions).filter(carriedOnlyOn(features)).map(symbol => symbol.id));
280
282
  const forget = forgettingFeatures(features, dropped);
281
- const edits = versions.map(version => edited(version.edits, forget));
283
+ const edits = versions.map(version => version.edits && edited(version.edits, forget));
282
284
  const gone = new Set([
283
285
  ...features,
284
286
  ...dropped,
285
- ...versions.flatMap((version, i) => droppedIds(version.edits, edits[i]))
287
+ ...versions.flatMap((version, i) => droppedIds(editsOf(version), edits[i] ?? []))
286
288
  ]);
287
289
  renameReferences(draft, ids => without(ids, id => gone.has(id)));
288
290
  draft.versions.forEach((version, i) => {
289
- version.edits = edits[i];
291
+ const stated = edits[i];
292
+ if (stated)
293
+ version.edits = stated;
290
294
  });
291
295
  };
292
296
  /**
@@ -417,7 +421,9 @@ const carryOver = (draft, replaced, mergedId) => {
417
421
  };
418
422
  const versions = stateOf(draft.versions);
419
423
  draft.versions.forEach((version, i) => {
420
- version.edits = mapped(versions[i].edits, recarrying);
424
+ const edits = versions[i].edits;
425
+ if (edits)
426
+ version.edits = mapped(edits, recarrying);
421
427
  });
422
428
  };
423
429
  /**
@@ -463,12 +469,34 @@ const differ = (child, parent) => {
463
469
  const other = trackerBarOf(parent?.system);
464
470
  return one !== undefined && other !== undefined && one.id !== other.id;
465
471
  };
472
+ /** Whether the version's text is read against the parent, its principal derivation naming it. */
473
+ const readsAgainst = (version, parentId) => {
474
+ const principal = principalDerivationOf(version);
475
+ return principal !== undefined && idOf(principal) === parentId;
476
+ };
477
+ /** The derivations the version states beside its principal one, less any naming the parent. */
478
+ const hypothesesBeside = (version, parentId) => {
479
+ const principal = principalDerivationOf(version);
480
+ return (version.basedOn ?? []).filter(derivation => derivation !== principal && idOf(derivation) !== parentId);
481
+ };
482
+ /** Takes out the derivations that match, and the list itself where none is left. */
483
+ const dropDerivations = (version, matches) => {
484
+ const derivations = stateOf(version).basedOn;
485
+ if (!derivations?.some(matches))
486
+ return;
487
+ const kept = derivations.filter(derivation => !matches(derivation));
488
+ if (kept.length > 0)
489
+ version.basedOn = kept;
490
+ else
491
+ delete version.basedOn;
492
+ };
466
493
  /**
467
494
  * Bases the child on the parent. A symbol of the child that collates
468
495
  * with one the parent hands down adds its carriers to that symbol; the
469
496
  * rest become the child's insertions, and what the parent hands down
470
497
  * and the child lacks becomes its deletions. The derivation states the
471
- * tolerance it was collated at.
498
+ * tolerance it was collated at and becomes the principal one; the
499
+ * hypotheses the child stated beside its former one stay.
472
500
  */
473
501
  export const connectVersions = (view, childId, parentId, tolerance = defaultCollationTolerance) => {
474
502
  const inherited = view.snapshot(parentId);
@@ -504,7 +532,10 @@ export const connectVersions = (view, childId, parentId, tolerance = defaultColl
504
532
  return onVersion(childId, (child, draft) => {
505
533
  handOverCarriers(view, draft, collations);
506
534
  child.edits = edits;
507
- child.basedOn = { ...assignReference(parentId), collationTolerance: tolerance };
535
+ child.basedOn = [
536
+ { ...assignReference(parentId), collationTolerance: tolerance },
537
+ ...hypothesesBeside(stateOf(child), parentId)
538
+ ];
508
539
  });
509
540
  };
510
541
  /**
@@ -514,11 +545,12 @@ export const connectVersions = (view, childId, parentId, tolerance = defaultColl
514
545
  */
515
546
  export const collateSymbols = (view, versionId, symbolIds, tolerance) => {
516
547
  const version = view.get(versionId);
517
- if (!version?.basedOn)
548
+ const principal = version && principalDerivationOf(version);
549
+ if (!version || !principal)
518
550
  return noChange;
519
551
  const chosen = new Set(symbolIds);
520
552
  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));
553
+ const collations = collationsOf(own, view.snapshot(idOf(principal)), symbol => view.placeOf(symbol), tolerance ?? collationToleranceOf(principal));
522
554
  const collated = new Set(collations.map(({ symbol }) => symbol.id));
523
555
  return onVersion(versionId, (version, draft) => {
524
556
  handOverCarriers(view, draft, collations);
@@ -527,8 +559,8 @@ export const collateSymbols = (view, versionId, symbolIds, tolerance) => {
527
559
  };
528
560
  /**
529
561
  * 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.
562
+ * own insertions, and its derivations go, the hypotheses among them,
563
+ * with the motivations that belonged to them.
532
564
  */
533
565
  export const detachVersion = (view, versionId) => {
534
566
  const edits = view.snapshot(versionId).map(insertion);
@@ -538,13 +570,17 @@ export const detachVersion = (view, versionId) => {
538
570
  version.motivations = [];
539
571
  });
540
572
  };
541
- /** Takes the version out; whatever was based on it comes to stand on its own. */
573
+ /**
574
+ * Takes the version out. Whatever read its text against it comes to
575
+ * stand on its own, and a hypothesis that something derives from it goes.
576
+ */
542
577
  export const removeVersion = (view, versionId) => {
543
578
  const detachments = view.edition.versions
544
- .filter(version => version.basedOn && idOf(version.basedOn) === versionId)
579
+ .filter(version => readsAgainst(version, versionId))
545
580
  .map(version => detachVersion(view, version.id));
546
581
  return draft => {
547
582
  detachments.forEach(detach => detach(draft));
583
+ draft.versions.forEach(version => dropDerivations(version, derivation => idOf(derivation) === versionId));
548
584
  draft.versions = without(draft.versions, version => version.id === versionId);
549
585
  };
550
586
  };
@@ -553,19 +589,42 @@ export const removeSymbols = (versionId, symbolIds) => onVersion(versionId, vers
553
589
  /** Moves the edits into a new version based on this one. */
554
590
  export const deriveVersion = (versionId, editIds) => onVersion(versionId, (version, draft) => {
555
591
  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));
592
+ const moved = editsOf(version).filter(edit => chosen.has(edit.id));
593
+ if (version.edits)
594
+ version.edits = without(version.edits, edit => chosen.has(edit.id));
558
595
  draft.versions.push({
559
596
  type: 'Version',
560
597
  id: v4(),
561
598
  siglum: `${version.siglum}_derived`,
562
599
  system: stateOf(version).system,
563
600
  versionType: 'unicum',
564
- basedOn: assignReference(versionId),
601
+ basedOn: [assignReference(versionId)],
565
602
  edits: moved,
566
603
  motivations: []
567
604
  });
568
605
  });
606
+ /**
607
+ * States that the version may also derive from the parent, beside what
608
+ * it derives from already: a hypothesis, such as a contamination, under
609
+ * the belief given. The text stays read against the principal derivation
610
+ * unless the belief holds this one more certain. A version derives from
611
+ * itself, or twice from one parent, in no statement.
612
+ */
613
+ export const stateDerivation = (versionId, parentId, belief) => onVersion(versionId, version => {
614
+ const derivations = stateOf(version).basedOn ?? [];
615
+ if (parentId === versionId || derivations.some(derivation => idOf(derivation) === parentId))
616
+ return;
617
+ version.basedOn = [
618
+ ...derivations,
619
+ { ...assignReference(parentId), ...(belief && { '@annotation': { id: v4(), belief } }) }
620
+ ];
621
+ });
622
+ /** Takes back the hypothesis that the version derives from the parent; the principal derivation goes with `detachVersion`. */
623
+ export const clearDerivation = (versionId, parentId) => onVersion(versionId, version => {
624
+ if (readsAgainst(stateOf(version), parentId))
625
+ return;
626
+ dropDerivations(version, derivation => idOf(derivation) === parentId);
627
+ });
569
628
  const sameSequence = (a, b) => a.length === b.length && a.every((value, i) => value === b[i]);
570
629
  const expressionTypesOf = (symbols) => symbols.filter((symbol) => symbol.type === 'expression').map(symbol => symbol.expressionType);
571
630
  /**
@@ -640,7 +699,7 @@ export const mergeEdits = (view, versionId, toMerge) => {
640
699
  merged.editType = guessEditType(view, versionId, merged);
641
700
  const mergedIds = new Set(toMerge.map(edit => edit.id));
642
701
  return onVersion(versionId, version => {
643
- version.edits = [...version.edits.filter(edit => !mergedIds.has(edit.id)), merged];
702
+ version.edits = [...editsOf(version).filter(edit => !mergedIds.has(edit.id)), merged];
644
703
  });
645
704
  };
646
705
  /** Replaces the edit with one edit per inserted and one per deleted symbol. */
@@ -650,7 +709,7 @@ export const splitEdit = (versionId, toSplit) => {
650
709
  ...(toSplit.delete ?? []).map(deletion)
651
710
  ];
652
711
  return onVersion(versionId, version => {
653
- version.edits = [...version.edits.filter(edit => edit.id !== toSplit.id), ...parts];
712
+ version.edits = [...editsOf(version).filter(edit => edit.id !== toSplit.id), ...parts];
654
713
  });
655
714
  };
656
715
  /**
package/lib/migrate.js CHANGED
@@ -69,7 +69,10 @@ 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
+ const migrateNode = (node) => [withRenamedKeys, withTypology, withReferences, withKeeper, withProductionNodes, withScale, withDerivationList]
73
76
  .reduce((result, step) => step(result), node);
74
77
  /** The items each walked, or the very same list where the walk changed none. */
75
78
  const walked = (items) => {
@@ -181,7 +184,7 @@ const withSystems = (edition) => {
181
184
  const withEditors = (edition) => !edition.creation || edition.creation.editors
182
185
  ? edition
183
186
  : { ...edition, creation: { ...edition.creation, editors: [] } };
184
- const statesNoTolerance = (version) => version.basedOn && !version.basedOn.collationTolerance;
187
+ const statesNoTolerance = (version) => isSingleDerivation(version.basedOn) && !version.basedOn.collationTolerance;
185
188
  /**
186
189
  * The collation tolerance was the edition's before it was stated on
187
190
  * each derivation. An edition written then collated every version at
@@ -200,5 +203,51 @@ const withDerivationTolerance = (edition) => {
200
203
  : version)
201
204
  };
202
205
  };
203
- const editionSteps = [withSystems, withEditors, withDerivationTolerance];
206
+ /** A statement an export quoted rather than stated: an included node whose id is a triple. */
207
+ const isQuotedStatement = (node) => node !== null && typeof node === 'object' && node['@id'] !== null && typeof node['@id'] === 'object';
208
+ /** The reference a quoted statement made, annotated again with the belief the export set beside it. */
209
+ const referenceOf = (statement) => {
210
+ const { '@id': { '@id': subject, ...made }, annotation, ...about } = statement;
211
+ const [key, value] = Object.entries(made)[0];
212
+ const listed = Array.isArray(value);
213
+ return {
214
+ subject,
215
+ key,
216
+ listed,
217
+ reference: {
218
+ ...(listed ? value[0] : value),
219
+ '@annotation': { ...(annotation !== undefined && { '@id': annotation }), ...about }
220
+ }
221
+ };
222
+ };
223
+ /** The document with each quoted reference back on the node that makes it, a node being what has a type. */
224
+ const withReferencesOn = (value, bySubject) => {
225
+ if (Array.isArray(value))
226
+ return value.map(item => withReferencesOn(item, bySubject));
227
+ if (!value || typeof value !== 'object')
228
+ return value;
229
+ const walked = Object.fromEntries(Object.entries(value).map(([key, child]) => [key, withReferencesOn(child, bySubject)]));
230
+ const references = typeof value['@id'] === 'string' && value['@type'] !== undefined
231
+ ? bySubject.get(value['@id']) ?? []
232
+ : [];
233
+ return references.reduce((node, { key, listed, reference }) => ({ ...node, [key]: listed ? [...(node[key] ?? []), reference] : reference }), walked);
234
+ };
235
+ /**
236
+ * Puts back what an export quoted. A reference the edition doubts goes
237
+ * out as a JSON-LD-star embedded node beside the document, so that RDF
238
+ * does not state it; in the edition it belongs on the node that makes
239
+ * it, under its belief. In a list it comes back after the references
240
+ * that were stated.
241
+ */
242
+ const withQuotedStatementsInPlace = (edition) => {
243
+ const included = Array.isArray(edition['@included']) ? edition['@included'] : [];
244
+ const statements = included.filter(isQuotedStatement);
245
+ if (statements.length === 0)
246
+ return edition;
247
+ const others = included.filter(node => !isQuotedStatement(node));
248
+ const { '@included': _quoted, ...rest } = edition;
249
+ const bySubject = Map.groupBy(statements.map(referenceOf), ({ subject }) => subject);
250
+ return withReferencesOn({ ...rest, ...(others.length > 0 && { '@included': others }) }, bySubject);
251
+ };
252
+ const editionSteps = [withQuotedStatementsInPlace, withSystems, withEditors, withDerivationTolerance];
204
253
  export const migrate = (edition) => walk(editionSteps.reduce((result, step) => step(result), edition));
@@ -1,17 +1,19 @@
1
1
  import { RollCopy } from "./RollCopy";
2
+ import { Version } from "./Version";
2
3
  export declare const reservationTypes: readonly ['source-not-stated', 'source-undocumented', 'features-interpreted', 'no-physical-evidence', 'measurement-undocumented', 'not-calibrated', 'system-unknown'];
3
4
  export type ReservationType = typeof reservationTypes[number];
4
5
  /**
5
- * Something an edition cannot vouch for in one of its copies.
6
+ * Something an edition cannot vouch for in one of its copies or
7
+ * versions.
6
8
  *
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
9
+ * A reservation is worked out from what the copy or version states
10
+ * about itself and is never written into the edition: it says that
11
+ * knowledge of it is incomplete, so filling in what is missing makes it
12
+ * go away. Nothing here describes the state of the paper, which is a
11
13
  * condition of the copy.
12
14
  */
13
- export interface Reservation {
14
- type: ReservationType;
15
+ export interface Reservation<T extends string = ReservationType> {
16
+ type: T;
15
17
  /** What the reservation means for a reader, in one sentence. */
16
18
  note: string;
17
19
  }
@@ -21,3 +23,7 @@ export interface Reservation {
21
23
  * the measurement leaves open.
22
24
  */
23
25
  export declare const reservationsAbout: (copy: RollCopy) => Reservation[];
26
+ export declare const versionReservationTypes: readonly ['text-not-stated', 'type-not-stated'];
27
+ export type VersionReservationType = typeof versionReservationTypes[number];
28
+ /** What the edition cannot vouch for in a version, in the order the checks are listed. */
29
+ export declare const reservationsAboutVersion: (version: Readonly<Version>) => Reservation<VersionReservationType>[];
@@ -73,3 +73,18 @@ const checks = [
73
73
  * the measurement leaves open.
74
74
  */
75
75
  export const reservationsAbout = (copy) => checks.flatMap(check => check(copy) ?? []);
76
+ export const versionReservationTypes = [
77
+ 'text-not-stated',
78
+ 'type-not-stated'
79
+ ];
80
+ const textStated = version => version.edits ? undefined : {
81
+ type: 'text-not-stated',
82
+ note: 'The version does not state its edits, so it reads as the version it derives from.'
83
+ };
84
+ const typeStated = version => version.versionType ? undefined : {
85
+ type: 'type-not-stated',
86
+ note: 'The version does not say whether it served as a master for several copies or exists on one only.'
87
+ };
88
+ const versionChecks = [textStated, typeStated];
89
+ /** What the edition cannot vouch for in a version, in the order the checks are listed. */
90
+ export const reservationsAboutVersion = (version) => versionChecks.flatMap(check => check(version) ?? []);
package/lib/schema.json CHANGED
@@ -2270,8 +2270,11 @@
2270
2270
  "description": "A version is defined by the sum of edits applied to the version it is based on. For simple identification, a siglum is given to each version.",
2271
2271
  "properties": {
2272
2272
  "basedOn": {
2273
- "$ref": "#/definitions/Derivation",
2274
- "description": "If no derivation is defined, it is assumed that this version represents the mother roll.",
2273
+ "description": "The versions this one is held to derive from, each under the belief it rests on. The text is read against the principal one (`principalDerivationOf`); the others stand as hypotheses, such as a contamination. A version that names none represents the mother roll.",
2274
+ "items": {
2275
+ "$ref": "#/definitions/Derivation"
2276
+ },
2277
+ "type": "array",
2275
2278
  "ontology": "lrmoo:R76 is derivative of"
2276
2279
  },
2277
2280
  "creation": {
@@ -2280,7 +2283,7 @@
2280
2283
  "ontology": "lrmoo:R17i was created by"
2281
2284
  },
2282
2285
  "edits": {
2283
- "description": "The list of edits that, applied to the base version, produce this version.",
2286
+ "description": "The list of edits that, applied to the base version, produce this version. A hypothetical version whose changes nobody can state leaves it out; it then reads as the version it derives from.",
2284
2287
  "items": {
2285
2288
  "$ref": "#/definitions/Edit"
2286
2289
  },
@@ -2306,7 +2309,7 @@
2306
2309
  },
2307
2310
  "versionType": {
2308
2311
  "$ref": "#/definitions/VersionType",
2309
- "description": "Whether the version served as a master for reproductions or exists on one copy only.",
2312
+ "description": "Whether the version served as a master for reproductions or exists on one copy only. Left out where that is not known, as for a version only a secondary witness hints at.",
2310
2313
  "ontology": "crm:P2 has type"
2311
2314
  },
2312
2315
  "@id": {
@@ -2321,13 +2324,11 @@
2321
2324
  }
2322
2325
  },
2323
2326
  "required": [
2324
- "edits",
2325
2327
  "@id",
2326
2328
  "motivations",
2327
2329
  "siglum",
2328
2330
  "system",
2329
- "@type",
2330
- "versionType"
2331
+ "@type"
2331
2332
  ],
2332
2333
  "type": "object",
2333
2334
  "ontology": "lrmoo:F2 Expression"
@@ -84,6 +84,7 @@
84
84
  "@type": "xsd:date"
85
85
  },
86
86
  "collationTolerance": null,
87
+ "annotation": null,
87
88
  "roll": "lrmoo:R3i_realises",
88
89
  "catalogueNumber": "dcterms:identifier",
89
90
  "system": "crm:P2_has_type",
@@ -117,7 +118,12 @@
117
118
  "role": {
118
119
  "@id": "crm:P2_has_type",
119
120
  "@type": "@vocab",
120
- "@context": { "@vocab": "https://w3id.org/reo/type/" }
121
+ "@context": {
122
+ "@vocab": "https://w3id.org/reo/type/",
123
+ "pianist": "https://w3id.org/reo/type/pianist",
124
+ "publisher": "https://w3id.org/reo/type/publisher",
125
+ "transcription": "https://w3id.org/reo/type/transcription"
126
+ }
121
127
  },
122
128
  "ops": null,
123
129
  "measurements": {
@@ -173,7 +179,11 @@
173
179
  "kind": {
174
180
  "@id": "crm:P2_has_type",
175
181
  "@type": "@vocab",
176
- "@context": { "@vocab": "https://w3id.org/reo/type/" }
182
+ "@context": {
183
+ "@vocab": "https://w3id.org/reo/type/",
184
+ "roll": "https://w3id.org/reo/type/roll",
185
+ "scan": "https://w3id.org/reo/type/scan"
186
+ }
177
187
  },
178
188
  "output": {
179
189
  "@id": "crmdig:L11_had_output",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linked-rolls",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Digital editions of piano rolls: import, collation, editorial assumptions, JSON-LD export, and emulation through a reproducing system",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -44,7 +44,9 @@
44
44
  "build:docs": "node schema/docs/generate.ts src/schema.json docs/index.html"
45
45
  },
46
46
  "devDependencies": {
47
+ "@types/jsonld": "^1.5.15",
47
48
  "@types/node": "^24.13.3",
49
+ "jsonld": "^9.0.0",
48
50
  "ts-json-schema-generator": "^2.9.0",
49
51
  "typescript": "^7.0.2",
50
52
  "vite": "^8.2.2",