@byline/core 6.12.0 → 6.13.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.
Files changed (48) hide show
  1. package/dist/@types/extraction-types.d.ts +423 -0
  2. package/dist/@types/extraction-types.js +8 -0
  3. package/dist/@types/index.d.ts +1 -0
  4. package/dist/@types/index.js +1 -0
  5. package/dist/extraction/artifact.d.ts +46 -0
  6. package/dist/extraction/artifact.js +121 -0
  7. package/dist/extraction/artifact.test.node.d.ts +8 -0
  8. package/dist/extraction/artifact.test.node.js +194 -0
  9. package/dist/extraction/errors.d.ts +47 -0
  10. package/dist/extraction/errors.js +57 -0
  11. package/dist/extraction/errors.test.node.d.ts +8 -0
  12. package/dist/extraction/errors.test.node.js +68 -0
  13. package/dist/extraction/evidence.d.ts +22 -0
  14. package/dist/extraction/evidence.js +54 -0
  15. package/dist/extraction/evidence.test.node.d.ts +8 -0
  16. package/dist/extraction/evidence.test.node.js +138 -0
  17. package/dist/extraction/index.d.ts +14 -0
  18. package/dist/extraction/index.js +17 -0
  19. package/dist/extraction/internal.d.ts +30 -0
  20. package/dist/extraction/internal.js +93 -0
  21. package/dist/extraction/outcomes.d.ts +20 -0
  22. package/dist/extraction/outcomes.js +30 -0
  23. package/dist/extraction/outcomes.test.node.d.ts +8 -0
  24. package/dist/extraction/outcomes.test.node.js +55 -0
  25. package/dist/extraction/page-map.d.ts +40 -0
  26. package/dist/extraction/page-map.js +190 -0
  27. package/dist/extraction/page-map.test.node.d.ts +8 -0
  28. package/dist/extraction/page-map.test.node.js +371 -0
  29. package/dist/extraction/producer.d.ts +62 -0
  30. package/dist/extraction/producer.js +76 -0
  31. package/dist/extraction/producer.test.node.d.ts +8 -0
  32. package/dist/extraction/producer.test.node.js +230 -0
  33. package/dist/extraction/root-graph-guard.test.node.d.ts +8 -0
  34. package/dist/extraction/root-graph-guard.test.node.js +293 -0
  35. package/dist/extraction/work.d.ts +55 -0
  36. package/dist/extraction/work.js +102 -0
  37. package/dist/extraction/work.test.node.d.ts +8 -0
  38. package/dist/extraction/work.test.node.js +409 -0
  39. package/dist/index.d.ts +1 -0
  40. package/dist/index.js +1 -0
  41. package/dist/storage/stored-files-server.d.ts +1 -0
  42. package/dist/storage/stored-files-server.js +9 -24
  43. package/dist/storage/stored-files-server.test.node.js +6 -0
  44. package/dist/storage/verified-read.d.ts +131 -0
  45. package/dist/storage/verified-read.js +328 -0
  46. package/dist/storage/verified-read.test.node.d.ts +8 -0
  47. package/dist/storage/verified-read.test.node.js +645 -0
  48. package/package.json +6 -6
