rubylens 0.1.0 → 0.2.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.
@@ -8,25 +8,27 @@ module RubyLens
8
8
  def initialize(package_count:)
9
9
  raise ArgumentError, "package_count must be nonnegative" if package_count.negative?
10
10
 
11
- @counts = Array.new(package_count, 0)
12
11
  @ruby_counts = Array.new(package_count) { Array.new(4, 0) }
13
12
  @signal_maxima = Array.new(6, 0)
14
13
  @rows = Array.new(package_count) { [] }
15
14
  end
16
15
 
17
16
  def add(package_index:, row:, construct_index:)
18
- @counts[package_index] += 1
17
+ local_index = @rows[package_index].length
19
18
  @ruby_counts[package_index][construct_index] += 1 if construct_index
20
- SIGNAL_COLUMNS.each_with_index do |column, index|
21
- @signal_maxima[index] = [@signal_maxima[index], row[column]].max
19
+ maxima = @signal_maxima
20
+ SIGNAL_COLUMNS.each do |column|
21
+ value = row[column]
22
+ index = column - 1
23
+ maxima[index] = value if value > maxima[index]
22
24
  end
23
- @rows[package_index] << row.dup.freeze
25
+ @rows[package_index] << (row.frozen? ? row : row.dup.freeze)
26
+ local_index
24
27
  end
25
28
 
26
29
  def packages
27
- @counts.each_index.map do |index|
30
+ @rows.each_index.map do |index|
28
31
  {
29
- declaration_count: @counts[index],
30
32
  ruby_counts: @ruby_counts[index].dup.freeze,
31
33
  declarations: @rows[index].dup.freeze,
32
34
  }.freeze
@@ -16,6 +16,10 @@ module RubyLens
16
16
  BAR_CONCENTRATION = 0.50
17
17
  SPIRAL_STAGE_WIDTH = 0.10
18
18
  SPIRAL_KNOB_RANGE = 0.35
19
+ LARGE_PACKAGE_ENRICHMENT_THRESHOLD = 10_000
20
+ LARGE_PACKAGE_ENRICHMENT_FAMILIES = [SPIRAL, BARRED_SPIRAL].freeze
21
+ UINT32_MASK = 0xffff_ffff
22
+ UINT32_RANGE = 0x1_0000_0000
19
23
 
20
24
  DEFAULT_KNOBS = [0, 240, 3, 105, 380, 0, 0, 0, 0].freeze
21
25
 
@@ -32,7 +36,10 @@ module RubyLens
32
36
  inputs = package_classification? ? package_classification_inputs : project_classification_inputs
33
37
  return fallback(phase_seed: fallback_phase_seed) unless inputs
34
38
 
35
- classify(**inputs)
39
+ classified = classify(**inputs)
40
+ return classified unless package_classification?
41
+
42
+ enrich_large_smooth_package(classified, size: inputs.fetch(:size), phase_seed: inputs.fetch(:phase_seed))
36
43
  rescue KeyError, TypeError, ArgumentError
37
44
  fallback(phase_seed: fallback_phase_seed)
38
45
  end
@@ -55,10 +62,10 @@ module RubyLens
55
62
  return if rows.empty?
56
63
  return unless rows.each_with_index.all? { |row, index| valid_namespace?(row, names[index]) }
57
64
 
58
- core_indexes = rows.each_index.reject { |index| rows[index][2] == 1 }
65
+ core_indexes = rows.each_index.reject { |index| rows[index][1] == 1 }
59
66
  core_count = core_indexes.length
60
67
  test_count = rows.length - core_count
61
- module_count = core_indexes.count { |index| rows[index][1] == 1 }
68
+ module_count = core_indexes.count { |index| rows[index][0] == 1 }
62
69
  dependency_count = packages.sum { |package| dependency_declaration_count(package) }
63
70
 
64
71
  module_fraction = ratio(module_count, core_count)
@@ -77,10 +84,12 @@ module RubyLens
77
84
  def package_classification_inputs
78
85
  return unless @package.is_a?(Hash) && valid_phase_seed?(@phase_seed)
