repo contract + coverage_gaps gates, test_changed, dependency bumps

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-10-04 16:27:28 +02:00
parent ba6f8b09fd
commit 6c71d531bb
63 changed files with 3900 additions and 786 deletions

19
.github/workflows/repo-contract.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: repo-contract
# utils owns the shared gate, so it runs its own copy directly (no shim, no
# second checkout). Other repos use repo_contract/templates/repo-contract.yml.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
jobs:
repo-contract:
name: repo contract
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Enforce the repo contract
run: bash scripts/check_repo_contract.sh

View File

@ -48,3 +48,17 @@ jobs:
--cov=file_length --cov-branch \ --cov=file_length --cov-branch \
--cov-report=term-missing \ --cov-report=term-missing \
--cov-fail-under=100 --cov-fail-under=100
- name: repo_contract — 100% branch coverage on the gate itself
run: |
PYTHONPATH=. python -m pytest repo_contract/tests -q \
--cov=repo_contract --cov-branch \
--cov-report=term-missing \
--cov-fail-under=100
- name: coverage_gaps — 100% branch coverage on the lister
run: |
PYTHONPATH=. python -m pytest coverage_gaps/tests -q \
--cov=coverage_gaps --cov-branch \
--cov-report=term-missing \
--cov-fail-under=100

View File

@ -43,6 +43,13 @@ repos:
language: system language: system
pass_filenames: true pass_filenames: true
- id: repo-contract
name: repo contract (CLAUDE.md Commands + test_changed)
entry: scripts/check_repo_contract.sh
language: system
pass_filenames: false
files: ^(CLAUDE\.md|AGENTS\.md|scripts/test_changed\.sh)$
- id: md-naming - id: md-naming
name: markdown naming convention name: markdown naming convention
entry: scripts/check_md_naming.sh entry: scripts/check_md_naming.sh

17
CLAUDE.md Normal file
View File

@ -0,0 +1,17 @@
# utils
Monorepo of shared gates and libraries (dep_freshness, file_length, md_naming,
repo_contract, coverage_gaps, crdt-sync, gatelock, freedays, music_theory, ...).
Python packages with a root-level `<pkg>/tests` run with `PYTHONPATH=.`;
subprojects with their own `pyproject.toml` run via `scripts/run_subproject_tests.sh <dir>`.
`scripts/check_repo_contract.sh` is the REAL shared gate (not a shim): never
overwrite it with `repo_contract/templates/check_repo_contract.shim.sh`.
## Commands
- run: n/a: collection of gates and libraries, no single entry point
- test: `PYTHONPATH=. python -m pytest -q coverage_gaps/tests dep_freshness/tests file_length/tests home_paths/tests md_naming/tests repo_contract/tests`
- test-changed: `scripts/test_changed.sh`
- lint: `ruff check coverage_gaps repo_contract`
- coverage: `PYTHONPATH=. python -m pytest -q repo_contract/tests --cov=repo_contract --cov-branch --cov-report=xml --cov-fail-under=100`
- coverage-gaps: `coverage-gaps coverage.xml`

View File

@ -0,0 +1,7 @@
"""Deterministic coverage-gap lister: a coverage report in, a task list out.
Replaces "spawn an LLM agent to explore where coverage is missing". Reads lcov,
Cobertura XML, JaCoCo XML or coverage.py JSON and lists, per file, exactly
which lines and branches are uncovered. Exit codes are the verdict (0 covered,
1 gaps, 2 unreadable) so CI and hooks adjudicate without a model.
"""

View File

@ -0,0 +1,5 @@
"""`python3 -m coverage_gaps` -- the entrypoint the `coverage-gaps` wrapper calls."""
from coverage_gaps.cli import main
raise SystemExit(main())

16
coverage_gaps/bin/coverage-gaps Executable file
View File

@ -0,0 +1,16 @@
#!/bin/bash
# ============================================================================
# coverage-gaps -- per-file task list of uncovered lines/branches.
# Thin wrapper over `python3 -m coverage_gaps` (see coverage_gaps/cli.py).
# Usage: coverage-gaps <report> [--json] [--max-files N] [--min-missed N]
# [--path-prefix P] Exit: 0 covered | 1 gaps | 2 bad input
# ============================================================================
set -euo pipefail
BIN_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd -P)"
readonly BIN_DIR
PYTHONPATH="$BIN_DIR/../.." python3 -m coverage_gaps "$@"
exit $?

63
coverage_gaps/cli.py Normal file
View File

@ -0,0 +1,63 @@
"""Command line: `coverage-gaps REPORT [--json] [--max-files N] ...`."""
from __future__ import annotations
import argparse
import sys
from collections.abc import Sequence
from pathlib import Path
from coverage_gaps.models import ReportError
from coverage_gaps.parsers import load_report
from coverage_gaps.render import select, to_json, to_markdown
EXIT_COVERED = 0
EXIT_GAPS = 1
EXIT_UNREADABLE = 2
def build_parser() -> argparse.ArgumentParser:
"""Argument parser (format is auto-detected from file content)."""
parser = argparse.ArgumentParser(
prog="coverage-gaps",
description="List uncovered lines/branches from lcov, Cobertura, JaCoCo "
"or coverage.py JSON reports. Exit 0 covered, 1 gaps, 2 unreadable.",
)
parser.add_argument("report", type=Path, help="coverage report file")
parser.add_argument("--json", action="store_true", help="emit JSON, not markdown")
parser.add_argument(
"--max-files",
type=int,
default=0,
metavar="N",
help="list only the N worst files (0 = all)",
)
parser.add_argument(
"--min-missed",
type=int,
default=1,
metavar="N",
help="skip files with fewer than N missed lines+branches",
)
parser.add_argument(
"--path-prefix",
default="",
metavar="PREFIX",
help="only files whose path starts with PREFIX",
)
return parser
def main(argv: Sequence[str] | None = None) -> int:
"""Run the tool and return the process exit code."""
args = build_parser().parse_args(argv)
try:
fmt, gaps = load_report(args.report)
except ReportError as exc:
print(f"coverage-gaps: {exc}", file=sys.stderr)
return EXIT_UNREADABLE
files = select(gaps, path_prefix=args.path_prefix, min_missed=args.min_missed)
shown = args.max_files if args.max_files > 0 else len(files)
render = to_json if args.json else to_markdown
sys.stdout.write(render(fmt, files, shown) + ("\n" if args.json else ""))
return EXIT_GAPS if files else EXIT_COVERED

69
coverage_gaps/models.py Normal file
View File

@ -0,0 +1,69 @@
"""Format-neutral data model shared by every parser and renderer."""
from __future__ import annotations
from dataclasses import dataclass, field
class ReportError(Exception):
"""The input is unreadable, unrecognised or malformed (exit code 2)."""
@dataclass
class FileAccumulator:
"""Mutable per-file collector that merges repeated records for one path.
Reports may list a file more than once (lcov re-runs, Cobertura inner
classes). A line or branch counts as covered if ANY record covered it.
"""
lines: dict[int, bool] = field(default_factory=dict)
branches: dict[tuple[int, int, int], bool] = field(default_factory=dict)
def add_line(self, line: int, covered: bool) -> None:
"""Record a line, OR-merging with earlier records of the same line."""
self.lines[line] = self.lines.get(line, False) or covered
def add_branch(self, line: int, block: int, index: int, covered: bool) -> None:
"""Record one branch outcome, OR-merging on (line, block, index)."""
key = (line, block, index)
self.branches[key] = self.branches.get(key, False) or covered
@dataclass(frozen=True)
class FileGap:
"""Coverage state of one source file."""
path: str
total_lines: int
missed_lines: tuple[int, ...]
total_branches: int
missed_branch_lines: tuple[int, ...]
missed_branches: int
@property
def missed(self) -> int:
"""Missed lines plus missed branches: the sort key."""
return len(self.missed_lines) + self.missed_branches
@property
def percent(self) -> float:
"""Percent covered over lines and branches combined (100 if empty)."""
total = self.total_lines + self.total_branches
if total == 0:
return 100.0
return 100.0 * (total - self.missed) / total
def build_gap(path: str, acc: FileAccumulator) -> FileGap:
"""Freeze an accumulator into a FileGap."""
missed_lines = tuple(sorted(n for n, ok in acc.lines.items() if not ok))
missed_br = [key for key, ok in acc.branches.items() if not ok]
return FileGap(
path=path,
total_lines=len(acc.lines),
missed_lines=missed_lines,
total_branches=len(acc.branches),
missed_branch_lines=tuple(sorted({key[0] for key in missed_br})),
missed_branches=len(missed_br),
)

132
coverage_gaps/parsers.py Normal file
View File

@ -0,0 +1,132 @@
"""Parsers for lcov, Cobertura XML, JaCoCo XML and coverage.py JSON."""
from __future__ import annotations
import json
import re
import xml.etree.ElementTree as ET
from collections.abc import Callable
from pathlib import Path
from coverage_gaps.models import FileAccumulator, FileGap, ReportError, build_gap
_CONDITION = re.compile(r"\((\d+)/(\d+)\)")
def _finish(files: dict[str, FileAccumulator]) -> list[FileGap]:
return [build_gap(path, acc) for path, acc in files.items()]
def parse_lcov(text: str) -> list[FileGap]:
"""Parse lcov.info: SF / DA / BRDA records, `end_of_record` optional."""
files: dict[str, FileAccumulator] = {}
acc: FileAccumulator | None = None
for raw in text.splitlines():
line = raw.strip()
if line.startswith("SF:"):
acc = files.setdefault(line[3:], FileAccumulator())
elif acc is not None and line.startswith("DA:"):
num, hits, *_ = line[3:].split(",")
acc.add_line(int(num), int(hits) > 0)
elif acc is not None and line.startswith("BRDA:"):
num, block, branch, taken = line[5:].split(",")
covered = taken not in ("-", "0")
acc.add_branch(int(num), int(block), _branch_id(branch), covered)
elif line == "end_of_record":
acc = None
return _finish(files)
def _branch_id(branch: str) -> int:
return int(branch) if branch.isdigit() else 0
def parse_cobertura(text: str) -> list[FileGap]:
"""Parse Cobertura `coverage.xml` (coverage.py, gcovr, ...)."""
root = ET.fromstring(text)
files: dict[str, FileAccumulator] = {}
for cls in root.iter("class"):
acc = files.setdefault(cls.get("filename", ""), FileAccumulator())
for node in cls.iter("line"):
num = int(node.get("number", "0"))
acc.add_line(num, int(node.get("hits", "0")) > 0)
match = _CONDITION.search(node.get("condition-coverage", ""))
if match:
hit, total = int(match[1]), int(match[2])
for idx in range(total):
acc.add_branch(num, 0, idx, idx < hit)
return _finish(files)
def parse_jacoco(text: str) -> list[FileGap]:
"""Parse JaCoCo XML: per `sourcefile` line records (mi/ci/mb/cb)."""
root = ET.fromstring(text)
files: dict[str, FileAccumulator] = {}
for package in root.iter("package"):
base = package.get("name", "")
for source in package.iter("sourcefile"):
name = source.get("name", "")
acc = files.setdefault(
f"{base}/{name}" if base else name, FileAccumulator()
)
for node in source.iter("line"):
num = int(node.get("nr", "0"))
acc.add_line(num, int(node.get("ci", "0")) > 0)
missed_b, covered_b = int(node.get("mb", "0")), int(node.get("cb", "0"))
for idx in range(missed_b + covered_b):
acc.add_branch(num, 0, idx, idx >= missed_b)
return _finish(files)
def parse_coverage_json(text: str) -> list[FileGap]:
"""Parse `coverage json` output (with or without branch data)."""
files: dict[str, FileAccumulator] = {}
for path, data in json.loads(text).get("files", {}).items():
acc = files.setdefault(path, FileAccumulator())
for num in data.get("executed_lines", []):
acc.add_line(num, True)
for num in data.get("missing_lines", []):
acc.add_line(num, False)
missing = data.get("missing_branches", [])
for idx, (src, _dst) in enumerate(missing):
acc.add_branch(abs(src), 1, idx, False)
covered = data.get("summary", {}).get("covered_branches", 0)
for idx in range(covered):
acc.add_branch(0, 2, idx, True)
return _finish(files)
def detect_format(text: str) -> str:
"""Return 'lcov' | 'cobertura' | 'jacoco' | 'json' from file content."""
head = text.lstrip()[:4000]
if head.startswith("{"):
return "json"
if head.startswith("<"):
if "<report" in head:
return "jacoco"
if "<coverage" in head:
return "cobertura"
elif re.search(r"^(TN|SF):", head, re.MULTILINE):
return "lcov"
raise ReportError("unrecognised coverage report format")
_PARSERS: dict[str, Callable[[str], list[FileGap]]] = {
"lcov": parse_lcov,
"cobertura": parse_cobertura,
"jacoco": parse_jacoco,
"json": parse_coverage_json,
}
def load_report(path: Path) -> tuple[str, list[FileGap]]:
"""Read and parse a report; raise ReportError on any failure."""
try:
text = path.read_text(encoding="utf-8", errors="replace")
except OSError as exc:
raise ReportError(f"cannot read {path}: {exc.strerror}") from exc
fmt = detect_format(text)
try:
return fmt, _PARSERS[fmt](text)
except (ET.ParseError, ValueError, TypeError, AttributeError) as exc:
raise ReportError(f"malformed {fmt} report {path}: {exc}") from exc

102
coverage_gaps/render.py Normal file
View File

@ -0,0 +1,102 @@
"""Filtering and rendering: markdown (agent task list) and JSON."""
from __future__ import annotations
import json
from collections.abc import Iterable, Sequence
from coverage_gaps.models import FileGap
def compact_ranges(lines: Iterable[int]) -> str:
"""Collapse line numbers into '12-18, 40'."""
nums = sorted(set(lines))
parts: list[str] = []
idx = 0
while idx < len(nums):
end = idx
while end + 1 < len(nums) and nums[end + 1] == nums[end] + 1:
end += 1
parts.append(_span(nums[idx], nums[end]))
idx = end + 1
return ", ".join(parts)
def _span(start: int, end: int) -> str:
return str(start) if start == end else f"{start}-{end}"
def select(
gaps: Sequence[FileGap], *, path_prefix: str = "", min_missed: int = 1
) -> list[FileGap]:
"""Keep files with gaps under the prefix, worst first (ties by path)."""
kept = [
gap
for gap in gaps
if gap.path.startswith(path_prefix) and gap.missed >= max(min_missed, 1)
]
return sorted(kept, key=lambda gap: (-gap.missed, gap.path))
def _totals(files: Sequence[FileGap]) -> dict[str, int]:
return {
"files": len(files),
"missed_lines": sum(len(gap.missed_lines) for gap in files),
"missed_branches": sum(gap.missed_branches for gap in files),
}
def to_json(fmt: str, files: Sequence[FileGap], shown: int) -> str:
"""Machine-readable output; `files` is already sorted and filtered."""
entries = [
{
"path": gap.path,
"percent_covered": round(gap.percent, 2),
"missed": gap.missed,
"missed_lines": compact_ranges(gap.missed_lines),
"missed_line_numbers": list(gap.missed_lines),
"missed_branch_count": gap.missed_branches,
"missed_branch_lines": list(gap.missed_branch_lines),
}
for gap in files[:shown]
]
doc = {
"format": fmt,
"total": _totals(files),
"shown": len(entries),
"files": entries,
}
return json.dumps(doc, indent=2)
def to_markdown(fmt: str, files: Sequence[FileGap], shown: int) -> str:
"""Markdown task list meant to be pasted to an agent verbatim."""
total = _totals(files)
out = [
"# Coverage gaps",
"",
(
f"Format: {fmt}. Total: {total['files']} files with gaps, "
f"{total['missed_lines']} missed lines, "
f"{total['missed_branches']} missed branches."
),
]
if shown < len(files):
out.append(f"Showing the {shown} worst files.")
if not files:
return "\n".join(out) + "\n\nNo coverage gaps.\n"
out += ["", "Write tests that cover every item below, then re-run the report.", ""]
for rank, gap in enumerate(files[:shown], start=1):
out.append(f"## {rank}. `{gap.path}` - {gap.percent:.1f}% covered")
if gap.missed_lines:
out.append(
f"- missed lines ({len(gap.missed_lines)}): "
f"{compact_ranges(gap.missed_lines)}"
)
if gap.missed_branches:
out.append(
f"- missed branches ({gap.missed_branches}) at lines: "
f"{compact_ranges(gap.missed_branch_lines)}"
)
out.append("")
return "\n".join(out).rstrip() + "\n"

View File

View File

@ -0,0 +1,89 @@
"""Small fixture reports, one per supported format, all describing a.py/b.py.
a.py: lines 1-5 with 2,3 and 5 missed + one missed branch on line 4.
b.py: fully covered (must never be listed).
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
LCOV = """TN:
SF:a.py
DA:1,1
DA:2,0
DA:3,0
DA:4,1
DA:5,0
BRDA:4,0,0,1
BRDA:4,0,1,-
end_of_record
SF:b.py
DA:1,3
end_of_record
SF:a.py
DA:1,1
DA:2,0
end_of_record
"""
COBERTURA = """<?xml version="1.0" ?>
<coverage version="7"><packages><package name="p"><classes>
<class filename="a.py"><lines>
<line number="1" hits="1"/><line number="2" hits="0"/><line number="3" hits="0"/>
<line number="4" hits="1" branch="true" condition-coverage="50% (1/2)"/>
<line number="5" hits="0"/></lines></class>
<class filename="b.py"><lines><line number="1" hits="2"/></lines></class>
<class filename="a.py"><lines><line number="1" hits="1"/></lines></class>
</classes></package></packages></coverage>
"""
JACOCO = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE report PUBLIC "-//JACOCO//DTD Report 1.1//EN" "report.dtd">
<report name="x"><package name="com/x">
<sourcefile name="A.kt">
<line nr="1" mi="0" ci="3"/><line nr="2" mi="2" ci="0"/><line nr="3" mi="1" ci="0"/>
<line nr="4" mi="0" ci="4" mb="1" cb="1"/><line nr="5" mi="1" ci="0"/></sourcefile>
<sourcefile name="B.kt"><line nr="1" mi="0" ci="1"/></sourcefile>
</package><package name=""><sourcefile name="Top.kt">
<line nr="1" mi="0" ci="1"/></sourcefile></package></report>
"""
COVERAGE_JSON = json.dumps(
{
"files": {
"a.py": {
"executed_lines": [1, 4],
"missing_lines": [2, 3, 5],
"missing_branches": [[4, 5]],
"summary": {"covered_branches": 1},
},
"b.py": {
"executed_lines": [1],
"missing_lines": [],
"missing_branches": [],
},
}
}
)
@pytest.fixture
def reports(tmp_path: Path) -> dict[str, Path]:
"""Write each fixture report to disk, keyed by format name."""
out = {}
for name, body in {
"lcov": LCOV,
"cobertura": COBERTURA,
"jacoco": JACOCO,
"json": COVERAGE_JSON,
}.items():
out[name] = tmp_path / f"{name}.rpt"
out[name].write_text(body, encoding="utf-8")
return out

View File

@ -0,0 +1,73 @@
"""Parser and format-detection tests."""
from __future__ import annotations
from pathlib import Path
import pytest
from coverage_gaps.models import FileGap, ReportError
from coverage_gaps.parsers import detect_format, load_report
def _by_path(gaps: list[FileGap]) -> dict[str, FileGap]:
return {gap.path: gap for gap in gaps}
@pytest.mark.parametrize("fmt", ["lcov", "cobertura", "json"])
def test_a_py_gaps_match_across_formats(reports: dict[str, Path], fmt: str) -> None:
detected, gaps = load_report(reports[fmt])
assert detected == fmt
a = _by_path(gaps)["a.py"]
assert a.missed_lines == (2, 3, 5)
assert a.missed_branch_lines == (4,)
assert a.missed_branches == 1
assert a.total_lines == 5
assert a.total_branches == 2
assert _by_path(gaps)["b.py"].missed == 0
def test_jacoco(reports: dict[str, Path]) -> None:
fmt, gaps = load_report(reports["jacoco"])
assert fmt == "jacoco"
by = _by_path(gaps)
assert by["com/x/A.kt"].missed_lines == (2, 3, 5)
assert by["com/x/A.kt"].missed_branch_lines == (4,)
assert by["com/x/A.kt"].total_branches == 2
assert "Top.kt" in by
def test_percent_and_empty_file() -> None:
gap = FileGap("x", 4, (1,), 4, (2,), 1)
assert gap.percent == 75.0
assert FileGap("y", 0, (), 0, (), 0).percent == 100.0
def test_lcov_ignores_records_outside_a_file_and_dash_branch_ids() -> None:
from coverage_gaps.parsers import parse_lcov
gaps = parse_lcov(
"DA:1,0\nBRDA:1,0,0,0\nSF:z\nBRDA:2,0,-,0\nend_of_record\nDA:9,0\n"
)
assert len(gaps) == 1
assert gaps[0].missed_lines == ()
assert gaps[0].missed_branch_lines == (2,)
def test_detect_unknown_and_xml_without_known_root() -> None:
for text in ("hello", "<other/>", ""):
with pytest.raises(ReportError):
detect_format(text)
def test_load_errors(tmp_path: Path) -> None:
with pytest.raises(ReportError, match="cannot read"):
load_report(tmp_path / "missing")
bad_xml = tmp_path / "bad.xml"
bad_xml.write_text("<coverage><oops", encoding="utf-8")
with pytest.raises(ReportError, match="malformed cobertura"):
load_report(bad_xml)
bad_json = tmp_path / "bad.json"
bad_json.write_text("{nope", encoding="utf-8")
with pytest.raises(ReportError, match="malformed json"):
load_report(bad_json)

View File

