@mettascript/fuzz 2.8.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MesTTo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,169 @@
1
+ # @mettascript/fuzz
2
+
3
+ Property testing for [MeTTaScript](https://github.com/MesTTo/MeTTaScript), written in MeTTa. You declare a generator and a property, and the library generates cases, shrinks a failure to its smallest form, and hands back a result you can replay. It also enumerates small domains exhaustively, checks a real system against a model over command sequences, and searches a transition relation for a reachable state.
4
+
5
+ The policy lives in MeTTa: generation, shrinking, the run loop, the state machines, and the search are all rewrite rules you can read in `src/metta`. TypeScript supplies only what a representation needs, in one place: a splitmix/xoroshiro random source, a structural key over atoms, and a versioned atom codec.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @mettascript/fuzz
11
+ ```
12
+
13
+ Importing the package registers the `fuzz` module, so MeTTa code reaches it with `import!`.
14
+
15
+ ## Usage
16
+
17
+ ```metta
18
+ !(import! &self fuzz)
19
+
20
+ (: reverse-involution (-> Atom FuzzProperty))
21
+ (= (reverse-involution $xs)
22
+ (expect-atom-equal (reverse (reverse $xs)) $xs))
23
+
24
+ !(fuzz-check reverse-involution
25
+ (gen-list (gen-int -100 100) 0 40)
26
+ reverse-involution
27
+ (fuzz-config (Runs 200)))
28
+ ```
29
+
30
+ A passing run reports what it did:
31
+
32
+ ```text
33
+ (FuzzPassed (Property reverse-involution) (Seed 0)
34
+ (FuzzStatistics (Counts (Passed 213) (PropertyDiscards 0) (GenerationDiscards 0)
35
+ (Regressions 0) (Examples 0) (Edges 13) (Random 200)) ...))
36
+ ```
37
+
38
+ `Runs` counts the random cases. Edge cases are drawn on top of them, which is why 200 runs report 213
39
+ passes here: the generator's boundary values are tried first, then the random ones.
40
+
41
+ Write the property with the expectation combinators rather than a bare `Bool`, so a failure carries a tag and details you can read:
42
+
43
+ - `(fuzz-pass)` and `(fuzz-fail <tag> <details>)` are the two results everything else builds on.
44
+ - `(expect-true <bool> <details>)`, `(expect-false ...)`.
45
+ - `(expect-atom-equal <a> <b>)` compares structurally; `(expect-alpha-equal ...)` ignores variable names.
46
+ - `(expect-results-exact <a> <b>)` compares result bags in order; `-alpha`, `-multiset` and `-set` relax that to variable renaming, order, and multiplicity.
47
+
48
+ ## Generators
49
+
50
+ Generators are data, not functions: `(gen-int 0 9)` is an atom the runner interprets, which is what lets one declaration be replayed, shrunk, and enumerated.
51
+
52
+ - Scalars: `gen-bool`, `gen-int`, `gen-int-origin`, `gen-sized-int`, `gen-float`, `gen-float-range`, `gen-float-bits`, `gen-char`, `gen-char-ascii`, `gen-char-unicode`, `gen-symbol`, `gen-syntax-token`, `gen-const`.
53
+ - Text: `gen-string`, `gen-ascii-string`, `gen-unicode-string`.
54
+ - Structure: `gen-tuple`, `gen-list`, `gen-option`, `gen-element`, `gen-one-of`, `gen-frequency`.
55
+ - Combinators: `gen-map`, `gen-bind`, `gen-filter`, `gen-sized`, `gen-resize`, `gen-recursive`, `gen-custom`.
56
+ - Grammars and types: `gen-grammar`, `gen-grammar-root`, `gen-well-typed` generate from a declared grammar or from type constructors, so you can generate well-formed terms of a language rather than raw trees.
57
+
58
+ ## Shrinking
59
+
60
+ A failure is shrunk before it is reported, under a named order (`mettascript-shrink-v1`) so the smallest form is stable across runs rather than a function of the seed. The result says whether it reached a local minimum or stopped early, and why. A custom generator can supply its own shrinker; anything else shrinks through its decision tree.
61
+
62
+ ## Exhaustive checking
63
+
64
+ For a small domain, enumerate it instead of sampling:
65
+
66
+ ```metta
67
+ (: always (-> Atom FuzzProperty))
68
+ (= (always $value) (fuzz-pass))
69
+
70
+ !(fuzz-check-exhaustive small (gen-bool) always (fuzz-config (MaxEnumerated 10)))
71
+ ```
72
+
73
+ `(FuzzExhaustivelyVerified (Property small) (DomainCount 2) ...)` means the whole domain was covered, which is a proof over that domain rather than evidence. A domain that does not fit the bound is reported as an incomplete run, never as verified.
74
+
75
+ ## State machines
76
+
77
+ Check a real system against a model over generated command sequences. Generation walks the model only, so a command sequence is chosen without touching the real system; execution then runs both and compares.
78
+
79
+ ```metta
80
+ (= (counter-initialize) (new-state 0))
81
+ (: counter-command-generator (-> Atom %Undefined%))
82
+ (= (counter-command-generator $model) (gen-element (increment reset)))
83
+ (= (counter-precondition $model $command) True)
84
+ (= (counter-execute $real increment)
85
+ (let $seen (get-state $real)
86
+ (let $changed (change-state! $real (+ $seen 1))
87
+ (+ $seen 1))))
88
+ (= (counter-execute $real reset) (let $changed (change-state! $real 0) 0))
89
+ (= (counter-next-model (Count $n) increment) (Count (+ $n 1)))
90
+ (= (counter-next-model (Count $n) reset) (Count 0))
91
+ (= (counter-postcondition (Count $n) increment $result) (== $result (+ $n 1)))
92
+ (= (counter-postcondition (Count $n) reset $result) (== $result 0))
93
+ (= (counter-invariant (Count $n)) (>= $n 0))
94
+ (= (counter-cleanup $real) Done)
95
+
96
+ (FuzzMachine Counter
97
+ (InitialModel (Count 0))
98
+ (InitializeReal counter-initialize)
99
+ (CommandGenerator counter-command-generator)
100
+ (Precondition counter-precondition)
101
+ (Execute counter-execute)
102
+ (NextModel counter-next-model)
103
+ (Postcondition counter-postcondition)
104
+ (Invariant counter-invariant)
105
+ (Cleanup counter-cleanup))
106
+
107
+ !(fuzz-check-machine Counter (fuzz-config (Runs 20) (MaxSize 6)))
108
+ ```
109
+
110
+ A divergence shrinks to a shorter command sequence that still diverges, so you get the shortest sequence that separates the real system from the model rather than the one that happened to be generated.
111
+
112
+ ## Bounded reachability
113
+
114
+ Search a transition relation, breadth first, for a state that satisfies a target:
115
+
116
+ ```metta
117
+ (= (counter-enumerate (Count $n))
118
+ (if (< $n 4) (FiniteCommands up split) (FiniteCommands)))
119
+ (= (counter-transition (Count $n) up) (Count (+ $n 1)))
120
+ (= (counter-transition (Count $n) split) (superpose ((Count (+ $n 1)) (Count (+ $n 2)))))
121
+ (= (counter-target (Count $n)) (== $n 3))
122
+
123
+ !(fuzz-reachable Counter (Count 0)
124
+ counter-enumerate counter-transition counter-target
125
+ (reach-config (MaxDepth 20)))
126
+ ```
127
+
128
+ A transition may return several next states, and the whole ordered result bag becomes outgoing edges, so a witness records which branch it took and can be replayed through a nondeterministic model.
129
+
130
+ The outcomes are deliberately distinct. `FuzzReachable` carries a witness that was replayed from the initial state before being reported. `FuzzReachabilityExhausted` means unreachable in the declared finite model. `FuzzUnreachableWithinDepth` means only that nothing was found at or below `MaxDepth`. Every limit, incomplete enumeration, or replay mismatch is a `FuzzReachabilityCutoff` and never becomes exhaustion.
131
+
132
+ ## Running a suite from the command line
133
+
134
+ Declare tests as data and run the file with [`@mettascript/node`](https://github.com/MesTTo/MeTTaScript/tree/main/packages/node):
135
+
136
+ ```metta
137
+ (FuzzTest involution (gen-list (gen-int -20 20) 0 6) reverse-involution
138
+ (fuzz-config (Runs 200)))
139
+ ```
140
+
141
+ ```bash
142
+ metta fuzz suite.metta # run every declaration
143
+ metta fuzz --seed 7 --runs 50 suite.metta # override each declaration's config
144
+ metta fuzz --exhaustive suite.metta # enumerate each domain instead
145
+ metta fuzz --corpus regressions suite.metta # replay stored counterexamples, record new ones
146
+ metta reach suite.metta # run every (FuzzReachTest ...)
147
+ ```
148
+
149
+ The CLI runs the declarations a file carries, not the file's own `!` queries. Exit codes are 0 for a pass or a definitive answer, 1 for a property failure, 2 for invalid input or corrupt stored data, and 3 for an incomplete run.
150
+
151
+ `--corpus <dir>` keeps found counterexamples as text so a later run replays them first, one file per case, meant to be committed. Values go through the versioned codec rather than plain formatting, so a counterexample of `NaN` survives exactly.
152
+
153
+ ## Reading a result from TypeScript
154
+
155
+ Results are atoms. `decodeFuzzOutcome` turns one into a typed union, `renderOutcomeLine` gives the one-line form, and `exitCodeForOutcomes` gives the worst-first exit code for a whole run:
156
+
157
+ ```ts
158
+ import { decodeFuzzOutcome, exitCodeForOutcomes, renderOutcomeLine } from "@mettascript/fuzz";
159
+
160
+ const outcome = decodeFuzzOutcome(resultAtom);
161
+ if (outcome.kind === "failed") console.log(renderOutcomeLine(outcome));
162
+ process.exit(exitCodeForOutcomes([outcome]));
163
+ ```
164
+
165
+ The decoder is strict: an atom it does not recognize becomes an `undecodable` outcome rather than a pass.
166
+
167
+ ## Determinism
168
+
169
+ A run is a function of its seed. The random source, the shrink order, the replay keys, and the exhaustive enumeration order are all named and versioned, so a reported failure reproduces, and `metta fuzz --seed <n>` twice gives the same cases.