ast-merge-git 7.1.3 → 7.1.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e6c6a8e98b15372fd7b01969b96b15bd463a9c339debba9150d67a336c2592d3
4
- data.tar.gz: ef2a36f8917e49e6b24e99955c822e100075d0db1fc06a02be2042ccf8fd6732
3
+ metadata.gz: be709f99d89a6dc5a6245b53d676a1d6136b64574724cfedada249935de9557b
4
+ data.tar.gz: dff9b5168e5dd8cac4af0c4a7510726db3038d389f2c0212df27a4954d67fcb1
5
5
  SHA512:
6
- metadata.gz: 493581576c35445cf976532db9320373bc7de6cdaf09b4a68a860758ea99de388af50052f4b9f14beacc2f0b550884fa27b1557459aec2a5b6d196a2c64d27ec
7
- data.tar.gz: aa796efcb1e9a40ef136347bcdc47f793302d023ebded28c19dba0328c6993803b4bc84ec9df6289bdf41fa73aee9a43799814e2de98c4a42a847af0315bcd6e
6
+ metadata.gz: dd3ccddce030b00948006e96a5c6029fda6d6ee7dab5f9af91bc24a4e361149a5e476ddaa5d45277bb981b593995e8d1135ce70c4d5b672cd5c799cd12078225
7
+ data.tar.gz: 431a3ab1a6fa2eb04521bbd5a3b99e7b4a786dc5d121de937aa0c5448bd882906c6cfcf013e520ce105bce587144c84dfbebab9475abd8e9d293eafde840329a
checksums.yaml.gz.sig CHANGED
Binary file
data/README.md CHANGED
@@ -282,7 +282,7 @@ If none of the available licenses suit your use case, please [contact us](mailto
282
282
  [📌gitmoji]: https://gitmoji.dev
283
283
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
284
284
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
285
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-0.163-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
285
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-1.022-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
286
286
  [🔐security]: https://github.com/structuredmerge/structuredmerge-ruby/blob/main/SECURITY.md
287
287
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
288
288
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
data/exe/ast-merge-git CHANGED
@@ -3,6 +3,35 @@
3
3
 
4
4
  require 'ast/merge/git'
5
5
 
6
+ if ARGV.first == 'benchmark-provider-diff'
7
+ require 'json'
8
+
9
+ begin
10
+ before_path, after_path, path_name = ARGV.drop(1)
11
+ raise ArgumentError, 'before, after, and path name are required' unless before_path && after_path && path_name
12
+
13
+ require ENV.fetch('AST_MERGE_REQUIRE')
14
+ result = Ast::Merge.dispatch_provider(
15
+ :diff2,
16
+ {
17
+ provider_id: ENV.fetch('AST_MERGE_PROVIDER'),
18
+ family: ENV.fetch('AST_MERGE_FAMILY'),
19
+ dialect: ENV.fetch('AST_MERGE_DIALECT'),
20
+ backend: ENV.fetch('AST_MERGE_BACKEND'),
21
+ profile_id: ENV.fetch('AST_MERGE_PROFILE'),
22
+ before_source: File.binread(before_path),
23
+ after_source: File.binread(after_path),
24
+ path_name: path_name
25
+ }
26
+ )
27
+ puts JSON.generate(Ast::Merge.json_ready(result))
28
+ exit(result[:ok] == true ? 0 : 2)
29
+ rescue ArgumentError, Ast::Merge::Error, KeyError, LoadError, SystemCallError => e
30
+ warn "ast-merge-git benchmark-provider-diff: #{e.message}"
31
+ exit 2
32
+ end
33
+ end
34
+
6
35
  if ARGV.first == 'benchmark'
7
36
  require 'json'
8
37
  require 'optparse'
@@ -20,6 +49,7 @@ if ARGV.first == 'benchmark'
20
49
  opts.on('--profile PROFILE') { |value| options[:profile] = value }
21
50
  opts.on('--changed-path PATH') { |value| options[:changed_paths] << value }
22
51
  opts.on('--driver PATH') { |value| options[:driver] = value }
52
+ opts.on('--mergiraf PATH') { |value| options[:mergiraf] = value }
23
53
  opts.on('--tmp-root PATH') { |value| options[:tmp_root] = value }
24
54
  opts.on('--timeout SECONDS', Integer) { |value| options[:timeout] = value }
25
55
  end
@@ -41,7 +71,8 @@ if ARGV.first == 'benchmark'
41
71
  benchmark: benchmark,
42
72
  driver_path: driver,
43
73
  tmp_root: options[:tmp_root],
44
- timeout: options[:timeout]
74
+ timeout: options[:timeout],
75
+ competitor_paths: options[:mergiraf] ? { 'mergiraf' => options[:mergiraf] } : {}
45
76
  ).run(profile: options[:profile], changed_paths: options[:changed_paths])
46
77
  command == 'report' ? Ast::Merge::Git::LocalBenchmarkReport.build(run) : run
47
78
  else
@@ -16,6 +16,7 @@ module Ast
16
16
  SCHEMA = 'structuredmerge.benchmark.corpus/v1'
17
17
  CASE_SCHEMA = 'structuredmerge.benchmark/v1'
18
18
  OPERATIONS = %w[merge3 metamorphic diff].freeze
19
+ EXECUTABLE_OPERATIONS = %w[merge3 metamorphic].freeze
19
20
  PARTITIONS = %w[sentinel gold metamorphic].freeze
20
21
  EXPECTATIONS = %w[clean conflict error excluded_ambiguous].freeze
21
22
  SEVERITIES = %w[none low high critical].freeze
@@ -24,6 +25,9 @@ module Ast
24
25
  rename move reorder formatting comment independent_edit delete_modify duplicate_key_identity
25
26
  schema_aware_mutation
26
27
  ].freeze
