n2b 0.7.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,7 @@ require 'net/http'
2
2
  require 'uri'
3
3
  require 'json'
4
4
  require 'base64'
5
+ require_relative 'template_engine'
5
6
 
6
7
  module N2B
7
8
  class JiraClient
@@ -70,19 +71,41 @@ module N2B
70
71
  # Generate comment using template system
71
72
  template_comment = generate_templated_comment(comment)
72
73
 
74
+ if debug_mode?
75
+ puts "🔍 DEBUG: Generated template comment (#{template_comment.length} chars):"
76
+ puts "--- TEMPLATE COMMENT START ---"
77
+ puts template_comment
78
+ puts "--- TEMPLATE COMMENT END ---"
79
+ end
80
+
73
81
  # Prepare the comment body in Jira's Atlassian Document Format (ADF)
74
82
  comment_body = {
75
83
  "body" => format_comment_as_adf(template_comment)
76
84
  }
77
85
 
86
+ if debug_mode?
87
+ puts "🔍 DEBUG: Formatted ADF comment body:"
88
+ puts "--- ADF BODY START ---"
89
+ puts JSON.pretty_generate(comment_body)
90
+ puts "--- ADF BODY END ---"
91
+ end
92
+
78
93
  # Make the API call to add a comment
79
94
  path = "/rest/api/3/issue/#{ticket_key}/comment"
95
+ puts "🔍 DEBUG: Making API request to: #{path}" if debug_mode?
96
+
80
97
  _response = make_api_request('POST', path, comment_body)
81
98
 
82
99
  puts "✅ Successfully added comment to Jira ticket #{ticket_key}"
83
100
  true
84
101
  rescue JiraApiError => e
85
102
  puts "❌ Failed to update Jira ticket #{ticket_key}: #{e.message}"
103
+ if debug_mode?
104
+ puts "🔍 DEBUG: Full error details:"
105
+ puts " - Ticket key: #{ticket_key}"
106
+ puts " - Template comment length: #{template_comment&.length || 'nil'}"
107
+ puts " - Comment body keys: #{comment_body&.keys || 'nil'}"
108
+ end
86
109
  false
87
110
  end
88
111
 
@@ -400,6 +423,11 @@ module N2B
400
423
  end
401
424
 
402
425
  def generate_templated_comment(comment_data)
426
+ # Handle structured hash data from format_analysis_for_jira
427
+ if comment_data.is_a?(Hash) && comment_data.key?(:implementation_summary)
428
+ return generate_structured_comment(comment_data)
429
+ end
430
+
403
431
  # Prepare template data from the analysis results
404
432
  template_data = prepare_template_data(comment_data)
405
433
 
@@ -412,6 +440,110 @@ module N2B
412
440
  engine.render
413
441
  end
414
442
 
