Fix commenter-targeted outreach attribution

This commit is contained in:
jjbrunton committed 2026-08-14 12:45:16 +01:00
1 parent b5304e24e3
commit f77157d303
19 files changed
+328 -49

No files matched your search

+45
View File
@@ -0,0 +1,45 @@
export interface CommentTargetedOpenerValidation {
safe: boolean;
reason?: string;
}
const COMMENT_GROUNDING = /\b(?:comment|said|wrote|mentioned|wanted|wanting|wish(?:ed)?|liked the idea|you(?:'d| would) love)\b/i;
const RECIPIENT_APPEARANCE_PATTERNS: ReadonlyArray<{ pattern: RegExp; reason: string }> = [
{
pattern: /\byour\s+(?:tits?|boobs?|breasts?|ass|pussy|body|curves?|figure|face|smile|eyes?|pose|photo|pic(?:ture)?|image|post|shot|selfie)\b/i,
reason: 'attributes the source post or depicted appearance to the commenter',
},
{
pattern: /\byou(?:'re| are| were)?\s+(?:look(?:ing|ed)?|pos(?:e|ing|ed)|smil(?:e|ing|ed)|kneel(?:ing|ed)|ly(?:ing)?|stand(?:ing)?|sit(?:ting)?)\b/i,
reason: 'describes the commenter as though they were depicted',
},
{
pattern: /(?:\byou\b.{0,60}\b(?:camera|photo|picture|image|shot)\b|\b(?:camera|photo|picture|image|shot)\b.{0,60}\byou\b)/i,
reason: 'connects the commenter to visual details from the source post',
},
{
pattern: /\b(?:you\s+(?:posted|shared|uploaded)|your\s+(?:upload|submission))\b/i,
reason: 'claims the commenter created the source post',
},
{
pattern: /\b(?:send|show|share|upload)(?:\s+\w+){0,8}\s+(?:another|more|a|some|your)?\s*(?:pic(?:ture)?s?|photos?|images?|shots?|selfies?)\b/i,
reason: 'asks the commenter for an image based on somebody else\'s post',
},
];
/**
* Fail-closed guard for openers aimed at a commenter on somebody else's post.
* It deliberately permits desired actions addressed to "you", but rejects
* visual or ownership claims that can only have come from the source post.
*/
export function validateCommentTargetedOpener(message: string): CommentTargetedOpenerValidation {
const trimmed = message.trim();
if (!COMMENT_GROUNDING.test(trimmed)) {
return { safe: false, reason: 'does not ground the opener in what the commenter expressed' };
}
for (const rule of RECIPIENT_APPEARANCE_PATTERNS) {
if (rule.pattern.test(trimmed)) return { safe: false, reason: rule.reason };
}
return { safe: true };
}
+24 -17
View File
@@ -25,6 +25,8 @@ export interface ProcessedPost {
sourceType: 'image' | 'chat';
toneId: ToneKey;
scripted: boolean;
/** Identifies the generation contract so stale commenter drafts can be re-prepared safely. */
openerContract?: 'post-author-v1' | 'comment-author-v2';
/** True when an operator replaced the generated opener before dispatch. */
operatorEdited?: boolean;
}
@@ -200,23 +202,27 @@ export class ProcessorService {
.replace('{author}', post.author)
.replace('{flair}', post.link_flair_text || '(none)')
: selectedStrategy?.promptOverride;
const messageArgs = [
analysis.description,
post.title,
post.author,
this.successfulExamples,
strategyPrompt,
analysis.attractivenessScore,
post.link_flair_text ?? undefined,
tone.scripted ? undefined : tone.profile.promptGuidance || undefined,
this.recentMessageProvider('image'),
] as const;
const message = post.targetComment
? await this.aiClient.generateMessage(
...messageArgs,
`<<<UNTRUSTED PUBLIC COMMENT>>>\n${post.targetComment.body.slice(0, 500)}\n<<<END COMMENT>>>`,
)
: await this.aiClient.generateMessage(...messageArgs);
? await this.aiClient.generateCommentTargetedMessage({
recipient: post.author,
comment: post.targetComment.body,
sourcePostTitle: post.title,
sourceImageAnalysis: analysis.description,
strategyGuidance: tone.scripted ? selectedStrategy?.promptOverride : strategyPrompt,
toneGuidance: tone.profile.promptGuidance || undefined,
recentMessages: this.recentMessageProvider('image'),
})
: await this.aiClient.generateMessage(
analysis.description,
post.title,
post.author,
this.successfulExamples,
strategyPrompt,
analysis.attractivenessScore,
post.link_flair_text ?? undefined,
tone.scripted ? undefined : tone.profile.promptGuidance || undefined,
this.recentMessageProvider('image'),
);
if (!message || message.length < 10) {
this.logger.warn(
@@ -241,7 +247,8 @@ export class ProcessorService {
modelId: this.config.openrouter.imageAnalysisModel,
sourceType: 'image',
toneId: tone.profile.id,
scripted: tone.scripted !== null,
scripted: post.targetComment ? false : tone.scripted !== null,
openerContract: post.targetComment ? 'comment-author-v2' : 'post-author-v1',
};
}