28
+ METAMORPHIC_INVARIANTS = %w[
29
+ comment-retained no-semantic-edit same-json-value same-jsonc-value
30
+ ].freeze
27
31
  ID_PATTERN = /\A[a-z0-9]+(?:[.-][a-z0-9]+)*\z/
28
32
  PROVENANCE_FIELDS = %w[
29
33
  origin_uri revision spdx_license license_evidence_uri authorship author_review reviewer derivation
@@ -54,7 +58,7 @@ module Ast
54
58
 
55
59
  def validate!
56
60
  require_keys(document, %w[schema_version kind id version extends provenance profiles capability_map
57
- selection cases expected_summary], 'corpus')
61
+ selection competitors cases expected_summary], 'corpus')
58
62
  error!('unsupported corpus schema') unless document['schema_version'] == SCHEMA
59
63
  error!('corpus must extend Slice 1022 v1') unless document['extends'] == CASE_SCHEMA
60
64
  error!('network must be denied') unless document['network_policy'] == 'denied'
@@ -62,6 +66,7 @@ module Ast
62
66
  validate_provenance!(document['provenance'], 'corpus')
63
67
  validate_profiles!
64
68
  validate_capability_map!
69
+ validate_competitors!
65
70
  validate_cases!
66
71
  validate_summary!
67
72
  true
@@ -109,7 +114,9 @@ module Ast
109
114
  'seed' => document.dig('selection', 'seed'),
110
115
  'selected_case_ids' => neighbors
111
116
  },
112
- 'unsupported_selected_cases' => selected.reject { |id| case_by_id(id)['operation'] == 'merge3' },
117
+ 'unsupported_selected_cases' => selected.reject do |id|
118
+ EXECUTABLE_OPERATIONS.include?(case_by_id(id)['operation'])
119
+ end,
113
120
  'budgets' => definition.fetch('budgets'),
114
121
  'explanation' => explanation(
115
122
  profile,
@@ -148,6 +155,25 @@ module Ast
148
155
  end
149
156
  end
150
157
 
158
+ def validate_competitors!
159
+ document.fetch('competitors').each do |id, competitor|
160
+ require_keys(
161
+ competitor,
162
+ %w[adapter_id source_url source_revision version spdx_license reuse_posture toolchain
163
+ build_command operations dialects],
164
+ "competitor #{id}"
165
+ )
166
+ error!("competitor #{id}: adapter ID differs") unless competitor['adapter_id'] == id
167
+ unless /\A[0-9a-f]{40}\z/.match?(competitor['source_revision'])
168
+ error!("competitor #{id}: source revision must be a full SHA")
169
+ end
170
+ error!("competitor #{id}: only merge3 is admitted") unless competitor['operations'] == ['merge3']
171
+ next if competitor['dialects'] == competitor['dialects'].sort
172
+
173
+ error!("competitor #{id}: dialects must be sorted")
174
+ end
175
+ end
176
+
151
177
  def validate_cases!
152
178
  records = document['cases']
153
179
  error!('cases must not be empty') unless records.is_a?(Array) && records.any?
@@ -260,6 +286,8 @@ module Ast
260
286
  digest = item.dig('generator', 'sha256')
261
287
  error!("#{id}: generator SHA-256 is malformed") unless /\A[0-9a-f]{64}\z/.match?(digest)
262
288
  error!("#{id}: expected invariants must not be empty") if item['expected_invariants'].empty?
289
+ unknown_invariants = item['expected_invariants'] - METAMORPHIC_INVARIANTS
290
+ error!("#{id}: unsupported expected invariants: #{unknown_invariants.join(', ')}") if unknown_invariants.any?
263
291
  item['transformations'].each do |transformation|
264
292
  require_keys(transformation, %w[id type parameters], "#{id} transformation")
265
293
  error!("#{id}: unknown transformation") unless TRANSFORMATIONS.include?(transformation['type'])
@@ -348,12 +376,23 @@ module Ast
348
376
  # Executes the same authored merge bytes through Git and the installed driver.
349
377
  class LocalBenchmarkRunner
350
378
  DEFAULT_TIMEOUT = 30
379
+ MERGIRAF_EXTENSIONS = {
380
+ 'bash' => 'sh',
381
+ 'html' => 'html',
382
+ 'json' => 'json',
383
+ 'markdown' => 'md',
384
+ 'ruby' => 'rb',
385
+ 'toml' => 'toml',
386
+ 'typescript' => 'ts',
387
+ 'yaml' => 'yaml'
388
+ }.freeze
351
389
 
352
- def initialize(benchmark:, driver_path:, tmp_root:, timeout: DEFAULT_TIMEOUT)
390
+ def initialize(benchmark:, driver_path:, tmp_root:, timeout: DEFAULT_TIMEOUT, competitor_paths: {})
353
391
  @benchmark = benchmark
354
392
  @driver_path = Pathname(driver_path).expand_path
355
393
  @tmp_root = Pathname(tmp_root).expand_path
356
394
  @timeout = Integer(timeout)
395
+ @competitor_paths = competitor_paths.transform_values { |path| Pathname(path).expand_path }
357
396
  end
358
397
 
359
398
  def run(profile:, changed_paths: [])
@@ -361,7 +400,8 @@ module Ast
361
400
  selection = @benchmark.select(profile: profile, changed_paths: changed_paths)
362
401
  original_cwd = Dir.pwd
363
402
  results = selection['selected_case_ids'].flat_map do |id|
364
- execute_case(@benchmark.case_by_id(id))
403
+ item = @benchmark.case_by_id(id)
404
+ execute_case(item) + execute_competitors(item)
365
405
  end
366
406
  raise LocalBenchmark::Error, 'runner changed its working directory' unless Dir.pwd == original_cwd
367
407
 
@@ -372,6 +412,7 @@ module Ast
372
412
  'corpus_digest' => @benchmark.corpus_digest,
373
413
  'selection' => selection,
374
414
  'cache_identity' => cache_identity(selection),
415
+ 'competitors' => competitor_provenance,
375
416
  'results' => results
376
417
  }
