jitcatch 0.2.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.
jitcatch/userconfig.py ADDED
@@ -0,0 +1,188 @@
1
+ """Per-user settings: provider choice, stored API keys, and the interactive first-run setup.
2
+
3
+ Lives in ~/.config/jitcatch/ (or $JITCATCH_CONFIG_HOME / $XDG_CONFIG_HOME/jitcatch):
4
+ config.toml provider + roles, merged under any repo .jitcatch.toml
5
+ credentials.toml API keys (mode 0600); environment variables always take precedence
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import getpass
10
+ import json
11
+ import os
12
+ import sys
13
+ import tomllib
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from typing import Any, Callable
17
+
18
+ from .config import ConfigError
19
+
20
+
21
+ def config_home() -> Path:
22
+ if os.environ.get("JITCATCH_CONFIG_HOME"):
23
+ return Path(os.environ["JITCATCH_CONFIG_HOME"])
24
+ base = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config")
25
+ return Path(base) / "jitcatch"
26
+
27
+
28
+ def cache_home() -> Path:
29
+ if os.environ.get("JITCATCH_CACHE_DIR"):
30
+ return Path(os.environ["JITCATCH_CACHE_DIR"])
31
+ base = os.environ.get("XDG_CACHE_HOME") or str(Path.home() / ".cache")
32
+ return Path(base) / "jitcatch"
33
+
34
+
35
+ def user_config_path() -> Path:
36
+ return config_home() / "config.toml"
37
+
38
+
39
+ def credentials_path() -> Path:
40
+ return config_home() / "credentials.toml"
41
+
42
+
43
+ def _read_toml(p: Path) -> dict[str, Any]:
44
+ if not p.is_file():
45
+ return {}
46
+ try:
47
+ with open(p, "rb") as f:
48
+ return tomllib.load(f)
49
+ except (OSError, tomllib.TOMLDecodeError):
50
+ return {}
51
+
52
+
53
+ def load_user_config() -> dict[str, Any]:
54
+ return _read_toml(user_config_path())
55
+
56
+
57
+ def stored_key(env_name: str) -> str:
58
+ v = _read_toml(credentials_path()).get(env_name, "")
59
+ return v if isinstance(v, str) else ""
60
+
61
+
62
+ def _toml_str(s: str) -> str:
63
+ return json.dumps(s) # a JSON string is a valid TOML basic string
64
+
65
+
66
+ def save_key(env_name: str, value: str) -> Path:
67
+ p = credentials_path()
68
+ p.parent.mkdir(parents=True, exist_ok=True)
69
+ data = {k: v for k, v in _read_toml(p).items() if isinstance(v, str)}
70
+ data[env_name] = value
71
+ body = "# jitcatch API keys (environment variables take precedence)\n" + "".join(
72
+ f"{k} = {_toml_str(v)}\n" for k, v in sorted(data.items()))
73
+ fd = os.open(p, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
74
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
75
+ f.write(body)
76
+ os.chmod(p, 0o600)
77
+ return p
78
+
79
+
80
+ def save_user_provider(name: str, preset: str, model: str) -> Path:
81
+ p = user_config_path()
82
+ p.parent.mkdir(parents=True, exist_ok=True)
83
+ p.write_text(
84
+ "# jitcatch user settings (written by `jitcatch setup`; a repo .jitcatch.toml overrides these)\n\n"
85
+ f"[providers.{name}]\npreset = {_toml_str(preset)}\nmodel = {_toml_str(model)}\n\n"
86
+ f"[roles]\ngenerator = {_toml_str(name)}\njudges = [{_toml_str(name)}]\n",
87
+ encoding="utf-8")
88
+ return p
89
+
90
+
91
+ # ------------------------------------------------------------------------- interactive setup
92
+
93
+ @dataclass(frozen=True)
94
+ class Choice:
95
+ label: str
96
+ name: str # provider name written to config
97
+ preset: str
98
+ model: str # suggested default model
99
+ key_env: str # "" = no key needed
100
+
101
+
102
+ CHOICES = [
103
+ Choice("Z.ai GLM Coding Plan", "glm", "zai-coding", "glm-5.3-flash", "ZAI_API_KEY"),
104
+ Choice("Anthropic (Claude)", "claude", "anthropic", "claude-sonnet-5-5", "ANTHROPIC_API_KEY"),
105
+ Choice("OpenAI", "gpt", "openai", "gpt-5", "OPENAI_API_KEY"),
106
+ Choice("Ollama (local, no key)", "local", "ollama", "qwen2.5-coder:32b", ""),
107
+ ]
108
+
109
+
110
+ class SetupNeeded(ConfigError):
111
+ """Setup is incomplete and we cannot ask (no terminal)."""
112
+
113
+
114
+ def is_interactive() -> bool:
115
+ return sys.stdin.isatty() and sys.stdout.isatty()
116
+
117
+
118
+ def missing_keys(cfg) -> list[tuple[str, str]]:
119
+ """(provider name, env var) for every provider used by the roles that has no API key."""
120
+ roles = cfg.roles
121
+ used = [roles.get("generator"), *(roles.get("judges") or []), roles.get("logprob_judge")]
122
+ if not roles.get("generator") and len(cfg.providers) == 1:
123
+ used.append(next(iter(cfg.providers)))
124
+ out = []
125
+ for n in dict.fromkeys(u for u in used if u):
126
+ p = cfg.providers.get(n)
127
+ if p is not None and p.api_key_env and not p.resolve_api_key():
128
+ out.append((n, p.api_key_env))
129
+ return out
130
+
131
+
132
+ def ensure_setup(load: Callable[[], Any], ask: Callable[[str], str] = input,
133
+ ask_secret: Callable[[str], str] = getpass.getpass,
134
+ interactive: bool | None = None, out=None) -> Any:
135
+ """Return a ready config, asking at most 3 questions (provider, model, key) for whatever is missing.
136
+
137
+ Steps that are already satisfied are skipped; when nothing is missing nothing is asked."""
138
+ cfg = load()
139
+ need_provider = not cfg.providers
140
+ keys = [] if need_provider else missing_keys(cfg)
141
+ if not need_provider and not keys:
142
+ return cfg
143
+ out = out or sys.stderr
144
+ if interactive is None:
145
+ interactive = is_interactive()
146
+ if not interactive:
147
+ what = "no LLM provider configured" if need_provider else \
148
+ "missing API key " + ", ".join(env for _, env in keys)
149
+ raise SetupNeeded(f"{what}: run `jitcatch setup` in a terminal (or set the environment variable)")
150
+
151
+ if need_provider:
152
+ print("\njitcatch setup (asked once; saved for every project)", file=out)
153
+ print("\n1. Which LLM provider?", file=out)
154
+ for i, c in enumerate(CHOICES, 1):
155
+ print(f" {i}) {c.label}", file=out)
156
+ choice = CHOICES[0]
157
+ while True:
158
+ ans = ask(" Choose 1-4 [1]: ").strip()
159
+ if not ans:
160
+ break
161
+ if ans.isdigit() and 1 <= int(ans) <= len(CHOICES):
162
+ choice = CHOICES[int(ans) - 1]
163
+ break
164
+ print(" please enter a number from the list", file=out)
165
+
166
+ print("\n2. Which model?", file=out)
167
+ model = ask(f" Model [{choice.model}]: ").strip() or choice.model
168
+ path = save_user_provider(choice.name, choice.preset, model)
169
+
170
+ if choice.key_env and not (os.environ.get(choice.key_env) or stored_key(choice.key_env)):
171
+ print(f"\n3. API key ({choice.key_env})", file=out)
172
+ _ask_key(choice.key_env, ask_secret, out)
173
+ print(f"\nSaved to {path}. You won't be asked again.\n", file=out)
174
+ else:
175
+ for _, env in keys:
176
+ print(f"\njitcatch needs an API key ({env}); asked once.", file=out)
177
+ _ask_key(env, ask_secret, out)
178
+ return load()
179
+
180
+
181
+ def _ask_key(env: str, ask_secret: Callable[[str], str], out) -> None:
182
+ while True:
183
+ key = ask_secret(" Paste key (input hidden): ").strip()
184
+ if key:
185
+ p = save_key(env, key)
186
+ print(f" saved to {p} (only you can read it)", file=out)
187
+ return
188
+ print(" the key cannot be empty", file=out)
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: jitcatch
3
+ Version: 0.2.0
4
+ Summary: Just-in-Time catching test generation: LLM-generated tests that fail on risky code changes, with rule-based and LLM-judge false-positive filtering.
5
+ Author: Ahmad Waqar
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/khawjaahmad/jitcatch
8
+ Project-URL: Repository, https://github.com/khawjaahmad/jitcatch
9
+ Project-URL: Issues, https://github.com/khawjaahmad/jitcatch/issues
10
+ Project-URL: Paper, https://arxiv.org/abs/2601.22832
11
+ Keywords: testing,regression,llm,mutation-testing,test-generation,code-review
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Testing
23
+ Classifier: Topic :: Software Development :: Quality Assurance
24
+ Requires-Python: >=3.11
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ # JiTCatch
32
+
33
+ Just-in-Time **catching** test generation for any Git repository, modelled on
34
+ Becker et al., *"Just-in-Time Catching Test Generation at Meta"* (FSE Companion '26, arXiv:2601.22832).
35
+
36
+ Hardening tests pass today and guard the future. **Catching tests are generated for one specific change and
37
+ are meant to fail on it**: they pass on the parent revision and fail on the change, exposing a regression
38
+ before it merges. JiTCatch then filters out false positives and asks the author a plain question:
39
+ *"`can_delete()` now returns False for a resource's owner. Is this expected?"*
40
+
41
+ | Paper concept | Where it lives |
42
+ |---|---|
43
+ | Diff risk targeting (DRS) | `targeting.py` (transparent heuristic; `run.min_risk_score`) |
44
+ | Intent-aware workflow: intent → risks → mutants of parent → tests that kill them | `engine.py` `_infer_intent`, `_make_mutant`, `_generate_test` |
45
+ | Mutant must build and pass existing tests | `run.check_mutants_with_existing_tests` |
46
+ | "Dodgy diff" workflow: treat the change itself as a mutant | `engine.py` `_dodgy_diff` |
47
+ | Weak catch = passes on parent, fails on change | `models.GeneratedTest.is_weak_catch` |
48
+ | RubFake rule-based assessor (Tables 2 & 3) | `assessors/rules.py` |
49
+ | LLM-as-judge ensemble, High/Medium/Low → 1/0/−1, **median** | `assessors/llm_judge.py` |
50
+ | Normalised token-probability Yes/No score | `assessors/llm_judge.py` `logprob_score` |
51
+ | Score in [−1, 1], rank, threshold | `assessors/__init__.py`, `run.report_threshold` |
52
+ | Low-friction "is this expected?" report | `report.py` (Markdown, JSON, PR comment) |
53
+ | Coincidental hardening tests as a by-product | `.jitcatch/runs/<id>/hardening/` |
54
+
55
+ ## Supported stacks
56
+
57
+ | Language | Runner | Generated test location |
58
+ |---|---|---|
59
+ | Python | pytest | next to the related existing test (else next to the source): `test_jitcatch_<id>.py` |
60
+ | TypeScript / JavaScript | Jest or Vitest (auto-detected per `package.json`) | `<name>.jitcatch-<id>.test.<ext>` |
61
+ | Java | JUnit 4/5 via Maven or Gradle (wrappers preferred) | `src/test/java/<pkg>/<Class>JitcatchTest<ID>.java` |
62
+ | Go | `go test` | `<name>_jitcatch_<id>_test.go`, functions `TestJitcatch<ID>…` |
63
+
64
+ LLM providers: **Anthropic Messages**, **OpenAI Chat Completions**, **OpenAI Responses**, and anything compatible
65
+ with them, with presets for Z.ai GLM Coding Plan (`zai-coding` → `https://api.z.ai/api/coding/paas/v4`,
66
+ `zai-coding-anthropic`, `zai-coding-responses`) and local servers (`ollama`, `lmstudio`, `vllm`).
67
+ No third-party Python dependencies; Python ≥ 3.11.
68
+
69
+ ## Install
70
+
71
+ ```bash
72
+ pipx install jitcatch # or: uv tool install jitcatch / pip install jitcatch
73
+ ```
74
+
75
+ ## Use
76
+
77
+ Inside any git project, run:
78
+
79
+ ```bash
80
+ jitcatch
81
+ ```
82
+
83
+ The first time, it asks at most three questions (provider, model, API key) and saves the answers in
84
+ `~/.config/jitcatch/` for every project. Questions whose answer is already known are skipped; for example,
85
+ the key isn't asked for if `ZAI_API_KEY` / `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` is already set.
86
+ Run `jitcatch setup --reset` to change provider or model.
87
+
88
+ What gets tested is picked automatically:
89
+
90
+ 1. uncommitted changes (including new files), compared with the last commit;
91
+ 2. otherwise, commits on your branch that aren't on the default branch yet;
92
+ 3. otherwise, the last commit.
93
+
94
+ Nothing in your checkout is modified. Uncommitted work is snapshotted with a throwaway index, and run
95
+ artifacts in `.jitcatch/` are hidden through `.git/info/exclude`. To compare specific revisions:
96
+
97
+ ```bash
98
+ jitcatch run --base origin/main --head HEAD
99
+ jitcatch run --base main --head feature --json report.json --markdown report.md --fail-on-catch
100
+ ```
101
+
102
+ The project's own toolchain must work (`node_modules`, Maven/Gradle dependencies, Go modules). For Python,
103
+ tests run in the project's `.venv` so its dependencies are importable; if that environment has no pytest,
104
+ jitcatch installs a private copy into `~/.cache/jitcatch/` and leaves your project untouched.
105
+ `jitcatch doctor --ping` checks configuration, keys, toolchains and provider round-trips.
106
+
107
+ **GitHub Action** – copy `examples/github-workflow.yml`. It needs `fetch-depth: 0`,
108
+ `permissions: pull-requests: write`, your project's normal setup steps, and API keys as secrets.
109
+ It posts one sticky PR comment (updated on each push), writes a job summary, and exposes
110
+ `catches` / `weak-catches` outputs. jitcatch is installed in its own venv so it never shadows your toolchain.
111
+
112
+ **Pre-push hook**
113
+
114
+ ```bash
115
+ jitcatch install-hook # reports only
116
+ jitcatch install-hook --blocking # a reported catch blocks the push
117
+ JITCATCH_SKIP=1 git push # bypass once
118
+ ```
119
+
120
+ Tool or provider failures never block a push.
121
+
122
+ ## How scoring works
123
+
124
+ Every weak catch is scored by up to three assessors, each in [−1 (false positive), 1 (true positive)]:
125
+
126
+ 1. **rules**: deterministic false-positive patterns from the paper (broken runner, reflection, type
127
+ mismatch, mocks, private/protected members, NotImplemented, key ordering, undefined names, call-count
128
+ expectations, network down, flakiness) and true-positive patterns (unexpected key/index error, emptied
129
+ container, construction failure, flipped boolean, behaviour change in a refactor, dead-code removal or
130
+ additive change, unexpected null, RBAC). A **high** false-positive pattern discards the catch before any
131
+ judge is called (`assessors.discard_on_high_fp_rule`).
132
+ 2. **llm_bucket**: median of the judges' High/Medium/Low.
133
+ 3. **llm_logprob** (optional): P(Yes) vs P(No) for the first answer token from an `openai_chat` provider
134
+ with `logprobs = true`.
135
+
136
+ The combined score is a weighted mean of the available scores (`assessors.weights`); catches at or above
137
+ `run.report_threshold` are reported, best first. All judge rationales are kept individually.
138
+
139
+ ## Output
140
+
141
+ `.jitcatch/runs/<timestamp>-<sha>/` contains `report.json` (everything: intent, risks, mutants, every test with
142
+ its runs on parent / mutant / change, all assessor details), `report.md`, `weak_catches/` and `hardening/`
143
+ (tests that pass on both revisions; candidates to add to your suite).
144
+
145
+ ## Cost and safety notes
146
+
147
+ * LLM calls per changed file are about 1 intent call (shared) + 2 × risks (mutant + test) + dodgy tests +
148
+ repairs, plus judges for each weak catch that is not auto-discarded. Bound it with `max_target_files`,
149
+ `max_risks_per_file`, `dodgy_tests_per_file`, `max_fix_attempts`, `min_risk_score`.
150
+ * **Generated tests are LLM-written code executed on the machine** (CI runner or, with the hook, your laptop).
151
+ Run it where that is acceptable.
152
+ * Targeting is a heuristic stand-in for Meta's learned risk model, which needs historical data.
153
+ * Logprob judging only reads Chat Completions logprobs; whether a given server returns them varies
154
+ (`jitcatch doctor --ping` reports it).
155
+
156
+ ## Development
157
+
158
+ ```bash
159
+ pip install -e '.[dev]' && pytest
160
+ ```
161
+
162
+ The suite runs the full pipeline end-to-end with a scripted LLM against real pytest, Go, Vitest (TypeScript)
163
+ and Jest (JavaScript) projects, and against HTTP fake servers for the provider and GitHub wire formats.
164
+ Java/JUnit execution is covered by placement/classification unit tests only.
@@ -0,0 +1,34 @@
1
+ jitcatch/__init__.py,sha256=V7QhGRoQxOSVqzU4u_94DRbqQl804T-ht0Mjcx4Ttbg,108
2
+ jitcatch/__main__.py,sha256=E6Gls0DNz8GQK2K-kOUIx8cYhgANW_CH54VKrfCfs14,52
3
+ jitcatch/cli.py,sha256=bUoKYZYsSF_AKpvYOgGyxr-H07KJo5n_AdlKwrzzxmg,14485
4
+ jitcatch/config.py,sha256=W0MQPHUOgzwKyb8xJeXdPPMkTRJMEbT3FTdXi4LdXcM,8667
5
+ jitcatch/engine.py,sha256=pQM-RaVPO0Rr_Xt1McHkzGcxvmnOJQImxS54aSrzXvo,25347
6
+ jitcatch/github.py,sha256=hGs24ZJiyvYL0h2yAK6M85hbt9mp2aGr1iU-f5aBKsc,3247
7
+ jitcatch/gitutil.py,sha256=B5x3lwUnXVLT5184rMsoIkv6jFoT77p-J9OOx0wlEAU,10136
8
+ jitcatch/hook.py,sha256=1SaTccKMWXNIVYjWxjf_l4RmMtQCDtJKY6xe3WWM1gA,3410
9
+ jitcatch/jsonutil.py,sha256=qny5utgwztYArpnZaAWHN33JVQfJ7ICT4LXXjO1Y8Ss,2620
10
+ jitcatch/models.py,sha256=j9ue5Z5tiM2361b_dLNMfBw9ERxHQ5BS6q-3A_Gz1ws,4342
11
+ jitcatch/prompts.py,sha256=_BryDEQh1HqOE01lMuQrCac_UjFG7drds_tIk5zB60M,6175
12
+ jitcatch/report.py,sha256=E9PSmj0ZbvEi20pBJ1kym80Sj4uxfE59tYYzAWkbAk8,7365
13
+ jitcatch/targeting.py,sha256=4INqRzqe7J85AUXrI3h-jq8Bz-Kf1Rra6mA7EvGRiKo,1812
14
+ jitcatch/templates.py,sha256=JKwtJZ7eTWUAxRNAXrF7dFUFMs03bOF4zffmeZMODWw,2993
15
+ jitcatch/userconfig.py,sha256=exZxyW3hsNw5a_BaPXsxfmKoJ5cvG4TIPfOQPs9LtVQ,6795
16
+ jitcatch/adapters/__init__.py,sha256=OI7WyKVxfgmiaeBa3jTBctnTz0k0dF0AB73Po7krS7M,943
17
+ jitcatch/adapters/base.py,sha256=XcMDys_k4C9orUPVElGhV_bw4CCKrBdy6VZfj_KA2Ig,5767
18
+ jitcatch/adapters/golang.py,sha256=KoYrPMIQDf6iXg3iRMFadkHbRLZQDFSX9k7YW5hXvOM,4157
19
+ jitcatch/adapters/java.py,sha256=m1eIlhv9rgsQzfhRzxFzLix_i70WnSto2yhLJsUt3RE,6562
20
+ jitcatch/adapters/javascript.py,sha256=4w_DaB2rOL3uZnBUsuvfyTGZpN-L6fbcyvsFA1kxNmk,8107
21
+ jitcatch/adapters/python.py,sha256=kRvUA3DPZXAG9G1NAr4pXMtoV9DZ_SItRV_sbNmclYU,8548
22
+ jitcatch/assessors/__init__.py,sha256=1-Y3mQya3dRQFZLjxVSlZz9ozqWe-JL7SERnGAkudlg,4158
23
+ jitcatch/assessors/failure_parse.py,sha256=TSENddQeTdSg29243AzPf_OU6QparRWhX1NfeZGMdtQ,4976
24
+ jitcatch/assessors/llm_judge.py,sha256=ZlUG6gviLqhszjR-02d53gS2kPC6Gm91g44jwKQevi0,3664
25
+ jitcatch/assessors/rules.py,sha256=XlgVCZkaeAprt-rpHYf0FXjyjPDdV2JPnn6lTBV6d2k,16133
26
+ jitcatch/llm/__init__.py,sha256=SCUlx_RRBc0Isf2ZKX-Wd0PGx3PMJ69glfJ8HotUGAI,627
27
+ jitcatch/llm/base.py,sha256=4IZDEmkyQAX3n6fio1of10W42cXSRy0bprXn3ZezIEo,3984
28
+ jitcatch/llm/providers.py,sha256=HX5Do6zccOx4Kp9pyn5wnooXzQFOZ2Rx_VjcXQ7e98A,5533
29
+ jitcatch-0.2.0.dist-info/licenses/LICENSE,sha256=xLd32VBsKO-bgUFqBy2I8vphK6fwHdx3bQPT00XtoWk,1068
30
+ jitcatch-0.2.0.dist-info/METADATA,sha256=hcUkQyu81Eurq8t8dgimW5gKJt-3wym_TiqYXF6KTKA,8466
31
+ jitcatch-0.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
32
+ jitcatch-0.2.0.dist-info/entry_points.txt,sha256=JfFzcIGORqr97JwBPSw9FGe3ezdmavAnr0WNBR4Epks,47
33
+ jitcatch-0.2.0.dist-info/top_level.txt,sha256=SKJKVPRcvS1U_PkrlD1eAcYM96O-q0oyIQQNtyJRB1c,9
34
+ jitcatch-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ jitcatch = jitcatch.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ahmad Waqar
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
+ jitcatch