linked-rolls 0.13.0 → 0.15.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
@@ -33,6 +33,41 @@ the keeper and the production metadata are nodes with a name and
33
33
  authority links, and the roll names its reproducing system. Exports
34
34
  are always in the current format.
35
35
 
36
+ ## Where a copy's features come from
37
+
38
+ `readFrom` states what a copy's features were read from: the roll
39
+ itself, a `scan` of it, an `analysis` somebody else measured on a
40
+ scan, an `emulation` in which the roll has already been read into
41
+ notes and commands, or a `recording` captured while the copy was
42
+ played. Beside the kind it holds who carried the capture out, on what
43
+ device, when, and a note. What is not known is left out.
44
+
45
+ This says where the numbers of the edition come from and nothing
46
+ about the state of the paper, which is a condition of the copy. In
47
+ RDF it is `reo:capture`, an activity typed by what it read from; the
48
+ file it produced is `crmdig:L11 had output`, the machine it ran on
49
+ `crmdig:L12 happened on device`.
50
+
51
+ Nothing records that a copy is doubtful. `reservationsAbout` works
52
+ out from what a copy states what the edition cannot vouch for in it:
53
+ that it names no source, that the making of its source is
54
+ undocumented, that its features are somebody else's reading, that its
55
+ source bears no physical evidence, that no measuring software is
56
+ recorded, that it is not calibrated. A reservation goes away when the
57
+ gap it names is filled.
58
+
59
+ ```ts
60
+ import { reservationsAbout, stateSource } from 'linked-rolls'
61
+
62
+ const next = produce(edition, stateSource(copyId, {
63
+ kind: 'emulation',
64
+ output: 'https://example.org/wm225.mid',
65
+ note: 'MIDI from a third party; the emulator is not named.'
66
+ }))
67
+
68
+ reservationsAbout(next.copies[0]).map(reservation => reservation.note)
69
+ ```
70
+
36
71
  ## Emulation
37
72
 
38
73
  `Emulation` turns a version of the edition into MIDI. The core of the