377
418
  ensure
@@ -382,14 +423,67 @@ module Ast
382
423
 
383
424
  def verify_environment!
384
425
  error!("missing installed driver: #{@driver_path}") unless @driver_path.file? && @driver_path.executable?
426
+ @competitor_paths.each do |id, path|
427
+ error!("unknown competitor: #{id}") unless @benchmark.document.fetch('competitors').key?(id)
428
+ error!("missing competitor executable: #{path}") unless path.file? && path.executable?
429
+ end
385
430
  root = Pathname(__dir__).join('..', '..', '..', '..').realpath
386
431
  resolved = @tmp_root.exist? ? @tmp_root.realpath : @tmp_root.dirname.realpath.join(@tmp_root.basename)
387
432
  error!('tmp_root must be inside the ast-merge-git repository') unless resolved.to_s.start_with?("#{root}/")
388
433
  end
389
434
 
390
435
  def execute_case(item)
391
- return unsupported_pair(item) unless item['operation'] == 'merge3'
436
+ return execute_merge3_case(item) if item['operation'] == 'merge3'
437
+ return execute_metamorphic_case(item) if item['operation'] == 'metamorphic'
438
+
439
+ unsupported_pair(item)
440
+ end
441
+
442
+ def execute_competitors(item)
443
+ @competitor_paths.map do |id, path|
444
+ metadata = @benchmark.document.dig('competitors', id)
445
+ unless metadata['operations'].include?(item['operation']) && metadata['dialects'].include?(item['dialect'])
446
+ next raw_result(
447
+ item,
448
+ id,
449
+ nil,
450
+ outcome: 'unsupported',
451
+ unsupported_reason: "#{id} does not support #{item['operation']}/#{item['dialect']}"
452
+ )
453
+ end
454
+
455
+ execute_mergiraf(item, path)
456
+ end
457
+ end
458
+
459
+ def execute_mergiraf(item, path)
460
+ workspace = @tmp_root.join("#{safe_id(item['id'])}-mergiraf-#{Process.pid}")
461
+ FileUtils.rm_rf(workspace)
462
+ FileUtils.mkdir_p(workspace)
463
+ write_roles(item, workspace)
464
+ output = workspace.join('output')
465
+ capture = timed_capture(
466
+ oracle_free_env,
467
+ path.to_s,
468
+ 'merge',
469
+ 'base',
470
+ 'ours',
471
+ 'theirs',
472
+ '--output',
473
+ output.to_s,
474
+ '--path-name',
475
+ "#{item['id']}.#{MERGIRAF_EXTENSIONS.fetch(item['dialect'])}",
476
+ '--conflict-marker-size',
477
+ '7',
478
+ chdir: workspace
479
+ )
480
+ capture[:output] = output.file? ? output.binread : ''
481
+ raw_result(item, 'mergiraf', capture)
482
+ ensure
483
+ FileUtils.rm_rf(workspace) if workspace
484
+ end
392
485
 
486
+ def execute_merge3_case(item)
393
487
  workspace = @tmp_root.join("#{safe_id(item['id'])}-#{Process.pid}")
394
488
  FileUtils.rm_rf(workspace)
395
489
  FileUtils.mkdir_p(workspace)
@@ -402,8 +496,22 @@ module Ast
402
496
  FileUtils.rm_rf(workspace) if workspace
403
497
  end
404
498
 
499
+ def execute_metamorphic_case(item)
500
+ workspace = @tmp_root.join("#{safe_id(item['id'])}-#{Process.pid}")
501
+ FileUtils.rm_rf(workspace)
502
+ FileUtils.mkdir_p(workspace)
503
+ write_metamorphic_roles(item, workspace)
504
+ baseline = execute_metamorphic_baseline(item, workspace)
505
+ candidate = execute_metamorphic_candidate(item, workspace)
506
+ rerun = execute_metamorphic_candidate(item, workspace)
507
+ candidate['deterministic_correctness_rerun'] = correctness_record(candidate) == correctness_record(rerun)
508
+ [baseline, candidate]
509
+ ensure
510
+ FileUtils.rm_rf(workspace) if workspace
511
+ end
512
+
405
513
  def unsupported_pair(item)
406
- %w[git.merge-file ast-merge-git].map do |adapter|
514
+ %w[git.unsupported structuredmerge.unsupported].map do |adapter|
407
515
  raw_result(item, adapter, nil, outcome: 'unsupported',
408
516
  unsupported_reason: "no installed #{item['operation']} adapter")
409
517
  end
@@ -434,6 +542,62 @@ module Ast
434
542
  end
435
543
  end
436
544
 
545
+ def write_metamorphic_roles(item, workspace)
546
+ %w[source transformed].each do |role|
547
+ workspace.join(role).binwrite(item.dig('inputs', role, 'bytes'))
548
+ end
549
+ end
550
+
551
+ def execute_metamorphic_baseline(item, workspace)
552
+ capture = timed_capture(
553
+ {},
554
+ 'git', 'diff', '--no-index', '--no-color', '--no-ext-diff', '--', 'source', 'transformed',
555
+ chdir: workspace
556
+ )
557
+ capture[:output] = capture[:stdout]
558
+ metamorphic_result(
559
+ item,
560
+ 'git.diff',
561
+ 'baseline',
562
+ capture,
563
+ edit_signal: capture[:status] == 1
564
+ )
565
+ end
566
+
567
+ def execute_metamorphic_candidate(item, workspace)
568
+ capture, changes = capture_provider_diff(item, workspace)
569
+ metamorphic_result(
570
+ item,
571
+ 'ast-merge-provider.diff2',
572
+ 'candidate',
573
+ capture,
574
+ edit_signal: changes.any?
575
+ )
576
+ end
577
+
578
+ def capture_provider_diff(item, workspace)
579
+ selector = item.fetch('selector')
580
+ capture = timed_capture(
581
+ candidate_env(selector),
582
+ @driver_path.to_s,
583
+ 'benchmark-provider-diff',
584
+ 'source',
585
+ 'transformed',
586
+ "#{item['id']}.#{item['dialect']}",
587
+ chdir: workspace
588
+ )
589
+ result = capture[:stdout].empty? ? {} : JSON.parse(capture[:stdout])
590
+ changes = capture[:status].zero? ? result.fetch('changes') : []
591
+ capture[:output] = JSON.generate(changes)
592
+ [capture, changes]
593
+ rescue JSON::ParserError, KeyError => e
594
+ capture ||= { stdout: '', stderr: '', status: 2, duration_ns: 0 }
595
+ capture[:stderr] = [capture[:stderr], e.message].reject(&:empty?).join("\n")
596
+ capture[:status] = 2
597
+ capture[:output] = ''
598
+ [capture, []]
599
+ end
600
+
437
601
  def timed_capture(env, *command, chdir:)
