lattice-cli 0.1.2__tar.gz → 0.2.0__tar.gz

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.
Files changed (40) hide show
  1. {lattice_cli-0.1.2/src/lattice_cli.egg-info → lattice_cli-0.2.0}/PKG-INFO +3 -2
  2. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/README.md +2 -1
  3. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/pyproject.toml +1 -1
  4. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice/__init__.py +1 -1
  5. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice/cli.py +1 -1
  6. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice/formatters.py +70 -0
  7. {lattice_cli-0.1.2 → lattice_cli-0.2.0/src/lattice_cli.egg-info}/PKG-INFO +3 -2
  8. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice_cli.egg-info/SOURCES.txt +2 -1
  9. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/tests/test_cli.py +1 -1
  10. lattice_cli-0.2.0/tests/test_yaml_format.py +130 -0
  11. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/LICENSE +0 -0
  12. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/setup.cfg +0 -0
  13. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice/__main__.py +0 -0
  14. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice/agent.py +0 -0
  15. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice/constraints.py +0 -0
  16. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice/ipog.py +0 -0
  17. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice/model.py +0 -0
  18. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice/parser.py +0 -0
  19. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice/resources/claude/lattice-workflow/SKILL.md +0 -0
  20. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice/resources/claude/lattice-workflow/references/modeling-rules.md +0 -0
  21. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice/resources/claude/lattice-workflow/references/worked-examples.md +0 -0
  22. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice/resources/codex/lattice-workflow/SKILL.md +0 -0
  23. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice/resources/codex/lattice-workflow/agents/openai.yaml +0 -0
  24. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice/resources/codex/lattice-workflow/references/modeling-rules.md +0 -0
  25. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice/resources/codex/lattice-workflow/references/worked-examples.md +0 -0
  26. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice/resources/generic/lattice-workflow/SKILL.md +0 -0
  27. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice/resources/generic/lattice-workflow/references/modeling-rules.md +0 -0
  28. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice/resources/generic/lattice-workflow/references/worked-examples.md +0 -0
  29. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice_cli.egg-info/dependency_links.txt +0 -0
  30. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice_cli.egg-info/entry_points.txt +0 -0
  31. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice_cli.egg-info/requires.txt +0 -0
  32. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/src/lattice_cli.egg-info/top_level.txt +0 -0
  33. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/tests/test_agent_bootstrap_matrix.py +0 -0
  34. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/tests/test_constraints.py +0 -0
  35. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/tests/test_examples.py +0 -0
  36. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/tests/test_ipog.py +0 -0
  37. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/tests/test_model.py +0 -0
  38. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/tests/test_parser.py +0 -0
  39. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/tests/test_schema_guidance_matrix.py +0 -0
  40. {lattice_cli-0.1.2 → lattice_cli-0.2.0}/tests/test_self_hosting_matrix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lattice-cli
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: Deterministic combinatorial coverage for agent workflows and constrained variant surfaces.
5
5
  Author-email: Tyler Klose <tylerklose@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -105,11 +105,12 @@ Supported output formats:
105
105
  - `table`
106
106
  - `csv`
107
107
  - `summary`
108
+ - `yaml` (same content as `json`, for files people edit by hand)
108
109
 
109
110
  `generate` defaults to JSON because the primary consumer is usually a coding harness. File-based schemas are supported, but the schema file is transport, not the product surface.
110
111
 
111
112
  For long higher-strength runs, use `--progress` to stream coverage progress to
112
- stderr while keeping stdout clean for JSON, CSV, table, or summary output.
113
+ stderr while keeping stdout clean for JSON, YAML, CSV, table, or summary output.
113
114
  `--progress-every N` controls how many generated scenarios pass between
114
115
  updates.
115
116
 
@@ -77,11 +77,12 @@ Supported output formats:
77
77
  - `table`
78
78
  - `csv`
79
79
  - `summary`
80
+ - `yaml` (same content as `json`, for files people edit by hand)
80
81
 
81
82
  `generate` defaults to JSON because the primary consumer is usually a coding harness. File-based schemas are supported, but the schema file is transport, not the product surface.
82
83
 
83
84
  For long higher-strength runs, use `--progress` to stream coverage progress to
84
- stderr while keeping stdout clean for JSON, CSV, table, or summary output.
85
+ stderr while keeping stdout clean for JSON, YAML, CSV, table, or summary output.
85
86
  `--progress-every N` controls how many generated scenarios pass between
