@remit/config-transfer 0.0.10 → 0.0.12

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-transfer",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
package/src/binder.ts CHANGED
@@ -1,17 +1,24 @@
1
1
  import { readConfigDocument } from "@remit/config-format";
2
- import type {
3
- ConfigImportItem,
4
- ConfigImportUnresolvedRefItem,
5
- IAccountSettingRepository,
6
- IConfigImportRepository,
7
- IFilterRepository,
8
- IMailboxRepository,
2
+ import {
3
+ type ConfigImportItem,
4
+ type ConfigImportUnresolvedRefItem,
5
+ type CreateMailboxResult,
6
+ FILTER_NO_ACTION,
7
+ type FilterItem,
8
+ type IAccountSettingRepository,
9
+ type IConfigImportRepository,
10
+ type IFilterRepository,
11
+ type IMailboxRepository,
12
+ type MailboxItem,
9
13
  } from "@remit/data-ports";
10
14
  import { composeSettingName } from "@remit/data-ports/account-settings";
11
15
  import {
12
16
  AccountSettingName,
13
17
  ConfigImportRefKind,
14
18
  ConfigImportState,
19
+ FilterDisabledReason,
20
+ FilterState,
21
+ MailboxSyncStatus,
15
22
  } from "@remit/domain-enums";
16
23
  import type { AppointFolderRole } from "./import-repositories.js";
17
24
 
@@ -19,12 +26,18 @@ export interface ConfigBinderRepositories {
19
26
  configImport: Pick<IConfigImportRepository, "listByAccountConfig" | "update">;
20
27
  accountSetting: Pick<IAccountSettingRepository, "upsert">;
21
28
  filter: Pick<IFilterRepository, "listByAccountConfig" | "update">;
22
- mailbox: Pick<IMailboxRepository, "listAllByAccount">;
29
+ mailbox: Pick<IMailboxRepository, "listAllByAccount" | "resolveAccountId">;
23
30
  }
24
31
 
32
+ export type CreateImportedFolder = (
33
+ accountId: string,
34
+ folderPath: string,
35
+ ) => Promise<CreateMailboxResult>;
36
+
25
37
  export interface ConfigBinderDeps {
26
38
  repositories: ConfigBinderRepositories;
27
39
  appointFolderRole: AppointFolderRole;
40
+ createFolder: CreateImportedFolder;
28
41
  now?: () => number;
29
42
  }
30
43
 
@@ -35,27 +48,34 @@ export interface ConfigBinderDeps {
35
48
  export interface BindResult {
36
49
  bound: number;
37
50
  dropped: number;
51
+ created: number;
52
+ disabled: number;
38
53
  stillPending: number;
39
54
  }
40
55
 
41
56
  type RefState =
42
57
  | { kind: "TargetGone" }
43
- | { kind: "Waiting" }
44
- | { kind: "Ready"; mailboxId: string };
58
+ | { kind: "Waiting"; mailboxId: string }
59
+ | { kind: "Ready"; mailboxId: string }
60
+ | { kind: "Absent" }
61
+ | { kind: "Refused"; reason: FilterItem["disabledReason"] };
45
62
 
46
63
  /**
47
64
  * Bind the folder references an import could not resolve, now that discovery
48
65
  * has produced this account's mailboxes.
49
66
  *
50
67
  * Every reference names a folder by IMAP path, because the ids are new on every
51
- * discovery. This runs where discovery ends, resolves what it can, writes the
52
- * row each reference belongs to, and drops it. An import whose last reference
53
- * is gone is Complete; the rest stay, and surface on `GET /config` until the
54
- * folder they name shows up or the person removes the expectation.
68
+ * discovery. A path discovery did not produce is a folder the account lacks,
69
+ * so the binder creates it through the folder-create mutation and waits for
70
+ * the server to confirm it (imap-mutations R1, R2: wait), binding to that row
71
+ * by id because the server may have normalized its path. A create the server
72
+ * refused, or a created folder that vanished, drops the reference and turns
73
+ * its filter off with the reason.
55
74
  *
56
75
  * Idempotent and replayable: every write is an upsert or an update onto a row
57
- * the import already created, so a second discovery finds nothing to do rather
58
- * than doing it again differently.
76
+ * the import already created, and a create is recorded on the reference before
77
+ * the next run, so a second discovery finds nothing to do rather than doing it
78
+ * again differently.
59
79
  */
60
80
  export const bindImportedFolders = async (
61
81
  deps: ConfigBinderDeps,
@@ -64,48 +84,108 @@ export const bindImportedFolders = async (
64
84
  ): Promise<BindResult> => {
65
85
  const { repositories } = deps;
66
86
  const now = deps.now ?? Date.now;
87
+ const result: BindResult = {
88
+ bound: 0,
89
+ dropped: 0,
90
+ created: 0,
91
+ disabled: 0,
92
+ stillPending: 0,
93
+ };
67
94
 
68
95
  const imports = (
69
96
  await repositories.configImport.listByAccountConfig(accountConfigId)
70
97
  ).filter((row) => row.state === ConfigImportState.Pending);
71
- if (imports.length === 0) return { bound: 0, dropped: 0, stillPending: 0 };
98
+ if (imports.length === 0) return result;
72
99
 
100
+ const mailboxes = await repositories.mailbox.listAllByAccount(accountId);
73
101
  const byPath = new Map(
74
- (await repositories.mailbox.listAllByAccount(accountId)).map(
75
- (mailbox) => [mailbox.fullPath, mailbox.mailboxId] as const,
76
- ),
102
+ mailboxes.map((mailbox) => [mailbox.fullPath, mailbox] as const),
103
+ );
104
+ const byId = new Map(
105
+ mailboxes.map((mailbox) => [mailbox.mailboxId, mailbox] as const),
77
106
  );
78
107
 
79
- const filterIds = new Set(
108
+ const filtersById = new Map(
80
109
  (await repositories.filter.listByAccountConfig(accountConfigId)).map(
81
- (filter) => filter.filterId,
110
+ (filter) => [filter.filterId, filter] as const,
82
111
  ),
83
112
  );
84
113
 
85
- let bound = 0;
86
- let dropped = 0;
87
- let stillPending = 0;
114
+ const createdByPath = new Map<string, string>();
115
+ const createFolder = async (folderPath: string): Promise<string> => {
116
+ const held = createdByPath.get(folderPath);
117
+ if (held !== undefined) return held;
118
+ const created = await deps.createFolder(accountId, folderPath);
119
+ if (created.outcome === "PathTaken") return FILTER_NO_ACTION;
120
+ createdByPath.set(folderPath, created.mailbox.mailboxId);
121
+ result.created++;
122
+ return created.mailbox.mailboxId;
123
+ };
88
124
 
89
125
  for (const row of imports) {
90
126
  const document = readConfigDocument(row.document);
91
127
  const remaining: ConfigImportUnresolvedRefItem[] = [];
128
+ let changed = false;
92
129
 
93
130
  for (const ref of row.unresolvedRefs) {
94
- const state = refStateOf(ref, accountId, byPath, filterIds);
131
+ const state = refStateOf(ref, accountId, byPath, byId, filtersById);
132
+ if (
133
+ (state.kind === "Ready" || state.kind === "Refused") &&
134
+ !movesIntoNothing(ref, filtersById)
135
+ ) {
136
+ await settlePickedFolder(
137
+ repositories,
138
+ accountConfigId,
139
+ filtersById.get(ref.target),
140
+ );
141
+ result.dropped++;
142
+ changed = true;
143
+ continue;
144
+ }
95
145
  if (state.kind === "TargetGone") {
96
- dropped++;
146
+ result.dropped++;
147
+ changed = true;
97
148
  continue;
98
149
  }
99
150
  if (state.kind === "Waiting") {
100
- remaining.push(ref);
151
+ if (state.mailboxId !== ref.mailboxId) changed = true;
152
+ remaining.push({ ...ref, mailboxId: state.mailboxId });
101
153
  continue;
102
154
  }
103
- await bindRef(deps, accountConfigId, ref, state.mailboxId, document);
104
- bound++;
155
+ if (state.kind === "Absent") {
156
+ remaining.push({
157
+ ...ref,
158
+ mailboxId: await createFolder(ref.folderPath),
159
+ });
160
+ changed = true;
161
+ continue;
162
+ }
163
+ if (state.kind === "Refused") {
164
+ if (ref.kind === ConfigImportRefKind.FilterAction) {
165
+ await repositories.filter.update(accountConfigId, ref.target, {
166
+ state: FilterState.Disabled,
167
+ disabledReason: state.reason,
168
+ });
169
+ result.disabled++;
170
+ }
171
+ result.dropped++;
172
+ changed = true;
173
+ continue;
174
+ }
175
+ await bindRef(
176
+ deps,
177
+ accountConfigId,
178
+ ref,
179
+ state.mailboxId,
180
+ document,
181
+ filtersById,
182
+ );
183
+ result.bound++;
184
+ changed = true;
105
185
  }
106
186
 
107
- stillPending += remaining.length;
108
- if (remaining.length === row.unresolvedRefs.length) continue;
187
+ result.stillPending += remaining.length;
188
+ if (!changed) continue;
109
189
 
110
190
  await repositories.configImport.update(row.importId, {
111
191
  unresolvedRefs: remaining,
@@ -115,27 +195,116 @@ export const bindImportedFolders = async (
115
195
  });
116
196
  }
117
197
 
118
- return { bound, dropped, stillPending };
198
+ return result;
199
+ };
200
+
201
+ export const disableFiltersMissingFolders = async (
202
+ repositories: {
203
+ filter: Pick<IFilterRepository, "listByAccountConfig" | "update">;
204
+ mailbox: Pick<IMailboxRepository, "resolveAccountId">;
205
+ },
206
+ accountConfigId: string,
207
+ ): Promise<number> => {
208
+ const filters =
209
+ await repositories.filter.listByAccountConfig(accountConfigId);
210
+ let disabled = 0;
211
+ for (const filter of filters) {
212
+ if (filter.state !== FilterState.Active) continue;
213
+ if (filter.actionMailboxId === FILTER_NO_ACTION) continue;
214
+ const owner = await repositories.mailbox.resolveAccountId(
215
+ filter.actionMailboxId,
216
+ );
217
+ if (owner !== null) continue;
218
+ await repositories.filter.update(accountConfigId, filter.filterId, {
219
+ state: FilterState.Disabled,
220
+ disabledReason: FilterDisabledReason.FolderMissing,
221
+ });
222
+ disabled++;
223
+ }
224
+ return disabled;
119
225
  };
120
226
 
121
227
  type BoundDocument = ReturnType<typeof readConfigDocument>;
122
228
 
229
+ const FOLDERLESS_REASONS: ReadonlySet<FilterItem["disabledReason"]> = new Set([
230
+ FilterDisabledReason.AwaitingFolder,
231
+ FilterDisabledReason.FolderCreateFailed,
232
+ ]);
233
+
234
+ const settlePickedFolder = async (
235
+ repositories: ConfigBinderRepositories,
236
+ accountConfigId: string,
237
+ filter: FilterItem | undefined,
238
+ ): Promise<void> => {
239
+ if (!filter || filter.state !== FilterState.Disabled) return;
240
+ if (!FOLDERLESS_REASONS.has(filter.disabledReason)) return;
241
+ await repositories.filter.update(accountConfigId, filter.filterId, {
242
+ disabledReason: FilterDisabledReason.UserDisabled,
243
+ });
244
+ };
245
+
246
+ const movesIntoNothing = (
247
+ ref: ConfigImportUnresolvedRefItem,
248
+ filtersById: ReadonlyMap<string, FilterItem>,
249
+ ): boolean =>
250
+ ref.kind !== ConfigImportRefKind.FilterAction ||
251
+ filtersById.get(ref.target)?.actionMailboxId === FILTER_NO_ACTION;
252
+
253
+ const findByPath = (
254
+ folderPath: string,
255
+ byPath: ReadonlyMap<string, MailboxItem>,
256
+ ): MailboxItem | undefined => {
257
+ const exact = byPath.get(folderPath);
258
+ if (exact) return exact;
259
+ const prefix = byPath.get("INBOX")?.namespacePrefix ?? "";
260
+ if (prefix === "" || folderPath.startsWith(prefix)) return undefined;
261
+ return byPath.get(`${prefix}${folderPath}`);
262
+ };
263
+
123
264
  const refStateOf = (
124
265
  ref: ConfigImportUnresolvedRefItem,
125
266
  accountId: string,
126
- byPath: ReadonlyMap<string, string>,
127
- filterIds: ReadonlySet<string>,
267
+ byPath: ReadonlyMap<string, MailboxItem>,
268
+ byId: ReadonlyMap<string, MailboxItem>,
269
+ filtersById: ReadonlyMap<string, FilterItem>,
128
270
  ): RefState => {
129
271
  if (
130
272
  ref.kind === ConfigImportRefKind.FilterAction &&
131
- !filterIds.has(ref.target)
273
+ !filtersById.has(ref.target)
132
274
  ) {
133
275
  return { kind: "TargetGone" };
134
276
  }
135
- const mailboxId =
136
- ref.accountId === accountId ? byPath.get(ref.folderPath) : undefined;
137
- if (mailboxId === undefined) return { kind: "Waiting" };
138
- return { kind: "Ready", mailboxId };
277
+ if (ref.accountId !== accountId) {
278
+ return { kind: "Waiting", mailboxId: ref.mailboxId };
279
+ }
280
+ const created = byId.get(ref.mailboxId);
281
+ if (created) {
282
+ if (created.syncStatus === MailboxSyncStatus.synced) {
283
+ return { kind: "Ready", mailboxId: created.mailboxId };
284
+ }
285
+ if (
286
+ created.syncStatus === MailboxSyncStatus.failed &&
287
+ created.pendingPath === undefined
288
+ ) {
289
+ return {
290
+ kind: "Refused",
291
+ reason: FilterDisabledReason.FolderCreateFailed,
292
+ };
293
+ }
294
+ return { kind: "Waiting", mailboxId: created.mailboxId };
295
+ }
296
+ const found = findByPath(ref.folderPath, byPath);
297
+ if (found?.syncStatus === MailboxSyncStatus.synced) {
298
+ return { kind: "Ready", mailboxId: found.mailboxId };
299
+ }
300
+ if (found) return { kind: "Waiting", mailboxId: FILTER_NO_ACTION };
301
+ if (ref.mailboxId !== FILTER_NO_ACTION) {
302
+ return {
303
+ kind: "Refused",
304
+ reason: FilterDisabledReason.FolderCreateFailed,
305
+ };
306
+ }
307
+ return { kind: "Absent" };
139
308
  };
140
309
 
141
310
  const bindRef = async (
@@ -144,11 +313,21 @@ const bindRef = async (
144
313
  ref: ConfigImportUnresolvedRefItem,
145
314
  mailboxId: string,
146
315
  document: BoundDocument,
316
+ filtersById: ReadonlyMap<string, FilterItem>,
147
317
  ): Promise<void> => {
148
318
  const { repositories } = deps;
149
319
  if (ref.kind === ConfigImportRefKind.FilterAction) {
320
+ const awaiting =
321
+ filtersById.get(ref.target)?.disabledReason ===
322
+ FilterDisabledReason.AwaitingFolder;
150
323
  await repositories.filter.update(accountConfigId, ref.target, {
151
324
  actionMailboxId: mailboxId,
325
+ ...(awaiting
326
+ ? {
327
+ state: FilterState.Active,
328
+ disabledReason: FilterDisabledReason.None,
329
+ }
330
+ : {}),
152
331
  });
153
332
  return;
154
333
  }
@@ -16,7 +16,6 @@ import {
16
16
  makeAccount,
17
17
  makeAccountConfig,
18
18
  makeAddress,
19
- makeAnchor,
20
19
  makeFilter,
21
20
  makeJunkOnlyFlaggedAddress,
22
21
  makeLabel,
@@ -158,14 +157,12 @@ const fullFixture = (): ConfigFixture => ({
158
157
  actionMailboxId: INVOICES_ID,
159
158
  }),
160
159
  makeFilter({
161
- filterId: "flt-semantic",
162
- name: "Anything like this release note",
160
+ filterId: "flt-release",
161
+ name: "Release notes",
163
162
  scope: "Temporary",
164
163
  expiresAt: "2026-12-31T23:59:59+01:00",
165
- hasAnchor: true,
166
164
  }),
167
165
  ],
168
- anchors: [makeAnchor({ filterId: "flt-semantic" })],
169
166
  addresses: [
170
167
  makeAddress({
171
168
  addressId: "adr-bank",
@@ -195,12 +192,12 @@ const fullFixture = (): ConfigFixture => ({
195
192
  const exportFixture = (fixture: ConfigFixture) =>
196
193
  readConfigForExport(asRepositories(fixture), ACCOUNT_CONFIG_ID, IDENTITY);
197
194
 
198
- test("a configuration exports as a document the v1 schema accepts", async () => {
195
+ test("a configuration exports as a document the current schema accepts", async () => {
199
196
  const document = await exportFixture(fullFixture());
200
197
 
201
198
  assert.equal(ReaderConfigDocumentSchema.safeParse(document).success, true);
202
199
  assert.equal(document.kind, "reader.config");
203
- assert.equal(document.schemaVersion, 1);
200
+ assert.equal(document.schemaVersion, 2);
204
201
  assert.deepEqual(document.provenance, {
205
202
  accountConfigId: ACCOUNT_CONFIG_ID,
206
203
  instance: "reader.ischen.nl",
@@ -379,7 +376,7 @@ test("an override on a folder that no longer exists is dropped", async () => {
379
376
 
380
377
  test("a filter names its label and its folder, never their ids", async () => {
381
378
  const document = await exportFixture(fullFixture());
382
- const [invoices, semantic] = document.filters;
379
+ const [invoices, release] = document.filters;
383
380
 
384
381
  assert.equal(invoices?.actionLabelName, "Facturen");
385
382
  assert.equal(invoices?.matchOperator, "Or");
@@ -388,11 +385,10 @@ test("a filter names its label and its folder, never their ids", async () => {
388
385
  { field: "FromDomain", value: "moneybird.com" },
389
386
  ]);
390
387
  assert.equal(invoices?.expiresAt, null);
391
- assert.equal(invoices?.anchor, null);
392
388
 
393
- assert.equal(semantic?.actionLabelName, null);
394
- assert.equal(semantic?.actionFolder, null);
395
- assert.equal(semantic?.expiresAt, "2026-12-31T23:59:59+01:00");
389
+ assert.equal(release?.actionLabelName, null);
390
+ assert.equal(release?.actionFolder, null);
391
+ assert.equal(release?.expiresAt, "2026-12-31T23:59:59+01:00");
396
392
  });
397
393
 
398
394
  test("a filter action naming something that is gone exports as no action", async () => {
@@ -408,31 +404,6 @@ test("a filter action naming something that is gone exports as no action", async
408
404
  assert.equal(document.filters[0]?.actionFolder, null);
409
405
  });
410
406
 
411
- test("an anchor exports its text and its provenance, never its vector", async () => {
412
- const document = await exportFixture(fullFixture());
413
-
414
- assert.deepEqual(document.filters[1]?.anchor, {
415
- sourceText: "the release note this filter was drawn from",
416
- embeddingId: "amazon.titan-embed-text-v2:0@1024",
417
- sourceMessageId: "2c9e4b117d5a4f609a315b8c0",
418
- });
419
- assert.equal(JSON.stringify(document).includes("anchorEmbedding"), false);
420
- assert.equal(JSON.stringify(document).includes("0.22"), false);
421
- });
422
-
423
- test("a filter claiming an anchor it has no row for exports without one", async () => {
424
- // hasAnchor is a cache of whether the sibling row exists, and a write that
425
- // failed after the filter landed leaves it saying yes over nothing. The
426
- // export answers with the rows it can read, never with the flag.
427
- const fixture = fullFixture();
428
- fixture.anchors = [];
429
-
430
- const document = await exportFixture(fixture);
431
-
432
- assert.equal(document.filters[1]?.anchor, null);
433
- assert.equal(ReaderConfigDocumentSchema.safeParse(document).success, true);
434
- });
435
-
436
407
  test("labels export by name and colour", async () => {
437
408
  const document = await exportFixture(fullFixture());
438
409
 
@@ -625,7 +596,6 @@ test("a configuration with nothing in it still exports a valid document", async
625
596
  mailboxes: [],
626
597
  labels: [],
627
598
  filters: [],
628
- anchors: [],
629
599
  addresses: [],
630
600
  });
631
601
 
package/src/export.ts CHANGED
@@ -14,7 +14,6 @@ import type {
14
14
  AccountItem,
15
15
  AddressFlags,
16
16
  AddressItem,
17
- FilterAnchorItem,
18
17
  FilterItem,
19
18
  LabelItem,
20
19
  MailboxItem,
@@ -245,7 +244,6 @@ const toConfigLabel = (label: LabelItem): ConfigLabel => ({
245
244
 
246
245
  const toConfigFilter = (
247
246
  filter: FilterItem,
248
- anchor: FilterAnchorItem | undefined,
249
247
  labelNames: ReadonlyMap<string, string>,
250
248
  index: MailboxIndex,
251
249
  ): ConfigFilter => {
@@ -274,16 +272,6 @@ const toConfigFilter = (
274
272
  actionAccountId === undefined || actionPath === undefined
275
273
  ? null
276
274
  : { accountId: actionAccountId, folderPath: actionPath },
277
- // The stored vector is a function of the model that produced it, so it
278
- // means nothing on an instance running another one. The source text is
279
- // what travels; import re-embeds from it.
280
- anchor: anchor
281
- ? {
282
- sourceText: anchor.anchorSourceText,
283
- embeddingId: anchor.anchorEmbeddingId,
284
- sourceMessageId: anchor.anchorMessageId,
285
- }
286
- : null,
287
275
  };
288
276
  };
289
277
 
@@ -378,7 +366,7 @@ export const readConfigForExport = async (
378
366
  accountConfigId: string,
379
367
  identity: ConfigExportIdentity,
380
368
  ): Promise<ReaderConfigDocument> => {
381
- const [accountConfig, accounts, settingRows, labels, filters, anchors] =
369
+ const [accountConfig, accounts, settingRows, labels, filters] =
382
370
  await Promise.all([
383
371
  // A configuration nobody has added an account to yet holds no row. That
384
372
  // is an empty export, not a failure: the file is the migration safety
@@ -393,7 +381,6 @@ export const readConfigForExport = async (
393
381
  repositories.accountSetting.listByAccountConfig(accountConfigId),
394
382
  repositories.label.listByAccountConfig(accountConfigId),
395
383
  repositories.filter.listByAccountConfig(accountConfigId),
396
- repositories.filterAnchor.listByAccountConfig(accountConfigId),
397
384
  ]);
398
385
 
399
386
  const liveAccounts = accounts.filter(
@@ -415,9 +402,6 @@ export const readConfigForExport = async (
415
402
  const labelNames = new Map(
416
403
  labels.map((label) => [label.labelId, label.name] as const),
417
404
  );
418
- const anchorsByFilter = new Map(
419
- anchors.map((anchor) => [anchor.filterId, anchor] as const),
420
- );
421
405
  const addresses = await readAddresses(repositories, accountConfigId);
422
406
 
423
407
  const document: ReaderConfigDocument = {
@@ -448,14 +432,7 @@ export const readConfigForExport = async (
448
432
  ),
449
433
  ),
450
434
  labels: labels.map(toConfigLabel),
451
- filters: filters.map((filter) =>
452
- toConfigFilter(
453
- filter,
454
- anchorsByFilter.get(filter.filterId),
455
- labelNames,
456
- index,
457
- ),
458
- ),
435
+ filters: filters.map((filter) => toConfigFilter(filter, labelNames, index)),
459
436
  addressFlags: addresses
460
437
  .filter((address) => carriesUserFlag(address.flags))
461
438
  .map(toConfigAddressFlags),
package/src/fixtures.ts CHANGED
@@ -3,7 +3,6 @@ import type {
3
3
  AccountItem,
4
4
  AccountSettingItem,
5
5
  AddressItem,
6
- FilterAnchorItem,
7
6
  FilterItem,
8
7
  IAddressRepository,
9
8
  LabelItem,
@@ -24,7 +23,6 @@ export interface ConfigFixture {
24
23
  mailboxes: MailboxItem[];
25
24
  labels: LabelItem[];
26
25
  filters: FilterItem[];
27
- anchors: FilterAnchorItem[];
28
26
  addresses: AddressItem[];
29
27
  /** Page size for the address listing, so paging is exercised. */
30
28
  addressPageSize?: number;
@@ -129,11 +127,11 @@ export const makeFilter = (
129
127
  accountConfigId: ACCOUNT_CONFIG_ID,
130
128
  scope: "Standing",
131
129
  state: "Active",
132
- hasAnchor: false,
130
+ disabledReason: "None",
133
131
  ruleChangedAt: NOW,
134
132
  actionChangedAt: NOW,
135
133
  matchOperator: "And",
136
- literalClauses: [],
134
+ literalClauses: [{ field: "Subject", value: "release notes" }],
137
135
  actionLabelId: "None",
138
136
  actionMailboxId: "None",
139
137
  createdAt: NOW,
@@ -141,19 +139,6 @@ export const makeFilter = (
141
139
  ...overrides,
142
140
  });
143
141
 
144
- export const makeAnchor = (
145
- overrides: Partial<FilterAnchorItem> & Pick<FilterAnchorItem, "filterId">,
146
- ): FilterAnchorItem => ({
147
- accountConfigId: ACCOUNT_CONFIG_ID,
148
- anchorEmbedding: [0.11, 0.22, 0.33],
149
- anchorEmbeddingId: "amazon.titan-embed-text-v2:0@1024",
150
- anchorSourceText: "the release note this filter was drawn from",
151
- anchorMessageId: "2c9e4b117d5a4f609a315b8c0",
152
- createdAt: NOW,
153
- updatedAt: NOW,
154
- ...overrides,
155
- });
156
-
157
142
  export const makeAddress = (
158
143
  overrides: Partial<AddressItem> &
159
144
  Pick<AddressItem, "addressId" | "normalizedEmail">,
@@ -286,8 +271,5 @@ export const asRepositories = (
286
271
  filter: {
287
272
  listByAccountConfig: async () => fixture.filters,
288
273
  },
289
- filterAnchor: {
290
- listByAccountConfig: async () => fixture.anchors,
291
- },
292
274
  address: asAddressRepository(fixture),
293
275
  });
@@ -5,7 +5,6 @@ import type {
5
5
  IAccountSettingRepository,
6
6
  IAddressRepository,
7
7
  IConfigImportRepository,
8
- IFilterAnchorRepository,
9
8
  IFilterRepository,
10
9
  ILabelRepository,
11
10
  IMailboxRepository,
@@ -38,7 +37,6 @@ export interface ConfigImportRepositories {
38
37
  mailbox: Pick<IMailboxRepository, "listAllByAccount">;
39
38
  label: Pick<ILabelRepository, "listByAccountConfig" | "create" | "update">;
40
39
  filter: Pick<IFilterRepository, "listByAccountConfig" | "create" | "update">;
41
- filterAnchor: Pick<IFilterAnchorRepository, "put">;
42
40
  /**
43
41
  * Not the suggest listing, for the reason the export refuses it too: what a
44
42
  * configuration already holds is decided over every stored row, junk-only
@@ -54,21 +52,6 @@ export interface ConfigImportRepositories {
54
52
  >;
55
53
  }
56
54
 
57
- /**
58
- * A filter anchor, re-embedded from the source text the file carries. The
59
- * stored vector is a function of the model that produced it, so it never
60
- * travels; this is the one thing an import has to compute rather than copy.
61
- *
62
- * Optional, and free to answer `undefined`, because a configuration file has to
63
- * import on a deployment that ships no embedding model at all. Either way the
64
- * anchor lands stamped `ANCHOR_EMBEDDING_PENDING`, which the existing lazy
65
- * repair (`refreshAnchorForEmbedder`) treats as needing a fresh vector the
66
- * first time the filter is matched.
67
- */
68
- export type EmbedAnchor = (
69
- sourceText: string,
70
- ) => Promise<{ embedding: number[]; embeddingId: string } | undefined>;
71
-
72
55
  /**
73
56
  * Persist one role's folder appointment. Injected rather than composed here:
74
57
  * an appointment row means a person decided, and it keeps that meaning only
@@ -86,17 +69,5 @@ export type AppointFolderRole = (
86
69
  export interface ConfigImportDeps {
87
70
  repositories: ConfigImportRepositories;
88
71
  appointFolderRole: AppointFolderRole;
89
- /**
90
- * Runs the whole apply as one write set, rolling back on any throw.
91
- *
92
- * Optional, and its presence is the fact the report words itself from: a
93
- * backend that supplies this rolls a failed apply back to nothing, and one
94
- * that does not leaves behind whatever landed before the failure. Only the
95
- * caller knows which, so it says so by passing this or omitting it rather
96
- * than by handing over a pass-through that would claim atomicity it has not
97
- * got.
98
- */
99
- transaction?: <T>(run: () => Promise<T>) => Promise<T>;
100
- embedAnchor?: EmbedAnchor;
101
72
  now?: () => number;
102
73
  }