@@ -0,0 +1,65 @@
1
+ import { ActorAssignment, DateAssignment } from "./Assumption";
2
+ import { Concept } from "./Agent";
3
+ import { WithNote } from "./utils";
4
+ /**
5
+ * What a copy's features were read from. The kinds run from the paper
6
+ * itself to a file in which the roll has already been read into notes
7
+ * and commands.
8
+ */
9
+ export declare const sourceKinds: readonly ['roll', 'scan', 'analysis', 'emulation', 'recording'];
10
+ export type SourceKind = typeof sourceKinds[number];
11
+ /** How each kind of source is referred to in prose. */
12
+ export declare const sourceLabels: Record<SourceKind, string>;
13
+ /**
14
+ * Whether a source of this kind gives positions that were measured.
15
+ * An emulation and a recording give the reading somebody else made:
16
+ * the roll has been turned into notes and commands already, and
17
+ * turning those back into holes reconstructs them.
18
+ */
19
+ export declare const isMeasured: (kind: SourceKind) => boolean;
20
+ /**
21
+ * Whether a source of this kind bears witness to the paper. Punch
22
+ * diameter, hole separation, punching pattern, marks and writings can
23
+ * be observed on the roll and on an image of it, and on nothing else.
24
+ */
25
+ export declare const bearsPhysicalEvidence: (kind: SourceKind) => boolean;
26
+ /**
27
+ * The capture by which a copy's features became data: what they were
28
+ * read from, who read them, on what device and when. It states where
29
+ * the numbers of the edition come from and says nothing about the
30
+ * state of the paper, which is a condition.
31
+ *
32
+ * What is not known is left out. A reader is told about the gap by
33
+ * `reservationsAbout`, which works out what a copy cannot vouch for
34
+ * from what it states here.
35
+ * @see crm:E7 Activity
36
+ */
37
+ export interface FeatureSource extends WithNote {
38
+ /**
39
+ * What the features were read from.
40
+ * @see crm:P2 has type
41
+ */
42
+ kind: SourceKind;
43
+ /**
44
+ * The file the capture produced, where it has an address.
45
+ * @see crmdig:L11 had output
46
+ */
47
+ output?: string;
48
+ /**
49
+ * Who carried out the capture.
50
+ * @see crm:P14 carried out by
51
+ */
52
+ actor?: ActorAssignment;
53
+ /**
54
+ * The make and model of the scanner, camera or player the capture
55
+ * ran on.
56
+ * @see crmdig:L12 happened on device
57
+ */
58
+ device?: Concept;
59
+ /**
60
+ * When the capture took place.
61
+ * @format date
62
+ * @see dcterms:date
63
+ */
64
+ date?: DateAssignment;
65
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * What a copy's features were read from. The kinds run from the paper
3
+ * itself to a file in which the roll has already been read into notes
4
+ * and commands.
5
+ */
6
+ export const sourceKinds = [
7
+ 'roll',
8
+ 'scan',
9
+ 'analysis',
10
+ 'emulation',
11
+ 'recording'
12
+ ];
13
+ /** How each kind of source is referred to in prose. */
14
+ export const sourceLabels = {
15
+ roll: 'the roll itself',
16
+ scan: 'a scan of the roll',
17
+ analysis: 'a hole analysis of a scan',
18
+ emulation: 'an emulated MIDI file',
19
+ recording: 'a MIDI recording of the copy being played'
20
+ };
21
+ const measured = ['roll', 'scan', 'analysis'];
22
+ const physical = ['roll', 'scan'];
23
+ /**
24
+ * Whether a source of this kind gives positions that were measured.
25
+ * An emulation and a recording give the reading somebody else made:
26
+ * the roll has been turned into notes and commands already, and
27
+ * turning those back into holes reconstructs them.
28
+ */
29
+ export const isMeasured = (kind) => measured.includes(kind);
30
+ /**
31
+ * Whether a source of this kind bears witness to the paper. Punch
32
+ * diameter, hole separation, punching pattern, marks and writings can
33
+ * be observed on the roll and on an image of it, and on nothing else.
34
+ */
35
+ export const bearsPhysicalEvidence = (kind) => physical.includes(kind);
package/lib/RollCopy.d.ts CHANGED
@@ -6,6 +6,7 @@ import { AnyFeature } from "./Feature";
6
6
  import { ActorAssignment, DateAssignment, ObjectAssumption } from "./Assumption";
7
7
  import { WithId, WithType } from "./utils";
8
8
  import { Agent, Concept } from "./Agent";
9
+ import { FeatureSource } from "./FeatureSource";
9
10
  import { Measure, Millimeters, Quantity, Track } from "./Quantity";
10
11
  /**
11
12
  * This condition state is used to describe the roll's
@@ -282,6 +283,13 @@ export interface RollCopy extends WithType<'RollCopy'>, WithId {
282
283
  * @see crm:P138i has representation
283
284
  */
284
285
  scan?: string;
286
+ /**
287
+ * What this copy's features were read from. A copy that states no
288
+ * source is one whose features reached the edition by a way that
289
+ * was not written down.
290
+ * @see reo:capture
291
+ */
292
+ readFrom?: FeatureSource;
285
293
  }
286
294
  /**
287
295
  * Reads the features of a copy as the tracker bar would read them.
@@ -5,7 +5,9 @@ import { AnySymbol, PlacementRelation } from "./Symbol";
5
5
  import { CollationTolerance } from "./Collation";
6
6
  import { Edit } from "./Edit";
7
7
  import { RollCopy, ScaleReading, Shift } from "./RollCopy";
8
+ import { FeatureSource } from "./FeatureSource";
8
9
  import { AnyArgumentation, Certainty } from "./Assumption";
10
+ import { AnyFeature } from "./Feature";
9
11
  /**
10
12
  * A change to an edition, written onto an immer draft of it. One
11
13
  * operation is one undo step, so an operation that has to read the
@@ -30,6 +32,10 @@ export declare const alignCopy: (copyId: string, shift: Shift, scale: number, re
30
32
  * being a fact about the copy.
31
33
  */
