@remit/config-format 0.0.6 → 0.0.8
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/package.json +1 -1
- package/schema/{reader-config.v1.schema.json → reader-config.v2.schema.json} +35 -37
- package/src/document.test.ts +0 -32
- package/src/document.ts +2 -15
- package/src/golden.test.ts +8 -4
- package/src/json-schema.test.ts +9 -2
- package/src/migrations.test.ts +19 -3
- package/src/migrations.ts +22 -4
- package/src/read.test.ts +5 -3
- package/src/schema-artifact.ts +1 -1
- package/src/version.ts +3 -3
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"schemaVersion": {
|
|
10
10
|
"type": "number",
|
|
11
|
-
"const":
|
|
11
|
+
"const": 2
|
|
12
12
|
},
|
|
13
13
|
"generator": {
|
|
14
14
|
"description": "Which reader wrote this file, and when.",
|
|
@@ -429,6 +429,7 @@
|
|
|
429
429
|
]
|
|
430
430
|
},
|
|
431
431
|
"literalClauses": {
|
|
432
|
+
"minItems": 1,
|
|
432
433
|
"type": "array",
|
|
433
434
|
"items": {
|
|
434
435
|
"type": "object",
|
|
@@ -494,38 +495,6 @@
|
|
|
494
495
|
"type": "null"
|
|
495
496
|
}
|
|
496
497
|
]
|
|
497
|
-
},
|
|
498
|
-
"anchor": {
|
|
499
|
-
"anyOf": [
|
|
500
|
-
{
|
|
501
|
-
"type": "object",
|
|
502
|
-
"properties": {
|
|
503
|
-
"sourceText": {
|
|
504
|
-
"type": "string",
|
|
505
|
-
"minLength": 1,
|
|
506
|
-
"maxLength": 512
|
|
507
|
-
},
|
|
508
|
-
"embeddingId": {
|
|
509
|
-
"type": "string",
|
|
510
|
-
"minLength": 1
|
|
511
|
-
},
|
|
512
|
-
"sourceMessageId": {
|
|
513
|
-
"type": "string",
|
|
514
|
-
"minLength": 25,
|
|
515
|
-
"maxLength": 25
|
|
516
|
-
}
|
|
517
|
-
},
|
|
518
|
-
"required": [
|
|
519
|
-
"sourceText",
|
|
520
|
-
"embeddingId",
|
|
521
|
-
"sourceMessageId"
|
|
522
|
-
],
|
|
523
|
-
"additionalProperties": false
|
|
524
|
-
},
|
|
525
|
-
{
|
|
526
|
-
"type": "null"
|
|
527
|
-
}
|
|
528
|
-
]
|
|
529
498
|
}
|
|
530
499
|
},
|
|
531
500
|
"required": [
|
|
@@ -535,8 +504,7 @@
|
|
|
535
504
|
"matchOperator",
|
|
536
505
|
"literalClauses",
|
|
537
506
|
"actionLabelName",
|
|
538
|
-
"actionFolder"
|
|
539
|
-
"anchor"
|
|
507
|
+
"actionFolder"
|
|
540
508
|
],
|
|
541
509
|
"additionalProperties": false
|
|
542
510
|
}
|
|
@@ -616,6 +584,36 @@
|
|
|
616
584
|
],
|
|
617
585
|
"additionalProperties": false
|
|
618
586
|
},
|
|
587
|
+
"neverSpam": {
|
|
588
|
+
"type": "object",
|
|
589
|
+
"properties": {
|
|
590
|
+
"value": {
|
|
591
|
+
"type": "boolean"
|
|
592
|
+
},
|
|
593
|
+
"setAt": {
|
|
594
|
+
"description": "Epoch milliseconds the flag was set. 0 means it was set at an unknown time, on a row written before the field existed.",
|
|
595
|
+
"type": "integer",
|
|
596
|
+
"minimum": -9007199254740991,
|
|
597
|
+
"maximum": 9007199254740991
|
|
598
|
+
},
|
|
599
|
+
"setBy": {
|
|
600
|
+
"type": "string"
|
|
601
|
+
},
|
|
602
|
+
"expiresAt": {
|
|
603
|
+
"type": "integer",
|
|
604
|
+
"minimum": -9007199254740991,
|
|
605
|
+
"maximum": 9007199254740991
|
|
606
|
+
},
|
|
607
|
+
"reason": {
|
|
608
|
+
"type": "string"
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
"required": [
|
|
612
|
+
"value",
|
|
613
|
+
"setAt"
|
|
614
|
+
],
|
|
615
|
+
"additionalProperties": false
|
|
616
|
+
},
|
|
619
617
|
"muted": {
|
|
620
618
|
"type": "object",
|
|
621
619
|
"properties": {
|
|
@@ -822,6 +820,6 @@
|
|
|
822
820
|
"addressFlags"
|
|
823
821
|
],
|
|
824
822
|
"additionalProperties": false,
|
|
825
|
-
"$id": "https://schemas.remit.email/reader-config.
|
|
826
|
-
"title": "reader configuration document,
|
|
823
|
+
"$id": "https://schemas.remit.email/reader-config.v2.schema.json",
|
|
824
|
+
"title": "reader configuration document, v2"
|
|
827
825
|
}
|
package/src/document.test.ts
CHANGED
|
@@ -97,7 +97,6 @@ test("a filter names its label by name and its folder by account and path", () =
|
|
|
97
97
|
accountId: "6f4c2c309a2c4a7f9f9f1f2c3",
|
|
98
98
|
folderPath: "INBOX.Facturen",
|
|
99
99
|
},
|
|
100
|
-
anchor: null,
|
|
101
100
|
};
|
|
102
101
|
|
|
103
102
|
assert.equal(FilterSchema.safeParse(filter).success, true);
|
|
@@ -114,37 +113,6 @@ test("a filter names its label by name and its folder by account and path", () =
|
|
|
114
113
|
);
|
|
115
114
|
});
|
|
116
115
|
|
|
117
|
-
test("a filter anchor carries its source text and no vector", () => {
|
|
118
|
-
const anchored = {
|
|
119
|
-
name: "like this",
|
|
120
|
-
scope: "Standing",
|
|
121
|
-
expiresAt: null,
|
|
122
|
-
matchOperator: "And",
|
|
123
|
-
literalClauses: [],
|
|
124
|
-
actionLabelName: null,
|
|
125
|
-
actionFolder: null,
|
|
126
|
-
anchor: {
|
|
127
|
-
sourceText: "the release note we file every month",
|
|
128
|
-
embeddingId: "amazon.titan-embed-text-v2:0@1024",
|
|
129
|
-
sourceMessageId: "2c9e4b117d5a4f609a315b8c0",
|
|
130
|
-
},
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
assert.equal(FilterSchema.safeParse(anchored).success, true);
|
|
134
|
-
assert.equal(
|
|
135
|
-
FilterSchema.safeParse({
|
|
136
|
-
...anchored,
|
|
137
|
-
anchor: {
|
|
138
|
-
sourceText: "x",
|
|
139
|
-
embeddingId: "m@1",
|
|
140
|
-
sourceMessageId: "2c9e4b117d5a4f609a315b8c0",
|
|
141
|
-
anchorEmbedding: [0.1, 0.2],
|
|
142
|
-
},
|
|
143
|
-
}).success,
|
|
144
|
-
false,
|
|
145
|
-
);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
116
|
test("an address flag keeps its whole payload", () => {
|
|
149
117
|
const entry = {
|
|
150
118
|
normalizedEmail: "post@bank.example",
|
package/src/document.ts
CHANGED
|
@@ -98,6 +98,7 @@ const CategoryFlagSchema = z.strictObject({
|
|
|
98
98
|
export const AddressFlagsSchema = z.strictObject({
|
|
99
99
|
trusted: FlagMetadataSchema.optional(),
|
|
100
100
|
blocked: FlagMetadataSchema.optional(),
|
|
101
|
+
neverSpam: FlagMetadataSchema.optional(),
|
|
101
102
|
muted: FlagMetadataSchema.optional(),
|
|
102
103
|
vip: FlagMetadataSchema.optional(),
|
|
103
104
|
category: CategoryFlagSchema.optional(),
|
|
@@ -202,19 +203,6 @@ export const FilterClauseSchema = z.strictObject({
|
|
|
202
203
|
value: z.string().min(1).max(256),
|
|
203
204
|
});
|
|
204
205
|
|
|
205
|
-
/**
|
|
206
|
-
* A semantic anchor as text, never as numbers. The vector is a function of the
|
|
207
|
-
* embedding model that produced it, so it means nothing on an instance running
|
|
208
|
-
* a different one; the source text re-embeds against whatever model the
|
|
209
|
-
* importing instance actually has.
|
|
210
|
-
*/
|
|
211
|
-
export const FilterAnchorSchema = z.strictObject({
|
|
212
|
-
sourceText: z.string().min(1).max(512),
|
|
213
|
-
/** `<modelId>@<dimensions>` the vector was built with, for provenance. */
|
|
214
|
-
embeddingId: NonEmpty,
|
|
215
|
-
sourceMessageId: StoredId,
|
|
216
|
-
});
|
|
217
|
-
|
|
218
206
|
/** A folder named the only way it can be named across instances. */
|
|
219
207
|
export const FolderRefSchema = z.strictObject({
|
|
220
208
|
accountId: StoredId,
|
|
@@ -227,11 +215,10 @@ export const FilterSchema = z.strictObject({
|
|
|
227
215
|
/** ISO 8601 with offset when the scope is Temporary, otherwise null. */
|
|
228
216
|
expiresAt: IsoDateTime.nullable(),
|
|
229
217
|
matchOperator: z.enum(FilterMatchOperator),
|
|
230
|
-
literalClauses: z.array(FilterClauseSchema),
|
|
218
|
+
literalClauses: z.array(FilterClauseSchema).min(1),
|
|
231
219
|
/** The label this filter applies, by name — ids are local to an instance. */
|
|
232
220
|
actionLabelName: z.string().min(1).max(140).nullable(),
|
|
233
221
|
actionFolder: FolderRefSchema.nullable(),
|
|
234
|
-
anchor: FilterAnchorSchema.nullable(),
|
|
235
222
|
});
|
|
236
223
|
|
|
237
224
|
/**
|
package/src/golden.test.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { test } from "node:test";
|
|
3
3
|
import { readGoldenConfigDocument } from "./fixtures.js";
|
|
4
|
+
import { liftToCurrentVersion } from "./migrations.js";
|
|
4
5
|
import { readConfigDocument } from "./read.js";
|
|
5
6
|
import { CURRENT_SCHEMA_VERSION } from "./version.js";
|
|
6
7
|
|
|
7
|
-
test("the golden v1 document
|
|
8
|
+
test("the golden v1 document reads at the current version", () => {
|
|
8
9
|
const document = readConfigDocument(readGoldenConfigDocument());
|
|
9
10
|
|
|
10
11
|
assert.equal(document.schemaVersion, CURRENT_SCHEMA_VERSION);
|
|
@@ -15,8 +16,11 @@ test("the golden v1 document parses", () => {
|
|
|
15
16
|
);
|
|
16
17
|
});
|
|
17
18
|
|
|
18
|
-
test("the golden
|
|
19
|
-
const source =
|
|
19
|
+
test("the lifted golden document round-trips through JSON unchanged", () => {
|
|
20
|
+
const source = liftToCurrentVersion(
|
|
21
|
+
readGoldenConfigDocument() as Record<string, unknown>,
|
|
22
|
+
1,
|
|
23
|
+
);
|
|
20
24
|
|
|
21
25
|
const parsed = readConfigDocument(source);
|
|
22
26
|
|
|
@@ -39,7 +43,7 @@ test("the golden v1 document carries the parts a move has to preserve", () => {
|
|
|
39
43
|
);
|
|
40
44
|
assert.equal(document.filters[0]?.actionFolder?.folderPath, "INBOX.Facturen");
|
|
41
45
|
assert.equal(document.filters[0]?.actionLabelName, "Facturen");
|
|
42
|
-
assert.
|
|
46
|
+
assert.equal(document.filters.length, 1, "the anchor-only filter is dropped");
|
|
43
47
|
assert.equal(document.addressFlags[0]?.flags.vip?.reason, "payment alerts");
|
|
44
48
|
assert.equal(
|
|
45
49
|
document.addressFlags[1]?.flags.unsubscribed?.expiresAt,
|
package/src/json-schema.test.ts
CHANGED
|
@@ -6,7 +6,11 @@ import {
|
|
|
6
6
|
serializeConfigJsonSchema,
|
|
7
7
|
} from "./json-schema.js";
|
|
8
8
|
import { artifactSchemaPath, committedSchemaPath } from "./schema-artifact.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
CONFIG_SCHEMA_ARTIFACT_PATH,
|
|
11
|
+
CONFIG_SCHEMA_ID,
|
|
12
|
+
CURRENT_SCHEMA_VERSION,
|
|
13
|
+
} from "./version.js";
|
|
10
14
|
|
|
11
15
|
type JsonObject = Record<string, unknown>;
|
|
12
16
|
|
|
@@ -24,7 +28,10 @@ test("the emitted schema pins the envelope and closes the document", () => {
|
|
|
24
28
|
const properties = emitted.properties as JsonObject;
|
|
25
29
|
|
|
26
30
|
assert.deepEqual((properties.kind as JsonObject).const, "reader.config");
|
|
27
|
-
assert.deepEqual(
|
|
31
|
+
assert.deepEqual(
|
|
32
|
+
(properties.schemaVersion as JsonObject).const,
|
|
33
|
+
CURRENT_SCHEMA_VERSION,
|
|
34
|
+
);
|
|
28
35
|
assert.equal(emitted.additionalProperties, false);
|
|
29
36
|
assert.deepEqual(emitted.required, [
|
|
30
37
|
"kind",
|
package/src/migrations.test.ts
CHANGED
|
@@ -10,8 +10,20 @@ import {
|
|
|
10
10
|
import { readConfigDocument } from "./read.js";
|
|
11
11
|
import { CURRENT_SCHEMA_VERSION } from "./version.js";
|
|
12
12
|
|
|
13
|
-
test("v1
|
|
14
|
-
|
|
13
|
+
test("v1 lifts to v2 by dropping each anchor and every anchor-only filter", () => {
|
|
14
|
+
const source = readGoldenConfigDocument() as Record<string, unknown>;
|
|
15
|
+
|
|
16
|
+
const lifted = liftToCurrentVersion(source, 1, configMigrations, 2);
|
|
17
|
+
|
|
18
|
+
assert.equal(lifted.schemaVersion, 2);
|
|
19
|
+
const filters = lifted.filters as Record<string, unknown>[];
|
|
20
|
+
assert.deepEqual(
|
|
21
|
+
filters.map((filter) => filter.name),
|
|
22
|
+
["Invoices to Facturen"],
|
|
23
|
+
);
|
|
24
|
+
for (const filter of filters) {
|
|
25
|
+
assert.equal("anchor" in filter, false);
|
|
26
|
+
}
|
|
15
27
|
});
|
|
16
28
|
|
|
17
29
|
test("a document already at the current version passes through untouched", () => {
|
|
@@ -72,7 +84,11 @@ test("a document already at the current version never enters the chain", () => {
|
|
|
72
84
|
},
|
|
73
85
|
];
|
|
74
86
|
|
|
75
|
-
const
|
|
87
|
+
const current = liftToCurrentVersion(
|
|
88
|
+
readGoldenConfigDocument() as Record<string, unknown>,
|
|
89
|
+
1,
|
|
90
|
+
);
|
|
91
|
+
const parsed = readConfigDocument(current, migrations);
|
|
76
92
|
|
|
77
93
|
assert.equal(ran, 0);
|
|
78
94
|
assert.equal(parsed.schemaVersion, CURRENT_SCHEMA_VERSION);
|
package/src/migrations.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
1
2
|
import { ConfigVersionError } from "./errors.js";
|
|
2
3
|
import { CURRENT_SCHEMA_VERSION } from "./version.js";
|
|
3
4
|
|
|
@@ -15,12 +16,29 @@ export type ConfigMigration = {
|
|
|
15
16
|
migrate(document: ConfigDocumentShape): ConfigDocumentShape;
|
|
16
17
|
};
|
|
17
18
|
|
|
19
|
+
const V1FiltersSchema = z.array(
|
|
20
|
+
z.looseObject({ literalClauses: z.array(z.unknown()), anchor: z.unknown() }),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
/** An anchor-only filter has no clauses left to match on, so it goes with its anchor. */
|
|
24
|
+
const dropFilterAnchors: ConfigMigration = {
|
|
25
|
+
from: 1,
|
|
26
|
+
to: 2,
|
|
27
|
+
migrate: (document) => ({
|
|
28
|
+
...document,
|
|
29
|
+
schemaVersion: 2,
|
|
30
|
+
filters: V1FiltersSchema.parse(document.filters)
|
|
31
|
+
.filter((filter) => filter.literalClauses.length > 0)
|
|
32
|
+
.map(({ anchor: _anchor, ...filter }) => filter),
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
|
|
18
36
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
37
|
+
* The golden v1 fixture is the seed that keeps the chain honest: every step
|
|
38
|
+
* has a committed v1 document to lift, so the chain is exercised by the same
|
|
39
|
+
* test that proves v1 still reads.
|
|
22
40
|
*/
|
|
23
|
-
export const configMigrations: readonly ConfigMigration[] = [];
|
|
41
|
+
export const configMigrations: readonly ConfigMigration[] = [dropFilterAnchors];
|
|
24
42
|
|
|
25
43
|
/**
|
|
26
44
|
* Apply the chain from `fromVersion` up to the current version. A gap in the
|
package/src/read.test.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "./errors.js";
|
|
12
12
|
import { readGoldenConfigDocument } from "./fixtures.js";
|
|
13
13
|
import { readConfigDocument } from "./read.js";
|
|
14
|
+
import { CURRENT_SCHEMA_VERSION } from "./version.js";
|
|
14
15
|
|
|
15
16
|
type Mutable = Record<string, unknown>;
|
|
16
17
|
type MutableAccount = Record<string, unknown>;
|
|
@@ -50,14 +51,15 @@ test("a document with no kind at all is refused as the wrong kind", () => {
|
|
|
50
51
|
});
|
|
51
52
|
|
|
52
53
|
test("a newer document is refused by version, before its shape is judged", () => {
|
|
53
|
-
const
|
|
54
|
+
const newer = CURRENT_SCHEMA_VERSION + 1;
|
|
55
|
+
const source = { ...document(), schemaVersion: newer, somethingAdded: true };
|
|
54
56
|
|
|
55
57
|
assert.throws(
|
|
56
58
|
() => readConfigDocument(source),
|
|
57
59
|
(error: unknown) => {
|
|
58
60
|
assert.ok(error instanceof ConfigVersionError);
|
|
59
|
-
assert.equal(error.documentVersion,
|
|
60
|
-
assert.equal(error.supportedVersion,
|
|
61
|
+
assert.equal(error.documentVersion, newer);
|
|
62
|
+
assert.equal(error.supportedVersion, CURRENT_SCHEMA_VERSION);
|
|
61
63
|
return true;
|
|
62
64
|
},
|
|
63
65
|
);
|
package/src/schema-artifact.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { CONFIG_SCHEMA_ARTIFACT_PATH } from "./version.js";
|
|
|
9
9
|
*/
|
|
10
10
|
export const committedSchemaPath = (): string =>
|
|
11
11
|
fileURLToPath(
|
|
12
|
-
new URL("../schema/reader-config.
|
|
12
|
+
new URL("../schema/reader-config.v2.schema.json", import.meta.url),
|
|
13
13
|
);
|
|
14
14
|
|
|
15
15
|
/** The generated artifact, at its path under the repository root. */
|
package/src/version.ts
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
export const CONFIG_KIND = "reader.config";
|
|
8
8
|
|
|
9
9
|
/** The document version this package writes and can read up to. */
|
|
10
|
-
export const CURRENT_SCHEMA_VERSION =
|
|
10
|
+
export const CURRENT_SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
/** `$id` of the emitted JSON Schema artifact. */
|
|
13
13
|
export const CONFIG_SCHEMA_ID =
|
|
14
|
-
"https://schemas.remit.email/reader-config.
|
|
14
|
+
"https://schemas.remit.email/reader-config.v2.schema.json";
|
|
15
15
|
|
|
16
16
|
/** Path of the emitted JSON Schema artifact, relative to the repository root. */
|
|
17
17
|
export const CONFIG_SCHEMA_ARTIFACT_PATH =
|
|
18
|
-
"build/config-schema/reader-config.
|
|
18
|
+
"build/config-schema/reader-config.v2.schema.json";
|