79
86
 
80
- size = @package.fetch("declaration_count")
87
+ declarations = @package.fetch("declarations")
81
88
  counts = @package.fetch("ruby_counts")
82
- return unless size.is_a?(Integer) && size >= 0
89
+ return unless declarations.is_a?(Array)
83
90
  return unless counts.is_a?(Array) && counts.length == 4
91
+
92
+ size = declarations.length
84
93
  return unless counts.all? { |count| count.is_a?(Integer) && count >= 0 }
85
94
  return if counts.sum.zero?
86
95
 
@@ -105,18 +114,18 @@ module RubyLens
105
114
  end
106
115
 
107
116
  def valid_namespace?(row, name)
108
- row.is_a?(Array) && row.length >= 3 &&
109
- [0, 1].include?(row[1]) && [0, 1, 2].include?(row[2]) &&
117
+ row.is_a?(Array) && row.length >= 2 &&
118
+ [0, 1].include?(row[0]) && [0, 1, 2].include?(row[1]) &&
110
119
  name.is_a?(String) && !name.empty?
111
120
  end
112
121
 
113
122
  def dependency_declaration_count(package)
114
123
  raise TypeError unless package.is_a?(Hash)
115
124
 
116
- count = package.fetch("declaration_count")
117
- raise TypeError unless count.is_a?(Integer) && count >= 0
125
+ declarations = package.fetch("declarations")
126
+ raise TypeError unless declarations.is_a?(Array)
118
127
 
119
- count
128
+ declarations.length
120
129
  end
121
130
 
122
131
  def ratio(numerator, denominator)
@@ -171,8 +180,31 @@ module RubyLens
171
180
 
172
181
  family = concentration >= BAR_CONCENTRATION ? BARRED_SPIRAL : SPIRAL
173
182
  stage = [[((structure - LENTICULAR_MAX) / SPIRAL_STAGE_WIDTH).floor, 0].max, 2].min
174
- designation = family == BARRED_SPIRAL ? "SB#{%w[a b c][stage]}" : "S#{%w[a b c][stage]}"
175
183
  progress = [[(structure - LENTICULAR_MAX) / SPIRAL_KNOB_RANGE, 0.0].max, 1.0].min
184
+
185
+ spiral_morphology(family:, stage:, progress:, phase_seed:)
186
+ end
187
+
188
+ def enrich_large_smooth_package(classified, size:, phase_seed:)
189
+ return classified if size < LARGE_PACKAGE_ENRICHMENT_THRESHOLD
190
+ return classified unless [ELLIPTICAL, LENTICULAR].include?(classified.fetch("family"))
191
+
192
+ family_count = LARGE_PACKAGE_ENRICHMENT_FAMILIES.length
193
+ family = LARGE_PACKAGE_ENRICHMENT_FAMILIES.fetch(phase_seed % family_count)
194
+ progress = seeded_unit(phase_seed / family_count, 91)
195
+ stage = [(progress * 3).floor, 2].min
196
+
197
+ spiral_morphology(
198
+ family:,
199
+ stage:,
200
+ progress:,
201
+ phase_seed:,
202
+ bar_length: 0.45 + 0.15 * progress,
203
+ )
204
+ end
205
+
206
+ def spiral_morphology(family:, stage:, progress:, phase_seed:, bar_length: nil)
207
+ designation = family == BARRED_SPIRAL ? "SB#{%w[a b c][stage]}" : "S#{%w[a b c][stage]}"
176
208
  arm_limit = family == BARRED_SPIRAL ? 2 : 4
177
209
  morphology(
178
210
  family,
@@ -181,11 +213,19 @@ module RubyLens
181
213
  arm_count: 2 + (progress * arm_limit).round,
182
214
  winding: scaled(0.18 - 0.115 * progress),
183
215
  arm_fraction: scaled(0.45 + 0.11 * progress),
184
- bar_length: family == BARRED_SPIRAL ? scaled(0.28 + 0.22 * progress) : 0,
216
+ bar_length: family == BARRED_SPIRAL ? scaled(bar_length || (0.50 - 0.22 * progress)) : 0,
185
217
  phase_seed:,
186
218
  )