@ -0,0 +1,97 @@
"""Rendering, filtering and CLI exit-code tests."""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from coverage_gaps.cli import main
from coverage_gaps.models import FileGap
from coverage_gaps.render import compact_ranges, select, to_markdown
def test_compact_ranges() -> None:
assert compact_ranges([]) == ""
assert compact_ranges([40, 12, 13, 14, 15, 16, 17, 18, 18]) == "12-18, 40"
assert compact_ranges([1, 3, 5, 6]) == "1, 3, 5-6"
def test_select_sorts_and_filters() -> None:
small = FileGap("src/b.py", 5, (1,), 0, (), 0)
big = FileGap("lib/a.py", 5, (1, 2), 2, (3,), 1)
clean = FileGap("src/c.py", 5, (), 0, (), 0)
assert [g.path for g in select([small, clean, big])] == ["lib/a.py", "src/b.py"]
assert [g.path for g in select([small, big], path_prefix="src")] == ["src/b.py"]
assert [g.path for g in select([small, big], min_missed=2)] == ["lib/a.py"]
def test_markdown_branch_only_gap() -> None:
out = to_markdown("lcov", [FileGap("z.py", 2, (), 2, (7,), 1)], 1)
assert "- missed lines" not in out
assert "missed branches (1) at lines: 7" in out
def test_module_entrypoint(
reports: dict[str, Path], monkeypatch: pytest.MonkeyPatch
) -> None:
import runpy
monkeypatch.setattr("sys.argv", ["coverage_gaps", str(reports["lcov"])])
with pytest.raises(SystemExit) as exc:
runpy.run_module("coverage_gaps", run_name="__main__")
assert exc.value.code == 1
def test_markdown_no_gaps() -> None:
assert "No coverage gaps." in to_markdown("lcov", [], 0)
@pytest.mark.parametrize("fmt", ["lcov", "cobertura", "jacoco", "json"])
def test_cli_markdown_exit_1(
reports: dict[str, Path], fmt: str, capsys: pytest.CaptureFixture[str]
) -> None:
assert main([str(reports[fmt])]) == 1
out = capsys.readouterr().out
assert "2-3, 5" in out
assert "missed branches (1) at lines: 4" in out
assert "b.py" not in out.replace("A.kt", "")
def test_cli_json_and_max_files(
reports: dict[str, Path], capsys: pytest.CaptureFixture[str]
) -> None:
assert main([str(reports["jacoco"]), "--json", "--max-files", "1"]) == 1
doc = json.loads(capsys.readouterr().out)
assert doc["shown"] == 1
assert doc["total"] == {"files": 1, "missed_lines": 3, "missed_branches": 1}
assert doc["files"][0]["missed_lines"] == "2-3, 5"
def test_cli_markdown_truncation_note(
reports: dict[str, Path], capsys: pytest.CaptureFixture[str], tmp_path: Path
) -> None:
two = tmp_path / "two.info"
two.write_text("SF:x\nDA:1,0\nend_of_record\nSF:y\nDA:1,0\nDA:2,0\nend_of_record\n")
assert main([str(two), "--max-files", "1"]) == 1
out = capsys.readouterr().out
assert "Showing the 1 worst files." in out
assert "`y`" in out
assert "`x`" not in out
def test_cli_exit_0_when_filtered_clean(
reports: dict[str, Path], capsys: pytest.CaptureFixture[str]
) -> None:
assert main([str(reports["lcov"]), "--path-prefix", "b."]) == 0
assert main([str(reports["lcov"]), "--json", "--min-missed", "99"]) == 0
assert "No coverage gaps." in capsys.readouterr().out
def test_cli_exit_2(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
assert main([str(tmp_path / "nope")]) == 2
junk = tmp_path / "junk"
junk.write_text("junk")
assert main([str(junk)]) == 2
assert "coverage-gaps:" in capsys.readouterr().err

View File

@ -27,7 +27,7 @@ exceptions:
pinned: "6.0.3" pinned: "6.0.3"
latest_known: "7.0.2" latest_known: "7.0.2"
reason: >- reason: >-
typescript-eslint 8.70.0 -- itself the latest release -- declares typescript-eslint 8.71.0 -- itself the latest release -- declares
`typescript: ">=4.8.4 <6.1.0"` and throws "typescript-eslint does not `typescript: ">=4.8.4 <6.1.0"` and throws "typescript-eslint does not
support TS 7.0" when eslint loads its config. No 9.x exists. Taking TS 7 support TS 7.0" when eslint loads its config. No 9.x exists. Taking TS 7
would mean dropping every type-aware lint rule from every repo that pins would mean dropping every type-aware lint rule from every repo that pins
@ -36,21 +36,4 @@ exceptions:
awesome-mcp-explorer, europe-county-map and every other repo pinning awesome-mcp-explorer, europe-county-map and every other repo pinning
typescript, which is exactly why the entry lives here and not in eight typescript, which is exactly why the entry lives here and not in eight
separate files. separate files.
blocked_by: "transitive:typescript-eslint@8.70.0" blocked_by: "transitive:typescript-eslint@8.71.0"
- ecosystem: npm
package: typescript-eslint
pinned: "8.70.0"
latest_known: "8.70.1"
reason: >-
The meta-package published 8.70.1, but its own dependency
@typescript-eslint/scope-manager has not: `pnpm install --force`
resolves the tree and fails with ERR_PNPM_NO_MATCHING_VERSION,
because typescript-eslint@8.70.1 requires
@typescript-eslint/scope-manager@8.70.1 and the npm registry's
latest published scope-manager is still 8.70.0. Not installable at
any pin, so 8.70.0 -- the last version whose whole dependency tree
actually resolves -- is what every repo pinning it stays on.
Held fleet-wide for the same reason the typescript entry above is:
it blocks every repo pinning typescript-eslint, not just this one.
blocked_by: "transitive:@typescript-eslint/scope-manager@8.70.0"

View File

@ -2,9 +2,9 @@
# floor is satisfied by anything newer, so it tells a checker nothing # floor is satisfied by anything newer, so it tells a checker nothing
# about whether this repo is actually current. # about whether this repo is actually current.
bandit[toml]==1.9.4 bandit[toml]==1.9.4
mypy==2.3.1 mypy==2.4.0
pre-commit==4.6.2 pre-commit==4.6.2
pylint==4.0.9 pylint==4.1.2
pytest==9.1.1 pytest==9.1.1
pytest-cov==7.1.0 pytest-cov==7.1.0
ruff==0.16.9 ruff==0.16.10

View File

@ -2,9 +2,9 @@
# floor is satisfied by anything newer, so it tells a checker nothing # floor is satisfied by anything newer, so it tells a checker nothing
# about whether this repo is actually current. # about whether this repo is actually current.
bandit[toml]==1.9.4 bandit[toml]==1.9.4
mypy==2.3.1 mypy==2.4.0
pre-commit==4.6.2 pre-commit==4.6.2
pylint==4.0.9 pylint==4.1.2
pytest==9.1.1 pytest==9.1.1
pytest-cov==7.1.0 pytest-cov==7.1.0
ruff==0.16.9 ruff==0.16.10

View File

@ -2,10 +2,10 @@
# floor is satisfied by anything newer, so it tells a checker nothing # floor is satisfied by anything newer, so it tells a checker nothing
# about whether this repo is actually current. # about whether this repo is actually current.
bandit[toml]==1.9.4 bandit[toml]==1.9.4
mypy==2.3.1 mypy==2.4.0
numpy==2.5.3 numpy==2.5.3
pre-commit==4.6.2 pre-commit==4.6.2
pylint==4.0.9 pylint==4.1.2
pytest==9.1.1 pytest==9.1.1
pytest-cov==7.1.0 pytest-cov==7.1.0
ruff==0.16.9 ruff==0.16.10

66
repo_contract/README.md Normal file
View File

@ -0,0 +1,66 @@
# repo_contract
Shared gate: every repo tells an agent how to run and test it, so nobody has to
explore. Enforced like the other shared gates (real gate in `~/src/utils`, a
thin shim per repo, pre-commit + CI).
## The contract
`CLAUDE.md` (or `AGENTS.md` that `CLAUDE.md` symlinks to) has a `## Commands`
section with one line per key, exactly `- <key>: \`<command>\``:
```markdown
## Commands
- run: `<command>` | n/a: library
- test: `<full suite>`
- test-changed: `scripts/test_changed.sh`
- lint: `<command>`
- coverage: `<command writing a report>`
- coverage-gaps: `coverage-gaps <report>`
```
Rules (all machine-checked):
1. All six keys present, once each. A value is a backticked command or
`n/a: <reason>` / `n/a (<reason>)`. A bare `n/a` fails: n/a carries a reason.
2. `test-changed` invokes an existing, executable `scripts/test_changed.sh`-style
script that runs only tests related to files changed vs HEAD, falls back to
the full suite when it cannot map, and prints failures plus a one-line
summary only.
3. `coverage-gaps` runs the shared lister `coverage-gaps <report>`
(`~/src/utils/coverage_gaps`).
4. `test`, `test-changed`, `lint`, `coverage` are quiet: no `-v`/`--verbose`/
expanded reporter, in the command or in the script's runner lines.
5. No placeholders (`<FILL IN>`, `TODO`, `...`).
6. If `test` is n/a, then `test-changed`, `coverage`, `coverage-gaps` are n/a
too, and the repo must contain no tests (bounded scan).
## Use
```bash
python3 ~/src/utils/repo_contract/check.py --repo <path> # 0 pass / 1 violations / 2 unreadable
python3 ~/src/utils/repo_contract/bootstrap.py --repo <path> # print proposal
python3 ~/src/utils/repo_contract/bootstrap.py --repo <path> --write
python3 ~/src/utils/repo_contract/sweep.py # ~/data/claude-scratch/repo-contract/SWEEP.md
```
Bootstrap never overwrites: it appends the section (or inserts missing lines at
the end of an existing `## Commands`), creates `CLAUDE.md -> AGENTS.md` when
only AGENTS.md exists, and writes `scripts/test_changed.sh` only if absent.
Sources, in priority order: existing CLAUDE.md, `scripts/<key>.sh`, Makefile,
package.json (node only), CI workflows, stack defaults. Anything it cannot infer
is written as `<FILL IN>`, which the gate rejects until a human fills it.
## Adding the gate to a repo
1. Copy `templates/check_repo_contract.shim.sh` to `scripts/check_repo_contract.sh`
(never copy `scripts/check_repo_contract.sh` from this repo).
2. Add `templates/pre-commit-snippet.yaml` to `.pre-commit-config.yaml`.
3. Copy `templates/repo-contract.yml` to `.github/workflows/`.
## Tests
```bash
PYTHONPATH=. python -m pytest repo_contract/tests -q --cov=repo_contract --cov-branch --cov-fail-under=100
```

View File

@ -0,0 +1 @@
"""Repo contract: every repo documents exact run/test/lint/coverage commands."""

123
repo_contract/bootstrap.py Normal file
View File

@ -0,0 +1,123 @@
"""Propose (or with --write, create) the `## Commands` section and test script.
Usage: bootstrap.py --repo PATH [--write]. Never overwrites: the section is
appended (or missing lines inserted at the end of an existing section) and
scripts/test_changed.sh is only created when absent.
"""
from __future__ import annotations
import argparse
import shutil
import sys
from pathlib import Path
if __package__ in (None, ""): # pragma: no cover - plain-script import path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from repo_contract.contract import HEADING, has_section, parse_entries
from repo_contract.infer import TEST_CHANGED, Proposal, propose
from repo_contract.stacks import detect_stacks
TEMPLATES = Path(__file__).resolve().parent / "templates"
def insert_lines(text: str, new: list[str]) -> str:
"""`text` with `new` lines added: appended section, or end of existing one."""
if not new:
return text
if not has_section(text):
sep = (
""
if not text or text.endswith("\n\n")
else ("\n" if text.endswith("\n") else "\n\n")
)
return f"{text}{sep}{HEADING}\n\n" + "\n".join(new) + "\n"
lines = text.splitlines()
start = next(i for i, ln in enumerate(lines) if ln.strip() == HEADING)
end = next(
(i for i in range(start + 1, len(lines)) if lines[i].startswith("## ")),
len(lines),
)
while end > start + 1 and not lines[end - 1].strip():
end -= 1
lines[end:end] = new
return "\n".join(lines) + "\n"
def doc_path(repo: Path) -> Path:
"""The file to edit: CLAUDE.md (through a symlink), else AGENTS.md, else new CLAUDE.md."""
claude, agents = repo / "CLAUDE.md", repo / "AGENTS.md"
if claude.exists():
return claude.resolve()
return agents if agents.is_file() else claude
def missing_lines(prop: Proposal, text: str) -> list[str]:
"""Proposal lines for keys the existing document does not yet define."""
present, _ = parse_entries(text)
return [ln for ln in prop.lines() if ln.split(":", 1)[0][2:] not in present]
def script_plan(repo: Path, prop: Proposal) -> Path | None:
"""The template to copy for scripts/test_changed.sh, or None if not needed."""
wants = prop.values.get("test-changed", "") == f"`{TEST_CHANGED}`"
if not wants or (repo / TEST_CHANGED).exists():
return None
template = TEMPLATES / f"{prop.stack}.sh"
return template if template.is_file() else None
def apply(
repo: Path, doc: Path, text: str, new: list[str], script: Path | None
) -> list[str]:
"""Write the changes; returns human-readable actions taken."""
done: list[str] = []
if not (repo / "CLAUDE.md").exists() and (repo / "AGENTS.md").is_file():
(repo / "CLAUDE.md").symlink_to("AGENTS.md")
done.append("symlinked CLAUDE.md -> AGENTS.md")
if new:
doc.write_text(insert_lines(text, new), encoding="utf-8")
done.append(f"wrote {len(new)} line(s) to {doc.name}")
if script is not None:
target = repo / TEST_CHANGED
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(script, target)
target.chmod(0o755)
done.append(f"created {TEST_CHANGED} from {script.name}")
return done
def main(argv: list[str] | None = None) -> int:
"""CLI entry point; 0 on success, 2 for a path that is not a git repo."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo", type=Path, required=True)
parser.add_argument("--write", action="store_true")
args = parser.parse_args(argv)
repo = args.repo.expanduser().resolve()
if not repo.is_dir() or not (repo / ".git").exists():
print(f"bootstrap: {repo} is not a git repo", file=sys.stderr)
return 2
stacks = detect_stacks(repo)
prop = propose(repo, stacks)
doc = doc_path(repo)
text = doc.read_text(encoding="utf-8") if doc.is_file() else ""
new = missing_lines(prop, text)
script = script_plan(repo, prop)
print(f"# {repo.name} (stack: {', '.join(stacks) or 'unknown'})")
print("\n".join(new) if new else "(Commands section already complete)")
print(
f"# script: {TEST_CHANGED} <- {script.name}"
if script
else "# script: none to create"
)
for note in prop.notes:
print(f"# note: {note}")
if args.write:
for action in apply(repo, doc, text, new, script):
print(f"# {action}")
return 0
if __name__ == "__main__": # pragma: no cover
sys.exit(main())

142
repo_contract/check.py Normal file
View File

@ -0,0 +1,142 @@
"""Gate: the repo documents exact run/test/lint/coverage commands.
Usage: check.py [--repo PATH]. Exit 0 pass, 1 violations (printed), 2 not a
repo or unreadable CLAUDE.md. Contract text lives in README.md / DOCS-*.md.
"""
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
if __package__ in (None, ""): # pragma: no cover - plain-script import path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from repo_contract.contract import (
KEYS,
LISTER,
Entry,
entry_problems,
has_section,
parse_entries,
script_token,
verbose_in_script,
)
from repo_contract.stacks import detect_stacks, has_tests
EXIT_OK, EXIT_VIOLATIONS, EXIT_UNREADABLE = 0, 1, 2
DOC_NAMES = ("CLAUDE.md", "AGENTS.md")
class Unreadable(Exception):
"""Raised for a path that is not a repo or a doc that cannot be read."""
def read_doc(repo: Path) -> str | None:
"""CLAUDE.md text (symlinks followed); None if absent; Unreadable on I/O error."""
doc = repo / "CLAUDE.md"
if not doc.exists():
return None
try:
return doc.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as exc:
raise Unreadable(f"{doc}: {exc}") from exc
def _missing_doc(repo: Path) -> list[str]:
"""Violation for a missing CLAUDE.md, hinting at the AGENTS.md symlink."""
if (repo / "CLAUDE.md").is_symlink():
return ["CLAUDE.md is a dangling symlink"]
if (repo / "AGENTS.md").is_file():
return [
"CLAUDE.md missing: symlink it to AGENTS.md (ln -s AGENTS.md CLAUDE.md)"
]
return ["CLAUDE.md missing (or AGENTS.md that CLAUDE.md symlinks to)"]
def _check_changed(repo: Path, command: str) -> list[str]:
"""test-changed must run an existing, quiet entry-point script."""
script = script_token(command, repo)
if script is None:
msg = "'- test-changed:' must invoke an existing changed-files script"
return [f"{msg} (e.g. scripts/test_changed.sh)"]
out: list[str] = []
if not os.access(script, os.X_OK):
out.append(f"{script.relative_to(repo)} is not executable")
try:
if verbose_in_script(script.read_text(encoding="utf-8")):
out.append(f"{script.relative_to(repo)} runs a test tool verbosely")
except (OSError, UnicodeDecodeError) as exc:
raise Unreadable(f"{script}: {exc}") from exc
return out
def _check_consistency(
repo: Path, entries: dict[str, Entry], stacks: list[str]
) -> list[str]:
"""Cross-entry rules: n/a vs real tests, coverage-gaps lister."""
out: list[str] = []
test = entries["test"]
if test.command is None:
if has_tests(repo, stacks):
out.append("'- test:' is n/a but the repo contains tests")
for key in ("test-changed", "coverage", "coverage-gaps"):
if entries[key].command is not None:
out.append(f"'- {key}:' must be n/a when test is n/a")
return out
if entries["test-changed"].command is None:
out.append("'- test-changed:' cannot be n/a while test is a real command")
cov, gaps = entries["coverage"], entries["coverage-gaps"]
if cov.command is not None and gaps.command is None:
out.append(
"'- coverage-gaps:' must run the shared lister: `coverage-gaps <report>`"
)
if gaps.command is not None and gaps.command.split()[0] != LISTER:
out.append(f"'- coverage-gaps:' must start with `{LISTER} <report>`")
return out
def violations(repo: Path) -> tuple[list[str], list[str]]:
"""(violations, detected stacks) for `repo`. Raises Unreadable."""
stacks = detect_stacks(repo)
text = read_doc(repo)
if text is None:
return _missing_doc(repo), stacks
if not has_section(text):
return ["CLAUDE.md has no '## Commands' section"], stacks
entries, out = parse_entries(text)
absent = [k for k in KEYS if k not in entries]
if absent:
out.append("'## Commands' is missing: " + ", ".join(f"- {k}:" for k in absent))
return out, stacks
for entry in entries.values():
out.extend(entry_problems(entry))
changed = entries["test-changed"]
if changed.command is not None:
out.extend(_check_changed(repo, changed.command))
out.extend(_check_consistency(repo, entries, stacks))
return out, stacks
def main(argv: list[str] | None = None) -> int:
"""CLI entry point; returns the process exit code."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo", type=Path, default=Path.cwd())
repo = parser.parse_args(argv).repo.expanduser().resolve()
if not repo.is_dir() or not (repo / ".git").exists():
print(f"repo-contract: {repo} is not a git repo", file=sys.stderr)
return EXIT_UNREADABLE
try:
found, _ = violations(repo)
except Unreadable as exc:
print(f"repo-contract: unreadable: {exc}", file=sys.stderr)
return EXIT_UNREADABLE
for problem in found:
print(f"repo-contract: {repo.name}: {problem}", file=sys.stderr)
return EXIT_VIOLATIONS if found else EXIT_OK
if __name__ == "__main__": # pragma: no cover
sys.exit(main())

134
repo_contract/contract.py Normal file
View File

@ -0,0 +1,134 @@
"""The contract text: keys, the `## Commands` parser, entry validation."""
from __future__ import annotations
import re
import shlex
from dataclasses import dataclass
from pathlib import Path
KEYS: tuple[str, ...] = (
"run",
"test",
"test-changed",
"lint",
"coverage",
"coverage-gaps",
)
QUIET_KEYS = frozenset({"test", "test-changed", "lint", "coverage"})
HEADING = "## Commands"
LISTER = "coverage-gaps"
_ENTRY = re.compile(r"^[-*]\s+([a-z-]+):\s*(.*?)\s*$")
_CMD = re.compile(r"^`([^`]+)`")
_NA = re.compile(r"^n/a\s*(?::\s*(.*)|\(\s*(.*?)\s*\))\s*$")
_PLACEHOLDER = re.compile(r"<[A-Za-z][A-Za-z _-]*>|\bFILL IN\b|\bTODO\b|\.\.\.")
_VERBOSE = re.compile(r"^(-v+|--verbose|--reporter=expanded)$")
RUNNER_LINE = re.compile(
r"pytest|flutter|jest|vitest|go test|cargo|gradlew|bats|npm|pnpm|yarn"
)
@dataclass(frozen=True)
class Entry:
"""One `- key: value` line: a command, or an n/a with its reason."""
key: str
command: str | None
na_reason: str | None
def commands_section(text: str) -> list[str]:
"""Lines of the `## Commands` section (empty if absent); fences respected."""
lines: list[str] = []
inside = fenced = False
for line in text.splitlines():
if line.lstrip().startswith("```"):
fenced = not fenced
elif not fenced and line.startswith("## "):
inside = line.strip() == HEADING
continue
if inside:
lines.append(line)
return lines
def has_section(text: str) -> bool:
"""Whether a `## Commands` heading exists outside code fences."""
fenced = False
for line in text.splitlines():
if line.lstrip().startswith("```"):
fenced = not fenced
elif not fenced and line.strip() == HEADING:
return True
return False
def parse_entries(text: str) -> tuple[dict[str, Entry], list[str]]:
"""Entries by key plus syntax problems (duplicates, n/a without reason)."""
entries: dict[str, Entry] = {}
problems: list[str] = []
for line in commands_section(text):
match = _ENTRY.match(line)
if not match or match.group(1) not in KEYS:
continue
key, value = match.group(1), match.group(2)
if key in entries:
problems.append(f"duplicate '- {key}:' entry")
continue
cmd, na = _CMD.match(value), _NA.match(value)
if cmd:
entries[key] = Entry(key, cmd.group(1).strip(), None)
elif na and (na.group(1) or na.group(2) or "").strip():
entries[key] = Entry(key, None, (na.group(1) or na.group(2)).strip())
elif value.startswith("n/a"):
problems.append(f"'- {key}:' n/a must carry a reason: 'n/a: <why>'")
else:
problems.append(f"'- {key}:' needs a backticked command or 'n/a: <why>'")
return entries, problems
def is_verbose(command: str) -> bool:
"""True if the command carries a verbose flag (-v, --verbose, expanded)."""
try:
tokens = shlex.split(command)
except ValueError:
tokens = command.split()
return any(_VERBOSE.match(t) for t in tokens) or "--reporter expanded" in command
def verbose_in_script(text: str) -> bool:
"""Verbose flag on a runner invocation line of a shell script."""
return any(
is_verbose(line)
for line in text.splitlines()
if not line.lstrip().startswith("#") and RUNNER_LINE.search(line)
)
def entry_problems(entry: Entry) -> list[str]:
"""Problems with a single command entry (placeholder, verbosity)."""
if entry.command is None:
return []
out: list[str] = []
if _PLACEHOLDER.search(entry.command):
out.append(f"'- {entry.key}:' still holds a placeholder: {entry.command}")
if entry.key in QUIET_KEYS and is_verbose(entry.command):
out.append(
f"'- {entry.key}:' must be quiet by default (verbose flag in: {entry.command})"
)
return out
def script_token(command: str, repo: Path) -> Path | None:
"""The first token of `command` that names an existing file in `repo`."""
try:
tokens = shlex.split(command)
except ValueError:
tokens = command.split()
for token in tokens:
if "/" in token or token.endswith((".sh", ".py")):
candidate = repo / token
if candidate.is_file():
return candidate
return None

241
repo_contract/infer.py Normal file
View File

