@remit/config-format 0.0.7 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/config-format",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "schemaVersion": {
10
10
  "type": "number",
11
- "const": 1
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
  }
@@ -852,6 +820,6 @@
852
820
  "addressFlags"
853
821
  ],
854
822
  "additionalProperties": false,
855
- "$id": "https://schemas.remit.email/reader-config.v1.schema.json",
856
- "title": "reader configuration document, v1"
823
+ "$id": "https://schemas.remit.email/reader-config.v2.schema.json",
824
+ "title": "reader configuration document, v2"
857
825
  }
@@ -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
@@ -203,19 +203,6 @@ export const FilterClauseSchema = z.strictObject({
203
203
  value: z.string().min(1).max(256),
204
204
  });
205
205
 
206
- /**
207
- * A semantic anchor as text, never as numbers. The vector is a function of the
208
- * embedding model that produced it, so it means nothing on an instance running
209
- * a different one; the source text re-embeds against whatever model the
210
- * importing instance actually has.
211
- */
212
- export const FilterAnchorSchema = z.strictObject({
213
- sourceText: z.string().min(1).max(512),
214
- /** `<modelId>@<dimensions>` the vector was built with, for provenance. */
215
- embeddingId: NonEmpty,
216
- sourceMessageId: StoredId,
217
- });
218
-
219
206
  /** A folder named the only way it can be named across instances. */
220
207
  export const FolderRefSchema = z.strictObject({
221
208
  accountId: StoredId,
@@ -228,11 +215,10 @@ export const FilterSchema = z.strictObject({
228
215
  /** ISO 8601 with offset when the scope is Temporary, otherwise null. */
229
216
  expiresAt: IsoDateTime.nullable(),
230
217
  matchOperator: z.enum(FilterMatchOperator),
231
- literalClauses: z.array(FilterClauseSchema),
218
+ literalClauses: z.array(FilterClauseSchema).min(1),
232
219
  /** The label this filter applies, by name — ids are local to an instance. */
233
220
  actionLabelName: z.string().min(1).max(140).nullable(),
234
221
  actionFolder: FolderRefSchema.nullable(),
235
- anchor: FilterAnchorSchema.nullable(),
236
222
  });
237
223
 
238
224
  /**
@@ -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 parses", () => {
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 v1 document round-trips through JSON unchanged", () => {
19
- const source = readGoldenConfigDocument();
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.ok(document.filters[1]?.anchor?.sourceText);
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,
@@ -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 { CONFIG_SCHEMA_ARTIFACT_PATH, CONFIG_SCHEMA_ID } from "./version.js";
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((properties.schemaVersion as JsonObject).const, 1);
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",
@@ -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 needs no migrations, and the chain is empty rather than absent", () => {
14
- assert.deepEqual(configMigrations, []);
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 parsed = readConfigDocument(readGoldenConfigDocument(), migrations);
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
- * Empty at v1, and the golden fixture is the seed that keeps it honest: the
20
- * first migration added here has a committed v1 document to lift, so the chain
21
- * is exercised by the same test that proves v1 still parses.
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 source = { ...document(), schemaVersion: 2, somethingV2Added: true };
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, 2);
60
- assert.equal(error.supportedVersion, 1);
61
+ assert.equal(error.documentVersion, newer);
62
+ assert.equal(error.supportedVersion, CURRENT_SCHEMA_VERSION);
61
63
  return true;
62
64
  },
63
65
  );
@@ -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.v1.schema.json", import.meta.url),
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 = 1;
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.v1.schema.json";
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.v1.schema.json";
18
+ "build/config-schema/reader-config.v2.schema.json";