443
+ def generate_structured_comment(data)
444
+ # Generate a properly formatted comment from structured analysis data
445
+ git_info = extract_git_info
446
+ timestamp = Time.now.strftime("%Y-%m-%d %H:%M UTC")
447
+
448
+ comment_parts = []
449
+
450
+ # Header
451
+ comment_parts << "*N2B Code Analysis Report*"
452
+ comment_parts << ""
453
+
454
+ # Implementation Summary (always expanded)
455
+ comment_parts << "*Implementation Summary:*"
456
+ comment_parts << (data[:implementation_summary] || "Unknown")
457
+ comment_parts << ""
458
+
459
+ # Custom message if provided (also expanded)
460
+ if data[:custom_analysis_focus] && !data[:custom_analysis_focus].empty?
461
+ comment_parts << "*Custom Analysis Focus:*"
462
+ comment_parts << data[:custom_analysis_focus]
463
+ comment_parts << ""
464
+ end
465
+
466
+ comment_parts << "---"
467
+ comment_parts << ""
468
+
469
+ # Automated Analysis Findings
470
+ comment_parts << "*Automated Analysis Findings:*"
471
+ comment_parts << ""
472
+
473
+ # Critical Issues (collapsed by default)
474
+ critical_issues = classify_issues_by_severity(data[:issues] || [], 'CRITICAL')
475
+ if critical_issues.any?
476
+ comment_parts << "{expand:🚨 Critical Issues (Must Fix Before Merge)}"
477
+ critical_issues.each { |issue| comment_parts << "☐ #{issue}" }
478
+ comment_parts << "{expand}"
479
+ else
480
+ comment_parts << "✅ No critical issues found"
481
+ end
482
+ comment_parts << ""
483
+
484
+ # Important Issues (collapsed by default)
485
+ important_issues = classify_issues_by_severity(data[:issues] || [], 'IMPORTANT')
486
+ if important_issues.any?
487
+ comment_parts << "{expand:⚠️ Important Issues (Should Address)}"
488
+ important_issues.each { |issue| comment_parts << "☐ #{issue}" }
489
+ comment_parts << "{expand}"
490
+ else
491
+ comment_parts << "✅ No important issues found"
492
+ end
493
+ comment_parts << ""
494
+
495
+ # Suggested Improvements (collapsed by default)
496
+ if data[:improvements] && data[:improvements].any?
497
+ comment_parts << "{expand:💡 Suggested Improvements (Nice to Have)}"
498
+ data[:improvements].each { |improvement| comment_parts << "☐ #{improvement}" }
499
+ comment_parts << "{expand}"
500
+ else
501
+ comment_parts << "✅ No specific improvements suggested"
502
+ end
503
+ comment_parts << ""
504
+
505
+ # Test Coverage Assessment
506
+ comment_parts << "{expand:🧪 Test Coverage Assessment}"
507
+ if data[:test_coverage] && !data[:test_coverage].empty?
508
+ comment_parts << "*Overall Assessment:* #{data[:test_coverage]}"
509
+ else
510
+ comment_parts << "*Overall Assessment:* Not assessed"
511
+ end
512
+ comment_parts << "{expand}"
513
+ comment_parts << ""
514
+
515
+ # Missing Test Coverage
516
+ comment_parts << "*Missing Test Coverage:*"
517
+ comment_parts << "☐ No specific missing tests identified"
518
+ comment_parts << ""
519
+
520
+ # Requirements Evaluation
521
+ comment_parts << "*📋 Requirements Evaluation:*"
522
+ if data[:requirements_evaluation] && !data[:requirements_evaluation].empty?
523
+ comment_parts << "#{data[:requirements_evaluation]}"
524
+ else
525
+ comment_parts << "🔍 *UNCLEAR:* Requirements not provided or assessed"
526
+ end
527
+ comment_parts << ""
528
+
529
+ comment_parts << "---"
530
+ comment_parts << ""
531
+
532
+ # Footer with metadata (simplified)
533
+ comment_parts << "Analysis completed on #{timestamp} | Branch: #{git_info[:branch]}"
534
+
535
+ comment_parts.join("\n")
536
+ end
537
+
538
+ def classify_issues_by_severity(issues, target_severity)
539
+ return [] unless issues.is_a?(Array)
540
+
541
+ issues.select do |issue|
542
+ severity = classify_error_severity(issue)
543
+ severity == target_severity
544
+ end
545
+ end
546
+
415
547
  def prepare_template_data(comment_data)
416
548
  # Handle both string and hash inputs
417
549
  if comment_data.is_a?(String)
@@ -641,15 +773,17 @@ module N2B
641
773
  end
642
774
 
643
775
  def get_config(reconfigure: false, advanced_flow: false)
644
- # This should match the config loading from the main CLI
645
- # For now, return empty hash - will be enhanced when config system is unified
646
- {}
776
+ # Return the config that was passed during initialization
777
+ # This is used for template resolution and other configuration needs
778
+ @config
647
779
  end
648
780
 
649
781
  def convert_markdown_to_adf(markdown_text)
650
782
  content = []
651
783
  lines = markdown_text.split("\n")
652
784
  current_paragraph = []
