feat(ai-moderation): enrich analysis context with recency, repetition, user history and channel topic

- <message> targets now carry time (ISO), repetitions (N identical short texts = spam signal), bot and edited flags; escape id/user XML
- rich <user_reputation>: total_infractions, clean_streak, last_offense_days_ago, repeat_offender (7-day window)
- <user_history> with last flagged messages for repeat offenders (wires dead getUserRecentInfractions)
- <user_profile as_of> staleness signal; <location_context topic> from captured channel topic
- prompt framing + output instructions teach the LLM to use the new signals without treating history as proof
- tests: contextEnrichment.test.ts (13) + topic cases in conversationContext.test.ts
This commit is contained in:
asepharyana
2026-08-10 17:15:33 +07:00
parent 0a5254bf20
commit 65c9c2cd9e
11 changed files with 488 additions and 26 deletions
@@ -197,6 +197,36 @@ describe("buildLocationContext — channel/thread/nsfw enrichment", () => {
expect(line).toContain('age_restricted="false"');
});
it("includes the channel topic (escaped) when captured", () => {
const t = target();
t.metadata = JSON.stringify({
channel: {
channelName: "rules",
topic: "Diskusi coding & programming — no self-promo",
nsfw: false,
},
});
const line = buildLocationContext([t]);
expect(line).toContain(
'topic="Diskusi coding &amp; programming — no self-promo"',
);
});
it("caps an oversized topic and omits empty/absent topic", () => {
const t = target();
t.metadata = JSON.stringify({
channel: { channelName: "general", topic: "x".repeat(500), nsfw: false },
});
const line = buildLocationContext([t]);
const match = line.match(/topic="([^"]*)"/);
expect(match).not.toBeNull();
expect(match?.[1].length).toBeLessThanOrEqual(201);
const t2 = target();
t2.metadata = JSON.stringify({ channel: { channelName: "general" } });
expect(buildLocationContext([t2])).not.toContain("topic=");
});
it("returns empty when no metadata", () => {
expect(buildLocationContext([target()])).toBe("");
});