438
602
  started = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
439
603
  stdin, stdout_io, stderr_io, process = Open3.popen3(env, *command, chdir: chdir.to_s)
@@ -475,6 +639,7 @@ module Ast
475
639
  'id' => "result.#{safe_id(item['id'])}.#{safe_id(adapter)}",
476
640
  'case_id' => item['id'],
477
641
  'adapter_id' => adapter,
642
+ 'adapter_role' => adapter_role(adapter),
478
643
  'case_tags' => item.slice(
479
644
  'operation', 'partition', 'family', 'provider', 'dialect',
480
645
  'capabilities', 'false_auto_merge_severity'
@@ -498,6 +663,135 @@ module Ast
498
663
  }
499
664
  end
500
665
 
666
+ def adapter_role(adapter)
667
+ return 'baseline' if adapter.start_with?('git.')
668
+ return 'candidate' if %w[ast-merge-git ast-merge-provider.diff2 structuredmerge.unsupported].include?(adapter)
669
+
670
+ 'competitor'
671
+ end
672
+
673
+ def metamorphic_result(item, adapter, adapter_role, capture, edit_signal:)
674
+ checks = metamorphic_checks(item, edit_signal)
675
+ classified = if capture[:status].nil? || capture[:status] >= 2
676
+ 'error'
677
+ elsif checks['acceptable']
678
+ 'correct_clean'
679
+ else
680
+ 'false_conflict'
681
+ end
682
+ eligible = item.dig('oracle', 'score_eligible') && classified != 'error'
683
+ {
684
+ 'schema_version' => 'structuredmerge.benchmark.result/v1',
685
+ 'id' => "result.#{safe_id(item['id'])}.#{safe_id(adapter)}",
686
+ 'case_id' => item['id'],
687
+ 'adapter_id' => adapter,
688
+ 'adapter_role' => adapter_role,
689
+ 'case_tags' => item.slice(
690
+ 'operation', 'partition', 'family', 'provider', 'dialect',
691
+ 'capabilities', 'false_auto_merge_severity'
692
+ ),
693
+ 'outcome' => classified,
694
+ 'score_eligible' => eligible,
695
+ 'provenance' => item['provenance'],
696
+ 'process' => {
697
+ 'status' => capture[:status],
698
+ 'exit_classification' => metamorphic_exit_class(adapter_role, capture[:status])
699
+ },
700
+ 'raw' => {
701
+ 'stdout' => raw_record(capture[:stdout]),
702
+ 'stderr' => raw_record(capture[:stderr]),
703
+ 'output' => raw_record(capture[:output])
704
+ },
705
+ 'diagnostics' => diagnostics(capture[:stderr], nil),
706
+ 'conflict_regions' => region_evidence(item, []),
707
+ 'checks' => checks,
708
+ 'independent_edit_evidence' => {},
709
+ 'dimensions' => dimensions(item, classified, checks),
710
+ 'runtime' => { 'duration_ns' => capture[:duration_ns], 'comparable' => true }
711
+ }
712
+ end
713
+
714
+ def metamorphic_checks(item, edit_signal)
715
+ evaluations = item.fetch('expected_invariants').map do |invariant|
716
+ matched, evidence = evaluate_metamorphic_invariant(item, invariant, edit_signal)
717
+ { 'id' => invariant, 'matched' => matched, 'evidence' => evidence }
718
+ end
719
+ accepted = evaluations.all? { |evaluation| evaluation['matched'] }
720
+ {
721
+ 'declared_invariants' => evaluations,
722
+ 'accepted_equivalence' => accepted ? 'declared_invariants' : nil,
723
+ 'acceptable_equivalence_evaluations' => [{
724
+ 'class' => 'declared_invariants',
725
+ 'matched' => accepted
726
+ }],
727
+ 'parse_validity_only_accepted' => false,
728
+ 'generator_replay' => {
729
+ 'generator_id' => item.dig('generator', 'id'),
730
+ 'generator_version' => item.dig('generator', 'version'),
731
+ 'seed' => item.dig('generator', 'seed'),
732
+ 'transformation_ids' => item.fetch('transformations').map { |transformation| transformation['id'] },
733
+ 'authored_bytes_digest_verified' => true
734
+ },
735
+ 'preservation_violations' => [],
736
+ 'acceptable' => accepted
737
+ }
738
+ end
739
+
740
+ def evaluate_metamorphic_invariant(item, invariant, edit_signal)
741
+ case invariant
742
+ when 'same-json-value', 'same-jsonc-value'
743
+ [json_values_equal?(item), 'JSON-family values parsed from source and transformed bytes']
744
+ when 'no-semantic-edit'
745
+ [!edit_signal, 'adapter emitted no edit units']
746
+ when 'comment-retained'
747
+ [json_comments_retained?(item), 'JSONC AST comment content retained']
748
+ else
749
+ [false, 'unsupported invariant']
750
+ end
751
+ end
752
+
753
+ def json_values_equal?(item)
754
+ require 'json/merge'
755
+ selector = item.fetch('selector')
756
+ source = Json::Merge.json_value_for_source(
757
+ item.dig('inputs', 'source', 'bytes'),
758
+ dialect: selector.fetch('dialect').to_sym,
759
+ backend: selector.fetch('backend')
760
+ )
761
+ transformed = Json::Merge.json_value_for_source(
762
+ item.dig('inputs', 'transformed', 'bytes'),
763
+ dialect: selector.fetch('dialect').to_sym,
764
+ backend: selector.fetch('backend')
765
+ )
766
+ source == transformed
767
+ rescue Json::Merge::ParseError, LoadError
768
+ false
769
+ end
770
+
771
+ def json_comments_retained?(item)
772
+ require 'json/merge'
773
+ selector = item.fetch('selector')
774
+ source = Json::Merge::FileAnalysis.new(
775
+ item.dig('inputs', 'source', 'bytes'),
776
+ dialect: selector.fetch('dialect').to_sym
777
+ )
778
+ transformed = Json::Merge::FileAnalysis.new(
779
+ item.dig('inputs', 'transformed', 'bytes'),
780
+ dialect: selector.fetch('dialect').to_sym
781
+ )
782
+ source.valid? && transformed.valid? &&
783
+ source.comment_nodes.map(&:normalized_content) == transformed.comment_nodes.map(&:normalized_content)
784
+ rescue TreeHaver::Error, LoadError
785
+ false
786
+ end
787
+
788
+ def metamorphic_exit_class(adapter_role, status)
789
+ return 'error' if status.nil? || status >= 2
790
+ return status == 1 ? 'differences' : 'no_differences' if adapter_role == 'baseline'
791
+
792
+ 'analyzed'
793
+ end
794
+
501
795
  def classify(item, status, checks)