785
+ current_expand = nil
786
+ expand_content = []
653
787
 
654
788
  lines.each do |line|
655
789
  case line
@@ -680,75 +814,118 @@ module N2B
680
814
  current_paragraph = []
681
815
  end
682
816
 
683
- # Create expand section
817
+ # Start collecting expand content
684
818
  expand_title = $1.strip
685
- content << {
819
+ current_expand = {
686
820
  "type" => "expand",
687
821
  "attrs" => { "title" => expand_title },
688
822
  "content" => []
689
823
  }
824
+ expand_content = []
690
825
  when /^\{expand\}$/ # Jira expand end
691
- # End of expand section - handled by the expand start
826
+ # End of expand section - add collected content
827
+ if current_expand
828
+ current_expand["content"] = expand_content
829
+ content << current_expand if expand_content.any? # Only add if has content
830
+ current_expand = nil
831
+ expand_content = []
832
+ end
692
833
  when /^☐\s+(.+)$/ # Unchecked checkbox
693
834
  # Flush current paragraph
694
835
  if current_paragraph.any?
695
- content << create_paragraph(current_paragraph.join(" "))
836
+ paragraph = create_paragraph(current_paragraph.join(" "))
837
+ if current_expand
838
+ expand_content << paragraph
839
+ else
840
+ content << paragraph
841
+ end
696
842
  current_paragraph = []
697
843
  end
698
844
 
699
- content << {
700
- "type" => "taskList",
701
- "content" => [
702
- {
703
- "type" => "taskItem",
704
- "attrs" => { "state" => "TODO" },
705
- "content" => [
706
- create_paragraph($1.strip)
707
- ]
708
- }
709
- ]
710
- }
845
+ # Convert checkbox to simple paragraph (no bullet points)
846
+ checkbox_paragraph = create_paragraph("" + $1.strip)
847
+
848
+ if current_expand
849
+ expand_content << checkbox_paragraph
850
+ else
851
+ content << checkbox_paragraph
852
+ end
711
853
  when /^☑\s+(.+)$/ # Checked checkbox
712
854
  # Flush current paragraph
713
855
  if current_paragraph.any?
714
- content << create_paragraph(current_paragraph.join(" "))
856
+ paragraph = create_paragraph(current_paragraph.join(" "))
857
+ if current_expand
858
+ expand_content << paragraph
859
+ else
860
+ content << paragraph
861
+ end
715
862
  current_paragraph = []
716
863
  end
717
864
 
718
- content << {
719
- "type" => "taskList",
720
- "content" => [
721
- {
722
- "type" => "taskItem",
723
- "attrs" => { "state" => "DONE" },
724
- "content" => [
725
- create_paragraph($1.strip)
726
- ]
727
- }
728
- ]
729
- }
865
+ # Convert checkbox to simple paragraph (no bullet points)
866
+ checkbox_paragraph = create_paragraph("" + $1.strip)
867
+
868
+ if current_expand
869
+ expand_content << checkbox_paragraph
870
+ else
871
+ content << checkbox_paragraph
872
+ end
730
873
  when /^---$/ # Horizontal rule
731
874
  # Flush current paragraph
732
875
  if current_paragraph.any?
733
- content << create_paragraph(current_paragraph.join(" "))
876
+ paragraph = create_paragraph(current_paragraph.join(" "))
877
+ if current_expand
878
+ expand_content << paragraph
879
+ else
880
+ content << paragraph
881
+ end
734
882
  current_paragraph = []
735
883
  end
736
884
 
737
- content << { "type" => "rule" }
885
+ rule = { "type" => "rule" }
886
+ if current_expand
887
+ expand_content << rule
888
+ else
889
+ content << rule
890
+ end
738
891
  when "" # Empty line
739
892
  # Flush current paragraph
740
893
  if current_paragraph.any?
741
- content << create_paragraph(current_paragraph.join(" "))
894
+ paragraph = create_paragraph(current_paragraph.join(" "))
895
+ if current_expand
896
+ expand_content << paragraph
897
+ else
898
+ content << paragraph
899
+ end
742
900
  current_paragraph = []
743
901
  end
744
902
  else # Regular text
745
- current_paragraph << line
903
+ # Skip empty or whitespace-only content
904
+ unless line.strip.empty? || line.strip == "{}"
905
+ current_paragraph << line
906
+ end
746
907
  end
747
908
  end
748
909
 
749
910
  # Flush any remaining paragraph
750
911
  if current_paragraph.any?
751
- content << create_paragraph(current_paragraph.join(" "))
912
+ paragraph = create_paragraph(current_paragraph.join(" "))
913
+ if current_expand
914
+ expand_content << paragraph
915
+ else
916
+ content << paragraph
917
+ end
918
+ end
919
+
920
+ # Close any remaining expand section
921
+ if current_expand && expand_content.any?
922
+ current_expand["content"] = expand_content
923
+ content << current_expand
924
+ end
925
+
926
+ # Ensure we have at least one content element
927
+ if content.empty?
928
+ content << create_paragraph("Analysis completed.")
752
929
  end
753
930
 
754
931
  {
@@ -772,6 +949,10 @@ module N2B
772
949
 
773
950
  private
774
951
 
952
+ def debug_mode?
953
+ ENV['N2B_DEBUG'] == 'true'
954
+ end
955
+
775
956
  def format_comment_as_adf(comment_data)
776
957
  # If comment_data is a string (from template), convert to simple ADF
777
958
  if comment_data.is_a?(String)
@@ -1096,8 +1277,26 @@ module N2B
1096
1277
  request['Content-Type'] = 'application/json'
1097
1278
  request['Accept'] = 'application/json'
1098
1279
 
1280
+ if debug_mode?
1281
+ puts "🔍 DEBUG: Making #{method} request to: #{full_url}"
1282
+ puts "🔍 DEBUG: Request headers: Content-Type=#{request['Content-Type']}, Accept=#{request['Accept']}"
1283
+ if body
1284
+ puts "🔍 DEBUG: Request body size: #{body.to_json.length} bytes"
1285
+ puts "🔍 DEBUG: Request body preview: #{body.to_json[0..500]}#{'...' if body.to_json.length > 500}"
1286
+ end
1287
+ end
1288
+
1099
1289
  response = http.request(request)
1100
1290
 
1291
+ if debug_mode?
1292
+ puts "🔍 DEBUG: Response code: #{response.code} #{response.message}"
1293
+ if response.body && !response.body.empty?
1294
+ # Force UTF-8 encoding to handle character encoding issues
1295
+ response_body = response.body.force_encoding('UTF-8')
1296
+ puts "🔍 DEBUG: Response body: #{response_body}"
1297
+ end
1298
+ end
1299
+
1101
1300
  unless response.is_a?(Net::HTTPSuccess)
1102
1301
  error_message = "Jira API Error: #{response.code} #{response.message}"
1103
1302
  error_message += " - #{response.body}" if response.body && !response.body.empty?
@@ -1,6 +1,6 @@
1
1
  require_relative '../model_config'
2
2
 
3
- module N2M
3
+ module N2B
4
4
  module Llm
5
5
  class Claude
6
6
  API_URI = URI.parse('https://api.anthropic.com/v1/messages')
@@ -3,7 +3,7 @@ require 'json'
3
3
  require 'uri'
4
4
  require_relative '../model_config'
5
5
 
6
- module N2M
6
+ module N2B
7
7
  module Llm
8
8
  class Gemini
9
9
  API_URI = URI.parse('https://generativelanguage.googleapis.com/v1beta/models')
@@ -3,7 +3,7 @@ require 'json'
3
3
  require 'uri'
4
4
  require_relative '../model_config'
5
5
 
6
- module N2M
6
+ module N2B
7
7
  module Llm
8
8
  class OpenAi
9
9
  API_URI = URI.parse('https://api.openai.com/v1/chat/completions')