86
87
  updates.
87
88
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lattice-cli"
7
- version = "0.1.2"
7
+ version = "0.2.0"
8
8
  description = "Deterministic combinatorial coverage for agent workflows and constrained variant surfaces."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.1.2"
5
+ __version__ = "0.2.0"
@@ -92,7 +92,7 @@ def _build_parser() -> argparse.ArgumentParser:
92
92
  )
93
93
  generate.add_argument("model", nargs="?", help="Path to a JSON or YAML schema. Reads stdin when omitted.")
94
94
  generate.add_argument("--strength", type=int, help="Override the schema strength.")
95
- generate.add_argument("--format", default="json", choices=("table", "json", "csv", "summary"))
95
+ generate.add_argument("--format", default="json", choices=("table", "json", "csv", "summary", "yaml"))
96
96
  generate.add_argument("--seed", type=int, default=42)
97
97
  generate.add_argument("--pool-size", type=int, default=50)
98
98
  generate.add_argument(
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import csv
4
4
  import io
5
5
  import json
6
+ import re
6
7
 
7
8
  from lattice.ipog import GenerationResult
8
9
  from lattice.model import Model
@@ -17,6 +18,8 @@ def render_output(model: Model, result: GenerationResult, *, output_format: str,
17
18
  return format_csv(model, result)
18
19
  if output_format == "summary":
19
20
  return format_summary(model, result)
21
+ if output_format == "yaml":
22
+ return format_yaml(model, result, seed=seed)
20
23
  raise ValueError(f"Unknown format `{output_format}`.")
21
24
 
22
25
 
@@ -85,6 +88,73 @@ def format_json(model: Model, result: GenerationResult, *, seed: int) -> str:
85
88
  return json.dumps(payload, indent=2)
86
89
 
87
90
 
91
+ def format_yaml(model: Model, result: GenerationResult, *, seed: int | None) -> str:
92
+ """Same content as format_json, written by hand in one fixed style.
93
+
94
+ PyYAML and Ruby's Psych quote and wrap differently, so the Python and Ruby
95
+ implementations write YAML themselves to emit identical bytes. Every string
96
+ is double-quoted, so no value can be read back as a boolean, null, number
97
+ or date.
98
+ """
99
+ coverage = f"{result.coverage_curve[-1].cumulative_pct:.1f}%" if result.coverage_curve else "0.0%"
100
+ lines = [
101
+ "meta:",
102
+ f" model_name: {_yaml_string(model.model_name)}",
103
+ f" strength: {model.strength}",
104
+ f" test_count: {len(result.rows)}",
105
+ f" exhaustive_count: {model.exhaustive_count}",
106
+ f" reduction: {_yaml_string(_reduction_label(model.exhaustive_count, len(result.rows)))}",
107
+ f" coverage: {_yaml_string(coverage)}",
108
+ f" seed: {'null' if seed is None else seed}",
109
+ ]
110
+
111
+ if not result.coverage_curve:
112
+ lines.append("coverage_curve: []")
113
+ else:
114
+ lines.append("coverage_curve:")
115
+ for point in result.coverage_curve:
116
+ lines.append(f" - scenario: {point.scenario}")
117
+ lines.append(f" cumulative_pct: {json.dumps(point.cumulative_pct)}")
118
+
119
+ rows = _named_rows(model, result)
120
+ if not rows:
121
+ lines.append("scenarios: []")
122
+ else:
123
+ lines.append("scenarios:")
124
+ for index, row in enumerate(rows):
125
+ lines.append(f" - id: {index + 1}")
126
+ lines.append(" values:")
127
+ for name in model.parameter_names:
128
+ lines.append(f" {_yaml_string(name)}: {_yaml_string(row[name])}")
129
+ return "\n".join(lines)
130
+
131
+
132
+ _YAML_ESCAPES = {
133
+ '"': '\\"',
134
+ "\\": "\\\\",
135
+ "\b": "\\b",
136
+ "\t": "\\t",
137
+ "\n": "\\n",
138
+ "\f": "\\f",
139
+ "\r": "\\r",
140
+ }
141
+
142
+ # Characters escaped inside a double-quoted YAML string: the quote and
143
+ # backslash, C0 controls, DEL, C1 controls (U+0085 would otherwise fold to a
144
+ # space), the Unicode line and paragraph separators, the byte-order mark, and
145
+ # the two noncharacters YAML cannot hold. Everything else is written as UTF-8,
146
+ # so accented letters and emoji stay readable.
147
+ _YAML_ESCAPED_CHARS = re.compile(r'["\\\u0000-\u001f\u007f-\u009f\u2028\u2029\ufeff\ufffe\uffff]')
148
+
149
+
150
+ def _yaml_string(value: str) -> str:
151
+ def escape(match: re.Match[str]) -> str:
152
+ char = match.group(0)
153
+ return _YAML_ESCAPES.get(char) or f"\\u{ord(char):04x}"
154
+
155
+ return '"' + _YAML_ESCAPED_CHARS.sub(escape, value) + '"'
156
+
157
+
88
158
  def format_csv(model: Model, result: GenerationResult) -> str:
89
159
  buffer = io.StringIO()
90
160
  writer = csv.writer(buffer)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lattice-cli
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: Deterministic combinatorial coverage for agent workflows and constrained variant surfaces.
5
5
  Author-email: Tyler Klose <tylerklose@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -105,11 +105,12 @@ Supported output formats:
105
105
  - `table`
106
106
  - `csv`
107
107
  - `summary`
108
+ - `yaml` (same content as `json`, for files people edit by hand)
108
109
 
109
110
  `generate` defaults to JSON because the primary consumer is usually a coding harness. File-based schemas are supported, but the schema file is transport, not the product surface.
110
111
 
111
112
  For long higher-strength runs, use `--progress` to stream coverage progress to
112
- stderr while keeping stdout clean for JSON, CSV, table, or summary output.
113
+ stderr while keeping stdout clean for JSON, YAML, CSV, table, or summary output.
113
114
  `--progress-every N` controls how many generated scenarios pass between
114
115
  updates.
115
116
 
@@ -34,4 +34,5 @@ tests/test_ipog.py
34
34
  tests/test_model.py
35
35
  tests/test_parser.py
36
36
  tests/test_schema_guidance_matrix.py
37
- tests/test_self_hosting_matrix.py
37
+ tests/test_self_hosting_matrix.py
38
+ tests/test_yaml_format.py
@@ -168,7 +168,7 @@ class CliTests(unittest.TestCase):
168
168
  self.assertEqual(parsed["surface"], "generic")
169
169
  self.assertTrue((target / "SKILL.md").is_file())
170
170
  self.assertTrue((target / "references" / "modeling-rules.md").is_file())
171
- self.assertIn("Do not strip true constraints", (target / "SKILL.md").read_text())
171
+ self.assertIn("do not strip them and mark invalid rows downstream", (target / "SKILL.md").read_text())
172
172
  self.assertIn(
173
173
  "mark them invalid downstream",
174
174
  (target / "references" / "modeling-rules.md").read_text(),
@@ -0,0 +1,130 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ import unittest
8
+ from pathlib import Path
9
+
10
+ import yaml
11
+
12
+ ROOT = Path(__file__).resolve().parents[1]
13
+ FIXTURES = ROOT / "tests" / "fixtures"
14
+ sys.path.insert(0, str(ROOT / "src"))
15
+
16
+ from lattice.formatters import _yaml_string, format_json, format_yaml, render_output
17
+ from lattice.ipog import GenerationResult, generate_covering_array
18
+ from lattice.parser import load_model
19
+
20
+
21
+ def uescape(hex_digits: str) -> str:
22
+ # Built in pieces so the source stays ASCII.
23
+ return '"' + "\\" + "u" + hex_digits + '"'
24
+
25
+
26
+ def quoted(*code_points: int) -> str:
27
+ return '"' + "".join(chr(point) for point in code_points) + '"'
28
+
29
+
30
+ class YamlFormatTests(unittest.TestCase):
31
+ """`--format yaml`: the same content as `--format json`, written by hand.
32
+
33
+ tests/fixtures/yaml_edges.golden.yaml pins the exact bytes for
34
+ tests/fixtures/yaml_edges.json; the Ruby gem (latticerb) tests against
35
+ the same pair.
36
+ """
37
+
38
+ def generate(self, fixture: str):
39
+ model = load_model(str(FIXTURES / fixture))
40
+ return model, generate_covering_array(model, strength=model.strength, seed=42, pool_size=50)
41
+
42
+ def test_edge_case_fixture_matches_golden_bytes(self) -> None:
43
+ model, result = self.generate("yaml_edges.json")
44
+ golden = (FIXTURES / "yaml_edges.golden.yaml").read_text(encoding="utf-8")
45
+ self.assertEqual(format_yaml(model, result, seed=42) + "\n", golden)
46
+
47
+ def test_yaml_reads_back_as_the_json_payload(self) -> None:
48
+ for fixture in ("simple.json", "constrained.json", "skillit.json", "yaml_edges.json"):
49
+ with self.subTest(fixture=fixture):
50
+ model, result = self.generate(fixture)
51
+ self.assertEqual(
52
+ yaml.safe_load(format_yaml(model, result, seed=42)),
53
+ json.loads(format_json(model, result, seed=42)),
54
+ )
55
+
56
+ def test_empty_result_writes_empty_lists_and_null_seed(self) -> None:
57
+ model = load_model(str(FIXTURES / "simple.json"))
58
+ result = GenerationResult(rows=(), coverage_curve=(), total_valid_tuples=0)
59
+ expected = "\n".join(
60
+ [
61
+ "meta:",
62
+ ' model_name: "simple"',
63
+ " strength: 2",
64
+ " test_count: 0",
65
+ " exhaustive_count: 8",
66
+ ' reduction: "inf"',
67
+ ' coverage: "0.0%"',
68
+ " seed: null",
69
+ "coverage_curve: []",
70
+ "scenarios: []",
71
+ ]
72
+ )
73
+ self.assertEqual(render_output(model, result, output_format="yaml", seed=None), expected)
74
+
75
+ def test_yaml_string_escapes(self) -> None:
76
+ backslash = "\\"
77
+ cases = {
78
+ "plain": '"plain"',
79
+ "": '""',
80
+ "yes": '"yes"',
81
+ "~": '"~"',
82
+ 'say "hi"': '"say ' + backslash + '"hi' + backslash + '""',
83
+ "back" + backslash + "slash": '"back' + backslash * 2 + 'slash"',
84
+ chr(0x08): '"' + backslash + 'b"',
85
+ chr(0x09): '"' + backslash + 't"',
86
+ chr(0x0A): '"' + backslash + 'n"',
87
+ chr(0x0C): '"' + backslash + 'f"',
88
+ chr(0x0D): '"' + backslash + 'r"',
89
+ chr(0x00): uescape("0000"),
90
+ chr(0x07): uescape("0007"),
91
+ chr(0x0B): uescape("000b"),
92
+ chr(0x1F): uescape("001f"),
93
+ chr(0x7E): '"~"',
94
+ chr(0x7F): uescape("007f"),
95
+ chr(0x80): uescape("0080"),
96
+ chr(0x85): uescape("0085"),
97
+ chr(0x9F): uescape("009f"),
98
+ chr(0xA0): quoted(0xA0),
99
+ "caf" + chr(0xE9): quoted(0x63, 0x61, 0x66, 0xE9),
100
+ chr(0x2027): quoted(0x2027),
101
+ chr(0x2028): uescape("2028"),
102
+ chr(0x2029): uescape("2029"),
103
+ chr(0x202A): quoted(0x202A),
104
+ chr(0xFEFF): uescape("feff"),
105
+ chr(0xFFFD): quoted(0xFFFD),
106
+ chr(0xFFFE): uescape("fffe"),
107
+ chr(0xFFFF): uescape("ffff"),
108
+ chr(0x1F600): quoted(0x1F600),
109
+ }
110
+ for value, expected in cases.items():
111
+ with self.subTest(value=ascii(value)):
112
+ self.assertEqual(_yaml_string(value), expected)
113
+
114
+ def test_cli_generate_format_yaml(self) -> None:
115
+ env = dict(os.environ)
116
+ env["PYTHONPATH"] = str(ROOT / "src")
117
+ env["PYTHONIOENCODING"] = "utf-8"
118
+ result = subprocess.run(
119
+ [sys.executable, "-m", "lattice", "generate", "--format", "yaml", str(FIXTURES / "yaml_edges.json")],
120
+ cwd=ROOT,
121
+ env=env,
122
+ capture_output=True,
123
+ check=False,
124
+ )
125
+ self.assertEqual(result.returncode, 0, msg=result.stderr)
126
+ self.assertEqual(result.stdout, (FIXTURES / "yaml_edges.golden.yaml").read_bytes())
127
+
128
+
129
+ if __name__ == "__main__":
130
+ unittest.main()
File without changes
File without changes