32
34
  export declare const unalignCopy: (copyId: string) => EditionOp;
35
+ /** States what the copy's features were read from, in place of any earlier statement. */
36
+ export declare const stateSource: (copyId: string, source: FeatureSource) => EditionOp;
37
+ /** Takes back the statement, leaving the copy silent about its source again. */
38
+ export declare const clearSource: (copyId: string) => EditionOp;
33
39
  /** The symbols of the versions that no other copy carries. */
34
40
  export declare const symbolsCarriedOnlyBy: (edition: Edition, copyId: string) => AnySymbol[];
35
41
  /** Takes the features off the copy, and out of the versions with what only they carried. */
@@ -39,6 +45,28 @@ export declare const removeFeatures: (copyId: string, featureIds: readonly strin
39
45
  * carries, and with every reference the versions made to those symbols.
40
46
  */
41
47
  export declare const removeCopy: (copyId: string) => EditionOp;
48
+ /** Why several features cannot be replaced by one. */
49
+ export type MergeObstacle = 'fewer-than-two' | 'different-types' | 'different-tracks' | 'differing-conditions' | 'unlike-features';
50
+ /**
51
+ * What stands in the way of reading the features as one, or nothing
52
+ * where they may be merged. Features may be merged where they differ
53
+ * in nothing but their place along the roll, their depiction and the
54
+ * condition at most one of them states. How far apart they lie is not
55
+ * asked: whether a gap is a bridge of the perforator, a tear or two
56
+ * perforations of their own is the editor's reading.
57
+ */
58
+ export declare const mergeObstacle: (features: readonly AnyFeature[]) => MergeObstacle | undefined;
59
+ /**
60
+ * Replaces the features of the copy with a single one covering them
61
+ * all, where a scan has split what the editor reads as one feature.
62
+ * The merged feature takes the place of the first it replaces and
63
+ * spans from the first to the last, any gap between them included.
64
+ * What the replaced features carried is carried by the merged one.
65
+ *
66
+ * Throws where the features cannot stand for one; `mergeObstacle`
67
+ * says beforehand whether they can.
68
+ */
69
+ export declare const mergeFeatures: (copyId: string, featureIds: readonly string[]) => EditionOp;
42
70
  /**
43
71
  * Bases the child on the parent. A symbol of the child that collates
44
72
  * with one the parent hands down adds its carriers to that symbol; the
package/lib/editionOps.js CHANGED
@@ -96,6 +96,14 @@ export const unalignCopy = (copyId) => onCopy(copyId, copy => {
96
96
  revertShift(copy);
97
97
  copy.conditions = without(copy.conditions, isPaperStretch);
98
98
  });
99
+ /** States what the copy's features were read from, in place of any earlier statement. */
100
+ export const stateSource = (copyId, source) => onCopy(copyId, copy => {
101
+ copy.readFrom = source;
102
+ });
103
+ /** Takes back the statement, leaving the copy silent about its source again. */
104
+ export const clearSource = (copyId) => onCopy(copyId, copy => {
105
+ copy.readFrom = undefined;
106
+ });
99
107
  const featureIdsOf = (copy) => new Set(copy.features.map(feature => feature.id));
100
108
  /**
101
109
  * A symbol every carrier of which lies among the features loses its
@@ -165,6 +173,117 @@ export const removeCopy = (copyId) => onCopy(copyId, (copy, draft) => {
165
173
  forgetFeatures(draft, featureIdsOf(copy));
166
174
  draft.copies = draft.copies.filter(c => c.id !== copyId);
167
175
  });
176
+ const isRecord = (value) => typeof value === 'object' && value !== null;
177
+ /**
178
+ * Whether two records of the edition state the same, the identity of a
179
+ * statement left out: two transcriptions reading the same word say the
180
+ * same, whichever ids they were given.
181
+ */
182
+ const sayTheSame = (a, b) => {
183
+ if (a === b)
184
+ return true;
185
+ if (!isRecord(a) || !isRecord(b) || Array.isArray(a) !== Array.isArray(b))
186
+ return false;
187
+ const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
188
+ keys.delete('id');
189
+ return [...keys].every(key => sayTheSame(a[key], b[key]));
190
+ };
191
+ /** What a feature states beyond its identity, its place along the roll, its depiction and its condition. */
192
+ const nature = (feature) => {
193
+ const { id, horizontal, depiction, condition, ...rest } = feature;
194
+ return rest;
195
+ };
196
+ const conditionsOf = (features) => features.flatMap(feature => feature.condition ? [feature.condition] : []);
197
+ /**
198
+ * What stands in the way of reading the features as one, or nothing
199
+ * where they may be merged. Features may be merged where they differ
200
+ * in nothing but their place along the roll, their depiction and the
201
+ * condition at most one of them states. How far apart they lie is not
202
+ * asked: whether a gap is a bridge of the perforator, a tear or two
203
+ * perforations of their own is the editor's reading.
204
+ */
205
+ export const mergeObstacle = (features) => {
206
+ if (features.length < 2)
207
+ return 'fewer-than-two';
208
+ const [first, ...rest] = features;
209
+ if (rest.some(feature => feature.type !== first.type))
210
+ return 'different-types';
211
+ if (rest.some(feature => !sayTheSame(feature.vertical, first.vertical)))
212
+ return 'different-tracks';
213
+ if (rest.some(feature => !sayTheSame(nature(feature), nature(first))))
214
+ return 'unlike-features';
215
+ const conditions = conditionsOf(features);
216
+ if (conditions.some(condition => !sayTheSame(condition, conditions[0])))
217
+ return 'differing-conditions';
218
+ return undefined;
219
+ };
220
+ /** The items with the replacement in the place of the first it stands for, the others dropped. */
221
+ const standingFor = (items, stands, replacement) => {
222
+ const first = items.findIndex(stands);
223
+ if (first < 0)
224
+ return items;
225
+ return items.flatMap((item, i) => i === first ? [replacement] : stands(item) ? [] : [item]);
226
+ };
227
+ const spanning = (features) => ({
228
+ unit: 'mm',
229
+ from: mm(Math.min(...features.map(feature => feature.horizontal.from))),
230
+ to: mm(Math.max(...features.map(feature => feature.horizontal.to)))
231
+ });
232
+ /**
233
+ * The feature the merged ones give way to. It spans them all and is in
234
+ * every other respect the one of them that states a condition, the
235
+ * others being alike in all but their place. The depiction goes: a
236
+ * region of the scan showing one part depicts no more than that part.
237
+ */
238
+ const mergedFrom = (features) => {
239
+ const stating = features.find(feature => feature.condition) ?? features[0];
240
+ const merged = { ...stating, id: v4(), horizontal: spanning(features) };
241
+ delete merged.depiction;
242
+ return merged;
243
+ };
244
+ /** The symbol carried by the merged feature where it named one of those it replaces, and naming it once. */
245
+ const carriedByMerged = (replaced, mergedId) => (symbol) => {
246
+ const names = (carrier) => replaced.has(idOf(carrier));
247
+ const first = symbol.carriers.find(names);
248
+ if (!first)
249
+ return symbol;
250
+ return { ...symbol, carriers: standingFor(symbol.carriers, names, { ...first, id: mergedId }) };
251
+ };
252
+ /** Points the versions at the merged feature wherever they name one it replaces. */
253
+ const carryOver = (draft, replaced, mergedId) => {
254
+ const recarry = carriedByMerged(replaced, mergedId);
255
+ const recarrying = (edit) => {
256
+ const insert = edit.insert && mapped(edit.insert, recarry);
257
+ return insert === edit.insert ? edit : { ...edit, insert };
258
+ };
259
+ const versions = stateOf(draft.versions);
260
+ draft.versions.forEach((version, i) => {
261
+ version.edits = mapped(versions[i].edits, recarrying);
262
+ });
263
+ };
264
+ /**
265
+ * Replaces the features of the copy with a single one covering them
266
+ * all, where a scan has split what the editor reads as one feature.
267
+ * The merged feature takes the place of the first it replaces and
268
+ * spans from the first to the last, any gap between them included.
269
+ * What the replaced features carried is carried by the merged one.
270
+ *
271
+ * Throws where the features cannot stand for one; `mergeObstacle`
272
+ * says beforehand whether they can.
273
+ */
274
+ export const mergeFeatures = (copyId, featureIds) => onCopy(copyId, (copy, draft) => {
275
+ const replaced = new Set(featureIds);
276
+ const isReplaced = (feature) => replaced.has(feature.id);
277
+ const features = stateOf(copy.features);
278
+ const toMerge = features.filter(isReplaced);
279
+ const obstacle = mergeObstacle(toMerge);
280
+ if (obstacle) {
281
+ throw new Error(`The features of copy ${copyId} cannot be merged: ${obstacle}`);
282
+ }
283
+ const merged = mergedFrom(toMerge);
284
+ copy.features = standingFor(features, isReplaced, merged);
285
+ carryOver(draft, replaced, merged.id);
286
+ });
168
287
  /** The carriers of each collated symbol pass to its counterpart. */
169
288
  const handOverCarriers = (view, draft, collations) => collations.forEach(({ symbol, counterpart }) => {
170
289
  const path = view.getPath(counterpart.id);
package/lib/index.d.ts CHANGED
@@ -14,7 +14,9 @@ export * from './systems/welteT100/bar';
14
14
  export * from './systems/welteLicensee/bar';
15
15
  export * from './systems';
16
16
  export * from './ReproducingSystem';
17
+ export * from './FeatureSource';
17
18
  export * from './RollCopy';
19
+ export * from './reservations';
18
20
  export * from './alignment';
19
21
  export * from './Edition';
20
22
  export * from './EditionView';
@@ -25,5 +27,6 @@ export * from './constraints';
25
27
  export * from './context';
26
28
  export * from './asJsonLd';
27
29
  export * from './importJsonLd';
30
+ export * from './migrate';
28
31
  export { readFromStanfordAton, type StanfordAtonOptions } from './readers/stanfordAton';
29
32
  export { readFromSpencerBar, readSpencerAnn, paperSpeedOfSpencerAnn, SPENCER_ROWS_PER_INCH, type SpencerBarOptions } from './readers/spencerBar';
package/lib/index.js CHANGED
@@ -14,7 +14,9 @@ export * from './systems/welteT100/bar';
14
14
  export * from './systems/welteLicensee/bar';
15
15
  export * from './systems';
16
16
  export * from './ReproducingSystem';
17
+ export * from './FeatureSource';
17
18
  export * from './RollCopy';
19
+ export * from './reservations';
18
20
  export * from './alignment';
19
21
  export * from './Edition';
20
22
  export * from './EditionView';
@@ -25,5 +27,6 @@ export * from './constraints';
25
27
  export * from './context';
26
28
  export * from './asJsonLd';
27
29
  export * from './importJsonLd';
30
+ export * from './migrate';
28
31
  export { readFromStanfordAton } from './readers/stanfordAton';
29
32
  export { readFromSpencerBar, readSpencerAnn, paperSpeedOfSpencerAnn, SPENCER_ROWS_PER_INCH } from './readers/spencerBar';
@@ -0,0 +1,23 @@
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'];
3
+ export type ReservationType = typeof reservationTypes[number];
4
+ /**
5
+ * Something an edition cannot vouch for in one of its copies.
6
+ *
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
11
+ * condition of the copy.
12
+ */
13
+ export interface Reservation {
14
+ type: ReservationType;
15
+ /** What the reservation means for a reader, in one sentence. */
16
+ note: string;
17
+ }
18
+ /**
19
+ * What the edition cannot vouch for in a copy, in the order the
20
+ * checks are listed: where its features came from first, then what
21
+ * the measurement leaves open.
22
+ */
23
+ export declare const reservationsAbout: (copy: RollCopy) => Reservation[];
@@ -0,0 +1,53 @@
1
+ import { bearsPhysicalEvidence, isMeasured, sourceLabels } from "./FeatureSource";
2
+ import { calibrationOf } from "./RollCopy";
3
+ export const reservationTypes = [
4
+ 'source-not-stated',
5
+ 'source-undocumented',
6
+ 'features-interpreted',
7
+ 'no-physical-evidence',
8
+ 'measurement-undocumented',
9
+ 'not-calibrated'
10
+ ];
11
+ const sourceStated = copy => copy.readFrom ? undefined : {
12
+ type: 'source-not-stated',
13
+ note: 'The copy does not state what its features were read from.'
14
+ };
15
+ const sourceDocumented = copy => {
16
+ const source = copy.readFrom;
17
+ if (!source || source.actor || source.device || source.date)
18
+ return undefined;
19
+ return {
20
+ type: 'source-undocumented',
21
+ note: `The copy names ${sourceLabels[source.kind]} as its source, but records nobody who made it, no device and no date.`
22
+ };
23
+ };
24
+ const featuresMeasured = copy => copy.readFrom && !isMeasured(copy.readFrom.kind) ? {
25
+ type: 'features-interpreted',
26
+ note: `The features come from ${sourceLabels[copy.readFrom.kind]}, in which the roll has already been read into notes and commands by somebody else.`
27
+ } : undefined;
28
+ const physicalEvidence = copy => copy.readFrom && !bearsPhysicalEvidence(copy.readFrom.kind) ? {
29
+ type: 'no-physical-evidence',
30
+ note: 'Punch diameter, hole separation, punching pattern, marks and writings cannot be observed on this source.'
31
+ } : undefined;
32
+ const measurementDocumented = copy => !copy.measurements.measuredBy && copy.readFrom?.kind !== 'roll' ? {
33
+ type: 'measurement-undocumented',
34
+ note: 'No measuring software is recorded for this copy.'
35
+ } : undefined;
36
+ const calibrated = copy => calibrationOf(copy) ? undefined : {
37
+ type: 'not-calibrated',
38
+ note: 'The copy is not calibrated against the tracker bar, so its track positions rest on the numbering its source used.'
39
+ };
40
+ const checks = [
41
+ sourceStated,
42
+ sourceDocumented,
43
+ featuresMeasured,
44
+ physicalEvidence,
45
+ measurementDocumented,
46
+ calibrated
47
+ ];
48
+ /**
49
+ * What the edition cannot vouch for in a copy, in the order the
50
+ * checks are listed: where its features came from first, then what
51
+ * the measurement leaves open.
52
+ */
53
+ export const reservationsAbout = (copy) => checks.flatMap(check => check(copy) ?? []);
package/lib/schema.json CHANGED
@@ -543,6 +543,40 @@
543
543
  ],
544
544
  "type": "string"
545
545
  },
546
+ "FeatureSource": {
547
+ "description": "The capture by which a copy's features became data: what they were read from, who read them, on what device and when. It states where the numbers of the edition come from and says nothing about the state of the paper, which is a condition.\n\nWhat is not known is left out. A reader is told about the gap by `reservationsAbout`, which works out what a copy cannot vouch for from what it states here. [ontology: crm:E7 Activity]",
548
+ "properties": {
549
+ "actor": {
550
+ "$ref": "#/definitions/ActorAssignment",
551
+ "description": "Who carried out the capture. [ontology: crm:P14 carried out by]"
552
+ },
553
+ "date": {
554
+ "$ref": "#/definitions/DateAssignment",
555
+ "description": "When the capture took place. [ontology: dcterms:date]",
556
+ "format": "date"
557
+ },
558
+ "device": {
559
+ "$ref": "#/definitions/Concept",
560
+ "description": "The make and model of the scanner, camera or player the capture ran on. [ontology: crmdig:L12 happened on device]"
561
+ },
562
+ "kind": {
563
+ "$ref": "#/definitions/SourceKind",
564
+ "description": "What the features were read from. [ontology: crm:P2 has type]"
565
+ },
566
+ "note": {
567
+ "description": "A free-text note providing additional context. [ontology: crm:P3 has note]",
568
+ "type": "string"
569
+ },
570
+ "output": {
571
+ "description": "The file the capture produced, where it has an address. [ontology: crmdig:L11 had output]",
572
+ "type": "string"
573
+ }
574
+ },
575
+ "required": [
576
+ "kind"
577
+ ],
578
+ "type": "object"
579
+ },
546
580
  "GluedOn": {
547
581
  "description": "A piece of material (paper or tape) glued onto the roll surface. Glued-on features are typically used to cover perforations (for corrections) or to reinforce damaged areas. They may themselves carry other features such as writings or additional holes. [ontology: crm:E22 Human-Made Object]",
548
582
  "properties": {
@@ -1816,6 +1850,10 @@
1816
1850
  "$ref": "#/definitions/ProductionEvent",
1817
1851
  "description": "The production event that created this roll copy. [ontology: lrmoo:R28i was produced by]"
1818
1852
  },
1853
+ "readFrom": {
1854
+ "$ref": "#/definitions/FeatureSource",
1855
+ "description": "What this copy's features were read from. A copy that states no source is one whose features reached the edition by a way that was not written down. [ontology: reo:capture]"
1856
+ },
1819
1857
  "scan": {
1820
1858
  "description": "The scan URL or IIIF URL of the roll. [ontology: crm:P138i has representation]",
1821
1859
  "type": "string"
@@ -1861,6 +1899,16 @@
1861
1899
  ],
1862
1900
  "type": "object"
1863
1901
  },
1902
+ "SourceKind": {
1903
+ "enum": [
1904
+ "roll",
1905
+ "scan",
1906
+ "analysis",
1907
+ "emulation",
1908
+ "recording"
1909
+ ],
1910
+ "type": "string"
1911
+ },
1864
1912
  "Text": {
1865
1913
  "description": "A textual symbol, e.g. a label or annotation found on the roll. [ontology: crm:E33 Linguistic Object]",
1866
1914
  "properties": {
@@ -165,6 +165,22 @@
165
165
  "@id": "crm:P138i_has_representation",
166
166
  "@type": "@id"
167
167
  },
168
+ "readFrom": {
169
+ "@id": "reo:capture",
170
+ "@context": {
171
+ "kind": {
172
+ "@id": "crm:P2_has_type",
173
+ "@type": "@vocab",
174
+ "@context": { "@vocab": "https://w3id.org/reo/type/" }
175
+ },
176
+ "output": {
177
+ "@id": "crmdig:L11_had_output",
178
+ "@type": "@id"
179
+ },
180
+ "device": "crmdig:L12_happened_on_device",
181
+ "note": "crm:P3_has_note"
182
+ }
183
+ },
168
184
  "depiction": {
169
185
  "@id": "crm:P138i_has_representation",
170
186
  "@type": "@id"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linked-rolls",
3
- "version": "0.13.0",
3
+ "version": "0.15.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": {