@opentermsarchive/engine 16.0.2 → 16.1.1
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/config/default.json +13 -0
- package/config/test.json +12 -0
- package/package.json +1 -1
- package/scripts/declarations/validate/index.mocha.js +7 -0
- package/scripts/import/index.js +1 -1
- package/scripts/import/loadCommits.js +1 -1
- package/scripts/rewrite/initializer/index.js +1 -1
- package/scripts/rewrite/rewrite-snapshots.js +1 -1
- package/scripts/rewrite/rewrite-versions.js +1 -1
- package/src/archivist/fetcher/htmlOnlyFetcher.js +8 -0
- package/src/archivist/fetcher/index.test.js +12 -0
- package/src/archivist/index.js +106 -23
- package/src/archivist/index.test.js +659 -9
- package/src/archivist/recorder/repositories/git/dataMapper.js +2 -1
- package/src/archivist/recorder/repositories/git/index.js +2 -13
- package/src/archivist/recorder/repositories/git/index.test.js +23 -1
- package/src/archivist/services/index.js +45 -1
- package/src/archivist/services/index.test.js +52 -1
- package/src/archivist/services/sourceDocument.js +8 -2
- package/src/archivist/services/sourceDocument.test.js +37 -0
- package/src/archivist/tracking-results/errors.js +5 -0
- package/src/archivist/tracking-results/index.js +233 -0
- package/src/archivist/tracking-results/index.test.js +406 -0
- package/src/archivist/tracking-results/recorder.js +156 -0
- package/src/archivist/tracking-results/recorder.test.js +559 -0
- package/src/archivist/tracking-results/repository.js +136 -0
- package/src/archivist/tracking-results/repository.test.js +763 -0
- package/src/archivist/tracking-results/run/dataMapper.js +51 -0
- package/src/archivist/tracking-results/run/dataMapper.test.js +168 -0
- package/src/archivist/tracking-results/run/index.js +115 -0
- package/src/archivist/tracking-results/run/index.test.js +221 -0
- package/src/archivist/tracking-results/terms-result/dataMapper.js +200 -0
- package/src/archivist/tracking-results/terms-result/dataMapper.test.js +575 -0
- package/src/archivist/tracking-results/terms-result/index.js +42 -0
- package/src/archivist/tracking-results/terms-result/index.test.js +228 -0
- package/src/git/errors.js +1 -0
- package/src/{archivist/recorder/repositories/git/git.js → git/index.js} +59 -9
- package/src/git/index.test.js +266 -0
- package/src/git/pathSegment.js +18 -0
- package/src/git/pathSegment.test.js +33 -0
- package/src/index.js +5 -4
- package/src/reporter/factory.js +2 -2
- package/src/reporter/github/index.js +7 -5
- package/src/reporter/github/index.test.js +16 -1
- package/src/reporter/gitlab/index.js +6 -6
- package/src/reporter/gitlab/index.test.js +24 -1
- package/src/reporter/index.js +9 -4
- package/src/reporter/index.test.js +74 -9
- package/src/archivist/recorder/repositories/git/git.test.js +0 -114
- /package/src/{archivist/recorder/repositories/git → git}/trailers.js +0 -0
- /package/src/{archivist/recorder/repositories/git → git}/trailers.test.js +0 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'util';
|
|
2
|
+
|
|
3
|
+
import { isPortableFileName } from '../../../git/pathSegment.js';
|
|
4
|
+
|
|
5
|
+
import TermsResult, { STATUSES } from './index.js';
|
|
6
|
+
|
|
7
|
+
export const EVENT_TYPES = Object.freeze({
|
|
8
|
+
FIRST_TRACKING: 'firstTracking',
|
|
9
|
+
FIRST_TRACKING_FAILURE: 'firstTrackingFailure',
|
|
10
|
+
TRACKING_FAILURE: 'trackingFailure',
|
|
11
|
+
TRACKING_RECOVERY: 'trackingRecovery',
|
|
12
|
+
REASONS_CHANGED: 'reasonsChanged',
|
|
13
|
+
TRANSIENT_ERROR_DETECTED: 'transientErrorDetected',
|
|
14
|
+
TRANSIENT_ERROR_RESOLVED: 'transientErrorResolved',
|
|
15
|
+
DECLARATION_UPDATED: 'declarationUpdated',
|
|
16
|
+
SERVICE_NAME_UPDATED: 'serviceNameUpdated',
|
|
17
|
+
MIME_TYPE_UPDATED: 'mimeTypeUpdated',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Run-level transition fed by each event type, colocated with EVENT_TYPES so a new substantive-change category decides here whether it is a transition; Run.recordTransition validates the values at runtime.
|
|
21
|
+
// FIRST_TRACKING and FIRST_TRACKING_FAILURE (initial appearance) and the declaration, MIME type and service-name updates are deliberately not transitions: only true transitions between runs of the same terms feed run.transitions.
|
|
22
|
+
export const TRANSITIONS_BY_EVENT_TYPE = Object.freeze({
|
|
23
|
+
[EVENT_TYPES.TRACKING_FAILURE]: 'newFailures',
|
|
24
|
+
[EVENT_TYPES.TRACKING_RECOVERY]: 'recoveries',
|
|
25
|
+
[EVENT_TYPES.REASONS_CHANGED]: 'reasonChanges',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const FAILURE_TYPE_PRIORITY = Object.freeze([ 'fetch', 'extraction', 'internal' ]);
|
|
29
|
+
|
|
30
|
+
export function termsKey(serviceId, termsType) { // Canonical composite identifier of a service/terms pair, shared by the recovery sets and the persisted file paths so the format is defined once
|
|
31
|
+
return `${serviceId}/${termsType}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function generateFilePath(serviceId, termsType) {
|
|
35
|
+
validatePathComponent(serviceId, 'serviceId');
|
|
36
|
+
validatePathComponent(termsType, 'termsType');
|
|
37
|
+
|
|
38
|
+
return `${termsKey(serviceId, termsType)}.json`; // Do not use `path.join` as Git requires forward slashes even on Windows
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function validatePathComponent(value, name) { // Rejects identifiers that would escape their directory once joined into a file path, or that could not exist as a file name on every supported platform
|
|
42
|
+
if (typeof value !== 'string' || !isPortableFileName(value)) {
|
|
43
|
+
throw new Error(`Invalid ${name}: must be usable as a cross-platform file name (a non-empty string without separators, "." or ".." forms, control characters, or the Windows-reserved characters \`:"<>|*?\`), got ${JSON.stringify(value)}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function toPersistence(newResult, previousResult) {
|
|
48
|
+
const eventType = determineEventType(previousResult, newResult);
|
|
49
|
+
|
|
50
|
+
if (!eventType) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const messageParams = {
|
|
55
|
+
serviceId: newResult.serviceId, // Commit subjects are keyed by the stable serviceId, aligned with snapshots/versions subjects and with the file paths, so a grep by id spans subjects and paths alike; the human-readable serviceName lives in the file content
|
|
56
|
+
termsType: newResult.termsType,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if ([ EVENT_TYPES.TRACKING_FAILURE, EVENT_TYPES.FIRST_TRACKING_FAILURE ].includes(eventType)) {
|
|
60
|
+
messageParams.failureType = deriveFailureType(newResult.event.reasons);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
eventType, // Returned so callers (TrackingResultsRepository) can surface the detected event without re-running determineEventType
|
|
65
|
+
message: formatMessage(eventType, messageParams),
|
|
66
|
+
content: `${JSON.stringify(toJSON(newResult), null, 2)}\n`,
|
|
67
|
+
filePath: generateFilePath(newResult.serviceId, newResult.termsType),
|
|
68
|
+
date: newResult.event.date,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function toDomain({ serviceId, termsType, data }) {
|
|
73
|
+
const result = new TermsResult({
|
|
74
|
+
serviceId,
|
|
75
|
+
termsType,
|
|
76
|
+
status: data?.status,
|
|
77
|
+
event: data?.event,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
result.validate();
|
|
82
|
+
} catch (error) {
|
|
83
|
+
throw new Error(`Invalid TermsResult content for ${serviceId}/${termsType}: ${error.message}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function deriveFailureType(reasons = []) {
|
|
90
|
+
return FAILURE_TYPE_PRIORITY.find(type => reasons.some(reason => reason.startsWith(`[${type}]`))) || 'internal'; // Defaults to "internal" when no recognised prefix is found, which best reflects an unclassified engine-side problem
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function determineEventType(previousResult, newResult) {
|
|
94
|
+
// Priority is deliberate and not commutative: when several substantive fields change in the same transition, the first matching branch wins and labels the commit.
|
|
95
|
+
// Side-effect: a lower-priority change (e.g. declaration update) that coincides with a higher-priority change (e.g. transient error appearance) is still persisted in the file content but not attributed in the commit subject; if the higher-priority change later resolves with the declaration still updated, no further commit fires because the declaration is already in place. This is a documented audit-trail trade-off.
|
|
96
|
+
|
|
97
|
+
if (!previousResult) {
|
|
98
|
+
return newResult.status === STATUSES.failed ? EVENT_TYPES.FIRST_TRACKING_FAILURE : EVENT_TYPES.FIRST_TRACKING;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (previousResult.status !== newResult.status) {
|
|
102
|
+
return newResult.status === STATUSES.failed
|
|
103
|
+
? EVENT_TYPES.TRACKING_FAILURE
|
|
104
|
+
: EVENT_TYPES.TRACKING_RECOVERY;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// From here, status is the same on both sides.
|
|
108
|
+
|
|
109
|
+
if (newResult.status === STATUSES.failed && !isDeepStrictEqual(previousResult.event.reasons, newResult.event.reasons)) { // Reasons are ordered: a different order is a different value
|
|
110
|
+
return EVENT_TYPES.REASONS_CHANGED;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const previousTransient = previousResult.event.transientError;
|
|
114
|
+
const newTransient = newResult.event.transientError;
|
|
115
|
+
|
|
116
|
+
if (!previousTransient && newTransient) {
|
|
117
|
+
return EVENT_TYPES.TRANSIENT_ERROR_DETECTED;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (previousTransient && !newTransient) {
|
|
121
|
+
return EVENT_TYPES.TRANSIENT_ERROR_RESOLVED;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// From here, the presence of a transient error is identical on both sides; a change of its reasons alone is not substantive.
|
|
125
|
+
|
|
126
|
+
if (sourceDocumentsDifferOnDeclaration(previousResult.event.sourceDocuments, newResult.event.sourceDocuments)) {
|
|
127
|
+
return EVENT_TYPES.DECLARATION_UPDATED;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// From here, the declared fields are identical on both sides: a remaining source-document difference is MIME type only.
|
|
131
|
+
|
|
132
|
+
if (!isDeepStrictEqual(mimeTypesOf(previousResult.event.sourceDocuments), mimeTypesOf(newResult.event.sourceDocuments))) {
|
|
133
|
+
return EVENT_TYPES.MIME_TYPE_UPDATED;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (previousResult.event.serviceName !== newResult.event.serviceName) {
|
|
137
|
+
return EVENT_TYPES.SERVICE_NAME_UPDATED;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function formatMessage(eventType, { serviceId, termsType, failureType } = {}) {
|
|
144
|
+
const suffix = `of ${serviceId} ${termsType}`;
|
|
145
|
+
|
|
146
|
+
switch (eventType) {
|
|
147
|
+
case EVENT_TYPES.FIRST_TRACKING:
|
|
148
|
+
return `Record first tracking ${suffix}`;
|
|
149
|
+
case EVENT_TYPES.TRACKING_FAILURE:
|
|
150
|
+
case EVENT_TYPES.FIRST_TRACKING_FAILURE: // Same subject as a failure that follows a success, so that every failing terms can be found by its subject from its first appearance
|
|
151
|
+
return `Record tracking failure ${suffix} (${failureType})`;
|
|
152
|
+
case EVENT_TYPES.TRACKING_RECOVERY:
|
|
153
|
+
return `Record tracking recovery ${suffix}`;
|
|
154
|
+
case EVENT_TYPES.REASONS_CHANGED:
|
|
155
|
+
return `Update failure reasons ${suffix}`;
|
|
156
|
+
case EVENT_TYPES.TRANSIENT_ERROR_DETECTED:
|
|
157
|
+
return `Record transient error ${suffix}`;
|
|
158
|
+
case EVENT_TYPES.TRANSIENT_ERROR_RESOLVED:
|
|
159
|
+
return `Clear transient error ${suffix}`;
|
|
160
|
+
case EVENT_TYPES.DECLARATION_UPDATED:
|
|
161
|
+
return `Update tracking declaration ${suffix}`;
|
|
162
|
+
case EVENT_TYPES.SERVICE_NAME_UPDATED:
|
|
163
|
+
return `Update service name ${suffix}`;
|
|
164
|
+
case EVENT_TYPES.MIME_TYPE_UPDATED:
|
|
165
|
+
return `Update MIME type ${suffix}`;
|
|
166
|
+
default:
|
|
167
|
+
throw new Error(`Unknown tracking-result event type: "${eventType}"`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function toJSON(result) {
|
|
172
|
+
const event = {
|
|
173
|
+
date: result.event.date,
|
|
174
|
+
serviceName: result.event.serviceName,
|
|
175
|
+
sourceDocuments: result.event.sourceDocuments,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
if (result.status === STATUSES.failed) {
|
|
179
|
+
event.reasons = result.event.reasons;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (result.event.transientError) {
|
|
183
|
+
event.transientError = result.event.transientError;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { status: result.status, event };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function declaredFieldsOf(sourceDocuments) {
|
|
190
|
+
// Keeps only the fields that come from the declaration file (id, fetch, select, remove, filter, executeClientScripts). Drops mimeType (an observation from fetch, handled by its own MIME_TYPE_UPDATED event) and snapshotId (an observation from the snapshot record, which changes on every new snapshot but does not represent a tracking-results-level change).
|
|
191
|
+
return sourceDocuments.map(({ mimeType, snapshotId, ...rest }) => rest); // eslint-disable-line no-unused-vars
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function mimeTypesOf(sourceDocuments) {
|
|
195
|
+
return sourceDocuments.map(doc => doc.mimeType ?? null);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function sourceDocumentsDifferOnDeclaration(previous, next) {
|
|
199
|
+
return !isDeepStrictEqual(declaredFieldsOf(previous), declaredFieldsOf(next)); // sourceDocuments[i] is positional: reordering documents is a declaration change
|
|
200
|
+
}
|