latticerb 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f5250f7a9782b3db18aa0002eda4b3194f1092d0a832e4b764ce1c49cbb8f202
4
- data.tar.gz: 7f2a83e342e5498728f3fac15d20e6706859a10a7b2697b666ccb0e67e72bccf
3
+ metadata.gz: 60031e0fe6baf8ed371a9e2705e905118008bd25c53f588754405399d7727429
4
+ data.tar.gz: 893b27940cca00121e203cf8ed0e685cdddfc8d7ee97758d0b1864d7eef1cb6b
5
5
  SHA512:
6
- metadata.gz: 51b3c0f8c486a628f9e4274d2a6f66b2f79a7eb75c519e892c760cd3c65cdebedb005cd845189505f29bcac63ac716c6f3d0d249f38fb4c3a5a8a96d4543a381
7
- data.tar.gz: 781caca48f95fc5ef3d8562c717ebe4dd2b987630a6ca2d5a886f51a8db0c06abdfeb4f19713531133c3e27a9149861821ff093385ad8b2f649d9ef5b118aa74
6
+ metadata.gz: 7d13bb576b7255cb8cfb255de5cf725d5a344ae8264861fcb0207803999b246fdf5ebf5ba4f07ba511846fbc8301df26256ccda8f48a7c785d66022570ac5a7d
7
+ data.tar.gz: fb8d83f8f505e560cdb1277e419a53dea19d2a9350c9090b04c2b5a380b9e6dbbaccd040b3d617d6d77bb1fa1fb1987b499d94cb5cf3803d8d27c54b9c5cbf34
data/README.md CHANGED
@@ -54,7 +54,7 @@ run = Lattice.generate(
54
54
 
55
55
  run.scenarios # array of { name => value } hashes
56
56
  run.coverage_curve # cumulative coverage per scenario
57
- run.to_table # human-readable table (also: to_json_output, to_csv, to_summary)
57
+ run.to_table # human-readable table (also: to_json_output, to_yaml_output, to_csv, to_summary)
58
58
 
59
59
  Lattice.scenarios(file: "...") # shortcut straight to the hashes
60
60
  Lattice.validate!(schema) # raises Lattice::ValidationError / Lattice::ConstraintError
@@ -70,6 +70,7 @@ The gem installs a `lattice` executable mirroring the Python CLI:
70
70
  cat schema.yaml | lattice validate
71
71
  cat schema.yaml | lattice generate # JSON by default
72
72
  lattice generate schema.yaml --format table
73
+ lattice generate schema.yaml --format yaml # same content as JSON, for hand-edited files
73
74
  lattice generate schema.yaml --strength 3 --seed 7
74
75
  lattice agent instructions # coding-agent integration guidance
75
76
  ```
data/lib/lattice/cli.rb CHANGED
@@ -65,7 +65,7 @@ module Lattice
65
65
  }
66
66
  parser = OptionParser.new do |opts|
67
67
  opts.on("--strength STRENGTH", Integer)
68
- opts.on("--format FORMAT", %w[table json csv summary])
68
+ opts.on("--format FORMAT", %w[table json csv summary yaml])
69
69
  opts.on("--seed SEED", Integer)
70
70
  opts.on("--pool-size SIZE", Integer) { |value| options[:pool_size] = value }
71
71
  opts.on("--max-rows ROWS", Integer) { |value| options[:max_rows] = value }
@@ -7,8 +7,9 @@ require_relative "ipog"
7
7
  require_relative "model"
8
8
 
9
9
  module Lattice
10
- # Output rendering. Table, JSON, CSV, and summary formats are byte-identical
11
- # to the Python implementation's output for the same model and result.
10
+ # Output rendering. Table, JSON, CSV, summary, and YAML formats are
11
+ # byte-identical to the Python implementation's output for the same model
12
+ # and result.
12
13
  module Formatters
13
14
  extend self
14
15
 
@@ -18,6 +19,7 @@ module Lattice
18
19
  when "json" then format_json(model, result, seed:)
19
20
  when "csv" then format_csv(model, result)
20
21
  when "summary" then format_summary(model, result)
22
+ when "yaml" then format_yaml(model, result, seed:)
21
23
  else
22
24
  raise ArgumentError, "Unknown format `#{output_format}`."
23
25
  end
@@ -77,16 +79,81 @@ module Lattice
77
79
  }
78
80
  end
79
81
  }
80
- JSON.pretty_generate(payload, ascii_only: true)
82
+ # Python's json.dumps also escapes DEL, which Ruby's ascii_only leaves raw.
83
+ JSON.pretty_generate(payload, ascii_only: true).gsub("\u007f", "\\u007f")
81
84
  end