@@ -0,0 +1,371 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import { describe, expect, it } from 'vitest';
9
+ import { validatePageMap } from './page-map.js';
10
+ // "alpha" + "\n\n" + "beta" + "\n\n" + "gamma"
11
+ const TEXT = 'alpha\n\nbeta\n\ngamma';
12
+ function validMap() {
13
+ return {
14
+ version: 1,
15
+ assembly: { pageSeparator: '\n\n' },
16
+ pages: [
17
+ { index: 1, charStart: 0, charEnd: 5 },
18
+ { index: 2, charStart: 7, charEnd: 11 },
19
+ { index: 3, charStart: 13, charEnd: 18 },
20
+ ],
21
+ };
22
+ }
23
+ function codes(plainText, map, options) {
24
+ const result = validatePageMap(plainText, map, options);
25
+ return result.ok ? [] : result.issues.map((issue) => issue.code);
26
+ }
27
+ function expectIssue(code, plainText, map, options) {
28
+ expect(codes(plainText, map, options)).toContain(code);
29
+ }
30
+ describe('validatePageMap', () => {
31
+ it('accepts a complete map', () => {
32
+ expect(validatePageMap(TEXT, validMap(), { pageCount: 3 })).toEqual({ ok: true });
33
+ });
34
+ it('accepts a label, blocks and a declared trailing mode', () => {
35
+ const map = validMap();
36
+ map.assembly.trailing = 'none';
37
+ map.pages[0] = { index: 1, label: 'i', charStart: 0, charEnd: 5 };
38
+ map.blocks = [
39
+ { page: 1, charStart: 0, charEnd: 2, kind: 'heading' },
40
+ { page: 1, charStart: 2, charEnd: 5, kind: 'paragraph' },
41
+ ];
42
+ expect(validatePageMap(TEXT, map)).toEqual({ ok: true });
43
+ });
44
+ it('accepts an empty page as a zero-length range', () => {
45
+ const map = {
46
+ version: 1,
47
+ assembly: { pageSeparator: '\n\n' },
48
+ pages: [
49
+ { index: 1, charStart: 0, charEnd: 2 },
50
+ { index: 2, charStart: 4, charEnd: 4 },
51
+ { index: 3, charStart: 6, charEnd: 8 },
52
+ ],
53
+ };
54
+ expect(validatePageMap('ab\n\n\n\ncd', map, { pageCount: 3 })).toEqual({ ok: true });
55
+ });
56
+ it('accepts empty text with no pages', () => {
57
+ const map = { version: 1, assembly: { pageSeparator: '' }, pages: [] };
58
+ expect(validatePageMap('', map)).toEqual({ ok: true });
59
+ });
60
+ it('accepts embedded text after the last page', () => {
61
+ const plainText = 'alpha\n\nextra';
62
+ const map = {
63
+ version: 1,
64
+ assembly: { pageSeparator: '\n\n', trailing: 'embedded' },
65
+ pages: [{ index: 1, charStart: 0, charEnd: 5 }],
66
+ unanchored: [{ kind: 'embedded', charStart: 7, charEnd: 12 }],
67
+ };
68
+ expect(validatePageMap(plainText, map, { pageCount: 1 })).toEqual({ ok: true });
69
+ });
70
+ it('accepts an unanchored range between pages when no trailing mode is embedded', () => {
71
+ const plainText = 'a\n\nnote\n\nb';
72
+ const map = {
73
+ version: 1,
74
+ assembly: { pageSeparator: '\n\n' },
75
+ pages: [
76
+ { index: 1, charStart: 0, charEnd: 1 },
77
+ { index: 2, charStart: 9, charEnd: 10 },
78
+ ],
79
+ unanchored: [{ kind: 'note', charStart: 3, charEnd: 7 }],
80
+ };
81
+ expect(validatePageMap(plainText, map)).toEqual({ ok: true });
82
+ });
83
+ describe('rule 1: version and separator', () => {
84
+ it('rejects another version', () => {
85
+ expectIssue('invalid-version', TEXT, { ...validMap(), version: 2 });
86
+ });
87
+ it('rejects a missing separator', () => {
88
+ expectIssue('invalid-separator', TEXT, { ...validMap(), assembly: {} });
89
+ });
90
+ it.each([
91
+ ['undefined', undefined],
92
+ ['null', null],
93
+ ['an array', []],
94
+ ['pages that are not an array', { ...validMap(), pages: 'nope' }],
95
+ ])('rejects a map that is %s as malformed', (_label, map) => {
96
+ expect(codes(TEXT, map).length).toBeGreaterThan(0);
97
+ });
98
+ it('rejects an unknown trailing mode', () => {
99
+ expectIssue('invalid-trailing', TEXT, {
100
+ ...validMap(),
101
+ assembly: { pageSeparator: '\n\n', trailing: 'sometimes' },
102
+ });
103
+ });
104
+ });
105
+ describe('rule 2: offsets', () => {
106
+ it.each([
107
+ ['a fractional offset', 1.5, 5],
108
+ ['a negative offset', -1, 5],
109
+ ['an end past the text', 0, 99],
110
+ ['a start after its end', 5, 2],
111
+ ['a string offset', '0', 5],
112
+ ])('rejects %s', (_label, charStart, charEnd) => {
113
+ const map = validMap();
114
+ map.pages[0] = { index: 1, charStart, charEnd };
115
+ expectIssue('invalid-offset', TEXT, map);
116
+ });
117
+ it('checks unanchored and block offsets too', () => {
118
+ expectIssue('invalid-offset', TEXT, {
119
+ ...validMap(),
120
+ unanchored: [{ kind: 'x', charStart: 99, charEnd: 100 }],
121
+ });
122
+ expectIssue('invalid-offset', TEXT, {
123
+ ...validMap(),
124
+ blocks: [{ page: 1, charStart: 4, charEnd: 2, kind: 'x' }],
125
+ });
126
+ });
127
+ });
128
+ describe('rule 3: surrogate pairs', () => {
129
+ const EMOJI_TEXT = 'a\u{1F600}b';
130
+ it('rejects a boundary inside a surrogate pair', () => {
131
+ expectIssue('surrogate-split', EMOJI_TEXT, {
132
+ version: 1,
133
+ assembly: { pageSeparator: '' },
134
+ pages: [
135
+ { index: 1, charStart: 0, charEnd: 2 },
136
+ { index: 2, charStart: 2, charEnd: 4 },
137
+ ],
138
+ });
139
+ });
140
+ it('accepts boundaries around a whole pair', () => {
141
+ const map = {
142
+ version: 1,
143
+ assembly: { pageSeparator: '' },
144
+ pages: [
145
+ { index: 1, charStart: 0, charEnd: 3 },
146
+ { index: 2, charStart: 3, charEnd: 4 },
147
+ ],
148
+ };
149
+ expect(validatePageMap(EMOJI_TEXT, map)).toEqual({ ok: true });
150
+ });
151
+ });
152
+ describe('rule 4: page indexes', () => {
153
+ it.each([
154
+ ['zero', 0],
155
+ ['a negative index', -1],
156
+ ['a fractional index', 1.5],
157
+ ])('rejects %s', (_label, index) => {
158
+ const map = validMap();
159
+ map.pages[0] = { index, charStart: 0, charEnd: 5 };
160
+ expectIssue('invalid-page-index', TEXT, map);
161
+ });
162
+ it('rejects indexes that do not strictly increase', () => {
163
+ const map = validMap();
164
+ map.pages[1] = { index: 1, charStart: 7, charEnd: 11 };
165
+ expectIssue('invalid-page-index', TEXT, map);
166
+ });
167
+ it('rejects a gap against pageCount, because an empty page must still have an entry', () => {
168
+ const map = {
169
+ version: 1,
170
+ assembly: { pageSeparator: '\n\n' },
171
+ pages: [
172
+ { index: 1, charStart: 0, charEnd: 5 },
173
+ { index: 3, charStart: 7, charEnd: 11 },
174
+ { index: 4, charStart: 13, charEnd: 18 },
175
+ ],
176
+ };
177
+ expectIssue('page-count-mismatch', TEXT, map, { pageCount: 3 });
178
+ });
179
+ it('rejects too few pages against pageCount', () => {
180
+ expectIssue('page-count-mismatch', TEXT, validMap(), { pageCount: 4 });
181
+ });
182
+ });
183
+ describe('rule 5: page order', () => {
184
+ it('rejects overlapping pages', () => {
185
+ const map = validMap();
186
+ map.pages[1] = { index: 2, charStart: 3, charEnd: 11 };
187
+ expectIssue('page-overlap', TEXT, map);
188
+ });
189
+ it('rejects pages whose offsets run backwards', () => {
190
+ const map = {
191
+ version: 1,
192
+ assembly: { pageSeparator: '\n\n' },
193
+ pages: [
194
+ { index: 1, charStart: 7, charEnd: 11 },
195
+ { index: 2, charStart: 0, charEnd: 5 },
196
+ ],
197
+ };
198
+ expectIssue('page-overlap', TEXT, map);
199
+ });
200
+ });
201
+ describe('rule 6: separators', () => {
202
+ it('rejects a gap that differs from the declared separator', () => {
203
+ expectIssue('separator-mismatch', TEXT, {
204
+ ...validMap(),
205
+ assembly: { pageSeparator: '\f' },
206
+ });
207
+ });
208
+ it('rejects text that a page range skipped', () => {
209
+ const map = validMap();
210
+ map.pages[1] = { index: 2, charStart: 8, charEnd: 11 };
211
+ expectIssue('separator-mismatch', TEXT, map);
212
+ });
213
+ it('checks the gap between a page and an unanchored range', () => {
214
+ const plainText = 'alpha--extra';
215
+ expectIssue('separator-mismatch', plainText, {
216
+ version: 1,
217
+ assembly: { pageSeparator: '\n\n', trailing: 'embedded' },
218
+ pages: [{ index: 1, charStart: 0, charEnd: 5 }],
219
+ unanchored: [{ kind: 'embedded', charStart: 7, charEnd: 12 }],
220
+ });
221
+ });
222
+ });
223
+ describe('rule 7: complete coverage', () => {
224
+ it('rejects text before the first range', () => {
225
+ const map = validMap();
226
+ map.pages[0] = { index: 1, charStart: 1, charEnd: 5 };
227
+ expectIssue('text-outside-ranges', TEXT, map);
228
+ });
229
+ it('rejects text after the last range', () => {
230
+ const map = validMap();
231
+ map.pages[2] = { index: 3, charStart: 13, charEnd: 17 };
232
+ expectIssue('text-outside-ranges', TEXT, map);
233
+ });
234
+ it('rejects text with no ranges at all', () => {
235
+ expectIssue('text-outside-ranges', 'alpha', {
236
+ version: 1,
237
+ assembly: { pageSeparator: '' },
238
+ pages: [],
239
+ });
240
+ });
241
+ });
242
+ describe('rule 8: unanchored ranges', () => {
243
+ it('rejects an unanchored range that overlaps a page', () => {
244
+ expectIssue('unanchored-overlap', TEXT, {
245
+ ...validMap(),
246
+ unanchored: [{ kind: 'x', charStart: 2, charEnd: 4 }],
247
+ });
248
+ });
249
+ it('rejects a zero-length unanchored range inside a page', () => {
250
+ expectIssue('unanchored-overlap', TEXT, {
251
+ ...validMap(),
252
+ unanchored: [{ kind: 'x', charStart: 2, charEnd: 2 }],
253
+ });
254
+ });
255
+ it('rejects unanchored ranges that overlap each other', () => {
256
+ const plainText = 'alpha\n\nextra';
257
+ expectIssue('unanchored-overlap', plainText, {
258
+ version: 1,
259
+ assembly: { pageSeparator: '\n\n', trailing: 'embedded' },
260
+ pages: [{ index: 1, charStart: 0, charEnd: 5 }],
261
+ unanchored: [
262
+ { kind: 'a', charStart: 7, charEnd: 10 },
263
+ { kind: 'b', charStart: 9, charEnd: 12 },
264
+ ],
265
+ });
266
+ });
267
+ it('rejects an embedded range before the last page', () => {
268
+ const plainText = 'a\n\nnote\n\nb';
269
+ expectIssue('unanchored-before-last-page', plainText, {
270
+ version: 1,
271
+ assembly: { pageSeparator: '\n\n', trailing: 'embedded' },
272
+ pages: [
273
+ { index: 1, charStart: 0, charEnd: 1 },
274
+ { index: 2, charStart: 9, charEnd: 10 },
275
+ ],
276
+ unanchored: [{ kind: 'note', charStart: 3, charEnd: 7 }],
277
+ });
278
+ });
279
+ it.each([
280
+ ['none', 'none'],
281
+ ['absent', undefined],
282
+ ])('rejects a range after the last page when trailing is %s', (_label, trailing) => {
283
+ const plainText = 'alpha\n\nextra';
284
+ expectIssue('unanchored-after-last-page', plainText, {
285
+ version: 1,
286
+ assembly: { pageSeparator: '\n\n', ...(trailing != null && { trailing }) },
287
+ pages: [{ index: 1, charStart: 0, charEnd: 5 }],
288
+ unanchored: [{ kind: 'x', charStart: 7, charEnd: 12 }],
289
+ });
290
+ });
291
+ });
292
+ describe('rule 9: blocks', () => {
293
+ it('rejects a block outside the page it names', () => {
294
+ expectIssue('block-outside-page', TEXT, {
295
+ ...validMap(),
296
+ blocks: [{ page: 1, charStart: 7, charEnd: 9, kind: 'x' }],
297
+ });
298
+ });
299
+ it('rejects a block naming a page that does not exist', () => {
300
+ expectIssue('block-unknown-page', TEXT, {
301
+ ...validMap(),
302
+ blocks: [{ page: 9, charStart: 0, charEnd: 2, kind: 'x' }],
303
+ });
304
+ });
305
+ it('rejects overlapping blocks on one page', () => {
306
+ expectIssue('block-overlap', TEXT, {
307
+ ...validMap(),
308
+ blocks: [
309
+ { page: 1, charStart: 0, charEnd: 3, kind: 'a' },
310
+ { page: 1, charStart: 2, charEnd: 5, kind: 'b' },
311
+ ],
312
+ });
313
+ });
314
+ });
315
+ describe('sparse lists', () => {
316
+ const filled = { index: 1, charStart: 0, charEnd: 5 };
317
+ const page2 = { index: 2, charStart: 7, charEnd: 11 };
318
+ const pagesWithHole = [filled];
319
+ pagesWithHole[2] = page2;
320
+ it.each([
321
+ ['pages of only holes', { pages: new Array(1) }],
322
+ ['pages with a hole between entries', { pages: pagesWithHole }],
323
+ ['a very long sparse pages array', { pages: new Array(2 ** 32 - 1) }],
324
+ ['unanchored holes', { pages: [filled], unanchored: new Array(1) }],
325
+ ['block holes', { pages: [filled], blocks: new Array(1) }],
326
+ ])('returns a structured failure for %s', (_label, change) => {
327
+ const map = { version: 1, assembly: { pageSeparator: '\n\n' }, ...change };
328
+ expect(codes('uncovered text', map, { pageCount: 1 })).toContain('invalid-shape');
329
+ });
330
+ });
331
+ it('records an issue whenever it stops before the coverage checks', () => {
332
+ const result = validatePageMap('text', { version: 1, assembly: {}, pages: [] });
333
+ expect(result.ok).toBe(false);
334
+ });
335
+ describe('member types', () => {
336
+ it('rejects a non-string label', () => {
337
+ const map = validMap();
338
+ map.pages[0] = { index: 1, label: 1, charStart: 0, charEnd: 5 };
339
+ expectIssue('invalid-shape', TEXT, map);
340
+ });
341
+ it('requires a string kind on an unanchored range', () => {
342
+ expectIssue('invalid-shape', 'alpha\n\nextra', {
343
+ version: 1,
344
+ assembly: { pageSeparator: '\n\n', trailing: 'embedded' },
345
+ pages: [{ index: 1, charStart: 0, charEnd: 5 }],
346
+ unanchored: [{ charStart: 7, charEnd: 12 }],
347
+ });
348
+ });
349
+ it('requires a string kind on a block', () => {
350
+ expectIssue('invalid-shape', TEXT, {
351
+ ...validMap(),
352
+ blocks: [{ page: 1, charStart: 0, charEnd: 2, kind: 3 }],
353
+ });
354
+ });
355
+ });
356
+ it('reports where an issue is', () => {
357
+ const map = validMap();
358
+ map.pages[1] = { index: 2, charStart: 99, charEnd: 100 };
359
+ const result = validatePageMap(TEXT, map);
360
+ expect(result.ok).toBe(false);
361
+ if (!result.ok) {
362
+ expect(result.issues[0]).toMatchObject({ code: 'invalid-offset', path: 'pages[1]' });
363
+ }
364
+ });
365
+ it('treats structural validity as the only claim: shifted pages with matching text pass', () => {
366
+ // Coverage is complete, yet nothing here proves "gamma" came from physical
367
+ // page 3. Only sentinel verification against a fixture can do that.
368
+ const shifted = 'gamma\n\nbeta\n\nalpha';
369
+ expect(validatePageMap(shifted, validMap(), { pageCount: 3 })).toEqual({ ok: true });
370
+ });
371
+ });
@@ -0,0 +1,62 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import type { ExtractionHintName, ExtractionHints, ExtractionProvider } from '../@types/extraction-types.js';
9
+ import type { JsonValue } from '../@types/field-data-types.js';
10
+ /** A deployment identifier is a non-empty string with no surrounding whitespace. */
11
+ export declare function assertDeploymentIdentifier(identifier: unknown): asserts identifier is string;
12
+ /**
13
+ * SHA-256 of the canonical JSON of everything that decides a producer's output,
14
+ * apart from the engine version. That version is recorded beside the hash as
15
+ * `providerVersion`, so an engine change is visible as its own reuse reason. A
16
+ * deployment identifier is what the host deployed rather than what the engine
17
+ * reports, so it is hashed.
18
+ *
19
+ * Rejects a malformed deployment identifier or a non-JSON value with a `TypeError`.
20
+ *
21
+ * @provisional
22
+ */
23
+ export declare function producerConfigHash(input: {
24
+ provider: string;
25
+ configuration?: Readonly<Record<string, JsonValue>>;
26
+ hints?: ExtractionHints;
27
+ /** Only these hints are kept; a provider that ignores a hint omits it. */
28
+ outputAffectingHints?: readonly ExtractionHintName[];
29
+ /** A host-chosen policy version. */
30
+ configurationVersion?: string;
31
+ /** A host-controlled engine deployment identifier, such as a pinned image tag. */
32
+ deploymentIdentifier?: string | null;
33
+ }): Promise<string>;
34
+ /**
35
+ * The producer of one run, built once so that its hash and the work decision come
36
+ * from the same values.
37
+ *
38
+ * @provisional
39
+ */
40
+ export interface ExtractionProducerIdentity {
41
+ provider: string;
42
+ /** The current engine version, from `resolveVersion`, or `null`. */
43
+ providerVersion: string | null;
44
+ /** Never `null`: a new extraction always records its real configuration. */
45
+ producerConfigHash: string;
46
+ /** Exactly the value hashed into `producerConfigHash`. Never in `providerVersion`. */
47
+ deploymentIdentifier: string | null;
48
+ }
49
+ /**
50
+ * The recommended way to obtain a producer. It keeps `deploymentIdentifier`
51
+ * consistent with `producerConfigHash` by construction.
52
+ *
53
+ * @provisional
54
+ */
55
+ export declare function buildExtractionProducer(input: {
56
+ provider: ExtractionProvider;
57
+ /** From `resolveVersion`, or `null`. Never copied from an artifact. */
58
+ providerVersion: string | null;
59
+ hints?: ExtractionHints;
60
+ configurationVersion?: string;
61
+ deploymentIdentifier?: string | null;
62
+ }): Promise<ExtractionProducerIdentity>;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import { sha256Hex } from '../storage/content-hash.js';
9
+ import { canonicalJson } from './internal.js';
10
+ /** An absent identifier and `null` mean the same: no identifier. */
11
+ function normaliseDeploymentIdentifier(identifier) {
12
+ if (identifier === undefined || identifier === null)
13
+ return null;
14
+ assertDeploymentIdentifier(identifier);
15
+ return identifier;
16
+ }
17
+ /** A deployment identifier is a non-empty string with no surrounding whitespace. */
18
+ export function assertDeploymentIdentifier(identifier) {
19
+ if (typeof identifier !== 'string' || identifier === '' || identifier !== identifier.trim()) {
20
+ throw new TypeError('deploymentIdentifier must be a non-empty string with no surrounding whitespace');
21
+ }
22
+ }
23
+ /**
24
+ * SHA-256 of the canonical JSON of everything that decides a producer's output,
25
+ * apart from the engine version. That version is recorded beside the hash as
26
+ * `providerVersion`, so an engine change is visible as its own reuse reason. A
27
+ * deployment identifier is what the host deployed rather than what the engine
28
+ * reports, so it is hashed.
29
+ *
30
+ * Rejects a malformed deployment identifier or a non-JSON value with a `TypeError`.
31
+ *
32
+ * @provisional
33
+ */
34
+ export async function producerConfigHash(input) {
35
+ const hints = {};
36
+ for (const name of input.outputAffectingHints ?? []) {
37
+ if (input.hints?.[name] !== undefined)
38
+ hints[name] = input.hints[name];
39
+ }
40
+ const canonical = canonicalJson({
41
+ provider: input.provider,
42
+ configuration: input.configuration,
43
+ hints: Object.keys(hints).length > 0 ? hints : undefined,
44
+ configurationVersion: input.configurationVersion,
45
+ deploymentIdentifier: normaliseDeploymentIdentifier(input.deploymentIdentifier) ?? undefined,
46
+ });
47
+ return sha256Hex(new TextEncoder().encode(canonical));
48
+ }
49
+ /**
50
+ * The recommended way to obtain a producer. It keeps `deploymentIdentifier`
51
+ * consistent with `producerConfigHash` by construction.
52
+ *
53
+ * @provisional
54
+ */
55
+ export async function buildExtractionProducer(input) {
56
+ const deploymentIdentifier = normaliseDeploymentIdentifier(input.deploymentIdentifier);
57
+ return {
58
+ provider: input.provider.name,
59
+ providerVersion: input.providerVersion,
60
+ deploymentIdentifier,
61
+ producerConfigHash: await producerConfigHash({
62
+ provider: input.provider.name,
63
+ ...(input.provider.configuration !== undefined && {
64
+ configuration: input.provider.configuration,
65
+ }),
66
+ ...(input.hints !== undefined && { hints: input.hints }),
67
+ ...(input.provider.outputAffectingHints !== undefined && {
68
+ outputAffectingHints: input.provider.outputAffectingHints,
69
+ }),
70
+ ...(input.configurationVersion !== undefined && {
71
+ configurationVersion: input.configurationVersion,
72
+ }),
73
+ deploymentIdentifier,
74
+ }),
75
+ };
76
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ export {};