n2b 0.5.1 → 0.7.1
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 +374 -23
- data/bin/n2b-diff +5 -0
- data/bin/n2b-test-jira +17 -14
- data/lib/n2b/cli.rb +137 -72
- data/lib/n2b/jira_client.rb +377 -17
- data/lib/n2b/llm/claude.rb +6 -19
- data/lib/n2b/llm/gemini.rb +8 -7
- data/lib/n2b/llm/open_ai.rb +26 -20
- data/lib/n2b/merge_cli.rb +694 -0
- data/lib/n2b/merge_conflict_parser.rb +70 -0
- data/lib/n2b/template_engine.rb +105 -0
- data/lib/n2b/templates/diff_json_instruction.txt +27 -0
- data/lib/n2b/templates/diff_system_prompt.txt +23 -0
- data/lib/n2b/templates/jira_comment.txt +67 -0
- data/lib/n2b/templates/merge_conflict_prompt.txt +20 -0
- data/lib/n2b/version.rb +1 -1
- data/lib/n2b.rb +3 -0
- metadata +34 -7
data/lib/n2b/cli.rb
CHANGED
@@ -316,32 +316,9 @@ module N2B
|
|
316
316
|
end
|
317
317
|
end
|
318
318
|
|
319
|
-
def build_diff_analysis_prompt(diff_output, user_prompt_addition = "", requirements_content = nil)
|
320
|
-
|
321
|
-
|
322
|
-
Your task is to provide a constructive and detailed analysis of the changes.
|
323
|
-
Focus on identifying potential bugs, suggesting improvements in code quality, style, performance, and security.
|
324
|
-
Also, provide a concise summary of the changes.
|
325
|
-
|
326
|
-
IMPORTANT: When referring to specific issues or improvements, always include:
|
327
|
-
- The exact file path (e.g., "lib/n2b/cli.rb")
|
328
|
-
- The specific line numbers or line ranges (e.g., "line 42" or "lines 15-20")
|
329
|
-
- The exact code snippet you're referring to when possible
|
330
|
-
|
331
|
-
This helps users quickly locate and understand the issues you identify.
|
332
|
-
|
333
|
-
SPECIAL FOCUS ON TEST COVERAGE:
|
334
|
-
Pay special attention to whether the developer has provided adequate test coverage for the changes:
|
335
|
-
- Look for new test files or modifications to existing test files
|
336
|
-
- Check if new functionality has corresponding tests
|
337
|
-
- Evaluate if edge cases and error conditions are tested
|
338
|
-
- Assess if the tests are meaningful and comprehensive
|
339
|
-
- Note any missing test coverage that should be added
|
340
|
-
|
341
|
-
NOTE: In addition to the diff, you will also receive the current code context around the changed areas.
|
342
|
-
This provides better understanding of the surrounding code and helps with more accurate analysis.
|
343
|
-
The user may provide additional instructions or specific requirements below.
|
344
|
-
SYSTEM_PROMPT
|
319
|
+
def build_diff_analysis_prompt(diff_output, user_prompt_addition = "", requirements_content = nil, config = {})
|
320
|
+
default_system_prompt_path = resolve_template_path('diff_system_prompt', config)
|
321
|
+
default_system_prompt = File.read(default_system_prompt_path).strip
|
345
322
|
|
346
323
|
user_instructions_section = ""
|
347
324
|
unless user_prompt_addition.to_s.strip.empty?
|
@@ -382,35 +359,8 @@ REQUIREMENTS_BLOCK
|
|
382
359
|
end
|
383
360
|
end
|
384
361
|
|
385
|
-
|
386
|
-
|
387
|
-
Do not include any explanatory text before or after the JSON.
|
388
|
-
Each error and improvement should include specific file paths and line numbers.
|
389
|
-
|
390
|
-
The JSON object must contain the following keys:
|
391
|
-
- "summary": (string) Brief overall description of the changes.
|
392
|
-
- "ticket_implementation_summary": (string) A concise summary of what was implemented or achieved in relation to the ticket's goals, based *only* on the provided diff. This is for developer status updates and Jira comments.
|
393
|
-
- "errors": (list of strings) Potential bugs or issues found.
|
394
|
-
- "improvements": (list of strings) Suggestions for code quality, style, performance, or security.
|
395
|
-
- "test_coverage": (string) Assessment of test coverage for the changes.
|
396
|
-
- "requirements_evaluation": (string, include only if requirements were provided in the prompt) Evaluation of how the changes meet the provided requirements.
|
397
|
-
|
398
|
-
Example format:
|
399
|
-
{
|
400
|
-
"summary": "Refactored the user authentication module and added password complexity checks.",
|
401
|
-
"ticket_implementation_summary": "Implemented the core logic for user password updates and strengthened security by adding complexity validation as per the ticket's primary goal. Some UI elements are pending.",
|
402
|
-
"errors": [
|
403
|
-
"lib/example.rb line 42: Potential null pointer exception when accessing user.name without checking if user is nil.",
|
404
|
-
"src/main.js lines 15-20: Missing error handling for async operation."
|
405
|
-
],
|
406
|
-
"improvements": [
|
407
|
-
"lib/example.rb line 30: Consider using a constant for the magic number 42.",
|
408
|
-
"src/utils.py lines 5-10: This method could be simplified using list comprehension."
|
409
|
-
],
|
410
|
-
"test_coverage": "Good: New functionality in lib/example.rb has corresponding tests in test/example_test.rb. Missing: No tests for error handling edge cases in the new validation method.",
|
411
|
-
"requirements_evaluation": "✅ IMPLEMENTED: User authentication feature is fully implemented in auth.rb. ⚠️ PARTIALLY IMPLEMENTED: Error handling is present but lacks specific error codes. ❌ NOT IMPLEMENTED: Email notifications are not addressed in this diff."
|
412
|
-
}
|
413
|
-
JSON_INSTRUCTION
|
362
|
+
json_instruction_path = resolve_template_path('diff_json_instruction', config)
|
363
|
+
json_instruction = File.read(json_instruction_path).strip
|
414
364
|
|
415
365
|
full_prompt = [
|
416
366
|
default_system_prompt,
|
@@ -426,7 +376,7 @@ JSON_INSTRUCTION
|
|
426
376
|
end
|
427
377
|
|
428
378
|
def analyze_diff(diff_output, config, user_prompt_addition = "", requirements_content = nil)
|
429
|
-
prompt = build_diff_analysis_prompt(diff_output, user_prompt_addition, requirements_content)
|
379
|
+
prompt = build_diff_analysis_prompt(diff_output, user_prompt_addition, requirements_content, config)
|
430
380
|
analysis_json_str = call_llm_for_diff_analysis(prompt, config)
|
431
381
|
|
432
382
|
begin
|
@@ -607,7 +557,8 @@ JSON_INSTRUCTION
|
|
607
557
|
raise N2B::Error, "Unsupported LLM service: #{llm_service_name}"
|
608
558
|
end
|
609
559
|
|
610
|
-
|
560
|
+
puts "🔍 AI is analyzing your code diff..."
|
561
|
+
response_json_str = analyze_diff_with_spinner(llm, prompt)
|
611
562
|
response_json_str
|
612
563
|
rescue N2B::LlmApiError => e # This catches errors from analyze_code_diff
|
613
564
|
puts "Error communicating with the LLM: #{e.message}"
|
@@ -667,23 +618,37 @@ JSON_INSTRUCTION
|
|
667
618
|
EOF
|
668
619
|
|
669
620
|
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
#
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
621
|
+
puts "🤖 AI is generating commands..."
|
622
|
+
response = make_request_with_spinner(llm, content)
|
623
|
+
|
624
|
+
# Handle both Hash (from JSON mode providers) and String responses
|
625
|
+
if response.is_a?(Hash)
|
626
|
+
# Already parsed by the LLM provider
|
627
|
+
parsed_response = response
|
628
|
+
response_str = response.to_json # For history logging
|
629
|
+
else
|
630
|
+
# String response that needs parsing
|
631
|
+
response_str = response
|
679
632
|
begin
|
680
|
-
parsed_response = JSON.parse(
|
681
|
-
parsed_response
|
633
|
+
parsed_response = JSON.parse(response_str)
|
682
634
|
rescue JSON::ParserError => e
|
683
|
-
puts "
|
684
|
-
|
685
|
-
|
635
|
+
puts "⚠️ Invalid JSON detected, attempting automatic repair..."
|
636
|
+
repaired_response = attempt_json_repair_for_commands(response_str, llm)
|
637
|
+
|
638
|
+
if repaired_response
|
639
|
+
puts "✅ JSON repair successful!"
|
640
|
+
parsed_response = repaired_response
|
641
|
+
else
|
642
|
+
puts "❌ JSON repair failed"
|
643
|
+
puts "Error parsing LLM response JSON for command generation: #{e.message}"
|
644
|
+
# This is a fallback for when the LLM response *content* is not valid JSON.
|
645
|
+
parsed_response = { "commands" => ["echo 'Error: LLM returned invalid JSON content.'"], "explanation" => "The response from the language model was not valid JSON." }
|
646
|
+
end
|
686
647
|
end
|
648
|
+
end
|
649
|
+
|
650
|
+
append_to_llm_history_file("#{prompt}\n#{response_str}") # Storing the response for history
|
651
|
+
parsed_response
|
687
652
|
rescue N2B::LlmApiError => e
|
688
653
|
puts "Error communicating with the LLM: #{e.message}"
|
689
654
|
|
@@ -784,7 +749,102 @@ JSON_INSTRUCTION
|
|
784
749
|
end
|
785
750
|
system("history -r") # Attempt to reload history in current session
|
786
751
|
end
|
787
|
-
|
752
|
+
|
753
|
+
def resolve_template_path(template_key, config)
|
754
|
+
user_path = config.dig('templates', template_key) if config.is_a?(Hash)
|
755
|
+
return user_path if user_path && File.exist?(user_path)
|
756
|
+
|
757
|
+
File.expand_path(File.join(__dir__, 'templates', "#{template_key}.txt"))
|
758
|
+
end
|
759
|
+
|
760
|
+
def make_request_with_spinner(llm, content)
|
761
|
+
spinner_chars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
762
|
+
spinner_thread = Thread.new do
|
763
|
+
i = 0
|
764
|
+
while true
|
765
|
+
print "\r⠿ #{spinner_chars[i % spinner_chars.length]} Processing..."
|
766
|
+
$stdout.flush
|
767
|
+
sleep(0.1)
|
768
|
+
i += 1
|
769
|
+
end
|
770
|
+
end
|
771
|
+
|
772
|
+
begin
|
773
|
+
result = llm.make_request(content)
|
774
|
+
spinner_thread.kill
|
775
|
+
print "\r#{' ' * 25}\r" # Clear the spinner line
|
776
|
+
puts "✅ Commands generated!"
|
777
|
+
result
|
778
|
+
rescue => e
|
779
|
+
spinner_thread.kill
|
780
|
+
print "\r#{' ' * 25}\r" # Clear the spinner line
|
781
|
+
raise e
|
782
|
+
end
|
783
|
+
end
|
784
|
+
|
785
|
+
def analyze_diff_with_spinner(llm, prompt)
|
786
|
+
spinner_chars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
787
|
+
spinner_thread = Thread.new do
|
788
|
+
i = 0
|
789
|
+
while true
|
790
|
+
print "\r🔍 #{spinner_chars[i % spinner_chars.length]} Analyzing diff..."
|
791
|
+
$stdout.flush
|
792
|
+
sleep(0.1)
|
793
|
+
i += 1
|
794
|
+
end
|
795
|
+
end
|
796
|
+
|
797
|
+
begin
|
798
|
+
result = llm.analyze_code_diff(prompt)
|
799
|
+
spinner_thread.kill
|
800
|
+
print "\r#{' ' * 30}\r" # Clear the spinner line
|
801
|
+
puts "✅ Diff analysis complete!"
|
802
|
+
result
|
803
|
+
rescue => e
|
804
|
+
spinner_thread.kill
|
805
|
+
print "\r#{' ' * 30}\r" # Clear the spinner line
|
806
|
+
raise e
|
807
|
+
end
|
808
|
+
end
|
809
|
+
|
810
|
+
def attempt_json_repair_for_commands(malformed_response, llm)
|
811
|
+
repair_prompt = <<~PROMPT
|
812
|
+
The following response was supposed to be valid JSON with keys "commands" (array) and "explanation" (string), but it has formatting issues. Please fix it and return ONLY the corrected JSON:
|
813
|
+
|
814
|
+
Original response:
|
815
|
+
#{malformed_response}
|
816
|
+
|
817
|
+
Requirements:
|
818
|
+
- Must be valid JSON
|
819
|
+
- Must have "commands" key with array of command strings
|
820
|
+
- Must have "explanation" key with explanation text
|
821
|
+
- Return ONLY the JSON, no other text
|
822
|
+
|
823
|
+
Fixed JSON:
|
824
|
+
PROMPT
|
825
|
+
|
826
|
+
begin
|
827
|
+
puts "🔧 Asking AI to fix the JSON..."
|
828
|
+
repaired_json_str = llm.make_request(repair_prompt)
|
829
|
+
|
830
|
+
# Handle both Hash and String responses
|
831
|
+
if repaired_json_str.is_a?(Hash)
|
832
|
+
repaired_response = repaired_json_str
|
833
|
+
else
|
834
|
+
repaired_response = JSON.parse(repaired_json_str)
|
835
|
+
end
|
836
|
+
|
837
|
+
# Validate the repaired response structure
|
838
|
+
if repaired_response.is_a?(Hash) && repaired_response.key?('commands') && repaired_response.key?('explanation')
|
839
|
+
return repaired_response
|
840
|
+
else
|
841
|
+
return nil
|
842
|
+
end
|
843
|
+
rescue JSON::ParserError, StandardError
|
844
|
+
return nil
|
845
|
+
end
|
846
|
+
end
|
847
|
+
|
788
848
|
|
789
849
|
def parse_options
|
790
850
|
options = {
|
@@ -834,6 +894,11 @@ JSON_INSTRUCTION
|
|
834
894
|
exit
|
835
895
|
end
|
836
896
|
|
897
|
+
opts.on('-v', '--version', 'Show version') do
|
898
|
+
puts "n2b version #{N2B::VERSION}"
|
899
|
+
exit
|
900
|
+
end
|
901
|
+
|
837
902
|
opts.on('-c', '--config', 'Configure the API key and model') do
|
838
903
|
options[:config] = true
|
839
904
|
end
|
data/lib/n2b/jira_client.rb
CHANGED
@@ -67,9 +67,12 @@ module N2B
|
|
67
67
|
|
68
68
|
puts "Updating Jira ticket #{ticket_key} with analysis comment..."
|
69
69
|
|
70
|
+
# Generate comment using template system
|
71
|
+
template_comment = generate_templated_comment(comment)
|
72
|
+
|
70
73
|
# Prepare the comment body in Jira's Atlassian Document Format (ADF)
|
71
74
|
comment_body = {
|
72
|
-
"body" => format_comment_as_adf(
|
75
|
+
"body" => format_comment_as_adf(template_comment)
|
73
76
|
}
|
74
77
|
|
75
78
|
# Make the API call to add a comment
|
@@ -396,26 +399,383 @@ module N2B
|
|
396
399
|
end
|
397
400
|
end
|
398
401
|
|
399
|
-
|
402
|
+
def generate_templated_comment(comment_data)
|
403
|
+
# Prepare template data from the analysis results
|
404
|
+
template_data = prepare_template_data(comment_data)
|
400
405
|
|
401
|
-
|
402
|
-
|
406
|
+
# Load and render template
|
407
|
+
config = get_config(reconfigure: false, advanced_flow: false)
|
408
|
+
template_path = resolve_template_path('jira_comment', config)
|
409
|
+
template_content = File.read(template_path)
|
410
|
+
|
411
|
+
engine = N2B::TemplateEngine.new(template_content, template_data)
|
412
|
+
engine.render
|
413
|
+
end
|
414
|
+
|
415
|
+
def prepare_template_data(comment_data)
|
416
|
+
# Handle both string and hash inputs
|
403
417
|
if comment_data.is_a?(String)
|
418
|
+
# For simple string comments, create a basic template data structure
|
419
|
+
git_info = extract_git_info
|
404
420
|
return {
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
421
|
+
'implementation_summary' => comment_data,
|
422
|
+
'critical_errors' => [],
|
423
|
+
'important_errors' => [],
|
424
|
+
'improvements' => [],
|
425
|
+
'missing_tests' => [],
|
426
|
+
'requirements' => [],
|
427
|
+
'test_coverage_summary' => "No specific test coverage analysis available",
|
428
|
+
'timestamp' => Time.now.strftime("%Y-%m-%d %H:%M UTC"),
|
429
|
+
'branch_name' => git_info[:branch],
|
430
|
+
'files_changed' => git_info[:files_changed],
|
431
|
+
'lines_added' => git_info[:lines_added],
|
432
|
+
'lines_removed' => git_info[:lines_removed],
|
433
|
+
'critical_errors_empty' => true,
|
434
|
+
'important_errors_empty' => true,
|
435
|
+
'improvements_empty' => true,
|
436
|
+
'missing_tests_empty' => true
|
437
|
+
}
|
438
|
+
end
|
439
|
+
|
440
|
+
# Handle hash input (structured analysis data)
|
441
|
+
# Extract and classify errors by severity
|
442
|
+
errors = comment_data[:issues] || comment_data['issues'] || []
|
443
|
+
critical_errors = []
|
444
|
+
important_errors = []
|
445
|
+
low_errors = []
|
446
|
+
|
447
|
+
errors.each do |error|
|
448
|
+
severity = classify_error_severity(error)
|
449
|
+
file_ref = extract_file_reference(error)
|
450
|
+
|
451
|
+
error_item = {
|
452
|
+
'file_reference' => file_ref,
|
453
|
+
'description' => clean_error_description(error),
|
454
|
+
'severity' => severity
|
418
455
|
}
|
456
|
+
|
457
|
+
case severity
|
458
|
+
when 'CRITICAL'
|
459
|
+
critical_errors << error_item
|
460
|
+
when 'IMPORTANT'
|
461
|
+
important_errors << error_item
|
462
|
+
else
|
463
|
+
low_errors << error_item
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
# Process improvements
|
468
|
+
improvements = (comment_data[:improvements] || comment_data['improvements'] || []).map do |improvement|
|
469
|
+
{
|
470
|
+
'file_reference' => extract_file_reference(improvement),
|
471
|
+
'description' => clean_error_description(improvement)
|
472
|
+
}
|
473
|
+
end
|
474
|
+
|
475
|
+
# Process missing tests
|
476
|
+
missing_tests = extract_missing_tests(comment_data[:test_coverage] || comment_data['test_coverage'] || "")
|
477
|
+
|
478
|
+
# Process requirements
|
479
|
+
requirements = extract_requirements_status(comment_data[:requirements_evaluation] || comment_data['requirements_evaluation'] || "")
|
480
|
+
|
481
|
+
# Get git/hg info
|
482
|
+
git_info = extract_git_info
|
483
|
+
|
484
|
+
{
|
485
|
+
'implementation_summary' => comment_data[:implementation_summary] || comment_data['implementation_summary'] || "Code analysis completed",
|
486
|
+
'critical_errors' => critical_errors,
|
487
|
+
'important_errors' => important_errors,
|
488
|
+
'improvements' => improvements,
|
489
|
+
'missing_tests' => missing_tests,
|
490
|
+
'requirements' => requirements,
|
491
|
+
'test_coverage_summary' => comment_data[:test_coverage] || comment_data['test_coverage'] || "No specific test coverage analysis available",
|
492
|
+
'timestamp' => Time.now.strftime("%Y-%m-%d %H:%M UTC"),
|
493
|
+
'branch_name' => git_info[:branch],
|
494
|
+
'files_changed' => git_info[:files_changed],
|
495
|
+
'lines_added' => git_info[:lines_added],
|
496
|
+
'lines_removed' => git_info[:lines_removed],
|
497
|
+
'critical_errors_empty' => critical_errors.empty?,
|
498
|
+
'important_errors_empty' => important_errors.empty?,
|
499
|
+
'improvements_empty' => improvements.empty?,
|
500
|
+
'missing_tests_empty' => missing_tests.empty?
|
501
|
+
}
|
502
|
+
end
|
503
|
+
|
504
|
+
def classify_error_severity(error_text)
|
505
|
+
text = error_text.downcase
|
506
|
+
case text
|
507
|
+
when /security|sql injection|xss|csrf|vulnerability|exploit|attack/
|
508
|
+
'CRITICAL'
|
509
|
+
when /performance|n\+1|timeout|memory leak|slow query|bottleneck/
|
510
|
+
'IMPORTANT'
|
511
|
+
when /error|exception|bug|fail|crash|break/
|
512
|
+
'IMPORTANT'
|
513
|
+
when /style|convention|naming|format|indent|space/
|
514
|
+
'LOW'
|
515
|
+
else
|
516
|
+
'IMPORTANT'
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
520
|
+
def extract_file_reference(text)
|
521
|
+
# Parse various file reference formats
|
522
|
+
if match = text.match(/(\S+\.(?:rb|js|py|java|cpp|c|h|ts|jsx|tsx|php|go|rs|swift|kt))(?:\s+(?:line|lines?)\s+(\d+(?:-\d+)?)|:(\d+(?:-\d+)?)|\s*\(line\s+(\d+)\))?/i)
|
523
|
+
file = match[1]
|
524
|
+
line = match[2] || match[3] || match[4]
|
525
|
+
line ? "*#{file}:#{line}*" : "*#{file}*"
|
526
|
+
else
|
527
|
+
"*General*"
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
def clean_error_description(text)
|
532
|
+
# Remove file references from description to avoid duplication
|
533
|
+
text.gsub(/\S+\.(?:rb|js|py|java|cpp|c|h|ts|jsx|tsx|php|go|rs|swift|kt)(?:\s+(?:line|lines?)\s+\d+(?:-\d+)?|:\d+(?:-\d+)?|\s*\(line\s+\d+\))?:?\s*/i, '').strip
|
534
|
+
end
|
535
|
+
|
536
|
+
def extract_missing_tests(test_coverage_text)
|
537
|
+
# Extract test-related items from coverage analysis
|
538
|
+
missing_tests = []
|
539
|
+
|
540
|
+
# Look for common patterns indicating missing tests
|
541
|
+
test_coverage_text.scan(/(?:missing|need|add|require).*?test.*?(?:\.|$)/i) do |match|
|
542
|
+
missing_tests << { 'description' => match.strip }
|
543
|
+
end
|
544
|
+
|
545
|
+
# If no specific missing tests found, create generic ones based on coverage
|
546
|
+
if missing_tests.empty? && test_coverage_text.include?('%')
|
547
|
+
if coverage_match = test_coverage_text.match(/(\d+)%/)
|
548
|
+
coverage = coverage_match[1].to_i
|
549
|
+
if coverage < 80
|
550
|
+
missing_tests << { 'description' => "Increase test coverage from #{coverage}% to target 80%+" }
|
551
|
+
end
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
missing_tests
|
556
|
+
end
|
557
|
+
|
558
|
+
def extract_requirements_status(requirements_text)
|
559
|
+
requirements = []
|
560
|
+
|
561
|
+
# Split by lines and process each line
|
562
|
+
requirements_text.split("\n").each do |line|
|
563
|
+
line = line.strip
|
564
|
+
next if line.empty?
|
565
|
+
|
566
|
+
# Parse requirements with status indicators - order matters for regex matching
|
567
|
+
if match = line.match(/(✅|⚠️|❌|🔍)?\s*(PARTIALLY\s+IMPLEMENTED|NOT\s+IMPLEMENTED|IMPLEMENTED|UNCLEAR)?:?\s*(.+)/i)
|
568
|
+
status_emoji, status_text, description = match.captures
|
569
|
+
status = case
|
570
|
+
when status_text&.include?('PARTIALLY')
|
571
|
+
'PARTIALLY_IMPLEMENTED'
|
572
|
+
when status_text&.include?('NOT')
|
573
|
+
'NOT_IMPLEMENTED'
|
574
|
+
when status_emoji == '✅' || (status_text&.include?('IMPLEMENTED') && !status_text&.include?('NOT') && !status_text&.include?('PARTIALLY'))
|
575
|
+
'IMPLEMENTED'
|
576
|
+
when status_emoji == '⚠️'
|
577
|
+
'PARTIALLY_IMPLEMENTED'
|
578
|
+
when status_emoji == '❌'
|
579
|
+
'NOT_IMPLEMENTED'
|
580
|
+
else
|
581
|
+
'UNCLEAR'
|
582
|
+
end
|
583
|
+
|
584
|
+
requirements << {
|
585
|
+
'status' => status,
|
586
|
+
'description' => description.strip,
|
587
|
+
'status_icon' => status_emoji || (status == 'IMPLEMENTED' ? '✅' : status == 'PARTIALLY_IMPLEMENTED' ? '⚠️' : status == 'NOT_IMPLEMENTED' ? '❌' : '🔍')
|
588
|
+
}
|
589
|
+
end
|
590
|
+
end
|
591
|
+
|
592
|
+
requirements
|
593
|
+
end
|
594
|
+
|
595
|
+
def extract_git_info
|
596
|
+
begin
|
597
|
+
if File.exist?('.git')
|
598
|
+
branch = `git branch --show-current 2>/dev/null`.strip
|
599
|
+
branch = 'unknown' if branch.empty?
|
600
|
+
|
601
|
+
# Get diff stats
|
602
|
+
diff_stats = `git diff --stat HEAD~1 2>/dev/null`.strip
|
603
|
+
files_changed = diff_stats.scan(/(\d+) files? changed/).flatten.first || "0"
|
604
|
+
lines_added = diff_stats.scan(/(\d+) insertions?/).flatten.first || "0"
|
605
|
+
lines_removed = diff_stats.scan(/(\d+) deletions?/).flatten.first || "0"
|
606
|
+
elsif File.exist?('.hg')
|
607
|
+
branch = `hg branch 2>/dev/null`.strip
|
608
|
+
branch = 'default' if branch.empty?
|
609
|
+
|
610
|
+
# Get diff stats for hg
|
611
|
+
diff_stats = `hg diff --stat 2>/dev/null`.strip
|
612
|
+
files_changed = diff_stats.lines.count.to_s
|
613
|
+
lines_added = "0" # hg diff --stat doesn't show +/- easily
|
614
|
+
lines_removed = "0"
|
615
|
+
else
|
616
|
+
branch = 'unknown'
|
617
|
+
files_changed = "0"
|
618
|
+
lines_added = "0"
|
619
|
+
lines_removed = "0"
|
620
|
+
end
|
621
|
+
rescue
|
622
|
+
branch = 'unknown'
|
623
|
+
files_changed = "0"
|
624
|
+
lines_added = "0"
|
625
|
+
lines_removed = "0"
|
626
|
+
end
|
627
|
+
|
628
|
+
{
|
629
|
+
branch: branch,
|
630
|
+
files_changed: files_changed,
|
631
|
+
lines_added: lines_added,
|
632
|
+
lines_removed: lines_removed
|
633
|
+
}
|
634
|
+
end
|
635
|
+
|
636
|
+
def resolve_template_path(template_key, config)
|
637
|
+
user_path = config.dig('templates', template_key) if config.is_a?(Hash)
|
638
|
+
return user_path if user_path && File.exist?(user_path)
|
639
|
+
|
640
|
+
File.expand_path(File.join(__dir__, 'templates', "#{template_key}.txt"))
|
641
|
+
end
|
642
|
+
|
643
|
+
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
|
+
{}
|
647
|
+
end
|
648
|
+
|
649
|
+
def convert_markdown_to_adf(markdown_text)
|
650
|
+
content = []
|
651
|
+
lines = markdown_text.split("\n")
|
652
|
+
current_paragraph = []
|
653
|
+
|
654
|
+
lines.each do |line|
|
655
|
+
case line
|
656
|
+
when /^\*(.+)\*$/ # Bold headers like *N2B Code Analysis Report*
|
657
|
+
# Flush current paragraph
|
658
|
+
if current_paragraph.any?
|
659
|
+
content << create_paragraph(current_paragraph.join(" "))
|
660
|
+
current_paragraph = []
|
661
|
+
end
|
662
|
+
|
663
|
+
content << {
|
664
|
+
"type" => "heading",
|
665
|
+
"attrs" => { "level" => 2 },
|
666
|
+
"content" => [
|
667
|
+
{
|
668
|
+
"type" => "text",
|
669
|
+
"text" => $1.strip,
|
670
|
+
"marks" => [{ "type" => "strong" }]
|
671
|
+
}
|
672
|
+
]
|
673
|
+
}
|
674
|
+
when /^=+$/ # Separator lines
|
675
|
+
# Skip separator lines
|
676
|
+
when /^\{expand:(.+)\}$/ # Jira expand start
|
677
|
+
# Flush current paragraph
|
678
|
+
if current_paragraph.any?
|
679
|
+
content << create_paragraph(current_paragraph.join(" "))
|
680
|
+
current_paragraph = []
|
681
|
+
end
|
682
|
+
|
683
|
+
# Create expand section
|
684
|
+
expand_title = $1.strip
|
685
|
+
content << {
|
686
|
+
"type" => "expand",
|
687
|
+
"attrs" => { "title" => expand_title },
|
688
|
+
"content" => []
|
689
|
+
}
|
690
|
+
when /^\{expand\}$/ # Jira expand end
|
691
|
+
# End of expand section - handled by the expand start
|
692
|
+
when /^☐\s+(.+)$/ # Unchecked checkbox
|
693
|
+
# Flush current paragraph
|
694
|
+
if current_paragraph.any?
|
695
|
+
content << create_paragraph(current_paragraph.join(" "))
|
696
|
+
current_paragraph = []
|
697
|
+
end
|
698
|
+
|
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
|
+
}
|
711
|
+
when /^☑\s+(.+)$/ # Checked checkbox
|
712
|
+
# Flush current paragraph
|
713
|
+
if current_paragraph.any?
|
714
|
+
content << create_paragraph(current_paragraph.join(" "))
|
715
|
+
current_paragraph = []
|
716
|
+
end
|
717
|
+
|
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
|
+
}
|
730
|
+
when /^---$/ # Horizontal rule
|
731
|
+
# Flush current paragraph
|
732
|
+
if current_paragraph.any?
|
733
|
+
content << create_paragraph(current_paragraph.join(" "))
|
734
|
+
current_paragraph = []
|
735
|
+
end
|
736
|
+
|
737
|
+
content << { "type" => "rule" }
|
738
|
+
when "" # Empty line
|
739
|
+
# Flush current paragraph
|
740
|
+
if current_paragraph.any?
|
741
|
+
content << create_paragraph(current_paragraph.join(" "))
|
742
|
+
current_paragraph = []
|
743
|
+
end
|
744
|
+
else # Regular text
|
745
|
+
current_paragraph << line
|
746
|
+
end
|
747
|
+
end
|
748
|
+
|
749
|
+
# Flush any remaining paragraph
|
750
|
+
if current_paragraph.any?
|
751
|
+
content << create_paragraph(current_paragraph.join(" "))
|
752
|
+
end
|
753
|
+
|
754
|
+
{
|
755
|
+
"type" => "doc",
|
756
|
+
"version" => 1,
|
757
|
+
"content" => content
|
758
|
+
}
|
759
|
+
end
|
760
|
+
|
761
|
+
def create_paragraph(text)
|
762
|
+
{
|
763
|
+
"type" => "paragraph",
|
764
|
+
"content" => [
|
765
|
+
{
|
766
|
+
"type" => "text",
|
767
|
+
"text" => text
|
768
|
+
}
|
769
|
+
]
|
770
|
+
}
|
771
|
+
end
|
772
|
+
|
773
|
+
private
|
774
|
+
|
775
|
+
def format_comment_as_adf(comment_data)
|
776
|
+
# If comment_data is a string (from template), convert to simple ADF
|
777
|
+
if comment_data.is_a?(String)
|
778
|
+
return convert_markdown_to_adf(comment_data)
|
419
779
|
end
|
420
780
|
|
421
781
|
# If comment_data is structured (new format), build proper ADF
|