11import {
2+ containsWriteVerb ,
3+ deriveAmbiguity ,
4+ deriveConsentRequirement ,
25 type CommitPolicyOutput ,
3- type ConversationEntity ,
46 type TurnAmbiguity ,
57 type TurnClassifierOutput ,
6- type TurnInterpretationType ,
78 type TurnPolicyDecision ,
89 type TurnRoutingInput
910} from "@atlas/core" ;
@@ -33,7 +34,12 @@ type StructuredWriteReadiness =
3334export function decideTurnPolicy ( input : DecideTurnPolicyInput ) : TurnPolicyDecision {
3435 const { classification, commitResult } = input ;
3536 const targetEntityId = classification . resolvedEntityIds [ 0 ] ;
36- const ambiguity = deriveAmbiguity ( classification , commitResult ) ;
37+ const ambiguity = deriveAmbiguity ( {
38+ classifierConfidence : classification . confidence ,
39+ missingSlots : commitResult . missingSlots ,
40+ needsClarification : commitResult . needsClarification ,
41+ blockingSlots : [ ]
42+ } ) ;
3743
3844 switch ( classification . turnType ) {
3945 case "informational" :
@@ -109,17 +115,6 @@ export function decideTurnPolicy(input: DecideTurnPolicyInput): TurnPolicyDecisi
109115 }
110116}
111117
112- function deriveAmbiguity (
113- classification : TurnClassifierOutput ,
114- commitResult : CommitPolicyOutput
115- ) : TurnAmbiguity {
116- if ( classification . confidence < 0.6 ) return "high" ;
117- if ( commitResult . missingSlots . length > 0 ) return "high" ;
118- if ( commitResult . needsClarification . length > 0 ) return "high" ;
119- if ( classification . confidence < 0.8 ) return "low" ;
120- return "none" ;
121- }
122-
123118function deriveStructuredWriteReadiness (
124119 input : DecideTurnPolicyInput ,
125120 ambiguity : TurnAmbiguity
@@ -152,8 +147,6 @@ function deriveStructuredWriteReadiness(
152147 } ;
153148 }
154149
155- // Bug 4 fix: if this is a clarification answer and the proposal is already confirmed,
156- // skip re-presenting and go straight to execution
157150 if ( classification . turnType === "clarification_answer" ) {
158151 const entityRegistry = input . routingContext . entityRegistry ?? [ ] ;
159152 const alreadyConfirmed = entityRegistry . some (
@@ -171,13 +164,19 @@ function deriveStructuredWriteReadiness(
171164 }
172165 }
173166
174- const consentRequirement = deriveConsentRequirement ( input ) ;
167+ const consentRequirement = deriveConsentRequirement ( {
168+ classification,
169+ entityRegistry : input . routingContext . entityRegistry ?? [ ] ,
170+ normalizedText : input . routingContext . normalizedText
171+ } ) ;
175172
176173 if ( consentRequirement . required ) {
177174 return {
178175 state : "ready_needs_consent" ,
179176 reason : consentRequirement . reason ,
180- ...( consentRequirement . targetProposalId ? { targetProposalId : consentRequirement . targetProposalId } : { } )
177+ ...( consentRequirement . required && "targetProposalId" in consentRequirement
178+ ? { targetProposalId : consentRequirement . targetProposalId }
179+ : { } )
181180 } ;
182181 }
183182
@@ -231,145 +230,3 @@ function buildPolicyFromStructuredReadiness(
231230 } ;
232231 }
233232}
234-
235- function deriveConsentRequirement ( input : DecideTurnPolicyInput ) {
236- const { classification } = input ;
237- // Bug 2 fix: match "presented" status in addition to "active"
238- const activeProposal = ( input . routingContext . entityRegistry ?? [ ] ) . find (
239- ( entity ) : entity is Extract < ConversationEntity , { kind : "proposal_option" } > =>
240- entity . kind === "proposal_option" &&
241- ( entity . status === "active" || entity . status === "presented" ) &&
242- entity . id === classification . resolvedProposalId &&
243- entity . data . confirmationRequired === true
244- ) ;
245-
246- if ( ! activeProposal ) {
247- return {
248- required : false ,
249- reason : "Deterministic product rules do not require additional consent."
250- } ;
251- }
252-
253- if ( ! matchesProposalTarget ( activeProposal . data . targetEntityId ?? null , classification . resolvedEntityIds ) ) {
254- return {
255- required : false ,
256- reason : "Deterministic product rules do not require additional consent."
257- } ;
258- }
259-
260- const compatibility = deriveProposalCompatibility ( input , activeProposal ) ;
261-
262- if ( ! compatibility . compatible ) {
263- return {
264- required : true ,
265- reason : compatibility . reason
266- } ;
267- }
268-
269- return {
270- required : true ,
271- reason : "Write request is ready, but deterministic product policy still requires user consent." ,
272- targetProposalId : activeProposal . id
273- } ;
274- }
275-
276- function matchesProposalTarget ( targetEntityId : string | null , resolvedEntityIds : string [ ] ) {
277- if ( ! targetEntityId || resolvedEntityIds . length === 0 ) {
278- return true ;
279- }
280-
281- return resolvedEntityIds . includes ( targetEntityId ) ;
282- }
283-
284- function deriveProposalCompatibility (
285- input : DecideTurnPolicyInput ,
286- proposal : Extract < ConversationEntity , { kind : "proposal_option" } >
287- ) {
288- if ( input . classification . turnType === "clarification_answer" ) {
289- return {
290- compatible : true ,
291- reason : "Clarification answers may continue the same consent-required proposal."
292- } ;
293- }
294-
295- const currentActionKind = deriveActionKind ( input . routingContext . normalizedText , input . classification . turnType ) ;
296- const proposalActionKind = deriveActionKind (
297- proposal . data . originatingTurnText ?? proposal . data . replyText ,
298- inferProposalTurnType ( proposal )
299- ) ;
300-
301- if ( currentActionKind !== proposalActionKind ) {
302- return {
303- compatible : false ,
304- reason : "The new turn changes the action type, so it needs fresh consent."
305- } ;
306- }
307-
308- const currentFingerprint = deriveParameterFingerprint ( input . routingContext . normalizedText ) ;
309- const proposalFingerprint = deriveParameterFingerprint ( proposal . data . originatingTurnText ?? proposal . data . replyText ) ;
310-
311- if ( currentFingerprint . explicit && proposalFingerprint . explicit && currentFingerprint . value !== proposalFingerprint . value ) {
312- return {
313- compatible : false ,
314- reason : "The new turn changes proposal parameters, so it needs fresh consent."
315- } ;
316- }
317-
318- return {
319- compatible : true ,
320- reason : "The pending proposal still matches the current turn."
321- } ;
322- }
323-
324- function inferProposalTurnType (
325- proposal : Extract < ConversationEntity , { kind : "proposal_option" } >
326- ) : TurnInterpretationType {
327- const source = ( proposal . data . originatingTurnText ?? proposal . data . replyText ) . toLowerCase ( ) ;
328-
329- if ( / \b ( m o v e | r e s c h e d u l e | s h i f t | p u s h | p u l l | c o m p l e t e | a r c h i v e | c a n c e l | d e l e t e | u p d a t e | c h a n g e | m a r k ) \b / . test ( source ) ) {
330- return "edit_request" ;
331- }
332-
333- return "planning_request" ;
334- }
335-
336- function deriveActionKind ( text : string , turnType : TurnInterpretationType ) {
337- if ( turnType === "edit_request" ) {
338- return "edit" ;
339- }
340-
341- if ( turnType === "planning_request" ) {
342- return "plan" ;
343- }
344-
345- const lower = text . toLowerCase ( ) ;
346-
347- if ( / \b ( m o v e | r e s c h e d u l e | s h i f t | p u s h | p u l l | c o m p l e t e | a r c h i v e | c a n c e l | d e l e t e | u p d a t e | c h a n g e | m a r k ) \b / . test ( lower ) ) {
348- return "edit" ;
349- }
350-
351- return "plan" ;
352- }
353-
354- function deriveParameterFingerprint ( text : string ) {
355- const lower = text . toLowerCase ( ) ;
356- const dayTokens = lower . match (
357- / \b ( t o d a y | t o n i g h t | t o m o r r o w | t m r | m o n d a y | t u e s d a y | w e d n e s d a y | t h u r s d a y | f r i d a y | s a t u r d a y | s u n d a y | w e e k e n d | n e x t w e e k | n e x t m o n t h | m o r n i n g | a f t e r n o o n | e v e n i n g ) \b / g
358- ) ?? [ ] ;
359- const timeTokens =
360- lower . match ( / \b \d { 1 , 2 } (?: : \d { 2 } ) ? \s ? (?: a m | p m ) ? \b | \b n o o n \b | \b m i d n i g h t \b / g) ?? [ ] ;
361- const durationTokens =
362- lower . match ( / \b f o r \s + \d + \s * (?: m i n u t e s ? | m i n s ? | h o u r s ? | h r s ? ) \b | \b \d + \s * (?: m i n u t e s ? | m i n s ? | h o u r s ? | h r s ? ) \b / g) ?? [ ] ;
363- const fingerprintParts = [ ...dayTokens , ...timeTokens , ...durationTokens ] . map ( ( part ) => part . trim ( ) ) . sort ( ) ;
364-
365- return {
366- explicit : fingerprintParts . length > 0 ,
367- value : fingerprintParts . join ( "|" )
368- } ;
369- }
370-
371- function containsWriteVerb ( text : string ) {
372- return / \b ( s c h e d u l e | p l a n | m o v e | r e s c h e d u l e | s h i f t | c r e a t e | a d d | b o o k | p u t | m a r k | c o m p l e t e | a r c h i v e | c a n c e l | d e l e t e | c h a n g e | u p d a t e ) \b / i. test (
373- text
374- ) ;
375- }
0 commit comments