@ -0,0 +1,241 @@
"""Infer a proposed `## Commands` section from a repo's existing config."""
from __future__ import annotations
import re
from dataclasses import dataclass, field
from pathlib import Path
from repo_contract.contract import KEYS, LISTER, is_verbose
from repo_contract.sources import (
MAKE_TARGETS,
NPM_SCRIPTS,
ci_commands,
doc_commands,
first_match,
make_targets,
package_scripts,
read_text,
)
from repo_contract.stacks import has_tests
TEST_CHANGED = "scripts/test_changed.sh"
NO_TESTS = "n/a: no automated tests yet"
NO_COVERAGE = "n/a: no coverage tooling for this stack"
REPORTS = {
"python": "coverage.xml",
"flutter": "coverage/lcov.info",
"node": "coverage/lcov.info",
"go": "coverage.out",
"rust": "lcov.info",
"gradle": "build/reports/jacoco/test/jacocoTestReport.xml",
}
_VERBOSE_TOKEN = re.compile(r"\s(-v+|--verbose)(?=\s|$)")
@dataclass
class Proposal:
"""Contract values by key (already formatted) plus notes for the human."""
stack: str
values: dict[str, str] = field(default_factory=dict)
notes: list[str] = field(default_factory=list)
def lines(self) -> list[str]:
"""The `- key: value` lines in contract order."""
return [f"- {k}: {self.values[k]}" for k in KEYS if k in self.values]
def quiet(command: str) -> str:
"""Strip verbose flags and add the tool's quiet default where known."""
command = _VERBOSE_TOKEN.sub("", command)
if "pytest" in command and not re.search(r"\s-q+\b", command):
command = command.replace("pytest", "pytest -q", 1)
if (
"flutter test" in command
and "--reporter" not in command
and " -r " not in command
):
command += " --reporter=compact"
return (
command
if not is_verbose(command)
else command.replace("--reporter=expanded", "")
)
def _prefix(repo: Path) -> str:
"""How python tools are launched in this repo."""
if (repo / "uv.lock").is_file():
return "uv run"
return "poetry run" if (repo / "poetry.lock").is_file() else "python3 -m"
def _pm(repo: Path) -> str:
"""Node package manager from the lockfile."""
if (repo / "pnpm-lock.yaml").is_file():
return "pnpm"
return "yarn" if (repo / "yarn.lock").is_file() else "npm"
def stack_defaults(repo: Path, stack: str) -> dict[str, str]:
"""Conventional commands for the stack (lowest-priority source)."""
cfg = read_text(repo / "pyproject.toml") + read_text(
repo / ".pre-commit-config.yaml"
)
py = _prefix(repo)
pm = _pm(repo)
ruff = "ruff check ." if py == "python3 -m" else f"{py} ruff check ."
table: dict[str, dict[str, str]] = {
"python": {
"test": f"{py} pytest -q",
"lint": ruff if "ruff" in cfg else "pre-commit run --all-files",
"coverage": f"{py} pytest -q --cov --cov-branch --cov-report=xml",
},
"flutter": {
"run": "flutter run",
"test": "flutter test --reporter=compact",
"lint": "flutter analyze",
"coverage": "flutter test --reporter=compact --coverage",
},
"node": {
"test": f"{pm} test --silent",
"lint": f"{pm} run lint",
"coverage": f"{pm} test --silent --coverage",
},
"go": {
"test": "go test ./...",
"lint": "go vet ./...",
"coverage": "go test -coverprofile=coverage.out ./...",
},
"rust": {
"test": "cargo test --quiet",
"lint": "cargo clippy --quiet",
"coverage": "cargo llvm-cov --lcov --output-path lcov.info",
},
"gradle": {
"test": "./gradlew test --quiet",
"lint": "./gradlew lint --quiet",
"coverage": "./gradlew jacocoTestReport --quiet",
},
"godot": _godot_defaults(repo),
"shell": {"lint": "shellcheck scripts/*.sh", "coverage": NO_COVERAGE},
}
return table.get(stack, {})
def _godot_defaults(repo: Path) -> dict[str, str]:
"""Godot commands; the test runner comes from addons/ (GUT or gdUnit4)."""
out = {"run": "godot --path .", "coverage": NO_COVERAGE}
if (repo / "addons/gut/gut_cmdln.gd").is_file():
out["test"] = "godot --headless -s addons/gut/gut_cmdln.gd -gexit -glog=0"
elif (repo / "addons/gdUnit4").is_dir():
out["test"] = (
"godot --headless -s -d addons/gdUnit4/bin/GdUnitCmdTool.gd -a res://tests"
)
return out
def _run_default(repo: Path, stack: str) -> str:
"""Best guess for `run`, or an n/a with reason."""
if stack == "python":
for name in ("main.py", "__main__.py", "app.py"):
if (repo / name).is_file():
return f"python3 {name}"
return "n/a: library (no entry point found)"
if stack == "go":
return (
"go run ." if (repo / "main.go").is_file() else "n/a: library (no main.go)"
)
if stack == "rust":
return (
"cargo run"
if (repo / "src" / "main.rs").is_file()
else "n/a: library (no src/main.rs)"
)
if stack == "gradle":
return (
"./gradlew installDebug"
if (repo / "app").is_dir()
else "n/a: library (no app module)"
)
if stack == "node":
return "n/a: library (no start/dev script)"
return "n/a: collection of scripts, no single entry point"
def _pick(
repo: Path, key: str, stack: str, doc: list[str], ci: list[str]
) -> tuple[str | None, str]:
"""(command, source) for one key by priority doc > Makefile > npm > CI > default."""
found = first_match(key, doc)
if found:
return found, "CLAUDE.md"
targets = make_targets(repo)
for name in MAKE_TARGETS.get(key, ()):
if name in targets:
return f"make {name}", "Makefile"
helper = repo / "scripts" / f"{key}.sh"
if helper.is_file():
return f"scripts/{key}.sh", "scripts/"
scripts = package_scripts(repo) if stack == "node" else {}
for name in NPM_SCRIPTS.get(key, ()):
if name in scripts:
return f"{_pm(repo)} run {name}", "package.json"
found = first_match(key, ci)
if found:
return found, "CI workflow"
return stack_defaults(repo, stack).get(key), "stack default"
def propose(repo: Path, stacks: list[str]) -> Proposal:
"""Build the proposal for the repo's primary stack."""
stack = stacks[0] if stacks else "unknown"
prop = Proposal(stack)
doc = doc_commands(read_text(repo / "CLAUDE.md"))
ci = ci_commands(repo)
tested = has_tests(repo, stacks)
for key in ("run", "lint"):
cmd, src = _pick(repo, key, stack, doc, ci)
prop.values[key] = _fmt(
cmd or (_run_default(repo, stack) if key == "run" else None), prop, key, src
)
if not tested and stack in {"godot", "shell", "unknown"}:
for key in ("test", "test-changed", "coverage", "coverage-gaps"):
prop.values[key] = NO_TESTS
prop.notes.append("no tests detected: test chain proposed as n/a")
return _order(prop)
test, src = _pick(repo, "test", stack, doc, ci)
prop.values["test"] = _fmt(test, prop, "test", src)
prop.values["test-changed"] = f"`{TEST_CHANGED}`"
cov, src = _pick(repo, "coverage", stack, doc, ci)
prop.values["coverage"] = _fmt(cov, prop, "coverage", src)
report = REPORTS.get(stack)
if prop.values["coverage"].startswith("n/a"):
prop.values["coverage-gaps"] = NO_COVERAGE
elif report:
prop.values["coverage-gaps"] = f"`{LISTER} {report}`"
else:
prop.values["coverage-gaps"] = "`<FILL IN>`"
prop.notes.append("coverage-gaps: unknown report path, fill in")
if not tested:
prop.notes.append("no tests detected, but the stack normally has them")
return _order(prop)
def _fmt(command: str | None, prop: Proposal, key: str, source: str) -> str:
"""Format a command (quiet-ified) or a placeholder, noting the source."""
if not command:
prop.notes.append(f"{key}: could not infer, fill in")
return "`<FILL IN>`"
if command.startswith("n/a"):
prop.notes.append(f"{key}: {command}")
return command
prop.notes.append(f"{key}: from {source}")
return f"`{quiet(command)}`"
def _order(prop: Proposal) -> Proposal:
"""Keep values in contract order."""
prop.values = {k: prop.values[k] for k in KEYS if k in prop.values}
return prop

87
repo_contract/sources.py Normal file
View File

@ -0,0 +1,87 @@
"""Where bootstrap reads existing commands from: doc, Makefile, package.json, CI."""
from __future__ import annotations
import json
import re
from pathlib import Path
PATTERNS: dict[str, re.Pattern[str]] = {
"test": re.compile(
r"pytest|flutter test|vitest|jest|go test|cargo (nextest|test)|gradlew.* test|bats"
),
"lint": re.compile(
r"ruff|flutter analyze|dart analyze|eslint|golangci|clippy|shellcheck|pre-commit run|ktlint"
),
"coverage": re.compile(r"--cov|--coverage|llvm-cov|jacoco|coverage run"),
}
MAKE_TARGETS = {
"run": ("run", "start", "dev"),
"test": ("test", "tests", "check"),
"lint": ("lint",),
"coverage": ("coverage", "cov"),
}
NPM_SCRIPTS = {
"run": ("start", "dev"),
"test": ("test",),
"lint": ("lint",),
"coverage": ("coverage", "test:coverage", "test:cov"),
}
_SPAN = re.compile(r"`([^`\n]+)`")
_MAKE = re.compile(r"^([A-Za-z0-9_.-]+):(?!=)", re.MULTILINE)
_CI_RUN = re.compile(r"^\s*(?:-\s*)?run:\s*(\S.*?)\s*$")
def read_text(path: Path) -> str:
"""File text, or '' when absent/unreadable (sources are best-effort)."""
try:
return path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
return ""
def doc_commands(text: str) -> list[str]:
"""Backticked spans of a CLAUDE.md that look like commands (contain a space)."""
return [s for s in _SPAN.findall(text) if " " in s and s[0] not in "/~"]
def make_targets(repo: Path) -> set[str]:
"""Target names defined in the repo's Makefile."""
return set(_MAKE.findall(read_text(repo / "Makefile")))
def package_scripts(repo: Path) -> dict[str, str]:
"""`scripts` of package.json ({} if absent or malformed)."""
try:
data = json.loads(read_text(repo / "package.json"))
except json.JSONDecodeError:
return {}
scripts = data.get("scripts") if isinstance(data, dict) else None
return scripts if isinstance(scripts, dict) else {}
def ci_commands(repo: Path) -> list[str]:
"""Single-line `run:` commands from .github/workflows (no expressions)."""
out: list[str] = []
for wf in sorted((repo / ".github" / "workflows").glob("*.y*ml")):
for line in read_text(wf).splitlines():
match = _CI_RUN.match(line)
if (
match
and "${{" not in match.group(1)
and match.group(1) not in {"|", ">", "|-"}
):
out.append(match.group(1))
return out
def first_match(key: str, commands: list[str]) -> str | None:
"""First command matching the key's pattern."""
pattern = PATTERNS.get(key)
if pattern is None:
return None
for cmd in commands:
covering = PATTERNS["coverage"].search(cmd) is not None
if pattern.search(cmd) and (covering == (key == "coverage")):
return cmd
return None

118
repo_contract/stacks.py Normal file
View File

@ -0,0 +1,118 @@
"""Stack detection and a bounded "does this repo have tests" probe."""
from __future__ import annotations
import os
from pathlib import Path
PRUNE = frozenset(
{
".git",
"node_modules",
"build",
".dart_tool",
".venv",
"venv",
"target",
"__pycache__",
".gradle",
".idea",
"dist",
".mypy_cache",
".ruff_cache",
".pytest_cache",
"Pods",
".godot",
"vendor",
"third_party",
}
)
MAX_DEPTH = 4
MAX_FILES = 5000
# stack -> marker files at the repo root (checked in order of this table).
MARKERS: tuple[tuple[str, tuple[str, ...]], ...] = (
("flutter", ("pubspec.yaml",)),
("godot", ("project.godot",)),
(
"gradle",
("build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts"),
),
("python", ("pyproject.toml", "setup.py", "setup.cfg", "requirements.txt")),
("node", ("package.json",)),
("go", ("go.mod",)),
("rust", ("Cargo.toml",)),
)
def _walk(repo: Path) -> list[Path]:
"""Files under `repo` (pruned, depth- and count-bounded; never follows links)."""
found: list[Path] = []
base = len(repo.parts)
for root, dirs, files in os.walk(repo, followlinks=False):
dirs[:] = [d for d in dirs if d not in PRUNE]
if len(Path(root).parts) - base >= MAX_DEPTH:
dirs[:] = []
found.extend(Path(root, f) for f in files)
if len(found) >= MAX_FILES:
break
return found
def detect_stacks(repo: Path) -> list[str]:
"""Stacks present in `repo`, most specific first; `shell` if only scripts."""
stacks = [s for s, names in MARKERS if any((repo / n).is_file() for n in names)]
if not stacks and any(p.suffix == ".sh" for p in _walk(repo)):
stacks.append("shell")
return stacks
def _has_pattern(
files: list[Path], suffixes: tuple[str, ...], names: tuple[str, ...]
) -> bool:
"""True when some file ends with a test suffix or sits under a test dir."""
for path in files:
if path.name.endswith(suffixes) or path.name.startswith(names):
return True
return False
def has_tests(repo: Path, stacks: list[str]) -> bool:
"""Whether the repo ships any automated tests (best-effort, bounded)."""
files = _walk(repo)
rels = [p.relative_to(repo) for p in files]
in_test_dir = any(
part in {"tests", "test", "spec", "__tests__"}
for r in rels
for part in r.parts[:-1]
)
named = _has_pattern(
files,
(
"_test.py",
"_test.dart",
".test.ts",
".test.js",
".test.tsx",
".spec.ts",
".spec.js",
"_test.go",
"Test.kt",
"Test.java",
".bats",
"_test.gd",
),
("test_",),
)
return in_test_dir or named or _node_test_script(repo, stacks)
def _node_test_script(repo: Path, stacks: list[str]) -> bool:
"""A real `scripts.test` in package.json (not npm's placeholder)."""
if "node" not in stacks:
return False
try:
text = (repo / "package.json").read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
return False
return '"test"' in text and "no test specified" not in text

102
repo_contract/sweep.py Normal file
View File

@ -0,0 +1,102 @@
"""Run the contract check over every git repo under ~/src and ~ and write SWEEP.md."""
from __future__ import annotations
import sys
from collections import Counter
from dataclasses import dataclass
from pathlib import Path
if __package__ in (None, ""): # pragma: no cover - plain-script import path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from repo_contract.check import Unreadable, violations
OUT = Path.home() / "data" / "claude-scratch" / "repo-contract" / "SWEEP.md"
@dataclass(frozen=True)
class Result:
"""Outcome for one repo."""
name: str
path: Path
stacks: list[str]
problems: list[str]
error: str | None = None
@property
def passed(self) -> bool:
"""True when there were no problems and no read error."""
return not self.problems and self.error is None
def find_repos(roots: list[Path]) -> list[Path]:
"""Direct children of each root that are git repos (`.git` is a directory)."""
found = [
d
for r in roots
if r.is_dir()
for d in sorted(r.iterdir())
if not d.name.startswith(".") and (d / ".git").is_dir()
]
return sorted(found, key=lambda p: str(p))
def run_one(repo: Path) -> Result:
"""Check one repo, folding read errors into the result."""
try:
problems, stacks = violations(repo)
except Unreadable as exc:
return Result(repo.name, repo, [], [], f"unreadable: {exc}")
return Result(repo.name, repo, stacks, problems)
def render(results: list[Result]) -> str:
"""Markdown report: summary, by-stack counts, per-repo table."""
passed = sum(r.passed for r in results)
by_stack: Counter[str] = Counter()
fails: Counter[str] = Counter()
for r in results:
key = r.stacks[0] if r.stacks else "none"
by_stack[key] += 1
fails[key] += not r.passed
out = [
"# Repo contract sweep",
"",
f"{len(results)} repos: {passed} pass, {len(results) - passed} fail.",
"",
"| primary stack | repos | fail |",
"|---|---|---|",
*[f"| {s} | {n} | {fails[s]} |" for s, n in sorted(by_stack.items())],
"",
"| repo | result | stacks | missing |",
"|---|---|---|---|",
]
for r in results:
verdict = "PASS" if r.passed else "FAIL"
detail = r.error or "; ".join(r.problems)
out.append(
f"| {r.name} | {verdict} | {', '.join(r.stacks) or '-'} | {detail.replace('|', '/')} |"
)
return "\n".join(out) + "\n"
def main(
argv: list[str] | None = None, roots: list[Path] | None = None, out: Path = OUT
) -> int:
"""Sweep, write the report, print the summary line. Always exit 0."""
del argv
home = Path.home()
results = [run_one(r) for r in find_repos(roots or [home / "src", home])]
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(render(results), encoding="utf-8")
passed = sum(r.passed for r in results)
print(
f"repo-contract sweep: {len(results)} repos, {passed} pass, {len(results) - passed} fail -> {out}"
)
return 0
if __name__ == "__main__": # pragma: no cover
sys.exit(main())

View File

@ -0,0 +1,26 @@
#!/bin/bash
# ============================================================================
# Fail if this repo breaks the shared repo contract.
#
# Thin delegate to the shared gate in ~/src/utils, which owns the contract
# text. Copy this file to scripts/check_repo_contract.sh in a repo; never copy
# the shared gate itself, or the shim would exec itself forever.
# ============================================================================
set -euo pipefail
readonly SHARED_GATE="${UTILS_ROOT:-$HOME/src/utils}/scripts/check_repo_contract.sh"
main() {
if [[ ! -x "$SHARED_GATE" ]]; then
echo "Error: shared repo-contract gate not found at $SHARED_GATE" >&2
echo " Clone github.com/kuhyx/utils to ~/src/utils, or set" >&2
echo " UTILS_ROOT to where it lives." >&2
exit 1
fi
exec bash "$SHARED_GATE" "$@"
}
main "$@"

View File

