latticerb 0.2.0 → 0.3.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: 60031e0fe6baf8ed371a9e2705e905118008bd25c53f588754405399d7727429
4
- data.tar.gz: 893b27940cca00121e203cf8ed0e685cdddfc8d7ee97758d0b1864d7eef1cb6b
3
+ metadata.gz: e1cd02dabe1821baf68283ad31fb90aa688bd1642943fa2f5d914cad759389fe
4
+ data.tar.gz: 4c1f1118737aa6fe2a2917db077fd6badf3f03fc169c110bf5c5255a1b8376aa
5
5
  SHA512:
6
- metadata.gz: 7d13bb576b7255cb8cfb255de5cf725d5a344ae8264861fcb0207803999b246fdf5ebf5ba4f07ba511846fbc8301df26256ccda8f48a7c785d66022570ac5a7d
7
- data.tar.gz: fb8d83f8f505e560cdb1277e419a53dea19d2a9350c9090b04c2b5a380b9e6dbbaccd040b3d617d6d77bb1fa1fb1987b499d94cb5cf3803d8d27c54b9c5cbf34
6
+ metadata.gz: '087a54e0c2896d3a71ffb4eb8737dbf3ad2a0e04ed36682d8fa232cee7b7a131f706cdf5456277c2c6b3eef59446c263dd8ff4277879345160b386c64b06f8d0'
7
+ data.tar.gz: 0fceafc2667a8042a38c7dc292b42d185b7305cf57cbf2a7d21bab7d1915723d0fd5fb33c27a444a349b9bff148357a7313f55542af393a69cf69402551426f3
data/README.md CHANGED
@@ -99,11 +99,22 @@ constraints:
99
99
 
100
100
  Constraint types: `invalid_pair`, `higher_order`, `forward_dep`, `bidirectional`, `conditional`, `forced`. Run `lattice agent instructions` for the full reference.
101
101
 
102
+ ### Values keep their text
103
+
104
+ Every value in a schema (the model name, parameter names and values, and constraint references) is used exactly as written. In YAML, unquoted `1.10`, `0x1F`, `.inf` or `2026-10-26T10:00:00Z` stay as typed rather than being turned into numbers or dates. The exceptions:
105
+
106
+ - Unquoted `yes`, `no`, `on`, `off`, `true` and `false` (lowercase, Capitalized or UPPERCASE) become `true` / `false`, and an empty value, `~` or `null` becomes `null`. Quote them (`"yes"`) to keep the word.
107
+ - JSON integers are written in plain decimal form (`-0` becomes `0`).
108
+
109
+ `strength` must be a whole number, and `weights` decimal numbers such as `0.7` or `1e-3`. Anchors, aliases and `<<` merge keys work; a key written twice, more than one YAML document, or a tag other than YAML's standard ones is an error.
110
+
111
+ Changed in 0.3.0: earlier versions converted unquoted numbers and dates, so `0x1F` became `31`, `1.10` became `1.1`, and some values (`.inf`, timestamps) came out differently in the Ruby and Python implementations.
112
+
102
113
  ## Parity with the Python implementation
103
114
 
104
115
  The generator seeds an MT19937 RNG that reproduces CPython's `random.Random` bit-for-bit, so both implementations walk the same candidate pools and pick the same rows. `test/python_parity_test.rb` shells out to the Python implementation (checkout via `LATTICE_PYTHON_SRC`, or an installed `lattice-cli`) and asserts byte-equal output across formats, seeds, strengths, and stopping rules; it skips when no Python implementation is available.
105
116
 
106
- YAML is parsed with Psych here and PyYAML in Python — both YAML 1.1 (libyaml lineage), so common scalars including unquoted `yes`/`no`/`on`/`off` booleans resolve identically. Exotic YAML 1.1 corners (e.g. sexagesimal numbers) may differ between the two parsers; quote ambiguous scalars or use JSON if in doubt.
117
+ Schemas are read with Psych here and PyYAML in Python, but both implementations build values from the parsed text with the same rules (see "Values keep their text"), so a schema means the same thing in either. `test/fixtures/scalar_text.*` pins those rules for both.
107
118
 
108
119
  ## Development
109
120
 
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
- require "yaml"
5
-
6
3
  require_relative "model"
4
+ require_relative "schema_text"
7
5
 
8
6
  module Lattice
9
7
  class ModelIOError < StandardError; end
@@ -78,7 +76,7 @@ module Lattice
78
76
 
79
77
  errors = []
80
78
  loaders.each do |loader|
81
- return loader == :json ? JSON.parse(text) : safe_load_yaml(text)
79
+ return loader == :json ? SchemaText.load_json(text) : SchemaText.load_yaml(text)
82
80
  rescue StandardError => e
83
81
  errors << e.message
84
82
  end
@@ -86,10 +84,6 @@ module Lattice
86
84
  raise ModelIOError, "Unable to parse model from `#{source_name}`: #{errors.join('; ')}"
87
85
  end
88
86
 
