mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 19:27:16 +01:00
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:
parent
ba6f8b09fd
commit
6c71d531bb
19
.github/workflows/repo-contract.yml
vendored
Normal file
19
.github/workflows/repo-contract.yml
vendored
Normal 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
|
||||||
14
.github/workflows/shared-gates-tests.yml
vendored
14
.github/workflows/shared-gates-tests.yml
vendored
@ -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
|
||||||
|
|||||||
@ -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
17
CLAUDE.md
Normal 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`
|
||||||
7
coverage_gaps/__init__.py
Normal file
7
coverage_gaps/__init__.py
Normal 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.
|
||||||
|
"""
|
||||||
5
coverage_gaps/__main__.py
Normal file
5
coverage_gaps/__main__.py
Normal 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
16
coverage_gaps/bin/coverage-gaps
Executable 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
63
coverage_gaps/cli.py
Normal 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
69
coverage_gaps/models.py
Normal 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
132
coverage_gaps/parsers.py
Normal 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
102
coverage_gaps/render.py
Normal 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"
|
||||||
0
coverage_gaps/tests/__init__.py
Normal file
0
coverage_gaps/tests/__init__.py
Normal file
89
coverage_gaps/tests/conftest.py
Normal file
89
coverage_gaps/tests/conftest.py
Normal 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
|
||||||
73
coverage_gaps/tests/test_parsers.py
Normal file
73
coverage_gaps/tests/test_parsers.py
Normal 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)
|
||||||
97
coverage_gaps/tests/test_render_cli.py
Normal file
97
coverage_gaps/tests/test_render_cli.py
Normal 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
|
||||||
@ -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"
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
66
repo_contract/README.md
Normal 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
|
||||||
|
```
|
||||||
1
repo_contract/__init__.py
Normal file
1
repo_contract/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Repo contract: every repo documents exact run/test/lint/coverage commands."""
|
||||||
123
repo_contract/bootstrap.py
Normal file
123
repo_contract/bootstrap.py
Normal 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
142
repo_contract/check.py
Normal 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
134
repo_contract/contract.py
Normal 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
241
repo_contract/infer.py
Normal 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
87
repo_contract/sources.py
Normal 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
118
repo_contract/stacks.py
Normal 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
102
repo_contract/sweep.py
Normal 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())
|
||||||
26
repo_contract/templates/check_repo_contract.shim.sh
Executable file
26
repo_contract/templates/check_repo_contract.shim.sh
Executable 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 "$@"
|
||||||
41
repo_contract/templates/flutter.sh
Executable file
41
repo_contract/templates/flutter.sh
Executable 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
29
repo_contract/templates/go.sh
Executable 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[@]}"
|
||||||
25
repo_contract/templates/godot.sh
Executable file
25
repo_contract/templates/godot.sh
Executable 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
|
||||||
21
repo_contract/templates/gradle.sh
Executable file
21
repo_contract/templates/gradle.sh
Executable 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
38
repo_contract/templates/node.sh
Executable 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
|
||||||
9
repo_contract/templates/pre-commit-snippet.yaml
Normal file
9
repo_contract/templates/pre-commit-snippet.yaml
Normal 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)$
|
||||||
50
repo_contract/templates/python.sh
Executable file
50
repo_contract/templates/python.sh
Executable 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
|
||||||
25
repo_contract/templates/repo-contract.yml
Normal file
25
repo_contract/templates/repo-contract.yml
Normal 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
21
repo_contract/templates/rust.sh
Executable 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
|
||||||
47
repo_contract/templates/shell.sh
Executable file
47
repo_contract/templates/shell.sh
Executable 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[@]}"
|
||||||
0
repo_contract/tests/__init__.py
Normal file
0
repo_contract/tests/__init__.py
Normal file
49
repo_contract/tests/conftest.py
Normal file
49
repo_contract/tests/conftest.py
Normal 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
|
||||||
108
repo_contract/tests/test_bootstrap.py
Normal file
108
repo_contract/tests/test_bootstrap.py
Normal 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>"]
|
||||||
134
repo_contract/tests/test_check.py
Normal file
134
repo_contract/tests/test_check.py
Normal 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)
|
||||||
73
repo_contract/tests/test_contract.py
Normal file
73
repo_contract/tests/test_contract.py
Normal 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"
|
||||||
121
repo_contract/tests/test_infer.py
Normal file
121
repo_contract/tests/test_infer.py
Normal 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)
|
||||||
63
repo_contract/tests/test_sources.py
Normal file
63
repo_contract/tests/test_sources.py
Normal 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
|
||||||
76
repo_contract/tests/test_stacks.py
Normal file
76
repo_contract/tests/test_stacks.py
Normal 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
|
||||||
45
repo_contract/tests/test_sweep.py
Normal file
45
repo_contract/tests/test_sweep.py
Normal 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()
|
||||||
109
repo_contract/tests/test_templates.py
Normal file
109
repo_contract/tests/test_templates.py
Normal 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
25
scripts/check_repo_contract.sh
Executable 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
48
scripts/test_changed.sh
Executable 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
|
||||||
@ -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.
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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: {}
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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: {}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user