187
219
  end
188
220
 
221
+ def seeded_unit(seed, channel)
222
+ value = (seed ^ (channel * 0x9e37_79b9)) & UINT32_MASK
223
+ value = ((value ^ (value >> 16)) * 0x21f0_aaad) & UINT32_MASK
224
+ value = ((value ^ (value >> 15)) * 0x735a_2d97) & UINT32_MASK
225
+ value = (value ^ (value >> 15)) & UINT32_MASK
226
+ value.fdiv(UINT32_RANGE)
227
+ end
228
+
189
229
  def morphology(family, designation, ellipticity: 0, bulge_share: 0, arm_count: 0, winding: 0,
190
230
  arm_fraction: 0, bar_length: 0, clump_count: 0, clump_spread: 0, phase_seed: 0)
191
231
  {
@@ -3,12 +3,14 @@
3
3
  require "base64"
4
4
  require "fileutils"
5
5
  require "json"
6
- require "securerandom"
6
+ require_relative "artifact_marker"
7
+ require_relative "atomic_output"
7
8
  require_relative "report_asset_assembler"
8
9
 
9
10
  module RubyLens
10
11
  class ReportWriter
11
12
  MODEL_PLACEHOLDER = "{{MODEL_BASE64}}"
13
+ MARKER = '<meta name="generator" content="RubyLens">'
12
14
 
13
15
  def initialize(asset_assembler: ReportAssetAssembler.new)
14
16
  @asset_assembler = asset_assembler
@@ -26,14 +28,7 @@ module RubyLens
26
28
 
27
29
  payload = Base64.strict_encode64(JSON.generate(model))
28
30
  html = template.sub(MODEL_PLACEHOLDER, payload)
29
- atomic_write(output, html)
30
- output
31
- end
32
-
33
- def rubylens_report?(path)
34
- File.file?(path) && File.open(path, "rb") { |file| file.read(2048).include?('<meta name="generator" content="RubyLens">') }
35
- rescue Errno::ENOENT, Errno::EACCES
36
- false
31
+ AtomicOutput.replace(output) { |temporary| File.binwrite(temporary, html) }
37
32
  end
38
33
 
39
34
  private
@@ -42,17 +37,7 @@ module RubyLens
42
37
  return unless File.basename(directory) == ".rubylens"
43
38
 
44
39
  ignore = File.join(directory, ".gitignore")
45
- atomic_write(ignore, "*\n") unless File.exist?(ignore)
46
- end
47
-
48
- def atomic_write(path, contents)
49
- temporary = File.join(File.dirname(path), ".#{File.basename(path)}.#{SecureRandom.hex(6)}.tmp")
50
- File.open(temporary, File::WRONLY | File::CREAT | File::EXCL, 0o600) { |file| file.write(contents) }
51
- File.chmod(0o600, temporary)
52
- File.rename(temporary, path)
53
- File.chmod(0o600, path)
54
- ensure
55
- FileUtils.rm_f(temporary) if temporary && File.exist?(temporary)
40
+ AtomicOutput.replace(ignore) { |temporary| File.binwrite(temporary, "*\n") } unless File.exist?(ignore)
56
41
  end
57
42
  end
58
43
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "artifact_marker"
4
+ require_relative "default_output"
3
5
  require_relative "generator"
4
6
  require_relative "showcase_model"
5
7
  require_relative "showcase_writer"
@@ -17,17 +19,11 @@ module RubyLens
17
19
 
18
20
  def call
19
21
  root = File.realpath(@path)
20
- showcase_writer = ShowcaseWriter.new
21
- output = @output
22
- if output.nil?
23
- output = File.join(root, DEFAULT_SHOWCASE_NAME)
24
- if File.exist?(output) && !showcase_writer.rubylens_showcase?(output)
25
- raise Error, "default showcase path already exists and is not a RubyLens showcase"
26
- end
27
- GitRepository.new(root).exclude_local(output, description: "showcase")
22
+ output = @output || DefaultOutput.resolve(root: root, name: DEFAULT_SHOWCASE_NAME, description: "showcase") do |existing|
23
+ ArtifactMarker.present?(existing, ShowcaseWriter::MARKER)
28
24
  end
29
25
  model, warnings = GenerationPipeline.new(root:, lockfile: @lockfile).call
30
- output_path = showcase_writer.write(ShowcaseModel.new(model, details: @details).call, output: output)
26
+ output_path = ShowcaseWriter.new.write(ShowcaseModel.new(model, details: @details).call, output: output)
31
27
 
32
28
  Result.new(output_path:, counts: model.fetch("totals").freeze, warnings:)
33
29
  rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP => error
@@ -6,7 +6,7 @@ require_relative "errors"
6
6
  module RubyLens
7
7
  class ShowcaseModel
8
8
  SIGNAL_FIELDS = ArtModelBuilder::SIGNAL_FIELDS
9
- TOTAL_FIELDS = %w[namespaces packages dependencyStars renderedDependencyStars].freeze
9
+ TOTAL_FIELDS = %w[namespaces packages dependencyStars].freeze
10
10
  CATEGORY_FIELDS = %w[core tests].freeze
11
11
  ANNOTATION_LIMIT = 200
12
12
  ANNOTATION_CATEGORIES = %w[core dependencies tests].freeze
@@ -14,7 +14,7 @@ module RubyLens
14
14
  RUBY_NAME_PATTERN = /\A\p{Lu}[\p{L}\p{N}_]*(?:::\p{Lu}[\p{L}\p{N}_]*)*\z/
15
15
  MAX_ANNOTATION_NAME_LENGTH = 160
16
16
  RSPEC_PROXY_PREFIX = "RSpec example group #"
17
- LEGACY_MORPHOLOGY_ROW = [MorphologyClassifier::SPIRAL, *MorphologyClassifier::DEFAULT_KNOBS].freeze
17
+ FALLBACK_MORPHOLOGY_ROW = [MorphologyClassifier::SPIRAL, *MorphologyClassifier::DEFAULT_KNOBS].freeze
18
18
 
19
19
  def initialize(model, details: false)
20
20
  @model = model
@@ -27,12 +27,13 @@ module RubyLens
27
27
  raise Error, "package morphology rows must align with packages" unless package_morphologies.length == packages.length
28
28
 
29
29
  showcase = {
30
- "schema" => "rubylens.showcase.v4",
30
+ "schema" => "rubylens.showcase.v7",
31
31
  "projectName" => @model.fetch("projectName"),
32
32
  "details" => @details,
33
33
  "domains" => project_hash(@model.fetch("domains"), SIGNAL_FIELDS),
34
34
  "morphology" => morphology_row,
35
- "namespaces" => @model.fetch("namespaces").map { |row| numeric_row(row, 15) },
35
+ "namespaces" => @model.fetch("namespaces").map { |row| numeric_row(row, 14) },
36
+ "constantReferenceLinks" => @model.fetch("constantReferenceLinks", []).map { |row| numeric_row(row, 2) },
36
37
  "packages" => packages.map { |row| numeric_row(row, 9) },
37
38
  "packageMorphologies" => package_morphologies.map { |row| numeric_row(row, 10) },
38
39
  "dependencySystems" => @model.fetch("dependencySystems", []).map { |row| numeric_row(row, 2) },
@@ -54,14 +55,11 @@ module RubyLens
54
55
 
55
56
  def morphology_row
56
57
  morphology = @model["morphology"]
57
- return LEGACY_MORPHOLOGY_ROW.dup unless morphology.is_a?(Hash)
58
+ return FALLBACK_MORPHOLOGY_ROW.dup unless morphology.is_a?(Array)
58
59
 
59
- knobs = morphology["knobs"]
60
- return LEGACY_MORPHOLOGY_ROW.dup unless knobs.is_a?(Array)
61
-
62
- numeric_row([morphology.fetch("family"), *knobs], 10)
63
- rescue KeyError, Error
64
- LEGACY_MORPHOLOGY_ROW.dup
60
+ numeric_row(morphology, 10)
61
+ rescue Error
62
+ FALLBACK_MORPHOLOGY_ROW.dup
65
63
  end
66
64
 
67
65
  def project_hash(source, fields)
@@ -112,14 +110,14 @@ module RubyLens
112
110
  rows = @model.fetch("namespaces")
113
111
  names.each_with_index.filter_map do |name, index|
114
112
  row = rows.fetch(index)
115
- next unless (row.fetch(3) == 1) == test
113
+ next unless (row.fetch(2) == 1) == test
116
114
  next if OMITTED_ANNOTATION_NAMES.include?(name)
117
115
  next unless safe_ruby_name?(name)
118
116
 
119
- [row.slice(4, 6).sum, {
117
+ [row.slice(3, 6).sum, {
120
118
  "category" => test ? "tests" : "core",
121
119
  "name" => name,
122
- "kind" => row.fetch(2) == 0 ? "Class" : "Module",
120
+ "kind" => row.fetch(1) == 0 ? "Class" : "Module",
123
121
  "anchor" => index,
124
122
  }]
125
123
  end.sort_by { |rank, candidate| [-rank, candidate.fetch("name"), candidate.fetch("anchor")] }
@@ -16,11 +16,5 @@ module RubyLens
16
16
  runtime_path: RUNTIME_PATH,
17
17
  ))
18
18
  end
19
-
20
- def rubylens_showcase?(path)
21
- File.file?(path) && File.open(path, "rb") { |file| file.read(2048).include?(MARKER) }
22
- rescue Errno::ENOENT, Errno::EACCES
23
- false
24
- end
25
19
  end
26
20
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLens
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/rubylens.rb CHANGED
@@ -8,6 +8,8 @@ end
8
8
  require_relative "rubylens/version"
9
9
  require_relative "rubylens/errors"
10
10
  require_relative "rubylens/paths"
11
+ require_relative "rubylens/atomic_output"
12
+ require_relative "rubylens/default_output"
11
13
  require_relative "rubylens/git_repository"
12
14
  require_relative "rubylens/dependency_warning"
13
15
  require_relative "rubylens/index/manifest"
@@ -20,6 +22,7 @@ require_relative "rubylens/generator"
20
22
  require_relative "rubylens/showcase_model"
21
23
  require_relative "rubylens/showcase_writer"
22
24
  require_relative "rubylens/showcase_generator"
25
+ require_relative "rubylens/clip_generator"
23
26
 
24
27
  module RubyLens
25
28
  module_function
@@ -31,4 +34,8 @@ module RubyLens
31
34
  def generate_showcase(path: Dir.pwd, output: nil, lockfile: nil, details: false)
32
35
  ShowcaseGenerator.new(path: path, output: output, lockfile: lockfile, details: details).call
33
36
  end
37
+
38
+ def generate_clip(path: Dir.pwd, output: nil, lockfile: nil, details: false, progress: nil)
39
+ ClipGenerator.new(path: path, output: output, lockfile: lockfile, details: details, progress: progress).call
40
+ end
34
41
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubylens
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - RubyLens contributors
@@ -55,7 +55,16 @@ files:
55
55
  - exe/rubylens
56
56
  - lib/rubylens.rb
57
57
  - lib/rubylens/art_model_builder.rb
58
+ - lib/rubylens/artifact_marker.rb
59
+ - lib/rubylens/atomic_output.rb
58
60
  - lib/rubylens/cli.rb
61
+ - lib/rubylens/clip/chrome_page.rb
62
+ - lib/rubylens/clip/deadline_io.rb
63
+ - lib/rubylens/clip/renderer.rb
64
+ - lib/rubylens/clip/toolchain.rb
65
+ - lib/rubylens/clip/web_socket_channel.rb
66
+ - lib/rubylens/clip_generator.rb
67
+ - lib/rubylens/default_output.rb
59
68
  - lib/rubylens/dependency_warning.rb
60
69
  - lib/rubylens/errors.rb
61
70
  - lib/rubylens/generator.rb