82
85
 
83
- def format_csv(model, result)
84
- CSV.generate(row_sep: "\r\n") do |csv|
85
- csv << model.parameter_names
86
- named_rows(model, result).each do |row|
87
- csv << model.parameter_names.map { |name| row[name] }
86
+ # Same content as format_json, written by hand in one fixed style so Ruby
87
+ # and Python emit identical bytes (Psych and PyYAML each quote and wrap
88
+ # differently). Every string is double-quoted, so no value can be read
89
+ # back as a boolean, null, number or date.
90
+ def format_yaml(model, result, seed:)
91
+ last_point = result.coverage_curve.last
92
+ lines = [
93
+ "meta:",
94
+ " model_name: #{yaml_string(model.model_name)}",
95
+ " strength: #{model.strength}",
96
+ " test_count: #{result.rows.length}",
97
+ " exhaustive_count: #{model.exhaustive_count}",
98
+ " reduction: #{yaml_string(reduction_label(model.exhaustive_count, result.rows.length))}",
99
+ " coverage: #{yaml_string(last_point ? format('%.1f%%', last_point.cumulative_pct) : '0.0%')}",
100
+ " seed: #{seed.nil? ? 'null' : seed}"
101
+ ]
102
+
103
+ if result.coverage_curve.empty?
104
+ lines << "coverage_curve: []"
105
+ else
106
+ lines << "coverage_curve:"
107
+ result.coverage_curve.each do |point|
108
+ lines << " - scenario: #{point.scenario}"
109
+ lines << " cumulative_pct: #{JSON.generate(point.cumulative_pct)}"
110
+ end
111
+ end
112
+
113
+ rows = named_rows(model, result)
114
+ if rows.empty?
115
+ lines << "scenarios: []"
116
+ else
117
+ lines << "scenarios:"
118
+ rows.each_with_index do |row, index|
119
+ lines << " - id: #{index + 1}"
120
+ lines << " values:"
121
+ model.parameter_names.each do |name|
122
+ lines << " #{yaml_string(name)}: #{yaml_string(row[name])}"
123
+ end
88
124
  end
89
- end.strip
125
+ end
126
+ lines.join("\n")
127
+ end
128
+
129
+ YAML_ESCAPES = {
130
+ "\"" => "\\\"", "\\" => "\\\\", "\b" => "\\b", "\t" => "\\t", "\n" => "\\n", "\f" => "\\f", "\r" => "\\r"
131
+ }.freeze
132
+
133
+ # Characters escaped inside a double-quoted YAML string: the quote and
134
+ # backslash, C0 controls, DEL, C1 controls (U+0085 would otherwise fold to
135
+ # a space), the Unicode line and paragraph separators, the byte-order mark,
136
+ # and the two noncharacters YAML cannot hold. Everything else is written as
137
+ # UTF-8, so accented letters and emoji stay readable.
138
+ YAML_ESCAPED_CHARS = /["\\\u0000-\u001f\u007f-\u009f\u2028\u2029\ufeff\ufffe\uffff]/
139
+
140
+ def yaml_string(value)
141
+ escaped = value.gsub(YAML_ESCAPED_CHARS) do |char|
142
+ YAML_ESCAPES.fetch(char) { format("\\u%04x", char.ord) }
143
+ end
144
+ "\"#{escaped}\""
145
+ end
146
+
147
+ # Matches Python's csv.writer: empty fields are written bare, except a row
148
+ # made of a single empty field, which is written as "" so it isn't a blank
149
+ # line.
150
+ def format_csv(model, result)
151
+ records = [model.parameter_names] + named_rows(model, result).map do |row|
152
+ model.parameter_names.map { |name| row[name] }
153
+ end
154
+ records.map do |record|
155
+ record == [""] ? "\"\"\r\n" : CSV.generate_line(record, row_sep: "\r\n", quote_empty: false)
156
+ end.join.strip
90
157
  end
91
158
 
92
159
  def headline(model, result)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lattice
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/lattice.rb CHANGED
@@ -32,6 +32,7 @@ module Lattice
32
32
  def to_json_output = Formatters.render_output(model, result, output_format: "json", seed:)
33
33
  def to_csv = Formatters.render_output(model, result, output_format: "csv", seed:)
34
34
  def to_summary = Formatters.render_output(model, result, output_format: "summary", seed:)
35
+ def to_yaml_output = Formatters.render_output(model, result, output_format: "yaml", seed:)
35
36
  end
36
37
 
37
38
  class << self
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: latticerb
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
  - Tyler Klose