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.
- checksums.yaml +4 -4
- data/README.md +291 -118
- data/bin/branch-audit.sh +397 -0
- data/bin/n2b-test-github +22 -0
- data/lib/n2b/base.rb +207 -37
- data/lib/n2b/cli.rb +53 -400
- data/lib/n2b/github_client.rb +391 -0
- data/lib/n2b/jira_client.rb +236 -37
- data/lib/n2b/llm/claude.rb +1 -1
- data/lib/n2b/llm/gemini.rb +1 -1
- data/lib/n2b/llm/open_ai.rb +1 -1
- data/lib/n2b/merge_cli.rb +1771 -136
- data/lib/n2b/message_utils.rb +59 -0
- data/lib/n2b/templates/diff_system_prompt.txt +40 -20
- data/lib/n2b/templates/github_comment.txt +67 -0
- data/lib/n2b/templates/jira_comment.txt +7 -0
- data/lib/n2b/templates/merge_conflict_prompt.txt +2 -2
- data/lib/n2b/version.rb +1 -1
- metadata +8 -3
data/lib/n2b/jira_client.rb
CHANGED
@@ -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
|
-
#
|
645
|
-
#
|
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
|
-
#
|
817
|
+
# Start collecting expand content
|
684
818
|
expand_title = $1.strip
|
685
|
-
|
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 -
|
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
|
-
|
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
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
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
|
-
|
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
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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?
|
data/lib/n2b/llm/claude.rb
CHANGED
data/lib/n2b/llm/gemini.rb
CHANGED