slmeval-cli 0.1.0__tar.gz
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.
- slmeval_cli-0.1.0/.gitattributes +15 -0
- slmeval_cli-0.1.0/.github/ISSUE_TEMPLATE/benchmark_result.yml +38 -0
- slmeval_cli-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +37 -0
- slmeval_cli-0.1.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
- slmeval_cli-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +19 -0
- slmeval_cli-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- slmeval_cli-0.1.0/.github/workflows/ci.yml +163 -0
- slmeval_cli-0.1.0/.github/workflows/gate-demo.yml +107 -0
- slmeval_cli-0.1.0/.github/workflows/release.yml +114 -0
- slmeval_cli-0.1.0/.gitignore +12 -0
- slmeval_cli-0.1.0/CHANGELOG.md +59 -0
- slmeval_cli-0.1.0/CODE_OF_CONDUCT.md +7 -0
- slmeval_cli-0.1.0/CONTRIBUTING.md +96 -0
- slmeval_cli-0.1.0/LICENSE +201 -0
- slmeval_cli-0.1.0/PKG-INFO +484 -0
- slmeval_cli-0.1.0/README.md +245 -0
- slmeval_cli-0.1.0/RELEASING.md +77 -0
- slmeval_cli-0.1.0/SECURITY.md +24 -0
- slmeval_cli-0.1.0/docs/architecture.md +106 -0
- slmeval_cli-0.1.0/docs/assets/fault-injection.svg +92 -0
- slmeval_cli-0.1.0/docs/assets/how-it-works.svg +100 -0
- slmeval_cli-0.1.0/docs/assets/pass-rates.svg +94 -0
- slmeval_cli-0.1.0/docs/assets/scorecard.svg +150 -0
- slmeval_cli-0.1.0/docs/assets/where-models-failed.svg +100 -0
- slmeval_cli-0.1.0/docs/ci.md +98 -0
- slmeval_cli-0.1.0/docs/configuration.md +174 -0
- slmeval_cli-0.1.0/docs/getting-started.md +194 -0
- slmeval_cli-0.1.0/docs/reading-results.md +114 -0
- slmeval_cli-0.1.0/examples/github-action/README.md +108 -0
- slmeval_cli-0.1.0/examples/github-action/action.yml +361 -0
- slmeval_cli-0.1.0/examples/github-action/slmeval.yml +62 -0
- slmeval_cli-0.1.0/examples/judge/README.md +104 -0
- slmeval_cli-0.1.0/examples/judge/cases.jsonl +1 -0
- slmeval_cli-0.1.0/examples/judge/eval.toml +42 -0
- slmeval_cli-0.1.0/examples/judge/labels.example.jsonl +31 -0
- slmeval_cli-0.1.0/examples/judge/stub_target.py +3 -0
- slmeval_cli-0.1.0/examples/support_agent/README.md +89 -0
- slmeval_cli-0.1.0/examples/support_agent/__init__.py +42 -0
- slmeval_cli-0.1.0/examples/support_agent/agent.py +179 -0
- slmeval_cli-0.1.0/examples/support_agent/erp.py +108 -0
- slmeval_cli-0.1.0/examples/support_agent/eval.toml +213 -0
- slmeval_cli-0.1.0/examples/support_agent/golden_cases.jsonl +4 -0
- slmeval_cli-0.1.0/examples/support_agent/graph.py +197 -0
- slmeval_cli-0.1.0/examples/support_agent/tools.py +142 -0
- slmeval_cli-0.1.0/pyproject.toml +98 -0
- slmeval_cli-0.1.0/scripts/install_smoke.py +350 -0
- slmeval_cli-0.1.0/scripts/readme_visuals.py +565 -0
- slmeval_cli-0.1.0/scripts/teardown.py +347 -0
- slmeval_cli-0.1.0/src/slmeval/__init__.py +71 -0
- slmeval_cli-0.1.0/src/slmeval/__main__.py +14 -0
- slmeval_cli-0.1.0/src/slmeval/adapters/__init__.py +24 -0
- slmeval_cli-0.1.0/src/slmeval/adapters/instrument.py +135 -0
- slmeval_cli-0.1.0/src/slmeval/adapters/langgraph.py +237 -0
- slmeval_cli-0.1.0/src/slmeval/cli/__init__.py +15 -0
- slmeval_cli-0.1.0/src/slmeval/cli/commands/__init__.py +35 -0
- slmeval_cli-0.1.0/src/slmeval/cli/commands/baseline_cmd.py +201 -0
- slmeval_cli-0.1.0/src/slmeval/cli/commands/cache_cmd.py +249 -0
- slmeval_cli-0.1.0/src/slmeval/cli/commands/dataset_cmd.py +52 -0
- slmeval_cli-0.1.0/src/slmeval/cli/commands/doctor_cmd.py +68 -0
- slmeval_cli-0.1.0/src/slmeval/cli/commands/gate_cmd.py +602 -0
- slmeval_cli-0.1.0/src/slmeval/cli/commands/init_cmd.py +144 -0
- slmeval_cli-0.1.0/src/slmeval/cli/commands/judge_cmd.py +284 -0
- slmeval_cli-0.1.0/src/slmeval/cli/commands/record_cmd.py +154 -0
- slmeval_cli-0.1.0/src/slmeval/cli/commands/report_cmd.py +98 -0
- slmeval_cli-0.1.0/src/slmeval/cli/commands/run_cmd.py +247 -0
- slmeval_cli-0.1.0/src/slmeval/cli/commands/show_cmd.py +482 -0
- slmeval_cli-0.1.0/src/slmeval/cli/commands/stubs.py +57 -0
- slmeval_cli-0.1.0/src/slmeval/cli/commands/validate_cmd.py +296 -0
- slmeval_cli-0.1.0/src/slmeval/cli/loading.py +149 -0
- slmeval_cli-0.1.0/src/slmeval/cli/main.py +91 -0
- slmeval_cli-0.1.0/src/slmeval/cli/render.py +251 -0
- slmeval_cli-0.1.0/src/slmeval/cli/wrap.py +31 -0
- slmeval_cli-0.1.0/src/slmeval/config/__init__.py +47 -0
- slmeval_cli-0.1.0/src/slmeval/config/loader.py +363 -0
- slmeval_cli-0.1.0/src/slmeval/config/locate.py +97 -0
- slmeval_cli-0.1.0/src/slmeval/config/models.py +430 -0
- slmeval_cli-0.1.0/src/slmeval/config/templates.py +246 -0
- slmeval_cli-0.1.0/src/slmeval/core/__init__.py +78 -0
- slmeval_cli-0.1.0/src/slmeval/core/adapter.py +253 -0
- slmeval_cli-0.1.0/src/slmeval/core/context.py +418 -0
- slmeval_cli-0.1.0/src/slmeval/core/outcome.py +260 -0
- slmeval_cli-0.1.0/src/slmeval/core/router.py +319 -0
- slmeval_cli-0.1.0/src/slmeval/core/runner.py +1209 -0
- slmeval_cli-0.1.0/src/slmeval/core/trace.py +188 -0
- slmeval_cli-0.1.0/src/slmeval/core/verdict.py +127 -0
- slmeval_cli-0.1.0/src/slmeval/dataset/__init__.py +8 -0
- slmeval_cli-0.1.0/src/slmeval/dataset/loader.py +130 -0
- slmeval_cli-0.1.0/src/slmeval/dataset/models.py +108 -0
- slmeval_cli-0.1.0/src/slmeval/doctor/__init__.py +7 -0
- slmeval_cli-0.1.0/src/slmeval/doctor/checks.py +380 -0
- slmeval_cli-0.1.0/src/slmeval/errors.py +76 -0
- slmeval_cli-0.1.0/src/slmeval/eval/__init__.py +64 -0
- slmeval_cli-0.1.0/src/slmeval/eval/assertions.py +356 -0
- slmeval_cli-0.1.0/src/slmeval/eval/calibration.py +364 -0
- slmeval_cli-0.1.0/src/slmeval/eval/cost.py +291 -0
- slmeval_cli-0.1.0/src/slmeval/eval/pipeline.py +273 -0
- slmeval_cli-0.1.0/src/slmeval/eval/stats.py +376 -0
- slmeval_cli-0.1.0/src/slmeval/eval/tier1.py +510 -0
- slmeval_cli-0.1.0/src/slmeval/eval/tier2.py +582 -0
- slmeval_cli-0.1.0/src/slmeval/eval/tier3.py +415 -0
- slmeval_cli-0.1.0/src/slmeval/exit_codes.py +28 -0
- slmeval_cli-0.1.0/src/slmeval/gate.py +722 -0
- slmeval_cli-0.1.0/src/slmeval/providers/__init__.py +69 -0
- slmeval_cli-0.1.0/src/slmeval/providers/base.py +426 -0
- slmeval_cli-0.1.0/src/slmeval/providers/fake.py +760 -0
- slmeval_cli-0.1.0/src/slmeval/providers/jsonx.py +239 -0
- slmeval_cli-0.1.0/src/slmeval/providers/ladder.py +339 -0
- slmeval_cli-0.1.0/src/slmeval/providers/openai_compat.py +392 -0
- slmeval_cli-0.1.0/src/slmeval/providers/registry.py +185 -0
- slmeval_cli-0.1.0/src/slmeval/providers/retry.py +81 -0
- slmeval_cli-0.1.0/src/slmeval/providers/schema.py +136 -0
- slmeval_cli-0.1.0/src/slmeval/providers/tokens.py +150 -0
- slmeval_cli-0.1.0/src/slmeval/py.typed +0 -0
- slmeval_cli-0.1.0/src/slmeval/report/__init__.py +115 -0
- slmeval_cli-0.1.0/src/slmeval/report/attribution.py +197 -0
- slmeval_cli-0.1.0/src/slmeval/report/baseline.py +353 -0
- slmeval_cli-0.1.0/src/slmeval/report/drilldown.py +1395 -0
- slmeval_cli-0.1.0/src/slmeval/report/export.py +450 -0
- slmeval_cli-0.1.0/src/slmeval/report/model.py +562 -0
- slmeval_cli-0.1.0/src/slmeval/report/progress.py +257 -0
- slmeval_cli-0.1.0/src/slmeval/report/scorecard.py +367 -0
- slmeval_cli-0.1.0/src/slmeval/sandbox/__init__.py +145 -0
- slmeval_cli-0.1.0/src/slmeval/sandbox/diagnosis.py +690 -0
- slmeval_cli-0.1.0/src/slmeval/sandbox/envelope.py +486 -0
- slmeval_cli-0.1.0/src/slmeval/sandbox/faults.py +794 -0
- slmeval_cli-0.1.0/src/slmeval/sandbox/keys.py +458 -0
- slmeval_cli-0.1.0/src/slmeval/sandbox/normalizers.py +333 -0
- slmeval_cli-0.1.0/src/slmeval/sandbox/store.py +525 -0
- slmeval_cli-0.1.0/src/slmeval/sandbox/tool.py +685 -0
- slmeval_cli-0.1.0/src/slmeval/storage/__init__.py +73 -0
- slmeval_cli-0.1.0/src/slmeval/storage/blobs.py +308 -0
- slmeval_cli-0.1.0/src/slmeval/storage/codec.py +93 -0
- slmeval_cli-0.1.0/src/slmeval/storage/db.py +1334 -0
- slmeval_cli-0.1.0/src/slmeval/storage/ingest.py +346 -0
- slmeval_cli-0.1.0/src/slmeval/storage/migrations.py +358 -0
- slmeval_cli-0.1.0/src/slmeval/storage/models.py +554 -0
- slmeval_cli-0.1.0/src/slmeval/storage/provenance.py +201 -0
- slmeval_cli-0.1.0/teardown/README.md +231 -0
- slmeval_cli-0.1.0/teardown/artifacts/diagnosis-cassette-miss.txt +42 -0
- slmeval_cli-0.1.0/teardown/artifacts/failures-faults.txt +42 -0
- slmeval_cli-0.1.0/teardown/artifacts/failures.txt +27 -0
- slmeval_cli-0.1.0/teardown/artifacts/gate.txt +22 -0
- slmeval_cli-0.1.0/teardown/artifacts/report-faults.json +519 -0
- slmeval_cli-0.1.0/teardown/artifacts/report.json +534 -0
- slmeval_cli-0.1.0/teardown/artifacts/scorecard-faults.md +24 -0
- slmeval_cli-0.1.0/teardown/artifacts/scorecard.md +24 -0
- slmeval_cli-0.1.0/teardown/artifacts/scorecard.txt +39 -0
- slmeval_cli-0.1.0/teardown/artifacts/trace-forbidden-refund.txt +41 -0
- slmeval_cli-0.1.0/teardown/artifacts/trace-repaired-json.txt +61 -0
- slmeval_cli-0.1.0/teardown/artifacts/uncovered.txt +19 -0
- slmeval_cli-0.1.0/teardown/eval.toml +228 -0
- slmeval_cli-0.1.0/tests/__init__.py +1 -0
- slmeval_cli-0.1.0/tests/conftest.py +144 -0
- slmeval_cli-0.1.0/tests/offline.py +355 -0
- slmeval_cli-0.1.0/tests/snapshots/gate_failed.txt +31 -0
- slmeval_cli-0.1.0/tests/snapshots/gate_markdown.txt +23 -0
- slmeval_cli-0.1.0/tests/snapshots/miss_diagnosis.txt +8 -0
- slmeval_cli-0.1.0/tests/snapshots/miss_unknown_tool.txt +7 -0
- slmeval_cli-0.1.0/tests/snapshots/nodes.txt +7 -0
- slmeval_cli-0.1.0/tests/snapshots/per_case.txt +3 -0
- slmeval_cli-0.1.0/tests/snapshots/scorecard_full.txt +28 -0
- slmeval_cli-0.1.0/tests/snapshots/scorecard_table.txt +5 -0
- slmeval_cli-0.1.0/tests/snapshots/show_diff.txt +29 -0
- slmeval_cli-0.1.0/tests/snapshots/show_exec.txt +42 -0
- slmeval_cli-0.1.0/tests/snapshots/show_run.txt +17 -0
- slmeval_cli-0.1.0/tests/snapshots/show_uncovered.txt +7 -0
- slmeval_cli-0.1.0/tests/snapshots/summary_line.txt +1 -0
- slmeval_cli-0.1.0/tests/snapshots/warnings.txt +5 -0
- slmeval_cli-0.1.0/tests/test_adapter.py +219 -0
- slmeval_cli-0.1.0/tests/test_assertions.py +242 -0
- slmeval_cli-0.1.0/tests/test_blobs.py +208 -0
- slmeval_cli-0.1.0/tests/test_cache_cmd.py +229 -0
- slmeval_cli-0.1.0/tests/test_calibration.py +423 -0
- slmeval_cli-0.1.0/tests/test_cli.py +325 -0
- slmeval_cli-0.1.0/tests/test_config.py +247 -0
- slmeval_cli-0.1.0/tests/test_context.py +241 -0
- slmeval_cli-0.1.0/tests/test_cost.py +240 -0
- slmeval_cli-0.1.0/tests/test_dataset.py +125 -0
- slmeval_cli-0.1.0/tests/test_determinism.py +248 -0
- slmeval_cli-0.1.0/tests/test_diagnosis.py +420 -0
- slmeval_cli-0.1.0/tests/test_doctor.py +174 -0
- slmeval_cli-0.1.0/tests/test_drilldown.py +360 -0
- slmeval_cli-0.1.0/tests/test_envelope.py +349 -0
- slmeval_cli-0.1.0/tests/test_example_agent.py +278 -0
- slmeval_cli-0.1.0/tests/test_example_benchmark.py +294 -0
- slmeval_cli-0.1.0/tests/test_failure_matrix.py +214 -0
- slmeval_cli-0.1.0/tests/test_fake.py +646 -0
- slmeval_cli-0.1.0/tests/test_faults.py +434 -0
- slmeval_cli-0.1.0/tests/test_gate.py +562 -0
- slmeval_cli-0.1.0/tests/test_gate_cmd.py +398 -0
- slmeval_cli-0.1.0/tests/test_github_action.py +358 -0
- slmeval_cli-0.1.0/tests/test_jsonx.py +103 -0
- slmeval_cli-0.1.0/tests/test_keys.py +326 -0
- slmeval_cli-0.1.0/tests/test_ladder.py +215 -0
- slmeval_cli-0.1.0/tests/test_langgraph_adapter.py +480 -0
- slmeval_cli-0.1.0/tests/test_live_providers.py +148 -0
- slmeval_cli-0.1.0/tests/test_locate.py +72 -0
- slmeval_cli-0.1.0/tests/test_normalizers.py +197 -0
- slmeval_cli-0.1.0/tests/test_openai_compat.py +466 -0
- slmeval_cli-0.1.0/tests/test_outcome.py +286 -0
- slmeval_cli-0.1.0/tests/test_pipeline.py +322 -0
- slmeval_cli-0.1.0/tests/test_render.py +159 -0
- slmeval_cli-0.1.0/tests/test_report.py +480 -0
- slmeval_cli-0.1.0/tests/test_router.py +283 -0
- slmeval_cli-0.1.0/tests/test_run_cmd.py +348 -0
- slmeval_cli-0.1.0/tests/test_runner.py +840 -0
- slmeval_cli-0.1.0/tests/test_sandbox_tool.py +979 -0
- slmeval_cli-0.1.0/tests/test_show_cmd.py +288 -0
- slmeval_cli-0.1.0/tests/test_snapshots.py +340 -0
- slmeval_cli-0.1.0/tests/test_stats.py +275 -0
- slmeval_cli-0.1.0/tests/test_storage.py +941 -0
- slmeval_cli-0.1.0/tests/test_store.py +311 -0
- slmeval_cli-0.1.0/tests/test_teardown.py +305 -0
- slmeval_cli-0.1.0/tests/test_tier1.py +307 -0
- slmeval_cli-0.1.0/tests/test_tier2.py +310 -0
- slmeval_cli-0.1.0/tests/test_tier3.py +389 -0
- slmeval_cli-0.1.0/tests/test_tokens.py +141 -0
- slmeval_cli-0.1.0/tests/test_visuals.py +101 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Line endings are a property of this repository, not of each contributor's
|
|
2
|
+
# git config. The harness hashes generated files to decide whether two runs are
|
|
3
|
+
# comparable, so a CRLF committed from Windows must not read as a different
|
|
4
|
+
# dataset in CI on Linux.
|
|
5
|
+
* text=auto eol=lf
|
|
6
|
+
|
|
7
|
+
# Golden datasets are compared byte-for-byte; never let git touch them.
|
|
8
|
+
*.jsonl text eol=lf
|
|
9
|
+
|
|
10
|
+
# Rendered goldens, compared byte-for-byte against what the tool prints.
|
|
11
|
+
tests/snapshots/*.txt text eol=lf
|
|
12
|
+
|
|
13
|
+
# Published teardown artifacts, diffed against a fresh run by
|
|
14
|
+
# `scripts/teardown.py --check`, which CI runs on Windows and Linux.
|
|
15
|
+
teardown/artifacts/* text eol=lf
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Benchmark result
|
|
2
|
+
description: Share results from running slmeval on real models.
|
|
3
|
+
labels: [benchmark-result]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: Please remove anything private first. Cassettes and traces can contain real data.
|
|
8
|
+
- type: input
|
|
9
|
+
id: models
|
|
10
|
+
attributes:
|
|
11
|
+
label: Models and how they were served
|
|
12
|
+
placeholder: gemma3:4b on Ollama, baseline gpt-4o-mini
|
|
13
|
+
validations:
|
|
14
|
+
required: true
|
|
15
|
+
- type: textarea
|
|
16
|
+
id: report
|
|
17
|
+
attributes:
|
|
18
|
+
label: Output of `slmeval report --format md`
|
|
19
|
+
validations:
|
|
20
|
+
required: true
|
|
21
|
+
- type: textarea
|
|
22
|
+
id: config
|
|
23
|
+
attributes:
|
|
24
|
+
label: Your eval.toml
|
|
25
|
+
render: toml
|
|
26
|
+
validations:
|
|
27
|
+
required: true
|
|
28
|
+
- type: input
|
|
29
|
+
id: dataset
|
|
30
|
+
attributes:
|
|
31
|
+
label: Dataset
|
|
32
|
+
description: A link, or how many cases and what kind if it can't be shared.
|
|
33
|
+
validations:
|
|
34
|
+
required: true
|
|
35
|
+
- type: textarea
|
|
36
|
+
id: hardware
|
|
37
|
+
attributes:
|
|
38
|
+
label: Hardware, if you measured latency
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Something doesn't work as documented.
|
|
3
|
+
labels: [bug]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: what
|
|
7
|
+
attributes:
|
|
8
|
+
label: What happened?
|
|
9
|
+
description: Include what you expected and the full output, with any private data removed.
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
12
|
+
- type: textarea
|
|
13
|
+
id: reproduce
|
|
14
|
+
attributes:
|
|
15
|
+
label: How can we reproduce it?
|
|
16
|
+
description: The commands you ran and the relevant parts of eval.toml.
|
|
17
|
+
validations:
|
|
18
|
+
required: true
|
|
19
|
+
- type: textarea
|
|
20
|
+
id: doctor
|
|
21
|
+
attributes:
|
|
22
|
+
label: Output of `slmeval doctor`
|
|
23
|
+
render: text
|
|
24
|
+
validations:
|
|
25
|
+
required: true
|
|
26
|
+
- type: dropdown
|
|
27
|
+
id: exit-code
|
|
28
|
+
attributes:
|
|
29
|
+
label: Exit code
|
|
30
|
+
options:
|
|
31
|
+
- "0"
|
|
32
|
+
- "1"
|
|
33
|
+
- "2"
|
|
34
|
+
- "3"
|
|
35
|
+
- "4"
|
|
36
|
+
- Other, or a traceback
|
|
37
|
+
- Not applicable
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Feature request
|
|
2
|
+
description: Suggest a change or a new capability.
|
|
3
|
+
labels: [enhancement]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: problem
|
|
7
|
+
attributes:
|
|
8
|
+
label: What are you trying to do?
|
|
9
|
+
description: Describe the problem before the solution.
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
12
|
+
- type: textarea
|
|
13
|
+
id: proposal
|
|
14
|
+
attributes:
|
|
15
|
+
label: What would you like to see?
|
|
16
|
+
- type: textarea
|
|
17
|
+
id: alternatives
|
|
18
|
+
attributes:
|
|
19
|
+
label: What have you tried instead?
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
## What and why
|
|
2
|
+
|
|
3
|
+
<!-- What changes, and what problem it solves. Link any related issue. -->
|
|
4
|
+
|
|
5
|
+
## Testing
|
|
6
|
+
|
|
7
|
+
<!-- How you checked it. Put new failure modes under the `fake` provider. -->
|
|
8
|
+
|
|
9
|
+
## Checklist
|
|
10
|
+
|
|
11
|
+
- [ ] `ruff`, `mypy` and `pytest` pass
|
|
12
|
+
- [ ] Docs are updated if a command or flag changed
|
|
13
|
+
- [ ] Snapshots, teardown artifacts and README images are regenerated if the output changed
|
|
14
|
+
- [ ] The changelog mentions any change to exit codes or the report `schema`
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# The offline suite, on both platforms it has to be identical on.
|
|
2
|
+
#
|
|
3
|
+
# Determinism bugs in a harness like this are nearly all platform bugs: a hash
|
|
4
|
+
# that iterates a dict, a path separator inside a cassette key, a CRLF
|
|
5
|
+
# checkout, a console that cannot encode a box-drawing character. None of them
|
|
6
|
+
# reproduce on the machine they were written on, which is why the matrix is
|
|
7
|
+
# Windows *and* Linux rather than one of them.
|
|
8
|
+
#
|
|
9
|
+
# Nothing here needs a GPU, a model server or an API key. If a job in this
|
|
10
|
+
# file ever needs one, the test that made it necessary belongs behind
|
|
11
|
+
# `-m live` instead.
|
|
12
|
+
|
|
13
|
+
name: ci
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
push:
|
|
17
|
+
branches: [main]
|
|
18
|
+
pull_request:
|
|
19
|
+
workflow_dispatch:
|
|
20
|
+
|
|
21
|
+
concurrency:
|
|
22
|
+
group: ci-${{ github.ref }}
|
|
23
|
+
cancel-in-progress: true
|
|
24
|
+
|
|
25
|
+
env:
|
|
26
|
+
PYTHONUTF8: "1"
|
|
27
|
+
# The developer-loop target is under 30 seconds; a cold CI runner is slower
|
|
28
|
+
# than a laptop, so the gate here only has to catch a regression in kind -
|
|
29
|
+
# someone reintroducing a real sleep, a retry with a real backoff, or a test
|
|
30
|
+
# that opens a socket.
|
|
31
|
+
SUITE_BUDGET_S: "120"
|
|
32
|
+
|
|
33
|
+
jobs:
|
|
34
|
+
offline:
|
|
35
|
+
name: offline suite (${{ matrix.os }}, py${{ matrix.python }})
|
|
36
|
+
runs-on: ${{ matrix.os }}
|
|
37
|
+
strategy:
|
|
38
|
+
fail-fast: false
|
|
39
|
+
matrix:
|
|
40
|
+
os: [ubuntu-latest, windows-latest]
|
|
41
|
+
python: ["3.11", "3.13"]
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/checkout@v4
|
|
44
|
+
|
|
45
|
+
- uses: actions/setup-python@v5
|
|
46
|
+
with:
|
|
47
|
+
python-version: ${{ matrix.python }}
|
|
48
|
+
cache: pip
|
|
49
|
+
cache-dependency-path: pyproject.toml
|
|
50
|
+
|
|
51
|
+
- name: Install
|
|
52
|
+
run: python -m pip install --upgrade pip && python -m pip install -e ".[dev]"
|
|
53
|
+
|
|
54
|
+
- name: Lint
|
|
55
|
+
run: python -m ruff check src tests scripts
|
|
56
|
+
|
|
57
|
+
- name: Type-check
|
|
58
|
+
run: python -m mypy src
|
|
59
|
+
|
|
60
|
+
- name: Offline suite, within its time budget
|
|
61
|
+
shell: bash
|
|
62
|
+
run: |
|
|
63
|
+
start=$(date +%s)
|
|
64
|
+
python -m pytest --durations=10
|
|
65
|
+
elapsed=$(( $(date +%s) - start ))
|
|
66
|
+
echo "offline suite: ${elapsed}s (budget ${SUITE_BUDGET_S}s)"
|
|
67
|
+
if [ "$elapsed" -gt "$SUITE_BUDGET_S" ]; then
|
|
68
|
+
echo "::error::the offline suite took ${elapsed}s, over the ${SUITE_BUDGET_S}s budget."
|
|
69
|
+
echo "A suite people stop running protects nothing - see the slowest tests above."
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# The one job that installs a model framework. The core must stay installable
|
|
74
|
+
# in seconds without one, so `langgraph` is an extra and the adapter's tests
|
|
75
|
+
# skip in every other job here - which means that without this job they would
|
|
76
|
+
# never run anywhere, and a skipped test reads as a passing one.
|
|
77
|
+
langgraph:
|
|
78
|
+
name: langgraph adapter
|
|
79
|
+
runs-on: ubuntu-latest
|
|
80
|
+
steps:
|
|
81
|
+
- uses: actions/checkout@v4
|
|
82
|
+
|
|
83
|
+
- uses: actions/setup-python@v5
|
|
84
|
+
with:
|
|
85
|
+
python-version: "3.11"
|
|
86
|
+
cache: pip
|
|
87
|
+
cache-dependency-path: pyproject.toml
|
|
88
|
+
|
|
89
|
+
- name: Install, with the extra
|
|
90
|
+
run: python -m pip install --upgrade pip && python -m pip install -e ".[dev,langgraph]"
|
|
91
|
+
|
|
92
|
+
- name: The adapter, and the example under both adapters
|
|
93
|
+
run: python -m pytest tests/test_langgraph_adapter.py -v
|
|
94
|
+
|
|
95
|
+
- name: Nothing was skipped
|
|
96
|
+
shell: bash
|
|
97
|
+
run: |
|
|
98
|
+
# A green job that skipped the only two tests it exists to run would
|
|
99
|
+
# be worse than no job: it would read as coverage we do not have.
|
|
100
|
+
python -m pytest tests/test_langgraph_adapter.py -q -rs 2>&1 | tee out.txt
|
|
101
|
+
if grep -q "skipped" out.txt; then
|
|
102
|
+
echo "::error::a langgraph test skipped in the job that installs langgraph."
|
|
103
|
+
exit 1
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# The published teardown, re-run. `tests/test_teardown.py` checks that the
|
|
107
|
+
# write-up still agrees with the artifacts next to it; only re-running the
|
|
108
|
+
# benchmark can check that the artifacts still agree with the tool. Without
|
|
109
|
+
# this, the first change that moved a number would leave a document on the
|
|
110
|
+
# front page of the repository quoting figures nothing produces any more.
|
|
111
|
+
#
|
|
112
|
+
# Both platforms, because the artifacts are committed from one of them and a
|
|
113
|
+
# path separator or a console codepage leaking into a published file is
|
|
114
|
+
# exactly the class of bug this repository's CI matrix exists to catch.
|
|
115
|
+
teardown:
|
|
116
|
+
name: the published teardown still reproduces (${{ matrix.os }})
|
|
117
|
+
runs-on: ${{ matrix.os }}
|
|
118
|
+
strategy:
|
|
119
|
+
fail-fast: false
|
|
120
|
+
matrix:
|
|
121
|
+
os: [ubuntu-latest, windows-latest]
|
|
122
|
+
steps:
|
|
123
|
+
- uses: actions/checkout@v4
|
|
124
|
+
|
|
125
|
+
- uses: actions/setup-python@v5
|
|
126
|
+
with:
|
|
127
|
+
python-version: "3.11"
|
|
128
|
+
cache: pip
|
|
129
|
+
cache-dependency-path: pyproject.toml
|
|
130
|
+
|
|
131
|
+
- name: Install
|
|
132
|
+
run: python -m pip install --upgrade pip && python -m pip install -e .
|
|
133
|
+
|
|
134
|
+
- name: Record, run, and diff against what is published
|
|
135
|
+
run: python scripts/teardown.py --check
|
|
136
|
+
|
|
137
|
+
install-smoke:
|
|
138
|
+
name: install smoke (${{ matrix.os }})
|
|
139
|
+
runs-on: ${{ matrix.os }}
|
|
140
|
+
strategy:
|
|
141
|
+
fail-fast: false
|
|
142
|
+
matrix:
|
|
143
|
+
os: [ubuntu-latest, windows-latest]
|
|
144
|
+
steps:
|
|
145
|
+
- uses: actions/checkout@v4
|
|
146
|
+
|
|
147
|
+
- uses: actions/setup-python@v5
|
|
148
|
+
with:
|
|
149
|
+
python-version: "3.11"
|
|
150
|
+
|
|
151
|
+
# `pytest` runs against the working tree and so proves nothing about what
|
|
152
|
+
# `pip install slmeval-cli` ships. This builds the wheel, installs it into a
|
|
153
|
+
# throwaway venv with nothing else in it, and runs init / validate /
|
|
154
|
+
# doctor / record / run / report as a new user would.
|
|
155
|
+
- name: Build and use the wheel
|
|
156
|
+
run: python scripts/install_smoke.py
|
|
157
|
+
|
|
158
|
+
- uses: actions/upload-artifact@v4
|
|
159
|
+
if: always()
|
|
160
|
+
with:
|
|
161
|
+
name: wheel-${{ matrix.os }}
|
|
162
|
+
path: dist/*.whl
|
|
163
|
+
if-no-files-found: error
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# The GitHub Action, proved on the bundled example.
|
|
2
|
+
#
|
|
3
|
+
# This is the "demo repo" check: it exercises `examples/github-action` end to
|
|
4
|
+
# end against `examples/support_agent`, on a stock runner, with **no GPU, no
|
|
5
|
+
# API key and no secret of any kind**. The baseline and both candidates are the
|
|
6
|
+
# scripted `fake` provider and every tool is a local Python function, so the
|
|
7
|
+
# whole thing - record, replay, gate - runs offline.
|
|
8
|
+
#
|
|
9
|
+
# Two jobs, because an action that can only go green is not a gate:
|
|
10
|
+
#
|
|
11
|
+
# `green` gates on a floor the example clears and expects exit 0.
|
|
12
|
+
# `red` gates on a floor nothing can clear and expects exit 1 - a regression,
|
|
13
|
+
# not a crash, not a usage error, and not exit 3. That distinction is the whole
|
|
14
|
+
# contract CI branches on, and it is the one thing worth asserting on every
|
|
15
|
+
# push.
|
|
16
|
+
#
|
|
17
|
+
# The job summary and the pull-request comment this produces are also the
|
|
18
|
+
# artefacts to screenshot for the README: this workflow is where the output in
|
|
19
|
+
# the docs comes from.
|
|
20
|
+
|
|
21
|
+
name: gate-demo
|
|
22
|
+
|
|
23
|
+
on:
|
|
24
|
+
pull_request:
|
|
25
|
+
push:
|
|
26
|
+
branches: [main]
|
|
27
|
+
workflow_dispatch:
|
|
28
|
+
|
|
29
|
+
permissions:
|
|
30
|
+
contents: read
|
|
31
|
+
pull-requests: write
|
|
32
|
+
|
|
33
|
+
concurrency:
|
|
34
|
+
group: gate-demo-${{ github.ref }}
|
|
35
|
+
cancel-in-progress: true
|
|
36
|
+
|
|
37
|
+
jobs:
|
|
38
|
+
green:
|
|
39
|
+
name: the action passes a healthy run
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v4
|
|
43
|
+
|
|
44
|
+
- id: slmeval
|
|
45
|
+
uses: ./examples/github-action
|
|
46
|
+
with:
|
|
47
|
+
working-directory: examples/support_agent
|
|
48
|
+
# The checkout itself, not a release: this workflow is what proves
|
|
49
|
+
# the action works against the code in the pull request.
|
|
50
|
+
version: .
|
|
51
|
+
# Safe here, and only here: every tool in the example is a local
|
|
52
|
+
# function and the provider is `fake`, so recording touches nothing
|
|
53
|
+
# outside the runner.
|
|
54
|
+
record: missing
|
|
55
|
+
# The candidate whose story is "passes, but only with help": it
|
|
56
|
+
# answers every case correctly and needs a lower rung on every
|
|
57
|
+
# structured call to do it.
|
|
58
|
+
run-args: --candidate fenced-json
|
|
59
|
+
# A floor the example clears, on the estimator that matters, and not
|
|
60
|
+
# a number tuned to pass: 4/4 cases over 3 runs each has a Wilson
|
|
61
|
+
# lower bound of 51%, so this is as high as a floor can honestly go
|
|
62
|
+
# at n=4. Asking for 0.9 here would be asking for a guarantee the
|
|
63
|
+
# sample size cannot give - which is the whole lesson of the tool.
|
|
64
|
+
gate-args: --min-pass-k 0.5 --use-wilson-lower
|
|
65
|
+
artifact-name: slmeval-demo-green
|
|
66
|
+
|
|
67
|
+
- name: The outputs say what happened
|
|
68
|
+
run: |
|
|
69
|
+
test '${{ steps.slmeval.outputs.verdict }}' = passed
|
|
70
|
+
test '${{ steps.slmeval.outputs.exit-code }}' = 0
|
|
71
|
+
test -n '${{ steps.slmeval.outputs.run-id }}'
|
|
72
|
+
test -n '${{ steps.slmeval.outputs.dataset-hash }}'
|
|
73
|
+
echo "run ${{ steps.slmeval.outputs.run-id }} passed"
|
|
74
|
+
|
|
75
|
+
red:
|
|
76
|
+
name: the action fails a regression with exit 1
|
|
77
|
+
runs-on: ubuntu-latest
|
|
78
|
+
steps:
|
|
79
|
+
- uses: actions/checkout@v4
|
|
80
|
+
|
|
81
|
+
- id: slmeval
|
|
82
|
+
# continue-on-error, not a removed threshold: the action *must* fail
|
|
83
|
+
# the step here. A demo that only ever went green would prove the
|
|
84
|
+
# pretty half of a gate and none of the useful half.
|
|
85
|
+
continue-on-error: true
|
|
86
|
+
uses: ./examples/github-action
|
|
87
|
+
with:
|
|
88
|
+
working-directory: examples/support_agent
|
|
89
|
+
version: .
|
|
90
|
+
record: missing
|
|
91
|
+
# Nothing can clear a floor of 100% on a Wilson lower bound, so this
|
|
92
|
+
# is the regression path.
|
|
93
|
+
gate-args: --min-pass-k 1.0 --use-wilson-lower
|
|
94
|
+
comment: "false"
|
|
95
|
+
artifact-name: slmeval-demo-red
|
|
96
|
+
|
|
97
|
+
- name: Exit 1, and labelled as a regression
|
|
98
|
+
run: |
|
|
99
|
+
if [ '${{ steps.slmeval.outputs.exit-code }}' != '1' ]; then
|
|
100
|
+
echo "::error::the gate exited '${{ steps.slmeval.outputs.exit-code }}', not 1."
|
|
101
|
+
echo "1 means a threshold was missed. 2 is a usage error, 3 is an unreachable"
|
|
102
|
+
echo "provider and 4 is thin coverage - if an unclearable floor produces any of"
|
|
103
|
+
echo "those, CI cannot tell a regression from a broken runner."
|
|
104
|
+
exit 1
|
|
105
|
+
fi
|
|
106
|
+
test '${{ steps.slmeval.outputs.verdict }}' = failed
|
|
107
|
+
echo "a missed threshold exits 1, as the contract says"
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Publish to PyPI on a tag, with Trusted Publishing.
|
|
2
|
+
#
|
|
3
|
+
# No long-lived token in this repository's secrets. PyPI mints a short-lived
|
|
4
|
+
# credential for this exact workflow in this exact repository, via OIDC, which
|
|
5
|
+
# is why the publish job asks for `id-token: write` and has no `password:`. A
|
|
6
|
+
# token in secrets is a credential that can be exfiltrated by any workflow that
|
|
7
|
+
# can read secrets, and for a measurement tool people are asked to install into
|
|
8
|
+
# their CI, that is the wrong trade.
|
|
9
|
+
#
|
|
10
|
+
# It also refuses to publish a tag that disagrees with the package: a `v0.1.1`
|
|
11
|
+
# tag on a tree whose `__version__` still says `0.1.0` would put a wheel on
|
|
12
|
+
# PyPI that no git tag describes.
|
|
13
|
+
#
|
|
14
|
+
# Setup, once, before the first tag: on PyPI, add a trusted publisher for this
|
|
15
|
+
# repository with workflow `release.yml` and environment `pypi`.
|
|
16
|
+
#
|
|
17
|
+
# git tag v0.1.0 && git push origin v0.1.0
|
|
18
|
+
|
|
19
|
+
name: release
|
|
20
|
+
|
|
21
|
+
on:
|
|
22
|
+
push:
|
|
23
|
+
tags: ["v*"]
|
|
24
|
+
workflow_dispatch:
|
|
25
|
+
inputs:
|
|
26
|
+
dry-run:
|
|
27
|
+
description: Build and check, but do not publish.
|
|
28
|
+
type: boolean
|
|
29
|
+
default: true
|
|
30
|
+
|
|
31
|
+
permissions:
|
|
32
|
+
contents: read
|
|
33
|
+
|
|
34
|
+
jobs:
|
|
35
|
+
# A tag is not a release until the thing it points at actually works. The
|
|
36
|
+
# offline suite and the install smoke both run again here rather than
|
|
37
|
+
# trusting that they ran on the commit: a tag can point anywhere.
|
|
38
|
+
verify:
|
|
39
|
+
name: verify the tagged tree (${{ matrix.os }})
|
|
40
|
+
runs-on: ${{ matrix.os }}
|
|
41
|
+
strategy:
|
|
42
|
+
fail-fast: true
|
|
43
|
+
matrix:
|
|
44
|
+
os: [ubuntu-latest, windows-latest]
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/checkout@v4
|
|
47
|
+
|
|
48
|
+
- uses: actions/setup-python@v5
|
|
49
|
+
with:
|
|
50
|
+
python-version: "3.11"
|
|
51
|
+
|
|
52
|
+
- name: Install
|
|
53
|
+
run: python -m pip install --upgrade pip && python -m pip install -e ".[dev]"
|
|
54
|
+
|
|
55
|
+
- name: The tag and the package agree
|
|
56
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
57
|
+
shell: bash
|
|
58
|
+
run: |
|
|
59
|
+
set -euo pipefail
|
|
60
|
+
tagged="${GITHUB_REF_NAME#v}"
|
|
61
|
+
packaged=$(python -c "import slmeval; print(slmeval.__version__)")
|
|
62
|
+
echo "tag $tagged, package $packaged"
|
|
63
|
+
if [ "$tagged" != "$packaged" ]; then
|
|
64
|
+
echo "::error::tag $GITHUB_REF_NAME does not match __version__ $packaged."
|
|
65
|
+
echo "Publishing would put a wheel on PyPI that no git tag describes."
|
|
66
|
+
exit 1
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
- name: Offline suite
|
|
70
|
+
run: python -m pytest
|
|
71
|
+
|
|
72
|
+
- name: The wheel installs and produces a scorecard
|
|
73
|
+
run: python scripts/install_smoke.py
|
|
74
|
+
|
|
75
|
+
build:
|
|
76
|
+
name: build the distributions
|
|
77
|
+
needs: verify
|
|
78
|
+
runs-on: ubuntu-latest
|
|
79
|
+
steps:
|
|
80
|
+
- uses: actions/checkout@v4
|
|
81
|
+
|
|
82
|
+
- uses: actions/setup-python@v5
|
|
83
|
+
with:
|
|
84
|
+
python-version: "3.11"
|
|
85
|
+
|
|
86
|
+
- name: Build
|
|
87
|
+
run: |
|
|
88
|
+
python -m pip install --upgrade pip build twine
|
|
89
|
+
python -m build
|
|
90
|
+
python -m twine check --strict dist/*
|
|
91
|
+
|
|
92
|
+
- uses: actions/upload-artifact@v4
|
|
93
|
+
with:
|
|
94
|
+
name: distributions
|
|
95
|
+
path: dist/*
|
|
96
|
+
if-no-files-found: error
|
|
97
|
+
|
|
98
|
+
publish:
|
|
99
|
+
name: publish to PyPI
|
|
100
|
+
needs: build
|
|
101
|
+
if: startsWith(github.ref, 'refs/tags/v') && !inputs.dry-run
|
|
102
|
+
runs-on: ubuntu-latest
|
|
103
|
+
environment: pypi
|
|
104
|
+
permissions:
|
|
105
|
+
# The whole point: PyPI mints a short-lived credential for this workflow
|
|
106
|
+
# over OIDC, so there is no token in this repository to leak.
|
|
107
|
+
id-token: write
|
|
108
|
+
steps:
|
|
109
|
+
- uses: actions/download-artifact@v4
|
|
110
|
+
with:
|
|
111
|
+
name: distributions
|
|
112
|
+
path: dist
|
|
113
|
+
|
|
114
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Versions follow [semantic versioning](https://semver.org). While the major version is 0, a
|
|
4
|
+
minor release can change the CLI and the stored schema.
|
|
5
|
+
|
|
6
|
+
The exit codes of `slmeval gate` and the `schema` field of the JSON report are public
|
|
7
|
+
contracts. Any change to them is listed here as breaking.
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-11-04
|
|
10
|
+
|
|
11
|
+
First release, published on PyPI as `slmeval-cli`. The command and the Python import are
|
|
12
|
+
both `slmeval`.
|
|
13
|
+
|
|
14
|
+
### Harness
|
|
15
|
+
|
|
16
|
+
- `slmeval.node()` and `slmeval.chat_model()` mark steps and route model calls per candidate.
|
|
17
|
+
Both do nothing outside a benchmark.
|
|
18
|
+
- `slmeval.instrument_langgraph()` marks every node of a LangGraph builder. Requires the
|
|
19
|
+
`langgraph` extra.
|
|
20
|
+
- `@sandbox_tool` records a tool call once and replays it for every candidate. A miss gets a
|
|
21
|
+
verdict and a diff of the arguments against the nearest recording.
|
|
22
|
+
- Recorded responses keep their types, including `Decimal`, `datetime` and enums. A value that
|
|
23
|
+
can't be stored faithfully raises an error at record time.
|
|
24
|
+
- Seven kinds of fault injection, scheduled per case and seed. Each run records the faults it met.
|
|
25
|
+
- Providers for OpenAI, Ollama, anything serving `/v1/chat/completions`, and a scripted `fake`.
|
|
26
|
+
Reports show which structured-output mechanism each call needed.
|
|
27
|
+
|
|
28
|
+
### Measurement
|
|
29
|
+
|
|
30
|
+
- Dataset assertions, including forbidden tools as a hard fail.
|
|
31
|
+
- Path distance, tool overlap with the baseline, loop detection and recovery from faults.
|
|
32
|
+
- `pass@k`, `pass^k` and Wilson intervals. The gate reads the lower bound by default.
|
|
33
|
+
- Cost per case. Local inference is reported as modelled from token counts and `--gpu-hourly`.
|
|
34
|
+
- Latency only in `--serial-latency` mode.
|
|
35
|
+
- Bugs in slmeval, unrecorded paths and unreachable providers stay out of pass rates and are
|
|
36
|
+
reported separately.
|
|
37
|
+
- An LLM judge with a calibration harness, off by default. It isn't connected to `slmeval run` yet.
|
|
38
|
+
|
|
39
|
+
### CLI
|
|
40
|
+
|
|
41
|
+
`init`, `doctor`, `validate`, `record`, `run`, `report`, `show run|exec|case|uncovered`, `gate`,
|
|
42
|
+
`baseline save|show`, `judge calibrate`, `dataset validate` and `cache stats|prune`.
|
|
43
|
+
|
|
44
|
+
Reports render as a table, JSON, Markdown, CSV or JUnit XML.
|
|
45
|
+
|
|
46
|
+
Exit codes: 0 pass, 1 gate failed, 2 usage, 3 environment, 4 coverage.
|
|
47
|
+
|
|
48
|
+
### Also included
|
|
49
|
+
|
|
50
|
+
- A [GitHub Action](examples/github-action) that runs the benchmark, writes the job summary and
|
|
51
|
+
keeps one pull request comment updated.
|
|
52
|
+
- A [runnable example](examples/support_agent) and a [judge calibration example](examples/judge).
|
|
53
|
+
- [Teardown #1](teardown), a published run with scripted models. CI re-runs it on Linux and Windows.
|
|
54
|
+
|
|
55
|
+
### Not in this release
|
|
56
|
+
|
|
57
|
+
- `slmeval compare` and `slmeval cassette`. They are hidden from `--help` and exit 2 if called.
|
|
58
|
+
- A native Anthropic provider. Reach Claude through an OpenAI-compatible gateway.
|
|
59
|
+
- Time-to-first-token measurement.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
This project follows the
|
|
4
|
+
[Contributor Covenant, version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
|
|
5
|
+
|
|
6
|
+
To report unacceptable behaviour, contact the maintainer privately through
|
|
7
|
+
[GitHub](https://github.com/mohitraj3697). Reports are kept confidential.
|