Repository navigation
Expand file tree
/
Copy pathdata-quality.js
More file actions
208 lines (192 loc) · 11.6 KB
/
Copy pathdata-quality.js
File metadata and controls
208 lines (192 loc) · 11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
(function (root, factory) {
var chronology = typeof module === 'object' && module.exports ? require('./chronology.js') : root.ParallelWorldsChronology;
var api = factory(chronology);
if (typeof module === 'object' && module.exports) module.exports = api;
root.ParallelWorldsDataQuality = api;
}(typeof self !== 'undefined' ? self : this, function (chronology) {
'use strict';
var TYPES = ['archaeological-culture', 'site', 'polity', 'regional-sequence', 'network', 'tradition'];
var PRECISIONS = ['exact', 'approximate', 'range', 'traditional', 'disputed'];
var BASES = ['historical', 'archaeological-chronology', 'radiocarbon', 'dendrochronology', 'stratigraphy', 'traditional'];
var CONFIDENCE = ['high', 'medium', 'low'];
var LOCALES = ['ru', 'en', 'zh'];
var SOURCE_TIERS = ['A', 'B', 'C'];
var SOURCE_KINDS = [
'academic-encyclopedia-article', 'academic-encyclopedia-chronology', 'academic-synthesis',
'heritage-dossier', 'museum-collection-record', 'peer-reviewed-article',
'peer-reviewed-museum-journal', 'scholarly-museum-chronology', 'scholarly-museum-essay',
'scholarly-museum-publication'
];
function issue(code, path, message) {
return { code: code, path: path, message: message };
}
function isNonEmptyString(value) {
return typeof value === 'string' && Boolean(value.trim());
}
function isGenericHomepage(value) {
try {
var url = new URL(value);
var path = url.pathname.replace(/\/+$/, '') || '/';
return path === '/' || ['/collection', '/education', '/toah/chronology', '/toah/chronology/'].indexOf(path) !== -1;
} catch (error) {
return true;
}
}
function isExactSource(source) {
if (!source || SOURCE_TIERS.indexOf(source.tier) === -1 || SOURCE_KINDS.indexOf(source.kind) === -1) return false;
if (!['title', 'publisher', 'url', 'accessed'].every(function (field) { return isNonEmptyString(source[field]); })) return false;
if (source.year === undefined || source.year === '') return false;
return /^https:\/\//.test(source.url) && !isGenericHomepage(source.url);
}
function validateSource(source, path) {
var issues = [];
['tier', 'kind', 'title', 'publisher', 'year', 'url', 'accessed'].forEach(function (field) {
if (!source || source[field] === undefined || source[field] === '') issues.push(issue('missing-source-field', path + '.' + field, 'Missing source field ' + field));
});
if (source && SOURCE_TIERS.indexOf(source.tier) === -1) issues.push(issue('invalid-source-tier', path + '.tier', 'Unknown evidence tier'));
if (source && SOURCE_KINDS.indexOf(source.kind) === -1) issues.push(issue('invalid-source-kind', path + '.kind', 'Unknown or non-academic source kind'));
if (source && (!/^https:\/\//.test(source.url || '') || isGenericHomepage(source.url))) issues.push(issue('generic-source-url', path + '.url', 'Reviewed sources must use an exact HTTPS page or DOI'));
return issues;
}
function validateSources(sources) {
return Object.keys(sources || {}).reduce(function (issues, id) {
return issues.concat(validateSource(sources[id], 'sources.' + id));
}, []);
}
function validateCopy(copy, kind, path) {
var issues = [];
LOCALES.forEach(function (locale) {
var localized = copy && copy[locale];
var primary = kind === 'event' ? 'title' : 'name';
if (!localized || !localized[primary]) issues.push(issue('missing-localization', path + '.copy.' + locale, 'Missing ' + locale + ' ' + primary));
if (kind === 'track' && (!localized || !localized.summary)) issues.push(issue('missing-localization', path + '.copy.' + locale + '.summary', 'Missing ' + locale + ' summary'));
});
return issues;
}
function validateDating(dating, path, range) {
var issues = [];
if (!dating || PRECISIONS.indexOf(dating.precision) === -1) issues.push(issue('invalid-precision', path + '.dating.precision', 'Missing or invalid dating precision'));
if (!dating || BASES.indexOf(dating.basis) === -1) issues.push(issue('invalid-dating-basis', path + '.dating.basis', 'Missing or invalid dating basis'));
if (!dating) return issues;
if (dating.confidence !== undefined && CONFIDENCE.indexOf(dating.confidence) === -1) {
issues.push(issue('invalid-confidence', path + '.dating.confidence', 'Unknown dating confidence'));
}
if (dating.calibrationCurve !== undefined && (typeof dating.calibrationCurve !== 'string' || !dating.calibrationCurve.trim())) {
issues.push(issue('invalid-calibration-curve', path + '.dating.calibrationCurve', 'Calibration curve must be a non-empty string'));
}
if (dating.model !== undefined && (typeof dating.model !== 'string' || !dating.model.trim())) {
issues.push(issue('invalid-chronology-model', path + '.dating.model', 'Chronology model must be a non-empty string'));
}
if (dating.model !== undefined) {
LOCALES.forEach(function (locale) {
if (!dating.copy || !dating.copy[locale] || !isNonEmptyString(dating.copy[locale].model)) {
issues.push(issue('missing-localization', path + '.dating.copy.' + locale + '.model', 'Missing ' + locale + ' chronology model'));
}
});
}
if (dating.alternatives !== undefined && !Array.isArray(dating.alternatives)) {
issues.push(issue('invalid-alternatives', path + '.dating.alternatives', 'Chronology alternatives must be an array'));
}
var alternativeIds = {};
(Array.isArray(dating.alternatives) ? dating.alternatives : []).forEach(function (alternative, index) {
var alternativePath = path + '.dating.alternatives[' + index + ']';
if (!alternative || typeof alternative.id !== 'string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(alternative.id)) {
issues.push(issue('invalid-alternative-id', alternativePath + '.id', 'Alternative chronology needs a stable ID'));
} else if (alternativeIds[alternative.id]) {
issues.push(issue('duplicate-alternative-id', alternativePath + '.id', 'Alternative chronology IDs must be unique'));
} else {
alternativeIds[alternative.id] = true;
}
if (alternative) {
issues = issues.concat(validateYear(alternative.start, range, alternativePath + '.start'));
issues = issues.concat(validateYear(alternative.end, range, alternativePath + '.end'));
if (Number.isFinite(alternative.start) && Number.isFinite(alternative.end) && alternative.start >= alternative.end) {
issues.push(issue('invalid-alternative-range', alternativePath, 'Alternative chronology start must precede end'));
}
LOCALES.forEach(function (locale) {
if (!alternative.copy || !alternative.copy[locale] || !isNonEmptyString(alternative.copy[locale].label)) {
issues.push(issue('missing-localization', alternativePath + '.copy.' + locale + '.label', 'Missing ' + locale + ' alternative label'));
}
});
}
});
if ((dating.precision === 'disputed' || Array.isArray(dating.alternatives) && dating.alternatives.length) &&
(typeof dating.disputeNote !== 'string' || !dating.disputeNote.trim())) {
issues.push(issue('missing-dispute-note', path + '.dating.disputeNote', 'Disputed or alternative chronology needs an explanation'));
}
if (dating.disputeNote !== undefined) {
LOCALES.forEach(function (locale) {
if (!dating.copy || !dating.copy[locale] || !isNonEmptyString(dating.copy[locale].disputeNote)) {
issues.push(issue('missing-localization', path + '.dating.copy.' + locale + '.disputeNote', 'Missing ' + locale + ' chronology dispute note'));
}
});
}
return issues;
}
function validateSourceIds(sourceIds, sources, path) {
if (!Array.isArray(sourceIds) || !sourceIds.length) return [issue('missing-source', path + '.sourceIds', 'Reviewed record has no source')];
return sourceIds.reduce(function (issues, id) {
if (!sources[id]) issues.push(issue('unknown-source', path + '.sourceIds', 'Unknown source ' + id));
else issues = issues.concat(validateSource(sources[id], 'sources.' + id));
return issues;
}, []);
}
function validateYear(year, range, path) {
var issues = [];
if (!chronology.isValidHistoricalYear(year)) issues.push(issue(year === 0 ? 'year-zero' : 'invalid-year', path, 'Historical years must be non-zero integers'));
if (Number.isFinite(year) && (year < range.start || year > range.end)) issues.push(issue('out-of-range', path, 'Date is outside the canonical range'));
return issues;
}
function validateReviewedTrack(track, sources, range) {
var issues = [];
var ids = {};
if (!track || !track.id) return [issue('missing-id', 'track', 'Track ID is required')];
if (TYPES.indexOf(track.type) === -1) issues.push(issue('invalid-type', track.id + '.type', 'Unknown academic track type'));
if (track.reviewStatus !== 'reviewed') issues.push(issue('invalid-review-status', track.id + '.reviewStatus', 'Academic validation expects a reviewed track'));
issues = issues.concat(validateCopy(track.copy, 'track', track.id));
if (!Array.isArray(track.periods) || !track.periods.length) issues.push(issue('missing-period', track.id + '.periods', 'Track must contain a period'));
if (!Array.isArray(track.events) || !track.events.length) issues.push(issue('missing-event', track.id + '.events', 'Track must contain an event'));
(track.periods || []).forEach(function (period, index) {
var path = track.id + '.periods[' + index + ']';
if (!period.id || ids[period.id]) issues.push(issue(period.id ? 'duplicate-id' : 'missing-id', path + '.id', 'Period ID must be unique'));
if (period.id) ids[period.id] = true;
issues = issues.concat(validateYear(period.start, range, path + '.start'));
issues = issues.concat(validateYear(period.end, range, path + '.end'));
if (Number.isFinite(period.start) && Number.isFinite(period.end) && period.start >= period.end) issues.push(issue('invalid-range', path, 'Period start must precede end'));
issues = issues.concat(validateDating(period.dating, path, range));
issues = issues.concat(validateSourceIds(period.sourceIds, sources, path));
issues = issues.concat(validateCopy(period.copy, 'period', path));
});
(track.events || []).forEach(function (event, index) {
var path = track.id + '.events[' + index + ']';
if (!event.id || ids[event.id]) issues.push(issue(event.id ? 'duplicate-id' : 'missing-id', path + '.id', 'Event ID must be unique'));
if (event.id) ids[event.id] = true;
issues = issues.concat(validateYear(event.year, range, path + '.year'));
issues = issues.concat(validateDating(event.dating, path, range));
issues = issues.concat(validateSourceIds(event.sourceIds, sources, path));
issues = issues.concat(validateCopy(event.copy, 'event', path));
});
return issues;
}
function validateDataset(data) {
var ids = {};
return (data.tracks || []).reduce(function (issues, track) {
if (ids[track.id]) issues.push(issue('duplicate-track-id', track.id, 'Track ID must be unique'));
ids[track.id] = true;
if (track.reviewStatus === 'reviewed') issues = issues.concat(validateReviewedTrack(track, data.sources || {}, data.range));
else if (track.reviewStatus !== 'legacy' && track.reviewStatus !== 'provisional') issues.push(issue('missing-review-status', track.id + '.reviewStatus', 'Track review status is required'));
return issues;
}, []);
}
return {
BASES: BASES,
CONFIDENCE: CONFIDENCE,
PRECISIONS: PRECISIONS,
TYPES: TYPES,
isExactSource: isExactSource,
isGenericHomepage: isGenericHomepage,
validateDataset: validateDataset,
validateReviewedTrack: validateReviewedTrack,
validateSources: validateSources
};
}));