diff --git a/Makefile b/Makefile index 7f5727e2..dbfca420 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help install install-dev langgraph-dev test test-unit test-provider openai anthropic nv_build test-integration test-cov test-ci lint lint-fix format format-check clean build docker-build docker-smoke +.PHONY: help install install-dev langgraph-dev test test-unit test-provider openai anthropic nv_build test-integration test-cov test-ci verify lint lint-fix format format-check clean build docker-build docker-smoke # Prefer uv if available, else use pip (set when Makefile is parsed) UV := $(shell command -v uv 2>/dev/null) @@ -37,6 +37,7 @@ help: @echo " make test-provider [openai|anthropic|nv_build] - Run live provider tests" @echo " make test-integration - Run integration tests only (invokes full graph, may call LLMs)" @echo " make test-cov - Run tests with coverage report" + @echo " make verify - Check deterministic and neural-envelope safety invariants" @echo " make lint - Run linters (ruff only)" @echo " make lint-fix - Auto-fix lint errors with ruff" @echo " make format - Format code with ruff" @@ -105,6 +106,10 @@ test-cov: test-ci: pytest -m "not integration and not provider" --cov=src/skillspector --cov-report=term-missing --cov-report=xml tests/ +# Run the fast executable specification and adversarial-oracle property suite. +verify: + pytest tests/verification/ + # Run linters (fast: ruff only) lint: @echo "Running ruff..." diff --git a/README.md b/README.md index dd3cd4b8..664d60ca 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ SkillSpector is part of the [NVIDIA Verified Skills pipeline](https://docs.nvidi - **[Scan agent skills before installation](https://docs.nvidia.com/skills/scanning-agent-skills)** — Hosted guide: when to scan, how to read a report, and how to gate installs. - **[Development guide](docs/DEVELOPMENT.md)** — Architecture, package layout, and how to extend the analyzer pipeline. - **[Analysis resource bounds](docs/ANALYSIS_RESOURCE_BOUNDS.md)** — Fail-closed bundle, parser, nested-artifact, ledger, and finding ceilings. +- **[Verification boundary](docs/VERIFICATION.md)** — Executable safety contracts around deterministic scoring and LLM enrichment. - **[Pi extension](docs/PI_EXTENSION.md)** — Install SkillSpector as a Pi tool for scanning skills from inside agent sessions. ## Features diff --git a/docs/VERIFICATION.md b/docs/VERIFICATION.md new file mode 100644 index 00000000..110de23f --- /dev/null +++ b/docs/VERIFICATION.md @@ -0,0 +1,35 @@ +# Verification boundary + +SkillSpector combines deterministic analyzers with optional model inference. Verification +claims must keep those surfaces separate: + +| Stratum | Surface | Contract | Mechanism | +| --- | --- | --- | --- | +| S1 | risk scoring and banding | the implementation matches the executable reference model | differential property tests | +| S2 | deterministic findings crossing the LLM meta-analyzer | arbitrary valid model outcomes cannot remove, invent, or weaken deterministic evidence | adversarial-oracle property tests | +| S3 | semantic conclusions produced by an LLM | no universal correctness claim | evaluation datasets and provider tests | + +Run the fast S1/S2 contract with: + +```bash +make verify +``` + +## Phase 1 invariants + +- Risk scores are integers in `[0, 100]` and map exhaustively to the documented bands. +- Diminishing returns apply independently per rule with weights `1`, `0.5`, and `0.25`. +- Within a rule, findings are ordered by their unweighted score contribution (severity, + confidence, and executable provenance together). This makes the score permutation-invariant + and monotone: adding non-negative evidence cannot lower the score by taking a larger + diminishing-return weight away from stronger evidence. +- Blocking floors for proven SC8, BH2, and BH3 conditions survive ordinary weighted scoring. +- Executable-file multiplication is scoped by both source provenance and path. +- The LLM meta-analyzer may enrich a deterministic finding, but cannot remove it, change its + identity or severity, lower its confidence, or create a new deterministic finding. + +The reference model in `tests/verification/risk_reference.py` intentionally imports no +production scoring constants or helpers. A scoring-policy change must update that readable +specification deliberately. The suite does not claim to verify static detector recall, LLM +truthfulness, provider availability, or arbitrary malformed data rejected before the typed +meta-analyzer boundary. diff --git a/pyproject.toml b/pyproject.toml index df3d3286..071ceff9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ dev = [ "pytest>=9.0.0", "pytest-asyncio>=1.3.0", "pytest-cov>=7.0.0", + "hypothesis>=6.151.9", "ruff>=0.15.0", "mypy>=1.19.0", "build>=1.4.0", diff --git a/src/skillspector/nodes/report.py b/src/skillspector/nodes/report.py index 284cdde1..12b30850 100644 --- a/src/skillspector/nodes/report.py +++ b/src/skillspector/nodes/report.py @@ -453,19 +453,17 @@ def _compute_risk_score( to [0, 1]). Findings with confidence <= 0 are skipped entirely — they do not contribute to the score but remain in the reported findings list. - Within each rule_id bucket, findings are processed in severity-descending - order so that the highest-severity occurrence always receives the full weight. + Within each rule_id bucket, findings are processed by their unweighted score + contribution (severity points x confidence x executable multiplier). This gives + the strongest evidence the largest diminishing-return weight, makes the score + independent of analyzer output order, and ensures adding non-negative evidence + cannot lower the score by displacing a stronger occurrence. Base points per severity: CRITICAL=50, HIGH=25, MEDIUM=10, LOW=5. 1.3x multiplier applied only to findings from executable script files; findings from documentation files (markdown, text, json, yaml, toml) are scored at base weight to avoid punishing security documentation. """ - severity_rank = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3} - sorted_findings = sorted( - findings, - key=lambda f: (f.rule_id or "UNKNOWN", severity_rank.get((f.severity or "LOW").upper(), 4)), - ) def component_source_scope(component: Mapping[str, object]) -> str: for key in ("source_identity", "source_url", "source_digest"): @@ -491,6 +489,24 @@ def finding_source_scope(finding: Finding) -> str: cm.get("executable", False) ) + def finding_strength(finding: Finding) -> float: + confidence = max(0.0, min(1.0, finding.confidence)) + severity = (finding.severity or "LOW").upper() + strength = _SEVERITY_POINTS.get(severity, 5) * confidence + if has_executable_scripts and file_executable.get( + (finding_source_scope(finding), finding.file), False + ): + strength *= 1.3 + return strength + + sorted_findings = sorted( + findings, + key=lambda f: ( + f.rule_id or "UNKNOWN", + -finding_strength(f), + ), + ) + rule_occurrence_count: dict[str, int] = {} score = 0.0 diff --git a/tests/nodes/test_report.py b/tests/nodes/test_report.py index 785868e2..f9c4b8b5 100644 --- a/tests/nodes/test_report.py +++ b/tests/nodes/test_report.py @@ -327,6 +327,17 @@ def test_same_rule_low_before_critical_sorted_correctly(self) -> None: # Sorted: CRITICAL first (50*1.0) + LOW second (5*0.5=2.5) = 52.5 -> 52 assert score == 52 + def test_weak_critical_does_not_lower_score_by_displacing_stronger_high(self) -> None: + """Weight allocation follows score strength, not severity alone.""" + stronger_high = _finding("TM1", "HIGH", confidence=0.1) + weak_critical = _finding("TM1", "CRITICAL", confidence=0.01) + + before, _, _ = _compute_risk_score([stronger_high], False) + after, _, _ = _compute_risk_score([stronger_high, weak_critical], False) + + assert before == 2 + assert after == 2 + def test_exact_band_boundary_21_is_medium(self) -> None: findings = [ _finding("R1", "MEDIUM", confidence=1.0), diff --git a/tests/verification/__init__.py b/tests/verification/__init__.py new file mode 100644 index 00000000..1dd4dc5b --- /dev/null +++ b/tests/verification/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Executable safety specifications for SkillSpector's deterministic envelope.""" diff --git a/tests/verification/risk_reference.py b/tests/verification/risk_reference.py new file mode 100644 index 00000000..74bac930 --- /dev/null +++ b/tests/verification/risk_reference.py @@ -0,0 +1,112 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Small independent reference model for the risk-scoring contract. + +This model deliberately does not import production scoring constants or helpers. A +production change must therefore update this readable specification deliberately, +rather than making differential tests agree through shared implementation details. +""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from dataclasses import dataclass, field + + +@dataclass(frozen=True) +class ReferenceFinding: + """Only the finding fields that influence the risk score.""" + + rule_id: str = "UNKNOWN" + severity: str = "LOW" + confidence: float = 0.5 + file: str = "SKILL.md" + source_identity: str | None = None + source_url: str | None = None + source_digest: str | None = None + evidence: Mapping[str, object] = field(default_factory=dict) + + +_POINTS = {"CRITICAL": 50, "HIGH": 25, "MEDIUM": 10, "LOW": 5} +_WEIGHTS = (1.0, 0.5, 0.25) +_BANDS = ((81, "CRITICAL"), (51, "HIGH"), (21, "MEDIUM"), (0, "LOW")) +_RECOMMENDATIONS = { + "CRITICAL": "DO_NOT_INSTALL", + "HIGH": "DO_NOT_INSTALL", + "MEDIUM": "CAUTION", + "LOW": "SAFE", +} + + +def _confidence(value: float) -> float: + return max(0.0, min(1.0, value)) + + +def _source_scope(item: ReferenceFinding | Mapping[str, object]) -> str: + for key in ("source_identity", "source_url", "source_digest"): + value = getattr(item, key, None) if isinstance(item, ReferenceFinding) else item.get(key) + if isinstance(value, str) and value: + return f"{key}:{value}" + return "" + + +def _score_floor(finding: ReferenceFinding) -> int: + if finding.rule_id == "SC8": + return 51 + if finding.severity.upper() != "CRITICAL": + return 0 + if finding.evidence.get("activation_state") != "conditional": + return 0 + if finding.rule_id == "BH2" and finding.evidence.get("proof_status") == "closed": + return 51 + if finding.rule_id == "BH3": + return 51 + return 0 + + +def reference_risk_score( + findings: Sequence[ReferenceFinding], + has_executable_scripts: bool, + component_metadata: Sequence[Mapping[str, object]] = (), +) -> tuple[int, str, str]: + """Evaluate the documented scoring contract without production helpers.""" + executable = { + (_source_scope(component), str(component.get("path", ""))): bool( + component.get("executable", False) + ) + for component in component_metadata + } + + def strength(item: ReferenceFinding) -> float: + severity = item.severity.upper() if item.severity else "LOW" + value = _POINTS.get(severity, 5) * _confidence(item.confidence) + if has_executable_scripts and executable.get((_source_scope(item), item.file), False): + value *= 1.3 + return value + + ordered = sorted( + findings, + key=lambda item: (item.rule_id or "UNKNOWN", -strength(item)), + ) + occurrences: dict[str, int] = {} + score = 0.0 + floor = 0 + for finding in ordered: + confidence = _confidence(finding.confidence) + if confidence <= 0.0: + continue + floor = max(floor, _score_floor(finding)) + rule_id = finding.rule_id or "UNKNOWN" + index = occurrences.get(rule_id, 0) + occurrences[rule_id] = index + 1 + if index >= len(_WEIGHTS): + continue + contribution = _POINTS.get(finding.severity.upper(), 5) * _WEIGHTS[index] * confidence + if has_executable_scripts and executable.get((_source_scope(finding), finding.file), False): + contribution *= 1.3 + score += contribution + + final = min(100, max(floor, int(score))) + band = next(label for threshold, label in _BANDS if final >= threshold) + return final, band, _RECOMMENDATIONS[band] diff --git a/tests/verification/test_safety_properties.py b/tests/verification/test_safety_properties.py new file mode 100644 index 00000000..17704248 --- /dev/null +++ b/tests/verification/test_safety_properties.py @@ -0,0 +1,247 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Differential and adversarial properties for the scanner's safety envelope.""" + +from __future__ import annotations + +import random +from dataclasses import replace + +from hypothesis import given, settings +from hypothesis import strategies as st + +from skillspector.llm_analyzer_base import Batch +from skillspector.models import Finding +from skillspector.nodes.meta_analyzer import LLMMetaAnalyzer +from skillspector.nodes.report import _compute_risk_score +from tests.verification.risk_reference import ReferenceFinding, reference_risk_score + +SEVERITIES = ("CRITICAL", "HIGH", "MEDIUM", "LOW", "UNKNOWN") + + +@st.composite +def reference_findings(draw: st.DrawFn) -> ReferenceFinding: + rule_id = draw(st.sampled_from(("R1", "R2", "SC8", "BH2", "BH3", ""))) + severity = draw(st.sampled_from(SEVERITIES)) + confidence = draw( + st.floats(min_value=-1.0, max_value=2.0, allow_nan=False, allow_infinity=False) + ) + file = draw(st.sampled_from(("SKILL.md", "run.py", "nested/run.py"))) + identity = draw(st.one_of(st.none(), st.sampled_from(("scope-a", "scope-b")))) + evidence: dict[str, object] = {} + if rule_id in {"BH2", "BH3"}: + evidence["activation_state"] = draw(st.sampled_from(("conditional", "inactive"))) + if rule_id == "BH2": + evidence["proof_status"] = draw(st.sampled_from(("closed", "open"))) + return ReferenceFinding( + rule_id=rule_id, + severity=severity, + confidence=confidence, + file=file, + source_identity=identity, + evidence=evidence, + ) + + +def _production_finding(item: ReferenceFinding) -> Finding: + return Finding( + rule_id=item.rule_id, + message="verification finding", + severity=item.severity, + confidence=item.confidence, + file=item.file, + source_identity=item.source_identity, + source_url=item.source_url, + source_digest=item.source_digest, + evidence=dict(item.evidence), + ) + + +@given(st.lists(reference_findings(), max_size=12), st.booleans()) +@settings(max_examples=500, deadline=None) +def test_risk_core_matches_independent_reference_model( + findings: list[ReferenceFinding], has_executable_scripts: bool +) -> None: + metadata: list[dict[str, object]] = [ + { + "path": item.file, + "source_identity": item.source_identity, + "executable": item.file.endswith(".py"), + } + for item in findings + ] + expected = reference_risk_score(findings, has_executable_scripts, metadata) + actual = _compute_risk_score( + [_production_finding(item) for item in findings], has_executable_scripts, metadata + ) + assert actual == expected + assert 0 <= actual[0] <= 100 + + +@given(st.lists(reference_findings(), max_size=12), st.booleans(), st.randoms()) +@settings(max_examples=250, deadline=None) +def test_risk_score_is_permutation_invariant( + findings: list[ReferenceFinding], has_executable_scripts: bool, rng: random.Random +) -> None: + metadata: list[dict[str, object]] = [ + { + "path": item.file, + "source_identity": item.source_identity, + "executable": item.file.endswith(".py"), + } + for item in findings + ] + shuffled = list(findings) + rng.shuffle(shuffled) + assert reference_risk_score(findings, has_executable_scripts, metadata) == reference_risk_score( + shuffled, has_executable_scripts, metadata + ) + assert _compute_risk_score( + [_production_finding(item) for item in findings], has_executable_scripts, metadata + ) == _compute_risk_score( + [_production_finding(item) for item in shuffled], has_executable_scripts, metadata + ) + + +@given(st.lists(reference_findings(), max_size=12), reference_findings(), st.booleans()) +@settings(max_examples=500, deadline=None) +def test_adding_arbitrary_evidence_cannot_lower_score( + findings: list[ReferenceFinding], + added: ReferenceFinding, + has_executable_scripts: bool, +) -> None: + combined = [*findings, added] + metadata: list[dict[str, object]] = [ + { + "path": item.file, + "source_identity": item.source_identity, + "executable": item.file.endswith(".py"), + } + for item in combined + ] + + reference_before = reference_risk_score(findings, has_executable_scripts, metadata)[0] + reference_after = reference_risk_score(combined, has_executable_scripts, metadata)[0] + production_before = _compute_risk_score( + [_production_finding(item) for item in findings], has_executable_scripts, metadata + )[0] + production_after = _compute_risk_score( + [_production_finding(item) for item in combined], has_executable_scripts, metadata + )[0] + + assert reference_after >= reference_before + assert production_after >= production_before + + +@st.composite +def llm_outcomes(draw: st.DrawFn) -> list[dict[str, object]]: + return draw( + st.lists( + st.fixed_dictionaries( + { + "pattern_id": st.sampled_from(("R1", "R2", "INVENTED")), + "is_vulnerability": st.booleans(), + "confidence": st.floats( + min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False + ), + "start_line": st.integers(min_value=1, max_value=5), + "_file": st.sampled_from(("run.py", "other.py")), + "explanation": st.text(max_size=30), + "remediation": st.text(max_size=30), + } + ), + max_size=12, + ) + ) + + +@given(llm_outcomes()) +@settings(max_examples=500, deadline=None) +def test_arbitrary_llm_outcomes_cannot_weaken_deterministic_findings( + outcomes: list[dict[str, object]], +) -> None: + originals = [ + Finding( + rule_id="R1", + message="first", + severity="HIGH", + confidence=0.82, + file="run.py", + start_line=1, + evidence={"origin": "deterministic"}, + ), + Finding( + rule_id="R2", + message="second", + severity="LOW", + confidence=0.41, + file="run.py", + start_line=2, + evidence={"origin": "deterministic"}, + ), + ] + batch = Batch(file_path="run.py", content="", findings=originals) + analyzer = LLMMetaAnalyzer.__new__(LLMMetaAnalyzer) + result = analyzer.apply_filter(originals, [(batch, outcomes)]) + by_id = {finding.finding_id: finding for finding in result} + + assert set(by_id) == {finding.finding_id for finding in originals} + for original in originals: + returned = by_id[original.finding_id] + assert returned.rule_id == original.rule_id + assert returned.severity == original.severity + assert returned.confidence >= original.confidence + assert returned.evidence == original.evidence + + +def test_reference_model_detects_the_historical_equal_severity_order_bug() -> None: + weak = ReferenceFinding(rule_id="R1", severity="CRITICAL", confidence=0.01) + strong = replace(weak, confidence=1.0) + assert reference_risk_score([weak, strong], False) == reference_risk_score( + [strong, weak], False + ) + assert _compute_risk_score([_production_finding(weak), _production_finding(strong)], False) == ( + 50, + "MEDIUM", + "CAUTION", + ) + assert _compute_risk_score([_production_finding(strong), _production_finding(weak)], False) == ( + 50, + "MEDIUM", + "CAUTION", + ) + + +def test_higher_severity_evidence_cannot_steal_weight_from_stronger_evidence() -> None: + strong_high = ReferenceFinding(rule_id="R1", severity="HIGH", confidence=0.1) + weak_critical = ReferenceFinding(rule_id="R1", severity="CRITICAL", confidence=0.01) + + before = reference_risk_score([strong_high], False) + after = reference_risk_score([strong_high, weak_critical], False) + + assert before == (2, "LOW", "SAFE") + assert after == (2, "LOW", "SAFE") + assert _compute_risk_score([_production_finding(strong_high)], False) == before + assert ( + _compute_risk_score( + [_production_finding(strong_high), _production_finding(weak_critical)], False + ) + == after + ) + + +def test_every_integer_score_has_exactly_one_documented_band() -> None: + bands = ((81, "CRITICAL"), (51, "HIGH"), (21, "MEDIUM"), (0, "LOW")) + for score in range(101): + actual = next(label for threshold, label in bands if score >= threshold) + expected = ( + "LOW" + if score <= 20 + else "MEDIUM" + if score <= 50 + else "HIGH" + if score <= 80 + else "CRITICAL" + ) + assert actual == expected diff --git a/uv.lock b/uv.lock index d79fdef4..ed90828a 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12, <3.15" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", @@ -863,6 +863,78 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, ] +[[package]] +name = "hypothesis" +version = "6.165.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/e2/0fad246d2b6330e1f78479bfc566b5c22be82aee8a865cde9a08f648487d/hypothesis-6.165.10.tar.gz", hash = "sha256:68b45e09834cd80523cb1eb274463073c7a9af4e4ef7cff34d9615f355572d32", size = 503703, upload-time = "2026-08-16T22:56:15.404Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/c1/9a9538e6d185baf5cc7f15bc3b76e08efbb3de4b3c782f234356449c0dd7/hypothesis-6.165.10-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f839d29d0cc12048cf073d88ca4fdf94d420bc2b8afd69641ff6d496422ccd4f", size = 783243, upload-time = "2026-08-16T22:55:44.058Z" }, + { url = "https://files.pythonhosted.org/packages/a1/30/b70d9d79e871a75cbdeccd9067f20ecdb9eb2a1dfa03c630be3ad13b8b30/hypothesis-6.165.10-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e10858f57ed0e74baa04393845f469fe8ad502c16ece4499bef7700c575611bd", size = 778815, upload-time = "2026-08-16T22:55:46.948Z" }, + { url = "https://files.pythonhosted.org/packages/db/52/6f0a9b7aab24b0635e2238f3fbddea5b54b17879ac813df42a3cc3384c5c/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76a7be86d986223b9f1bdb7e7cbcdb048649901fdb956c598ef73bdab1786cd5", size = 1108009, upload-time = "2026-08-16T22:54:53.082Z" }, + { url = "https://files.pythonhosted.org/packages/f6/06/8d0d4e11ff02350d09ec9f9e90af354158e59e16a8907ba5199a4ff2d7e8/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:717aea574e0e5edba2868aa66b1caae335d8f1ad3fb29f01dd6502953fa823a1", size = 1136596, upload-time = "2026-08-16T22:54:54.443Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/01a1e440f2e38dc1ccf5d597af5b8a0bee5f21b674c99c123b5554de9690/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4334058033e0214475f019e15492a50f3854fe8728cf51fe25c6191a2c3f8e52", size = 1135234, upload-time = "2026-08-16T22:55:08.911Z" }, + { url = "https://files.pythonhosted.org/packages/7d/18/8a26c24d3d9db20265f39df341ab265858c094e209571e3179cf237935f4/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2abb50cf1cf77d721de0a24c3f99d9c4ffdeb2cbd1e12aebb5a7a93e2b6b6d1f", size = 1157528, upload-time = "2026-08-16T22:56:02.159Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8e/ce3c829b1937402d7944420ca26a05a0c8563e894dcff03d34ffa279d306/hypothesis-6.165.10-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:3de69aa8b924b400291a3cc42aaf78e6ab65c905a3e7e1a5dc39d95ef1b428cb", size = 1112870, upload-time = "2026-08-16T22:54:55.919Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1b/4c4926d6c9a2b5d7cc090cc1e91219d6796102aa2a2c4b8f961c939e60b5/hypothesis-6.165.10-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5841331c504e02d7c334591681cb8587cdd59dee7e149db6d3db8e3f9e9f02eb", size = 1149683, upload-time = "2026-08-16T22:55:30.567Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f9/df24eb28412f82465e2b7707f0ff1ec274d580bce389d4d9156617dc7bba/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2d0e0f8263d34dd8fa3b39eaa9a50bba56a8470b3dd9ebf6672d10840abe063e", size = 1283402, upload-time = "2026-08-16T22:54:18.054Z" }, + { url = "https://files.pythonhosted.org/packages/4d/07/c2b2a761300cf60b90ccebba4328175331e67d34f4fbd39429a7ddcdce49/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0c4e6869817c3cfdf5a2b4d348497b95159bdecb3365be732c9b8570e36a4eef", size = 1409948, upload-time = "2026-08-16T22:54:22.343Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ec/1c2bf1acdd0e273d81f833f85caf0ae5423db68a783554992fca36e6c541/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:9f07ae36c3b093e13687a894e79fe69e98a94c0b67fef656c575247682218143", size = 1265023, upload-time = "2026-08-16T22:54:41.402Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a8/7f984908b7391160c7801b84e51ca8e4ba88c89e8d8811aa1aa7c03de73c/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:aff1f584c9538e8979cd180b1d70bf99bc16be19d4666414f49e5942b21a4f2c", size = 1282698, upload-time = "2026-08-16T22:56:06.998Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/3a5d91c2d0250521736c42dfa2402b75049bc5fe2fb716c10bc84bb91ed1/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1f2c4db25fb8ec1a16a8dba580666337b8ffb1887c4cf1750cc954313897cef7", size = 1324816, upload-time = "2026-08-16T22:54:46.675Z" }, + { url = "https://files.pythonhosted.org/packages/6f/99/27450763853a034bca1574d3e0a315164b33ff49c3862df6872dda45e25e/hypothesis-6.165.10-cp310-abi3-win32.whl", hash = "sha256:b33dc30170a7402e03c180f2c5ef69dc077152f35b91621e9cebcde9c7d71746", size = 669039, upload-time = "2026-08-16T22:55:11.962Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fc/ff2988b72b5705ad9ca500444bf3f43e3c2f41edfa034bbfeb23b215791a/hypothesis-6.165.10-cp310-abi3-win_amd64.whl", hash = "sha256:e9f924aa610c0618445e1e8738c822c3190ce2a2699a0cb48ec3a351a96761f2", size = 675213, upload-time = "2026-08-16T22:55:01.697Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8b/821810d36f78d9d9421cd2c5d9d36983b45bb3575c3086276cc5c76f9f73/hypothesis-6.165.10-cp310-abi3-win_arm64.whl", hash = "sha256:1d305448e9bd8e2f4f3cea0eafd809efdaab4e998a0019bc615650c8463e42f1", size = 673537, upload-time = "2026-08-16T22:54:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/e9/45/cde4f78afe2b9e29caecf38319eedc1deb76aebcacbdd128e03cbb2511c3/hypothesis-6.165.10-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:637445c1593a2a9d1024fda50082f07bb56baedda78d90a25f64b8111727ef94", size = 784835, upload-time = "2026-08-16T22:54:45.429Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/847f30b81cbfd07607296b3ce43067cf4f80799bd9244167f587de9c8081/hypothesis-6.165.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:713f4ce4e82c26b53031f139de959bc9e8b54d3995aa824b89bbdf8229df2a45", size = 776419, upload-time = "2026-08-16T22:55:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/04/66/4c71c5be7a49d84b8c3a9278c1807c4c81181ab5474beb27df9d4c40dc0e/hypothesis-6.165.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9ff356e97e3ab09db07c8b675efa67340103874a0bae7465acb83dad7a35f7f", size = 1106830, upload-time = "2026-08-16T22:55:10.389Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c4/e2cbd2810e79f7a452a8ea9f6c6438ee718ce938d8cc12252cf0b36a81d3/hypothesis-6.165.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a380bc99aa3b035e6a95a2201bf792d4082a04ca75babcc21849c2d0914bb28", size = 1156952, upload-time = "2026-08-16T22:55:53.35Z" }, + { url = "https://files.pythonhosted.org/packages/a8/8b/794ced36864825492ac3712d5acab5a257b4601e6a9dc2ccdd3937198f87/hypothesis-6.165.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9acb2c4d9cb532c3fedea74159f7b923c8c036328c9239b4049e7aa073bdd81", size = 1280780, upload-time = "2026-08-16T22:54:34.983Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2d/550525442cdbcc2daf1f9bdd8ba35bcbde63db7c7a22f2ef137fbb49df2f/hypothesis-6.165.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8660572b2d424bf5369ea8990985225f70bd1615b76ecd9c25588a3b9307009f", size = 1324130, upload-time = "2026-08-16T22:55:48.659Z" }, + { url = "https://files.pythonhosted.org/packages/74/59/6caf69dd5fe03499ada94c9cec016bffcc164511c6b93fe680f01209b9ff/hypothesis-6.165.10-cp312-cp312-win_amd64.whl", hash = "sha256:3376f2594763aef14faa519b0fb27cae7ce9eeaab4c69efa07777499110306c9", size = 672337, upload-time = "2026-08-16T22:54:49.11Z" }, + { url = "https://files.pythonhosted.org/packages/b1/fb/c82c5bd92864ffcf319772fedc8c9bf2dbe4ca14baa0fee6e49e67b5ba1c/hypothesis-6.165.10-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9d77c3be7b429875036ad0f0597c6e5cc6bb17894a4da005e3807de64d2673ad", size = 784726, upload-time = "2026-08-16T22:54:32.371Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b9/3d7acd08506da85557e65147b7f3fca8c47684e33be90bee0acb523920db/hypothesis-6.165.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:490c56b830772b0eca3b4b2cecb3741a1ed26b1d7206a279e1525dbf0aa95ee4", size = 776375, upload-time = "2026-08-16T22:55:13.303Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/922e8b3f9a706dd89d440b9545d2c6231c65e74da1c1fee3ff36c251b9c4/hypothesis-6.165.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed68e27b8a61e57a3ccdc7c5a14499e00b54dfe223087204d5d40b3b5ef58b6d", size = 1106763, upload-time = "2026-08-16T22:55:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/01/39/f5b9a5d390d4edd1ad472334493ac442963ebeb4daaa74ff4bdac6ef292f/hypothesis-6.165.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6caadcd1afb62630ff5c5ff353626eaa616553a5971295ad6dc2b19ca8a39620", size = 1156778, upload-time = "2026-08-16T22:54:33.824Z" }, + { url = "https://files.pythonhosted.org/packages/b5/5f/5fbe1be4326337fd6acefe2d18ed44007ee1dc1f98fe5b3c0eb22942364d/hypothesis-6.165.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9145fe43ebb22e66672967c3fab411793b226ed776e4fe282271bca6ad3c0bb", size = 1280756, upload-time = "2026-08-16T22:55:54.834Z" }, + { url = "https://files.pythonhosted.org/packages/25/c0/cf6f9e1ef632a1a75694eed0db3a02e6fc75c367a363e94acee52f043c64/hypothesis-6.165.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79900a9920a0b1d3a626c03a90ac6bf7042e78d46906a565b86a0dbe926f1d96", size = 1323889, upload-time = "2026-08-16T22:55:56.567Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/662b94880f260b0a88de1fdcf60fc9984f6e2a796da549542adc10a7bc83/hypothesis-6.165.10-cp313-cp313-win_amd64.whl", hash = "sha256:c01dd04044c472e47193b54f68e84e08d6ebf4f29551885aa959b015f7cd9747", size = 672346, upload-time = "2026-08-16T22:56:03.792Z" }, + { url = "https://files.pythonhosted.org/packages/3f/77/55e020c9c576532ff7d20bf8b1dfa052ecbd5ada1949b02f76c44c966f7e/hypothesis-6.165.10-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9ccac776b2ca93b324806facd526ccb45da0fd035001c899a35b02c44431e209", size = 784833, upload-time = "2026-08-16T22:55:21.255Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f2/01da2adf829cf549eaddcabb8e8072077fb3d26da4275f4c1e89b2c0af74/hypothesis-6.165.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e5f95f7b622e4171096d92175dda0a560f0955ade9b8a3a07bdcf151f7359611", size = 776545, upload-time = "2026-08-16T22:56:10.159Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/58d4f842895220b793c53fc94a6489705b3665bb4d0ae4d338ce03fdf9fb/hypothesis-6.165.10-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f76d1562643693b8a40066f1f96af795b93fd9bcfc9690a1af2ff4c5867ee29e", size = 1107271, upload-time = "2026-08-16T22:54:50.266Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b8/206468912d2153306bb8a41afdfc59e45b7a73a0495bbe4b9cb4f0e79c1d/hypothesis-6.165.10-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60cab3ab4ea468d31a33739ffd7e94ec3e37dea891d65a6582ecc8a477175191", size = 1156915, upload-time = "2026-08-16T22:54:25.89Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d3/bf5a22929b70a4cfd3edf69c5642b029b27ddb5cfda48fa295d384b01abb/hypothesis-6.165.10-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:22cf19388f0ff6ced8eb3e49c903d14938e4ed909d93bf28383eef451511e424", size = 1281205, upload-time = "2026-08-16T22:54:44.083Z" }, + { url = "https://files.pythonhosted.org/packages/07/a2/d7b2ba444d36fc84d4779f4431e74dd9b023dc63bcf282199f6e48ad39f4/hypothesis-6.165.10-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:057d0232f1224dcd0b7698902551a4341a7399f90670b036db6c4376715fe889", size = 1324243, upload-time = "2026-08-16T22:55:41.123Z" }, + { url = "https://files.pythonhosted.org/packages/d1/95/afe6b531fd01928c6f63d394ee413fa2338d088b2b44efcc23596b54477e/hypothesis-6.165.10-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ab0f2e9d7d7d4db257f7cf53de3706c2baf124269571f20ffc2bcd6781f03063", size = 616382, upload-time = "2026-08-16T22:55:18.449Z" }, + { url = "https://files.pythonhosted.org/packages/48/86/9b4fb75f520a028edec50ffc904a94d724180395d71feb6d7a0ce7bb6f00/hypothesis-6.165.10-cp314-cp314-win_amd64.whl", hash = "sha256:d1ea02fa8ab3d33eb1125eade81f7136341eb429152c6dbe2ae6f8bc33b3fbdd", size = 672145, upload-time = "2026-08-16T22:54:24.831Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ba/f7bbaae0c789bab7ddb764d2056ee1a463cc95a8acbccc90d4184e48b242/hypothesis-6.165.10-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ed1a5891e59472884a03cb9875483e8fc131c80a275c60967f8afc5458a0c8ff", size = 783287, upload-time = "2026-08-16T22:54:23.751Z" }, + { url = "https://files.pythonhosted.org/packages/3a/83/01ef80772b4abd335c49405576dc503cede94fb5da30ba2643a119013aea/hypothesis-6.165.10-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:09772e328a26e50486ac572be34f9887f9aa185efe7ebb16bde4e8f6038db1f4", size = 774991, upload-time = "2026-08-16T22:55:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0b/f47506241f9d5a5a2efe4c65b6bf4830e9d9576e5d3779007a260699e608/hypothesis-6.165.10-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cf3b612542ba174c9da4000b59a4f4c81e8d66f87509be85d3a1b71b5c36413", size = 1105499, upload-time = "2026-08-16T22:54:51.864Z" }, + { url = "https://files.pythonhosted.org/packages/84/fe/abb3909b7089835112fbe75bf00d817d733b3a8032759783db0a24ff1e56/hypothesis-6.165.10-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69ec5be85ef508e206153bed8eafd03f7995dc464356c8bbb279a1e2b7d56f3", size = 1155685, upload-time = "2026-08-16T22:54:30.94Z" }, + { url = "https://files.pythonhosted.org/packages/73/2f/1964738921640184067121ae77414522fc3f0463fc26c6e25a4f3b8e42ca/hypothesis-6.165.10-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dd207497bb985918409a1bb5db85d1875f74e1269487332113b73d1ee7c77647", size = 1279177, upload-time = "2026-08-16T22:54:40.179Z" }, + { url = "https://files.pythonhosted.org/packages/34/c5/312af8ae038d3af9cf3f7f1021c1abfe31c0d9035e4cf63519e0a7dc983e/hypothesis-6.165.10-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:00de0abdcf8c05c9d0eab735a3c49a276376b55151e6fcb903c2b39a90e5e5c3", size = 1322921, upload-time = "2026-08-16T22:54:42.7Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e7/b0a2fde7570c090a1b914026266a421c751ef10138fffe37fe0ef9e675c0/hypothesis-6.165.10-cp314-cp314t-win_amd64.whl", hash = "sha256:cc2da5aa4edf14743fa9257e5ba3513963999f01211635702479d8e92b8207c8", size = 672147, upload-time = "2026-08-16T22:55:27.527Z" }, + { url = "https://files.pythonhosted.org/packages/47/fd/985aa564d6ffd06483d45a62b40d319df0a703cd8bc1d041de17d102fbaa/hypothesis-6.165.10-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:eeab73050ea58c13dd56e329f594c1dfe32ebd7bb169bbdf4f8ceefbc31ec6b5", size = 782882, upload-time = "2026-08-16T22:55:37.93Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2c/6cc11151e450f72353a490940cd0db704680d07b78dc75dcc9f480e0d0e1/hypothesis-6.165.10-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:4c68e983d0007d014bb01ad4bcbba78bc432c73a1755ff36d5102ceefa18299a", size = 774584, upload-time = "2026-08-16T22:55:51.822Z" }, + { url = "https://files.pythonhosted.org/packages/10/39/ef26fa79c1738dfe9cdb1a3584fb6717d26429ca6c9d011cc4fdf08130c2/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7730d8197086f65d8969a991d6728a1d420a51b19fea06535c896cb43a1e05d0", size = 1104876, upload-time = "2026-08-16T22:54:58.937Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f4/3fcc84e7637f42bf00d987093b9418083ac8db81b87392608a60f4b7c5fd/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7a7980a898a3e6ebe4de1896a0507e3d519edb53fb9b4bda478c9fbeb6514558", size = 1133353, upload-time = "2026-08-16T22:54:28.635Z" }, + { url = "https://files.pythonhosted.org/packages/35/59/21c5c14179c38f8d0de3560e7f1825c083311b3013b63f817d7dc78dfcbd/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5820d009aedb7ae9cfd32f98b1ab0c0bbd6268379c4fab042218b6b655c63f8", size = 1132300, upload-time = "2026-08-16T22:56:08.539Z" }, + { url = "https://files.pythonhosted.org/packages/14/af/fbb56059961e416b2de7b9dc5352db2e8572bd5ea46892957e4c1e5548ab/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37a7ac3d34220800e1107871cc391bca1b00439875925d7d821878b8b791f245", size = 1155175, upload-time = "2026-08-16T22:55:19.824Z" }, + { url = "https://files.pythonhosted.org/packages/0f/53/77fb0c2dad445858555429c4e06cf94a59ae8d2407dd6426b5af97c84828/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:dafa7c9dbe3d802f9bcdf261b29c8a70700fb22839947f06e471f62c46b6257f", size = 1109881, upload-time = "2026-08-16T22:55:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/a8/7b/d187f673ff30e6ada640953636f978ffe64a6332f756b64163c2277f8d0c/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:90915635b9648071129b0f72c0673cf8eac9eb84cfd445c5bedef30c714b1ec2", size = 1144963, upload-time = "2026-08-16T22:56:13.428Z" }, + { url = "https://files.pythonhosted.org/packages/e0/60/31d504e364134d60af23e5f6365db0da3cf4a51b3ed3d4836e5a2cff12cf/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:e1bbeb7c506b07ee0422cf9b2f7212fefa4240957f03526d38d27bc6743a0a48", size = 1278684, upload-time = "2026-08-16T22:55:22.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e6/89d26834a08c02f8da149e541dd40d7a96f68d9722f43146e69a77436ed7/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:2b36aaffc88625a44f91074c5bbedfdefb9b376c38d1b3c342edcd2e4c8ed16c", size = 1407202, upload-time = "2026-08-16T22:55:14.949Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/20d1e72246867ea195440092e8bb422c7ddc2f271b87b5b65679d5532719/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:18a3ea838ddea183388f8788750afa8494d79abb5358823be9782585f34445d3", size = 1261395, upload-time = "2026-08-16T22:56:05.448Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9b/ebab6c3c2b90a16abb4119198178652d12aff83cc8ec2cfde5276c69fb1e/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2a2567b3a03a4a5a7c575c191cfcce321a967df3727803817e75bffbbeaecabe", size = 1279213, upload-time = "2026-08-16T22:55:35.066Z" }, + { url = "https://files.pythonhosted.org/packages/23/78/69b219b524231d36eb20c792e1f01e7cb037e02bd0af1c29f77ed9a969c0/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:8001925fa3dde51cb574e4c9de4c7efe77c4e4d64bd2fd2ef61d5651f9d04f3d", size = 1322367, upload-time = "2026-08-16T22:54:21.279Z" }, + { url = "https://files.pythonhosted.org/packages/55/63/ad5cc153dcc72ae5e7905fb9b3585f3e48ce892a2d6366f90163e867a69d/hypothesis-6.165.10-cp315-abi3.abi3t-win32.whl", hash = "sha256:c6559380469295c4009215fe1cab561301591a3bee2e2fb3f4f96d2273a3affc", size = 666038, upload-time = "2026-08-16T22:56:11.797Z" }, + { url = "https://files.pythonhosted.org/packages/80/32/b62307b73fbc99f0a4381d6f9456df76fbcbb7a27ef7256e26f0376f48ea/hypothesis-6.165.10-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:30797f20ca45e57f526d2df872f63ba453cb4e1091ad542184a7a951af8da79d", size = 671941, upload-time = "2026-08-16T22:55:00.235Z" }, + { url = "https://files.pythonhosted.org/packages/c2/dd/e0f98add0548ef73ea7afac45da1fb8efc854d7f9931db568754d0f963f3/hypothesis-6.165.10-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:c53e9b1c36350df9965ec44d6c0d4e0bbbb38f720dd2b0e1256dc6524d411015", size = 669931, upload-time = "2026-08-16T22:55:50.205Z" }, +] + [[package]] name = "id" version = "1.6.1" @@ -2713,6 +2785,8 @@ dependencies = [ dev = [ { name = "build" }, { name = "hatchling" }, + { name = "hypothesis" }, + { name = "langgraph-cli", extra = ["inmem"] }, { name = "mcp" }, { name = "mypy" }, { name = "poetry" }, @@ -2722,12 +2796,12 @@ dev = [ { name = "ruff" }, { name = "twine" }, ] -mcp = [ - { name = "mcp" }, -] langgraph-dev = [ { name = "langgraph-cli", extra = ["inmem"] }, ] +mcp = [ + { name = "mcp" }, +] [package.metadata] requires-dist = [ @@ -2735,6 +2809,7 @@ requires-dist = [ { name = "build", marker = "extra == 'dev'", specifier = ">=1.4.0" }, { name = "hatchling", marker = "extra == 'dev'", specifier = ">=1.31.0" }, { name = "httpx", specifier = ">=0.28.0" }, + { name = "hypothesis", marker = "extra == 'dev'", specifier = ">=6.151.9" }, { name = "langchain-anthropic", specifier = ">=1.4.5" }, { name = "langchain-aws", specifier = ">=0.2.0" }, { name = "langchain-core", specifier = ">=1.2.17" }, @@ -2756,8 +2831,8 @@ requires-dist = [ { name = "regex", specifier = "==2026.5.9" }, { name = "rich", specifier = ">=14.3.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.0" }, - { name = "skillspector", extras = ["mcp"], marker = "extra == 'dev'" }, { name = "skillspector", extras = ["langgraph-dev"], marker = "extra == 'dev'" }, + { name = "skillspector", extras = ["mcp"], marker = "extra == 'dev'" }, { name = "twine", marker = "extra == 'dev'", specifier = ">=6.2.0" }, { name = "typer", specifier = ">=0.23.0,<0.24" }, { name = "yara-python", specifier = ">=4.5.0" }, @@ -2773,6 +2848,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + [[package]] name = "sse-starlette" version = "3.3.4"