502
796
  expected = item.dig('expected', 'outcome')
503
797
  return 'excluded_ambiguous' if expected == 'excluded_ambiguous'
@@ -677,8 +971,11 @@ module Ast
677
971
  end
678
972
 
679
973
  def candidate_env(selector)
680
- forbidden = ENV.keys.grep(/(?:ORACLE|EXPECTED)/i).to_h { |name| [name, nil] }
681
- forbidden.merge(selector_env(selector))
974
+ oracle_free_env.merge(selector_env(selector))
975
+ end
976
+
977
+ def oracle_free_env
978
+ ENV.keys.grep(/(?:ORACLE|EXPECTED)/i).to_h { |name| [name, nil] }
682
979
  end
683
980
 
684
981
  def cache_identity(selection)
@@ -693,10 +990,12 @@ module Ast
693
990
  configuration = selection['selected_case_ids'].map do |id|
694
991
  @benchmark.case_by_id(id).slice('id', 'selector')
695
992
  end
993
+ competitors = competitor_provenance
696
994
  identity = {
697
995
  'adapter_source_sha' => source_sha.strip,
698
996
  'adapter_artifact_sha256' => Digest::SHA256.file(@driver_path).hexdigest,
699
997
  'configuration_sha256' => Digest::SHA256.hexdigest(JSON.generate(configuration)),
998
+ 'competitors' => competitors,
700
999
  'corpus_sha256' => @benchmark.corpus_digest,
701
1000
  'environment' => environment,
702
1001
  'profile' => selection['profile'],
@@ -710,6 +1009,22 @@ module Ast
710
1009
  identity.merge('sha256' => Digest::SHA256.hexdigest(JSON.generate(identity)))
711
1010
  end
712
1011
 
1012
+ def competitor_provenance
1013
+ @competitor_provenance ||= @competitor_paths.to_h do |id, path|
1014
+ metadata = @benchmark.document.fetch('competitors').fetch(id)
1015
+ capture = timed_capture(oracle_free_env, path.to_s, '--version', chdir: path.dirname)
1016
+ error!("#{id} version probe failed: #{capture[:stderr].strip}") unless capture[:status].zero?
1017
+ expected = "#{metadata.fetch('adapter_id')} #{metadata.fetch('version')}"
1018
+ error!("#{id} version differs: #{capture[:stdout].strip}") unless capture[:stdout].strip == expected
1019
+
1020
+ [id, metadata.merge(
1021
+ 'binary_path' => path.to_s,
1022
+ 'binary_sha256' => Digest::SHA256.file(path).hexdigest,
1023
+ 'reported_version' => capture[:stdout].strip
1024
+ )]
1025
+ end
1026
+ end
1027
+
713
1028
  def deep_sort(value)
714
1029
  case value
715
1030
  when Hash then value.keys.sort.to_h { |key| [key, deep_sort(value.fetch(key))] }
@@ -750,7 +1065,7 @@ module Ast
750
1065
 
751
1066
  def build
752
1067
  pairs = @results.group_by { |result| result['case_id'] }.values.map { |items| transition(items) }
753
- false_auto_merges = eligible.select { |item| item['outcome'] == 'false_auto_merge' }
1068
+ false_auto_merges = candidate_eligible.select { |item| item['outcome'] == 'false_auto_merge' }
754
1069
  {
755
1070
  'schema_version' => 'structuredmerge.benchmark.report/v1',
756
1071
  'kind' => 'paired_aggregate_report',
@@ -760,7 +1075,7 @@ module Ast
760
1075
  'cache_identity' => @run['cache_identity'],
761
1076
  'dimensions' => {
762
1077
  'safety' => {
763
- 'eligible' => eligible.length,
1078
+ 'eligible' => candidate_eligible.length,
764
1079
  'false_auto_merge_result_ids' => false_auto_merges.map { |item| item['id'] },
765
1080
  'gate' => false_auto_merges.empty? ? 'pass' : 'fail',
766
1081
  'non_compensable' => true
@@ -776,7 +1091,8 @@ module Ast
776
1091
  'reliability' => { 'error_result_ids' => @results.filter_map do |item|
777
1092
  item['id'] if item['outcome'] == 'error'
778
1093
  end },
779
- 'coverage' => coverage
1094
+ 'coverage' => coverage,
1095
+ 'competitive' => competitive
780
1096
  },
781
1097
  'strata' => strata,
782
1098
  'transitions' => pairs,
@@ -794,6 +1110,10 @@ module Ast
794
1110
  @eligible ||= @results.select { |item| item['score_eligible'] }
795
1111
  end
796
1112
 
1113
+ def candidate_eligible
1114
+ eligible.select { |item| item['adapter_role'] == 'candidate' }
1115
+ end
1116
+
797
1117
  def outcome_counts
798
1118
  %w[correct_clean false_conflict true_conflict false_auto_merge error unsupported
799
1119
  excluded_ambiguous].to_h do |outcome|
@@ -802,8 +1122,8 @@ module Ast
802
1122
  end
803
1123
 
804
1124
  def transition(items)
805
- baseline = items.find { |item| item['adapter_id'] == 'git.merge-file' }
806
- candidate = items.find { |item| item['adapter_id'] == 'ast-merge-git' }
1125
+ baseline = items.find { |item| item['adapter_role'] == 'baseline' }
1126
+ candidate = items.find { |item| item['adapter_role'] == 'candidate' }
807
1127
  {
808
1128
  'case_id' => items.first['case_id'],
809
1129
  'baseline_result_id' => baseline['id'],
@@ -831,15 +1151,30 @@ module Ast
831
1151
  def coverage
832
1152
  selected = @run.dig('selection', 'selected_case_ids').length
833
1153
  unsupported = @results.select do |item|
834
- item['adapter_id'] == 'ast-merge-git' && item['outcome'] == 'unsupported'
1154
+ item['adapter_role'] == 'candidate' && item['outcome'] == 'unsupported'
835
1155
  end
836
1156
  { 'selected_cases' => selected, 'executed_candidate_cases' => selected - unsupported.length,
837
1157
  'unsupported_case_ids' => unsupported.map { |item| item['case_id'] },
838
1158
  'unsupported_is_quality_failure' => false }
839
1159
  end
840
1160
 
1161
+ def competitive
1162
+ results = @results.select { |item| item['adapter_role'] == 'competitor' }
1163
+ {
1164
+ 'configured' => @run.fetch('competitors'),
1165
+ 'outcomes' => results.group_by { |item| item['outcome'] }.transform_values(&:length),
1166
+ 'unsupported_case_ids' => results.filter_map do |item|
1167
+ item['case_id'] if item['outcome'] == 'unsupported'
1168
+ end,
1169
+ 'false_auto_merge_result_ids' => results.filter_map do |item|
1170
+ item['id'] if item['outcome'] == 'false_auto_merge'
1171
+ end,
1172
+ 'affects_candidate_safety_gate' => false
1173
+ }
1174
+ end
1175
+
841
1176
  def strata
842
- case_results = @results.select { |item| item['adapter_id'] == 'ast-merge-git' }
1177
+ case_results = @results.select { |item| item['adapter_role'] == 'candidate' }
843
1178
  tag_fields = %w[operation partition family provider dialect false_auto_merge_severity]
844
1179
  tag_strata = tag_fields.to_h do |field|
845
1180
  [field, case_results.group_by { |item| item.dig('case_tags', field) }.transform_values(&:length)]
@@ -6,7 +6,7 @@ module Ast
6
6
  # Version namespace for this gem.
7
7
  module Version
8
8
  # Current gem version.
9
- VERSION = '7.1.3'
9
+ VERSION = '7.1.4'
10
10
  end
11
11
  # Current gem version exposed at the traditional constant location.
12
12
  VERSION = Version::VERSION # Traditional Constant Location
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ast-merge-git
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.1.3
4
+ version: 7.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -43,14 +43,14 @@ dependencies:
43
43
  requirements:
44
44
  - - '='
45
45
  - !ruby/object:Gem::Version
46
- version: 7.1.3
46
+ version: 7.1.4
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - '='
52
52
  - !ruby/object:Gem::Version
53
- version: 7.1.3
53
+ version: 7.1.4
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: version_gem
56
56
  requirement: !ruby/object:Gem::Requirement
@@ -80,7 +80,7 @@ dependencies:
80
80
  version: '3.0'
81
81
  - - ">="
82
82
  - !ruby/object:Gem::Version
83
- version: 3.0.0
83
+ version: 3.0.7
84
84
  type: :development
85
85
  prerelease: false
86
86
  version_requirements: !ruby/object:Gem::Requirement
@@ -90,7 +90,7 @@ dependencies:
90
90
  version: '3.0'
91
91
  - - ">="
92
92
  - !ruby/object:Gem::Version
93
- version: 3.0.0
93
+ version: 3.0.7
94
94
  - !ruby/object:Gem::Dependency
95
95
  name: bundler-audit
96
96
  requirement: !ruby/object:Gem::Requirement
@@ -188,7 +188,7 @@ dependencies:
188
188
  version: '2.0'
189
189
  - - ">="
190
190
  - !ruby/object:Gem::Version
191
- version: 2.0.19
191
+ version: 2.0.20
192
192
  type: :development
193
193
  prerelease: false
194
194
  version_requirements: !ruby/object:Gem::Requirement
@@ -198,7 +198,7 @@ dependencies:
198
198
  version: '2.0'
199
199
  - - ">="
200
200
  - !ruby/object:Gem::Version
201
- version: 2.0.19
201
+ version: 2.0.20
202
202
  - !ruby/object:Gem::Dependency
203
203
  name: turbo_tests2
204
204
  requirement: !ruby/object:Gem::Requirement
@@ -208,7 +208,7 @@ dependencies:
208
208
  version: '3.2'
209
209
  - - ">="
210
210
  - !ruby/object:Gem::Version
211
- version: 3.2.5
211
+ version: 3.2.6
212
212
  type: :development
213
213
  prerelease: false
214
214
  version_requirements: !ruby/object:Gem::Requirement
@@ -218,287 +218,7 @@ dependencies:
218
218
  version: '3.2'
219
219
  - - ">="
220
220
  - !ruby/object:Gem::Version
221
- version: 3.2.5
222
- - !ruby/object:Gem::Dependency
223
- name: bash-merge
224
- requirement: !ruby/object:Gem::Requirement
225
- requirements:
226
- - - '='
227
- - !ruby/object:Gem::Version
228
- version: 7.1.3
229
- type: :development
230
- prerelease: false
231
- version_requirements: !ruby/object:Gem::Requirement
232
- requirements:
233
- - - '='
234
- - !ruby/object:Gem::Version
235
- version: 7.1.3
236
- - !ruby/object:Gem::Dependency
237
- name: citrus-toml-merge
238
- requirement: !ruby/object:Gem::Requirement
239
- requirements:
240
- - - '='
241
- - !ruby/object:Gem::Version
242
- version: 7.1.3
243
- type: :development
244
- prerelease: false
245
- version_requirements: !ruby/object:Gem::Requirement
246
- requirements:
247
- - - '='
248
- - !ruby/object:Gem::Version
249
- version: 7.1.3
250
- - !ruby/object:Gem::Dependency
251
- name: commonmarker-merge
252
- requirement: !ruby/object:Gem::Requirement
253
- requirements:
254
- - - '='
255
- - !ruby/object:Gem::Version
256
- version: 7.1.3
257
- type: :development
258
- prerelease: false
259
- version_requirements: !ruby/object:Gem::Requirement
260
- requirements:
261
- - - '='
262
- - !ruby/object:Gem::Version
263
- version: 7.1.3
264
- - !ruby/object:Gem::Dependency
265
- name: dotenv-merge
266
- requirement: !ruby/object:Gem::Requirement
267
- requirements:
268
- - - '='
269
- - !ruby/object:Gem::Version
270
- version: 7.1.3
271
- type: :development
272
- prerelease: false
273
- version_requirements: !ruby/object:Gem::Requirement
274
- requirements:
275
- - - '='
276
- - !ruby/object:Gem::Version
277
- version: 7.1.3
278
- - !ruby/object:Gem::Dependency
279
- name: go-merge
280
- requirement: !ruby/object:Gem::Requirement
281
- requirements:
282
- - - '='
283
- - !ruby/object:Gem::Version
284
- version: 7.1.3
285
- type: :development
286
- prerelease: false
287
- version_requirements: !ruby/object:Gem::Requirement
288
- requirements:
289
- - - '='
290
- - !ruby/object:Gem::Version
291
- version: 7.1.3
292
- - !ruby/object:Gem::Dependency
293
- name: html-merge
294
- requirement: !ruby/object:Gem::Requirement
295
- requirements:
296
- - - '='
297
- - !ruby/object:Gem::Version
298
- version: 7.1.3
299
- type: :development
300
- prerelease: false
301
- version_requirements: !ruby/object:Gem::Requirement
302
- requirements:
303
- - - '='
304
- - !ruby/object:Gem::Version
305
- version: 7.1.3
306
- - !ruby/object:Gem::Dependency
307
- name: json-merge
308
- requirement: !ruby/object:Gem::Requirement
309
- requirements:
310
- - - '='
311
- - !ruby/object:Gem::Version
312
- version: 7.1.3
313
- type: :development
314
- prerelease: false
315
- version_requirements: !ruby/object:Gem::Requirement
316
- requirements:
317
- - - '='
318
- - !ruby/object:Gem::Version
319
- version: 7.1.3
320
- - !ruby/object:Gem::Dependency
321
- name: kramdown-merge
322
- requirement: !ruby/object:Gem::Requirement
323
- requirements:
324
- - - '='
325
- - !ruby/object:Gem::Version
326
- version: 7.1.3
327
- type: :development
328
- prerelease: false
329
- version_requirements: !ruby/object:Gem::Requirement
330
- requirements:
331
- - - '='
332
- - !ruby/object:Gem::Version
333
- version: 7.1.3
334
- - !ruby/object:Gem::Dependency
335
- name: markdown-merge
336
- requirement: !ruby/object:Gem::Requirement
337
- requirements:
338
- - - '='
339
- - !ruby/object:Gem::Version
340
- version: 7.1.3
341
- type: :development
342
- prerelease: false
343
- version_requirements: !ruby/object:Gem::Requirement
344
- requirements:
345
- - - '='
346
- - !ruby/object:Gem::Version
347
- version: 7.1.3
348
- - !ruby/object:Gem::Dependency
349
- name: markly-merge
350
- requirement: !ruby/object:Gem::Requirement
351
- requirements:
352
- - - '='
353
- - !ruby/object:Gem::Version
354
- version: 7.1.3
355
- type: :development
356
- prerelease: false
357
- version_requirements: !ruby/object:Gem::Requirement
358
- requirements:
359
- - - '='
360
- - !ruby/object:Gem::Version
361
- version: 7.1.3
362
- - !ruby/object:Gem::Dependency
363
- name: parslet-toml-merge
364
- requirement: !ruby/object:Gem::Requirement
365
- requirements:
366
- - - '='
367
- - !ruby/object:Gem::Version
368
- version: 7.1.3
369
- type: :development
370
- prerelease: false
371
- version_requirements: !ruby/object:Gem::Requirement
372
- requirements:
373
- - - '='
374
- - !ruby/object:Gem::Version
375
- version: 7.1.3
376
- - !ruby/object:Gem::Dependency
377
- name: plain-merge
378
- requirement: !ruby/object:Gem::Requirement
379
- requirements:
380
- - - '='
381
- - !ruby/object:Gem::Version
382
- version: 7.1.3
383
- type: :development
384
- prerelease: false
385
- version_requirements: !ruby/object:Gem::Requirement
386
- requirements:
387
- - - '='
388
- - !ruby/object:Gem::Version
389
- version: 7.1.3
390
- - !ruby/object:Gem::Dependency
391
- name: psych-merge
392
- requirement: !ruby/object:Gem::Requirement
393
- requirements:
394
- - - '='
395
- - !ruby/object:Gem::Version
396
- version: 7.1.3
397
- type: :development
398
- prerelease: false
399
- version_requirements: !ruby/object:Gem::Requirement
400
- requirements:
401
- - - '='
402
- - !ruby/object:Gem::Version
403
- version: 7.1.3
404
- - !ruby/object:Gem::Dependency
405
- name: rbs-merge
406
- requirement: !ruby/object:Gem::Requirement
407
- requirements:
408
- - - '='
409
- - !ruby/object:Gem::Version
410
- version: 7.1.3
411
- type: :development
412
- prerelease: false
413
- version_requirements: !ruby/object:Gem::Requirement
414
- requirements:
415
- - - '='
416
- - !ruby/object:Gem::Version
417
- version: 7.1.3
418
- - !ruby/object:Gem::Dependency
419
- name: ruby-merge
420
- requirement: !ruby/object:Gem::Requirement
421
- requirements:
422
- - - '='
423
- - !ruby/object:Gem::Version
424
- version: 7.1.3
425
- type: :development
426
- prerelease: false
427
- version_requirements: !ruby/object:Gem::Requirement
428
- requirements:
429
- - - '='
430
- - !ruby/object:Gem::Version
431
- version: 7.1.3
432
- - !ruby/object:Gem::Dependency
433
- name: rust-merge
434
- requirement: !ruby/object:Gem::Requirement
435
- requirements:
436
- - - '='
437
- - !ruby/object:Gem::Version
438
- version: 7.1.3
439
- type: :development
440
- prerelease: false
441
- version_requirements: !ruby/object:Gem::Requirement
442
- requirements:
443
- - - '='
444
- - !ruby/object:Gem::Version
445
- version: 7.1.3
446
- - !ruby/object:Gem::Dependency
447
- name: toml-merge
448
- requirement: !ruby/object:Gem::Requirement
449
- requirements:
450
- - - '='
451
- - !ruby/object:Gem::Version
452
- version: 7.1.3
453
- type: :development
454
- prerelease: false
455
- version_requirements: !ruby/object:Gem::Requirement
456
- requirements:
457
- - - '='
458
- - !ruby/object:Gem::Version
459
- version: 7.1.3
460
- - !ruby/object:Gem::Dependency
461
- name: typescript-merge
462
- requirement: !ruby/object:Gem::Requirement
463
- requirements:
464
- - - '='
465
- - !ruby/object:Gem::Version
466
- version: 7.1.3
467
- type: :development
468
- prerelease: false
469
- version_requirements: !ruby/object:Gem::Requirement
470
- requirements:
471
- - - '='
472
- - !ruby/object:Gem::Version
473
- version: 7.1.3
474
- - !ruby/object:Gem::Dependency
475
- name: yaml-merge
476
- requirement: !ruby/object:Gem::Requirement
477
- requirements:
478
- - - '='
479
- - !ruby/object:Gem::Version
480
- version: 7.1.3
481
- type: :development
482
- prerelease: false
483
- version_requirements: !ruby/object:Gem::Requirement
484
- requirements:
485
- - - '='
486
- - !ruby/object:Gem::Version
487
- version: 7.1.3
488
- - !ruby/object:Gem::Dependency
489
- name: zip-merge
490
- requirement: !ruby/object:Gem::Requirement
491
- requirements:
492
- - - '='
493
- - !ruby/object:Gem::Version
494
- version: 7.1.3
495
- type: :development
496
- prerelease: false
497
- version_requirements: !ruby/object:Gem::Requirement
498
- requirements:
499
- - - '='
500
- - !ruby/object:Gem::Version
501
- version: 7.1.3
221
+ version: 3.2.6
502
222
  - !ruby/object:Gem::Dependency
503
223
  name: ruby-progressbar
504
224
  requirement: !ruby/object:Gem::Requirement
@@ -542,7 +262,7 @@ dependencies:
542
262
  version: '2.0'
543
263
  - - ">="
544
264
  - !ruby/object:Gem::Version
545
- version: 2.0.11
265
+ version: 2.0.12
546
266
  type: :development
547
267
  prerelease: false
548
268
  version_requirements: !ruby/object:Gem::Requirement
@@ -552,7 +272,7 @@ dependencies:
552
272
  version: '2.0'
553
273
  - - ">="
554
274
  - !ruby/object:Gem::Version
555
- version: 2.0.11
275
+ version: 2.0.12
556
276
  description: "☯️ Git protocol adaptation and provider dispatch over ast-merge primitives."
557
277
  email:
558
278
  - floss@galtzo.com
@@ -576,10 +296,10 @@ licenses:
576
296
  - PolyForm-Small-Business-1.0.0
577
297
  metadata:
578
298
  homepage_uri: https://structuredmerge.org
579
- source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.3
580
- changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.3/CHANGELOG.md
299
+ source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.4
300
+ changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.4/CHANGELOG.md
581
301
  bug_tracker_uri: https://github.com/structuredmerge/structuredmerge-ruby/issues
582
- documentation_uri: https://www.rubydoc.info/gems/ast-merge-git/7.1.3
302
+ documentation_uri: https://www.rubydoc.info/gems/ast-merge-git/7.1.4
583
303
  funding_uri: https://github.com/sponsors/pboling
584
304
  wiki_uri: https://github.com/structuredmerge/structuredmerge-ruby/wiki
585
305
  news_uri: https://www.railsbling.com/tags/ast-merge-git
metadata.gz.sig CHANGED
Binary file