89
- def safe_load_yaml(text)
90
- YAML.safe_load(text, permitted_classes: [Date, Time], aliases: true)
91
- end
92
-
93
87
  def parse_parameters(raw_parameters, errors)
94
88
  parameters = {}
95
89
  if raw_parameters.nil?
@@ -351,8 +345,9 @@ module Lattice
351
345
  end
352
346
 
353
347
  parsed = []
354
- raw_weights.each do |weight|
355
- unless weight.is_a?(Numeric)
348
+ raw_weights.each do |raw_weight|
349
+ weight = parse_number(raw_weight)
350
+ if weight.nil?
356
351
  errors << "`weights` for parameter `#{parameter_name}` must be numeric."
357
352
  return nil
358
353
  end
@@ -360,24 +355,37 @@ module Lattice
360
355
  errors << "`weights` for parameter `#{parameter_name}` must be greater than zero."
361
356
  return nil
362
357
  end
363
- parsed << weight.to_f
358
+ parsed << weight
364
359
  end
365
360
  parsed
366
361
  end
367
362
 
363
+ DECIMAL_TEXT = /\A[-+]?[0-9]+(\.[0-9]+)?([eE][-+]?[0-9]+)?\z/
364
+ INTEGER_TEXT = /\A[-+]?[0-9]+\z/
365
+
366
+ # A finite number, from a Ruby number or decimal text such as "0.7" or
367
+ # "1e-3" (schema files keep numbers as text). Booleans are not numbers.
368
+ def parse_number(raw)
369
+ number =
370
+ case raw
371
+ when Integer, Float then raw.to_f
372
+ when String then DECIMAL_TEXT.match?(raw) ? Float(raw) : nil
373
+ end
374
+ number&.finite? ? number : nil
375
+ end
376
+
368
377
  def parse_strength(raw_strength, parameter_count, errors)
369
378
  strength =
370
379
  case raw_strength
371
380
  when nil then 2
372
- when Numeric then raw_strength.to_i
373
- else
374
- begin
375
- Integer(raw_strength)
376
- rescue TypeError, ArgumentError
377
- errors << "`strength` must be an integer."
378
- return 2
379
- end
381
+ when Integer then raw_strength
382
+ when Float then raw_strength.finite? ? raw_strength.to_i : nil
383
+ when String then INTEGER_TEXT.match?(raw_strength) ? Integer(raw_strength, 10) : nil
380
384
  end
385
+ if strength.nil?
386
+ errors << "`strength` must be an integer."
387
+ return 2
388
+ end
381
389
 
382
390
  errors << "`strength` must be at least 1." if strength < 1