@ -0,0 +1,41 @@
#!/bin/bash
# ============================================================================
# Run only the tests related to files changed vs HEAD (staged, unstaged and
# untracked). Quiet: failures plus a one-line summary. Falls back to the full
# suite when a change cannot be mapped to tests. Generated by repo_contract.
# ============================================================================
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
CHANGED=()
while IFS= read -r f; do
[[ -n "$f" && -e "$f" ]] && CHANGED+=("$f")
done < <({ git diff --name-only HEAD 2>/dev/null || true; git ls-files --others --exclude-standard; } | sort -u)
full=0
tests=()
for f in "${CHANGED[@]}"; do
case "$f" in
pubspec.yaml | pubspec.lock | test/flutter_test_config.dart) full=1 ;;
test/*_test.dart) tests+=("$f") ;;
lib/*.dart)
rel="${f#lib/}"
if [[ -f "test/${rel%.dart}_test.dart" ]]; then
tests+=("test/${rel%.dart}_test.dart")
else
while IFS= read -r t; do tests+=("$t"); done \
< <(git ls-files "test/*$(basename "${f%.dart}")_test.dart")
fi
;;
esac
done
if [[ $full -eq 0 && ${#CHANGED[@]} -eq 0 ]]; then echo "no changes vs HEAD: nothing to test"; exit 0; fi
if [[ $full -eq 0 && ${#tests[@]} -gt 0 ]]; then flutter test --reporter=compact "${tests[@]}"; exit $?; fi
if [[ $full -eq 0 ]] && ! printf '%s\n' "${CHANGED[@]}" | grep -q '\.dart$'; then
echo "no dart changes: nothing to test"; exit 0
fi
echo "no mapped tests: running full suite"
flutter test --reporter=compact

29
repo_contract/templates/go.sh Executable file
View File

@ -0,0 +1,29 @@
#!/bin/bash
# ============================================================================
# Run only the tests related to files changed vs HEAD (staged, unstaged and
# untracked). Quiet: failures plus a one-line summary. Falls back to the full
# suite when a change cannot be mapped to tests. Generated by repo_contract.
# ============================================================================
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
CHANGED=()
while IFS= read -r f; do
[[ -n "$f" && -e "$f" ]] && CHANGED+=("$f")
done < <({ git diff --name-only HEAD 2>/dev/null || true; git ls-files --others --exclude-standard; } | sort -u)
pkgs=()
full=0
for f in "${CHANGED[@]}"; do
case "$f" in
go.mod | go.sum) full=1 ;;
*.go) pkgs+=("./$(dirname "$f")") ;;
esac
done
if [[ $full -eq 0 && ${#pkgs[@]} -eq 0 ]]; then echo "no go changes: nothing to test"; exit 0; fi
if [[ $full -eq 1 ]]; then echo "module changed: running full suite"; go test ./...; exit $?; fi
mapfile -t uniq < <(printf '%s\n' "${pkgs[@]}" | sort -u)
go test "${uniq[@]}"

View File

@ -0,0 +1,25 @@
#!/bin/bash
# ============================================================================
# Run only the tests related to files changed vs HEAD (staged, unstaged and
# untracked). Quiet: failures plus a one-line summary. Falls back to the full
# suite when a change cannot be mapped to tests. Generated by repo_contract.
# ============================================================================
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
CHANGED=()
while IFS= read -r f; do
[[ -n "$f" && -e "$f" ]] && CHANGED+=("$f")
done < <({ git diff --name-only HEAD 2>/dev/null || true; git ls-files --others --exclude-standard; } | sort -u)
# Godot test runners cannot select tests by changed file: any change runs the suite headless.
if [[ ${#CHANGED[@]} -eq 0 ]]; then echo "no changes vs HEAD: nothing to test"; exit 0; fi
if [[ -f addons/gut/gut_cmdln.gd ]]; then
godot --headless -s addons/gut/gut_cmdln.gd -gexit -glog=0
elif [[ -f addons/gdUnit4/bin/GdUnitCmdTool.gd ]]; then
godot --headless -s -d addons/gdUnit4/bin/GdUnitCmdTool.gd -a res://test --ignoreHeadlessMode
else
echo "no GUT or gdUnit4 runner found under addons/" >&2; exit 1
fi

View File

@ -0,0 +1,21 @@
#!/bin/bash
# ============================================================================
# Run only the tests related to files changed vs HEAD (staged, unstaged and
# untracked). Quiet: failures plus a one-line summary. Falls back to the full
# suite when a change cannot be mapped to tests. Generated by repo_contract.
# ============================================================================
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
CHANGED=()
while IFS= read -r f; do
[[ -n "$f" && -e "$f" ]] && CHANGED+=("$f")
done < <({ git diff --name-only HEAD 2>/dev/null || true; git ls-files --others --exclude-standard; } | sort -u)
# Gradle test selection by class needs per-project knowledge: any source change runs the suite.
if ! printf '%s\n' "${CHANGED[@]:-}" | grep -qE '\.(kt|java|kts|gradle|xml)$'; then
echo "no jvm source changes: nothing to test"; exit 0
fi
./gradlew test --quiet

38
repo_contract/templates/node.sh Executable file
View File

@ -0,0 +1,38 @@
#!/bin/bash
# ============================================================================
# Run only the tests related to files changed vs HEAD (staged, unstaged and
# untracked). Quiet: failures plus a one-line summary. Falls back to the full
# suite when a change cannot be mapped to tests. Generated by repo_contract.
# ============================================================================
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
CHANGED=()
while IFS= read -r f; do
[[ -n "$f" && -e "$f" ]] && CHANGED+=("$f")
done < <({ git diff --name-only HEAD 2>/dev/null || true; git ls-files --others --exclude-standard; } | sort -u)
if [[ -f pnpm-lock.yaml ]]; then PM=pnpm; elif [[ -f yarn.lock ]]; then PM=yarn; else PM=npm; fi
full=0
src=()
for f in "${CHANGED[@]}"; do
case "$f" in
package.json | pnpm-lock.yaml | yarn.lock | package-lock.json) full=1 ;;
*.ts | *.tsx | *.js | *.jsx | *.mjs | *.cjs) src+=("$f") ;;
esac
done
full_suite() { "$PM" test --silent; }
if [[ $full -eq 0 && ${#CHANGED[@]} -eq 0 ]]; then echo "no changes vs HEAD: nothing to test"; exit 0; fi
if [[ $full -eq 0 && ${#src[@]} -eq 0 ]]; then echo "no js/ts changes: nothing to test"; exit 0; fi
if [[ $full -eq 0 ]] && grep -qs '"vitest"' package.json; then
"$PM" exec vitest related --run --reporter=dot "${src[@]}"; exit $?
fi
if [[ $full -eq 0 ]] && grep -qs '"jest"' package.json; then
"$PM" exec jest --findRelatedTests --silent "${src[@]}"; exit $?
fi
echo "cannot map changes to tests: running full suite"
full_suite

View File

@ -0,0 +1,9 @@
# Add under `repos: - repo: local: hooks:` in a repo's .pre-commit-config.yaml,
# after copying check_repo_contract.shim.sh to scripts/check_repo_contract.sh.
# pass_filenames is false: the contract is about the repo, not the staged files.
- id: repo-contract
name: repo contract (CLAUDE.md Commands + test_changed)
entry: scripts/check_repo_contract.sh
language: system
pass_filenames: false
files: ^(CLAUDE\.md|AGENTS\.md|scripts/test_changed\.sh)$

View File

@ -0,0 +1,50 @@
#!/bin/bash
# ============================================================================
# Run only the tests related to files changed vs HEAD (staged, unstaged and
# untracked). Quiet: failures plus a one-line summary. Falls back to the full
# suite when a change cannot be mapped to tests. Generated by repo_contract.
# ============================================================================
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
CHANGED=()
while IFS= read -r f; do
[[ -n "$f" && -e "$f" ]] && CHANGED+=("$f")
done < <({ git diff --name-only HEAD 2>/dev/null || true; git ls-files --others --exclude-standard; } | sort -u)
pytest_run() {
if [[ -f uv.lock ]]; then uv run pytest "$@"
elif [[ -f poetry.lock ]]; then poetry run pytest "$@"
else python3 -m pytest "$@"; fi
}
if grep -qs testmon pyproject.toml requirements*.txt; then
pytest_run --testmon -q --tb=short; exit $?
fi
full=0
tests=()
for f in "${CHANGED[@]}"; do
case "$f" in
*/conftest.py | conftest.py | pyproject.toml | requirements*.txt | pytest.ini | setup.cfg) full=1 ;;
*.py)
base="$(basename "$f" .py)"
if [[ "$base" == test_* || "$base" == *_test ]]; then
tests+=("$f")
else
while IFS= read -r t; do tests+=("$t"); done \
< <(git ls-files "*test_${base}.py" "*${base}_test.py")
fi
;;
esac
done
if [[ $full -eq 0 && ${#CHANGED[@]} -eq 0 ]]; then echo "no changes vs HEAD: nothing to test"; exit 0; fi
if [[ $full -eq 0 && ${#tests[@]} -gt 0 ]]; then pytest_run -q --tb=short "${tests[@]}"; exit $?; fi
if [[ $full -eq 0 ]] && ! printf '%s\n' "${CHANGED[@]}" | grep -q '\.py$'; then
echo "no python changes: nothing to test"; exit 0
fi
echo "no mapped tests: running full suite"
pytest_run -q --tb=short

View File

@ -0,0 +1,25 @@
name: repo-contract
# The local pre-commit hook can be skipped (a fresh clone has no hooks), so the
# contract is enforced on push too. Mirrors md-naming.yml.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
jobs:
repo-contract:
name: repo contract
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check out the shared gate
uses: actions/checkout@v4
with:
repository: kuhyx/utils
path: .utils
- name: Enforce the repo contract
run: bash .utils/scripts/check_repo_contract.sh

21
repo_contract/templates/rust.sh Executable file
View File

@ -0,0 +1,21 @@
#!/bin/bash
# ============================================================================
# Run only the tests related to files changed vs HEAD (staged, unstaged and
# untracked). Quiet: failures plus a one-line summary. Falls back to the full
# suite when a change cannot be mapped to tests. Generated by repo_contract.
# ============================================================================
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
CHANGED=()
while IFS= read -r f; do
[[ -n "$f" && -e "$f" ]] && CHANGED+=("$f")
done < <({ git diff --name-only HEAD 2>/dev/null || true; git ls-files --others --exclude-standard; } | sort -u)
# cargo cannot map files to tests without extra tooling: any rust change runs the suite.
if ! printf '%s\n' "${CHANGED[@]:-}" | grep -qE '\.rs$|Cargo\.(toml|lock)$'; then
echo "no rust changes: nothing to test"; exit 0
fi
cargo test --quiet

View File

@ -0,0 +1,47 @@
#!/bin/bash
# ============================================================================
# Run only the tests related to files changed vs HEAD (staged, unstaged and
# untracked). Quiet: failures plus a one-line summary. Falls back to the full
# suite when a change cannot be mapped to tests. Generated by repo_contract.
# ============================================================================
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
CHANGED=()
while IFS= read -r f; do
[[ -n "$f" && -e "$f" ]] && CHANGED+=("$f")
done < <({ git diff --name-only HEAD 2>/dev/null || true; git ls-files --others --exclude-standard; } | sort -u)
tests=()
for f in "${CHANGED[@]}"; do
case "$f" in
tests/*.bats | test/*.bats) tests+=("$f") ;;
tests/test_*.sh | test/test_*.sh) tests+=("$f") ;;
*.sh)
base="$(basename "$f" .sh)"
for cand in "tests/${base}.bats" "test/${base}.bats" "tests/test_${base}.sh"; do
[[ -f "$cand" ]] && tests+=("$cand")
done
;;
esac
done
run_tests() {
local rc=0 out
for t in "$@"; do
if [[ "$t" == *.bats ]]; then out="$(bats -F tap "$t" 2>&1)" || rc=1
else out="$(bash "$t" 2>&1)" || rc=1; fi
grep -v '^ok ' <<<"$out" | grep -v '^1\.\.' || true
done
echo "ran ${#} test file(s): $([[ $rc -eq 0 ]] && echo pass || echo FAIL)"
return "$rc"
}
if [[ ${#CHANGED[@]} -eq 0 ]]; then echo "no changes vs HEAD: nothing to test"; exit 0; fi
if [[ ${#tests[@]} -gt 0 ]]; then run_tests "${tests[@]}"; exit $?; fi
mapfile -t all < <(git ls-files 'tests/*.bats' 'test/*.bats' 'tests/test_*.sh' 'test/test_*.sh')
if [[ ${#all[@]} -eq 0 ]]; then echo "no shell tests found"; exit 1; fi
echo "no mapped tests: running full suite"
run_tests "${all[@]}"

View File

View File

@ -0,0 +1,49 @@
"""Shared fixtures for the repo_contract tests."""
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
GOOD = """# x
## Commands
- run: n/a: library
- test: `python3 -m pytest -q`
- test-changed: `scripts/test_changed.sh`
- lint: `ruff check .`
- coverage: `python3 -m pytest -q --cov --cov-report=xml`
- coverage-gaps: `coverage-gaps coverage.xml`
"""
@pytest.fixture
def repo(tmp_path: Path) -> Path:
"""A throwaway git repo."""
subprocess.run(["git", "init", "-q", str(tmp_path)], check=True)
return tmp_path
def write(root: Path, rel: str, body: str = "x\n", mode: int | None = None) -> Path:
"""Create `rel` under `root` (parents included)."""
path = root / rel
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(body, encoding="utf-8")
if mode is not None:
path.chmod(mode)
return path
def good_repo(root: Path) -> Path:
"""Make `root` satisfy the contract (python stack with tests)."""
write(root, "CLAUDE.md", GOOD)
write(root, "scripts/test_changed.sh", "#!/bin/bash\npython3 -m pytest -q\n", 0o755)
write(root, "pyproject.toml", "[project]\nname='x'\n")
write(root, "tests/test_a.py", "def test_a():\n pass\n")
return root

View File

@ -0,0 +1,108 @@
"""Bootstrap: proposals, appends, never overwriting."""
from __future__ import annotations
from pathlib import Path
import pytest
from repo_contract.bootstrap import (
doc_path,
insert_lines,
main,
missing_lines,
script_plan,
)
from repo_contract.check import violations
from repo_contract.infer import propose
from .conftest import write
def test_insert_lines_variants() -> None:
assert insert_lines("# t\n", []) == "# t\n"
assert insert_lines("", ["- a: b"]) == "## Commands\n\n- a: b\n"
assert insert_lines("# t", ["- a: b"]) == "# t\n\n## Commands\n\n- a: b\n"
assert insert_lines("# t\n", ["- a: b"]) == "# t\n\n## Commands\n\n- a: b\n"
assert insert_lines("# t\n\n", ["- a: b"]) == "# t\n\n## Commands\n\n- a: b\n"
got = insert_lines("## Commands\n- run: `x`\n\n## After\ntext\n", ["- test: `y`"])
assert got == "## Commands\n- run: `x`\n- test: `y`\n\n## After\ntext\n"
assert insert_lines("## Commands\n- run: `x`\n", ["- t: `y`"]).endswith(
"- t: `y`\n"
)
def test_doc_path(repo: Path) -> None:
assert doc_path(repo) == repo / "CLAUDE.md"
write(repo, "AGENTS.md")
assert doc_path(repo) == repo / "AGENTS.md"
(repo / "CLAUDE.md").symlink_to("AGENTS.md")
assert doc_path(repo) == (repo / "AGENTS.md").resolve()
def test_missing_lines_skips_existing(repo: Path) -> None:
write(repo, "tests/test_a.py")
prop = propose(repo, ["python"])
got = missing_lines(prop, "## Commands\n- test: `mine`\n")
assert all(not ln.startswith("- test:") for ln in got)
assert any(ln.startswith("- lint:") for ln in got)
def test_script_plan(repo: Path) -> None:
write(repo, "tests/test_a.py")
prop = propose(repo, ["python"])
assert script_plan(repo, prop) is not None
write(repo, "scripts/test_changed.sh")
assert script_plan(repo, prop) is None
prop.stack = "unknown"
(repo / "scripts/test_changed.sh").unlink()
assert script_plan(repo, prop) is None
def test_not_a_repo(tmp_path: Path) -> None:
assert main(["--repo", str(tmp_path)]) == 2
def test_dry_run_changes_nothing(
repo: Path, capsys: pytest.CaptureFixture[str]
) -> None:
write(repo, "pyproject.toml")
write(repo, "tests/test_a.py")
assert main(["--repo", str(repo)]) == 0
out = capsys.readouterr().out
assert "- test: `python3 -m pytest -q`" in out
assert not (repo / "CLAUDE.md").exists()
assert not (repo / "scripts").exists()
def test_write_makes_repo_pass_and_is_idempotent(
repo: Path, capsys: pytest.CaptureFixture[str]
) -> None:
write(repo, "pyproject.toml", "[tool.ruff]\n")
write(repo, "tests/test_a.py")
write(repo, "CLAUDE.md", "# Mine\n\nKeep this.\n")
assert main(["--repo", str(repo), "--write"]) == 0
text = (repo / "CLAUDE.md").read_text(encoding="utf-8")
assert text.startswith("# Mine\n\nKeep this.\n") and "## Commands" in text
assert (repo / "scripts/test_changed.sh").stat().st_mode & 0o111
assert violations(repo)[0] == []
capsys.readouterr()
assert main(["--repo", str(repo), "--write"]) == 0
out = capsys.readouterr().out
assert "already complete" in out and "none to create" in out
assert (repo / "CLAUDE.md").read_text(encoding="utf-8") == text
def test_write_symlinks_claude_to_agents(repo: Path) -> None:
write(repo, "AGENTS.md", "# a\n")
write(repo, "pyproject.toml")
assert main(["--repo", str(repo), "--write"]) == 0
assert (repo / "CLAUDE.md").is_symlink()
assert "## Commands" in (repo / "AGENTS.md").read_text(encoding="utf-8")
def test_write_new_claude_md(repo: Path) -> None:
write(repo, "project.godot")
assert main(["--repo", str(repo), "--write"]) == 0
assert (repo / "CLAUDE.md").is_file()
assert violations(repo)[0] == ["'- lint:' still holds a placeholder: <FILL IN>"]

View File

@ -0,0 +1,134 @@
"""The gate end to end."""
from __future__ import annotations
from pathlib import Path
import pytest
from repo_contract.check import Unreadable, main, read_doc, violations
from .conftest import GOOD, good_repo, write
def problems(repo: Path) -> list[str]:
return violations(repo)[0]
def test_good_repo_passes(repo: Path, capsys: pytest.CaptureFixture[str]) -> None:
good_repo(repo)
assert main(["--repo", str(repo)]) == 0
assert capsys.readouterr().err == ""
def test_exit_2_when_not_repo(tmp_path: Path) -> None:
assert main(["--repo", str(tmp_path)]) == 2
assert main(["--repo", str(tmp_path / "nope")]) == 2
def test_exit_2_when_doc_unreadable(repo: Path) -> None:
write(repo, "CLAUDE.md").write_bytes(b"\xff\xfe\x00bad")
assert main(["--repo", str(repo)]) == 2
with pytest.raises(Unreadable):
read_doc(repo)
def test_exit_1_prints_each(repo: Path, capsys: pytest.CaptureFixture[str]) -> None:
assert main(["--repo", str(repo)]) == 1
assert "CLAUDE.md missing" in capsys.readouterr().err
def test_missing_doc_variants(repo: Path) -> None:
assert "or AGENTS.md" in problems(repo)[0]
write(repo, "AGENTS.md", GOOD)
assert "symlink it to AGENTS.md" in problems(repo)[0]
(repo / "CLAUDE.md").symlink_to("nowhere.md")
assert "dangling" in problems(repo)[0]
def test_symlinked_agents_passes(repo: Path) -> None:
good_repo(repo)
(repo / "AGENTS.md").write_text(GOOD, encoding="utf-8")
(repo / "CLAUDE.md").unlink()
(repo / "CLAUDE.md").symlink_to("AGENTS.md")
assert problems(repo) == []
def test_no_section_and_missing_keys(repo: Path) -> None:
write(repo, "CLAUDE.md", "# x\n")
assert "no '## Commands'" in problems(repo)[0]
write(repo, "CLAUDE.md", "## Commands\n- test: `pytest -q`\n")
assert "missing: - run:, - test-changed:" in problems(repo)[0]
def test_script_rules(repo: Path) -> None:
good_repo(repo)
(repo / "scripts/test_changed.sh").chmod(0o644)
assert "not executable" in problems(repo)[0]
write(repo, "scripts/test_changed.sh", "pytest -v\n", 0o755)
assert "verbosely" in problems(repo)[0]
(repo / "scripts/test_changed.sh").unlink()
assert "existing changed-files script" in problems(repo)[0]
write(repo, "scripts/test_changed.sh").write_bytes(b"\xff\xfe")
(repo / "scripts/test_changed.sh").chmod(0o755)
with pytest.raises(Unreadable):
violations(repo)
def test_na_test_with_real_tests_fails(repo: Path) -> None:
good_repo(repo)
body = GOOD.replace("`python3 -m pytest -q`", "n/a: none", 1)
write(repo, "CLAUDE.md", body)
out = problems(repo)
assert any("n/a but the repo contains tests" in p for p in out)
assert any("must be n/a when test is n/a" in p for p in out)
def test_godot_without_tests_may_be_na(repo: Path) -> None:
na = "n/a: no tests"
body = "## Commands\n- run: `godot --path .`\n" + "".join(
f"- {k}: {na}\n"
for k in ("test", "test-changed", "lint", "coverage", "coverage-gaps")
)
write(repo, "CLAUDE.md", body)
write(repo, "project.godot")
assert problems(repo) == []
def test_real_test_cannot_have_na_chain(repo: Path) -> None:
good_repo(repo)
body = GOOD.replace("`scripts/test_changed.sh`", "n/a: why")
body = body.replace("`coverage-gaps coverage.xml`", "n/a: why")
write(repo, "CLAUDE.md", body)
out = problems(repo)
assert any("test-changed:' cannot be n/a" in p for p in out)
assert any("must run the shared lister" in p for p in out)
def test_wrong_lister_and_coverage_na_ok(repo: Path) -> None:
good_repo(repo)
write(
repo,
"CLAUDE.md",
GOOD.replace("`coverage-gaps coverage.xml`", "`cat coverage.xml`"),
)
assert any("must start with `coverage-gaps" in p for p in problems(repo))
body = GOOD.replace("`python3 -m pytest -q --cov --cov-report=xml`", "n/a: no cov")
write(
repo, "CLAUDE.md", body.replace("`coverage-gaps coverage.xml`", "n/a: no cov")
)
assert problems(repo) == []
def test_verbose_and_placeholder_reported(repo: Path) -> None:
good_repo(repo)
write(
repo,
"CLAUDE.md",
GOOD.replace("pytest -q`", "pytest -v`", 1).replace(
"ruff check .", "<FILL IN>"
),
)
out = problems(repo)
assert any("quiet by default" in p for p in out)
assert any("placeholder" in p for p in out)

View File

@ -0,0 +1,73 @@
"""Parser and entry validation."""
from __future__ import annotations
from pathlib import Path
from repo_contract.contract import (
Entry,
commands_section,
entry_problems,
has_section,
is_verbose,
parse_entries,
script_token,
verbose_in_script,
)
from .conftest import GOOD, write
def test_section_extraction_respects_fences_and_next_heading() -> None:
text = "# t\n## Commands\n- a: b\n```\n## Other\n```\n## Next\n- c: d\n"
assert commands_section(text) == ["- a: b", "```", "## Other", "```"]
assert has_section(text)
assert not has_section("```\n## Commands\n```\n")
assert not has_section("## Commandsx\n")
def test_parse_good() -> None:
entries, problems = parse_entries(GOOD)
assert not problems
assert entries["run"] == Entry("run", None, "library")
assert entries["test"].command == "python3 -m pytest -q"
def test_parse_na_forms_and_errors() -> None:
text = (
"## Commands\n- run: n/a (a game)\n- test: n/a\n- lint: n/a: \n"
"- coverage: plain words\n- test: `x`\n- lint: `a`\n- lint: `b`\n- other: `z`\nprose\n"
)
entries, problems = parse_entries(text)
assert entries["run"].na_reason == "a game"
assert any("must carry a reason" in p for p in problems)
assert any("needs a backticked command" in p for p in problems)
assert any("duplicate '- lint:'" in p for p in problems)
assert "other" not in entries
def test_verbosity() -> None:
assert is_verbose("pytest -v")
assert is_verbose("pytest -vv x")
assert is_verbose("tool --verbose")
assert is_verbose("flutter test --reporter expanded")
assert not is_verbose("pytest -q")
assert is_verbose("echo 'unterminated -v")
assert not verbose_in_script("# pytest -v\ngrep -v x\npytest -q\n")
assert verbose_in_script("python3 -m pytest -v\n")
def test_entry_problems() -> None:
assert entry_problems(Entry("run", None, "lib")) == []
assert entry_problems(Entry("test", "pytest -q", None)) == []
assert entry_problems(Entry("run", "go run -v .", None)) == []
assert len(entry_problems(Entry("test", "pytest -v", None))) == 1
assert "placeholder" in entry_problems(Entry("lint", "<FILL IN>", None))[0]
def test_script_token(tmp_path: Path) -> None:
write(tmp_path, "scripts/t.sh")
assert script_token("bash scripts/t.sh", tmp_path) == tmp_path / "scripts/t.sh"
assert script_token("scripts/missing.sh", tmp_path) is None
assert script_token("pytest -q", tmp_path) is None
assert script_token("echo 'bad scripts/t.sh", tmp_path) == tmp_path / "scripts/t.sh"

View File

@ -0,0 +1,121 @@
"""Command inference."""
from __future__ import annotations
from pathlib import Path
from repo_contract.infer import propose, quiet, stack_defaults
from .conftest import write
def test_quiet() -> None:
assert quiet("pytest -v tests") == "pytest -q tests"
assert quiet("pytest -q") == "pytest -q"
assert quiet("flutter test") == "flutter test --reporter=compact"
assert quiet("flutter test -r expanded") == "flutter test -r expanded"
assert quiet("x --reporter=expanded") == "x "
assert quiet("go test ./...") == "go test ./..."
def test_python_defaults_and_launchers(tmp_path: Path) -> None:
write(tmp_path, "pyproject.toml", "[tool.ruff]\n")
assert stack_defaults(tmp_path, "python")["lint"] == "ruff check ."
write(tmp_path, "uv.lock")
assert stack_defaults(tmp_path, "python")["test"] == "uv run pytest -q"
assert stack_defaults(tmp_path, "python")["lint"] == "uv run ruff check ."
(tmp_path / "uv.lock").unlink()
write(tmp_path, "poetry.lock")
assert stack_defaults(tmp_path, "python")["test"].startswith("poetry run")
def test_python_no_ruff_and_run_default(tmp_path: Path) -> None:
write(tmp_path, "pyproject.toml")
write(tmp_path, "tests/test_a.py")
write(tmp_path, "main.py")
prop = propose(tmp_path, ["python"])
assert prop.values["lint"] == "`pre-commit run --all-files`"
assert prop.values["run"] == "`python3 main.py`"
assert prop.values["coverage-gaps"] == "`coverage-gaps coverage.xml`"
assert prop.values["test-changed"] == "`scripts/test_changed.sh`"
assert prop.lines()[0].startswith("- run:")
def test_run_defaults_with_entry_points(tmp_path: Path) -> None:
write(tmp_path, "tests/a.txt")
for stack, marker, expected in (
("go", "main.go", "go run ."),
("rust", "src/main.rs", "cargo run"),
("gradle", "app/x", "./gradlew installDebug"),
("shell", "x.sh", "n/a: collection of scripts"),
):
write(tmp_path, marker)
assert propose(tmp_path, [stack]).values["run"].strip("`").startswith(expected)
def test_library_run_is_na(tmp_path: Path) -> None:
write(tmp_path, "tests/a.txt")
for stack in ("python", "go", "rust", "gradle", "node"):
assert propose(tmp_path, [stack]).values["run"].startswith("n/a: library")
def test_stack_defaults_each(tmp_path: Path) -> None:
write(tmp_path, "tests/a.txt")
write(tmp_path, "pnpm-lock.yaml")
for stack in ("flutter", "node", "go", "rust", "gradle"):
prop = propose(tmp_path, [stack])
assert prop.values["test"].startswith("`")
assert "coverage-gaps " in prop.values["coverage-gaps"]
write(tmp_path, "yarn.lock")
(tmp_path / "pnpm-lock.yaml").unlink()
assert propose(tmp_path, ["node"]).values["test"].startswith("`yarn")
(tmp_path / "yarn.lock").unlink()
assert propose(tmp_path, ["node"]).values["test"].startswith("`npm")
def test_priority_doc_make_helper_npm_ci(tmp_path: Path) -> None:
write(tmp_path, "tests/a.txt")
write(tmp_path, "package.json", '{"scripts": {"test": "x", "lint": "y"}}')
assert propose(tmp_path, ["node"]).values["lint"] == "`npm run lint`"
write(tmp_path, "scripts/lint.sh")
assert propose(tmp_path, ["node"]).values["lint"] == "`scripts/lint.sh`"
write(tmp_path, "Makefile", "lint:\n\tx\n")
assert propose(tmp_path, ["node"]).values["lint"] == "`make lint`"
write(tmp_path, "CLAUDE.md", "Lint with `eslint . -v`.\n")
assert propose(tmp_path, ["node"]).values["lint"] == "`eslint .`"
write(tmp_path, ".github/workflows/a.yml", "- run: go test -v ./...\n")
assert propose(tmp_path, ["go"]).values["test"] == "`go test ./...`"
def test_untested_godot_and_shell_are_na(tmp_path: Path) -> None:
write(tmp_path, "project.godot")
prop = propose(tmp_path, ["godot"])
assert prop.values["test"].startswith("n/a")
assert prop.values["test-changed"].startswith("n/a")
assert any("no tests detected" in n for n in prop.notes)
assert propose(tmp_path, []).stack == "unknown"
def test_godot_with_runner(tmp_path: Path) -> None:
write(tmp_path, "tests/test_a.gd")
write(tmp_path, "addons/gut/gut_cmdln.gd")
prop = propose(tmp_path, ["godot"])
assert "gut_cmdln" in prop.values["test"]
assert prop.values["coverage-gaps"].startswith("n/a")
(tmp_path / "addons/gut/gut_cmdln.gd").unlink()
(tmp_path / "addons/gdUnit4").mkdir()
assert "GdUnitCmdTool" in propose(tmp_path, ["godot"]).values["test"]
(tmp_path / "addons/gdUnit4").rmdir()
assert propose(tmp_path, ["godot"]).values["test"] == "`<FILL IN>`"
def test_unknown_report_and_untested_note(tmp_path: Path) -> None:
write(tmp_path, "scripts/a.sh")
prop = propose(tmp_path, ["python"])
assert any("no tests detected, but" in n for n in prop.notes)
write(tmp_path, "tests/a.txt")
write(tmp_path, "Makefile", "coverage:\n\tx\n")
shell = propose(tmp_path, ["shell"])
assert shell.values["coverage"].startswith("`make coverage")
assert shell.values["coverage-gaps"] == "`<FILL IN>`"
assert any("fill in" in n for n in shell.notes)

View File

@ -0,0 +1,63 @@
"""Command sources: doc, Makefile, package.json, CI."""
from __future__ import annotations
from pathlib import Path
from repo_contract.sources import (
ci_commands,
doc_commands,
first_match,
make_targets,
package_scripts,
read_text,
)
from .conftest import write
def test_read_text_missing(tmp_path: Path) -> None:
assert read_text(tmp_path / "x") == ""
(tmp_path / "d").mkdir()
assert read_text(tmp_path / "d") == ""
def test_doc_commands() -> None:
assert doc_commands("use `pytest -q` and `scripts/x.sh` and `~/a b` and `one`") == [
"pytest -q"
]
def test_make_targets(tmp_path: Path) -> None:
write(tmp_path, "Makefile", "CC := gcc\ntest:\n\tx\nlint-all: y\n")
assert make_targets(tmp_path) == {"test", "lint-all"}
def test_package_scripts(tmp_path: Path) -> None:
assert package_scripts(tmp_path) == {}
write(tmp_path, "package.json", "{bad")
assert package_scripts(tmp_path) == {}
write(tmp_path, "package.json", "[1]")
assert package_scripts(tmp_path) == {}
write(tmp_path, "package.json", '{"scripts": 3}')
assert package_scripts(tmp_path) == {}
write(tmp_path, "package.json", '{"scripts": {"test": "jest"}}')
assert package_scripts(tmp_path) == {"test": "jest"}
def test_ci_commands(tmp_path: Path) -> None:
write(
tmp_path,
".github/workflows/ci.yml",
"steps:\n - run: pytest -q\n - run: |\n multi\n - run: echo ${{ x }}\n run: ruff check .\n",
)
assert ci_commands(tmp_path) == ["pytest -q", "ruff check ."]
def test_first_match() -> None:
cmds = ["flutter test --coverage", "flutter test", "ruff check ."]
assert first_match("test", cmds) == "flutter test"
assert first_match("coverage", cmds) == "flutter test --coverage"
assert first_match("lint", cmds) == "ruff check ."
assert first_match("run", cmds) is None
assert first_match("lint", ["x"]) is None

View File

@ -0,0 +1,76 @@
"""Stack detection and the test-presence probe."""
from __future__ import annotations
from pathlib import Path
import pytest
from repo_contract.stacks import MAX_DEPTH, detect_stacks, has_tests
from .conftest import write
def test_markers_detected(tmp_path: Path) -> None:
write(tmp_path, "pubspec.yaml")
write(tmp_path, "pyproject.toml")
assert detect_stacks(tmp_path) == ["flutter", "python"]
def test_each_marker(tmp_path: Path) -> None:
for name, stack in (
("project.godot", "godot"),
("go.mod", "go"),
("Cargo.toml", "rust"),
("build.gradle.kts", "gradle"),
("package.json", "node"),
):
sub = tmp_path / stack
write(sub, name)
assert detect_stacks(sub) == [stack]
def test_shell_only_and_unknown(tmp_path: Path) -> None:
assert detect_stacks(tmp_path) == []
write(tmp_path, "scripts/a.sh")
assert detect_stacks(tmp_path) == ["shell"]
def test_walk_prunes_and_bounds_depth(tmp_path: Path) -> None:
write(tmp_path, "node_modules/x/y_test.py")
deep = "/".join(["d"] * (MAX_DEPTH + 1))
write(tmp_path, f"{deep}/test_deep.py")
assert not has_tests(tmp_path, [])
def test_has_tests_by_dir_and_name(tmp_path: Path) -> None:
write(tmp_path / "a", "tests/anything.txt")
assert has_tests(tmp_path / "a", [])
write(tmp_path / "b", "src/foo_test.go")
assert has_tests(tmp_path / "b", [])
write(tmp_path / "c", "test_x.py")
assert has_tests(tmp_path / "c", [])
write(tmp_path / "d", "src/a.py")
assert not has_tests(tmp_path / "d", [])
def test_node_test_script(tmp_path: Path) -> None:
write(tmp_path, "package.json", '{"scripts": {"test": "vitest"}}')
assert has_tests(tmp_path, ["node"])
write(tmp_path, "package.json", '{"scripts": {"test": "echo no test specified"}}')
assert not has_tests(tmp_path, ["node"])
assert not has_tests(tmp_path, ["python"])
def test_node_package_json_unreadable(tmp_path: Path) -> None:
(tmp_path / "package.json").mkdir()
assert not has_tests(tmp_path, ["node"])
def test_file_cap(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
from repo_contract import stacks
monkeypatch.setattr(stacks, "MAX_FILES", 2)
for i in range(5):
write(tmp_path, f"f{i}.txt")
assert len(stacks._walk(tmp_path)) >= 2

View File

@ -0,0 +1,45 @@
"""Sweep report."""
from __future__ import annotations
import subprocess
from pathlib import Path
import pytest
from repo_contract.sweep import Result, find_repos, main, render, run_one
from .conftest import good_repo, write
def make_repo(path: Path) -> Path:
path.mkdir(parents=True)
subprocess.run(["git", "init", "-q", str(path)], check=True)
return path
def test_find_repos_skips_dot_and_non_repos(tmp_path: Path) -> None:
make_repo(tmp_path / "a")
make_repo(tmp_path / ".hidden")
(tmp_path / "plain").mkdir()
assert find_repos([tmp_path, tmp_path / "missing"]) == [tmp_path / "a"]
def test_run_one_and_render(tmp_path: Path) -> None:
ok = good_repo(make_repo(tmp_path / "ok"))
bad = make_repo(tmp_path / "bad")
write(bad, "CLAUDE.md").write_bytes(b"\xff\xfe")
results = [run_one(ok), run_one(bad), Result("x", tmp_path, [], ["a|b"])]
assert [r.passed for r in results] == [True, False, False]
text = render(results)
assert "3 repos: 1 pass, 2 fail." in text
assert "| python | 1 | 0 |" in text and "| none | 2 | 2 |" in text
assert "a/b" in text and "unreadable" in text
def test_main_writes_report(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
make_repo(tmp_path / "r")
out = tmp_path / "out" / "SWEEP.md"
assert main(roots=[tmp_path], out=out) == 0
assert "1 repos, 0 pass, 1 fail" in capsys.readouterr().out
assert out.is_file()

View File

@ -0,0 +1,109 @@
"""Run the generated test_changed.sh scripts against real throwaway repos."""
from __future__ import annotations
import subprocess
from pathlib import Path
from repo_contract.bootstrap import TEMPLATES
from .conftest import write
def run(
repo: Path, stack: str, *, env_path: str | None = None
) -> subprocess.CompletedProcess[str]:
subprocess.run(["git", "-C", str(repo), "add", "-A"], check=True)
subprocess.run(
[
"git",
"-C",
str(repo),
"-c",
"user.name=t",
"-c",
"user.email=t@t",
"commit",
"-qm",
"i",
],
check=True,
)
return subprocess.run(
["bash", str(TEMPLATES / f"{stack}.sh")],
cwd=repo,
capture_output=True,
text=True,
check=False,
)
def test_python_runs_only_mapped_tests(repo: Path) -> None:
write(repo, "pkg/a.py", "x = 1\n")
write(repo, "tests/test_a.py", "def test_a():\n assert True\n")
write(repo, "tests/test_b.py", "def test_b():\n assert False\n")
done = run(repo, "python")
assert "nothing to test" in done.stdout and done.returncode == 0
write(repo, "pkg/a.py", "x = 2\n")
done = subprocess.run(
["bash", str(TEMPLATES / "python.sh")],
cwd=repo,
capture_output=True,
text=True,
check=False,
)
assert done.returncode == 0 and "1 passed" in done.stdout
subprocess.run(["git", "-C", str(repo), "add", "-A"], check=True)
subprocess.run(
[
"git",
"-C",
str(repo),
"-c",
"user.name=t",
"-c",
"user.email=t@t",
"commit",
"-qm",
"2",
],
check=True,
)
write(repo, "pkg/c.py", "y = 1\n")
done = subprocess.run(
["bash", str(TEMPLATES / "python.sh")],
cwd=repo,
capture_output=True,
text=True,
check=False,
)
assert "full suite" in done.stdout and done.returncode != 0
def test_python_ignores_non_python_changes(repo: Path) -> None:
write(repo, "README.md")
run(repo, "python")
write(repo, "README.md", "changed\n")
done = subprocess.run(
["bash", str(TEMPLATES / "python.sh")],
cwd=repo,
capture_output=True,
text=True,
check=False,
)
assert "no python changes" in done.stdout
def test_shell_template(repo: Path) -> None:
write(repo, "scripts/a.sh", "echo a\n")
write(repo, "tests/test_a.sh", "exit 0\n")
run(repo, "shell")
write(repo, "scripts/a.sh", "echo b\n")
done = subprocess.run(
["bash", str(TEMPLATES / "shell.sh")],
cwd=repo,
capture_output=True,
text=True,
check=False,
)
assert done.returncode == 0 and "pass" in done.stdout

25
scripts/check_repo_contract.sh Executable file
View File

@ -0,0 +1,25 @@
#!/bin/bash
# ============================================================================
# Fail if the current repo breaks the repo contract: CLAUDE.md `## Commands`
# with run / test / test-changed / lint / coverage / coverage-gaps entries and
# a quiet scripts/test_changed.sh. See repo_contract/README.md.
#
# This is the REAL gate. Per-repo scripts/check_repo_contract.sh files are thin
# shims that exec this one -- do not overwrite this file with a shim.
#
# Usage: scripts/check_repo_contract.sh [--repo PATH] # default: cwd
# Exit: 0 pass, 1 violations, 2 not a repo / unreadable.
# ============================================================================
set -euo pipefail
UTILS_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
readonly UTILS_ROOT
main() {
PYTHONPATH="$UTILS_ROOT${PYTHONPATH:+:$PYTHONPATH}" \
python3 "$UTILS_ROOT/repo_contract/check.py" "$@"
}
main "$@"

48
scripts/test_changed.sh Executable file
View File

@ -0,0 +1,48 @@
#!/bin/bash
# ============================================================================
# Run only the tests related to files changed vs HEAD (staged, unstaged and
# untracked). Quiet: failures plus a one-line summary. Maps a change to its
# top-level directory: `<dir>/tests` runs with PYTHONPATH=., a subproject with
# its own pyproject.toml runs via scripts/run_subproject_tests.sh. Anything
# unmappable falls back to the shared-gate suites.
# ============================================================================
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
CHANGED=()
while IFS= read -r f; do
[[ -n "$f" && -e "$f" ]] && CHANGED+=("$f")
done < <({ git diff --name-only HEAD 2>/dev/null || true; git ls-files --others --exclude-standard; } | sort -u)
readonly GATES=(coverage_gaps dep_freshness file_length home_paths md_naming repo_contract)
dirs=()
unmapped=0
for f in "${CHANGED[@]}"; do
d="${f%%/*}"
case "$f" in
*.py | *.sh | *.yaml | *.yml | *.toml | *.json) ;;
*) continue ;;
esac
if [[ "$d" == "$f" ]]; then unmapped=1; continue; fi
if [[ -d "$d/tests" || -f "$d/pyproject.toml" ]]; then dirs+=("$d"); else unmapped=1; fi
done
mapfile -t dirs < <(printf '%s\n' "${dirs[@]}" | sort -u)
if [[ ${#CHANGED[@]} -eq 0 ]]; then echo "no changes vs HEAD: nothing to test"; exit 0; fi
if [[ ${#dirs[@]} -eq 0 && $unmapped -eq 0 ]]; then echo "no code changes: nothing to test"; exit 0; fi
rc=0
if [[ ${#dirs[@]} -eq 0 ]]; then
echo "no mapped tests: running shared-gate suites"
dirs=("${GATES[@]}")
fi
for d in "${dirs[@]}"; do
if [[ -f "$d/pyproject.toml" ]]; then
scripts/run_subproject_tests.sh "$d" -q --tb=short || rc=$?
elif [[ -d "$d/tests" ]]; then
PYTHONPATH=. python3 -m pytest -q --tb=short "$d/tests" || rc=$?
fi
done
exit $rc

View File

@ -72,14 +72,15 @@ needs them rewritten before the rule can go on. That is a script, not a
session's worth of hand edits: session's worth of hand edits:
```sh ```sh
node ~/src/utils/ts_config/scripts/alias-imports.mjs <repo-root> [src-dir] node ~/src/utils/ts_config/scripts/alias-imports.mjs <repo-root> [src-dir] [extra-dir...]
``` ```
Every `import` / `export … from` / `import()` specifier under `src/` that Every `import` / `export … from` / `import()` specifier under `src/` that
starts with `../` and resolves inside `src/` becomes `@/<path from src>`; starts with `../` and resolves inside `src/` becomes `@/<path from src>`;
`./sibling`, package imports and a `../` that leaves `src/` are left alone `./sibling`, package imports and a `../` that leaves `src/` are left alone
(the last one is the lint report's to surface). Extensions are carried over (the last one is the lint report's to surface). Extensions are carried over
untouched. Idempotent. The repo still needs the alias itself: `paths` in untouched. Idempotent. Extra directories (`tests`, `scripts`) are walked
too and aliased against `src`. The repo still needs the alias itself: `paths` in
tsconfig (see above) and `resolve.alias` in vite.config, which vitest reads. tsconfig (see above) and `resolve.alias` in vite.config, which vitest reads.
`test/alias-imports.test.js` runs it over a fixture tree. `test/alias-imports.test.js` runs it over a fixture tree.

View File

@ -6,5 +6,13 @@ import { defineConfig } from "./eslint/index.js";
export default defineConfig({ export default defineConfig({
aliasOnly: false, aliasOnly: false,
ignores: ["test/fixture"], ignores: ["test/fixture"],
overrides: [
{
// fast.js IS a preset: its default export is the built config, so the
// call at the top level is the module's whole purpose.
files: ["eslint/fast.js"],
rules: { "unicorn/no-top-level-side-effects": "off" },
},
],
tsconfigRootDir: import.meta.dirname, tsconfigRootDir: import.meta.dirname,
}); });

View File

@ -11,6 +11,13 @@
/** @type {import("eslint").Linter.Config[]} */ /** @type {import("eslint").Linter.Config[]} */
export const overrides = [ export const overrides = [
{
// A tool config file exists to `export default` the result of a call
// (`defineConfig(...)`); that is its entire job, not a hidden side
// effect, so the module-purity rule has nothing to protect here.
files: ["**/*.config.{js,mjs,cjs,ts,mts,cts}"],
rules: { "unicorn/no-top-level-side-effects": "off" },
},
{ {
// A package's entry point IS a barrel: its whole job is to name the // A package's entry point IS a barrel: its whole job is to name the
// public API. The rule stays on everywhere else, where a barrel is how // public API. The rule stays on everywhere else, where a barrel is how

View File

@ -9,7 +9,7 @@
"node": ">=24", "node": ">=24",
"pnpm": ">=10" "pnpm": ">=10"
}, },
"packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec", "packageManager": "pnpm@12.9.0+sha512.8f64efa0b792986ca98ae568791bcd7722807e274c4f0c1d9853c62116495c584119db98762a2a5343b1684d361b9ae26fdd16d551666d7635346d3fae830ddd",
"files": [ "files": [
"eslint", "eslint",
"tsconfig.base.json", "tsconfig.base.json",
@ -39,7 +39,7 @@
"coverage": "vitest run --coverage" "coverage": "vitest run --coverage"
}, },
"dependencies": { "dependencies": {
"@eslint-react/eslint-plugin": "5.20.8", "@eslint-react/eslint-plugin": "5.23.5",
"@eslint/js": "10.0.1", "@eslint/js": "10.0.1",
"@vitest/eslint-plugin": "1.6.27", "@vitest/eslint-plugin": "1.6.27",
"eslint-import-resolver-typescript": "4.4.5", "eslint-import-resolver-typescript": "4.4.5",
@ -50,9 +50,9 @@
"eslint-plugin-react-hooks": "7.1.1", "eslint-plugin-react-hooks": "7.1.1",
"eslint-plugin-react-refresh": "0.5.7", "eslint-plugin-react-refresh": "0.5.7",
"eslint-plugin-react-you-might-not-need-an-effect": "1.0.2", "eslint-plugin-react-you-might-not-need-an-effect": "1.0.2",
"eslint-plugin-sonarjs": "4.2.1", "eslint-plugin-sonarjs": "4.2.2",
"eslint-plugin-unicorn": "76.0.0", "eslint-plugin-unicorn": "77.0.0",
"globals": "17.12.0" "globals": "17.13.0"
}, },
"peerDependencies": { "peerDependencies": {
"eslint": ">=10", "eslint": ">=10",
@ -62,11 +62,11 @@
"devDependencies": { "devDependencies": {
"@playwright/test": "1.63.0", "@playwright/test": "1.63.0",
"@types/react": "19.3.0", "@types/react": "19.3.0",
"@vitest/coverage-v8": "5.0.2", "@vitest/coverage-v8": "5.0.3",
"eslint": "10.11.0", "eslint": "10.12.0",
"react": "19.3.0", "react": "19.3.0",
"typescript": "6.0.3", "typescript": "6.0.3",
"typescript-eslint": "8.70.0", "typescript-eslint": "8.71.0",
"vitest": "5.0.2" "vitest": "5.0.3"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -2,3 +2,16 @@ allowBuilds:
unrs-resolver: false unrs-resolver: false
minimumReleaseAgeExclude: minimumReleaseAgeExclude:
- eslint-plugin-unicorn@75.0.0 - eslint-plugin-unicorn@75.0.0
- '@eslint-react/ast@5.23.3'
- '@eslint-react/core@5.23.3'
- '@eslint-react/eslint-plugin@5.23.3'
- '@eslint-react/eslint@5.23.3'
- '@eslint-react/jsx@5.23.3'
- '@eslint-react/shared@5.23.3'
- '@eslint-react/var@5.23.3'
- eslint-plugin-react-dom@5.23.3
- eslint-plugin-react-jsx@5.23.3
- eslint-plugin-react-naming-convention@5.23.3
- eslint-plugin-react-rsc@5.23.3
- eslint-plugin-react-web-api@5.23.3
- eslint-plugin-react-x@5.23.3

View File

@ -69,20 +69,27 @@ export function rewriteSource(file, text, sourceDirectory) {
} }
/** /**
* Rewrite every source file under `<root>/<source>` in place. * Rewrite every source file under `<root>/<source>` in place -- and under
* each extra directory, whose imports are still aliased against `source`
* (a `tests/` tree importing `../src/x` becomes `@/x`).
* *
* @param {string} root Repo root. * @param {string} root Repo root.
* @param {string} [source] Aliased directory, relative to root. * @param {string} [source] Aliased directory, relative to root.
* @param {string[]} [extra] More directories to walk, relative to root.
* @returns {{ files: number, imports: number }} What changed. * @returns {{ files: number, imports: number }} What changed.
*/ */
export function aliasImports(root, source = "src") { export function aliasImports(root, source = "src", extra = []) {
const sourceDirectory = path.resolve(root, source); const sourceDirectory = path.resolve(root, source);
if (!fs.existsSync(sourceDirectory)) { const walked = [sourceDirectory, ...extra.map((directory) => path.resolve(root, directory))];
throw new Error(`alias-imports: no such directory ${sourceDirectory}`); for (const directory of walked) {
if (!fs.existsSync(directory)) {
throw new Error(`alias-imports: no such directory ${directory}`);
}
} }
let files = 0; let files = 0;
let imports = 0; let imports = 0;
for (const file of listSourceFiles(sourceDirectory)) { const candidates = walked.flatMap((directory) => listSourceFiles(directory));
for (const file of candidates) {
const before = fs.readFileSync(file, "utf8"); const before = fs.readFileSync(file, "utf8");
const { count, text } = rewriteSource(file, before, sourceDirectory); const { count, text } = rewriteSource(file, before, sourceDirectory);
if (count === 0) { if (count === 0) {
@ -96,19 +103,20 @@ export function aliasImports(root, source = "src") {
} }
/** /**
* CLI body: `alias-imports <repo-root> [src-dir]`. Returns the exit code * CLI body: `alias-imports <repo-root> [src-dir] [extra-dir...]`. Returns
* and writes the one-line report, so the shim stays at two lines. * the exit code and writes the one-line report, so the shim stays at two
* lines.
* *
* @param {string[]} argv Arguments after the script name. * @param {string[]} argv Arguments after the script name.
* @param {{ log: (line: string) => void }} [io] * @param {{ log: (line: string) => void }} [io]
*/ */
export function main(argv, io = console) { export function main(argv, io = console) {
const [root, source] = argv; const [root, source, ...extra] = argv;
if (root === undefined) { if (root === undefined) {
io.log("usage: alias-imports <repo-root> [src-dir=src]"); io.log("usage: alias-imports <repo-root> [src-dir=src] [extra-dir...]");
return 2; return 2;
} }
const { files, imports } = aliasImports(root, source); const { files, imports } = aliasImports(root, source, extra);
io.log(`alias-imports: rewrote ${imports} import(s) in ${files} file(s) under ${path.resolve(root, source ?? "src")}`); io.log(`alias-imports: rewrote ${imports} import(s) in ${files} file(s) under ${path.resolve(root, source ?? "src")}`);
return 0; return 0;
} }

View File

@ -28,6 +28,7 @@ const FIXTURE = {
"src/services/api.ts": 'import { format } from "../utils/format";\nexport const api = format;\n', "src/services/api.ts": 'import { format } from "../utils/format";\nexport const api = format;\n',
"src/services/notes.md": "not source: [x](../utils/format)\n", "src/services/notes.md": "not source: [x](../utils/format)\n",
"src/utils/format.ts": "export const format = 1;\n", "src/utils/format.ts": "export const format = 1;\n",
"tests/api.test.ts": 'import { api } from "../src/services/api";\nimport { fixture } from "./fixture";\nexport const t = api(fixture);\n',
"tooling/outside.ts": "export const outside = 1;\n", "tooling/outside.ts": "export const outside = 1;\n",
}; };
@ -111,8 +112,15 @@ describe("aliasImports", () => {
expect(aliasImports(rootOf(), "lib")).toEqual({ files: 2, imports: 6 }); expect(aliasImports(rootOf(), "lib")).toEqual({ files: 2, imports: 6 });
}); });
it("fails loudly on a missing source dir", () => { it("walks extra directories, aliasing them against src", () => {
expect(aliasImports(rootOf(), "src", ["tests"])).toEqual({ files: 3, imports: 7 });
expect(read("tests/api.test.ts")).toContain('from "@/services/api"');
expect(read("tests/api.test.ts")).toContain('from "./fixture"');
});
it("fails loudly on a missing source or extra dir", () => {
expect(() => aliasImports(rootOf(), "nope")).toThrow(/no such directory/); expect(() => aliasImports(rootOf(), "nope")).toThrow(/no such directory/);
expect(() => aliasImports(rootOf(), "src", ["nope"])).toThrow(/no such directory/);
}); });
}); });
@ -130,8 +138,8 @@ describe("main", () => {
}); });
it("runs as a CLI", () => { it("runs as a CLI", () => {
const stdout = execFileSync(process.execPath, [CLI, rootOf()], { encoding: "utf8" }); const stdout = execFileSync(process.execPath, [CLI, rootOf(), "src", "tests"], { encoding: "utf8" });
expect(stdout).toContain("rewrote 6 import(s) in 2 file(s)"); expect(stdout).toContain("rewrote 7 import(s) in 3 file(s)");
expect(read("src/services/api.ts")).toContain("@/utils/format"); expect(read("src/services/api.ts")).toContain("@/utils/format");
}); });
}); });

View File

@ -9,7 +9,7 @@
"node": ">=24", "node": ">=24",
"pnpm": ">=10" "pnpm": ">=10"
}, },
"packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec", "packageManager": "pnpm@12.9.0+sha512.8f64efa0b792986ca98ae568791bcd7722807e274c4f0c1d9853c62116495c584119db98762a2a5343b1684d361b9ae26fdd16d551666d7635346d3fae830ddd",
"files": [ "files": [
"dist" "dist"
], ],
@ -30,11 +30,11 @@
"devDependencies": { "devDependencies": {
"@eslint/js": "10.0.1", "@eslint/js": "10.0.1",
"@kuhyx/ts-config": "link:../ts_config", "@kuhyx/ts-config": "link:../ts_config",
"@types/node": "26.6.3", "@types/node": "26.6.4",
"@vitest/coverage-v8": "5.0.2", "@vitest/coverage-v8": "5.0.3",
"eslint": "10.11.0", "eslint": "10.12.0",
"typescript": "6.0.3", "typescript": "6.0.3",
"typescript-eslint": "8.70.0", "typescript-eslint": "8.71.0",
"vitest": "5.0.2" "vitest": "5.0.3"
} }
} }

