11import {
2+ containsWriteVerb ,
23 deriveAmbiguity ,
4+ deriveConsentRequirement ,
35 type CommitPolicyOutput ,
4- type ConversationEntity ,
56 type TurnAmbiguity ,
67 type TurnClassifierOutput ,
7- type TurnInterpretationType ,
88 type TurnPolicyDecision ,
99 type TurnRoutingInput
1010} from "@atlas/core" ;
@@ -147,8 +147,6 @@ function deriveStructuredWriteReadiness(
147147 } ;
148148 }
149149
150- // Bug 4 fix: if this is a clarification answer and the proposal is already confirmed,
151- // skip re-presenting and go straight to execution
152150 if ( classification . turnType === "clarification_answer" ) {
153151 const entityRegistry = input . routingContext . entityRegistry ?? [ ] ;
154152 const alreadyConfirmed = entityRegistry . some (
@@ -166,13 +164,19 @@ function deriveStructuredWriteReadiness(
166164 }
167165 }
168166
169- const consentRequirement = deriveConsentRequirement ( input ) ;
167+ const consentRequirement = deriveConsentRequirement ( {
168+ classification,
169+ entityRegistry : input . routingContext . entityRegistry ?? [ ] ,
170+ normalizedText : input . routingContext . normalizedText
171+ } ) ;
170172
171173 if ( consentRequirement . required ) {
172174 return {
173175 state : "ready_needs_consent" ,
174176 reason : consentRequirement . reason ,
175- ...( consentRequirement . targetProposalId ? { targetProposalId : consentRequirement . targetProposalId } : { } )
177+ ...( consentRequirement . required && "targetProposalId" in consentRequirement
178+ ? { targetProposalId : consentRequirement . targetProposalId }
179+ : { } )
176180 } ;
177181 }
178182
@@ -226,145 +230,3 @@ function buildPolicyFromStructuredReadiness(
226230 } ;
227231 }
228232}
229-
230- function deriveConsentRequirement ( input : DecideTurnPolicyInput ) {
231- const { classification } = input ;
232- // Bug 2 fix: match "presented" status in addition to "active"
233- const activeProposal = ( input . routingContext . entityRegistry ?? [ ] ) . find (
234- ( entity ) : entity is Extract < ConversationEntity , { kind : "proposal_option" } > =>
235- entity . kind === "proposal_option" &&
236- ( entity . status === "active" || entity . status === "presented" ) &&
237- entity . id === classification . resolvedProposalId &&
238- entity . data . confirmationRequired === true
239- ) ;
240-
241- if ( ! activeProposal ) {
242- return {
243- required : false ,
244- reason : "Deterministic product rules do not require additional consent."
245- } ;
246- }
247-
248- if ( ! matchesProposalTarget ( activeProposal . data . targetEntityId ?? null , classification . resolvedEntityIds ) ) {
249- return {
250- required : false ,
251- reason : "Deterministic product rules do not require additional consent."
252- } ;
253- }
254-
255- const compatibility = deriveProposalCompatibility ( input , activeProposal ) ;
256-
257- if ( ! compatibility . compatible ) {
258- return {
259- required : true ,
260- reason : compatibility . reason
261- } ;
262- }
263-
264- return {
265- required : true ,
266- reason : "Write request is ready, but deterministic product policy still requires user consent." ,
267- targetProposalId : activeProposal . id
268- } ;
269- }
270-
271- function matchesProposalTarget ( targetEntityId : string | null , resolvedEntityIds : string [ ] ) {
272- if ( ! targetEntityId || resolvedEntityIds . length === 0 ) {
273- return true ;
274- }
275-
276- return resolvedEntityIds . includes ( targetEntityId ) ;
277- }
278-
279- function deriveProposalCompatibility (
280- input : DecideTurnPolicyInput ,
281- proposal : Extract < ConversationEntity , { kind : "proposal_option" } >
282- ) {
283- if ( input . classification . turnType === "clarification_answer" ) {
284- return {
285- compatible : true ,
286- reason : "Clarification answers may continue the same consent-required proposal."
287- } ;
288- }
289-
290- const currentActionKind = deriveActionKind ( input . routingContext . normalizedText , input . classification . turnType ) ;
291- const proposalActionKind = deriveActionKind (
292- proposal . data . originatingTurnText ?? proposal . data . replyText ,
293- inferProposalTurnType ( proposal )
294- ) ;
295-
296- if ( currentActionKind !== proposalActionKind ) {
297- return {
298- compatible : false ,
299- reason : "The new turn changes the action type, so it needs fresh consent."
300- } ;
301- }
302-
303- const currentFingerprint = deriveParameterFingerprint ( input . routingContext . normalizedText ) ;
304- const proposalFingerprint = deriveParameterFingerprint ( proposal . data . originatingTurnText ?? proposal . data . replyText ) ;
305-
306- if ( currentFingerprint . explicit && proposalFingerprint . explicit && currentFingerprint . value !== proposalFingerprint . value ) {
307- return {
308- compatible : false ,
309- reason : "The new turn changes proposal parameters, so it needs fresh consent."
310- } ;
311- }
312-
313- return {
314- compatible : true ,
315- reason : "The pending proposal still matches the current turn."
316- } ;
317- }
318-
319- function inferProposalTurnType (
320- proposal : Extract < ConversationEntity , { kind : "proposal_option" } >
321- ) : TurnInterpretationType {
322- const source = ( proposal . data . originatingTurnText ?? proposal . data . replyText ) . toLowerCase ( ) ;
323-
324- 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 ) ) {
325- return "edit_request" ;
326- }
327-
328- return "planning_request" ;
329- }
330-
331- function deriveActionKind ( text : string , turnType : TurnInterpretationType ) {
332- if ( turnType === "edit_request" ) {
333- return "edit" ;
334- }
335-
336- if ( turnType === "planning_request" ) {
337- return "plan" ;
338- }
339-
340- const lower = text . toLowerCase ( ) ;
341-
342- 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 ) ) {
343- return "edit" ;
344- }
345-
346- return "plan" ;
347- }
348-
349- function deriveParameterFingerprint ( text : string ) {
350- const lower = text . toLowerCase ( ) ;
351- const dayTokens = lower . match (
352- / \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
353- ) ?? [ ] ;
354- const timeTokens =
355- 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) ?? [ ] ;
356- const durationTokens =
357- 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) ?? [ ] ;
358- const fingerprintParts = [ ...dayTokens , ...timeTokens , ...durationTokens ] . map ( ( part ) => part . trim ( ) ) . sort ( ) ;
359-
360- return {
361- explicit : fingerprintParts . length > 0 ,
362- value : fingerprintParts . join ( "|" )
363- } ;
364- }
365-
366- function containsWriteVerb ( text : string ) {
367- 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 (
368- text
369- ) ;
370- }
0 commit comments