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 +4 -4
- data/README.md +12 -1
- data/lib/lattice/parser.rb +27 -19
- data/lib/lattice/schema_text.rb +153 -0
- data/lib/lattice/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e1cd02dabe1821baf68283ad31fb90aa688bd1642943fa2f5d914cad759389fe
|
|
4
|
+
data.tar.gz: 4c1f1118737aa6fe2a2917db077fd6badf3f03fc169c110bf5c5255a1b8376aa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
|
data/lib/lattice/parser.rb
CHANGED
|
@@ -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 ?
|
|
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 |
|
|
355
|
-
|
|
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
|
|
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
|
|
373
|
-
|
|
374
|
-
|
|
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
|
data/lib/lattice/version.rb
CHANGED
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.
|
|
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
|