linked-rolls 0.35.0 → 0.37.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
@@ -42,7 +42,10 @@ which versions and conditions carry a typology key beside their type,
42
42
  the keeper and the production metadata are nodes with a name and
43
43
  authority links, and the roll names its reproducing system. A date
44
44
  written as a value of its own is read as the day the event falls
45
- within. Exports are always in the current format.
45
+ within. A copy that held its features in one list, naming by id what a
46
+ modification had added, has each feature put into the act that brought
47
+ it about, and a feature a patch bears that was left unnamed is named
48
+ after the patch. Exports are always in the current format.
46
49
 
47
50
  ## How a date is stated
48
51
 
@@ -72,6 +75,30 @@ at. It then reads as the version it derives from, and
72
75
  `reservationsAboutVersion` says so. A file that names a single
73
76
  derivation loads as a list of one.
74
77
 
78
+ ## What brought a feature about
79
+
80
+ A copy states no list of features. Each feature stands in the act that
81
+ brought it about: the perforations in the copy's own production, a
82
+ writing or a mark in the `Alteration` that made it, a patch in the
83
+ `Attachment` that glued it on. A `Removal` names by id what was taken
84
+ off, which stood there before the act. A feature no later act is known
85
+ to have made belongs to the punching, which is where a reading of a
86
+ scan puts every hole it finds.
87
+
88
+ Three kinds of act rather than two, because the CRM tells them apart.
89
+ E79 Part Addition asks that what is added be "a separate identifiable
90
+ whole prior to" the act, which a glued-on label is and a pencil line is
91
+ not: drawing, writing and punching bring a feature into being and are
92
+ productions (E12), which state what they made with P108 has produced.
93
+
94
+ The export states what that shape only implies. A copy bears every
95
+ feature its acts brought about (P56 bears feature) and is composed of
96
+ the patches glued onto it (P46 is composed of, a patch being an object
97
+ rather than a feature); an attachment augments the copy and a removal
98
+ diminishes it. The four keys are derived on the way out and read off
99
+ again on the way in, so they are no part of the edition, and they are
100
+ the one place where the JSON tree and the graph differ in shape.
101
+
75
102
  ## Where a copy's features come from
76
103
 
77
104
  `readFrom` states what a copy's features were read from: the roll
@@ -1,5 +1,5 @@
1
1
  import { Edition } from "./Edition";
2
- import { HorizontalSpan, AnyFeature } from "./Feature";
2
+ import { HorizontalSpan, FeatureOrPatch } from "./Feature";
3
3
  import { AnySymbol, Expression, Note } from "./Symbol";
4
4
  import { Version } from "./Version";
5
5
  import { NegotiatedEvent } from "./ReproducingSystem";