View File

@ -7,153 +7,153 @@ importers:
configDependencies: {} configDependencies: {}
packageManagerDependencies: packageManagerDependencies:
pnpm: pnpm:
specifier: 12.6.0 specifier: 12.9.0
version: 12.6.0 version: 12.9.0
packages: packages:
'@pnpm/exe.android-arm64@12.6.0': '@pnpm/exe.android-arm64@12.9.0':
resolution: {integrity: sha512-kviIHft9h02q+7N2In7tSL9T/HRUSGwYmU74YPtsWU5ee7vgqPyC0JT6RRX6miW+NxX1CvHgQxg69qq1qTLJYQ==} resolution: {integrity: sha512-/53fHFwP1mBYQjli6h3LBZsocAMjrEY6hCDiomIwdLIFbtxHrWucPg/s8ozOG3K6Qt8yEA78v7fCcNrgZKTlEQ==}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
'@pnpm/exe.android-x64@12.6.0': '@pnpm/exe.android-x64@12.9.0':
resolution: {integrity: sha512-CT8aJKLq2mtZFE71pr4E5Z2xHL8uGnRGr+NclDqsXlVF4SVcQ2QAs14mWi0C8gjT4hbmi+Q0lJRRIkMAWfjjng==} resolution: {integrity: sha512-QI+Md3q3Df0WbDGpqciRw9ge0r7CGbxSlP3HcyIf34/YdZsbmUTl416oWOe3RrkZpVVVCA20f8B9esYrITpArA==}
cpu: [x64] cpu: [x64]
os: [android] os: [android]
'@pnpm/exe.darwin-arm64@12.6.0': '@pnpm/exe.darwin-arm64@12.9.0':
resolution: {integrity: sha512-rafpVkjzugKBMxSvGQd5wK1x5eOTkD/+lcJIFHeQhvd+BJ8kDisOOvwhQDrpGd4vd2Fx+hhW0P2Ptt638OlhFg==} resolution: {integrity: sha512-mWr7fGPYJ12GuvU/q0SyJfj7X/9Eqa5dA6cc+yUUAKRdYEcqW4lQcsDd1ArfxJNSDYooz9X+UhFeVxKO9e/xDA==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@pnpm/exe.darwin-x64@12.6.0': '@pnpm/exe.darwin-x64@12.9.0':
resolution: {integrity: sha512-72Jpuv1m24gI8zUNcaylYpBWEhBzz1VPhnDIU2GYM2eRP+KsOqVLwV78i+JrihI39zY2NNXmaN4eAKX1inpaDQ==} resolution: {integrity: sha512-fr4ns4FyMLptdYbWqW3yc/yvbfQP7D1RBCFUJpSPr7tOzd3JxZvl+Tkvg65kEAGU5wXI6vl3eCOofumEj/n3Gw==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@pnpm/exe.freebsd-x64@12.6.0': '@pnpm/exe.freebsd-x64@12.9.0':
resolution: {integrity: sha512-LON4QgNy1w/XF4uySZIyC/5hEB0oGdelXsY9tSKqstER+2j2X2oHz67skD11RRd/HWFRtMr64shQZ4I9ZoZ0zg==} resolution: {integrity: sha512-4+hZPeJzSSw1oOrfUg+v4QylHv6qKhdgcCnKt9ooryE79p1Z9oyozVJ5em+g6ajbvv6jlH+epnht8/s9Cp5M3Q==}
cpu: [x64] cpu: [x64]
os: [freebsd] os: [freebsd]
'@pnpm/exe.linux-arm64-musl@12.6.0': '@pnpm/exe.linux-arm64-musl@12.9.0':
resolution: {integrity: sha512-BdDpX+DeaMUc5x6WNcE6FkmFy36NwrBbBvwsZU737Zt3u1fTY+iKFPchjEQNCM4rT6q6xvF2oN3ZwExGsJCkxQ==} resolution: {integrity: sha512-+mHQSbA6M6W71PyV0IxYu2T43lZA8rAhlw4Gy7EMlyiv8H2GWJAXtUXl2huw7tnsar2AhCRlPn/ZvUv+/mIBkA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-arm64@12.6.0': '@pnpm/exe.linux-arm64@12.9.0':
resolution: {integrity: sha512-8h2sNoIhHDpHDYqZZCh9PXr6krqW10btb0GHVGuw710muDHlFAHMtUG2ogmaHge6zJG1xgY4Y7mwX5g/LWvrYg==} resolution: {integrity: sha512-U1mBoG27+J/HuWYtVZOQrvbiKnTlDsDVzyroKQqHsJaTHTXi0H5Y+fU1GjUyMcNG5r/EXtbhp9iAwQt64uXTxQ==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-ppc64@12.6.0': '@pnpm/exe.linux-ppc64@12.9.0':
resolution: {integrity: sha512-ld6xgcEhFCFrsVsJhbmdmR714PnsEspO/Ij8w0/D7OSiz2hYGu3fZd5tKRxOI5m6H6Gl0/RRFAPncTHbsgXVjQ==} resolution: {integrity: sha512-P5bgei3i6wjlKq6s4PNg/0cvik354upY2wW3YGSfweYTABnAZP2zGzgMc0eO5SHTz934Cbya8Z0YFRdw6MfmCg==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-riscv64@12.6.0': '@pnpm/exe.linux-riscv64@12.9.0':
resolution: {integrity: sha512-HIbImydoC3J8NFt+8MXfhTCf5rO6NjCMyinSAHac0VmV8uCHw+kbQkyAzxV84rzz9ZOzmBOnj0b1N461UIYGsw==} resolution: {integrity: sha512-oNx2uOo2gVrRckEvz7V+FKeq9HOF0rKNHcCyxRRHSXEXpmP6ZMMgJ+iU/m/zfSGp6bAcC5mvAE270FhAeIeKhA==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-s390x@12.6.0': '@pnpm/exe.linux-s390x@12.9.0':
resolution: {integrity: sha512-DjSqT7+BZ/lWcDHwNMuzsRVfyxUimh+D54Xw4tSWbdVVQhtWtNMUyI93LVnwCmsoEpar2gC3SFeAgywx37svKQ==} resolution: {integrity: sha512-OChHS7hOlqdBMtZqjIB1QoZGNfzLPSsvTa2kYanf3qso5Q+KgnROQ+uV4a0wRGC6sdshSA63+Ui8MCylRy6iwA==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.6.0': '@pnpm/exe.linux-x64-musl@12.9.0':
resolution: {integrity: sha512-31lKeGPmRE6xfV6I3VjaEGVFRei14pl/zgoFDK4w+UkbJqOuNl2Ht3O1GuqNIg3Gl14qTEb4kZRAYFHXifDx9A==} resolution: {integrity: sha512-ubZPsDMXS+LEnPa7wZD18E+KTpOAZPt+ms/khfIROt+6Cp0A2kayQA8aIsvV7+jGZD0xoknvaGN/P3v9OisZUg==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-x64@12.6.0': '@pnpm/exe.linux-x64@12.9.0':
resolution: {integrity: sha512-qFWBneHJAJ73W4whtbaFOL1M/7DBC6ILHXuxc7ZPtEhfPuT1zeZiGrmKHoMAfJA+mcm6xhOFljqVTUS+00Jabw==} resolution: {integrity: sha512-ElANYUwQuMfWqMJR4m5L5h9zKjvGKcA8hi2put4J1DMx2smVaNGkGnxkgeIYgMjmF7UFxlZDcL4jItkScgsW4A==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.win32-arm64@12.6.0': '@pnpm/exe.win32-arm64@12.9.0':
resolution: {integrity: sha512-OhfefXEEykZlslSUhR8PPRe6MVPV3Oink/dVfIecE5velpc7j80cZYsHlaDrtRDcm3TPwQHkaRW1SJ1efX9oFg==} resolution: {integrity: sha512-cqnBKZg5e9fFghmYtgJLyZOAsS1EyLeDVJ0qNsjR7hcc+dq1mWRiDo0erqdj2PGoYaVogyF9WejZSJoKvlfXDw==}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@pnpm/exe.win32-x64@12.6.0': '@pnpm/exe.win32-x64@12.9.0':
resolution: {integrity: sha512-L2tuyrD2+Imgxs3VK/ST/2L3xD6vDP0ktJQxM0TaGw7TbAQEMq+RMc8iK6Ew7Id579uzjJe8jSfJID2ZGsOHCA==} resolution: {integrity: sha512-khqst6QGgHdKs2lEmz0N75qN/EoOcEywNQlRPfBrBrq1SytHra6ro/xViueJkVvDOz9gEO8oYcXTdZeAjErjMg==}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
pnpm@12.6.0: pnpm@12.9.0:
resolution: {integrity: sha512-PvaPlRyxEawgS0paFvCy3fDaVqluBBPoHYVdnwtV75JnFHCQKOHNAMQFwsX7e56OxNxGd3yAXQNzwvL/AP0g7A==} resolution: {integrity: sha512-j2TvoLeSmGypiuVoeRvNdyKAfidMTwwdmFPGIRZJXFhBGduYdioqU0OxaE02G5rib90W1VFmbXY1NG0/roMN3Q==}
engines: {node: '>=18.*'} engines: {node: '>=18.*'}
hasBin: true hasBin: true
snapshots: snapshots:
'@pnpm/exe.android-arm64@12.6.0': '@pnpm/exe.android-arm64@12.9.0':
optional: true optional: true
'@pnpm/exe.android-x64@12.6.0': '@pnpm/exe.android-x64@12.9.0':
optional: true optional: true
'@pnpm/exe.darwin-arm64@12.6.0': '@pnpm/exe.darwin-arm64@12.9.0':
optional: true optional: true
'@pnpm/exe.darwin-x64@12.6.0': '@pnpm/exe.darwin-x64@12.9.0':
optional: true optional: true
'@pnpm/exe.freebsd-x64@12.6.0': '@pnpm/exe.freebsd-x64@12.9.0':
optional: true optional: true
'@pnpm/exe.linux-arm64-musl@12.6.0': '@pnpm/exe.linux-arm64-musl@12.9.0':
optional: true optional: true
'@pnpm/exe.linux-arm64@12.6.0': '@pnpm/exe.linux-arm64@12.9.0':
optional: true optional: true
'@pnpm/exe.linux-ppc64@12.6.0': '@pnpm/exe.linux-ppc64@12.9.0':
optional: true optional: true
'@pnpm/exe.linux-riscv64@12.6.0': '@pnpm/exe.linux-riscv64@12.9.0':
optional: true optional: true
'@pnpm/exe.linux-s390x@12.6.0': '@pnpm/exe.linux-s390x@12.9.0':
optional: true optional: true
'@pnpm/exe.linux-x64-musl@12.6.0': '@pnpm/exe.linux-x64-musl@12.9.0':
optional: true optional: true
'@pnpm/exe.linux-x64@12.6.0': '@pnpm/exe.linux-x64@12.9.0':
optional: true optional: true
'@pnpm/exe.win32-arm64@12.6.0': '@pnpm/exe.win32-arm64@12.9.0':
optional: true optional: true
'@pnpm/exe.win32-x64@12.6.0': '@pnpm/exe.win32-x64@12.9.0':
optional: true optional: true
pnpm@12.6.0: pnpm@12.9.0:
optionalDependencies: optionalDependencies:
'@pnpm/exe.android-arm64': 12.6.0 '@pnpm/exe.android-arm64': 12.9.0
'@pnpm/exe.android-x64': 12.6.0 '@pnpm/exe.android-x64': 12.9.0
'@pnpm/exe.darwin-arm64': 12.6.0 '@pnpm/exe.darwin-arm64': 12.9.0
'@pnpm/exe.darwin-x64': 12.6.0 '@pnpm/exe.darwin-x64': 12.9.0
'@pnpm/exe.freebsd-x64': 12.6.0 '@pnpm/exe.freebsd-x64': 12.9.0
'@pnpm/exe.linux-arm64': 12.6.0 '@pnpm/exe.linux-arm64': 12.9.0
'@pnpm/exe.linux-arm64-musl': 12.6.0 '@pnpm/exe.linux-arm64-musl': 12.9.0
'@pnpm/exe.linux-ppc64': 12.6.0 '@pnpm/exe.linux-ppc64': 12.9.0
'@pnpm/exe.linux-riscv64': 12.6.0 '@pnpm/exe.linux-riscv64': 12.9.0
'@pnpm/exe.linux-s390x': 12.6.0 '@pnpm/exe.linux-s390x': 12.9.0
'@pnpm/exe.linux-x64': 12.6.0 '@pnpm/exe.linux-x64': 12.9.0
'@pnpm/exe.linux-x64-musl': 12.6.0 '@pnpm/exe.linux-x64-musl': 12.9.0
'@pnpm/exe.win32-arm64': 12.6.0 '@pnpm/exe.win32-arm64': 12.9.0
'@pnpm/exe.win32-x64': 12.6.0 '@pnpm/exe.win32-x64': 12.9.0
--- ---
lockfileVersion: '9.0' lockfileVersion: '9.0'
@ -168,28 +168,28 @@ importers:
devDependencies: devDependencies:
'@eslint/js': '@eslint/js':
specifier: 10.0.1 specifier: 10.0.1
version: 10.0.1(eslint@10.11.0(supports-color@7.2.0)) version: 10.0.1(eslint@10.12.0(supports-color@7.2.0))
'@kuhyx/ts-config': '@kuhyx/ts-config':
specifier: link:../ts_config specifier: link:../ts_config
version: link:../ts_config version: link:../ts_config
'@types/node': '@types/node':
specifier: 26.6.3 specifier: 26.6.4
version: 26.6.3 version: 26.6.4
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: 5.0.2 specifier: 5.0.3
version: 5.0.2(vitest@5.0.2) version: 5.0.3(vitest@5.0.3)
eslint: eslint:
specifier: 10.11.0 specifier: 10.12.0
version: 10.11.0(supports-color@7.2.0) version: 10.12.0(supports-color@7.2.0)
typescript: typescript:
specifier: 6.0.3 specifier: 6.0.3
version: 6.0.3 version: 6.0.3
typescript-eslint: typescript-eslint:
specifier: 8.70.0 specifier: 8.71.0
version: 8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) version: 8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
vitest: vitest:
specifier: 5.0.2 specifier: 5.0.3
version: 5.0.2(@types/node@26.6.3)(@vitest/coverage-v8@5.0.2)(vite@8.2.1(@types/node@26.6.3)) version: 5.0.3(@types/node@26.6.4)(@vitest/coverage-v8@5.0.3)(vite@8.2.1(@types/node@26.6.4))
packages: packages:
@ -409,73 +409,73 @@ packages:
'@types/json-schema@7.0.15': '@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/node@26.6.3': '@types/node@26.6.4':
resolution: {integrity: sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==} resolution: {integrity: sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==}
'@typescript-eslint/eslint-plugin@8.70.0': '@typescript-eslint/eslint-plugin@8.71.0':
resolution: {integrity: sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==} resolution: {integrity: sha512-pqcS9c1HxZTHt7End4nXqd0s5lJrrFzrgCkKFJrsbUnaL6M3+6oBFZaslg6Gjsl3argl2DDRFROnXARaZ2e4Nw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
'@typescript-eslint/parser': ^8.70.0 '@typescript-eslint/parser': ^8.71.0
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/parser@8.70.0': '@typescript-eslint/parser@8.71.0':
resolution: {integrity: sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==} resolution: {integrity: sha512-CG4nPk1f2zc8yw4pALqHsFYH2hdo+h1T9daSp21+Hnxi9LOE3GT9hAfTKJCBXVNM2GmYs1eMEP615wPoeOgk3A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/project-service@8.70.0': '@typescript-eslint/project-service@8.71.0':
resolution: {integrity: sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==} resolution: {integrity: sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/scope-manager@8.70.0': '@typescript-eslint/scope-manager@8.71.0':
resolution: {integrity: sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==} resolution: {integrity: sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.70.0': '@typescript-eslint/tsconfig-utils@8.71.0':
resolution: {integrity: sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==} resolution: {integrity: sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/type-utils@8.70.0': '@typescript-eslint/type-utils@8.71.0':
resolution: {integrity: sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==} resolution: {integrity: sha512-i8uO1qbdxeKgRnS5sCRt6On3/nfo2d2DwQe3Yvjx543zLy7r8ySqRuPPiIIXAhS03U0v5NfAFx+rUgxFzKKwNw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/types@8.70.0': '@typescript-eslint/types@8.71.0':
resolution: {integrity: sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==} resolution: {integrity: sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.70.0': '@typescript-eslint/typescript-estree@8.71.0':
resolution: {integrity: sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==} resolution: {integrity: sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/utils@8.70.0': '@typescript-eslint/utils@8.71.0':
resolution: {integrity: sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==} resolution: {integrity: sha512-pKR/tEMVrXZG23UFKUn5BQf3zfmfk7KQceI2cGzywZ5nxM5Eu3hEJU1utjWzydtzBbcJAQhHN8iPCxobHpPcZQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/visitor-keys@8.70.0': '@typescript-eslint/visitor-keys@8.71.0':
resolution: {integrity: sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==} resolution: {integrity: sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@vitest/coverage-v8@5.0.2': '@vitest/coverage-v8@5.0.3':
resolution: {integrity: sha512-3ffHBEi8DOOBLwIGBhOBZbRfYFYWjMUuxZicONdhuFEFEG5AVsMOSrVeuROskqnqpbOmEJwxM2eTVZ9N3a1t+A==} resolution: {integrity: sha512-+klsyz7BvT1vCU28Zkfzms1Ia78XD0V41U3FUtRaa3S+vOr/EXvK1O6BvVD7l1RzEjm6SONR2aybOvDDf9u0mQ==}
peerDependencies: peerDependencies:
'@vitest/browser': 5.0.2 '@vitest/browser': 5.0.3
vitest: 5.0.2 vitest: 5.0.3
peerDependenciesMeta: peerDependenciesMeta:
'@vitest/browser': '@vitest/browser':
optional: true optional: true
@ -488,8 +488,8 @@ packages:
resolution: {integrity: sha512-1EOLRfsTMnyAr3+kEAsP4o9dhaDlGPpD7H5iLBBeq//YpNB1VIahkPhB+eRp9N2Dkfw8oySROjE3yf9XDeaIkQ==} resolution: {integrity: sha512-1EOLRfsTMnyAr3+kEAsP4o9dhaDlGPpD7H5iLBBeq//YpNB1VIahkPhB+eRp9N2Dkfw8oySROjE3yf9XDeaIkQ==}
engines: {node: '>=22'} engines: {node: '>=22'}
'@vitest/mocker@5.0.2': '@vitest/mocker@5.0.3':
resolution: {integrity: sha512-Z5FS00Q1SJHkB35xATsmWGdQ5WA1/0MV3CDjqyv7GavHv1OfOj145MNfHOlHk7QLes21dKFDHr8EO2zvL+9WGA==} resolution: {integrity: sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==}
peerDependencies: peerDependencies:
msw: ^2.4.9 msw: ^2.4.9
vite: ^6.0.0 || ^7.0.0 || ^8.0.0 vite: ^6.0.0 || ^7.0.0 || ^8.0.0
@ -499,8 +499,8 @@ packages:
vite: vite:
optional: true optional: true
'@vitest/spy@5.0.2': '@vitest/spy@5.0.3':
resolution: {integrity: sha512-Ijc7T1nT9efNb5LxvjaBrEqw3f/QwUv5EE0nKqZxgqsaV/FxAAZ8baGylA8X/Z2oS4Lp+K74Jr6dTJsDKxJDeg==} resolution: {integrity: sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==}
acorn-jsx@5.3.2: acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
@ -576,8 +576,8 @@ packages:
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24} engines: {node: ^20.19.0 || ^22.13.0 || >=24}
eslint@10.11.0: eslint@10.12.0:
resolution: {integrity: sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==} resolution: {integrity: sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24} engines: {node: ^20.19.0 || ^22.13.0 || >=24}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@ -832,10 +832,6 @@ packages:
picocolors@1.1.1: picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@4.0.5:
resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
engines: {node: '>=12'}
picomatch@4.0.7: picomatch@4.0.7:
resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -911,8 +907,8 @@ packages:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
typescript-eslint@8.70.0: typescript-eslint@8.71.0:
resolution: {integrity: sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==} resolution: {integrity: sha512-fBdHYiqQ14RW6mOMXD14Svn82ZsCYAoQSzGRzyEjR59S5A2Krh/l7fGTOQ7iCr8gGy/mHVXtEF7s5fgjEdV0Pw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
@ -972,20 +968,20 @@ packages:
yaml: yaml:
optional: true optional: true
vitest@5.0.2: vitest@5.0.3:
resolution: {integrity: sha512-7MQrx9pDv5aHiUcovIb/70Ys3tgtkUVgCtledvKdCmEO+/1Dicq5ZqoSxOW034m03oqC+oHOKui2dM6qtMLoJg==} resolution: {integrity: sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==}
engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
'@edge-runtime/vm': '*' '@edge-runtime/vm': '*'
'@opentelemetry/api': ^1.9.0 '@opentelemetry/api': ^1.9.0
'@types/node': ^22.0.0 || >=24.0.0 '@types/node': ^22.0.0 || >=24.0.0
'@vitest/browser-playwright': 5.0.2 '@vitest/browser-playwright': 5.0.3
'@vitest/browser-preview': 5.0.2 '@vitest/browser-preview': 5.0.3
'@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0
'@vitest/coverage-istanbul': 5.0.2 '@vitest/coverage-istanbul': 5.0.3
'@vitest/coverage-v8': 5.0.2 '@vitest/coverage-v8': 5.0.3
'@vitest/ui': 5.0.2 '@vitest/ui': 5.0.3
happy-dom: '*' happy-dom: '*'
jsdom: '*' jsdom: '*'
vite: ^6.4.0 || ^7.0.0 || ^8.0.0 vite: ^6.4.0 || ^7.0.0 || ^8.0.0
@ -1018,8 +1014,8 @@ packages:
engines: {node: '>= 8'} engines: {node: '>= 8'}
hasBin: true hasBin: true
why-is-node-running@3.2.2: why-is-node-running@3.2.1:
resolution: {integrity: sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A==} resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==}
engines: {node: '>=20.11'} engines: {node: '>=20.11'}
hasBin: true hasBin: true
@ -1060,9 +1056,9 @@ snapshots:
hashery: 1.5.1 hashery: 1.5.1
keyv: 5.6.0 keyv: 5.6.0
'@eslint-community/eslint-utils@4.10.1(eslint@10.11.0(supports-color@7.2.0))': '@eslint-community/eslint-utils@4.10.1(eslint@10.12.0(supports-color@7.2.0))':
dependencies: dependencies:
eslint: 10.11.0(supports-color@7.2.0) eslint: 10.12.0(supports-color@7.2.0)
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {} '@eslint-community/regexpp@4.12.2': {}
@ -1083,9 +1079,9 @@ snapshots:
dependencies: dependencies:
'@types/json-schema': 7.0.15 '@types/json-schema': 7.0.15
'@eslint/js@10.0.1(eslint@10.11.0(supports-color@7.2.0))': '@eslint/js@10.0.1(eslint@10.12.0(supports-color@7.2.0))':
optionalDependencies: optionalDependencies:
eslint: 10.11.0(supports-color@7.2.0) eslint: 10.12.0(supports-color@7.2.0)
'@eslint/object-schema@3.0.5': {} '@eslint/object-schema@3.0.5': {}
@ -1186,19 +1182,19 @@ snapshots:
'@types/json-schema@7.0.15': {} '@types/json-schema@7.0.15': {}
'@types/node@26.6.3': '@types/node@26.6.4':
dependencies: dependencies:
undici-types: 8.9.0 undici-types: 8.9.0
'@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': '@typescript-eslint/eslint-plugin@8.71.0(@typescript-eslint/parser@8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
dependencies: dependencies:
'@eslint-community/regexpp': 4.12.2 '@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/parser': 8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
'@typescript-eslint/scope-manager': 8.70.0 '@typescript-eslint/scope-manager': 8.71.0
'@typescript-eslint/type-utils': 8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/type-utils': 8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
'@typescript-eslint/utils': 8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/utils': 8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
'@typescript-eslint/visitor-keys': 8.70.0 '@typescript-eslint/visitor-keys': 8.71.0
eslint: 10.11.0(supports-color@7.2.0) eslint: 10.12.0(supports-color@7.2.0)
ignore: 7.0.6 ignore: 7.0.6
natural-compare: 1.4.0 natural-compare: 1.4.0
ts-api-utils: 2.5.0(typescript@6.0.3) ts-api-utils: 2.5.0(typescript@6.0.3)
@ -1206,56 +1202,56 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/parser@8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': '@typescript-eslint/parser@8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
dependencies: dependencies:
'@typescript-eslint/scope-manager': 8.70.0 '@typescript-eslint/scope-manager': 8.71.0
'@typescript-eslint/types': 8.70.0 '@typescript-eslint/types': 8.71.0
'@typescript-eslint/typescript-estree': 8.70.0(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/typescript-estree': 8.71.0(supports-color@7.2.0)(typescript@6.0.3)
'@typescript-eslint/visitor-keys': 8.70.0 '@typescript-eslint/visitor-keys': 8.71.0
debug: 4.4.3(supports-color@7.2.0) debug: 4.4.3(supports-color@7.2.0)
eslint: 10.11.0(supports-color@7.2.0) eslint: 10.12.0(supports-color@7.2.0)
typescript: 6.0.3 typescript: 6.0.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/project-service@8.70.0(supports-color@7.2.0)(typescript@6.0.3)': '@typescript-eslint/project-service@8.71.0(supports-color@7.2.0)(typescript@6.0.3)':
dependencies: dependencies:
'@typescript-eslint/tsconfig-utils': 8.70.0(typescript@6.0.3) '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@6.0.3)
'@typescript-eslint/types': 8.70.0 '@typescript-eslint/types': 8.71.0
debug: 4.4.3(supports-color@7.2.0) debug: 4.4.3(supports-color@7.2.0)
typescript: 6.0.3 typescript: 6.0.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/scope-manager@8.70.0': '@typescript-eslint/scope-manager@8.71.0':
dependencies: dependencies:
'@typescript-eslint/types': 8.70.0 '@typescript-eslint/types': 8.71.0
'@typescript-eslint/visitor-keys': 8.70.0 '@typescript-eslint/visitor-keys': 8.71.0
'@typescript-eslint/tsconfig-utils@8.70.0(typescript@6.0.3)': '@typescript-eslint/tsconfig-utils@8.71.0(typescript@6.0.3)':
dependencies: dependencies:
typescript: 6.0.3 typescript: 6.0.3
'@typescript-eslint/type-utils@8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': '@typescript-eslint/type-utils@8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
dependencies: dependencies:
'@typescript-eslint/types': 8.70.0 '@typescript-eslint/types': 8.71.0
'@typescript-eslint/typescript-estree': 8.70.0(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/typescript-estree': 8.71.0(supports-color@7.2.0)(typescript@6.0.3)
'@typescript-eslint/utils': 8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/utils': 8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
debug: 4.4.3(supports-color@7.2.0) debug: 4.4.3(supports-color@7.2.0)
eslint: 10.11.0(supports-color@7.2.0) eslint: 10.12.0(supports-color@7.2.0)
ts-api-utils: 2.5.0(typescript@6.0.3) ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3 typescript: 6.0.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/types@8.70.0': {} '@typescript-eslint/types@8.71.0': {}
'@typescript-eslint/typescript-estree@8.70.0(supports-color@7.2.0)(typescript@6.0.3)': '@typescript-eslint/typescript-estree@8.71.0(supports-color@7.2.0)(typescript@6.0.3)':
dependencies: dependencies:
'@typescript-eslint/project-service': 8.70.0(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/project-service': 8.71.0(supports-color@7.2.0)(typescript@6.0.3)
'@typescript-eslint/tsconfig-utils': 8.70.0(typescript@6.0.3) '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@6.0.3)
'@typescript-eslint/types': 8.70.0 '@typescript-eslint/types': 8.71.0
'@typescript-eslint/visitor-keys': 8.70.0 '@typescript-eslint/visitor-keys': 8.71.0
debug: 4.4.3(supports-color@7.2.0) debug: 4.4.3(supports-color@7.2.0)
minimatch: 10.2.6 minimatch: 10.2.6
semver: 7.8.5 semver: 7.8.5
@ -1265,23 +1261,23 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/utils@8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': '@typescript-eslint/utils@8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.10.1(eslint@10.11.0(supports-color@7.2.0)) '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0(supports-color@7.2.0))
'@typescript-eslint/scope-manager': 8.70.0 '@typescript-eslint/scope-manager': 8.71.0
'@typescript-eslint/types': 8.70.0 '@typescript-eslint/types': 8.71.0
'@typescript-eslint/typescript-estree': 8.70.0(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/typescript-estree': 8.71.0(supports-color@7.2.0)(typescript@6.0.3)
eslint: 10.11.0(supports-color@7.2.0) eslint: 10.12.0(supports-color@7.2.0)
typescript: 6.0.3 typescript: 6.0.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/visitor-keys@8.70.0': '@typescript-eslint/visitor-keys@8.71.0':
dependencies: dependencies:
'@typescript-eslint/types': 8.70.0 '@typescript-eslint/types': 8.71.0
eslint-visitor-keys: 5.0.1 eslint-visitor-keys: 5.0.1
'@vitest/coverage-v8@5.0.2(vitest@5.0.2)': '@vitest/coverage-v8@5.0.3(vitest@5.0.3)':
dependencies: dependencies:
'@bcoe/v8-coverage': 1.0.2 '@bcoe/v8-coverage': 1.0.2
'@vitest/istanbul-lib-coverage': 1.0.1 '@vitest/istanbul-lib-coverage': 1.0.1
@ -1291,7 +1287,7 @@ snapshots:
obug: 2.1.4 obug: 2.1.4
std-env: 4.2.0 std-env: 4.2.0
tinyrainbow: 3.1.1 tinyrainbow: 3.1.1
vitest: 5.0.2(@types/node@26.6.3)(@vitest/coverage-v8@5.0.2)(vite@8.2.1(@types/node@26.6.3)) vitest: 5.0.3(@types/node@26.6.4)(@vitest/coverage-v8@5.0.3)(vite@8.2.1(@types/node@26.6.4))
'@vitest/istanbul-lib-coverage@1.0.1': {} '@vitest/istanbul-lib-coverage@1.0.1': {}
@ -1299,16 +1295,16 @@ snapshots:
dependencies: dependencies:
'@vitest/istanbul-lib-coverage': 1.0.1 '@vitest/istanbul-lib-coverage': 1.0.1
'@vitest/mocker@5.0.2(vite@8.2.1(@types/node@26.6.3))': '@vitest/mocker@5.0.3(vite@8.2.1(@types/node@26.6.4))':
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.31 '@jridgewell/trace-mapping': 0.3.31
'@vitest/spy': 5.0.2 '@vitest/spy': 5.0.3
estree-walker: 3.0.3 estree-walker: 3.0.3
magic-string: 1.3.1 magic-string: 1.3.1
optionalDependencies: optionalDependencies:
vite: 8.2.1(@types/node@26.6.3) vite: 8.2.1(@types/node@26.6.4)
'@vitest/spy@5.0.2': {} '@vitest/spy@5.0.3': {}
acorn-jsx@5.3.2(acorn@8.18.0): acorn-jsx@5.3.2(acorn@8.18.0):
dependencies: dependencies:
@ -1378,9 +1374,9 @@ snapshots:
eslint-visitor-keys@5.0.1: {} eslint-visitor-keys@5.0.1: {}
eslint@10.11.0(supports-color@7.2.0): eslint@10.12.0(supports-color@7.2.0):
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.10.1(eslint@10.11.0(supports-color@7.2.0)) '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0(supports-color@7.2.0))
'@eslint-community/regexpp': 4.12.2 '@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.23.5(supports-color@7.2.0) '@eslint/config-array': 0.23.5(supports-color@7.2.0)
'@eslint/config-helpers': 0.7.0 '@eslint/config-helpers': 0.7.0
@ -1443,9 +1439,9 @@ snapshots:
fast-levenshtein@2.0.6: {} fast-levenshtein@2.0.6: {}
fdir@6.5.0(picomatch@4.0.5): fdir@6.5.0(picomatch@4.0.7):
optionalDependencies: optionalDependencies:
picomatch: 4.0.5 picomatch: 4.0.7
file-entry-cache@11.1.5: file-entry-cache@11.1.5:
dependencies: dependencies:
@ -1609,8 +1605,6 @@ snapshots:
picocolors@1.1.1: {} picocolors@1.1.1: {}
picomatch@4.0.5: {}
picomatch@4.0.7: {} picomatch@4.0.7: {}
postcss@8.5.26: postcss@8.5.26:
@ -1670,8 +1664,8 @@ snapshots:
tinyglobby@0.2.17: tinyglobby@0.2.17:
dependencies: dependencies:
fdir: 6.5.0(picomatch@4.0.5) fdir: 6.5.0(picomatch@4.0.7)
picomatch: 4.0.5 picomatch: 4.0.7
tinyrainbow@3.1.1: {} tinyrainbow@3.1.1: {}
@ -1683,13 +1677,13 @@ snapshots:
dependencies: dependencies:
prelude-ls: 1.2.1 prelude-ls: 1.2.1
typescript-eslint@8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): typescript-eslint@8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3):
dependencies: dependencies:
'@typescript-eslint/eslint-plugin': 8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/eslint-plugin': 8.71.0(@typescript-eslint/parser@8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
'@typescript-eslint/parser': 8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/parser': 8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
'@typescript-eslint/typescript-estree': 8.70.0(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/typescript-estree': 8.71.0(supports-color@7.2.0)(typescript@6.0.3)
'@typescript-eslint/utils': 8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/utils': 8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
eslint: 10.11.0(supports-color@7.2.0) eslint: 10.12.0(supports-color@7.2.0)
typescript: 6.0.3 typescript: 6.0.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -1702,7 +1696,7 @@ snapshots:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
vite@8.2.1(@types/node@26.6.3): vite@8.2.1(@types/node@26.6.4):
dependencies: dependencies:
lightningcss: 1.33.0 lightningcss: 1.33.0
picomatch: 4.0.7 picomatch: 4.0.7
@ -1710,13 +1704,13 @@ snapshots:
rolldown: 1.2.4 rolldown: 1.2.4
tinyglobby: 0.2.17 tinyglobby: 0.2.17
optionalDependencies: optionalDependencies:
'@types/node': 26.6.3 '@types/node': 26.6.4
fsevents: 2.3.3 fsevents: 2.3.3
vitest@5.0.2(@types/node@26.6.3)(@vitest/coverage-v8@5.0.2)(vite@8.2.1(@types/node@26.6.3)): vitest@5.0.3(@types/node@26.6.4)(@vitest/coverage-v8@5.0.3)(vite@8.2.1(@types/node@26.6.4)):
dependencies: dependencies:
'@types/chai': 5.2.3 '@types/chai': 5.2.3
'@vitest/mocker': 5.0.2(vite@8.2.1(@types/node@26.6.3)) '@vitest/mocker': 5.0.3(vite@8.2.1(@types/node@26.6.4))
chai: 6.2.2 chai: 6.2.2
es-module-lexer: 2.3.2 es-module-lexer: 2.3.2
expect-type: 1.4.0 expect-type: 1.4.0
@ -1727,11 +1721,11 @@ snapshots:
tinybench: 6.1.4 tinybench: 6.1.4
tinyexec: 1.3.0 tinyexec: 1.3.0
tinyglobby: 0.2.17 tinyglobby: 0.2.17
vite: 8.2.1(@types/node@26.6.3) vite: 8.2.1(@types/node@26.6.4)
why-is-node-running: 3.2.2 why-is-node-running: 3.2.1
optionalDependencies: optionalDependencies:
'@types/node': 26.6.3 '@types/node': 26.6.4
'@vitest/coverage-v8': 5.0.2(vitest@5.0.2) '@vitest/coverage-v8': 5.0.3(vitest@5.0.3)
transitivePeerDependencies: transitivePeerDependencies:
- msw - msw
@ -1739,7 +1733,7 @@ snapshots:
dependencies: dependencies:
isexe: 2.0.0 isexe: 2.0.0
why-is-node-running@3.2.2: {} why-is-node-running@3.2.1: {}
word-wrap@1.2.5: {} word-wrap@1.2.5: {}

View File

@ -9,7 +9,7 @@
"node": ">=24", "node": ">=24",
"pnpm": ">=10" "pnpm": ">=10"
}, },
"packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec", "packageManager": "pnpm@12.9.0+sha512.8f64efa0b792986ca98ae568791bcd7722807e274c4f0c1d9853c62116495c584119db98762a2a5343b1684d361b9ae26fdd16d551666d7635346d3fae830ddd",
"files": [ "files": [
"dist" "dist"
], ],
@ -38,16 +38,16 @@
"@testing-library/jest-dom": "7.0.1", "@testing-library/jest-dom": "7.0.1",
"@testing-library/react": "16.3.3", "@testing-library/react": "16.3.3",
"@testing-library/user-event": "14.6.7", "@testing-library/user-event": "14.6.7",
"@types/node": "26.6.3", "@types/node": "26.6.4",
"@types/react": "19.3.0", "@types/react": "19.3.0",
"@types/react-dom": "19.3.0", "@types/react-dom": "19.3.0",
"@vitest/coverage-v8": "5.0.2", "@vitest/coverage-v8": "5.0.3",
"eslint": "10.11.0", "eslint": "10.12.0",
"jsdom": "30.1.1", "jsdom": "30.1.1",
"react": "19.3.0", "react": "19.3.0",
"react-dom": "19.3.0", "react-dom": "19.3.0",
"typescript": "6.0.3", "typescript": "6.0.3",
"typescript-eslint": "8.70.0", "typescript-eslint": "8.71.0",
"vitest": "5.0.2" "vitest": "5.0.3"
} }
} }

View File

@ -7,153 +7,153 @@ importers:
configDependencies: {} configDependencies: {}
packageManagerDependencies: packageManagerDependencies:
pnpm: pnpm:
specifier: 12.6.0 specifier: 12.9.0
version: 12.6.0 version: 12.9.0
packages: packages:
'@pnpm/exe.android-arm64@12.6.0': '@pnpm/exe.android-arm64@12.9.0':
resolution: {integrity: sha512-kviIHft9h02q+7N2In7tSL9T/HRUSGwYmU74YPtsWU5ee7vgqPyC0JT6RRX6miW+NxX1CvHgQxg69qq1qTLJYQ==} resolution: {integrity: sha512-/53fHFwP1mBYQjli6h3LBZsocAMjrEY6hCDiomIwdLIFbtxHrWucPg/s8ozOG3K6Qt8yEA78v7fCcNrgZKTlEQ==}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
'@pnpm/exe.android-x64@12.6.0': '@pnpm/exe.android-x64@12.9.0':
resolution: {integrity: sha512-CT8aJKLq2mtZFE71pr4E5Z2xHL8uGnRGr+NclDqsXlVF4SVcQ2QAs14mWi0C8gjT4hbmi+Q0lJRRIkMAWfjjng==} resolution: {integrity: sha512-QI+Md3q3Df0WbDGpqciRw9ge0r7CGbxSlP3HcyIf34/YdZsbmUTl416oWOe3RrkZpVVVCA20f8B9esYrITpArA==}
cpu: [x64] cpu: [x64]
os: [android] os: [android]
'@pnpm/exe.darwin-arm64@12.6.0': '@pnpm/exe.darwin-arm64@12.9.0':
resolution: {integrity: sha512-rafpVkjzugKBMxSvGQd5wK1x5eOTkD/+lcJIFHeQhvd+BJ8kDisOOvwhQDrpGd4vd2Fx+hhW0P2Ptt638OlhFg==} resolution: {integrity: sha512-mWr7fGPYJ12GuvU/q0SyJfj7X/9Eqa5dA6cc+yUUAKRdYEcqW4lQcsDd1ArfxJNSDYooz9X+UhFeVxKO9e/xDA==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@pnpm/exe.darwin-x64@12.6.0': '@pnpm/exe.darwin-x64@12.9.0':
resolution: {integrity: sha512-72Jpuv1m24gI8zUNcaylYpBWEhBzz1VPhnDIU2GYM2eRP+KsOqVLwV78i+JrihI39zY2NNXmaN4eAKX1inpaDQ==} resolution: {integrity: sha512-fr4ns4FyMLptdYbWqW3yc/yvbfQP7D1RBCFUJpSPr7tOzd3JxZvl+Tkvg65kEAGU5wXI6vl3eCOofumEj/n3Gw==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@pnpm/exe.freebsd-x64@12.6.0': '@pnpm/exe.freebsd-x64@12.9.0':
resolution: {integrity: sha512-LON4QgNy1w/XF4uySZIyC/5hEB0oGdelXsY9tSKqstER+2j2X2oHz67skD11RRd/HWFRtMr64shQZ4I9ZoZ0zg==} resolution: {integrity: sha512-4+hZPeJzSSw1oOrfUg+v4QylHv6qKhdgcCnKt9ooryE79p1Z9oyozVJ5em+g6ajbvv6jlH+epnht8/s9Cp5M3Q==}
cpu: [x64] cpu: [x64]
os: [freebsd] os: [freebsd]
'@pnpm/exe.linux-arm64-musl@12.6.0': '@pnpm/exe.linux-arm64-musl@12.9.0':
resolution: {integrity: sha512-BdDpX+DeaMUc5x6WNcE6FkmFy36NwrBbBvwsZU737Zt3u1fTY+iKFPchjEQNCM4rT6q6xvF2oN3ZwExGsJCkxQ==} resolution: {integrity: sha512-+mHQSbA6M6W71PyV0IxYu2T43lZA8rAhlw4Gy7EMlyiv8H2GWJAXtUXl2huw7tnsar2AhCRlPn/ZvUv+/mIBkA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-arm64@12.6.0': '@pnpm/exe.linux-arm64@12.9.0':
resolution: {integrity: sha512-8h2sNoIhHDpHDYqZZCh9PXr6krqW10btb0GHVGuw710muDHlFAHMtUG2ogmaHge6zJG1xgY4Y7mwX5g/LWvrYg==} resolution: {integrity: sha512-U1mBoG27+J/HuWYtVZOQrvbiKnTlDsDVzyroKQqHsJaTHTXi0H5Y+fU1GjUyMcNG5r/EXtbhp9iAwQt64uXTxQ==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-ppc64@12.6.0': '@pnpm/exe.linux-ppc64@12.9.0':
resolution: {integrity: sha512-ld6xgcEhFCFrsVsJhbmdmR714PnsEspO/Ij8w0/D7OSiz2hYGu3fZd5tKRxOI5m6H6Gl0/RRFAPncTHbsgXVjQ==} resolution: {integrity: sha512-P5bgei3i6wjlKq6s4PNg/0cvik354upY2wW3YGSfweYTABnAZP2zGzgMc0eO5SHTz934Cbya8Z0YFRdw6MfmCg==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-riscv64@12.6.0': '@pnpm/exe.linux-riscv64@12.9.0':
resolution: {integrity: sha512-HIbImydoC3J8NFt+8MXfhTCf5rO6NjCMyinSAHac0VmV8uCHw+kbQkyAzxV84rzz9ZOzmBOnj0b1N461UIYGsw==} resolution: {integrity: sha512-oNx2uOo2gVrRckEvz7V+FKeq9HOF0rKNHcCyxRRHSXEXpmP6ZMMgJ+iU/m/zfSGp6bAcC5mvAE270FhAeIeKhA==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-s390x@12.6.0': '@pnpm/exe.linux-s390x@12.9.0':
resolution: {integrity: sha512-DjSqT7+BZ/lWcDHwNMuzsRVfyxUimh+D54Xw4tSWbdVVQhtWtNMUyI93LVnwCmsoEpar2gC3SFeAgywx37svKQ==} resolution: {integrity: sha512-OChHS7hOlqdBMtZqjIB1QoZGNfzLPSsvTa2kYanf3qso5Q+KgnROQ+uV4a0wRGC6sdshSA63+Ui8MCylRy6iwA==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.6.0': '@pnpm/exe.linux-x64-musl@12.9.0':
resolution: {integrity: sha512-31lKeGPmRE6xfV6I3VjaEGVFRei14pl/zgoFDK4w+UkbJqOuNl2Ht3O1GuqNIg3Gl14qTEb4kZRAYFHXifDx9A==} resolution: {integrity: sha512-ubZPsDMXS+LEnPa7wZD18E+KTpOAZPt+ms/khfIROt+6Cp0A2kayQA8aIsvV7+jGZD0xoknvaGN/P3v9OisZUg==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-x64@12.6.0': '@pnpm/exe.linux-x64@12.9.0':
resolution: {integrity: sha512-qFWBneHJAJ73W4whtbaFOL1M/7DBC6ILHXuxc7ZPtEhfPuT1zeZiGrmKHoMAfJA+mcm6xhOFljqVTUS+00Jabw==} resolution: {integrity: sha512-ElANYUwQuMfWqMJR4m5L5h9zKjvGKcA8hi2put4J1DMx2smVaNGkGnxkgeIYgMjmF7UFxlZDcL4jItkScgsW4A==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.win32-arm64@12.6.0': '@pnpm/exe.win32-arm64@12.9.0':
resolution: {integrity: sha512-OhfefXEEykZlslSUhR8PPRe6MVPV3Oink/dVfIecE5velpc7j80cZYsHlaDrtRDcm3TPwQHkaRW1SJ1efX9oFg==} resolution: {integrity: sha512-cqnBKZg5e9fFghmYtgJLyZOAsS1EyLeDVJ0qNsjR7hcc+dq1mWRiDo0erqdj2PGoYaVogyF9WejZSJoKvlfXDw==}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@pnpm/exe.win32-x64@12.6.0': '@pnpm/exe.win32-x64@12.9.0':
resolution: {integrity: sha512-L2tuyrD2+Imgxs3VK/ST/2L3xD6vDP0ktJQxM0TaGw7TbAQEMq+RMc8iK6Ew7Id579uzjJe8jSfJID2ZGsOHCA==} resolution: {integrity: sha512-khqst6QGgHdKs2lEmz0N75qN/EoOcEywNQlRPfBrBrq1SytHra6ro/xViueJkVvDOz9gEO8oYcXTdZeAjErjMg==}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
pnpm@12.6.0: pnpm@12.9.0:
resolution: {integrity: sha512-PvaPlRyxEawgS0paFvCy3fDaVqluBBPoHYVdnwtV75JnFHCQKOHNAMQFwsX7e56OxNxGd3yAXQNzwvL/AP0g7A==} resolution: {integrity: sha512-j2TvoLeSmGypiuVoeRvNdyKAfidMTwwdmFPGIRZJXFhBGduYdioqU0OxaE02G5rib90W1VFmbXY1NG0/roMN3Q==}
engines: {node: '>=18.*'} engines: {node: '>=18.*'}
hasBin: true hasBin: true
snapshots: snapshots:
'@pnpm/exe.android-arm64@12.6.0': '@pnpm/exe.android-arm64@12.9.0':
optional: true optional: true
'@pnpm/exe.android-x64@12.6.0': '@pnpm/exe.android-x64@12.9.0':
optional: true optional: true
'@pnpm/exe.darwin-arm64@12.6.0': '@pnpm/exe.darwin-arm64@12.9.0':
optional: true optional: true
'@pnpm/exe.darwin-x64@12.6.0': '@pnpm/exe.darwin-x64@12.9.0':
optional: true optional: true
'@pnpm/exe.freebsd-x64@12.6.0': '@pnpm/exe.freebsd-x64@12.9.0':
optional: true optional: true
'@pnpm/exe.linux-arm64-musl@12.6.0': '@pnpm/exe.linux-arm64-musl@12.9.0':
optional: true optional: true
'@pnpm/exe.linux-arm64@12.6.0': '@pnpm/exe.linux-arm64@12.9.0':
optional: true optional: true
'@pnpm/exe.linux-ppc64@12.6.0': '@pnpm/exe.linux-ppc64@12.9.0':
optional: true optional: true
'@pnpm/exe.linux-riscv64@12.6.0': '@pnpm/exe.linux-riscv64@12.9.0':
optional: true optional: true
'@pnpm/exe.linux-s390x@12.6.0': '@pnpm/exe.linux-s390x@12.9.0':
optional: true optional: true
'@pnpm/exe.linux-x64-musl@12.6.0': '@pnpm/exe.linux-x64-musl@12.9.0':
optional: true optional: true
'@pnpm/exe.linux-x64@12.6.0': '@pnpm/exe.linux-x64@12.9.0':
optional: true optional: true
'@pnpm/exe.win32-arm64@12.6.0': '@pnpm/exe.win32-arm64@12.9.0':
optional: true optional: true
'@pnpm/exe.win32-x64@12.6.0': '@pnpm/exe.win32-x64@12.9.0':
optional: true optional: true
pnpm@12.6.0: pnpm@12.9.0:
optionalDependencies: optionalDependencies:
'@pnpm/exe.android-arm64': 12.6.0 '@pnpm/exe.android-arm64': 12.9.0
'@pnpm/exe.android-x64': 12.6.0 '@pnpm/exe.android-x64': 12.9.0
'@pnpm/exe.darwin-arm64': 12.6.0 '@pnpm/exe.darwin-arm64': 12.9.0
'@pnpm/exe.darwin-x64': 12.6.0 '@pnpm/exe.darwin-x64': 12.9.0
'@pnpm/exe.freebsd-x64': 12.6.0 '@pnpm/exe.freebsd-x64': 12.9.0
'@pnpm/exe.linux-arm64': 12.6.0 '@pnpm/exe.linux-arm64': 12.9.0
'@pnpm/exe.linux-arm64-musl': 12.6.0 '@pnpm/exe.linux-arm64-musl': 12.9.0
'@pnpm/exe.linux-ppc64': 12.6.0 '@pnpm/exe.linux-ppc64': 12.9.0
'@pnpm/exe.linux-riscv64': 12.6.0 '@pnpm/exe.linux-riscv64': 12.9.0
'@pnpm/exe.linux-s390x': 12.6.0 '@pnpm/exe.linux-s390x': 12.9.0
'@pnpm/exe.linux-x64': 12.6.0 '@pnpm/exe.linux-x64': 12.9.0
'@pnpm/exe.linux-x64-musl': 12.6.0 '@pnpm/exe.linux-x64-musl': 12.9.0
'@pnpm/exe.win32-arm64': 12.6.0 '@pnpm/exe.win32-arm64': 12.9.0
'@pnpm/exe.win32-x64': 12.6.0 '@pnpm/exe.win32-x64': 12.9.0
--- ---
lockfileVersion: '9.0' lockfileVersion: '9.0'
@ -168,13 +168,13 @@ importers:
devDependencies: devDependencies:
'@eslint/js': '@eslint/js':
specifier: 10.0.1 specifier: 10.0.1
version: 10.0.1(eslint@10.11.0(supports-color@7.2.0)) version: 10.0.1(eslint@10.12.0(supports-color@7.2.0))
'@kuhyx/ts-config': '@kuhyx/ts-config':
specifier: link:../ts_config specifier: link:../ts_config
version: link:../ts_config version: link:../ts_config
'@testing-library/jest-dom': '@testing-library/jest-dom':
specifier: 7.0.1 specifier: 7.0.1
version: 7.0.1(@testing-library/dom@10.4.1)(vitest@5.0.2) version: 7.0.1(@testing-library/dom@10.4.1)(vitest@5.0.3)
'@testing-library/react': '@testing-library/react':
specifier: 16.3.3 specifier: 16.3.3
version: 16.3.3(@testing-library/dom@10.4.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) version: 16.3.3(@testing-library/dom@10.4.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
@ -182,8 +182,8 @@ importers:
specifier: 14.6.7 specifier: 14.6.7
version: 14.6.7(@testing-library/dom@10.4.1) version: 14.6.7(@testing-library/dom@10.4.1)
'@types/node': '@types/node':
specifier: 26.6.3 specifier: 26.6.4
version: 26.6.3 version: 26.6.4
'@types/react': '@types/react':
specifier: 19.3.0 specifier: 19.3.0
version: 19.3.0 version: 19.3.0
@ -191,11 +191,11 @@ importers:
specifier: 19.3.0 specifier: 19.3.0
version: 19.3.0(@types/react@19.3.0) version: 19.3.0(@types/react@19.3.0)
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: 5.0.2 specifier: 5.0.3
version: 5.0.2(vitest@5.0.2) version: 5.0.3(vitest@5.0.3)
eslint: eslint:
specifier: 10.11.0 specifier: 10.12.0
version: 10.11.0(supports-color@7.2.0) version: 10.12.0(supports-color@7.2.0)
jsdom: jsdom:
specifier: 30.1.1 specifier: 30.1.1
version: 30.1.1 version: 30.1.1
@ -209,11 +209,11 @@ importers:
specifier: 6.0.3 specifier: 6.0.3
version: 6.0.3 version: 6.0.3
typescript-eslint: typescript-eslint:
specifier: 8.70.0 specifier: 8.71.0
version: 8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) version: 8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
vitest: vitest:
specifier: 5.0.2 specifier: 5.0.3
version: 5.0.2(@types/node@26.6.3)(@vitest/coverage-v8@5.0.2)(jsdom@30.1.1)(vite@8.2.2(@types/node@26.6.3)) version: 5.0.3(@types/node@26.6.4)(@vitest/coverage-v8@5.0.3)(jsdom@30.1.1)(vite@8.2.2(@types/node@26.6.4))
packages: packages:
@ -545,8 +545,8 @@ packages:
'@types/json-schema@7.0.15': '@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/node@26.6.3': '@types/node@26.6.4':
resolution: {integrity: sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==} resolution: {integrity: sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==}
'@types/react-dom@19.3.0': '@types/react-dom@19.3.0':
resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==}
@ -556,70 +556,70 @@ packages:
'@types/react@19.3.0': '@types/react@19.3.0':
resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==}
'@typescript-eslint/eslint-plugin@8.70.0': '@typescript-eslint/eslint-plugin@8.71.0':
resolution: {integrity: sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==} resolution: {integrity: sha512-pqcS9c1HxZTHt7End4nXqd0s5lJrrFzrgCkKFJrsbUnaL6M3+6oBFZaslg6Gjsl3argl2DDRFROnXARaZ2e4Nw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
'@typescript-eslint/parser': ^8.70.0 '@typescript-eslint/parser': ^8.71.0
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/parser@8.70.0': '@typescript-eslint/parser@8.71.0':
resolution: {integrity: sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==} resolution: {integrity: sha512-CG4nPk1f2zc8yw4pALqHsFYH2hdo+h1T9daSp21+Hnxi9LOE3GT9hAfTKJCBXVNM2GmYs1eMEP615wPoeOgk3A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/project-service@8.70.0': '@typescript-eslint/project-service@8.71.0':
resolution: {integrity: sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==} resolution: {integrity: sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/scope-manager@8.70.0': '@typescript-eslint/scope-manager@8.71.0':
resolution: {integrity: sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==} resolution: {integrity: sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.70.0': '@typescript-eslint/tsconfig-utils@8.71.0':
resolution: {integrity: sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==} resolution: {integrity: sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/type-utils@8.70.0': '@typescript-eslint/type-utils@8.71.0':
resolution: {integrity: sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==} resolution: {integrity: sha512-i8uO1qbdxeKgRnS5sCRt6On3/nfo2d2DwQe3Yvjx543zLy7r8ySqRuPPiIIXAhS03U0v5NfAFx+rUgxFzKKwNw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/types@8.70.0': '@typescript-eslint/types@8.71.0':
resolution: {integrity: sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==} resolution: {integrity: sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.70.0': '@typescript-eslint/typescript-estree@8.71.0':
resolution: {integrity: sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==} resolution: {integrity: sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/utils@8.70.0': '@typescript-eslint/utils@8.71.0':
resolution: {integrity: sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==} resolution: {integrity: sha512-pKR/tEMVrXZG23UFKUn5BQf3zfmfk7KQceI2cGzywZ5nxM5Eu3hEJU1utjWzydtzBbcJAQhHN8iPCxobHpPcZQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0' typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/visitor-keys@8.70.0': '@typescript-eslint/visitor-keys@8.71.0':
resolution: {integrity: sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==} resolution: {integrity: sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@vitest/coverage-v8@5.0.2': '@vitest/coverage-v8@5.0.3':
resolution: {integrity: sha512-3ffHBEi8DOOBLwIGBhOBZbRfYFYWjMUuxZicONdhuFEFEG5AVsMOSrVeuROskqnqpbOmEJwxM2eTVZ9N3a1t+A==} resolution: {integrity: sha512-+klsyz7BvT1vCU28Zkfzms1Ia78XD0V41U3FUtRaa3S+vOr/EXvK1O6BvVD7l1RzEjm6SONR2aybOvDDf9u0mQ==}
peerDependencies: peerDependencies:
'@vitest/browser': 5.0.2 '@vitest/browser': 5.0.3
vitest: 5.0.2 vitest: 5.0.3
peerDependenciesMeta: peerDependenciesMeta:
'@vitest/browser': '@vitest/browser':
optional: true optional: true
@ -632,8 +632,8 @@ packages:
resolution: {integrity: sha512-1EOLRfsTMnyAr3+kEAsP4o9dhaDlGPpD7H5iLBBeq//YpNB1VIahkPhB+eRp9N2Dkfw8oySROjE3yf9XDeaIkQ==} resolution: {integrity: sha512-1EOLRfsTMnyAr3+kEAsP4o9dhaDlGPpD7H5iLBBeq//YpNB1VIahkPhB+eRp9N2Dkfw8oySROjE3yf9XDeaIkQ==}
engines: {node: '>=22'} engines: {node: '>=22'}
'@vitest/mocker@5.0.2': '@vitest/mocker@5.0.3':
resolution: {integrity: sha512-Z5FS00Q1SJHkB35xATsmWGdQ5WA1/0MV3CDjqyv7GavHv1OfOj145MNfHOlHk7QLes21dKFDHr8EO2zvL+9WGA==} resolution: {integrity: sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==}
peerDependencies: peerDependencies:
msw: ^2.4.9 msw: ^2.4.9
vite: ^6.0.0 || ^7.0.0 || ^8.0.0 vite: ^6.0.0 || ^7.0.0 || ^8.0.0
@ -643,8 +643,8 @@ packages:
vite: vite:
optional: true optional: true
'@vitest/spy@5.0.2': '@vitest/spy@5.0.3':
resolution: {integrity: sha512-Ijc7T1nT9efNb5LxvjaBrEqw3f/QwUv5EE0nKqZxgqsaV/FxAAZ8baGylA8X/Z2oS4Lp+K74Jr6dTJsDKxJDeg==} resolution: {integrity: sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==}
acorn-jsx@5.3.2: acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
@ -769,8 +769,8 @@ packages:
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24} engines: {node: ^20.19.0 || ^22.13.0 || >=24}
eslint@10.11.0: eslint@10.12.0:
resolution: {integrity: sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==} resolution: {integrity: sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24} engines: {node: ^20.19.0 || ^22.13.0 || >=24}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@ -1195,8 +1195,8 @@ packages:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
typescript-eslint@8.70.0: typescript-eslint@8.71.0:
resolution: {integrity: sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==} resolution: {integrity: sha512-fBdHYiqQ14RW6mOMXD14Svn82ZsCYAoQSzGRzyEjR59S5A2Krh/l7fGTOQ7iCr8gGy/mHVXtEF7s5fgjEdV0Pw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
@ -1260,20 +1260,20 @@ packages:
yaml: yaml:
optional: true optional: true
vitest@5.0.2: vitest@5.0.3:
resolution: {integrity: sha512-7MQrx9pDv5aHiUcovIb/70Ys3tgtkUVgCtledvKdCmEO+/1Dicq5ZqoSxOW034m03oqC+oHOKui2dM6qtMLoJg==} resolution: {integrity: sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==}
engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
'@edge-runtime/vm': '*' '@edge-runtime/vm': '*'
'@opentelemetry/api': ^1.9.0 '@opentelemetry/api': ^1.9.0
'@types/node': ^22.0.0 || >=24.0.0 '@types/node': ^22.0.0 || >=24.0.0
'@vitest/browser-playwright': 5.0.2 '@vitest/browser-playwright': 5.0.3
'@vitest/browser-preview': 5.0.2 '@vitest/browser-preview': 5.0.3
'@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0
'@vitest/coverage-istanbul': 5.0.2 '@vitest/coverage-istanbul': 5.0.3
'@vitest/coverage-v8': 5.0.2 '@vitest/coverage-v8': 5.0.3
'@vitest/ui': 5.0.2 '@vitest/ui': 5.0.3
happy-dom: '*' happy-dom: '*'
jsdom: '*' jsdom: '*'
vite: ^6.4.0 || ^7.0.0 || ^8.0.0 vite: ^6.4.0 || ^7.0.0 || ^8.0.0
@ -1326,8 +1326,8 @@ packages:
engines: {node: '>= 8'} engines: {node: '>= 8'}
hasBin: true hasBin: true
why-is-node-running@3.2.2: why-is-node-running@3.2.1:
resolution: {integrity: sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A==} resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==}
engines: {node: '>=20.11'} engines: {node: '>=20.11'}
hasBin: true hasBin: true
@ -1428,9 +1428,9 @@ snapshots:
'@csstools/css-tokenizer@4.0.0': {} '@csstools/css-tokenizer@4.0.0': {}
'@eslint-community/eslint-utils@4.10.1(eslint@10.11.0(supports-color@7.2.0))': '@eslint-community/eslint-utils@4.10.1(eslint@10.12.0(supports-color@7.2.0))':
dependencies: dependencies:
eslint: 10.11.0(supports-color@7.2.0) eslint: 10.12.0(supports-color@7.2.0)
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {} '@eslint-community/regexpp@4.12.2': {}
@ -1451,9 +1451,9 @@ snapshots:
dependencies: dependencies:
'@types/json-schema': 7.0.15 '@types/json-schema': 7.0.15
'@eslint/js@10.0.1(eslint@10.11.0(supports-color@7.2.0))': '@eslint/js@10.0.1(eslint@10.12.0(supports-color@7.2.0))':
optionalDependencies: optionalDependencies:
eslint: 10.11.0(supports-color@7.2.0) eslint: 10.12.0(supports-color@7.2.0)
'@eslint/object-schema@3.0.5': {} '@eslint/object-schema@3.0.5': {}
@ -1557,7 +1557,7 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
pretty-format: 27.5.1 pretty-format: 27.5.1
'@testing-library/jest-dom@7.0.1(@testing-library/dom@10.4.1)(vitest@5.0.2)': '@testing-library/jest-dom@7.0.1(@testing-library/dom@10.4.1)(vitest@5.0.3)':
dependencies: dependencies:
'@adobe/css-tools': 4.5.0 '@adobe/css-tools': 4.5.0
'@testing-library/dom': 10.4.1 '@testing-library/dom': 10.4.1
@ -1567,7 +1567,7 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
redent: 3.0.0 redent: 3.0.0
optionalDependencies: optionalDependencies:
vitest: 5.0.2(@types/node@26.6.3)(@vitest/coverage-v8@5.0.2)(jsdom@30.1.1)(vite@8.2.2(@types/node@26.6.3)) vitest: 5.0.3(@types/node@26.6.4)(@vitest/coverage-v8@5.0.3)(jsdom@30.1.1)(vite@8.2.2(@types/node@26.6.4))
'@testing-library/react@16.3.3(@testing-library/dom@10.4.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': '@testing-library/react@16.3.3(@testing-library/dom@10.4.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies: dependencies:
@ -1598,7 +1598,7 @@ snapshots:
'@types/json-schema@7.0.15': {} '@types/json-schema@7.0.15': {}
'@types/node@26.6.3': '@types/node@26.6.4':
dependencies: dependencies:
undici-types: 8.9.0 undici-types: 8.9.0
@ -1610,15 +1610,15 @@ snapshots:
dependencies: dependencies:
csstype: 3.2.3 csstype: 3.2.3
'@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': '@typescript-eslint/eslint-plugin@8.71.0(@typescript-eslint/parser@8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
dependencies: dependencies:
'@eslint-community/regexpp': 4.12.2 '@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/parser': 8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
'@typescript-eslint/scope-manager': 8.70.0 '@typescript-eslint/scope-manager': 8.71.0
'@typescript-eslint/type-utils': 8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/type-utils': 8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
'@typescript-eslint/utils': 8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/utils': 8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
'@typescript-eslint/visitor-keys': 8.70.0 '@typescript-eslint/visitor-keys': 8.71.0
eslint: 10.11.0(supports-color@7.2.0) eslint: 10.12.0(supports-color@7.2.0)
ignore: 7.0.6 ignore: 7.0.6
natural-compare: 1.4.0 natural-compare: 1.4.0
ts-api-utils: 2.5.0(typescript@6.0.3) ts-api-utils: 2.5.0(typescript@6.0.3)
@ -1626,56 +1626,56 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/parser@8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': '@typescript-eslint/parser@8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
dependencies: dependencies:
'@typescript-eslint/scope-manager': 8.70.0 '@typescript-eslint/scope-manager': 8.71.0
'@typescript-eslint/types': 8.70.0 '@typescript-eslint/types': 8.71.0
'@typescript-eslint/typescript-estree': 8.70.0(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/typescript-estree': 8.71.0(supports-color@7.2.0)(typescript@6.0.3)
'@typescript-eslint/visitor-keys': 8.70.0 '@typescript-eslint/visitor-keys': 8.71.0
debug: 4.4.3(supports-color@7.2.0) debug: 4.4.3(supports-color@7.2.0)
eslint: 10.11.0(supports-color@7.2.0) eslint: 10.12.0(supports-color@7.2.0)
typescript: 6.0.3 typescript: 6.0.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/project-service@8.70.0(supports-color@7.2.0)(typescript@6.0.3)': '@typescript-eslint/project-service@8.71.0(supports-color@7.2.0)(typescript@6.0.3)':
dependencies: dependencies:
'@typescript-eslint/tsconfig-utils': 8.70.0(typescript@6.0.3) '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@6.0.3)
'@typescript-eslint/types': 8.70.0 '@typescript-eslint/types': 8.71.0
debug: 4.4.3(supports-color@7.2.0) debug: 4.4.3(supports-color@7.2.0)
typescript: 6.0.3 typescript: 6.0.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/scope-manager@8.70.0': '@typescript-eslint/scope-manager@8.71.0':
dependencies: dependencies:
'@typescript-eslint/types': 8.70.0 '@typescript-eslint/types': 8.71.0
'@typescript-eslint/visitor-keys': 8.70.0 '@typescript-eslint/visitor-keys': 8.71.0
'@typescript-eslint/tsconfig-utils@8.70.0(typescript@6.0.3)': '@typescript-eslint/tsconfig-utils@8.71.0(typescript@6.0.3)':
dependencies: dependencies:
typescript: 6.0.3 typescript: 6.0.3
'@typescript-eslint/type-utils@8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': '@typescript-eslint/type-utils@8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
dependencies: dependencies:
'@typescript-eslint/types': 8.70.0 '@typescript-eslint/types': 8.71.0
'@typescript-eslint/typescript-estree': 8.70.0(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/typescript-estree': 8.71.0(supports-color@7.2.0)(typescript@6.0.3)
'@typescript-eslint/utils': 8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/utils': 8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
debug: 4.4.3(supports-color@7.2.0) debug: 4.4.3(supports-color@7.2.0)
eslint: 10.11.0(supports-color@7.2.0) eslint: 10.12.0(supports-color@7.2.0)
ts-api-utils: 2.5.0(typescript@6.0.3) ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3 typescript: 6.0.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/types@8.70.0': {} '@typescript-eslint/types@8.71.0': {}
'@typescript-eslint/typescript-estree@8.70.0(supports-color@7.2.0)(typescript@6.0.3)': '@typescript-eslint/typescript-estree@8.71.0(supports-color@7.2.0)(typescript@6.0.3)':
dependencies: dependencies:
'@typescript-eslint/project-service': 8.70.0(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/project-service': 8.71.0(supports-color@7.2.0)(typescript@6.0.3)
'@typescript-eslint/tsconfig-utils': 8.70.0(typescript@6.0.3) '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@6.0.3)
'@typescript-eslint/types': 8.70.0 '@typescript-eslint/types': 8.71.0
'@typescript-eslint/visitor-keys': 8.70.0 '@typescript-eslint/visitor-keys': 8.71.0
debug: 4.4.3(supports-color@7.2.0) debug: 4.4.3(supports-color@7.2.0)
minimatch: 10.2.6 minimatch: 10.2.6
semver: 7.8.5 semver: 7.8.5
@ -1685,23 +1685,23 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/utils@8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': '@typescript-eslint/utils@8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.10.1(eslint@10.11.0(supports-color@7.2.0)) '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0(supports-color@7.2.0))
'@typescript-eslint/scope-manager': 8.70.0 '@typescript-eslint/scope-manager': 8.71.0
'@typescript-eslint/types': 8.70.0 '@typescript-eslint/types': 8.71.0
'@typescript-eslint/typescript-estree': 8.70.0(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/typescript-estree': 8.71.0(supports-color@7.2.0)(typescript@6.0.3)
eslint: 10.11.0(supports-color@7.2.0) eslint: 10.12.0(supports-color@7.2.0)
typescript: 6.0.3 typescript: 6.0.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/visitor-keys@8.70.0': '@typescript-eslint/visitor-keys@8.71.0':
dependencies: dependencies:
'@typescript-eslint/types': 8.70.0 '@typescript-eslint/types': 8.71.0
eslint-visitor-keys: 5.0.1 eslint-visitor-keys: 5.0.1
'@vitest/coverage-v8@5.0.2(vitest@5.0.2)': '@vitest/coverage-v8@5.0.3(vitest@5.0.3)':
dependencies: dependencies:
'@bcoe/v8-coverage': 1.0.2 '@bcoe/v8-coverage': 1.0.2
'@vitest/istanbul-lib-coverage': 1.0.1 '@vitest/istanbul-lib-coverage': 1.0.1
@ -1711,7 +1711,7 @@ snapshots:
obug: 2.1.4 obug: 2.1.4
std-env: 4.2.0 std-env: 4.2.0
tinyrainbow: 3.1.1 tinyrainbow: 3.1.1
vitest: 5.0.2(@types/node@26.6.3)(@vitest/coverage-v8@5.0.2)(jsdom@30.1.1)(vite@8.2.2(@types/node@26.6.3)) vitest: 5.0.3(@types/node@26.6.4)(@vitest/coverage-v8@5.0.3)(jsdom@30.1.1)(vite@8.2.2(@types/node@26.6.4))
'@vitest/istanbul-lib-coverage@1.0.1': {} '@vitest/istanbul-lib-coverage@1.0.1': {}
@ -1719,16 +1719,16 @@ snapshots:
dependencies: dependencies:
'@vitest/istanbul-lib-coverage': 1.0.1 '@vitest/istanbul-lib-coverage': 1.0.1
'@vitest/mocker@5.0.2(vite@8.2.2(@types/node@26.6.3))': '@vitest/mocker@5.0.3(vite@8.2.2(@types/node@26.6.4))':
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.31 '@jridgewell/trace-mapping': 0.3.31
'@vitest/spy': 5.0.2 '@vitest/spy': 5.0.3
estree-walker: 3.0.3 estree-walker: 3.0.3
magic-string: 1.3.1 magic-string: 1.3.1
optionalDependencies: optionalDependencies:
vite: 8.2.2(@types/node@26.6.3) vite: 8.2.2(@types/node@26.6.4)
'@vitest/spy@5.0.2': {} '@vitest/spy@5.0.3': {}
acorn-jsx@5.3.2(acorn@8.18.0): acorn-jsx@5.3.2(acorn@8.18.0):
dependencies: dependencies:
@ -1838,9 +1838,9 @@ snapshots:
eslint-visitor-keys@5.0.1: {} eslint-visitor-keys@5.0.1: {}
eslint@10.11.0(supports-color@7.2.0): eslint@10.12.0(supports-color@7.2.0):
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.10.1(eslint@10.11.0(supports-color@7.2.0)) '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0(supports-color@7.2.0))
'@eslint-community/regexpp': 4.12.2 '@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.23.5(supports-color@7.2.0) '@eslint/config-array': 0.23.5(supports-color@7.2.0)
'@eslint/config-helpers': 0.7.0 '@eslint/config-helpers': 0.7.0
@ -2239,13 +2239,13 @@ snapshots:
dependencies: dependencies:
prelude-ls: 1.2.1 prelude-ls: 1.2.1
typescript-eslint@8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): typescript-eslint@8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3):
dependencies: dependencies:
'@typescript-eslint/eslint-plugin': 8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/eslint-plugin': 8.71.0(@typescript-eslint/parser@8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
'@typescript-eslint/parser': 8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/parser': 8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
'@typescript-eslint/typescript-estree': 8.70.0(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/typescript-estree': 8.71.0(supports-color@7.2.0)(typescript@6.0.3)
'@typescript-eslint/utils': 8.70.0(eslint@10.11.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/utils': 8.71.0(eslint@10.12.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
eslint: 10.11.0(supports-color@7.2.0) eslint: 10.12.0(supports-color@7.2.0)
typescript: 6.0.3 typescript: 6.0.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -2260,7 +2260,7 @@ snapshots:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
vite@8.2.2(@types/node@26.6.3): vite@8.2.2(@types/node@26.6.4):
dependencies: dependencies:
lightningcss: 1.33.0 lightningcss: 1.33.0
picomatch: 4.0.7 picomatch: 4.0.7
@ -2268,13 +2268,13 @@ snapshots:
rolldown: 1.2.6 rolldown: 1.2.6
tinyglobby: 0.2.17 tinyglobby: 0.2.17
optionalDependencies: optionalDependencies:
'@types/node': 26.6.3 '@types/node': 26.6.4
fsevents: 2.3.3 fsevents: 2.3.3
vitest@5.0.2(@types/node@26.6.3)(@vitest/coverage-v8@5.0.2)(jsdom@30.1.1)(vite@8.2.2(@types/node@26.6.3)): vitest@5.0.3(@types/node@26.6.4)(@vitest/coverage-v8@5.0.3)(jsdom@30.1.1)(vite@8.2.2(@types/node@26.6.4)):
dependencies: dependencies:
'@types/chai': 5.2.3 '@types/chai': 5.2.3
'@vitest/mocker': 5.0.2(vite@8.2.2(@types/node@26.6.3)) '@vitest/mocker': 5.0.3(vite@8.2.2(@types/node@26.6.4))
chai: 6.2.2 chai: 6.2.2
es-module-lexer: 2.3.2 es-module-lexer: 2.3.2
expect-type: 1.4.0 expect-type: 1.4.0
@ -2285,11 +2285,11 @@ snapshots:
tinybench: 6.1.4 tinybench: 6.1.4
tinyexec: 1.3.0 tinyexec: 1.3.0
tinyglobby: 0.2.17 tinyglobby: 0.2.17
vite: 8.2.2(@types/node@26.6.3) vite: 8.2.2(@types/node@26.6.4)
why-is-node-running: 3.2.2 why-is-node-running: 3.2.1
optionalDependencies: optionalDependencies:
'@types/node': 26.6.3 '@types/node': 26.6.4
'@vitest/coverage-v8': 5.0.2(vitest@5.0.2) '@vitest/coverage-v8': 5.0.3(vitest@5.0.3)
jsdom: 30.1.1 jsdom: 30.1.1
transitivePeerDependencies: transitivePeerDependencies:
- msw - msw
@ -2322,7 +2322,7 @@ snapshots:
dependencies: dependencies:
isexe: 2.0.0 isexe: 2.0.0
why-is-node-running@3.2.2: {} why-is-node-running@3.2.1: {}
word-wrap@1.2.5: {} word-wrap@1.2.5: {}