lattice-cli 0.1.0__py3-none-any.whl
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.
- lattice/__init__.py +5 -0
- lattice/__main__.py +5 -0
- lattice/agent.py +150 -0
- lattice/cli.py +488 -0
- lattice/constraints.py +391 -0
- lattice/formatters.py +131 -0
- lattice/ipog.py +308 -0
- lattice/model.py +116 -0
- lattice/parser.py +404 -0
- lattice/resources/claude/lattice-workflow/SKILL.md +109 -0
- lattice/resources/claude/lattice-workflow/references/modeling-rules.md +52 -0
- lattice/resources/claude/lattice-workflow/references/worked-examples.md +59 -0
- lattice/resources/codex/lattice-workflow/SKILL.md +118 -0
- lattice/resources/codex/lattice-workflow/agents/openai.yaml +7 -0
- lattice/resources/codex/lattice-workflow/references/modeling-rules.md +52 -0
- lattice/resources/codex/lattice-workflow/references/worked-examples.md +59 -0
- lattice/resources/generic/lattice-workflow/SKILL.md +124 -0
- lattice/resources/generic/lattice-workflow/references/modeling-rules.md +52 -0
- lattice/resources/generic/lattice-workflow/references/worked-examples.md +59 -0
- lattice_cli-0.1.0.dist-info/METADATA +229 -0
- lattice_cli-0.1.0.dist-info/RECORD +25 -0
- lattice_cli-0.1.0.dist-info/WHEEL +5 -0
- lattice_cli-0.1.0.dist-info/entry_points.txt +2 -0
- lattice_cli-0.1.0.dist-info/licenses/LICENSE +176 -0
- lattice_cli-0.1.0.dist-info/top_level.txt +1 -0
lattice/parser.py
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from collections import OrderedDict
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
from lattice.model import (
|
|
12
|
+
Assignment,
|
|
13
|
+
BidirectionalConstraint,
|
|
14
|
+
ConditionalConstraint,
|
|
15
|
+
ForcedConstraint,
|
|
16
|
+
ForwardDependencyConstraint,
|
|
17
|
+
HigherOrderConstraint,
|
|
18
|
+
InvalidPairConstraint,
|
|
19
|
+
Model,
|
|
20
|
+
Parameter,
|
|
21
|
+
normalize_scalar,
|
|
22
|
+
NA_VALUE,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ModelIOError(Exception):
|
|
27
|
+
"""Raised when reading or deserializing a model fails."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ValidationError(Exception):
|
|
31
|
+
"""Raised when a model is structurally invalid."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, errors: list[str]):
|
|
34
|
+
super().__init__("\n".join(errors))
|
|
35
|
+
self.errors = errors
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_model(path: str | None, strength_override: int | None = None) -> Model:
|
|
39
|
+
text, source_name, extension = _read_model_input(path)
|
|
40
|
+
data = _deserialize_model(text, source_name, extension)
|
|
41
|
+
return parse_model_data(data, strength_override=strength_override)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def parse_model_data(data: Any, strength_override: int | None = None) -> Model:
|
|
45
|
+
if not isinstance(data, dict):
|
|
46
|
+
raise ValidationError(["Model root must be a JSON/YAML object."])
|
|
47
|
+
|
|
48
|
+
errors: list[str] = []
|
|
49
|
+
parameters = _parse_parameters(data.get("parameters"), errors)
|
|
50
|
+
raw_constraints = data.get("constraints", [])
|
|
51
|
+
if raw_constraints is None:
|
|
52
|
+
raw_constraints = []
|
|
53
|
+
if not isinstance(raw_constraints, list):
|
|
54
|
+
errors.append("`constraints` must be a list.")
|
|
55
|
+
raw_constraints = []
|
|
56
|
+
|
|
57
|
+
_apply_conditional_parameters(parameters, raw_constraints, errors)
|
|
58
|
+
constraints = _parse_constraints(raw_constraints, parameters, errors)
|
|
59
|
+
strength = _parse_strength(strength_override if strength_override is not None else data.get("strength"), len(parameters), errors)
|
|
60
|
+
model_name = normalize_scalar(data.get("model_name", "unnamed_model"))
|
|
61
|
+
|
|
62
|
+
if errors:
|
|
63
|
+
raise ValidationError(errors)
|
|
64
|
+
|
|
65
|
+
return Model(
|
|
66
|
+
model_name=model_name,
|
|
67
|
+
parameters=tuple(parameters.values()),
|
|
68
|
+
constraints=tuple(constraints),
|
|
69
|
+
strength=strength,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _read_model_input(path: str | None) -> tuple[str, str, str | None]:
|
|
74
|
+
if path in (None, "-"):
|
|
75
|
+
text = sys.stdin.read()
|
|
76
|
+
if not text.strip():
|
|
77
|
+
raise ModelIOError("No model content found on stdin.")
|
|
78
|
+
return text, "stdin", None
|
|
79
|
+
|
|
80
|
+
file_path = Path(path)
|
|
81
|
+
try:
|
|
82
|
+
text = file_path.read_text(encoding="utf-8")
|
|
83
|
+
except OSError as exc:
|
|
84
|
+
raise ModelIOError(f"Unable to read model file `{path}`: {exc}") from exc
|
|
85
|
+
return text, str(file_path), file_path.suffix.lower()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _deserialize_model(text: str, source_name: str, extension: str | None) -> Any:
|
|
89
|
+
loaders = []
|
|
90
|
+
if extension == ".json":
|
|
91
|
+
loaders = [json.loads]
|
|
92
|
+
elif extension in {".yaml", ".yml"}:
|
|
93
|
+
loaders = [yaml.safe_load]
|
|
94
|
+
else:
|
|
95
|
+
loaders = [json.loads, yaml.safe_load]
|
|
96
|
+
|
|
97
|
+
errors: list[str] = []
|
|
98
|
+
for loader in loaders:
|
|
99
|
+
try:
|
|
100
|
+
return loader(text)
|
|
101
|
+
except Exception as exc: # pragma: no cover - exact parser error text varies
|
|
102
|
+
errors.append(str(exc))
|
|
103
|
+
|
|
104
|
+
joined = "; ".join(errors)
|
|
105
|
+
raise ModelIOError(f"Unable to parse model from `{source_name}`: {joined}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _parse_parameters(raw_parameters: Any, errors: list[str]) -> "OrderedDict[str, Parameter]":
|
|
109
|
+
parameters: "OrderedDict[str, Parameter]" = OrderedDict()
|
|
110
|
+
if raw_parameters is None:
|
|
111
|
+
errors.append("`parameters` is required.")
|
|
112
|
+
return parameters
|
|
113
|
+
|
|
114
|
+
if isinstance(raw_parameters, dict):
|
|
115
|
+
items = raw_parameters.items()
|
|
116
|
+
for name, spec in items:
|
|
117
|
+
_add_parameter(parameters, name, spec, errors)
|
|
118
|
+
return parameters
|
|
119
|
+
|
|
120
|
+
if isinstance(raw_parameters, list):
|
|
121
|
+
for index, raw_parameter in enumerate(raw_parameters):
|
|
122
|
+
if not isinstance(raw_parameter, dict):
|
|
123
|
+
errors.append(f"`parameters[{index}]` must be an object.")
|
|
124
|
+
continue
|
|
125
|
+
name = raw_parameter.get("name")
|
|
126
|
+
if name is None:
|
|
127
|
+
errors.append(f"`parameters[{index}]` is missing `name`.")
|
|
128
|
+
continue
|
|
129
|
+
_add_parameter(parameters, name, raw_parameter, errors)
|
|
130
|
+
return parameters
|
|
131
|
+
|
|
132
|
+
errors.append("`parameters` must be a mapping or list.")
|
|
133
|
+
return parameters
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _add_parameter(parameters: "OrderedDict[str, Parameter]", name: Any, spec: Any, errors: list[str]) -> None:
|
|
137
|
+
normalized_name = normalize_scalar(name)
|
|
138
|
+
if normalized_name in parameters:
|
|
139
|
+
errors.append(f"Duplicate parameter `{normalized_name}`.")
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
values: Any
|
|
143
|
+
weights: Any = None
|
|
144
|
+
if isinstance(spec, dict):
|
|
145
|
+
values = spec.get("values")
|
|
146
|
+
weights = spec.get("weights")
|
|
147
|
+
else:
|
|
148
|
+
values = spec
|
|
149
|
+
|
|
150
|
+
parsed_values = _parse_values(values, f"parameter `{normalized_name}`", errors)
|
|
151
|
+
parsed_weights = _parse_weights(weights, parsed_values, normalized_name, errors)
|
|
152
|
+
if not parsed_values:
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
parameters[normalized_name] = Parameter(
|
|
156
|
+
name=normalized_name,
|
|
157
|
+
values=parsed_values,
|
|
158
|
+
weights=parsed_weights,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _apply_conditional_parameters(parameters: "OrderedDict[str, Parameter]", raw_constraints: list[Any], errors: list[str]) -> None:
|
|
163
|
+
for index, raw_constraint in enumerate(raw_constraints):
|
|
164
|
+
if not isinstance(raw_constraint, dict):
|
|
165
|
+
continue
|
|
166
|
+
if raw_constraint.get("type") != "conditional":
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
parameter_name = normalize_scalar(raw_constraint.get("parameter"))
|
|
170
|
+
values = _parse_values(raw_constraint.get("values"), f"`constraints[{index}]` conditional values", errors)
|
|
171
|
+
if not values:
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
conditional_values = values
|
|
175
|
+
if NA_VALUE not in conditional_values:
|
|
176
|
+
conditional_values = conditional_values + (NA_VALUE,)
|
|
177
|
+
|
|
178
|
+
existing = parameters.get(parameter_name)
|
|
179
|
+
if existing is None:
|
|
180
|
+
parameters[parameter_name] = Parameter(name=parameter_name, values=conditional_values)
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
expected = set(conditional_values)
|
|
184
|
+
actual = set(existing.values)
|
|
185
|
+
if actual != expected:
|
|
186
|
+
errors.append(
|
|
187
|
+
f"Conditional parameter `{parameter_name}` conflicts with an existing parameter definition. "
|
|
188
|
+
"Use `conditional` only to define a child parameter that applies under parent values. "
|
|
189
|
+
"For exclusions or requirements between existing parameters, use `invalid_pair`, "
|
|
190
|
+
"`higher_order`, `forward_dep`, or `bidirectional`; run `lattice agent instructions` "
|
|
191
|
+
"for the full schema reference."
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _parse_constraints(
|
|
196
|
+
raw_constraints: list[Any],
|
|
197
|
+
parameters: "OrderedDict[str, Parameter]",
|
|
198
|
+
errors: list[str],
|
|
199
|
+
) -> list[Any]:
|
|
200
|
+
parsed: list[Any] = []
|
|
201
|
+
for index, raw_constraint in enumerate(raw_constraints):
|
|
202
|
+
if not isinstance(raw_constraint, dict):
|
|
203
|
+
errors.append(f"`constraints[{index}]` must be an object.")
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
constraint_type = raw_constraint.get("type")
|
|
207
|
+
if constraint_type == "invalid_pair":
|
|
208
|
+
if_assignment = _parse_assignment(raw_constraint.get("if"), parameters, f"`constraints[{index}].if`", errors)
|
|
209
|
+
then_not = _parse_assignment(raw_constraint.get("then_not"), parameters, f"`constraints[{index}].then_not`", errors)
|
|
210
|
+
if if_assignment and then_not:
|
|
211
|
+
parsed.append(InvalidPairConstraint(if_assignment=if_assignment, then_not=then_not))
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
if constraint_type == "bidirectional":
|
|
215
|
+
pair = raw_constraint.get("pair")
|
|
216
|
+
if not isinstance(pair, list) or len(pair) != 2:
|
|
217
|
+
errors.append(f"`constraints[{index}].pair` must contain exactly two assignments.")
|
|
218
|
+
continue
|
|
219
|
+
left = _parse_assignment(pair[0], parameters, f"`constraints[{index}].pair[0]`", errors)
|
|
220
|
+
right = _parse_assignment(pair[1], parameters, f"`constraints[{index}].pair[1]`", errors)
|
|
221
|
+
if left and right:
|
|
222
|
+
parsed.append(BidirectionalConstraint(pair=(left, right)))
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
if constraint_type == "forward_dep":
|
|
226
|
+
source = _parse_assignment(raw_constraint.get("source"), parameters, f"`constraints[{index}].source`", errors)
|
|
227
|
+
target = _parse_assignment(raw_constraint.get("target"), parameters, f"`constraints[{index}].target`", errors)
|
|
228
|
+
if source and target:
|
|
229
|
+
parsed.append(ForwardDependencyConstraint(source=source, target=target))
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
if constraint_type == "conditional":
|
|
233
|
+
parent = normalize_scalar(raw_constraint.get("parent"))
|
|
234
|
+
parameter_name = normalize_scalar(raw_constraint.get("parameter"))
|
|
235
|
+
values = _parse_values(raw_constraint.get("values"), f"`constraints[{index}].values`", errors)
|
|
236
|
+
applies_when = _parse_values(raw_constraint.get("applies_when"), f"`constraints[{index}].applies_when`", errors)
|
|
237
|
+
|
|
238
|
+
if parent not in parameters:
|
|
239
|
+
errors.append(f"`constraints[{index}]` references unknown parent parameter `{parent}`.")
|
|
240
|
+
continue
|
|
241
|
+
if parameter_name not in parameters:
|
|
242
|
+
errors.append(
|
|
243
|
+
f"`constraints[{index}]` references unknown conditional parameter `{parameter_name}`. "
|
|
244
|
+
"`conditional` must define a child parameter with `values` and a parent gate. "
|
|
245
|
+
"For exclusions or requirements between existing parameters, use `invalid_pair`, "
|
|
246
|
+
"`higher_order`, `forward_dep`, or `bidirectional`; run `lattice agent instructions` "
|
|
247
|
+
"for the full schema reference."
|
|
248
|
+
)
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
parent_parameter = parameters[parent]
|
|
252
|
+
missing = [value for value in applies_when if value not in parent_parameter.value_set]
|
|
253
|
+
if missing:
|
|
254
|
+
errors.append(
|
|
255
|
+
f"`constraints[{index}]` applies_when contains unknown values for `{parent}`: {', '.join(missing)}."
|
|
256
|
+
)
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
if not values:
|
|
260
|
+
continue
|
|
261
|
+
parsed.append(
|
|
262
|
+
ConditionalConstraint(
|
|
263
|
+
parameter=parameter_name,
|
|
264
|
+
values=tuple(value for value in values if value != NA_VALUE),
|
|
265
|
+
parent=parent,
|
|
266
|
+
applies_when=applies_when,
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
if constraint_type == "forced":
|
|
272
|
+
assignments = raw_constraint.get("assignments")
|
|
273
|
+
if not isinstance(assignments, list) or not assignments:
|
|
274
|
+
errors.append(f"`constraints[{index}].assignments` must contain at least one assignment.")
|
|
275
|
+
continue
|
|
276
|
+
seen_parameters: set[str] = set()
|
|
277
|
+
parsed_assignments: list[Assignment] = []
|
|
278
|
+
for assignment_index, raw_assignment in enumerate(assignments):
|
|
279
|
+
assignment = _parse_assignment(
|
|
280
|
+
raw_assignment,
|
|
281
|
+
parameters,
|
|
282
|
+
f"`constraints[{index}].assignments[{assignment_index}]`",
|
|
283
|
+
errors,
|
|
284
|
+
)
|
|
285
|
+
if assignment is None:
|
|
286
|
+
continue
|
|
287
|
+
if assignment.parameter in seen_parameters:
|
|
288
|
+
errors.append(
|
|
289
|
+
f"`constraints[{index}]` repeats parameter `{assignment.parameter}` inside a forced interaction."
|
|
290
|
+
)
|
|
291
|
+
continue
|
|
292
|
+
seen_parameters.add(assignment.parameter)
|
|
293
|
+
parsed_assignments.append(assignment)
|
|
294
|
+
if parsed_assignments:
|
|
295
|
+
parsed.append(ForcedConstraint(assignments=tuple(parsed_assignments)))
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
if constraint_type == "higher_order":
|
|
299
|
+
if_all = raw_constraint.get("if_all")
|
|
300
|
+
then_not = raw_constraint.get("then_not")
|
|
301
|
+
if not isinstance(if_all, list) or len(if_all) < 2:
|
|
302
|
+
errors.append(f"`constraints[{index}].if_all` must contain at least two assignments.")
|
|
303
|
+
continue
|
|
304
|
+
parsed_if_all: list[Assignment] = []
|
|
305
|
+
for assignment_index, raw_assignment in enumerate(if_all):
|
|
306
|
+
assignment = _parse_assignment(
|
|
307
|
+
raw_assignment,
|
|
308
|
+
parameters,
|
|
309
|
+
f"`constraints[{index}].if_all[{assignment_index}]`",
|
|
310
|
+
errors,
|
|
311
|
+
)
|
|
312
|
+
if assignment is not None:
|
|
313
|
+
parsed_if_all.append(assignment)
|
|
314
|
+
parsed_then_not = _parse_assignment(then_not, parameters, f"`constraints[{index}].then_not`", errors)
|
|
315
|
+
if parsed_if_all and parsed_then_not:
|
|
316
|
+
parsed.append(HigherOrderConstraint(if_all=tuple(parsed_if_all), then_not=parsed_then_not))
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
errors.append(f"`constraints[{index}]` has unknown type `{constraint_type}`.")
|
|
320
|
+
|
|
321
|
+
return parsed
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _parse_assignment(
|
|
325
|
+
raw_assignment: Any,
|
|
326
|
+
parameters: "OrderedDict[str, Parameter]",
|
|
327
|
+
field_name: str,
|
|
328
|
+
errors: list[str],
|
|
329
|
+
) -> Assignment | None:
|
|
330
|
+
if not isinstance(raw_assignment, dict):
|
|
331
|
+
errors.append(f"{field_name} must be an object.")
|
|
332
|
+
return None
|
|
333
|
+
|
|
334
|
+
parameter_name = normalize_scalar(raw_assignment.get("parameter"))
|
|
335
|
+
value = normalize_scalar(raw_assignment.get("value"))
|
|
336
|
+
|
|
337
|
+
parameter = parameters.get(parameter_name)
|
|
338
|
+
if parameter is None:
|
|
339
|
+
errors.append(f"{field_name} references unknown parameter `{parameter_name}`.")
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
if value not in parameter.value_set:
|
|
343
|
+
errors.append(f"{field_name} references unknown value `{value}` for parameter `{parameter_name}`.")
|
|
344
|
+
return None
|
|
345
|
+
|
|
346
|
+
return Assignment(parameter=parameter_name, value=value)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _parse_values(raw_values: Any, field_name: str, errors: list[str]) -> tuple[str, ...]:
|
|
350
|
+
if not isinstance(raw_values, list) or not raw_values:
|
|
351
|
+
errors.append(f"{field_name} must be a non-empty list.")
|
|
352
|
+
return ()
|
|
353
|
+
|
|
354
|
+
values = tuple(normalize_scalar(value) for value in raw_values)
|
|
355
|
+
if len(set(values)) != len(values):
|
|
356
|
+
errors.append(f"{field_name} contains duplicate values.")
|
|
357
|
+
return ()
|
|
358
|
+
return values
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _parse_weights(
|
|
362
|
+
raw_weights: Any,
|
|
363
|
+
values: tuple[str, ...],
|
|
364
|
+
parameter_name: str,
|
|
365
|
+
errors: list[str],
|
|
366
|
+
) -> tuple[float, ...] | None:
|
|
367
|
+
if raw_weights is None:
|
|
368
|
+
return None
|
|
369
|
+
if not isinstance(raw_weights, list):
|
|
370
|
+
errors.append(f"`weights` for parameter `{parameter_name}` must be a list.")
|
|
371
|
+
return None
|
|
372
|
+
if len(raw_weights) != len(values):
|
|
373
|
+
errors.append(
|
|
374
|
+
f"`weights` for parameter `{parameter_name}` must have the same length as its values."
|
|
375
|
+
)
|
|
376
|
+
return None
|
|
377
|
+
|
|
378
|
+
parsed: list[float] = []
|
|
379
|
+
for weight in raw_weights:
|
|
380
|
+
if not isinstance(weight, (int, float)):
|
|
381
|
+
errors.append(f"`weights` for parameter `{parameter_name}` must be numeric.")
|
|
382
|
+
return None
|
|
383
|
+
if weight <= 0:
|
|
384
|
+
errors.append(f"`weights` for parameter `{parameter_name}` must be greater than zero.")
|
|
385
|
+
return None
|
|
386
|
+
parsed.append(float(weight))
|
|
387
|
+
return tuple(parsed)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _parse_strength(raw_strength: Any, parameter_count: int, errors: list[str]) -> int:
|
|
391
|
+
if raw_strength is None:
|
|
392
|
+
strength = 2
|
|
393
|
+
else:
|
|
394
|
+
try:
|
|
395
|
+
strength = int(raw_strength)
|
|
396
|
+
except (TypeError, ValueError):
|
|
397
|
+
errors.append("`strength` must be an integer.")
|
|
398
|
+
return 2
|
|
399
|
+
|
|
400
|
+
if strength < 1:
|
|
401
|
+
errors.append("`strength` must be at least 1.")
|
|
402
|
+
if parameter_count and strength > parameter_count:
|
|
403
|
+
errors.append("`strength` cannot exceed the number of parameters.")
|
|
404
|
+
return strength
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lattice-workflow
|
|
3
|
+
description: Use when planning or testing behavior or variant surfaces with interacting states, roles, permissions, feature flags, modes, providers, optional fields, component props, rendering branches, template variants, config matrices, lifecycle states, combinatorial edge cases, pairwise coverage, or t-way coverage. Extract a Lattice schema, run the local Lattice CLI, and use generated rows to strengthen plans, tests, fixtures, rendered variants, or review matrices.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Lattice Workflow
|
|
7
|
+
|
|
8
|
+
Use this skill to keep Claude on extraction and interpretation while Lattice handles combinatorial coverage.
|
|
9
|
+
|
|
10
|
+
## When To Use It
|
|
11
|
+
|
|
12
|
+
Use this skill when the task is any of:
|
|
13
|
+
|
|
14
|
+
- derive a combinatorial model from a feature or PRD
|
|
15
|
+
- derive a test matrix from existing code
|
|
16
|
+
- derive a variant matrix from a component, partial, template, design-system story, or config surface
|
|
17
|
+
- convert edge cases into explicit parameter/value constraints
|
|
18
|
+
- generate pairwise or 3-way scenarios without hand-enumerating combinations
|
|
19
|
+
- render a compact visual review matrix instead of every possible variant
|
|
20
|
+
- review a plan involving roles, permissions, modes, providers, feature flags, optional fields, component props, rendering branches, template variants, config matrices, or lifecycle states
|
|
21
|
+
- find coverage gaps where bugs are likely to hide in interactions rather than single branches
|
|
22
|
+
|
|
23
|
+
Do not invent pairwise combinations manually. The schema is the contract. The generated rows are the source of truth. The schema file is optional transport.
|
|
24
|
+
|
|
25
|
+
## Preflight
|
|
26
|
+
|
|
27
|
+
Before using this skill, check whether `lattice` is already installed:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
command -v lattice >/dev/null 2>&1 && lattice --help >/dev/null
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
If that succeeds, use the installed command.
|
|
34
|
+
|
|
35
|
+
If it fails and you know the path to the Lattice source checkout, install it into the current Python environment:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
python3 -m pip install -e /path/to/lattice
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
To install or refresh this skill from the installed package:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
lattice agent install-claude-skill
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Workflow
|
|
48
|
+
|
|
49
|
+
1. Extract the schema.
|
|
50
|
+
First choose the scope. A schema should usually describe one coherent interaction surface: one feature, one workflow, one service behavior, one endpoint family, one rendering surface, one component's variant space, one template family, one config matrix, or one data-model interaction surface. Then choose parameters that represent independent decisions or state partitions inside that scope. Choose values that are meaningful partitions, not every literal in the codebase.
|
|
51
|
+
2. Encode constraints.
|
|
52
|
+
Prefer the smallest constraint type that matches the rule. Lattice supports exclusion-style constraints, not only conditional parameters: use `invalid_pair` when two assignments cannot coexist and `higher_order` when a combination only becomes invalid with multiple antecedents. Use `conditional` instead of hand-writing `N/A` values.
|
|
53
|
+
3. Validate before generation.
|
|
54
|
+
Prefer the installed `lattice` command with JSON or YAML on stdin:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
cat schema.json | lattice validate
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
If you are working inside this repository and the package is not installed yet, the fallback is:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
cat schema.json | PYTHONPATH=src python3 -m lattice validate
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Use a temp file only if the harness needs one for debugging or tool interop.
|
|
67
|
+
4. Generate deterministic scenarios.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
cat schema.json | lattice generate
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The development fallback in this repo is:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
cat schema.json | PYTHONPATH=src python3 -m lattice generate
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Default to pairwise unless the user explicitly asks for a higher strength.
|
|
80
|
+
5. Interpret the output.
|
|
81
|
+
In plan mode, turn rows into plan revisions, missing decisions, and review scenarios. In test mode, diff the rows against the existing tests and write the missing cases. In variant mode, turn rows into fixtures, rendered variants, screenshots, contact sheets, visual diff inputs, or review checklists.
|
|
82
|
+
|
|
83
|
+
In test mode, source expected behavior from intent — the user's request, a spec, a PRD, a ticket, or explicit reasoning about what the system *should* do. The code under test is evidence, not truth. If intent is ambiguous for a row, stop and ask the user. Do not silently encode current behavior and hedge with `bug_signal:`-style annotations: that produces characterization tests, which lock in the implementation (bugs included) instead of catching divergence from intent. The asymmetries Lattice is designed to expose disappear when the test suite mirrors the same mental model as the code.
|
|
84
|
+
|
|
85
|
+
## Modeling Rules
|
|
86
|
+
|
|
87
|
+
- Scope one schema to one interaction surface. Do not default to the whole app unless the app is genuinely small and the behavior or rendering surface is still coherent.
|
|
88
|
+
- Keep parameter names stable and implementation-adjacent.
|
|
89
|
+
- Use value partitions such as `present` and `absent`, not brittle prose.
|
|
90
|
+
- Avoid derived duplicate parameters. If one field is determined by another, represent that as a constraint.
|
|
91
|
+
- Add constraints only for true business or system rules. Do not use them to encode preferences.
|
|
92
|
+
- Do not strip true constraints to make generation easier, and do not generate nonsensical rows just to mark them invalid later. Encoding constraints changes the coverage universe to valid interactions, which is the point of using Lattice.
|
|
93
|
+
- If two assignments cannot coexist, use `invalid_pair`.
|
|
94
|
+
- If a parameter only matters under a parent value, use `conditional`.
|
|
95
|
+
- If a scenario must appear at least once, use `forced`.
|
|
96
|
+
- If a rule only breaks under multiple antecedents, use `higher_order`.
|
|
97
|
+
|
|
98
|
+
Read [references/modeling-rules.md](references/modeling-rules.md) when you need extraction heuristics or constraint selection guidance.
|
|
99
|
+
|
|
100
|
+
## Output Discipline
|
|
101
|
+
|
|
102
|
+
- Run `validate` before `generate`.
|
|
103
|
+
- Prefer stdin and JSON because another agent step usually consumes the output.
|
|
104
|
+
- Do not add or remove rows after generation.
|
|
105
|
+
- If validation fails, fix the schema instead of weakening the generation step.
|
|
106
|
+
- If generation emits rows that are impossible in the target system, stop and add the missing constraint rather than filtering or annotating those rows downstream.
|
|
107
|
+
- In test mode, assertions must reflect intended behavior, not observed behavior. Read intent from the spec, PRD, ticket, or user — never from the code under test. If intent is ambiguous, stop and ask.
|
|
108
|
+
|
|
109
|
+
Read [references/worked-examples.md](references/worked-examples.md) for concrete examples in this repo.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Modeling Rules
|
|
2
|
+
|
|
3
|
+
Use this file when you are extracting a schema from code or prose and need tighter guardrails than the main skill body provides.
|
|
4
|
+
|
|
5
|
+
## Parameter Selection
|
|
6
|
+
|
|
7
|
+
- Scope one schema to one coherent behavior or rendering surface.
|
|
8
|
+
Good scopes: one feature, one service behavior, one workflow, one endpoint family, one state-machine slice, one component's variant space, one template family.
|
|
9
|
+
Bad default scope: the whole app.
|
|
10
|
+
- Model the decisions that change behavior, not every field in a payload.
|
|
11
|
+
- Prefer parameters that are visible in tests, UI branches, rendering branches, validation logic, template variants, component props, or feature flags.
|
|
12
|
+
- Split large raw domains into behavioral partitions.
|
|
13
|
+
Good: `pay_range = present|absent`
|
|
14
|
+
Bad: every exact salary string
|
|
15
|
+
- Avoid separate parameters for values that are already implied by another parameter unless the implication is partial and needs a constraint.
|
|
16
|
+
|
|
17
|
+
## Value Selection
|
|
18
|
+
|
|
19
|
+
- Values should be mutually exclusive and collectively useful for testing.
|
|
20
|
+
- Keep domains small and meaningful. Most parameters should land in the 2-5 value range.
|
|
21
|
+
- Normalize booleans as `true` and `false`.
|
|
22
|
+
- For optional subordinate parameters, let `conditional` introduce `N/A`. Do not hand-add `N/A` unless the schema genuinely needs it as a first-class value.
|
|
23
|
+
|
|
24
|
+
## Constraint Choice
|
|
25
|
+
|
|
26
|
+
- `invalid_pair`: two assignments cannot coexist
|
|
27
|
+
- `bidirectional`: one-to-one pairing; if A then B and if B then A
|
|
28
|
+
- `forward_dep`: A requires B, but B may appear elsewhere
|
|
29
|
+
- `conditional`: a child parameter only applies for some parent values
|
|
30
|
+
- `forced`: this assignment set must appear in at least one generated row
|
|
31
|
+
- `higher_order`: the invalid rule only appears when two or more antecedents are true
|
|
32
|
+
|
|
33
|
+
Choose the smallest expressive type. If a rule can be represented as `invalid_pair`, do not use `higher_order`.
|
|
34
|
+
|
|
35
|
+
Do not strip true constraints to get a smaller or easier unconstrained run. Do not generate nonsensical rows and mark them invalid downstream. Lattice's coverage accounting should run over valid interactions, so impossible combinations belong in the schema as `invalid_pair`, `forward_dep`, `bidirectional`, `conditional`, or `higher_order` constraints.
|
|
36
|
+
|
|
37
|
+
## Extraction Process
|
|
38
|
+
|
|
39
|
+
1. Choose the behavior or rendering surface you are modeling.
|
|
40
|
+
2. Start from branches, validations, and business rules inside that scope.
|
|
41
|
+
3. Convert prose like "only when", "must", "cannot", and "except" into candidate constraints.
|
|
42
|
+
4. Remove cosmetic parameters that do not affect behavior.
|
|
43
|
+
5. Check whether a missing value is really a separate partition or just noise.
|
|
44
|
+
6. Validate the schema.
|
|
45
|
+
7. Only after validation, generate scenarios.
|
|
46
|
+
|
|
47
|
+
## Interpretation
|
|
48
|
+
|
|
49
|
+
- Plan mode: each row becomes a review scenario or a missing decision in the plan.
|
|
50
|
+
- Test mode: each row becomes a candidate setup matrix. Source the expected behavior from intent — user request, spec, PRD, or ticket — not from the code under test. If you cannot determine intent for a row, stop and surface the ambiguity to the user. Do not encode the current behavior and label it `bug_signal:` to hedge: characterization tests pass for the wrong reasons and hide the asymmetries Lattice is designed to expose. Example: if a mailbox accepts a spoofed `From:` header today, the assertion is still "rejects spoofed sender" — let the test fail and fix the code, do not invert the assertion to match the bug.
|
|
51
|
+
- Variant mode: each row becomes a rendered variant, fixture, screenshot, visual diff input, contact-sheet item, or review checklist item.
|
|
52
|
+
- Generated rows are not test names by themselves. The agent still needs to map them to application-specific assertions.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Worked Examples
|
|
2
|
+
|
|
3
|
+
These examples live in the repo-level `examples/` directory.
|
|
4
|
+
|
|
5
|
+
## `examples/plan-mode-saved-search`
|
|
6
|
+
|
|
7
|
+
Use when the agent starts from a feature discussion or PRD and needs a review checklist.
|
|
8
|
+
|
|
9
|
+
- Input: `feature.md`
|
|
10
|
+
- Schema transport: `model.yaml`
|
|
11
|
+
- Output: `scenarios.json`
|
|
12
|
+
|
|
13
|
+
## `examples/test-mode-checkout`
|
|
14
|
+
|
|
15
|
+
Use when the agent starts from code and an existing test surface and needs missing cases.
|
|
16
|
+
|
|
17
|
+
- Input: `code-notes.md`
|
|
18
|
+
- Schema transport: `model.json`
|
|
19
|
+
- Output: `scenarios.json`
|
|
20
|
+
|
|
21
|
+
## `examples/three-way-notifications`
|
|
22
|
+
|
|
23
|
+
Use when pairwise is not enough and the user explicitly wants 3-way coverage.
|
|
24
|
+
|
|
25
|
+
- Input: `brief.md`
|
|
26
|
+
- Schema transport: `model.yaml`
|
|
27
|
+
- Output: `scenarios.json`
|
|
28
|
+
|
|
29
|
+
## `examples/component-variant-matrix`
|
|
30
|
+
|
|
31
|
+
Use when the agent starts from a visual component, rendering surface, template, or design-system story and needs compact review variants instead of every possible combination.
|
|
32
|
+
|
|
33
|
+
- Input: `brief.md`
|
|
34
|
+
- Schema transport: `model.yaml`
|
|
35
|
+
- Output: `scenarios.json`
|
|
36
|
+
|
|
37
|
+
## `examples/lattice-self-test`
|
|
38
|
+
|
|
39
|
+
Use when you want a meta example of the harness extracting a schema for Lattice's own CLI surface and then using generated rows to drive CLI smoke tests.
|
|
40
|
+
|
|
41
|
+
- Input: `brief.md`
|
|
42
|
+
- Schema transport: `model.yaml`
|
|
43
|
+
- Output: `scenarios.json`
|
|
44
|
+
|
|
45
|
+
## `examples/agent-bootstrap-matrix`
|
|
46
|
+
|
|
47
|
+
Use when you want a meta example of Lattice testing its own agent skill bootstrap behavior across harness detection, force/skip directives, target modes, and output formats.
|
|
48
|
+
|
|
49
|
+
- Input: `brief.md`
|
|
50
|
+
- Schema transport: `model.yaml`
|
|
51
|
+
- Output: `scenarios.json`
|
|
52
|
+
|
|
53
|
+
## `examples/schema-guidance-matrix`
|
|
54
|
+
|
|
55
|
+
Use when you want a meta example of Lattice testing its own schema-discoverability guidance across help text, agent instructions, installed skill files, and validation errors.
|
|
56
|
+
|
|
57
|
+
- Input: `brief.md`
|
|
58
|
+
- Schema transport: `model.yaml`
|
|
59
|
+
- Output: `scenarios.json`
|