@remit/mailbox-service 0.0.61 → 0.0.62
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/src/filters/anchor-drift.ts +55 -0
- package/src/filters/pipeline.ts +19 -35
- package/src/index.ts +5 -0
package/package.json
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FilterAnchorItem,
|
|
3
|
+
IFilterAnchorRepository,
|
|
4
|
+
} from "@remit/data-ports";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The half of an embedding service an anchor refresh needs: the text-to-vector
|
|
8
|
+
* call and the `<modelId>@<dimensions>` identifier of the model behind it (the
|
|
9
|
+
* same scheme `EmbeddingService.embeddingId` derives). Compared against
|
|
10
|
+
* `FilterAnchor.anchorEmbeddingId` to detect a model drift a same-dimension
|
|
11
|
+
* swap would otherwise pass through silently (RFC 039 Decision 1a, reader
|
|
12
|
+
* #295).
|
|
13
|
+
*/
|
|
14
|
+
export interface AnchorEmbedder {
|
|
15
|
+
embed(text: string): Promise<number[]>;
|
|
16
|
+
readonly embeddingId: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AnchorDriftDeps {
|
|
20
|
+
anchorRepository: Pick<IFilterAnchorRepository, "put">;
|
|
21
|
+
/** Absent on a deployment with no embedder wired; then nothing can drift. */
|
|
22
|
+
embedder: AnchorEmbedder | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The anchor to score against, re-embedded in place when the embedding model
|
|
27
|
+
* has drifted since it was written (RFC 039 Decision 1a). No migration job
|
|
28
|
+
* walks these rows proactively, so the refresh is lazy: the first read that
|
|
29
|
+
* notices the stamp no longer matches the configured model re-embeds the
|
|
30
|
+
* already-persisted `anchorSourceText` and writes it back under the current
|
|
31
|
+
* id. A failure here is never a terminal state — the row is left as it was,
|
|
32
|
+
* so the next read that reaches this anchor retries.
|
|
33
|
+
*
|
|
34
|
+
* The single mechanism behind both index-time matching
|
|
35
|
+
* ({@link FilterPipeline}) and the back-apply pass's cross-filter precedence
|
|
36
|
+
* check, which must agree on what a filter currently matches (reader #399).
|
|
37
|
+
*/
|
|
38
|
+
export const refreshAnchorForEmbedder = async (
|
|
39
|
+
deps: AnchorDriftDeps,
|
|
40
|
+
anchor: FilterAnchorItem,
|
|
41
|
+
): Promise<FilterAnchorItem> => {
|
|
42
|
+
const { embedder } = deps;
|
|
43
|
+
if (!embedder || anchor.anchorEmbeddingId === embedder.embeddingId) {
|
|
44
|
+
return anchor;
|
|
45
|
+
}
|
|
46
|
+
const anchorEmbedding = await embedder.embed(anchor.anchorSourceText);
|
|
47
|
+
return deps.anchorRepository.put({
|
|
48
|
+
accountConfigId: anchor.accountConfigId,
|
|
49
|
+
filterId: anchor.filterId,
|
|
50
|
+
anchorEmbedding,
|
|
51
|
+
anchorEmbeddingId: embedder.embeddingId,
|
|
52
|
+
anchorSourceText: anchor.anchorSourceText,
|
|
53
|
+
anchorMessageId: anchor.anchorMessageId,
|
|
54
|
+
});
|
|
55
|
+
};
|
package/src/filters/pipeline.ts
CHANGED
|
@@ -7,6 +7,10 @@ import type {
|
|
|
7
7
|
} from "@remit/data-ports";
|
|
8
8
|
import { FilterState } from "@remit/domain-enums";
|
|
9
9
|
import type { PlacementMoveService } from "../placement-move.js";
|
|
10
|
+
import {
|
|
11
|
+
type AnchorEmbedder,
|
|
12
|
+
refreshAnchorForEmbedder,
|
|
13
|
+
} from "./anchor-drift.js";
|
|
10
14
|
import {
|
|
11
15
|
buildMatchText,
|
|
12
16
|
cosineSimilarity,
|
|
@@ -20,22 +24,12 @@ import {
|
|
|
20
24
|
/**
|
|
21
25
|
* Turns the candidate message's text into a single message-level vector to
|
|
22
26
|
* compare against a filter's persisted `anchorEmbedding`. The anchor side is
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
27
|
+
* embedded here only to repair a drifted anchor in place (see
|
|
28
|
+
* {@link refreshAnchorForEmbedder}); the match itself reads `FilterAnchor` as
|
|
29
|
+
* a fixed fact (RFC 034 Decision 2.1/2.3), and the incoming message is
|
|
30
|
+
* embedded once per message, only when a semantic filter is actually in play.
|
|
26
31
|
*/
|
|
27
|
-
export
|
|
28
|
-
embed(text: string): Promise<number[]>;
|
|
29
|
-
/**
|
|
30
|
-
* `<modelId>@<dimensions>` identifier of the model currently configured —
|
|
31
|
-
* the same scheme `EmbeddingService.embeddingId`
|
|
32
|
-
* (`packages/search-service/src/embeddings.ts`) derives. Compared against
|
|
33
|
-
* `FilterAnchor.anchorEmbeddingId` to detect a model drift a same-dimension
|
|
34
|
-
* swap would otherwise pass through silently (RFC 039 Decision 1a, reader
|
|
35
|
-
* #295).
|
|
36
|
-
*/
|
|
37
|
-
readonly embeddingId: string;
|
|
38
|
-
}
|
|
32
|
+
export type MessageEmbedder = AnchorEmbedder;
|
|
39
33
|
|
|
40
34
|
export interface FilterLogger {
|
|
41
35
|
info(obj: Record<string, unknown>, msg: string): void;
|
|
@@ -236,26 +230,16 @@ export class FilterPipeline {
|
|
|
236
230
|
return false;
|
|
237
231
|
}
|
|
238
232
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const anchorEmbedding = await embedder.embed(anchor.anchorSourceText);
|
|
250
|
-
anchor = await this.config.filterAnchorService.put({
|
|
251
|
-
accountConfigId,
|
|
252
|
-
filterId: filter.filterId,
|
|
253
|
-
anchorEmbedding,
|
|
254
|
-
anchorEmbeddingId: embedder.embeddingId,
|
|
255
|
-
anchorSourceText: anchor.anchorSourceText,
|
|
256
|
-
anchorMessageId: anchor.anchorMessageId,
|
|
257
|
-
});
|
|
258
|
-
}
|
|
233
|
+
// A re-embed failure here propagates to the per-filter catch in
|
|
234
|
+
// `match()`, which logs and skips this filter for this one evaluation
|
|
235
|
+
// exactly as an unrecoverable stale anchor does today.
|
|
236
|
+
anchor = await refreshAnchorForEmbedder(
|
|
237
|
+
{
|
|
238
|
+
anchorRepository: this.config.filterAnchorService,
|
|
239
|
+
embedder: this.config.embedder,
|
|
240
|
+
},
|
|
241
|
+
anchor,
|
|
242
|
+
);
|
|
259
243
|
|
|
260
244
|
const vector = await embed();
|
|
261
245
|
if (!vector) {
|
package/src/index.ts
CHANGED
|
@@ -59,6 +59,11 @@ export {
|
|
|
59
59
|
testImapConnection,
|
|
60
60
|
testSmtpConnection,
|
|
61
61
|
} from "./connection-test.js";
|
|
62
|
+
export {
|
|
63
|
+
type AnchorDriftDeps,
|
|
64
|
+
type AnchorEmbedder,
|
|
65
|
+
refreshAnchorForEmbedder,
|
|
66
|
+
} from "./filters/anchor-drift.js";
|
|
62
67
|
export { extractListId, normalizeListId } from "./filters/list-id.js";
|
|
63
68
|
export {
|
|
64
69
|
buildMatchText,
|