@@ -370,8 +370,8 @@ public async Task<Result<ProposalDto>> ParseInstructionAsync(
370370 }
371371
372372 if ( ! operations . Any ( ) )
373- return Result . Failure < ProposalDto > ( ErrorCodes . ValidationError ,
374- "Could not parse instruction. Supported patterns: 'create card \" title \" ', 'move card {id} to column \" name \" ', 'archive card {id}', 'archive cards matching \" pattern \" ', 'update card {id} title/description \" value \" ', 'rename board to \" name \" ', 'update board description \" value \" ', 'archive board', 'unarchive board', 'move column \" name \" to position {n}'" ) ;
373+ return Result . Failure < ProposalDto > ( ErrorCodes . ValidationError ,
374+ BuildParseHintMessage ( instruction ) ) ;
375375
376376 // Classify risk
377377 var operationDtos = operations . Select ( o => new ProposalOperationDto (
@@ -420,6 +420,110 @@ public async Task<Result<ProposalDto>> ParseInstructionAsync(
420420 }
421421 }
422422
423+ internal static readonly string ParseHintMarker = "[PARSE_HINT]" ;
424+
425+ internal static readonly ( string Pattern , string Example , string [ ] Keywords ) [ ] SupportedPatterns = new [ ]
426+ {
427+ ( "create card \" title\" " , "create card \" My new task\" " , new [ ] { "create" , "add" , "new" , "card" , "task" } ) ,
428+ ( "create card \" title\" in column \" name\" " , "create card \" Bug fix\" in column \" In Progress\" " , new [ ] { "create" , "add" , "new" , "card" , "column" , "in" } ) ,
429+ ( "move card {id} to column \" name\" " , "move card abc-123 to column \" Done\" " , new [ ] { "move" , "card" , "column" , "to" } ) ,
430+ ( "archive card {id}" , "archive card abc-123" , new [ ] { "archive" , "card" , "remove" , "delete" } ) ,
431+ ( "archive cards matching \" pattern\" " , "archive cards matching \" old tasks\" " , new [ ] { "archive" , "cards" , "matching" , "bulk" , "batch" } ) ,
432+ ( "update card {id} title \" value\" " , "update card abc-123 title \" New title\" " , new [ ] { "update" , "edit" , "change" , "card" , "title" , "rename" } ) ,
433+ ( "update card {id} description \" value\" " , "update card abc-123 description \" Updated details\" " , new [ ] { "update" , "edit" , "change" , "card" , "description" , "desc" } ) ,
434+ ( "rename board to \" name\" " , "rename board to \" Sprint 5\" " , new [ ] { "rename" , "board" , "name" , "title" } ) ,
435+ ( "update board description \" value\" " , "update board description \" Team workspace\" " , new [ ] { "update" , "board" , "description" , "desc" } ) ,
436+ ( "archive board" , "archive board" , new [ ] { "archive" , "board" } ) ,
437+ ( "unarchive board" , "unarchive board" , new [ ] { "unarchive" , "restore" , "board" } ) ,
438+ ( "move column \" name\" to position {n}" , "move column \" Done\" to position 0" , new [ ] { "move" , "column" , "position" , "reorder" } ) ,
439+ } ;
440+
441+ internal static string BuildParseHintMessage ( string instruction )
442+ {
443+ var detectedIntent = DetectIntent ( instruction ) ;
444+ var bestMatch = FindClosestPattern ( instruction , detectedIntent ) ;
445+
446+ var patterns = SupportedPatterns . Select ( p => p . Pattern ) . ToArray ( ) ;
447+ var hint = new ParseHintPayload (
448+ patterns ,
449+ bestMatch . Example ,
450+ bestMatch . Pattern ,
451+ detectedIntent ) ;
452+
453+ var hintJson = JsonSerializer . Serialize ( hint , new JsonSerializerOptions
454+ {
455+ PropertyNamingPolicy = JsonNamingPolicy . CamelCase
456+ } ) ;
457+
458+ return $ "Could not parse instruction into a proposal.{ ParseHintMarker } { hintJson } ";
459+ }
460+
461+ internal static string ? DetectIntent ( string instruction )
462+ {
463+ var lower = instruction . Trim ( ) . ToLowerInvariant ( ) ;
464+ var words = lower . Split ( ' ' , StringSplitOptions . RemoveEmptyEntries ) ;
465+
466+ // Check more-specific intents before their substrings (e.g. "unarchive" before "archive",
467+ // "rename" before "new"). Use word-level matching to avoid substring false positives
468+ // like "sunset" matching "set" or "address" matching "add".
469+ bool hasWord ( string word ) => words . Any ( w => w == word ) ;
470+
471+ if ( lower . Contains ( "unarchive" ) || hasWord ( "restore" ) )
472+ return "unarchive" ;
473+ if ( lower . Contains ( "rename" ) || hasWord ( "edit" ) || hasWord ( "change" ) || hasWord ( "update" ) )
474+ return "update" ;
475+ if ( hasWord ( "reorder" ) || hasWord ( "position" ) )
476+ return "reorder" ;
477+ if ( hasWord ( "create" ) || hasWord ( "add" ) || hasWord ( "new" ) )
478+ return "create" ;
479+ if ( hasWord ( "move" ) || hasWord ( "drag" ) || hasWord ( "transfer" ) )
480+ return "move" ;
481+ if ( hasWord ( "archive" ) || hasWord ( "remove" ) || hasWord ( "delete" ) )
482+ return "archive" ;
483+
484+ return null ;
485+ }
486+
487+ internal static ( string Pattern , string Example ) FindClosestPattern ( string instruction , string ? detectedIntent )
488+ {
489+ var lower = instruction . Trim ( ) . ToLowerInvariant ( ) ;
490+ var words = lower . Split ( ' ' , StringSplitOptions . RemoveEmptyEntries ) ;
491+
492+ var bestScore = - 1 ;
493+ var bestPattern = SupportedPatterns [ 0 ] ;
494+
495+ foreach ( var entry in SupportedPatterns )
496+ {
497+ var score = 0 ;
498+
499+ // Boost patterns whose keywords match whole words in the instruction
500+ foreach ( var keyword in entry . Keywords )
501+ {
502+ if ( words . Any ( w => w == keyword ) )
503+ score += 2 ;
504+ }
505+
506+ // Extra boost if the detected intent matches the first keyword
507+ if ( detectedIntent != null && entry . Keywords . Length > 0 &&
508+ entry . Keywords [ 0 ] . Equals ( detectedIntent , StringComparison . OrdinalIgnoreCase ) )
509+ score += 5 ;
510+
511+ if ( score > bestScore )
512+ {
513+ bestScore = score ;
514+ bestPattern = entry ;
515+ }
516+ }
517+
518+ return ( bestPattern . Pattern , bestPattern . Example ) ;
519+ }
520+
521+ internal record ParseHintPayload (
522+ string [ ] SupportedPatterns ,
523+ string ExampleInstruction ,
524+ string ClosestPattern ,
525+ string ? DetectedIntent ) ;
526+
423527 private static bool TryResolveCorrelationId ( string ? correlationId , out string resolvedCorrelationId , out string error )
424528 {
425529 if ( correlationId == null )
0 commit comments