@@ -34,7 +34,7 @@ export declare class EditionView {
34
34
  travelUp(versionId: string, callback: (version: Readonly<Version>) => void): void;
35
35
  /** The version and its ancestors along the principal line, from the version up to the root. */
36
36
  lineageOf(versionId: string): Readonly<Version>[];
37
- carriersOf(symbol: AnySymbol): Readonly<AnyFeature>[];
37
+ carriersOf(symbol: AnySymbol): Readonly<FeatureOrPatch>[];
38
38
  /** The copy a feature sits on, a patch and everything it bears included. */
39
39
  copyOf(featureId: string): Readonly<RollCopy> | undefined;
40
40
  /**
@@ -1,7 +1,7 @@
1
1
  import { withBorneFeatures } from "./Feature";
2
2
  import { deletedBy, insertedBy, principalDerivationOf } from "./Version";
3
3
  import { systemIdOf } from "./TrackerBar";
4
- import { isPaperStretch } from "./RollCopy";
4
+ import { featuresOf, isPaperStretch } from "./RollCopy";
5
5
  import { idOf, idsOf } from "./Assumption";
6
6
  import { mean } from "./Quantity";
7
7
  export const getAt = (path, obj) => {
@@ -155,7 +155,7 @@ export class EditionView {
155
155
  /** The copy a feature sits on, a patch and everything it bears included. */
156
156
  copyOf(featureId) {
157
157
  if (!this.copiesByFeature) {
158
- this.copiesByFeature = new Map(this.edition.copies.flatMap(copy => copy.features
158
+ this.copiesByFeature = new Map(this.edition.copies.flatMap(copy => featuresOf(copy)
159
159
  .flatMap(withBorneFeatures)
160
160
  .map(feature => [feature.id, copy])));
161
161
  }
package/lib/Feature.d.ts CHANGED
@@ -55,10 +55,12 @@ export interface VerticalSpan {
55
55
  }
56
56
  export declare const featureTypes: readonly ['Hole', 'Writing', 'Mark', 'GluedOn'];
57
57
  export type FeatureType = typeof featureTypes[number];
58
+ export declare const isFeatureType: (value: unknown) => value is FeatureType;
58
59
  /**
59
- * A physical feature on the roll, e.g. a perforation, a tear, a mark, etc.,
60
- * defined by its horizontal and vertical position and extent.
61
- * @see crm:E26 Physical Feature
60
+ * A feature on the roll, e.g. a perforation, a writing, a mark or a
61
+ * glued-on patch, defined by its horizontal and vertical position and
62
+ * extent. Each kind of feature is a class of its own, which its type
63
+ * names.
62
64
  */
63
65
  export interface RollFeature<T extends FeatureType, DamageT extends string> extends WithId, WithType<T> {
64
66
  /**
@@ -101,6 +103,10 @@ export type FeatureConditionAssignment = ObjectAssumption<ConditionState<Feature
101
103
  * A hole (perforation) in the roll paper. Holes are the primary
102
104
  * carriers of musical information on piano rolls, as they trigger
103
105
  * notes and expression controls when passing over the tracker bar.
106
+ * A punched hole is a human-made feature: CRM counts "the information
107
+ * encoding features on mechanical or digital carriers" among the
108
+ * features purposely created by human activity.
109
+ * @see reo:Hole
104
110
  */
105
111
  export interface Hole extends RollFeature<'Hole', typeof conditions.Hole[number]> {
106
112
  /**
@@ -119,25 +125,28 @@ export interface Hole extends RollFeature<'Hole', typeof conditions.Hole[number]
119
125
  */
120
126
  export interface Trace<T extends FeatureType> extends RollFeature<T, typeof conditions[T][number]> {
121
127
  }
122
- export declare const writingMethods: readonly ['Print', 'Handwriting', 'Stamp'];
128
+ export declare const writingMethods: readonly ['print', 'handwriting', 'stamp'];
123
129
  /**
124
130
  * The method by which a writing was produced on the roll:
125
131
  * printed, handwritten, or stamped.
126
132
  */
127
133
  export type WritingMethod = typeof writingMethods[number];
134
+ export declare const markMethods: readonly ['ink', 'pencil', 'crayon'];
135
+ /** The medium a mark was drawn in. */
136
+ export type MarkMethod = typeof markMethods[number];
128
137
  /** The text a writing carries. It names no carriers of its own, the writing being its carrier. */
129
138
  export type Transcription = Omit<Text, 'carriers'>;
130
139
  /**
131
140
  * A piece of writing found on the roll, such as a label,
132
141
  * catalogue number, or annotation. Writings have a method
133
142
  * of production and a transcription of their content.
134
- * @see crm:E25 Human-Made Feature
143
+ * @see reo:Writing
135
144
  */
136
145
  export interface Writing extends Trace<'Writing'> {
137
146
  /**
138
147
  * The method by which this writing was produced
139
148
  * (e.g. through print, handwriting, or stamping).
140
- * @see crm:P2 has type
149
+ * @see reo:method
141
150
  */
142
151
  method: WritingMethod;
143
152
  /**
@@ -149,46 +158,65 @@ export interface Writing extends Trace<'Writing'> {
149
158
  transcription: ObjectAssumption<Transcription>;
150
159
  }
151
160
  /**
152
- * A visible mark on the roll, such as a pencil mark,
153
- * ink mark, or other non-textual annotation.
154
- * @see crm:E25 Human-Made Feature
161
+ * A visible mark on the roll, such as a pencil circle, an ink stroke,
162
+ * or other non-textual annotation. What a mark was meant to say is a
163
+ * reading of it (crminf:I16 Meaning Comprehension) rather than a
164
+ * property of the feature, so its shape is not stated here.
165
+ * @see reo:Mark
155
166
  */
156
167
  export interface Mark extends Trace<'Mark'> {
168
+ /**
169
+ * The medium the mark was drawn in, where it can be told.
170
+ * @see reo:method
171
+ */
172
+ method?: MarkMethod;
157
173
  }
158
174
  /**
159
175
  * A piece of material (paper or tape) glued onto the roll surface.
160
176
  * Glued-on features are typically used to cover perforations (for corrections)
161
177
  * or to reinforce damaged areas. They may themselves carry other features
162
178
  * such as writings or additional holes.
163
- * @see crm:E22 Human-Made Object
179
+ * @see reo:GluedOn
164
180
  */
165
181
  export interface GluedOn extends RollFeature<'GluedOn', typeof conditions.GluedOn[number]> {
166
182
  /**
167
183
  * The material of the glued-on feature.
168
184
  * @see crm:P45 consists of
169
185
  */
170
- material: 'Paper' | 'Tape';
186
+ material: 'paper' | 'tape';
171
187
  /**
172
188
  * A glued-on feature itself may carry other features.
173
189
  * Nested features do not need to be positioned explicitly.
174
- * @see crm:P56 bears feature
190
+ * @see crm:P46 is composed of
175
191
  */
176
192
  features?: NestedFeature[];
177
193
  }
178
194
  /**
179
- * The union of all physical feature types that can appear on a roll:
180
- * holes (perforations), writings (labels, annotations), marks (pencil, ink),
181
- * and glued-on patches (paper, tape).
195
+ * The features proper: a hole, a writing and a mark are all human-made
196
+ * features, and each of them is borne by whatever it sits on. A patch
197
+ * is an object glued onto the paper and stands apart from them.
198
+ */
199
+ export type AnyFeature = Hole | Writing | Mark;
200
+ /**
201
+ * Anything found at a place of its own on the roll: a feature or a
202
+ * glued-on patch. E24 Physical Human-Made Thing is the class both fall
203
+ * under, E22 and E25 being its subclasses.
204
+ */
205
+ export type FeatureOrPatch = AnyFeature | GluedOn;
206
+ /**
207
+ * A feature without the place it would state of its own. The union is
208
+ * distributed over, so that each kind of feature keeps its own keys.
182
209
  */
183
- export type AnyFeature = Hole | Writing | Mark | GluedOn;
210
+ type Unplaced<T> = T extends FeatureOrPatch ? PartialBy<T, 'horizontal' | 'vertical'> : never;
184
211
  /**
185
212
  * A feature borne by another feature. It states no place of its own,
186
213
  * the feature bearing it standing in one.
187
214
  */
188
- export type NestedFeature = PartialBy<AnyFeature, 'horizontal' | 'vertical'>;
189
- export declare const isRollFeature: (obj: object) => obj is AnyFeature;
215
+ export type NestedFeature = Unplaced<FeatureOrPatch>;
216
+ export declare const isRollFeature: (obj: object) => obj is FeatureOrPatch;
190
217
  export declare const isGluedOn: <T extends NestedFeature>(feature: T) => feature is T & GluedOn;
191
218
  /** The features a feature bears: a patch those stated as parts of it, any other feature none. */
192
219
  export declare const featuresBorneBy: (feature: NestedFeature) => NestedFeature[];
193
220
  /** The feature together with everything it bears, as deep as a patch on a patch goes. */
194
221
  export declare const withBorneFeatures: (feature: NestedFeature) => NestedFeature[];
222
+ export {};
package/lib/Feature.js CHANGED
@@ -1,13 +1,15 @@
1
1
  export const featureTypes = ['Hole', 'Writing', 'Mark', 'GluedOn'];
2
+ export const isFeatureType = (value) => featureTypes.includes(value);
2
3
  export const conditions = {
3
4
  Hole: ['partially-torn', 'missing-perforation'],
4
5
  Writing: ['illegible'],
5
6
  Mark: ['faded'],
6
7
  GluedOn: ['detaching', 'ripped']
7
8
  };
8
- export const writingMethods = ['Print', 'Handwriting', 'Stamp'];
9
+ export const writingMethods = ['print', 'handwriting', 'stamp'];
10
+ export const markMethods = ['ink', 'pencil', 'crayon'];
9
11
  export const isRollFeature = (obj) => {
10
- return 'type' in obj && featureTypes.includes(obj.type);
12
+ return 'type' in obj && isFeatureType(obj.type);
11
13
  };
12
14
  export const isGluedOn = (feature) => feature.type === 'GluedOn';
13
15
  /** The features a feature bears: a patch those stated as parts of it, any other feature none. */
package/lib/RollCopy.d.ts CHANGED
@@ -2,7 +2,7 @@ import { ConditionState } from "./ConditionState";
2
2
  import { AnySymbol } from "./Symbol";
3
3
  import { TrackerBar } from "./TrackerBar";
4
4
  import { TrackCalibration } from "./TrackCalibration";
5
- import { AnyFeature } from "./Feature";
5
+ import { AnyFeature, FeatureOrPatch, GluedOn } from "./Feature";
6
6
  import { ActorAssignment, DateAssignment, ObjectAssumption, ReferenceAssumption } from "./Assumption";
7
7
  import { WithId, WithType } from "./utils";
8
8
  import { Agent, Concept } from "./Agent";
@@ -34,6 +34,11 @@ export interface GeneralRollCondition extends ConditionState<'general'> {
34
34
  * to a roll copy, annotatable with a belief about its certainty.
35
35
  */
36
36
  export type RollConditionAssignment = ObjectAssumption<GeneralRollCondition | PaperStretch>;
37
+ /**
38
+ * The keeper a copy is held by, as an object assumption, so that the
39
+ * statement can carry the belief it rests on and the reasons for it.
40
+ */
41
+ export type KeeperAssignment = ObjectAssumption<Agent>;
37
42
  export declare const rollConditions: readonly ['general', 'paper-stretch'];
38
43
  /**
39
44
  * A shift correction applied to a roll copy to align it
@@ -109,12 +114,26 @@ export interface ProductionEvent {
109
114
  * @see reo:paperSpeed
110
115
  */
111
116
  speed?: ObjectAssumption<PaperSpeed>;
117
+ /**
118
+ * The features the punching brought into being, which are the note
119
+ * and expression perforations. A reading of a scan finds every hole
120
+ * on the paper at once and states it here. A hole an editor reads as
121
+ * punched later belongs in the act that made it.
122
+ * @see crm:P108 has produced
123
+ */
124
+ produced?: AnyFeature[];
112
125
  }
126
+ export declare const modificationPurposes: readonly ['musical-improvement', 'technical-improvement', 'repair', 'labeling', 'control', 'dating', 'glossing'];
127
+ /** What a modification of a copy was for, as far as it can be told. */
128
+ export type ModificationPurpose = typeof modificationPurposes[number];
113
129
  /**
114
- * This type denotes identifiable activities that modified
115
- * the roll copy after its production, e.g. annotations, repairs,
116
- * etc.
117
- * @see crm:E79 Part Addition, crm:E80 Part Removal
130
+ * An identifiable act that changed the roll copy after it was punched.
131
+ * Three kinds of act are told apart, because the CRM tells them apart.
132
+ * E79 Part Addition asks that what is added be "a separate identifiable
133
+ * whole prior to" the act, which a label glued on is and a pencil line
134
+ * is not: drawing, writing and punching bring a feature into being and
135
+ * are productions.
136
+ * @see crm:E79 Part Addition, crm:E80 Part Removal, crm:E12 Production
118
137
  */
119
138
  export type Modification = Partial<{
120
139
  /**
@@ -128,36 +147,49 @@ export type Modification = Partial<{
128
147
  */
129
148
  date: DateAssignment;
130
149
  }> & ({
131
- type: 'Addition';
150
+ type: 'Attachment';
132
151
  /**
152
+ * The patches glued onto the copy, each with whatever it bears.
133
153
  * @see crm:P111 added
134
154
  */
135
- added: string[];
155
+ added: GluedOn[];
156
+ /**
157
+ * @see crm:P21 had general purpose
158
+ */
159
+ purpose?: ModificationPurpose;
160
+ } | {
161
+ type: 'Alteration';
162
+ /**
163
+ * The features the act brought into being: a date written on the
164
+ * paper, a circle in pencil, a hole punched by hand.
165
+ * @see crm:P108 has produced
166
+ */
167
+ produced: AnyFeature[];
136
168
  /**
137
169
  * @see crm:P21 had general purpose
138
170
  */
139
- purpose: 'musical-improvement' | 'technical-improvement' | 'repair' | 'labeling' | 'control' | 'dating' | 'glossing';
171
+ purpose?: ModificationPurpose;
140
172
  } | {
141
173
  type: 'Removal';
142
174
  /**
175
+ * What was taken off the copy, by id. It stood there before the act
176
+ * and is named rather than stated here. Removals are read from what
177
+ * they leave behind, such as a bright spot where a label sat.
143
178
  * @see crm:P113 removed
144
179
  */
145
180
  removed: string[];
146
181
  /**
147
- * Usually, roll features are being added.
148
- * Sometimes however, we may see traces of features
149
- * that have been removed, e.g. through bright spots on
150
- * the roll.
151
182
  * @see crm:P21 had general purpose
152
183
  */
153
- purpose: 'delabeling';
184
+ purpose?: 'delabeling';
154
185
  });
155
186
  /**
156
- * A physical copy of a roll, held at a specific location.
157
- * Each roll copy has its own set of features, measurements,
158
- * conditions, and modifications. Multiple copies of the same
159
- * roll may exist across different archives or collections.
160
- * @see lrmoo:F5 Item
187
+ * A physical copy of a roll, held at a specific location. Each roll
188
+ * copy has its own measurements, conditions and modifications, and
189
+ * states its features in the act that brought each of them about.
190
+ * Multiple copies of the same roll may exist across different archives
191
+ * or collections.
192
+ * @see reo:RollCopy
161
193
  */
162
194
  export interface RollCopy extends WithType<'RollCopy'>, WithId {
163
195
  /**
@@ -283,16 +315,13 @@ export interface RollCopy extends WithType<'RollCopy'>, WithId {
283
315
  siglum?: string;
284
316
  /**
285
317
  * The institution or person holding this copy. Left out where it is
286
- * not known, as for a copy known only from a recording.
318
+ * not known, as for a copy known only from a recording. An object
319
+ * assumption, so that a keeper read off a file header or adopted
320
+ * from a letter can say where it comes from. Who held the copy
321
+ * before is not stated here: P50 names the current keeper.
287
322
  * @see crm:P50 has current keeper
288
323
  */
289
- keeper?: Agent;
290
- /**
291
- * The physical features found on this copy, with shift
292
- * and stretch already applied when `ops` says so.
293
- * @see crm:P56 bears feature
294
- */
295
- features: AnyFeature[];
324
+ keeper?: KeeperAssignment;
296
325
  /**
297
326
  * @see crm:P31i was modified by
298
327
  */
@@ -317,6 +346,17 @@ export interface RollCopy extends WithType<'RollCopy'>, WithId {
317
346
  */
318
347
  carries?: ReferenceAssumption[];
319
348
  }
349
+ /** Whether the act is left stating nothing: it produced, added or removed nothing. */
350
+ export declare const statesNothing: (modification: Modification) => boolean;
351
+ /**
352
+ * The features of the copy act by act: what the punching produced
353
+ * first, then what each modification produced or glued on. A feature a
354
+ * patch bears states no place of its own and is not among them;
355
+ * `withBorneFeatures` reaches those.
356
+ */
357
+ export declare const featuresByAct: (copy: Pick<RollCopy, 'production' | 'modifications'>) => FeatureOrPatch[][];
358
+ /** Every feature the copy states at a place of its own, whichever act made it. */
359
+ export declare const featuresOf: (copy: Pick<RollCopy, 'production' | 'modifications'>) => FeatureOrPatch[];
320
360
  /**
321
361
  * Whether the condition is the stretch or shrinkage of the paper. A
322
362
  * scale put down to this belongs to the one exemplar and says nothing
@@ -345,14 +385,14 @@ export declare const barOf: (copy: Pick<RollCopy, 'production'>) => TrackerBar;
345
385
  * since they are really on the paper and are his calibration of his own
346
386
  * scan; it is the reading that stops.
347
387
  */
348
- export declare function asSymbols(features: AnyFeature[], bar: TrackerBar): AnySymbol[];
388
+ export declare function asSymbols(features: readonly FeatureOrPatch[], bar: TrackerBar): AnySymbol[];
349
389
  /**
350
390
  * The tracks a copy carries holes on that the tracker bar does not read.
351
391
  * A non-empty result usually means the scan is calibrated wrongly. A hole
352
392
  * lying across several positions is counted against each one the bar
353
393
  * cannot read, and not at all where it reads them all.
354
394
  */
355
- export declare function unreadTracks(features: AnyFeature[], bar: TrackerBar): Map<Track, number>;
395
+ export declare function unreadTracks(features: readonly FeatureOrPatch[], bar: TrackerBar): Map<Track, number>;
356
396
  /**
357
397
  * Falls back to the way the scan geometry was reconstructed before the
358
398
  * calibration was recorded: the bass hard margin stood in for the phase
package/lib/RollCopy.js CHANGED
@@ -7,6 +7,32 @@ export const rollConditions = [
7
7
  'general',
8
8
  'paper-stretch'
9
9
  ];
10
+ export const modificationPurposes = [
11
+ 'musical-improvement',
12
+ 'technical-improvement',
13
+ 'repair',
14
+ 'labeling',
15
+ 'control',
16
+ 'dating',
17
+ 'glossing'
18
+ ];
19
+ /** What an act brought onto the copy: what it produced or glued on, a removal nothing. */
20
+ const madeBy = (modification) => {
21
+ if (modification.type === 'Alteration')
22
+ return modification.produced;
23
+ return modification.type === 'Attachment' ? modification.added : [];
24
+ };
25
+ /** Whether the act is left stating nothing: it produced, added or removed nothing. */
26
+ export const statesNothing = (modification) => modification.type === 'Removal' ? modification.removed.length === 0 : madeBy(modification).length === 0;
27
+ /**
28
+ * The features of the copy act by act: what the punching produced
29
+ * first, then what each modification produced or glued on. A feature a
30
+ * patch bears states no place of its own and is not among them;
31
+ * `withBorneFeatures` reaches those.
32
+ */
33
+ export const featuresByAct = (copy) => [copy.production?.produced ?? [], ...copy.modifications.map(madeBy)];
34
+ /** Every feature the copy states at a place of its own, whichever act made it. */
35
+ export const featuresOf = (copy) => featuresByAct(copy).flat();
10
36
  /**
11
37
  * Whether the condition is the stretch or shrinkage of the paper. A
12
38
  * scale put down to this belongs to the one exemplar and says nothing
@@ -1,4 +1,4 @@
1
- import { AnyFeature } from "./Feature";
1
+ import { FeatureOrPatch } from "./Feature";
2
2
  import { RollCopy, Shift } from "./RollCopy";
3
3
  import { TrackerBar } from "./TrackerBar";
4
4
  import { Millimeters } from "./Quantity";
@@ -38,4 +38,4 @@ export interface AlignmentResult {
38
38
  * sounds, and it is the pitches that are matched. Where a copy's holes
39
39
  * have already been put onto the edition's bar, that bar reads it.
40
40
  */
41
- export declare function alignFeatures(rollA: readonly AnyFeature[], rollB: readonly AnyFeature[], barA?: TrackerBar, barB?: TrackerBar): AlignmentResult | undefined;
41
+ export declare function alignFeatures(rollA: readonly FeatureOrPatch[], rollB: readonly FeatureOrPatch[], barA?: TrackerBar, barB?: TrackerBar): AlignmentResult | undefined;
package/lib/alignment.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { featuresOf } from "./RollCopy";
1
2
  import { welteT100 } from "./systems/welteT100/bar";
2
3
  import { add, mm, scale } from "./Quantity";
3
4
  /** Moves both ends of a span, the far end only where the span has one. */
@@ -16,7 +17,7 @@ const back = (shift) => ({ horizontal: scale(shift.horizontal, -1), vertical: sc
16
17
  export const applyShift = (shift, copy) => {
17
18
  if (copy.ops.includes('shifted'))
18
19
  return;
19
- copy.features.forEach(feature => {
20
+ featuresOf(copy).forEach(feature => {
20
21
  move(feature.horizontal, shift.horizontal);
21
22
  move(feature.vertical, shift.vertical);
22
23
  });
@@ -27,7 +28,7 @@ export const applyShift = (shift, copy) => {
27
28
  export const applyScale = (factor, copy) => {
28
29
  if (copy.ops.includes('stretched'))
29
30
  return;
30
- copy.features.forEach(feature => stretch(feature.horizontal, factor));
31
+ featuresOf(copy).forEach(feature => stretch(feature.horizontal, factor));
31
32
  copy.ops = [...copy.ops, 'stretched'];
32
33
  copy.measurements.scale = factor;
33
34
  };
@@ -37,7 +38,7 @@ export const revertShift = (copy) => {
37
38
  if (!copy.ops.includes('shifted') || !shift)
38
39
  return;
39
40
  const reversed = back(shift);
40
- copy.features.forEach(feature => {
41
+ featuresOf(copy).forEach(feature => {
41
42
  move(feature.horizontal, reversed.horizontal);
42
43
  move(feature.vertical, reversed.vertical);
43
44
  });
@@ -49,7 +50,7 @@ export const revertScale = (copy) => {
49
50
  const factor = copy.measurements.scale;
50
51
  if (!copy.ops.includes('stretched') || factor === undefined)
51
52
  return;
52
- copy.features.forEach(feature => stretch(feature.horizontal, 1 / factor));
53
+ featuresOf(copy).forEach(feature => stretch(feature.horizontal, 1 / factor));
53
54
  copy.ops = copy.ops.filter(op => op !== 'stretched');
54
55
  delete copy.measurements.scale;
55
56
  };
package/lib/asJsonLd.d.ts CHANGED
@@ -1,3 +1,8 @@
1
1
  import { Edition } from "./Edition";
2
+ /**
3
+ * The keys an export derives from the tree, which no node of the
4
+ * edition states itself. An import reads them off again.
5
+ */
6
+ export declare const derivedKeys: ReadonlySet<string>;
2
7
  export declare const exportDate: (date: Date) => string;
3
8
  export declare const asJsonLd: (edition: Edition) => any;
package/lib/asJsonLd.js CHANGED
@@ -1,6 +1,11 @@
1
1
  import { systemIdIn } from "./TrackerBar";
2
2
  import { certaintyOf, isAsserted } from "./Assumption";
3
3
  import context from "./spec/context.json";
4
+ /**
5
+ * The keys an export derives from the tree, which no node of the
6
+ * edition states itself. An import reads them off again.
7
+ */
8
+ export const derivedKeys = new Set(['bears', 'composedOf', 'augmented', 'diminished']);
4
9
  export const exportDate = (date) => {
5
10
  const year = date.getFullYear();
6
11
  const month = String(date.getMonth() + 1).padStart(2, "0");
@@ -62,6 +67,55 @@ const withSystemContexts = (node) => {
62
67
  : walked;
63
68
  };
64
69
  const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
70
+ const nodesIn = (value) => Array.isArray(value) ? value.filter(isRecord) : [];
71
+ /** What the acts of a copy brought onto it: the features they produced and the patches they glued on. */
72
+ const madeOn = (copy) => [
73
+ ...nodesIn(copy.production?.produced),
74
+ ...nodesIn(copy.modifications).flatMap(act => [...nodesIn(act.produced), ...nodesIn(act.added)])
75
+ ];
76
+ const referencing = (nodes, patches) => nodes.filter(node => (node['@type'] === 'GluedOn') === patches && typeof node['@id'] === 'string')
77
+ .map(node => ({ '@id': node['@id'] }));
78
+ const featuresAmong = (nodes) => referencing(nodes, false);
79
+ const patchesAmong = (nodes) => referencing(nodes, true);
80
+ /** The key, where there is anything to state under it. */
81
+ const stating = (key, references) => references.length > 0 ? { [key]: references } : {};
82
+ /** The act with what it changed named: an attachment augments the copy, a removal diminishes it. */
83
+ const changing = (copyId) => (act) => {
84
+ if (typeof copyId !== 'string' || !isRecord(act))
85
+ return act;
86
+ if (act['@type'] === 'Attachment')
87
+ return { ...act, augmented: { '@id': copyId } };
88
+ if (act['@type'] === 'Removal')
89
+ return { ...act, diminished: { '@id': copyId } };
90
+ return act;
91
+ };
92
+ /**
93
+ * The document with every bearing stated that its tree only implies. A
94
+ * copy bears the features its acts brought about and is composed of the
95
+ * patches they glued on, a patch bears the features glued onto it in
96
+ * turn. Each feature stands in the act that made it and nothing reads a
97
+ * bearing off that, so the export states it; a patch is no feature, and
98
+ * P56 bears feature takes only features, so a patch is stated as a part.
99
+ */
100
+ const withBearings = (value) => {
101
+ if (Array.isArray(value))
102
+ return value.map(withBearings);
103
+ if (!isRecord(value))
104
+ return value;
105
+ const node = Object.fromEntries(Object.entries(value).map(([key, child]) => [key, withBearings(child)]));
106
+ if (node['@type'] === 'RollCopy') {
107
+ const made = madeOn(node);
108
+ return {
109
+ ...node,
110
+ ...(Array.isArray(node.modifications) && { modifications: node.modifications.map(changing(node['@id'])) }),
111
+ ...stating('bears', featuresAmong(made)),
112
+ ...stating('composedOf', patchesAmong(made))
113
+ };
114
+ }
115
+ if (node['@type'] === 'GluedOn')
116
+ return { ...node, ...stating('bears', featuresAmong(nodesIn(node.features))) };
117
+ return node;
118
+ };
65
119
  /** Terms the context sets to null, which say nothing when the edition is read as RDF. */
66
120
  const silentTerms = new Set(Object.entries(context['@context'])
67
121
  .filter(([, definition]) => definition === null)
@@ -128,7 +182,7 @@ const withDoubtedReferencesQuoted = (value) => {
128
182
  };
129
183
  };
130
184
  export const asJsonLd = (edition) => {
131
- const { node, quoted } = withDoubtedReferencesQuoted(withSystemContexts(asJsonLdEntity(edition)));
185
+ const { node, quoted } = withDoubtedReferencesQuoted(withSystemContexts(withBearings(asJsonLdEntity(edition))));
132
186
  // The context is the export's own; one carried in from an import must not override it.
133
187
  const { base, '@context': carried, ...rest } = node;
134
188
  return {
@@ -7,7 +7,7 @@ import { Edit } from "./Edit";
7
7
  import { GeneralRollCondition, RollCopy, ScaleReading, Shift } from "./RollCopy";
8
8
  import { FeatureSource } from "./FeatureSource";
9
9
  import { AnyArgumentation, Belief, Certainty, ObjectAssumption } from "./Assumption";
10
- import { AnyFeature, FeatureConditionAssignment } from "./Feature";
10
+ import { FeatureConditionAssignment, FeatureOrPatch } from "./Feature";
11
11
  /**
12
12
  * A change to an edition, written onto an immer draft of it. One
13
13
  * operation is one undo step, so an operation that has to read the
@@ -74,7 +74,8 @@ export declare const symbolsCarriedOnlyBy: (edition: Edition, copyId: string) =>
74
74
  * Takes the features off the copy, and out of the versions with what
75
75
  * only they carried. A feature may be named wherever the copy bears it:
76
76
  * a patch goes with everything glued onto it, and a feature of a patch
77
- * may be taken back on its own, the patch staying where it is.
77
+ * may be taken back on its own, the patch staying where it is. An act
78
+ * left having produced or added nothing goes as well.
78
79
  */
79
80
  export declare const removeFeatures: (copyId: string, featureIds: readonly string[]) => EditionOp;
80
81
  /**
@@ -84,7 +85,7 @@ export declare const removeFeatures: (copyId: string, featureIds: readonly strin
84
85
  */
85
86
  export declare const removeCopy: (copyId: string) => EditionOp;
86
87
  /** Why several features cannot be replaced by one. */
87
- export type MergeObstacle = 'fewer-than-two' | 'different-types' | 'different-tracks' | 'differing-conditions' | 'unlike-features';
88
+ export type MergeObstacle = 'fewer-than-two' | 'different-types' | 'different-tracks' | 'differing-conditions' | 'unlike-features' | 'different-acts';
88
89
  /**
89
90
  * What stands in the way of reading the features as one, or nothing
90
91
  * where they may be merged. Features may be merged where they differ
@@ -93,7 +94,7 @@ export type MergeObstacle = 'fewer-than-two' | 'different-types' | 'different-tr
93
94
  * asked: whether a gap is a bridge of the perforator, a tear or two
94
95
  * perforations of their own is the editor's reading.
95
96
  */
96
- export declare const mergeObstacle: (features: readonly AnyFeature[]) => MergeObstacle | undefined;
97
+ export declare const mergeObstacle: (features: readonly FeatureOrPatch[]) => MergeObstacle | undefined;
97
98
  /**
98
99
  * Replaces the features of the copy with a single one covering them
99
100
  * all, where a scan has split what the editor reads as one feature.
@@ -106,7 +107,8 @@ export declare const mergeObstacle: (features: readonly AnyFeature[]) => MergeOb
106
107
  * Only the features the copy bears itself are merged. A feature of a
107
108
  * patch states no place along the roll of its own, so there is nothing
108
109
  * for a merge to span, and an id naming one is passed over as an id the
109
- * copy does not bear is.
110
+ * copy does not bear is. Features of two different acts are not merged
111
+ * either: one feature is the work of one act.
110
112
  *
111
113
  * Throws where the features cannot stand for one; `mergeObstacle`
112
114
  * says beforehand whether they can.