383
391
  errors << "`strength` cannot exceed the number of parameters." if parameter_count.positive? && strength > parameter_count
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "psych"
5
+
6
+ module Lattice
7
+ # Reads schema text (JSON or YAML) into plain Hashes, Arrays and Strings,
8
+ # keeping every value as the text the user wrote. `1.10` stays "1.10",
9
+ # `0x1F` stays "0x1F" and `2026-10-26T10:00:00Z` stays as typed, instead of
10
+ # being converted to a number or time and printed back differently by Ruby
11
+ # and Python. The only conversions:
12
+ #
13
+ # - unquoted yes/no/on/off/true/false (in the case forms below) become
14
+ # true/false, and an empty value, ~ or null becomes nil;
15
+ # - JSON integers are written in plain decimal form.
16
+ #
17
+ # The Python implementation (lattice.schema_text) follows the same rules, and
18
+ # test/fixtures/scalar_text.* pins the result for both.
19
+ module SchemaText
20
+ extend self
21
+
22
+ class SyntaxError < StandardError; end
23
+
24
+ TRUE_WORDS = %w[yes Yes YES true True TRUE on On ON].freeze
25
+ FALSE_WORDS = %w[no No NO false False FALSE off Off OFF].freeze
26
+ NULL_WORDS = ["", "~", "null", "Null", "NULL"].freeze
27
+
28
+ YAML_TAG_PREFIX = "tag:yaml.org,2002:"
29
+ STR_TAGS = ["!", "#{YAML_TAG_PREFIX}str"].freeze
30
+ SCALAR_TAGS = %w[str int float bool null timestamp binary merge value].map { |name| "#{YAML_TAG_PREFIX}#{name}" }.freeze
31
+ SEQUENCE_TAGS = [nil, "!", "#{YAML_TAG_PREFIX}seq"].freeze
32
+ MAPPING_TAGS = [nil, "!", "#{YAML_TAG_PREFIX}map"].freeze
33
+
34
+ # Hands JSON floats back as their source text.
35
+ class RawNumber
36
+ def self.new(text) = text
37
+ end
38
+
39
+ # Rejects a key written twice. json 2.x calls []= on a custom object_class;
40
+ # json 3.x checks duplicates itself when allow_duplicate_key is false.
41
+ class UniqueHash < Hash
42
+ def []=(key, value)
43
+ raise JSON::ParserError, "duplicate key `#{key}`" if key?(key)
44
+
45
+ super
46
+ end
47
+ end
48
+
49
+ def load_json(text)
50
+ json_strings(
51
+ JSON.parse(text, decimal_class: RawNumber, object_class: UniqueHash, allow_duplicate_key: false)
52
+ )
53
+ end
54
+
55
+ def load_yaml(text)
56
+ documents = Psych.parse_stream(text).children
57
+ raise SyntaxError, "expected a single YAML document, found #{documents.length}" if documents.length > 1
58
+ return nil if documents.empty?
59
+
60
+ yaml_value(documents.first.root, {})
61
+ end
62
+
63
+ private
64
+
65
+ def json_strings(value)
66
+ case value
67
+ when Hash then value.to_h { |key, child| [key, json_strings(child)] }
68
+ when Array then value.map { |child| json_strings(child) }
69
+ when Integer then value.to_s
70
+ else value
71
+ end
72
+ end
73
+
74
+ # `anchors` maps each anchor to its value once built; an anchor still
75
+ # being built (nil value) means an alias points back into itself.
76
+ def yaml_value(node, anchors)
77
+ return yaml_alias(node, anchors) if node.is_a?(Psych::Nodes::Alias)
78
+
79
+ anchors[node.anchor] = nil if node.anchor
80
+ value =
81
+ case node
82
+ when Psych::Nodes::Scalar then yaml_scalar(node)
83
+ when Psych::Nodes::Sequence
84
+ check_tag(node.tag, SEQUENCE_TAGS)
85
+ node.children.map { |child| yaml_value(child, anchors) }
86
+ when Psych::Nodes::Mapping then yaml_mapping(node, anchors)
87
+ end
88
+ anchors[node.anchor] = [value] if node.anchor
89
+ value
90
+ end
91
+
92
+ def yaml_alias(node, anchors)
93
+ built = anchors.fetch(node.anchor) { raise SyntaxError, "unknown alias `*#{node.anchor}`" }
94
+ raise SyntaxError, "recursive alias" if built.nil?
95
+
96
+ built.first
97
+ end
98
+
99
+ def yaml_scalar(node)
100
+ check_tag(node.tag, [nil, "!"] + SCALAR_TAGS)
101
+ return node.value unless node.style == Psych::Nodes::Scalar::PLAIN && !STR_TAGS.include?(node.tag)
102
+ return true if TRUE_WORDS.include?(node.value)
103
+ return false if FALSE_WORDS.include?(node.value)
104
+ return nil if NULL_WORDS.include?(node.value)
105
+
106
+ node.value
107
+ end
108
+
109
+ # Keys from `<<` merges come first, earlier sources winning; the
110
+ # mapping's own keys then override them. A key written twice is an error.
111
+ def yaml_mapping(node, anchors)
112
+ check_tag(node.tag, MAPPING_TAGS)
113
+ merged = {}
114
+ own = {}
115
+ seen_merge = false
116
+ node.children.each_slice(2) do |key_node, value_node|
117
+ key = yaml_value(key_node, anchors)
118
+ raise SyntaxError, "mapping keys must be scalars" if key.is_a?(Enumerable)
119
+
120
+ if merge_key?(key_node)
121
+ raise SyntaxError, "duplicate key `<<`" if seen_merge
122
+
123
+ seen_merge = true
124
+ merge_sources(yaml_value(value_node, anchors)).each do |source|
125
+ source.each { |merged_key, merged_value| merged[merged_key] = merged_value unless merged.key?(merged_key) }
126
+ end
127
+ next
128
+ end
129
+
130
+ raise SyntaxError, "duplicate key `#{Lattice.normalize_scalar(key)}`" if own.key?(key)
131
+
132
+ own[key] = yaml_value(value_node, anchors)
133
+ end
134
+ merged.merge(own)
135
+ end
136
+
137
+ def merge_key?(key_node)
138
+ key_node.is_a?(Psych::Nodes::Scalar) && key_node.style == Psych::Nodes::Scalar::PLAIN && key_node.value == "<<" &&
139
+ [nil, "#{YAML_TAG_PREFIX}merge"].include?(key_node.tag)
140
+ end
141
+
142
+ def merge_sources(value)
143
+ sources = [value].flatten(1)
144
+ raise SyntaxError, "merge key `<<` needs a mapping or a list of mappings" unless sources.all?(Hash)
145
+
146
+ sources
147
+ end
148
+
149
+ def check_tag(tag, allowed)
150
+ raise SyntaxError, "unsupported YAML tag `#{tag}`" unless allowed.include?(tag)
151
+ end
152
+ end
153
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lattice
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
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.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tyler Klose
@@ -56,6 +56,7 @@ files:
56
56
  - lib/lattice/resources/generic/lattice-workflow/SKILL.md
57
57
  - lib/lattice/resources/generic/lattice-workflow/references/modeling-rules.md
58
58
  - lib/lattice/resources/generic/lattice-workflow/references/worked-examples.md
59
+ - lib/lattice/schema_text.rb
59
60
  - lib/lattice/version.rb
60
61
  - lib/latticerb.rb
61
62
  homepage: https://github.com/tylerklose/lattice-ruby