statelock 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.
statelock/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """statelock -- verified finite machines inside neural networks.
2
+
3
+ Three things, in the order you would use them:
4
+ induce(...) work the machine out from labelled strings, by search
5
+ StateDiscoverer or let a network find it by gradient descent
6
+ equivalent(...) check the result against a reference by exhaustive BFS
7
+ StateLock attach the verified machine to your model
8
+
9
+ Everything here is decided by exact equivalence, not by held-out accuracy.
10
+ """
11
+ from .verify import equivalent, reachable, minimal_size
12
+ from .induce import induce
13
+ from .discover import StateDiscoverer, fit
14
+ from .layer import StateLock, attach
15
+
16
+ __version__ = "0.1.0"
17
+ __all__ = ["induce", "equivalent", "reachable", "minimal_size",
18
+ "StateDiscoverer", "fit", "StateLock", "attach"]
statelock/discover.py ADDED
@@ -0,0 +1,114 @@
1
+ """Discover the automaton by gradient descent, with no oracle-driven search.
2
+
3
+ The state is a learned transition table run as a hard argmax scan with a
4
+ straight-through gradient. What makes it work, and what four earlier attempts
5
+ lacked, is the target: the state is trained to predict its MYHILL-NERODE ROW --
6
+ the labels the sequence would receive under a bundle of probe continuations --
7
+ rather than only the current label. Predicting the current label gives the
8
+ optimiser no reason to separate two states that agree now and differ later, and
9
+ a partition that is coarser than the target's cannot be repaired afterwards,
10
+ because merging only coarsens.
11
+
12
+ Three settings matter and were each fixed by measurement, not taste:
13
+ * train on strings that COVER the alphabet, not only in-distribution ones.
14
+ With grammatical data alone, loss reached 0.0 and every held-out label was
15
+ right, and exact equivalence still failed, because the table was
16
+ unconstrained on strings the generator never emits.
17
+ * interleave sequence lengths rather than staging them, or the short gradient
18
+ path is forgotten.
19
+ * hold the straight-through temperature constant. The forward pass is a hard
20
+ argmax at any temperature, so annealing only sharpens the surrogate until
21
+ the gradient vanishes.
22
+
23
+ Exactness tracks the training loss: runs reaching zero verified exact, runs that
24
+ stalled did not. So `fit` reports the loss and you restart on that, without
25
+ needing an answer key.
26
+ """
27
+ import numpy as np
28
+ import torch
29
+ import torch.nn as nn
30
+
31
+ __all__ = ["StateDiscoverer", "fit"]
32
+
33
+
34
+ class _Scan(nn.Module):
35
+ def __init__(self, V, K, d, seed=0):
36
+ super().__init__()
37
+ g = torch.Generator().manual_seed(seed)
38
+ self.W = nn.Parameter(torch.randn(V, K, K, generator=g) * 0.5)
39
+ self.s0 = nn.Parameter(torch.randn(K, generator=g) * 0.5)
40
+ self.emb = nn.Embedding(K, d)
41
+ with torch.no_grad():
42
+ self.emb.weight.normal_(0, 1.0, generator=g)
43
+ self.V, self.K = V, K
44
+
45
+ def forward(self, x, tau=1.0):
46
+ B, L = x.shape
47
+ p = torch.softmax(self.s0 / tau, -1).expand(B, self.K)
48
+ hard = torch.zeros_like(p).scatter_(1, p.argmax(-1, keepdim=True), 1.0)
49
+ st = hard + p - p.detach()
50
+ outs = []
51
+ for t in range(L):
52
+ logits = torch.einsum('bk,bkj->bj', st, self.W[x[:, t]])
53
+ p = torch.softmax(logits / tau, -1)
54
+ hard = torch.zeros_like(p).scatter_(1, p.argmax(-1, keepdim=True), 1.0)
55
+ st = hard + p - p.detach()
56
+ outs.append(st)
57
+ return torch.stack(outs, 1) @ self.emb.weight
58
+
59
+ def table(self):
60
+ return (self.W.argmax(-1).detach().cpu().numpy().astype(np.int64),
61
+ int(self.s0.argmax().item()))
62
+
63
+
64
+ class StateDiscoverer(nn.Module):
65
+ """A discrete state layer that learns its own transition table."""
66
+
67
+ def __init__(self, alphabet_size, n_slots, n_probes, d=64, seed=0):
68
+ super().__init__()
69
+ self.scan = _Scan(alphabet_size, n_slots, d, seed)
70
+ self.row = nn.Linear(d, n_probes)
71
+
72
+ def forward(self, x, tau=1.0):
73
+ """Returns per-position row logits."""
74
+ return self.row(self.scan(x, tau))
75
+
76
+ def states(self, x):
77
+ """The discovered state index at each position (no gradient)."""
78
+ delta, s0 = self.scan.table()
79
+ x = x.cpu().numpy()
80
+ s = np.full(x.shape[0], s0, np.int64)
81
+ out = np.empty(x.shape, np.int64)
82
+ for t in range(x.shape[1]):
83
+ s = delta[x[:, t], s]; out[:, t] = s
84
+ return out
85
+
86
+ def table(self):
87
+ return self.scan.table()
88
+
89
+
90
+ def fit(model, batches, steps=40000, lr=2e-2, tau=1.0, log_every=0):
91
+ """Train on an iterable-returning callable `batches(step) -> (x, rows)`.
92
+
93
+ x: LongTensor (B, L) of symbols
94
+ rows: FloatTensor (B, L, n_probes) of 0/1 probe labels
95
+ Returns the smoothed final loss. Exactness tracks this: restart if it does
96
+ not approach zero.
97
+ """
98
+ head = [p for n, p in model.named_parameters() if not n.startswith("scan.W")
99
+ and not n.startswith("scan.s0")]
100
+ opt = torch.optim.AdamW([{"params": [model.scan.W, model.scan.s0], "lr": lr},
101
+ {"params": head, "lr": lr / 3}], weight_decay=0.0)
102
+ sch = torch.optim.lr_scheduler.OneCycleLR(opt, max_lr=[lr, lr / 3], total_steps=steps)
103
+ lf = nn.BCEWithLogitsLoss(); ema = None
104
+ for it in range(steps):
105
+ x, rows = batches(it)
106
+ loss = lf(model(x, tau), rows)
107
+ opt.zero_grad(); loss.backward()
108
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
109
+ opt.step(); sch.step()
110
+ v = float(loss.detach())
111
+ ema = v if ema is None else 0.98 * ema + 0.02 * v
112
+ if log_every and it % log_every == 0:
113
+ print(f"step {it} loss {ema:.5f}", flush=True)
114
+ return ema
statelock/induce.py ADDED
@@ -0,0 +1,90 @@
1
+ """Induce an exact automaton from (string, label) pairs, by search.
2
+
3
+ The learner never sees a state. It needs an oracle it can query for the label of
4
+ a string it chooses -- `label_fn(word) -> int` -- which is the same access a
5
+ person has when they can run the system they are modelling on inputs of their
6
+ choosing.
7
+
8
+ Construction: a cover set holding one representative prefix per discovered
9
+ state, closed under the alphabet; a suffix set grown by consistency repair when
10
+ two prefixes share a row but their extensions do not. Both corrections were
11
+ forced by verification failures, not chosen for elegance:
12
+ * sampling prefixes does not close the table (64k sampled prefixes still left
13
+ transitions undefined, and the verifier refused);
14
+ * a fixed suffix set under-separates (K = 149 against a minimal 152, with no
15
+ unresolved transitions -- a wrong merge that looks nearly right).
16
+ """
17
+ import numpy as np
18
+
19
+ __all__ = ["induce"]
20
+
21
+
22
+ def induce(alphabet_size, label_fn, suffixes=None, max_rounds=200, max_states=100000):
23
+ """Return (delta, out, start, info).
24
+
25
+ alphabet_size: number of input symbols, which are 0..alphabet_size-1.
26
+ label_fn: callable taking a tuple of symbols, returning an int label.
27
+ suffixes: optional starting suffix set (list of tuples). Defaults to the
28
+ empty string plus every single symbol; more are added as needed.
29
+ """
30
+ V = alphabet_size
31
+ S = list(suffixes) if suffixes else [()] + [(a,) for a in range(V)]
32
+ P = [()]
33
+ cache = {}
34
+
35
+ def lab(w):
36
+ if w not in cache: cache[w] = int(label_fn(w))
37
+ return cache[w]
38
+
39
+ def row(p): return tuple(lab(p + s) for s in S)
40
+
41
+ rounds = added = 0
42
+ reps = {}; sid = {}; cover = {}
43
+ while rounds < max_rounds:
44
+ rounds += 1
45
+ e1 = {p + (a,) for p in P for a in range(V)}
46
+ e2 = {q + (b,) for q in e1 for b in range(V)}
47
+ cand = sorted(set(P) | e1 | e2, key=lambda w: (len(w), w))
48
+ rows = {p: row(p) for p in cand}
49
+ reps = {}; sid = {}; cover = {}
50
+ for p in cand:
51
+ r = rows[p]
52
+ if r not in reps:
53
+ reps[r] = len(reps); cover[reps[r]] = p
54
+ sid[p] = reps[r]
55
+ if len(reps) > max_states:
56
+ raise RuntimeError("state count exceeded max_states; the target "
57
+ "may not be finite-state at this resolution")
58
+ bad = None
59
+ bystate = {}
60
+ for p in cand: bystate.setdefault(sid[p], []).append(p)
61
+ for _, ps in bystate.items():
62
+ if len(ps) < 2: continue
63
+ p0 = ps[0]
64
+ for q in ps[1:]:
65
+ for a in range(V):
66
+ u, w = p0 + (a,), q + (a,)
67
+ if u in rows and w in rows and rows[u] != rows[w]:
68
+ j = next(i for i in range(len(S)) if rows[u][i] != rows[w][i])
69
+ bad = (a,) + S[j]; break
70
+ if bad: break
71
+ if bad: break
72
+ if bad is not None and bad not in S:
73
+ S.append(bad); added += 1; continue
74
+ newP = sorted(set(cover.values()), key=lambda w: (len(w), w))
75
+ if set(newP) == set(P): break
76
+ P = newP
77
+
78
+ K = len(reps)
79
+ delta = np.zeros((V, K), np.int64); unresolved = 0
80
+ for i in range(K):
81
+ p = cover[i]
82
+ for a in range(V):
83
+ q = p + (a,)
84
+ if q in sid: delta[a, i] = sid[q]
85
+ else: unresolved += 1
86
+ out = np.array([lab(cover[i]) for i in range(K)], np.int64)
87
+ info = {"states": K, "suffixes": len(S), "suffixes_added": added,
88
+ "rounds": rounds, "unresolved_transitions": unresolved,
89
+ "queries": len(cache)}
90
+ return delta, out, sid[()], info
statelock/layer.py ADDED
@@ -0,0 +1,70 @@
1
+ """Attach a verified machine to a model.
2
+
3
+ `StateLock` turns a transition table into an embedding lookup over machine
4
+ states and adds it to a hidden-state tensor. The table is fixed: it is never
5
+ trained, so whatever was verified about it stays true inside the network.
6
+
7
+ Two ways to use it:
8
+ * directly, adding the state embedding to your own hidden states;
9
+ * `attach(hf_model, lock, input_ids_fn)` installs a forward hook on a Hugging
10
+ Face model's embedding layer, so the state is added to the token embeddings
11
+ with no change to the model's code.
12
+ """
13
+ import numpy as np
14
+ import torch
15
+ import torch.nn as nn
16
+
17
+ __all__ = ["StateLock", "attach"]
18
+
19
+
20
+ class StateLock(nn.Module):
21
+ def __init__(self, delta, start=0, d_model=768, out=None):
22
+ super().__init__()
23
+ delta = np.asarray(delta, dtype=np.int64)
24
+ self.register_buffer("delta", torch.from_numpy(delta))
25
+ self.start = int(start)
26
+ self.n_states = delta.shape[1]
27
+ self.emb = nn.Embedding(self.n_states, d_model)
28
+ nn.init.normal_(self.emb.weight, std=0.02)
29
+ self.register_buffer("out", torch.from_numpy(np.asarray(out, dtype=np.int64))
30
+ if out is not None else torch.zeros(0, dtype=torch.long))
31
+
32
+ @torch.no_grad()
33
+ def run(self, x):
34
+ """State index at every position. Exact, O(length), never trained."""
35
+ d = self.delta.cpu().numpy(); xi = x.cpu().numpy()
36
+ s = np.full(xi.shape[0], self.start, np.int64)
37
+ st = np.empty(xi.shape, np.int64)
38
+ for t in range(xi.shape[1]):
39
+ s = d[xi[:, t], s]; st[:, t] = s
40
+ return torch.from_numpy(st).to(x.device)
41
+
42
+ def forward(self, hidden, x):
43
+ """hidden: (B, L, d). x: (B, L) symbols. Returns hidden + state embedding."""
44
+ return hidden + self.emb(self.run(x))
45
+
46
+
47
+ def attach(hf_model, lock, symbol_fn=None):
48
+ """Add the machine's state to a Hugging Face model's token embeddings.
49
+
50
+ symbol_fn maps input_ids to machine symbols; omit it when the model's token
51
+ ids already are the machine's alphabet. Returns a handle; call
52
+ handle.remove() to detach.
53
+ """
54
+ emb = hf_model.get_input_embeddings()
55
+ state = {"ids": None}
56
+
57
+ def pre_hook(_module, args, kwargs):
58
+ ids = args[0] if args else kwargs.get("input_ids")
59
+ state["ids"] = ids
60
+ return None
61
+
62
+ def hook(_module, args, output):
63
+ ids = args[0] if args else state["ids"]
64
+ if ids is None:
65
+ return output
66
+ sym = ids if symbol_fn is None else symbol_fn(ids)
67
+ return output + lock.emb(lock.run(sym)).to(output.dtype)
68
+
69
+ hf_model.register_forward_pre_hook(pre_hook, with_kwargs=True)
70
+ return emb.register_forward_hook(hook)
statelock/verify.py ADDED
@@ -0,0 +1,65 @@
1
+ """Exact equivalence checking. This is the load-bearing function of the whole
2
+ library: it decides whether two automata agree on EVERY string, by breadth-first
3
+ search over reachable pairs of states, with no length cutoff.
4
+
5
+ Accuracy on sampled strings cannot substitute. In the work this library comes
6
+ from, a bounded enumeration to length 11 reported 6/6 correct and disagreed with
7
+ true equivalence on 22 of 48 deliberately corrupted targets.
8
+ """
9
+ from collections import deque
10
+ import numpy as np
11
+
12
+ __all__ = ["equivalent", "reachable", "minimal_size"]
13
+
14
+
15
+ def equivalent(delta_a, out_a, delta_b, out_b, start_a=0, start_b=0):
16
+ """True if the two DFAs produce the same output on every input string.
17
+
18
+ delta_*: int array (alphabet_size, n_states); delta[x, s] is the next state.
19
+ out_*: int array (n_states,); the output symbol emitted in that state.
20
+ """
21
+ delta_a, out_a = np.asarray(delta_a), np.asarray(out_a)
22
+ delta_b, out_b = np.asarray(delta_b), np.asarray(out_b)
23
+ A = delta_a.shape[0]
24
+ if delta_b.shape[0] != A:
25
+ raise ValueError("alphabet sizes differ")
26
+ seen = {(int(start_a), int(start_b))}
27
+ q = deque(seen)
28
+ while q:
29
+ a, b = q.popleft()
30
+ for x in range(A):
31
+ a2, b2 = int(delta_a[x, a]), int(delta_b[x, b])
32
+ if int(out_a[a2]) != int(out_b[b2]):
33
+ return False
34
+ if (a2, b2) not in seen:
35
+ seen.add((a2, b2)); q.append((a2, b2))
36
+ return True
37
+
38
+
39
+ def reachable(delta, start=0):
40
+ """Indices of states reachable from `start`."""
41
+ delta = np.asarray(delta); A = delta.shape[0]
42
+ seen = {int(start)}; q = deque(seen)
43
+ while q:
44
+ s = q.popleft()
45
+ for x in range(A):
46
+ t = int(delta[x, s])
47
+ if t not in seen:
48
+ seen.add(t); q.append(t)
49
+ return sorted(seen)
50
+
51
+
52
+ def minimal_size(delta, out):
53
+ """Number of states in the minimal equivalent DFA (Moore refinement)."""
54
+ delta, out = np.asarray(delta), np.asarray(out)
55
+ K = delta.shape[1]; part = out.copy()
56
+ while True:
57
+ sig = [tuple([int(part[i])] + [int(part[delta[x, i]]) for x in range(delta.shape[0])])
58
+ for i in range(K)]
59
+ m = {}; new = np.empty(K, np.int64)
60
+ for i, s in enumerate(sig):
61
+ if s not in m: m[s] = len(m)
62
+ new[i] = m[s]
63
+ if len(m) == len(set(part.tolist())):
64
+ return len(set(part.tolist()))
65
+ part = new
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: statelock
3
+ Version: 0.1.0
4
+ Summary: Verified finite machines inside neural networks: induce one from labelled strings or discover it by gradient descent, check it by exhaustive equivalence, and attach it to your model.
5
+ License: MIT
6
+ Project-URL: Source, https://github.com/RaheemNazir/statelock
7
+ Keywords: state tracking,automata,length generalization,transformers,neural algorithmic reasoning,verification
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: numpy>=1.22
12
+ Requires-Dist: torch>=2.0
13
+ Dynamic: license-file
14
+
15
+ # statelock
16
+
17
+ Transformers lose track of state as inputs get longer. On a prose connectivity
18
+ task, a model goes from 0.92 accuracy at its training length to 0.50 at 16x
19
+ length, and growing it from 1.3M to 88M parameters does not help: the largest
20
+ model is the best in distribution and the worst at length.
21
+
22
+ `statelock` gives the model a finite machine to consult, and the machine is
23
+ **checked exhaustively** rather than measured on a test set. With one attached,
24
+ the same model holds 1.0000 at 16x length.
25
+
26
+ ```python
27
+ from statelock import induce, equivalent, StateLock
28
+
29
+ delta, out, start, info = induce(alphabet_size=8, label_fn=my_system) # find it
30
+ assert equivalent(delta, out, ref_delta, ref_out, start, ref_start) # check it
31
+ lock = StateLock(delta, start, d_model=768) # attach it
32
+ hidden = lock(hidden, symbols) # inside your model's forward
33
+ ```
34
+
35
+ That is the whole interface.
36
+
37
+ ## Install
38
+
39
+ ```
40
+ pip install statelock
41
+ ```
42
+
43
+ ## The three things it does
44
+
45
+ **Induce.** Work the machine out from labelled strings. No state supervision, no
46
+ hints. You supply `label_fn(word) -> int`, the label your system gives a string
47
+ you choose. On a 152-state target it recovers exactly 152 states, equal to the
48
+ minimal machine, and passes exact equivalence.
49
+
50
+ **Discover.** Or let a network find the machine during ordinary training, with
51
+ no search at all:
52
+
53
+ ```python
54
+ from statelock import StateDiscoverer, fit
55
+ model = StateDiscoverer(alphabet_size=6, n_slots=32, n_probes=64)
56
+ loss = fit(model, batches, steps=3000) # exactness tracks this loss
57
+ delta, start = model.table()
58
+ ```
59
+
60
+ The state is trained to predict its Myhill-Nerode row, the labels a bundle of
61
+ probe continuations would produce, not just the current label. Predicting the
62
+ current label alone gives the optimiser no reason to separate two states that
63
+ agree now and differ later, and four earlier attempts failed for exactly that
64
+ reason. Exactness tracks the training loss: a run that reaches zero verifies,
65
+ one that stalls does not, so you restart on the loss without needing an answer
66
+ key.
67
+
68
+ **Verify.** `equivalent(...)` decides agreement on *every* string by BFS over
69
+ reachable state pairs, with no length cutoff. This is not optional rigour. A
70
+ bounded enumeration to length 11 once reported 6/6 correct here and disagreed
71
+ with true equivalence on 22 of 48 corrupted targets.
72
+
73
+ ## With Hugging Face
74
+
75
+ ```python
76
+ from statelock import StateLock, attach
77
+ lock = StateLock(delta, start, d_model=model.config.hidden_size)
78
+ handle = attach(model, lock, symbol_fn=my_tokens_to_symbols) # forward hook
79
+ ```
80
+
81
+ No change to the model's code. `handle.remove()` detaches.
82
+
83
+ ## What it is for, and what it is not for
84
+
85
+ Use it where the task has a discrete core with a state count in the hundreds:
86
+ entity and relation tracking across a document, protocol and permission state,
87
+ bracket and scope structure, small formal languages, board and game state.
88
+
89
+ It will not help where there is no such core, and it does not make a model
90
+ better at anything except keeping that state. The machine is a component you
91
+ attach, and attaching it is a design decision about your task.
92
+
93
+ ## Measured
94
+
95
+ Prose connectivity, dense per-position labels, trained at length 128:
96
+
97
+ | model | arm | L=128 | L=2048 (16x) |
98
+ |---|---|---|---|
99
+ | 1.3M | none | 0.9155 | 0.5848 |
100
+ | 1.3M | shuffled machine | 0.8984 | 0.6033 |
101
+ | 1.3M | statelock | 1.0000 | 0.9998 |
102
+ | 27M | none | 0.9099 | 0.6191 |
103
+ | 27M | statelock | 1.0000 | 1.0000 |
104
+ | 88M | none | 0.9167 | 0.5028 |
105
+ | 88M | statelock | 1.0000 | 1.0000 |
106
+
107
+ Constant-predictor baseline 0.5204. The shuffled-machine row is the control that
108
+ matters: same parameters, same embedding, random transitions, no gain.
109
+
110
+ Discovery (the network finds the machine itself, then it is verified):
111
+
112
+ | target minimal states | slots offered | exact | loss on exact runs |
113
+ |---|---|---|---|
114
+ | 11 | 32 | 2/3 seeds | 0.0 |
115
+ | 38 | 128 | 1/3 seeds | 0.0 |
116
+
117
+ ## Reproduce
118
+
119
+ ```
120
+ python tests/test_all.py
121
+ ```
122
+
123
+ Induction to 152 states, 40 corruptions all flagged by the verifier, exact
124
+ running at length 4096, and a discovery run that verifies. Runs on CPU.
125
+
126
+ ## Licence
127
+
128
+ MIT.
@@ -0,0 +1,10 @@
1
+ statelock/__init__.py,sha256=bYYRj2IVMb-l0qv2k4RcyYyWtcbxWJ4BAlO2-7QoDvI,775
2
+ statelock/discover.py,sha256=7GDOIzW7KACAK5QJUiS5PgGmv2_iy6j1J7mvZKJ212Q,4817
3
+ statelock/induce.py,sha256=6n7VyKTvYutcbLjvLPyW2GgK-oqGUDNs-ioEsHeESEA,3689
4
+ statelock/layer.py,sha256=V39porJMt2sWKy-9gjIswUOrQ5CUvVD8nvIAtd27Pck,2705
5
+ statelock/verify.py,sha256=sFhV01_iggZE1dnQMu4doWX2EngsysRPZBr9S7KzYtE,2435
6
+ statelock-0.1.0.dist-info/licenses/LICENSE,sha256=JK2p0N_CKg82aKhciBBf1_pONnCSyhuOAAn3Gw0P7WU,1075
7
+ statelock-0.1.0.dist-info/METADATA,sha256=_--5kw7Tau553QUWlgDDRDuL7MtmQ2JxPjKpqq1L328,4728
8
+ statelock-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
9
+ statelock-0.1.0.dist-info/top_level.txt,sha256=OHWmWuALtqlMVydbdI0I3LPMLEFJe7XJj5HaEAF__PI,10
10
+ statelock-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abdul Raheem Nazir
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.
@@ -0,0 +1 @@
1
+ statelock