@remit/backend 0.0.29 → 0.0.30
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/service/organize.test.ts +120 -1
- package/src/service/organize.ts +8 -3
package/package.json
CHANGED
|
@@ -213,7 +213,14 @@ const candidate = (
|
|
|
213
213
|
over: Partial<OrganizeCandidate["message"]> = {},
|
|
214
214
|
): OrganizeCandidate => ({
|
|
215
215
|
messageId,
|
|
216
|
-
message: {
|
|
216
|
+
message: {
|
|
217
|
+
from: "",
|
|
218
|
+
fromName: "",
|
|
219
|
+
subject: "",
|
|
220
|
+
text: "",
|
|
221
|
+
listId: "",
|
|
222
|
+
...over,
|
|
223
|
+
},
|
|
217
224
|
});
|
|
218
225
|
|
|
219
226
|
describe("matchOrganize", () => {
|
|
@@ -597,3 +604,115 @@ describe("back-apply pipeline (matchOrganize -> applyOrganize)", () => {
|
|
|
597
604
|
);
|
|
598
605
|
});
|
|
599
606
|
});
|
|
607
|
+
|
|
608
|
+
describe("matchOrganize with ListId and FromDomain clauses", () => {
|
|
609
|
+
const senderChunk = (
|
|
610
|
+
messageId: string,
|
|
611
|
+
fromEmail: string,
|
|
612
|
+
over: Partial<ChunkMetadata> = {},
|
|
613
|
+
): VectorRecord => ({
|
|
614
|
+
chunkId: `${messageId}#sender`,
|
|
615
|
+
vector: ANCHOR_VECTOR,
|
|
616
|
+
metadata: metadata({
|
|
617
|
+
messageId,
|
|
618
|
+
chunkType: "sender",
|
|
619
|
+
textPreview: fromEmail,
|
|
620
|
+
...over,
|
|
621
|
+
}),
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it("matches a ListId clause exactly off the vector-free projection", async () => {
|
|
625
|
+
const deps = vectorlessDeps([
|
|
626
|
+
candidate("msg-1", { listId: "weekly.news.example.com" }),
|
|
627
|
+
candidate("msg-2", { listId: "news.example.com" }),
|
|
628
|
+
candidate("msg-3", {}),
|
|
629
|
+
]);
|
|
630
|
+
|
|
631
|
+
const { messageIds } = await matchOrganize(deps, ACCOUNT_CONFIG_ID, {
|
|
632
|
+
...predicate(),
|
|
633
|
+
anchorMessageId: "None",
|
|
634
|
+
literalClauses: [{ field: "ListId", value: "weekly.news.example.com" }],
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
assert.deepEqual(messageIds, ["msg-1"]);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it("matches a FromDomain clause public-suffix aware off the vector-free projection", async () => {
|
|
641
|
+
const deps = vectorlessDeps([
|
|
642
|
+
candidate("msg-1", { from: "notifications@github.com" }),
|
|
643
|
+
candidate("msg-2", { from: "ci@sub.github.com" }),
|
|
644
|
+
candidate("msg-3", { from: "attacker@github.com.evil.example" }),
|
|
645
|
+
]);
|
|
646
|
+
|
|
647
|
+
const { messageIds } = await matchOrganize(deps, ACCOUNT_CONFIG_ID, {
|
|
648
|
+
...predicate(),
|
|
649
|
+
anchorMessageId: "None",
|
|
650
|
+
literalClauses: [{ field: "FromDomain", value: "github.com" }],
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
assert.deepEqual(messageIds, ["msg-1", "msg-2"]);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it("round-trips ListId and FromDomain through the semantic-arm chunk projection", async () => {
|
|
657
|
+
const store = createMemoryVectorStore();
|
|
658
|
+
await store.upsert([
|
|
659
|
+
senderChunk("msg-1", "ci@github.com", {
|
|
660
|
+
listId: "actions.github.com",
|
|
661
|
+
}),
|
|
662
|
+
bodyChunk("msg-1", ANCHOR_VECTOR, { listId: "actions.github.com" }),
|
|
663
|
+
senderChunk("msg-2", "hi@othersender.example", {
|
|
664
|
+
listId: "other.list.example",
|
|
665
|
+
}),
|
|
666
|
+
bodyChunk("msg-2", ANCHOR_VECTOR, { listId: "other.list.example" }),
|
|
667
|
+
]);
|
|
668
|
+
|
|
669
|
+
const byListId = await matchOrganize(matchDeps(store), ACCOUNT_CONFIG_ID, {
|
|
670
|
+
...predicate(),
|
|
671
|
+
literalClauses: [{ field: "ListId", value: "actions.github.com" }],
|
|
672
|
+
});
|
|
673
|
+
assert.deepEqual(byListId.messageIds, ["msg-1"]);
|
|
674
|
+
|
|
675
|
+
const byFromDomain = await matchOrganize(
|
|
676
|
+
matchDeps(store),
|
|
677
|
+
ACCOUNT_CONFIG_ID,
|
|
678
|
+
{
|
|
679
|
+
...predicate(),
|
|
680
|
+
literalClauses: [{ field: "FromDomain", value: "github.com" }],
|
|
681
|
+
},
|
|
682
|
+
);
|
|
683
|
+
assert.deepEqual(byFromDomain.messageIds, ["msg-1"]);
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it("applies a ListId predicate to exactly the previewed set (preview == apply)", async () => {
|
|
687
|
+
const deps = vectorlessDeps([
|
|
688
|
+
candidate("msg-1", { listId: "weekly.news.example.com" }),
|
|
689
|
+
candidate("msg-2", { listId: "weekly.news.example.com" }),
|
|
690
|
+
candidate("msg-3", { listId: "other.example.com" }),
|
|
691
|
+
]);
|
|
692
|
+
const p = predicate({
|
|
693
|
+
anchorMessageId: "None",
|
|
694
|
+
actionLabelId: "lbl-list",
|
|
695
|
+
literalClauses: [{ field: "ListId", value: "weekly.news.example.com" }],
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
const previewed = await matchOrganize(deps, ACCOUNT_CONFIG_ID, p);
|
|
699
|
+
const applied = await matchOrganize(deps, ACCOUNT_CONFIG_ID, p);
|
|
700
|
+
assert.deepEqual(previewed, applied);
|
|
701
|
+
assert.deepEqual(previewed.messageIds, ["msg-1", "msg-2"]);
|
|
702
|
+
|
|
703
|
+
const tracked = trackingClient();
|
|
704
|
+
const result = await applyOrganize(
|
|
705
|
+
{ client: tracked.client },
|
|
706
|
+
ACCOUNT_CONFIG_ID,
|
|
707
|
+
applied.messageIds,
|
|
708
|
+
p,
|
|
709
|
+
);
|
|
710
|
+
|
|
711
|
+
assert.equal(result.applied, 2);
|
|
712
|
+
assert.equal(result.failed, 0);
|
|
713
|
+
assert.deepEqual(tracked.labeled.map((row) => row.messageId).sort(), [
|
|
714
|
+
"msg-1",
|
|
715
|
+
"msg-2",
|
|
716
|
+
]);
|
|
717
|
+
});
|
|
718
|
+
});
|
package/src/service/organize.ts
CHANGED
|
@@ -131,11 +131,13 @@ const filterMessageFromChunks = (
|
|
|
131
131
|
let subject = "";
|
|
132
132
|
let fromName = "";
|
|
133
133
|
let from = "";
|
|
134
|
+
let listId = "";
|
|
134
135
|
const textParts: string[] = [];
|
|
135
136
|
for (const record of records) {
|
|
136
137
|
const meta = record.metadata;
|
|
137
138
|
if (meta.subject && !subject) subject = meta.subject;
|
|
138
139
|
if (meta.fromName && !fromName) fromName = meta.fromName;
|
|
140
|
+
if (meta.listId && !listId) listId = meta.listId;
|
|
139
141
|
if (meta.chunkType === "sender" && meta.textPreview && !from) {
|
|
140
142
|
from = meta.textPreview;
|
|
141
143
|
}
|
|
@@ -146,7 +148,7 @@ const filterMessageFromChunks = (
|
|
|
146
148
|
textParts.push(meta.textPreview);
|
|
147
149
|
}
|
|
148
150
|
}
|
|
149
|
-
return { from, fromName, subject, text: textParts.join("\n") };
|
|
151
|
+
return { from, fromName, subject, listId, text: textParts.join("\n") };
|
|
150
152
|
};
|
|
151
153
|
|
|
152
154
|
/**
|
|
@@ -355,8 +357,10 @@ const buildSemanticFromEnv = (): OrganizeSemanticDeps => {
|
|
|
355
357
|
* deployment that ships no vector pipeline. Deduped by message id — the same
|
|
356
358
|
* mail filed in two folders is one candidate.
|
|
357
359
|
*
|
|
358
|
-
* `From`/`Subject` come from the row verbatim (full fidelity)
|
|
359
|
-
*
|
|
360
|
+
* `From`/`Subject`/`listId` come from the row verbatim (full fidelity), so a
|
|
361
|
+
* `From`, `Subject`, `FromDomain` or `ListId` clause matches here exactly as it
|
|
362
|
+
* does at index time. `text` (the body) is left empty: the thread row carries
|
|
363
|
+
* only a truncated `snippet`, and matching
|
|
360
364
|
* a body-content clause against a preview would silently diverge from the live
|
|
361
365
|
* index-time filter's full-body match. Body-content (`HasWords`) clauses are
|
|
362
366
|
* rejected before this is read (see {@link assertNoBodyContentClause}), so the
|
|
@@ -384,6 +388,7 @@ const listAccountFilterMessagesFromClient =
|
|
|
384
388
|
fromName: row.fromName ?? "",
|
|
385
389
|
subject: row.subject ?? "",
|
|
386
390
|
text: "",
|
|
391
|
+
listId: row.listId ?? "",
|
|
387
392
|
},
|
|
388
393
|
});
|
|
389
394
|
if (candidates.length >= limit) return candidates;
|