Skip to content

Commit 533bd33

Browse files
committed
Add structured critique and continuation rescues
1 parent 4cac3e7 commit 533bd33

15 files changed

Lines changed: 636 additions & 30 deletions

src/canonical/auditLog.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ export function buildAuditRecord(params: {
9191
classifierIntent?: IntentClass;
9292
baseIntent?: IntentClass;
9393
sourceIntent?: IntentClass;
94+
hasParentContext?: boolean;
95+
continuationSignal?: boolean;
96+
continuationSupportScore?: number;
97+
structuredCritiqueSignal?: boolean;
98+
structuredCritiqueSupportScore?: number;
9499
orchestrationEligibleMinimal?: boolean;
95100
conceptualProbeRescue?: boolean;
96101
fastPathBypassReason?: string;
@@ -135,6 +140,11 @@ export function buildAuditRecord(params: {
135140
classifierIntent: params.classifierIntent,
136141
baseIntent: params.baseIntent,
137142
sourceIntent: params.sourceIntent,
143+
hasParentContext: params.hasParentContext,
144+
continuationSignal: params.continuationSignal,
145+
continuationSupportScore: params.continuationSupportScore,
146+
structuredCritiqueSignal: params.structuredCritiqueSignal,
147+
structuredCritiqueSupportScore: params.structuredCritiqueSupportScore,
138148
orchestrationEligibleMinimal: params.orchestrationEligibleMinimal,
139149
conceptualProbeRescue: params.conceptualProbeRescue,
140150
fastPathBypassReason: params.fastPathBypassReason,

src/canonical/classifier.ts

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ import type {
77
EvidenceClass,
88
} from "./types.js";
99
import { isConceptualProbe } from "./conceptualProbe.js";
10+
import {
11+
assessConversationContinue,
12+
} from "./conversationContinue.js";
13+
import {
14+
assessStructuredCritique,
15+
} from "./structuredCritique.js";
1016
import { normalizeCanonicalInputText } from "./inputNormalization.js";
1117

1218
const GREETING_PATTERNS = [
@@ -125,23 +131,77 @@ function classifyIntentText(text: string, event: CanonicalEvent): IntentClass {
125131
return "irrelevant";
126132
}
127133

128-
function classifyIntent(event: CanonicalEvent): IntentClass {
134+
function classifyIntentTrace(event: CanonicalEvent): {
135+
intent: IntentClass;
136+
baseIntent: IntentClass;
137+
hasParentContext: boolean;
138+
continuationSignal: boolean;
139+
continuationSupportScore: number;
140+
structuredCritiqueSignal: boolean;
141+
structuredCritiqueSupportScore: number;
142+
} {
129143
const normalization = normalizeCanonicalInputText(event.text);
130144
for (const candidate of normalization.classifierTextCandidates) {
131145
const intent = classifyIntentText(candidate, event);
146+
const continuation = assessConversationContinue(candidate, event);
147+
const critique = assessStructuredCritique(candidate, event);
148+
149+
if ((intent === "question" || intent === "irrelevant") && continuation.continuationSignal) {
150+
return {
151+
intent: "conversation_continue",
152+
baseIntent: intent,
153+
hasParentContext: continuation.hasParentContext,
154+
continuationSignal: true,
155+
continuationSupportScore: continuation.continuationSupportScore,
156+
structuredCritiqueSignal: false,
157+
structuredCritiqueSupportScore: critique.structuredCritiqueSupportScore,
158+
};
159+
}
160+
161+
if ((intent === "question" || intent === "irrelevant") && critique.structuredCritiqueSignal) {
162+
return {
163+
intent: "structured_critique",
164+
baseIntent: intent,
165+
hasParentContext: critique.hasParentContext,
166+
continuationSignal: false,
167+
continuationSupportScore: continuation.continuationSupportScore,
168+
structuredCritiqueSignal: true,
169+
structuredCritiqueSupportScore: critique.structuredCritiqueSupportScore,
170+
};
171+
}
172+
132173
if (intent !== "irrelevant") {
133-
return intent;
174+
return {
175+
intent,
176+
baseIntent: intent,
177+
hasParentContext: continuation.hasParentContext,
178+
continuationSignal: false,
179+
continuationSupportScore: continuation.continuationSupportScore,
180+
structuredCritiqueSignal: false,
181+
structuredCritiqueSupportScore: critique.structuredCritiqueSupportScore,
182+
};
134183
}
135184
}
136185

137-
return "irrelevant";
186+
const continuation = assessConversationContinue(normalization.normalizedText, event);
187+
const critique = assessStructuredCritique(normalization.normalizedText, event);
188+
return {
189+
intent: "irrelevant",
190+
baseIntent: "irrelevant",
191+
hasParentContext: continuation.hasParentContext,
192+
continuationSignal: false,
193+
continuationSupportScore: continuation.continuationSupportScore,
194+
structuredCritiqueSignal: false,
195+
structuredCritiqueSupportScore: critique.structuredCritiqueSupportScore,
196+
};
138197
}
139198

140199
function classifyTarget(event: CanonicalEvent, intent: IntentClass): TargetClass {
141200
if (event.cashtags.length > 0) return "token";
142201
if (intent === "embodiment_query") return "embodiment";
143202
if (intent === "lore_query") return "lore";
144203
if (intent === "conceptual_probe") return "claim";
204+
if (intent === "structured_critique") return "claim";
145205
if (intent === "greeting" || intent === "casual_ping" || intent === "conversation_continue") return "conversation";
146206
if (intent === "market_question_general") return "market_structure";
147207
if (intent === "accusation") return "behavior";
@@ -154,8 +214,8 @@ function classifyTarget(event: CanonicalEvent, intent: IntentClass): TargetClass
154214

155215
function classifyEvidence(event: CanonicalEvent, intent: IntentClass): EvidenceClass {
156216
const text = event.text;
157-
const hasParent = !!event.parent_text;
158-
const hasContext = event.conversation_context.length > 0;
217+
const hasParent = !!event.parent_text?.trim();
218+
const hasContext = event.conversation_context.length > 0 || Boolean(event.context?.trim()) || Boolean(event.quoted_text?.trim());
159219

160220
const productProof = countPatternMatches(text, PRODUCT_PROOF_PATTERNS);
161221
const parentProof = event.parent_text
@@ -165,7 +225,7 @@ function classifyEvidence(event: CanonicalEvent, intent: IntentClass): EvidenceC
165225
if (productProof >= 2 || (productProof >= 1 && parentProof >= 1)) {
166226
return "self_contained_strong";
167227
}
168-
if (intent === "conceptual_probe") {
228+
if (intent === "conceptual_probe" || intent === "structured_critique") {
169229
return "contextual_medium";
170230
}
171231
if (hasParent || hasContext || productProof >= 1) {
@@ -210,10 +270,18 @@ function extractEvidenceBullets(event: CanonicalEvent, intent: IntentClass): str
210270
if (intent === "conceptual_probe") {
211271
bullets.push("structured frontier question");
212272
}
273+
if (intent === "structured_critique") {
274+
bullets.push("structured skepticism about incentives or architecture");
275+
}
213276
if (countPatternMatches(text, PERFORMANCE_PATTERNS) > 0) {
214277
bullets.push("includes unverified performance claims");
215278
}
216-
if (countPatternMatches(text, PRODUCT_PROOF_PATTERNS) === 0 && intent !== "question") {
279+
if (
280+
countPatternMatches(text, PRODUCT_PROOF_PATTERNS) === 0 &&
281+
intent !== "question" &&
282+
intent !== "conversation_continue" &&
283+
intent !== "structured_critique"
284+
) {
217285
bullets.push("no concrete product proof in visible text");
218286
}
219287
if (countPatternMatches(text, VOLUME_BEHAVIOR_PATTERNS) > 0) {
@@ -273,7 +341,8 @@ function classifyPolicyBlock(intent: IntentClass, spamProb: number, riskFlags: s
273341
}
274342

275343
export function classify(event: CanonicalEvent): ClassifierOutput {
276-
const intent = classifyIntent(event);
344+
const intentTrace = classifyIntentTrace(event);
345+
const intent = intentTrace.intent;
277346
const target = classifyTarget(event, intent);
278347
const evidence_class = classifyEvidence(event, intent);
279348
const bait_probability = estimateBaitProbability(event);
@@ -293,5 +362,11 @@ export function classify(event: CanonicalEvent): ClassifierOutput {
293362
policy_reasons: policy.reasons,
294363
evidence_bullets,
295364
risk_flags,
365+
baseIntent: intentTrace.baseIntent,
366+
hasParentContext: intentTrace.hasParentContext,
367+
continuationSignal: intentTrace.continuationSignal,
368+
continuationSupportScore: intentTrace.continuationSupportScore,
369+
structuredCritiqueSignal: intentTrace.structuredCritiqueSignal,
370+
structuredCritiqueSupportScore: intentTrace.structuredCritiqueSupportScore,
296371
};
297372
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import type { CanonicalEvent } from "./types.js";
2+
3+
export interface ConversationContinueAssessment {
4+
hasParentContext: boolean;
5+
formSignal: boolean;
6+
relevanceSignal: boolean;
7+
continuationSignal: boolean;
8+
continuationSupportScore: number;
9+
reasons: string[];
10+
}
11+
12+
const CONTINUATION_FORM_PATTERNS: RegExp[] = [
13+
/^(?:and|so|then)\b/i,
14+
/\band\s+what\s+about\b/i,
15+
/\bthen\s+where\s+is\b/i,
16+
/\bwhat\s+fails?\s+first\b/i,
17+
/\bwhat\s+bottlenecks?\b/i,
18+
/\bwhere\s+is\s+the\s+bottleneck\b/i,
19+
/\bwhere\s+does\b.*\b(?:break|fail|bottleneck|break\s+down)\b/i,
20+
];
21+
22+
const CONTINUATION_RELEVANCE_PATTERNS: RegExp[] = [
23+
/\bexecution\b/i,
24+
/\bbottleneck(?:s)?\b/i,
25+
/\barchitecture\b/i,
26+
/\bstructure\b/i,
27+
/\bthesis\b/i,
28+
/\bincentive(?:s)?\b/i,
29+
/\brisk\b/i,
30+
/\binterface\b/i,
31+
/\bfailure(?:\s+mode)?\b/i,
32+
/\bbreak(?:s|down)?\b/i,
33+
/\bconstraint(?:s)?\b/i,
34+
/\btrade[- ]?off(?:s)?\b/i,
35+
/\bdependenc(?:y|ies)\b/i,
36+
/\bimplementation\b/i,
37+
];
38+
39+
const CONTINUATION_NEGATIVE_PATTERNS: RegExp[] = [
40+
/^\s*(?:and|so|then)\s*[?!.]*$/i,
41+
/^\s*(?:and|so|then)\s+(?:lol|lmao|lmfao|haha|hehe|bruh|bro)\b/i,
42+
/^\s*(?:and|so|then)\s+(?:buy|sell|ape|long|short|trade)\b/i,
43+
/^\s*(?:what\s+now|now\s+what|why\??|so\??|and\??)\s*$/i,
44+
];
45+
46+
function normalize(text: string): string {
47+
return (text ?? "").trim().replace(/\s+/g, " ");
48+
}
49+
50+
function wordCount(text: string): number {
51+
const normalized = normalize(text);
52+
if (!normalized) return 0;
53+
return normalized.split(" ").filter(Boolean).length;
54+
}
55+
56+
function hasPatternMatch(text: string, patterns: RegExp[]): boolean {
57+
return patterns.some((pattern) => pattern.test(text));
58+
}
59+
60+
export function hasRelevantParentContext(event: CanonicalEvent): boolean {
61+
return Boolean(
62+
event.parent_text?.trim() ||
63+
event.quoted_text?.trim() ||
64+
event.context?.trim() ||
65+
(event.conversation_context?.length ?? 0) > 0,
66+
);
67+
}
68+
69+
export function looksLikeContinuation(text: string): boolean {
70+
const normalized = normalize(text);
71+
if (!normalized) return false;
72+
return hasPatternMatch(normalized, CONTINUATION_FORM_PATTERNS);
73+
}
74+
75+
export function hasContinuationRelevance(text: string): boolean {
76+
const normalized = normalize(text);
77+
if (!normalized) return false;
78+
return hasPatternMatch(normalized, CONTINUATION_RELEVANCE_PATTERNS);
79+
}
80+
81+
function isNegativeContinuation(text: string): boolean {
82+
const normalized = normalize(text);
83+
if (!normalized) return true;
84+
if (wordCount(normalized) < 3) return true;
85+
return hasPatternMatch(normalized, CONTINUATION_NEGATIVE_PATTERNS);
86+
}
87+
88+
export function assessConversationContinue(
89+
text: string,
90+
event: CanonicalEvent,
91+
): ConversationContinueAssessment {
92+
const normalized = normalize(text);
93+
const hasParentContext = hasRelevantParentContext(event);
94+
const formSignal = looksLikeContinuation(normalized);
95+
const relevanceSignal = hasContinuationRelevance(normalized);
96+
const negativeSignal = isNegativeContinuation(normalized);
97+
const supportScore = Math.round(
98+
(
99+
(
100+
(formSignal ? 1 : 0) +
101+
(hasParentContext ? 1 : 0) +
102+
(relevanceSignal ? 1 : 0)
103+
) / 3
104+
) * 100,
105+
) / 100;
106+
107+
const reasons: string[] = [];
108+
if (formSignal) reasons.push("form_signal");
109+
if (hasParentContext) reasons.push("parent_context");
110+
if (relevanceSignal) reasons.push("relevance_signal");
111+
if (negativeSignal) reasons.push("negative_followup");
112+
113+
return {
114+
hasParentContext,
115+
formSignal,
116+
relevanceSignal,
117+
continuationSignal: formSignal && hasParentContext && relevanceSignal && !negativeSignal,
118+
continuationSupportScore: supportScore,
119+
reasons,
120+
};
121+
}

src/canonical/modeSelector.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const SOCIAL_MODE_MAP: Partial<Record<IntentClass, CanonicalMode>> = {
1515
market_question_general: "market_banter",
1616
embodiment_query: "embodiment_reply",
1717
lore_query: "lore_drop",
18-
conversation_continue: "conversation_hook",
18+
conversation_continue: "neutral_clarification",
1919
};
2020

2121
export function selectMode(
@@ -39,6 +39,10 @@ export function selectMode(
3939
return "neutral_clarification";
4040
}
4141

42+
if (cls.intent === "structured_critique") {
43+
return "skeptical_breakdown";
44+
}
45+
4246
const socialMode = SOCIAL_MODE_MAP[cls.intent];
4347
if (socialMode) {
4448
return socialMode;

0 commit comments

Comments
 (0)