Persist comment source monitoring

This commit is contained in:
jjbrunton committed 2026-08-14 13:40:55 +01:00
1 parent 5e1fc5def9
commit 8d932fcf36
16 files changed
+376 -24

No files matched your search

@@ -0,0 +1,110 @@
import { createHash } from 'node:crypto';
import type Database from 'better-sqlite3';
import { z } from 'zod';
import type { CommentReactionVerdict } from '../ai/openrouter.js';
import type { BrowsingThing, RedditPost } from '../reddit/api.js';
const StoredVerdictSchema = z.object({
id: z.string(),
qualifies: z.boolean(),
expressedGender: z.enum(['male', 'female', 'other', 'unknown']),
confidence: z.number().min(0).max(1),
evidence: z.string(),
});
export interface CommentWatchRefreshResult {
added: number;
active: number;
expired: number;
}
/** Durable source-post rotation and prompt-free comment classification cache. */
export class CommentSourceWatchRepository {
constructor(private readonly db: Database.Database) {}
refreshSources(posts: readonly RedditPost[], watchWindowMs: number, now = Date.now()): CommentWatchRefreshResult {
const expired = this.db.prepare('DELETE FROM comment_source_watches WHERE expires_at <= ?').run(now).changes;
const existing = this.db.prepare('SELECT 1 FROM comment_source_watches WHERE source_post_id = ?');
const upsert = this.db.prepare(`
INSERT INTO comment_source_watches (
source_post_id, subreddit, payload_json, post_created_at,
discovered_at, updated_at, expires_at
) VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(source_post_id) DO UPDATE SET
subreddit = excluded.subreddit,
payload_json = excluded.payload_json,
updated_at = excluded.updated_at,
expires_at = MAX(comment_source_watches.expires_at, excluded.expires_at)
`);
let added = 0;
this.db.transaction(() => {
for (const post of posts) {
const createdAt = Math.round(post.created_utc * 1_000);
const expiresAt = createdAt + watchWindowMs;
if (expiresAt <= now) continue;
if (!existing.get(post.id)) added += 1;
upsert.run(post.id, post.subreddit, JSON.stringify(post), createdAt, now, now, expiresAt);
}
})();
const active = Number((this.db.prepare(`
SELECT COUNT(*) AS count FROM comment_source_watches WHERE expires_at > ?
`).get(now) as { count: number }).count);
return { added, active, expired };
}
nextSource(subreddit: string, now = Date.now()): RedditPost | null {
const row = this.db.prepare(`
SELECT payload_json FROM comment_source_watches
WHERE subreddit = ? COLLATE NOCASE AND expires_at > ?
ORDER BY COALESCE(last_checked_at, 0) ASC, post_created_at DESC
LIMIT 1
`).get(subreddit, now) as { payload_json: string } | undefined;
if (!row) return null;
return JSON.parse(row.payload_json) as RedditPost;
}
markChecked(sourcePostId: string, now = Date.now()): void {
this.db.prepare(`
UPDATE comment_source_watches SET last_checked_at = ?, updated_at = ?
WHERE source_post_id = ?
`).run(now, now, sourcePostId);
}
getVerdict(comment: Pick<BrowsingThing, 'fullname' | 'text'>): CommentReactionVerdict | null {
const row = this.db.prepare(`
SELECT body_sha256, verdict_json FROM comment_source_comment_observations
WHERE comment_id = ?
`).get(comment.fullname) as { body_sha256: string; verdict_json: string } | undefined;
if (!row || row.body_sha256 !== hashBody(comment.text)) return null;
try {
const parsed = StoredVerdictSchema.safeParse(JSON.parse(row.verdict_json));
return parsed.success ? parsed.data : null;
} catch {
return null;
}
}
recordVerdict(comment: BrowsingThing, verdict: CommentReactionVerdict, now = Date.now()): void {
this.db.prepare(`
INSERT INTO comment_source_comment_observations (
comment_id, source_post_id, body_sha256, verdict_json, first_seen_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(comment_id) DO UPDATE SET
source_post_id = excluded.source_post_id,
body_sha256 = excluded.body_sha256,
verdict_json = excluded.verdict_json,
updated_at = excluded.updated_at
`).run(
comment.fullname,
comment.parentPostId,
hashBody(comment.text),
JSON.stringify({ ...verdict, evidence: '' }),
now,
now,
);
}
}
function hashBody(body: string): string {
return createHash('sha256').update(body).digest('hex');
}