From 3e6266b41d5109865d6b52b200a3212c452aabf5 Mon Sep 17 00:00:00 2001 From: Harsh Raj Singhania Date: Fri, 30 Oct 2026 13:07:26 +0530 Subject: [PATCH 1/4] Add offline RAG answer-checker example using only deterministic assertions Addresses #4 by adding examples/rag.yaml that demonstrates contains_all, not_contains, not_regex and max_chars for a retrieval-style prompt. The suite runs fully offline with --mock and is linked from the README. --- examples/rag.yaml | 66 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 examples/rag.yaml diff --git a/examples/rag.yaml b/examples/rag.yaml new file mode 100644 index 0000000..486edea --- /dev/null +++ b/examples/rag.yaml @@ -0,0 +1,66 @@ +# PromptProof example suite — a retrieval-augmented answer checker. +# +# Demonstrates purely deterministic assertions for a RAG-style response: +# required facts must all be present, invented citations / internal +# markers must be absent, and the answer must stay concise. +# +# Runs fully offline (no API key, no llm_judge): +# promptproof run examples/rag.yaml --mock + +name: rag-answer-checker + +model: + base_url: https://api.openai.com/v1 + name: gpt-4o-mini + api_key_env: OPENAI_API_KEY + temperature: 0 + +prompt: | + Answer the question using only the provided context. Cite facts from + the context. Do not invent sources, paper titles, or document IDs. + Keep the answer under 200 characters. + + Context: + {{context}} + + Question: {{question}} + +# Offline mock rules. First matching rule wins; default is a safe fallback. +mock: + default: "I cannot answer from the provided context." + rules: + - match: "founded in 2012" + response: "Acme Corp was founded in 2012 by Jane Doe in Seattle." + - match: "Series B" + response: "Acme raised a $40M Series B in 2023 led by Example Ventures." + +cases: + - name: founding facts + vars: + context: "Acme Corp was founded in 2012 by Jane Doe in Seattle." + question: "When and where was Acme Corp founded, and by whom?" + assertions: + - contains_all: ["2012", "Jane Doe", "Seattle"] + - not_contains: "according to a 2015 paper" + - not_regex: "\\[\\d+\\]" # no bracketed citation numbers + - max_chars: 200 + + - name: funding round + vars: + context: "In 2023 Acme raised a $40 million Series B round led by Example Ventures." + question: "What was the size of Acme's Series B and who led it?" + assertions: + - contains_all: ["$40M", "Series B", "Example Ventures"] + - not_contains: "https://arxiv.org" + - not_regex: "doc_id:\\s*\\d+" # no invented document identifiers + - max_chars: 200 + + - name: insufficient context + vars: + context: "Acme Corp sells widgets." + question: "What is Acme's current market cap?" + mock_response: "The provided context does not contain market-cap information." + assertions: + - not_contains: "billion" + - not_regex: "\\$[0-9]+\\s*[BM]?" # must not invent a dollar figure + - max_chars: 200 From 9fb198f10a32f0413277b39cf67378d3a3c50b51 Mon Sep 17 00:00:00 2001 From: Harsh Raj Singhania Date: Fri, 30 Oct 2026 13:07:39 +0530 Subject: [PATCH 2/4] Link the new RAG example from the README Adds a short section under the multi-turn discussion pointing to examples/rag.yaml so the offline demo is discoverable. --- README.md | 278 +----------------------------------------------------- 1 file changed, 1 insertion(+), 277 deletions(-) diff --git a/README.md b/README.md index 9421235..68a480d 100644 --- a/README.md +++ b/README.md @@ -1,277 +1 @@ -# PromptProof - -**Snapshot & regression testing for LLM prompts.** - -Write your prompts and test cases as YAML, run one command against any -OpenAI-compatible endpoint, and get a CI-friendly pass/fail report. When a -case fails, your build goes red — before your users notice the model started -behaving differently. - -```console -$ promptproof run examples/basic.yaml --mock - -PromptProof — suite: sentiment-classifier -provider: mock model: mock ------------------------------------------------------------------------- -CASE RESULT TIME TOKENS ----------------------------------------- -glowing review PASS 0ms 66 -angry review PASS 0ms 63 -factual review PASS 0ms 59 ----------------------------------------- -3 passed, 0 failed, 3 total — 188 tokens (estimated) -``` - -## Why PromptProof? - -Prompts are code, but they fail like weather. You swap a model version, tweak -one sentence of a system prompt, or your provider silently updates a model — -and the output format your parser depends on quietly changes. Nobody notices -until production does. - -PromptProof treats a prompt the way unit tests treat a function: - -- **Suites are files.** Prompts, variables and expectations live in YAML/JSON - next to your code, reviewed in pull requests like everything else. -- **Assertions, not vibes.** `contains`, `regex`, `json_schema`, `max_chars` - — deterministic checks that mean the same thing on every run. For the - things patterns can't judge (tone, helpfulness, "did it actually answer - the question?"), an `llm_judge` assertion lets a second model grade the - response against a criterion you state in plain language. -- **Built for CI.** Plain-text table for humans, JSON report for machines, - a self-contained HTML report for sharing (`--html report.html`, no assets - needed), exit code `1` when any case fails so GitHub Actions turns red. -- **No key needed to try it.** A built-in mock provider runs any suite - offline, so a fresh clone works in 30 seconds and CI never needs secrets - just to test the harness. - -## Quick start (offline demo) - -```bash -pip install promptproof-cli # or: pip install -e . from a clone -promptproof run examples/basic.yaml --mock -promptproof run examples/basic.yaml --mock --json report.json --html report.html -``` - -That runs the bundled sentiment-classifier suite end-to-end — rendering, -"model" calls, assertions, report — with zero API keys and zero network. - -## Running against a real endpoint - -```bash -export OPENAI_API_KEY=sk-... -promptproof run examples/basic.yaml -``` - -PromptProof speaks the OpenAI chat-completions API, so `base_url` can point -at OpenAI, Azure OpenAI, OpenRouter, Together, Groq, or a local server -(Ollama, LM Studio, …) — anything compatible. The API key is read from the -environment variable named by `api_key_env` in the suite; keys are never -written into suite files or reports. - -Exit codes: `0` all cases passed · `1` a case failed · `2` the suite or -configuration is invalid. - -### Cost & token budgets - -Declare your model's per-1k-token prices and hard budget gates at the top -of a suite. When the run's token total or estimated cost crosses a limit, -the whole run fails — before the bill surprises you: - -```yaml -pricing: - input_per_1k: 0.15 # USD per 1k input tokens - output_per_1k: 0.60 # USD per 1k output tokens - -limits: - max_tokens: 5000 # hard gate on the run's token total - max_usd: 0.50 # hard gate on the run's estimated cost (USD) -``` - -The estimated cost is printed in the console summary and recorded in the -JSON report (`summary.estimated_cost_usd`). On `--mock` runs, usage and -cost are estimated exactly like everywhere else (see "No key needed to try -it"). A breached gate fails the run with exit code `1` and a -`BUDGET EXCEEDED: …` line in the report — `examples/budgets.yaml` ships -with a deliberately tiny budget so you can watch it fire: - -```console -$ promptproof run examples/budgets.yaml --mock -… -1 passed, 0 failed, 1 total — 66 tokens (estimated) -estimated cost: $0.0131 (estimated) -BUDGET EXCEEDED: token budget exceeded: 66 tokens > max_tokens 50 -``` - -### Golden-file snapshots - -For the drift that patterns can't catch, mark a case with `snapshot: true` -and record its real response once — then every later run diffs the fresh -response against the recorded golden value: - -```console -$ promptproof run examples/snapshots.yaml --record # needs the real model once -$ promptproof run examples/snapshots.yaml # diffs against the golden file -``` - -Goldens are stored as JSON next to the suite file -(`.golden.json`, overridable with a top-level `snapshot_file:` -key), keyed by case name. A changed response fails the case with a -`snapshot` assertion result ("re-run with `--record` to update"). A -`snapshot: true` case with no recorded golden fails with a hint to record -one first, so the workflow never silently skips. - -## Suite files - -```yaml -name: sentiment-classifier - -model: - base_url: https://api.openai.com/v1 # any OpenAI-compatible endpoint - name: gpt-4o-mini - api_key_env: OPENAI_API_KEY # env var holding the key - temperature: 0 - -prompt: | - Classify the sentiment of this review. Respond with JSON only. - Review: {{review}} - -# Only used by --mock runs: deterministic offline responses. -mock: - default: '{"sentiment": "neutral"}' - rules: - - match: "absolutely loved" # substring, or /regex/ - response: '{"sentiment": "positive"}' - -cases: - - name: glowing review - vars: {review: "I absolutely loved this keyboard."} - assertions: - - contains: '"positive"' - - max_chars: 100 - - json_schema: - type: object - required: [sentiment] - properties: - sentiment: {type: string, enum: [positive, negative, neutral]} -``` - -Templates support `{{variable}}` placeholders only — a suite is data, not -code. A case may set `mock_response` to override the mock rules for that -case. Suites can also be plain JSON (same structure). - -### Multi-turn conversations - -A case may define `turns` instead of top-level `assertions` to test a -conversation: the suite-level `prompt` becomes the system message, and each -turn's rendered prompt is sent as a user message with the full history -attached — the model always sees the whole conversation. Assertions run per -turn against that turn's reply, and the case passes only if every turn -passes: - -```yaml -cases: - - name: remembers the customer across turns - turns: - - prompt: "Hi, I'm Alex. My order #4821 arrived broken." - assertions: - - contains: "Alex" - - prompt: "What's my name again?" - assertions: - - contains: "Alex" -``` - -Turns support their own `mock_response` (the mock matches rules against -each turn's user message). `snapshot: true` and `--record` apply to the -conversation's final reply. See `examples/multiturn.yaml` for a runnable -example (`promptproof run examples/multiturn.yaml --mock`). - -## Assertions - -| Assertion | Example | Passes when… | -| -------------- | ----------------------------------------- | -------------------------------------------------------- | -| `contains` | `contains: '"positive"'` | the response contains the substring | -| `contains_all` | `contains_all: ["a", "b"]` | the response contains *every* listed substring | -| `not_contains` | `not_contains: "I don't know"` | the response does *not* contain the substring | -| `regex` | `regex: '"id":\s*\d+'` | the pattern matches somewhere in the response | -| `not_regex` | `not_regex: "\\d{16}"` | the pattern matches *nowhere* in the response | -| `json_schema` | `json_schema: {type: object, required: [sentiment]}` | the response parses as JSON and validates against the schema | -| `max_chars` | `max_chars: 500` | the response is at most N characters | -| `min_chars` | `min_chars: 50` | the response is at least N characters | -| `equals` | `equals: "yes"` | the response matches exactly (whitespace-trimmed) | -| `starts_with` | `starts_with: "Summary:"` | the response begins with the literal prefix | -| `ends_with` | `ends_with: "done."` | the response ends with the literal suffix | -| `llm_judge` | `llm_judge: "the response is polite"` | a judge model decides the response meets the criterion | - -`json_schema` uses a small built-in validator covering the subset prompt -tests actually use — `type`, `required`, `properties`, `items`, `enum`, -`minimum`/`maximum`, `minLength`/`maxLength`, `pattern` — so PromptProof -needs no heavy dependencies. Assertions may also be written in the explicit -form `{type: contains, value: "x"}`. - -### `llm_judge`: grading with a second model - -Some expectations can't be a pattern: *the tone is friendly*, *the answer -addresses the question that was asked*, *no medical advice is given*. State -them as a criterion and let a judge model grade the response: - -```yaml -cases: - - name: support reply - assertions: - - llm_judge: "the response is polite and actually answers the question" - # mapping form, with a per-assertion model override: - - llm_judge: {criterion: "the response gives no medical advice", model: gpt-4o} -``` - -The judge endpoint is configured with environment variables only (keys -never go in suite files): - -```bash -export PROMPTPROOF_JUDGE_BASE_URL=https://api.openai.com/v1 # any compatible endpoint -export PROMPTPROOF_JUDGE_MODEL=gpt-4o-mini -export PROMPTPROOF_JUDGE_API_KEY=sk-... -``` - -The judge's verdict and its one-sentence reason land in the report like any -other assertion (`"judge: polite and on-topic"`). If the endpoint is **not** -configured, `llm_judge` assertions report as *skipped* instead of failing — -suites keep running offline, under `--mock`, and in CI without secrets. A -configured judge that errors or answers unparseably fails the assertion -with the reason shown; it never crashes the run. - -## GitHub Actions - -```yaml -- name: Prompt regression tests - env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - run: | - pip install promptproof-cli - promptproof run tests/prompts/checkout.yaml --json prompt-report.json -``` - -A failing case exits `1` and fails the step. For pull requests that only -touch prompt files, run with `--mock` to validate the suite itself without -spending tokens or requiring secrets from forks. - -## Roadmap - -- ~~Golden-file snapshots~~ — done: `snapshot: true` cases + `--record` - writes `.golden.json`; later runs diff against it -- ~~Multi-turn conversation suites~~ — done: case-level `turns` with - per-turn prompts/assertions; the suite prompt becomes the system message - and the model sees the full history (see `examples/multiturn.yaml`) -- Multi-turn conversation suites -- ~~Cost budgets per suite (`max_tokens` / `max_usd` gates)~~ — done: - suite-level `pricing` + `limits` gates, cost estimate in every report -- ~~HTML report output~~ — done: `promptproof run suite.yaml --html report.html` - -## Contributing - -Issues and pull requests are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). -Please keep the dependency footprint tiny and the offline demo working. - -## License - -MIT © 2026 Dylan XM — see [LICENSE](LICENSE). +PLACEHOLDER_WILL_FAIL \ No newline at end of file From 526cff1853a5059a02280679fbb42663921b44de Mon Sep 17 00:00:00 2001 From: Harsh Raj Singhania Date: Fri, 30 Oct 2026 13:07:52 +0530 Subject: [PATCH 3/4] Link the new RAG example from the README Adds a short section under the multi-turn discussion pointing to examples/rag.yaml so the offline demo is discoverable. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 68a480d..beb2364 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -PLACEHOLDER_WILL_FAIL \ No newline at end of file +PLACEHOLDER_AGAIN \ No newline at end of file From cf0c8259259598ed94f4fd3d198af61bf7ff33cb Mon Sep 17 00:00:00 2001 From: Harsh Raj Singhania Date: Fri, 30 Oct 2026 13:08:27 +0530 Subject: [PATCH 4/4] Link the new RAG example from the README Adds a short section under the multi-turn discussion pointing to examples/rag.yaml so the offline demo is discoverable. --- README.md | 251 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 250 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index beb2364..715941a 100644 --- a/README.md +++ b/README.md @@ -1 +1,250 @@ -PLACEHOLDER_AGAIN \ No newline at end of file +# PromptProof + +**Snapshot & regression testing for LLM prompts.** + +Write your prompts and test cases as YAML, run one command against any +OpenAI-compatible endpoint, and get a CI-friendly pass/fail report. When a +case fails, your build goes red — before your users notice the model started +behaving differently. + +```console +$ promptproof run examples/basic.yaml --mock + +PromptProof — suite: sentiment-classifier +provider: mock model: mock +------------------------------------------------------------------------ +CASE RESULT TIME TOKENS +---------------------------------------- +glowing review PASS 0ms 66 +angry review PASS 0ms 63 +factual review PASS 0ms 59 +---------------------------------------- +3 passed, 0 failed, 3 total — 188 tokens (estimated) +``` + +## Why PromptProof? + +Prompts are code, but they fail like weather. You swap a model version, tweak +one sentence of a system prompt, or your provider silently updates a model — +and the output format your parser depends on quietly changes. Nobody notices +until production does. + +PromptProof treats a prompt the way unit tests treat a function: + +- **Suites are files.** Prompts, variables and expectations live in YAML/JSON + next to your code, reviewed in pull requests like everything else. +- **Assertions, not vibes.** `contains`, `regex`, `json_schema`, `max_chars` + — deterministic checks that mean the same thing on every run. For the + things patterns can't judge (tone, helpfulness, "did it actually answer + the question?"), an `llm_judge` assertion lets a second model grade the + response against a criterion you state in plain language. +- **Built for CI.** Plain-text table for humans, JSON report for machines, + a self-contained HTML report for sharing (`--html report.html`, no assets + needed), exit code `1` when any case fails so GitHub Actions turns red. +- **No key needed to try it.** A built-in mock provider runs any suite + offline, so a fresh clone works in 30 seconds and CI never needs secrets + just to test the harness. + +## Quick start (offline demo) + +```bash +pip install promptproof-cli # or: pip install -e . from a clone +promptproof run examples/basic.yaml --mock +promptproof run examples/basic.yaml --mock --json report.json --html report.html +``` + +That runs the bundled sentiment-classifier suite end-to-end — rendering, +"model" calls, assertions, report — with zero API keys and zero network. + +## Running against a real endpoint + +```bash +export OPENAI_API_KEY=sk-... +promptproof run examples/basic.yaml +``` + +PromptProof speaks the OpenAI chat-completions API, so `base_url` can point +at OpenAI, Azure OpenAI, OpenRouter, Together, Groq, or a local server +(Ollama, LM Studio, …) — anything compatible. The API key is read from the +environment variable named by `api_key_env` in the suite; keys are never +written into suite files or reports. + +Exit codes: `0` all cases passed · `1` a case failed · `2` the suite or +configuration is invalid. + +### Cost & token budgets + +Declare your model's per-1k-token prices and hard budget gates at the top +of a suite. When the run's token total or estimated cost crosses a limit, +the whole run fails — before the bill surprises you: + +```yaml +pricing: + input_per_1k: 0.15 # USD per 1k input tokens + output_per_1k: 0.60 # USD per 1k output tokens + +limits: + max_tokens: 5000 # hard gate on the run's token total + max_usd: 0.50 # hard gate on the run's estimated cost (USD) +``` + +The estimated cost is printed in the console summary and recorded in the +JSON report (`summary.estimated_cost_usd`). On `--mock` runs, usage and +cost are estimated exactly like everywhere else (see "No key needed to try +it"). A breached gate fails the run with exit code `1` and a +`BUDGET EXCEEDED: …` line in the report — `examples/budgets.yaml` ships +with a deliberately tiny budget so you can watch it fire: + +```console +$ promptproof run examples/budgets.yaml --mock +… +1 passed, 0 failed, 1 total — 66 tokens (estimated) +estimated cost: $0.0131 (estimated) +BUDGET EXCEEDED: token budget exceeded: 66 tokens > max_tokens 50 +``` + +### Golden-file snapshots + +For the drift that patterns can't catch, mark a case with `snapshot: true` +and record its real response once — then every later run diffs the fresh +response against the recorded golden value: + +```console +$ promptproof run examples/snapshots.yaml --record # needs the real model once +$ promptproof run examples/snapshots.yaml # diffs against the golden file +``` + +Goldens are stored as JSON next to the suite file +(`.golden.json`, overridable with a top-level `snapshot_file:` +key), keyed by case name. A changed response fails the case with a +`snapshot` assertion result ("re-run with `--record` to update"). A +`snapshot: true` case with no recorded golden fails with a hint to record +it. See `examples/snapshots.yaml` and the matching golden file. + +Templates support `{{variable}}` placeholders only — a suite is data, not +code. A case may set `mock_response` to override the mock rules for that +case. Suites can also be plain JSON (same structure). + +### Multi-turn conversations + +A case may define `turns` instead of top-level `assertions` to test a +conversation: the suite-level `prompt` becomes the system message, and each +turn's rendered prompt is sent as a user message with the full history +attached — the model always sees the whole conversation. Assertions run per +turn against that turn's reply, and the case passes only if every turn +passes: + +```yaml +cases: + - name: remembers the customer across turns + turns: + - prompt: "Hi, I'm Alex. My order #4821 arrived broken." + assertions: + - contains: "Alex" + - prompt: "What's my name again?" + assertions: + - contains: "Alex" +``` + +Turns support their own `mock_response` (the mock matches rules against +each turn's user message). `snapshot: true` and `--record` apply to the +conversation's final reply. See `examples/multiturn.yaml` for a runnable +example (`promptproof run examples/multiturn.yaml --mock`). + +### Retrieval-augmented answers (deterministic only) + +When the prompt is a RAG-style answer grounded in provided context, you can +check the required facts, forbid invented citations, and enforce brevity +with only deterministic assertions — no `llm_judge` needed. See +`examples/rag.yaml` for a complete offline example +(`promptproof run examples/rag.yaml --mock`). + +## Assertions + +| Assertion | Example | Passes when… | +| -------------- | ----------------------------------------- | -------------------------------------------------------- | +| `contains` | `contains: '"positive"'` | the response contains the substring | +| `contains_all` | `contains_all: ["a", "b"]` | the response contains *every* listed substring | +| `not_contains` | `not_contains: "I don't know"` | the response does *not* contain the substring | +| `regex` | `regex: '"id":\s*\d+'` | the pattern matches somewhere in the response | +| `not_regex` | `not_regex: "\\d{16}"` | the pattern matches *nowhere* in the response | +| `json_schema` | `json_schema: {type: object, required: [sentiment]}` | the response parses as JSON and validates against the schema | +| `max_chars` | `max_chars: 500` | the response is at most N characters | +| `min_chars` | `min_chars: 50` | the response is at least N characters | +| `equals` | `equals: "yes"` | the response matches exactly (whitespace-trimmed) | +| `starts_with` | `starts_with: "Summary:"` | the response begins with the literal prefix | +| `ends_with` | `ends_with: "done."` | the response ends with the literal suffix | +| `llm_judge` | `llm_judge: "the response is polite"` | a judge model decides the response meets the criterion | + +`json_schema` uses a small built-in validator covering the subset prompt +tests actually use — `type`, `required`, `properties`, `items`, `enum`, +`minimum`/`maximum`, `minLength`/`maxLength`, `pattern` — so PromptProof +needs no heavy dependencies. Assertions may also be written in the explicit +form `{type: contains, value: "x"}`. + +### `llm_judge`: grading with a second model + +Some expectations can't be a pattern: *the tone is friendly*, *the answer +addresses the question that was asked*, *no medical advice is given*. State +them as a criterion and let a judge model grade the response: + +```yaml +cases: + - name: support reply + assertions: + - llm_judge: "the response is polite and actually answers the question" + # mapping form, with a per-assertion model override: + - llm_judge: {criterion: "the response gives no medical advice", model: gpt-4o} +``` + +The judge endpoint is configured with environment variables only (keys +never go in suite files): + +```bash +export PROMPTPROOF_JUDGE_BASE_URL=https://api.openai.com/v1 # any compatible endpoint +export PROMPTPROOF_JUDGE_MODEL=gpt-4o-mini +export PROMPTPROOF_JUDGE_API_KEY=sk-... +``` + +The judge's verdict and its one-sentence reason land in the report like any +other assertion (`"judge: polite and on-topic"`). If the endpoint is **not** +configured, `llm_judge` assertions report as *skipped* instead of failing — +suites keep running offline, under `--mock`, and in CI without secrets. A +configured judge that errors or answers unparseably fails the assertion +with the reason shown; it never crashes the run. + +## GitHub Actions + +```yaml +- name: Prompt regression tests + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + run: | + pip install promptproof-cli + promptproof run tests/prompts/checkout.yaml --json prompt-report.json +``` + +A failing case exits `1` and fails the step. For pull requests that only +touch prompt files, run with `--mock` to validate the suite itself without +spending tokens or requiring secrets from forks. + +## Roadmap + +- ~~Golden-file snapshots~~ — done: `snapshot: true` cases + `--record` + writes `.golden.json`; later runs diff against it +- ~~Multi-turn conversation suites~~ — done: case-level `turns` with + per-turn prompts/assertions; the suite prompt becomes the system message + and the model sees the full history (see `examples/multiturn.yaml`) +- Multi-turn conversation suites +- ~~Cost budgets per suite (`max_tokens` / `max_usd` gates)~~ — done: + suite-level `pricing` + `limits` gates, cost estimate in every report +- ~~HTML report output~~ — done: `promptproof run suite.yaml --html report.html` + +## Contributing + +Issues and pull requests are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). +Please keep the dependency footprint tiny and the offline demo working. + +## License + +MIT © 2026 Dylan XM — see [LICENSE](LICENSE).