diff --git a/.github/workflows/python-quality.yml b/.github/workflows/python-quality.yml index 7362ade3..d07164d4 100644 --- a/.github/workflows/python-quality.yml +++ b/.github/workflows/python-quality.yml @@ -286,6 +286,47 @@ jobs: /bin/bash --version /bin/bash plugins/foundation/darrow-decisions/backend/tests/decision.test.sh + information-architecture-windows: + name: Information architecture Python ${{ matrix.python-version }} on windows-latest + needs: changes + if: contains(needs.changes.outputs.packages, 'plugins/foundation/darrow-information-architecture/backend') + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + runs-on: windows-latest + defaults: + run: + shell: bash + env: + UV_PYTHON: ${{ matrix.python-version }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + - run: scripts/check-python --package plugins/foundation/darrow-information-architecture/backend + + information-architecture-fresh-install: + name: Fresh install information architecture on ${{ matrix.os }} + needs: changes + if: contains(needs.changes.outputs.packages, 'plugins/foundation/darrow-information-architecture/backend') + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - uses: astral-sh/setup-uv@v6 + - run: uv run --quiet --frozen --no-dev --project plugins/foundation/darrow-information-architecture/backend python plugins/foundation/darrow-information-architecture/backend/tests/fresh_install.py + inventory: name: Python inventory guard runs-on: ubuntu-latest @@ -425,6 +466,8 @@ jobs: - tickets-fresh-install - decisions-windows - decisions-fresh-install + - information-architecture-windows + - information-architecture-fresh-install - inventory - skill-authoring-fresh-install - observability-fresh-install @@ -461,6 +504,9 @@ jobs: DECISIONS_SELECTED: ${{ contains(needs.changes.outputs.packages, 'plugins/foundation/darrow-decisions/backend') }} DECISIONS_WINDOWS_RESULT: ${{ needs.decisions-windows.result }} DECISIONS_INSTALL_RESULT: ${{ needs.decisions-fresh-install.result }} + IA_SELECTED: ${{ contains(needs.changes.outputs.packages, 'plugins/foundation/darrow-information-architecture/backend') }} + IA_WINDOWS_RESULT: ${{ needs.information-architecture-windows.result }} + IA_INSTALL_RESULT: ${{ needs.information-architecture-fresh-install.result }} run: | scripts/verify-python-quality-results \ "$CHANGES_RESULT" "$INVENTORY_RESULT" \ @@ -484,3 +530,7 @@ jobs: if [ "$DECISIONS_SELECTED" = true ]; then expected=success; fi test "$DECISIONS_WINDOWS_RESULT" = "$expected" test "$DECISIONS_INSTALL_RESULT" = "$expected" + expected=skipped + if [ "$IA_SELECTED" = true ]; then expected=success; fi + test "$IA_WINDOWS_RESULT" = "$expected" + test "$IA_INSTALL_RESULT" = "$expected" diff --git a/docs/specs/information-architecture.md b/docs/specs/information-architecture.md index 7e06f387..01b136b5 100644 --- a/docs/specs/information-architecture.md +++ b/docs/specs/information-architecture.md @@ -7,6 +7,42 @@ reachable at the moment it matters. Plugin: `darrow-information-architecture`. Skills: `setup-information-architecture`, `doctor-information-architecture`. +## Portable mechanics + +- **IA-P1 — Contained locked execution.** Setup inspection and doctor + inspection/verification run through the plugin's frozen UV package on Python + 3.10–3.13, Linux, macOS, and native Windows. Preserve inventory groups, + diagnostics, summary records, and statuses: 64 for invalid arguments, 2 for + inaccessible repository/input, 1 for critical verification findings, and 0 + for completed inspection or successful verification. No Bash runtime shim is + required. Setup and doctor retain separate intent and judgment in their skills. +- **IA-P2 — Filesystem semantics.** Resolve the primary worktree from Git's + first porcelain record, otherwise the supplied directory. Accept native + repository paths, including Windows drives and UNC paths. Interpret portable + guidance routes with forward slashes; accept Windows separators on Windows. + Match instruction names with their documented case even on case-insensitive + filesystems. Recognize CRLF, LF, and UTF-8 BOM guidance without changing bytes. + Emit UTF-8 records even when a native pipe defaults to a legacy code page, + so non-Latin repository paths remain reportable. + Follow file adapters only within the repository; report broken, looping, + external, unreadable, or non-file dependencies rather than silently dropping + selected evidence. Existing native imports outside the repository retain + their host-owned semantics and are outside the repository audit scope. + Do not recurse into directory symlinks. An unavailable + native symlink facility never authorizes replacing an existing adapter. +- **IA-P3 — Safe guidance updates.** Inventory and verification are read-only. + Approved edits preserve the source of truth, newline convention, and file + permissions. Publish complete replacement content atomically from a temporary + file in the destination directory, preserving symlink adapters by updating + their canonical source. A failed replacement leaves original bytes intact; + concurrent source changes must be detected before replacement. Multi-file + edits are not a transaction and must be verified as a complete graph. +- **IA-P4 — Release evidence.** Retain regression evidence for routing, + reachability, native scope, adapters, duplicate content, stale references, + refusals, and output. Enforce the repository Python quality gates, property + tests, both skill eval suites, and fresh copied-plugin execution using only + locked runtime dependencies on all three native platforms. + ## Why Large root instruction files spend context on every task. Bare repositories diff --git a/plugins/foundation/darrow-information-architecture/.claude-plugin/plugin.json b/plugins/foundation/darrow-information-architecture/.claude-plugin/plugin.json index c853d69b..3782aafe 100644 --- a/plugins/foundation/darrow-information-architecture/.claude-plugin/plugin.json +++ b/plugins/foundation/darrow-information-architecture/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "darrow-information-architecture", "description": "Set up and doctor lean, routed repository information architecture", - "version": "0.1.5", + "version": "0.2.1", "license": "BUSL-1.1" } diff --git a/plugins/foundation/darrow-information-architecture/.codex-plugin/plugin.json b/plugins/foundation/darrow-information-architecture/.codex-plugin/plugin.json index 3b4fc5ec..bb0ec865 100644 --- a/plugins/foundation/darrow-information-architecture/.codex-plugin/plugin.json +++ b/plugins/foundation/darrow-information-architecture/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "darrow-information-architecture", - "version": "0.1.5", + "version": "0.2.1", "description": "Set up and doctor lean, routed repository information architecture", "author": { "name": "Björn Rochel", diff --git a/plugins/foundation/darrow-information-architecture/README.md b/plugins/foundation/darrow-information-architecture/README.md index 2ddde1fb..05bd9fd9 100644 --- a/plugins/foundation/darrow-information-architecture/README.md +++ b/plugins/foundation/darrow-information-architecture/README.md @@ -32,10 +32,10 @@ Example: _“Doctor this repository's agent instructions.”_ ### Bundled inspectors -`skills/setup-information-architecture/scripts/ia-setup.sh` inventories the -repository surfaces needed to design an instruction graph. `bin/ia-doctor` -inspects routes, runtime reachability, adapter drift, duplicates, cycles, and -root context size; the doctor skill reaches it through its colocated script. +The contained `backend/` package supplies `ia-setup inspect`, `ia-doctor +inspect|verify`, and `ia-write` for approved atomic guidance replacements. Run +all commands through `uv run --quiet --frozen --no-dev --project /backend`. +The inspectors are read-only; policy and edit decisions remain in the skills. These are compact structural checks, not complete validators for every Codex or Claude Code configuration format. @@ -70,7 +70,31 @@ Organize or audit repository agent guidance. Use another capability for product ## Hosts and prerequisites -Codex and Claude Code; repository file access, Bash, and baseline Unix tools. +Codex and Claude Code; UV, Python 3.10–3.13, and repository file access on +Linux, macOS, or native Windows. Git resolves the primary worktree when available; +plain directories work without it. The runtime package has no third-party dependencies. + +Paths use native filesystem identity while guidance names require exact documented +case. LF, CRLF, and UTF-8 BOM guidance are supported. Directory symlinks are not +traversed; selected broken, looping, external file adapters and unreadable evidence +fail closed. Native imports outside the repository remain host-owned and outside +this compact repository audit. Windows symlink privileges are not assumed. +Output records use UTF-8 on every host, including redirected Windows output. + +Approved updates preserve adapter sources, permissions, and newline conventions. +See [atomic guidance updates](references/file-updates.md) for the digest-checked +writer and its concurrency boundary. Inventory and verification never write guidance. + +## Validation + +The package follows the contained Python + UV migration pattern: committed lock, +separate development tools, Ruff complexity ≤5, strict mypy, property tests, and +separate 95% line/branch coverage gates. Run `scripts/check-python --package +plugins/foundation/darrow-information-architecture/backend` from the Darrow checkout. +The setup and doctor regressions run in pytest across all supported hosts; +no Bash test harness or runtime launcher remains. +The fresh-artifact test copies only this plugin and exercises all public entrypoints +with locked runtime dependencies on Linux, macOS, and native Windows in CI. ## Installation diff --git a/plugins/foundation/darrow-information-architecture/backend/pyproject.toml b/plugins/foundation/darrow-information-architecture/backend/pyproject.toml new file mode 100644 index 00000000..ff18ca1a --- /dev/null +++ b/plugins/foundation/darrow-information-architecture/backend/pyproject.toml @@ -0,0 +1,59 @@ +[project] +name = "darrow-information-architecture" +version = "0.2.1" +description = "Portable repository guidance inventory and verification" +requires-python = ">=3.10,<3.14" +dependencies = [] + +[project.scripts] +ia-doctor = "darrow_ia.cli:doctor_entrypoint" +ia-setup = "darrow_ia.cli:setup_entrypoint" +ia-write = "darrow_ia.updates:entrypoint" + +[dependency-groups] +dev = [ + "coverage[toml]>=7.10,<8", + "hypothesis>=6.138,<7", + "mypy>=1.18,<2", + "pytest>=8.4,<9", + "ruff>=0.12,<1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.uv] +default-groups = [] + +[tool.hatch.build.targets.wheel] +packages = ["src/darrow_ia"] + +[tool.ruff] +target-version = "py310" +line-length = 88 +src = ["src", "tests"] + +[tool.ruff.lint] +select = ["B", "C4", "C90", "E4", "E7", "E9", "F", "I", "N", "RUF", "SIM", "UP"] +mccabe.max-complexity = 5 + +[tool.ruff.format] +docstring-code-format = true + +[tool.mypy] +python_version = "3.10" +strict = true +files = ["src", "tests"] + +[tool.pytest.ini_options] +addopts = ["--strict-config", "--strict-markers"] +testpaths = ["tests"] + +[tool.coverage.run] +branch = true +source = ["darrow_ia"] + +[tool.coverage.report] +show_missing = true +skip_covered = false diff --git a/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/__init__.py b/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/__init__.py new file mode 100644 index 00000000..126b3527 --- /dev/null +++ b/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/__init__.py @@ -0,0 +1 @@ +"""Portable repository guidance mechanics.""" diff --git a/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/checks.py b/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/checks.py new file mode 100644 index 00000000..e2685e99 --- /dev/null +++ b/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/checks.py @@ -0,0 +1,177 @@ +"""Adapter, duplicate-content, and root-budget checks.""" + +import hashlib +import os +import re +from collections import defaultdict +from pathlib import Path, PureWindowsPath + +from darrow_ia.filesystem import ROOT_NAMES, inside, read_text, readable, walk +from darrow_ia.graph import Audit + + +def mirror_paths(spec: str, root: Path) -> tuple[Path, Path]: + if "=" not in spec: + raise ValueError(f"mirror must be source=target: {spec}") + parts = spec.split("=", 1) + if not all(valid_mirror_path(part) for part in parts): + raise ValueError(f"mirror paths must stay inside the repository: {spec}") + paths = (root / parts[0], root / parts[1]) + if not all(inside(path.resolve(), root) for path in paths): + raise ValueError(f"mirror paths must stay inside the repository: {spec}") + return paths + + +def valid_mirror_path(value: str) -> bool: + windows = PureWindowsPath(value) + return ( + bool(value) + and not windows.drive + and not windows.root + and not Path(value).is_absolute() + and ".." not in windows.parts + ) + + +def mirror_files(directory: Path) -> dict[str, bytes]: + files, _ = walk(directory, prune=False) + return { + path.relative_to(directory).as_posix(): path.read_bytes() + for path in files + if path.name != ".DS_Store" + } + + +def compare_mirror(source: Path, target: Path) -> list[str]: + prefix = f"{source} -> {target}" + if not all( + path.is_dir() and os.access(path, os.R_OK | os.X_OK) + for path in (source, target) + ): + return [ + f"{prefix} | declared mirror source or target is unreadable, unsearchable, or missing" + ] + try: + left, right = mirror_files(source), mirror_files(target) + except OSError as error: + return [f"{prefix} | traversal failed: {error}"] + if left.keys() != right.keys(): + return [f"{prefix} | file sets differ"] + return [ + f"{prefix} | content differs: {name}" + for name in sorted(left) + if left[name] != right[name] + ] + + +def check_mirrors(audit: Audit, specs: list[str]) -> set[Path]: + extra: set[Path] = set() + for spec in specs: + source, target = mirror_paths(spec, audit.root) + drift = compare_mirror(source, target) + audit.findings["adapter-drift"].update(drift) + status = "drift" if drift else "aligned" + audit.mirrors.append(f"{source} -> {target} | status={status} | declared=true") + extra.update(mirror_guidance(source, target)) + return extra + + +def mirror_guidance(source: Path, target: Path) -> set[Path]: + extra: set[Path] = set() + for directory in (source, target): + if directory.is_dir() and os.access(directory, os.R_OK | os.X_OK): + files, _ = walk(directory) + extra.update(path for path in files if path.suffix == ".md") + return extra + + +def root_adapter(audit: Audit) -> tuple[Path, Path] | None: + agents, claude = audit.root / "AGENTS.md", audit.root / "CLAUDE.md" + for source, target in ((agents, claude), (claude, agents)): + if ( + source.is_file() + and target.is_symlink() + and target.is_file() + and source.samefile(target) + ): + audit.mirrors.insert(0, f"{source} -> {target} | status=symlink") + return source, target + return None + + +def duplicates( + audit: Audit, specs: list[str], adapter: tuple[Path, Path] | None +) -> None: + groups: dict[bytes, list[Path]] = defaultdict(list) + for path in sorted(audit.files): + if readable(path): + groups[hashlib.sha256(path.read_bytes()).digest()].append(path) + for paths in groups.values(): + if len(paths) > 1 and not intentional(paths, audit, specs, adapter): + audit.findings["duplicate-content"].add(" = ".join(map(str, paths))) + + +def intentional( + paths: list[Path], audit: Audit, specs: list[str], adapter: tuple[Path, Path] | None +) -> bool: + if adapter and set(paths) == set(adapter): + return True + return any(mirrored_group(paths, audit, spec) for spec in specs) + + +def mirrored_group(paths: list[Path], audit: Audit, spec: str) -> bool: + source, target = mirror_paths(spec, audit.root) + record = f"{source} -> {target} | status=aligned | declared=true" + return ( + record in audit.mirrors + and any(inside(path, source) for path in paths) + and any(inside(path, target) for path in paths) + ) + + +def root_budget(audit: Audit) -> int: + config = audit.root / ".codex/config.toml" + if audit.runtime == "claude" or not (config.exists() or config.is_symlink()): + return 32768 + if not audit.add(config): + return 32768 + match = re.search( + r"^\s*project_doc_max_bytes\s*=\s*([0-9]+)\s*(?:#.*)?$", + read_text(config), + re.MULTILINE, + ) + return int(match[1]) if match else 32768 + + +def metrics(audit: Audit) -> None: + budget = root_budget(audit) + for runtime, seeds in audit.seeds.items(): + for path in sorted(seeds): + root_metric(audit, runtime, path, budget) + for path in audit.files: + if audit.relative(path).startswith(".claude/rules/") and readable(path): + check_always_loaded(audit, path) + + +def root_metric(audit: Audit, runtime: str, path: Path, budget: int) -> None: + if not readable(path) or audit.relative(path) not in ROOT_NAMES: + return + size = path.stat().st_size + audit.root_bytes[runtime] += size + if runtime == "codex" and runtime in audit.runtimes and size > budget: + audit.findings["root-budget"].add(f"{path} bytes={size} limit={budget}") + + +def check_always_loaded(audit: Audit, path: Path) -> None: + try: + content = read_text(path) + except (OSError, UnicodeError): + # Selected dependencies are diagnosed by traversal. Advisory inventory + # must not let an unselected runtime block verification. + return + scoped = re.search(r"^\s*paths\s*:", content, re.MULTILINE) + universal = re.search( + r'paths:\s*\[[^\]]*"\*\*"|^\s*-\s*"\*\*"', content, re.MULTILINE + ) + if not scoped or universal: + audit.findings["always-loaded-rule"].add(str(path)) diff --git a/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/cli.py b/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/cli.py new file mode 100644 index 00000000..4416af93 --- /dev/null +++ b/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/cli.py @@ -0,0 +1,98 @@ +"""Public CLI boundaries with stable status codes.""" + +import argparse +import io +import sys +from collections.abc import Callable, Sequence +from typing import NoReturn + +from darrow_ia import checks, report, setup +from darrow_ia.filesystem import resolve_root +from darrow_ia.graph import ( + Audit, + check_reachability, + choose_runtime, + cycles, + inventory, + traverse, +) + +DOCTOR_USAGE = "usage: ia-doctor inspect|verify [--runtime auto|both|codex|claude] [--mirror source=target]... [repository]" +SETUP_USAGE = "usage: ia-setup inspect [repository]" + + +class UsageError(Exception): + """Arguments do not match the public command contract.""" + + +class Parser(argparse.ArgumentParser): + def error(self, message: str) -> NoReturn: + raise UsageError(self.usage) + + +def parser(doctor: bool) -> Parser: + result = Parser(add_help=False, usage=DOCTOR_USAGE if doctor else SETUP_USAGE) + result.add_argument( + "mode", choices=("inspect", "verify") if doctor else ("inspect",) + ) + if doctor: + result.add_argument( + "--runtime", choices=("auto", "both", "codex", "claude"), default="auto" + ) + result.add_argument("--mirror", action="append", default=[]) + result.add_argument("repository", nargs="?", default=".") + return result + + +def doctor(arguments: Sequence[str]) -> int: + args = parser(True).parse_intermixed_args(arguments) + root = resolve_root(args.repository) + for spec in args.mirror: + try: + checks.mirror_paths(spec, root) + except ValueError as error: + raise UsageError(f"error: {error}") from error + audit = Audit(root, choose_runtime(root, args.runtime)) + inventory(audit) + extra = checks.check_mirrors(audit, args.mirror) + traverse(audit, extra) + check_reachability(audit) + audit.findings["instruction-cycle"].update(cycles(audit.edges)) + checks.duplicates(audit, args.mirror, checks.root_adapter(audit)) + checks.metrics(audit) + print(report.render(audit), end="") + return int(args.mode == "verify" and report.failed(audit)) + + +def setup_command(arguments: Sequence[str]) -> int: + args = parser(False).parse_args(arguments) + print(setup.inspect(resolve_root(args.repository)), end="") + return 0 + + +def run(command: Callable[[Sequence[str]], int], arguments: Sequence[str]) -> int: + try: + configure_output() + return command(arguments) + except UsageError as error: + print(error, file=sys.stderr) + return 64 + except (OSError, ValueError, RuntimeError) as error: + # Process boundary: inaccessible evidence must never produce a passing audit. + print(f"error: {error}", file=sys.stderr) + return 2 + + +def configure_output() -> None: + """Keep machine-facing path records UTF-8 on native Windows pipes too.""" + for stream in (sys.stdout, sys.stderr): + if isinstance(stream, io.TextIOWrapper): + stream.reconfigure(encoding="utf-8", errors="backslashreplace") + + +def doctor_entrypoint() -> None: + sys.exit(run(doctor, sys.argv[1:])) + + +def setup_entrypoint() -> None: + sys.exit(run(setup_command, sys.argv[1:])) diff --git a/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/filesystem.py b/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/filesystem.py new file mode 100644 index 00000000..fb9fa927 --- /dev/null +++ b/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/filesystem.py @@ -0,0 +1,126 @@ +"""Repository identity and bounded, explicit filesystem operations.""" + +import os +import subprocess +from pathlib import Path, PureWindowsPath + +ENTRY_NAMES = {"AGENTS.md", "AGENTS.override.md", "CLAUDE.md", "CLAUDE.local.md"} +ROOT_NAMES = (*sorted(ENTRY_NAMES), ".claude/CLAUDE.md") +RULE_DIRS = ( + ".claude/rules", + ".agent-shared/rules", + ".agents/rules", + ".codex/rules", + ".pi/rules", +) + + +def readable(path: Path) -> bool: + return path.is_file() and os.access(path, os.R_OK) + + +def resolve_root(target: str) -> Path: + path = Path(target) + if not path.is_dir() or not os.access(path, os.R_OK): + raise ValueError(f"repository path is not a readable directory: {target}") + try: + result = subprocess.run( + ["git", "-C", str(path), "worktree", "list", "--porcelain", "-z"], + capture_output=True, + check=False, + ) + except FileNotFoundError: + return path.resolve() + output = os.fsdecode(result.stdout) + first = output.split("\0" if "\0" in output else "\n", 1)[0] + if result.returncode == 0 and first.startswith("worktree "): + return Path(first.removeprefix("worktree ")).resolve() + return path.resolve() + + +def inside(path: Path, root: Path) -> bool: + return path.is_relative_to(root) + + +def absolute_reference(ref: str) -> bool: + return Path(ref).is_absolute() or bool(PureWindowsPath(ref).drive) + + +def exact_path(path: Path) -> bool: + """Require portable spelling even when the underlying filesystem folds case.""" + while path != path.parent: + if path.name != ".." and path.name not in os.listdir(path.parent): + return False + path = path.parent + return True + + +def existing(path: Path, root: Path) -> Path | None: + try: + if not path.is_file() or not exact_path(path): + return None + # Preserve a file adapter's spelling for runtime metrics, but validate + # its actual destination. Resolve directory components for containment. + target = path.resolve(strict=True) + if not inside(target, root): + return None + return path.parent.resolve() / path.name + except (OSError, RuntimeError): + return None + + +def walk(root: Path, *, prune: bool = True) -> tuple[list[Path], list[Path]]: + files: list[Path] = [] + directories: list[Path] = [] + for parent, names, leaves in os.walk(root, onerror=raise_walk_error): + base = Path(parent) + names[:] = [ + name for name in names if not prune or allowed_directory(base / name, root) + ] + directories.extend(base / name for name in names) + files.extend(base / name for name in leaves) + files.extend(base / name for name in names if (base / name).is_symlink()) + names[:] = [name for name in names if not (base / name).is_symlink()] + return sorted(files), sorted(directories) + + +def raise_walk_error(error: OSError) -> None: + raise error + + +def allowed_directory(path: Path, root: Path) -> bool: + if path.name in {".git", "node_modules", ".worktrees", ".venv", "__pycache__"}: + return False + return path.relative_to(root).as_posix() != ".claude/worktrees" + + +def read_text(path: Path) -> str: + return path.read_bytes().decode("utf-8-sig") + + +def unreadable_reason(path: Path, root: Path) -> str | None: + try: + return dependency_reason(path, root) + except (OSError, RuntimeError, UnicodeError): + return str(path) + + +def dependency_reason(path: Path, root: Path) -> str | None: + reason = symlink_reason(path) + if reason: + return reason + if not inside(path.resolve(strict=True), root): + return f"{path} (external target)" + if not readable(path): + return str(path) + if path.suffix in {".md", ".toml"}: + read_text(path) + return None + + +def symlink_reason(path: Path) -> str | None: + if not path.is_symlink(): + return None + if not path.exists(): + return f"{path} (broken symlink)" + return None if path.is_file() else f"{path} (non-file symlink)" diff --git a/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/graph.py b/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/graph.py new file mode 100644 index 00000000..9c75d96a --- /dev/null +++ b/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/graph.py @@ -0,0 +1,213 @@ +"""Selected-runtime graph traversal and reachability.""" + +from collections import defaultdict +from dataclasses import dataclass, field +from pathlib import Path + +from darrow_ia.filesystem import ( + ENTRY_NAMES, + RULE_DIRS, + exact_path, + read_text, + unreadable_reason, + walk, +) +from darrow_ia.references import guidance_reference, references, resolve + + +@dataclass +class Audit: + root: Path + runtime: str + files: set[Path] = field(default_factory=set) + entrypoints: list[Path] = field(default_factory=list) + seeds: dict[str, set[Path]] = field(default_factory=dict) + edges: set[tuple[Path, Path]] = field(default_factory=set) + findings: dict[str, set[str]] = field(default_factory=lambda: defaultdict(set)) + mirrors: list[str] = field(default_factory=list) + root_bytes: dict[str, int] = field( + default_factory=lambda: {"codex": 0, "claude": 0} + ) + + @property + def runtimes(self) -> tuple[str, ...]: + return ("codex", "claude") if self.runtime == "both" else (self.runtime,) + + def relative(self, path: Path) -> str: + return path.relative_to(self.root).as_posix() + + def add(self, path: Path) -> bool: + self.files.add(path) + reason = unreadable_reason(path, self.root) + if reason: + self.findings["unreadable-guidance"].add(reason) + return reason is None + + +def choose_runtime(root: Path, runtime: str) -> str: + if runtime != "auto": + return runtime + codex = any( + named_entrypoint(root / name) for name in ("AGENTS.md", "AGENTS.override.md") + ) + claude = any( + named_entrypoint(root / name) for name in ("CLAUDE.md", ".claude/CLAUDE.md") + ) + if claude: + return "both" if codex else "claude" + return "codex" + + +def named_entrypoint(path: Path) -> bool: + return path.is_file() and exact_path(path) + + +def inventory(audit: Audit) -> None: + files, _ = walk(audit.root) + audit.entrypoints = [path for path in files if path.name in ENTRY_NAMES] + audit.files.update(audit.entrypoints) + rules = {path for path in files if is_rule(audit.relative(path))} + audit.files.update(rules) + override = audit.root / "AGENTS.override.md" + active = ( + "AGENTS.override.md" + if override in audit.entrypoints + and override.is_file() + and override.stat().st_size + else "AGENTS.md" + ) + audit.seeds = { + "codex": {path for path in audit.entrypoints if audit.relative(path) == active}, + "claude": { + path for path in audit.entrypoints if path.name.startswith("CLAUDE") + }, + } + audit.seeds["claude"].update( + path for path in rules if audit.relative(path).startswith(".claude/rules/") + ) + missing_entrypoints(audit) + + +def is_rule(relative: str) -> bool: + return relative.endswith(".md") and any( + relative.startswith(directory + "/") for directory in RULE_DIRS + ) + + +def missing_entrypoints(audit: Audit) -> None: + present = { + "codex": bool(audit.seeds["codex"]), + "claude": any( + named_entrypoint(audit.root / name) + for name in ("CLAUDE.md", ".claude/CLAUDE.md") + ), + } + for runtime in audit.runtimes: + if not present[runtime]: + audit.findings["missing-entrypoint"].add(f"runtime={runtime}") + + +def traverse(audit: Audit, extra: set[Path]) -> None: + pending = extra.union(*(audit.seeds[runtime] for runtime in audit.runtimes)) + parsed: set[Path] = set() + while pending: + source = min(pending) + pending.remove(source) + if source in parsed: + continue + parsed.add(source) + if audit.add(source): + pending.update(parse_source(audit, source) - parsed) + + +def parse_source(audit: Audit, source: Path) -> set[Path]: + pending: set[Path] = set() + for ref in references(read_text(source)): + result = resolve(ref, source, audit.root) + if result.nonroot: + audit.findings["nonroot-path"].add( + f"{source} -> {result.requested} | expected session-root-relative path" + ) + if result.target is not None: + audit.files.add(result.target) + record_route(audit, source, result.target, ref.explicit, pending) + elif not result.external_import and ( + ref.explicit or guidance_reference(ref.value) + ): + audit.findings["broken-reference"].add(f"{source} -> {result.requested}") + return pending + + +def record_route( + audit: Audit, source: Path, target: Path, explicit: bool, pending: set[Path] +) -> None: + if not explicit: + return + audit.add(target) + audit.edges.add((source, target)) + if target.suffix == ".md": + pending.add(target) + + +def reachable(seeds: set[Path], edges: set[tuple[Path, Path]]) -> set[Path]: + adjacency: dict[Path, set[Path]] = defaultdict(set) + for source, target in edges: + adjacency[source].add(target) + found = set(seeds) + pending = list(seeds) + while pending: + for target in adjacency[pending.pop()] - found: + found.add(target) + pending.append(target) + return found + + +def requires_route(runtime: str, relative: str) -> bool: + if relative.startswith(".agent-shared/rules/"): + return True + if runtime == "claude": + return False + return relative.startswith((".agents/rules/", ".codex/rules/", ".pi/rules/")) or ( + "/" in relative and Path(relative).name in {"AGENTS.md", "AGENTS.override.md"} + ) + + +def check_reachability(audit: Audit) -> None: + for runtime in audit.runtimes: + for path in audit.files - reachable(audit.seeds[runtime], audit.edges): + if requires_route(runtime, audit.relative(path)): + audit.findings["unrouted-guidance"].add(f"runtime={runtime} | {path}") + + +def cycles(edges: set[tuple[Path, Path]]) -> set[str]: + """Iterative DFS avoids a recursion limit on long instruction graphs.""" + adjacency: dict[Path, list[Path]] = defaultdict(list) + for source, target in sorted(edges): + adjacency[source].append(target) + state: dict[Path, int] = {} + found: set[str] = set() + for node in sorted(adjacency): + if node not in state: + visit(node, adjacency, state, found) + return found + + +def visit( + node: Path, + adjacency: dict[Path, list[Path]], + state: dict[Path, int], + found: set[str], +) -> None: + stack = [(node, iter(adjacency.get(node, [])))] + state[node] = 1 + while stack: + source, children = stack[-1] + target = next(children, None) + if target is None: + state[source] = 2 + stack.pop() + elif state.get(target) == 1: + found.add(f"{source} -> {target}") + elif target not in state: + state[target] = 1 + stack.append((target, iter(adjacency.get(target, [])))) diff --git a/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/references.py b/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/references.py new file mode 100644 index 00000000..a3631294 --- /dev/null +++ b/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/references.py @@ -0,0 +1,121 @@ +"""Compact Markdown reference extraction; deliberately not a runtime parser.""" + +import re +from dataclasses import dataclass +from pathlib import Path + +from darrow_ia.filesystem import absolute_reference, existing, inside + + +@dataclass(frozen=True) +class Reference: + value: str + explicit: bool + native: bool = False + + +def prose_lines(text: str) -> list[str]: + lines: list[str] = [] + fence = "" + for line in text.lstrip("\ufeff").splitlines(): + marker = re.match(r"^ {0,3}(`{3,}|~{3,})", line) + if fence: + if re.fullmatch( + r" {0,3}" + re.escape(fence[0]) + "{" + str(len(fence)) + r",}\s*", line + ): + fence = "" + elif marker: + fence = marker[1] + else: + lines.append(line) + return lines + + +def clean_reference(raw: str) -> str | None: + value = raw.strip().removeprefix("<").removesuffix(">").split("#", 1)[0] + if re.match(r"^(https?://|mailto:|codex:|file:)", value): + return None + if not value or re.search(r"[\s<>${}*]", value): + return None + return value.rstrip("/") if re.search(r"[/\\].*\.|\.md$", value) else None + + +def line_references(line: str) -> list[Reference]: + explicit = bool( + re.search( + r"\b(read|consult|load|follow)\b|before .*(edit|chang|work)", line.lower() + ) + ) + explicit = explicit or bool(re.search(r"(?:^|\s)(?:->|→)(?:\s|$)", line)) + ordinary = re.findall(r"\[[^\[\]]*\]\(([^()]*)\)", line) + ordinary.extend(re.findall(r"`([^`]+)`", line)) + refs = [ + Reference(value, explicit) + for raw in ordinary + if (value := clean_reference(raw)) + ] + bare = re.sub(r"`[^`]*`", "", line) + refs.extend( + Reference(value, True, True) + for value in re.findall(r"(?:^|\s)@([.A-Za-z0-9_/\\-]+\.md)", bare) + ) + return refs + + +def references(text: str) -> list[Reference]: + return [ref for line in prose_lines(text) for ref in line_references(line)] + + +def owning_skill(source: Path, root: Path) -> Path | None: + directory = source.parent + while inside(directory, root): + if (directory / "SKILL.md").is_file(): + return directory + if directory == root: + break + directory = directory.parent + return None + + +@dataclass(frozen=True) +class Resolution: + target: Path | None + requested: Path + nonroot: bool = False + external_import: bool = False + + +def resolve(ref: Reference, source: Path, root: Path) -> Resolution: + if ref.native: + requested = source.parent / ref.value + # Native absolute imports outside the repository belong to host memory. + external = requested.is_file() and not inside(requested.resolve(), root) + return Resolution( + existing(requested, root), requested, external_import=external + ) + skill = owning_skill(source, root) + if skill is not None and not absolute_reference(ref.value): + requested = skill / ref.value + return Resolution(existing(requested, root), requested) + return resolve_ordinary(ref, source, root) + + +def resolve_ordinary(ref: Reference, source: Path, root: Path) -> Resolution: + requested = root / ref.value + target = existing(requested, root) + nonroot = absolute_reference(ref.value) and ref.explicit + if target is None: + target = existing(source.parent / ref.value, root) + nonroot = nonroot or (target is not None and ref.explicit) + return Resolution(target, requested, nonroot) + + +def guidance_reference(value: str) -> bool: + return value.startswith( + (".agent-shared/", ".claude/", ".agents/", ".codex/", ".pi/") + ) or Path(value).name in { + "AGENTS.md", + "AGENTS.override.md", + "CLAUDE.md", + "CLAUDE.local.md", + } diff --git a/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/report.py b/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/report.py new file mode 100644 index 00000000..cfcc4c1a --- /dev/null +++ b/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/report.py @@ -0,0 +1,112 @@ +"""Stable human-readable inventory and diagnostic records.""" + +from pathlib import Path + +from darrow_ia.filesystem import ROOT_NAMES, readable +from darrow_ia.graph import Audit + +COUNTS = { + "broken": "broken-reference", + "nonroot_paths": "nonroot-path", + "cycles": "instruction-cycle", + "unrouted": "unrouted-guidance", + "missing_runtime": "missing-entrypoint", + "duplicate_files": "duplicate-content", + "unreadable": "unreadable-guidance", + "adapter_drift": "adapter-drift", +} +FINDINGS = ( + "missing-entrypoint", + "broken-reference", + "nonroot-path", + "instruction-cycle", + "unrouted-guidance", + "unreadable-guidance", + "root-budget", + "adapter-drift", + "always-loaded-rule", + "duplicate-content", +) +ADVISORY = {"always-loaded-rule", "duplicate-content"} + + +def scope(relative: str) -> str: + if relative in ROOT_NAMES: + return "root" + if Path(relative).name in { + "AGENTS.md", + "AGENTS.override.md", + "CLAUDE.md", + "CLAUDE.local.md", + }: + return "nested" + if relative.startswith(".claude/rules/"): + return "claude-native-rule" + return "adapter-rule" if "/rules/" in relative else "referenced" + + +def file_record(path: Path, audit: Audit) -> str: + if not readable(path): + return f"{path} | unreadable" + size = path.stat().st_size + return f"{path} | scope={scope(audit.relative(path))} | bytes={size} | approx_tokens={(size + 3) // 4}" + + +def summary(audit: Audit) -> str: + values: dict[str, str | int] = { + "runtime": audit.runtime, + "entrypoints": len(audit.entrypoints), + "files": len(audit.files), + "routes": len(audit.edges), + } + values.update({name: len(audit.findings[key]) for name, key in COUNTS.items()}) + for runtime, size in audit.root_bytes.items(): + values[f"{runtime}_root_bytes"] = size + values[f"{runtime}_root_approx_tokens"] = (size + 3) // 4 + return "summary: " + " ".join(f"{key}={value}" for key, value in values.items()) + + +def limited(records: list[str], limit: int, noun: str, prefix: str = "") -> list[str]: + result = [f" - {prefix}{record}" for record in records[:limit]] + if len(records) > limit: + result.append( + f" - {prefix}... {len(records) - limit} additional {noun} omitted" + ) + return result + + +def render(audit: Audit) -> str: + lines = [f"root: {audit.root}", summary(audit), "entrypoints:"] + lines.extend( + limited( + [file_record(path, audit) for path in audit.entrypoints], 40, "entrypoints" + ) + ) + lines.append("adapter_mirrors:") + lines.extend(f" - {record}" for record in audit.mirrors or ["none detected"]) + lines.append("findings:") + lines.extend(render_findings(audit)) + lines.append("routes:") + routes = [f"{source} -> {target}" for source, target in sorted(audit.edges)] + lines.extend(limited(routes or ["none"], 120, "routes")) + return "\n".join(lines) + "\n" + + +def render_findings(audit: Audit) -> list[str]: + lines: list[str] = [] + for name in FINDINGS: + severity = "advisory" if name in ADVISORY else "critical" + records = sorted(audit.findings[name]) + limit = ( + len(records) + if name in {"missing-entrypoint", "instruction-cycle", "root-budget"} + else 40 + ) + lines.extend(limited(records, limit, "findings", f"{severity} {name} | ")) + return lines or [" - none"] + + +def failed(audit: Audit) -> bool: + return any( + records for name, records in audit.findings.items() if name not in ADVISORY + ) diff --git a/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/setup.py b/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/setup.py new file mode 100644 index 00000000..259d549f --- /dev/null +++ b/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/setup.py @@ -0,0 +1,101 @@ +"""Read-only design evidence inventory.""" + +from pathlib import Path + +from darrow_ia.filesystem import ENTRY_NAMES, ROOT_NAMES, unreadable_reason, walk + +MANIFESTS = { + "package.json", + "pyproject.toml", + "Cargo.toml", + "go.mod", + "pom.xml", + "build.gradle", + "Makefile", +} +AUTOMATION = {"lefthook.yml", "lefthook.yaml", ".pre-commit-config.yaml", "hooks.json"} + + +def validate_required(root: Path) -> None: + for name in ROOT_NAMES: + validate_file(root / name, root, "instruction entrypoint") + validate_file(root / ".codex/config.toml", root, "Codex project configuration") + + +def validate_file(path: Path, root: Path, label: str) -> None: + if (path.exists() or path.is_symlink()) and unreadable_reason(path, root): + raise ValueError( + f"{label} is unreadable, missing, or not a regular file: {path}" + ) + + +def group(label: str, records: list[str]) -> list[str]: + lines = [f"{label}:", *(f" - {record}" for record in records[:40])] + if len(records) > 40: + lines.append(" - ...") + return lines + + +def entry_record(path: Path, root: Path) -> str: + validate_file(path, root, "instruction entrypoint") + size = path.stat().st_size + return f"{path} | bytes={size} | approx_tokens={(size + 3) // 4}" + + +def guidance_candidate(path: Path, root: Path) -> bool: + parts = path.relative_to(root).parts[:-1] + return path.suffix == ".md" and any( + part in {"rules", "docs"} or part.startswith("agent") for part in parts + ) + + +def inspect(root: Path) -> str: + validate_required(root) + files, directories = walk(root) + lines = [f"root: {root}"] + lines.extend( + group( + "entrypoints", + [entry_record(path, root) for path in files if path.name in ENTRY_NAMES], + ) + ) + lines.extend( + group( + "runtime_adapters", + [ + str(path) + for path in directories + if path.name in {".claude", ".agents", ".codex", ".pi"} + ], + ) + ) + lines.extend( + group( + "manifests", + [ + str(path) + for path in files + if len(path.relative_to(root).parts) <= 3 and path.name in MANIFESTS + ], + ) + ) + lines.extend( + group("automation", [str(path) for path in files if automation(path, root)]) + ) + lines.extend( + group("skills", [str(path) for path in files if path.name == "SKILL.md"]) + ) + lines.extend( + group( + "guidance_candidates", + [str(path) for path in files if guidance_candidate(path, root)], + ) + ) + return "\n".join(lines) + "\n" + + +def automation(path: Path, root: Path) -> bool: + relative = path.relative_to(root) + return len(relative.parts) <= 4 and ( + ".github/workflows/" in relative.as_posix() or path.name in AUTOMATION + ) diff --git a/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/updates.py b/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/updates.py new file mode 100644 index 00000000..2b74687a --- /dev/null +++ b/plugins/foundation/darrow-information-architecture/backend/src/darrow_ia/updates.py @@ -0,0 +1,86 @@ +"""Atomic publication of already approved guidance content, without policy choices.""" + +import argparse +import hashlib +import os +import stat +import sys +import tempfile +from pathlib import Path + +from darrow_ia.cli import Parser, run +from darrow_ia.filesystem import inside, resolve_root + + +def fingerprint(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() if path.exists() else "missing" + + +def destination(root: Path, relative: str) -> Path: + path = root / relative + resolved = path.resolve() + if not inside(resolved, root) or resolved == root: + raise ValueError( + f"guidance destination must stay inside the repository: {path}" + ) + if path.is_symlink() and not path.is_file(): + raise ValueError(f"guidance adapter is broken or not a file: {path}") + return resolved + + +def replacement_bytes(content: bytes, original: bytes) -> bytes: + text = content.decode("utf-8-sig").replace("\r\n", "\n") + newline = "\r\n" if b"\r\n" in original else "\n" + bom = b"\xef\xbb\xbf" if original.startswith(b"\xef\xbb\xbf") else b"" + return bom + text.replace("\n", newline).encode("utf-8") + + +def check_unchanged(root: Path, relative: str, target: Path, expected: str) -> None: + if destination(root, relative) != target or fingerprint(target) != expected: + raise ValueError(f"guidance changed since approval: {root / relative}") + + +def replace(root: Path, relative: str, content: bytes, expected: str) -> Path: + target = destination(root, relative) + check_unchanged(root, relative, target, expected) + original = target.read_bytes() if target.exists() else b"" + mode = stat.S_IMODE(target.stat().st_mode) if target.exists() else 0o644 + payload = replacement_bytes(content, original) + descriptor, temporary = tempfile.mkstemp(prefix=".ia-", dir=target.parent) + try: + with os.fdopen(descriptor, "wb") as stream: + stream.write(payload) + stream.flush() + os.fsync(stream.fileno()) + os.chmod(temporary, mode) + check_unchanged(root, relative, target, expected) + os.replace(temporary, target) + finally: + Path(temporary).unlink(missing_ok=True) + return target + + +def arguments_parser() -> argparse.ArgumentParser: + parser = Parser( + add_help=False, + usage="usage: ia-write --expected SHA256|missing --content-file FILE path [repository]", + ) + parser.add_argument("--expected", required=True) + parser.add_argument("--content-file", required=True) + parser.add_argument("path") + parser.add_argument("repository", nargs="?", default=".") + return parser + + +def command(arguments: list[str] | tuple[str, ...]) -> int: + args = arguments_parser().parse_args(arguments) + root = resolve_root(args.repository) + target = replace( + root, args.path, Path(args.content_file).read_bytes(), args.expected + ) + print(f"updated: {target}") + return 0 + + +def entrypoint() -> None: + sys.exit(run(lambda arguments: command(list(arguments)), sys.argv[1:])) diff --git a/plugins/foundation/darrow-information-architecture/backend/tests/acceptance.md b/plugins/foundation/darrow-information-architecture/backend/tests/acceptance.md new file mode 100644 index 00000000..f3ffb6f6 --- /dev/null +++ b/plugins/foundation/darrow-information-architecture/backend/tests/acceptance.md @@ -0,0 +1,53 @@ +# Migration acceptance + +Both existing skills pass the bundled metadata inspector before migration. +The original Bash regression suites supplied the mechanical baseline. Their +scenarios now run in pytest on every supported host; the shell suites are removed. +Full pre-migration live skill +baselines are not claimed; live trials exercise the migrated skills. + +Regression coverage is retained in these native tests: + +| Original scenarios | Pytest coverage | +| ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| Missing entrypoints, routes, cycles, scoped reachability, default budgets | `test_commands.py`: `test_graph_cases`, `test_long_graph_and_caps` | +| Deferred chains, arrow indexes, fences, native imports, skill lookalikes | `test_regressions.py`: routing and parsing cases | +| Selected runtimes, imported defects, broken adapters, shadowed budgets | `test_regressions.py`: runtime and budget cases; `test_commands.py`: override cases | +| Mirror alignment, drift, dependencies, unreadable mirrors, duplicates | `test_commands.py`: `test_mirrors`; `test_regressions.py`: mirror and duplicate cases | +| Root adapters in both directions and independent copies | `test_commands.py`: `test_root_adapter`, `test_bad_selected_adapter` | +| Ordinary path boundaries, incidental mentions, source files | `test_regressions.py`: boundary and mention cases; `test_failures.py`: `test_non_guidance_references` | +| Setup inventory, no writes, subdirectory identity, output caps | `test_commands.py`: `test_setup_inventory_and_limits`, `test_worktree_identity`, `test_long_graph_and_caps` | +| Large Git worktree output for setup and doctor | `test_regressions.py`: `test_large_worktree_output` with real Python subprocess output, both NUL and newline records | + +The fresh-artifact test additionally exercises the installed console commands +with frozen runtime dependencies. Permission-denial cases inject OS access +failures so they remain meaningful on Windows and under privileged test users. + +Representative requests, covered by the existing colocated eval suites: + +| Dimension | Setup | Doctor | +| ---------- | -------------------------------- | --------------------------- | +| Direct | Set up repository agent guidance | Audit AGENTS.md | +| Indirect | Reorganize our guidance graph | Trim duplicated agent rules | +| Incomplete | Propose the graph before editing | Show findings before fixing | +| Negative | Only audit existing guidance | Create a new guidance graph | +| Pressure | Preserve reverse symlink adapter | Preserve rare safety route | + +The migration additionally exercises native path handling, CRLF/BOM parsing, +case sensitivity, unavailable symlink support, read-only audit behavior, and +atomic source replacement. Fresh copied artifacts must run without Bash, +development dependencies, sibling plugins, or the original checkout. + +The bounded independent review found two regressions: parent-relative native +and skill resources were rejected by case validation, and invalid encoding in +unselected Claude guidance could abort a Codex-only audit. Both have native +command regressions in `test_failures.py` and were repaired. + +Live-trial diagnosis also corrected two eval calibration problems. The +Codex-only setup fixture now supplies an actual service contract instead of +expecting invented guidance in an empty repository. The unselected-runtime +semantic check accepts an issue-free outcome in context without requiring the +literal word “verified”; its deterministic verification check is unchanged. +A failed procedure-move trial motivated clearer skill instructions to confirm +the exact intent route in the inspector output, preserving the existing +cross-runtime reachability requirement. diff --git a/plugins/foundation/darrow-information-architecture/backend/tests/fresh_install.py b/plugins/foundation/darrow-information-architecture/backend/tests/fresh_install.py new file mode 100644 index 00000000..743d998f --- /dev/null +++ b/plugins/foundation/darrow-information-architecture/backend/tests/fresh_install.py @@ -0,0 +1,131 @@ +"""Exercise every entrypoint from an independently copied runtime-only plugin.""" + +import os +import shutil +import subprocess +import tempfile +from pathlib import Path + + +def run(arguments: list[str], cwd: Path, expected: int = 0) -> str: + result = subprocess.run( + arguments, + cwd=cwd, + capture_output=True, + encoding="utf-8", + env={**os.environ, "PYTHONUTF8": "1", "UV_NO_PROGRESS": "1"}, + check=False, + ) + if result.returncode != expected: + raise AssertionError( + f"{arguments}: {result.returncode}\n{result.stdout}\n{result.stderr}" + ) + return result.stdout + + +def verify(work: Path) -> None: + source = Path(__file__).resolve().parents[2] + plugin = work / "copied plugin ü 漢字" + shutil.copytree( + source, + plugin, + ignore=shutil.ignore_patterns( + ".venv", + "__pycache__", + ".pytest_cache", + ".mypy_cache", + ".ruff_cache", + ".hypothesis", + ".coverage*", + "coverage.json", + ), + ) + assert not (plugin / "bin/ia-doctor").exists() + assert not list(plugin.glob("skills/*/scripts/*.sh")) + project = plugin / "backend" + command = [ + "uv", + "run", + "--quiet", + "--frozen", + "--no-dev", + "--project", + str(project), + ] + run( + ["uv", "sync", "--quiet", "--frozen", "--no-dev", "--project", str(project)], + work, + ) + run( + [ + *command, + "python", + "-c", + "import importlib.metadata as m; assert not m.requires('darrow-information-architecture'); assert not {'pytest','mypy','ruff','coverage','hypothesis'} & {d.metadata['Name'] for d in m.distributions()}", + ], + work, + ) + repo = work / "native repository ü 漢字" + repo.mkdir() + run(["git", "init", "-q", str(repo)], work) + exercise(command, repo, work) + print( + "fresh copied plugin: setup, inspect, verify, refusals, atomic updates passed; runtime dependencies only" + ) + + +def exercise(command: list[str], repo: Path, work: Path) -> None: + guidance = repo / "AGENTS.md" + guidance.write_bytes(b"# Root\r\nRead `docs/policy.md`.\r\n") + (repo / "CLAUDE.md").write_bytes(b"@AGENTS.md\r\n") + (repo / "docs").mkdir() + assert "root:" in run([*command, "ia-setup", "inspect", str(repo)], work) + assert "broken-reference" in run( + [*command, "ia-doctor", "inspect", "--runtime", "both", str(repo)], work + ) + run([*command, "ia-doctor", "verify", "--runtime", "both", str(repo)], work, 1) + content = work / "content.txt" + content.write_bytes(b"# Policy\n") + assert "updated:" in run( + [ + *command, + "ia-write", + "--expected", + "missing", + "--content-file", + str(content), + "docs/policy.md", + str(repo), + ], + work, + ) + assert "broken=0" in run( + [*command, "ia-doctor", "verify", "--runtime", "both", str(repo)], work + ) + run( + [ + *command, + "ia-write", + "--expected", + "missing", + "--content-file", + str(content), + "docs/policy.md", + str(repo), + ], + work, + 2, + ) + run( + [*command, "ia-doctor", "verify", "--mirror", "../escape=docs", str(repo)], + work, + 64, + ) + run([*command, "ia-setup", "inspect", str(repo / "missing")], work, 2) + assert guidance.read_bytes().endswith(b"\r\n") + assert not list(repo.rglob(".ia-*")) + + +if __name__ == "__main__": + with tempfile.TemporaryDirectory(prefix="darrow-ia-fresh-") as temporary: + verify(Path(temporary).resolve()) diff --git a/plugins/foundation/darrow-information-architecture/backend/tests/test_commands.py b/plugins/foundation/darrow-information-architecture/backend/tests/test_commands.py new file mode 100644 index 00000000..3cf7f449 --- /dev/null +++ b/plugins/foundation/darrow-information-architecture/backend/tests/test_commands.py @@ -0,0 +1,536 @@ +"""Native filesystem regression tests at the command boundary.""" + +import hashlib +import os +import subprocess +import sys +from pathlib import Path + +import pytest + +from darrow_ia import cli, updates +from darrow_ia.filesystem import existing, resolve_root + + +def write(root: Path, files: dict[str, str]) -> None: + for name, content in files.items(): + path = root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(content.encode()) + + +def invoke( + root: Path, capsys: pytest.CaptureFixture[str], *args: str +) -> tuple[int, str]: + status = cli.run(cli.doctor, [*args, str(root)]) + return status, capsys.readouterr().out + + +@pytest.mark.parametrize( + ("files", "runtime", "status", "finding"), + [ + ({}, "auto", 1, "missing-entrypoint | runtime=codex"), + ({"AGENTS.md": "# Root\n"}, "auto", 0, "runtime=codex"), + ({"CLAUDE.md": "# Root\n"}, "auto", 0, "runtime=claude"), + ( + {"AGENTS.md": "# Root\n", "CLAUDE.md": "@AGENTS.md\n"}, + "auto", + 0, + "runtime=both", + ), + ( + { + "AGENTS.md": "Read `docs/a.md`.\n", + "docs/a.md": "Read `docs/missing.md`.", + }, + "codex", + 1, + "broken-reference", + ), + ( + {"AGENTS.md": "Read `docs/a.md`.\n", "docs/a.md": "Read `AGENTS.md`."}, + "codex", + 1, + "instruction-cycle", + ), + ( + { + "AGENTS.md": "Read `docs/AGENTS.md`.", + "docs/AGENTS.md": "Read `local.md`.", + "docs/local.md": "# Local", + }, + "codex", + 1, + "nonroot-path", + ), + ( + { + "AGENTS.md": "See `.agent-shared/rules/a.md`.", + ".agent-shared/rules/a.md": "# Rule", + }, + "codex", + 1, + "unrouted-guidance", + ), + ( + {"AGENTS.md": "# Root", "src/AGENTS.md": "# Nested"}, + "codex", + 1, + "unrouted-guidance", + ), + ( + { + ".claude/CLAUDE.md": "# Root", + "src/CLAUDE.md": "@local.md", + "src/local.md": "# Local", + }, + "claude", + 0, + "routes=1", + ), + ( + { + "CLAUDE.md": "# Root", + "AGENTS.md": "Read `missing.md`.", + "src/AGENTS.md": "Read `missing.md`.", + }, + "claude", + 0, + "broken=0", + ), + ( + {"AGENTS.md": "Read `missing.md`.", "AGENTS.override.md": "# Override"}, + "codex", + 0, + "broken=0", + ), + ( + { + "AGENTS.md": "Read `missing.md`.", + "AGENTS.override.md": "Read `AGENTS.md`.", + }, + "codex", + 1, + "broken=1", + ), + ( + {"AGENTS.md": "# Root", ".claude/rules/all.md": "# Rule"}, + "codex", + 0, + "always-loaded-rule", + ), + ( + { + "CLAUDE.md": "# Root", + ".claude/rules/all.md": 'paths: ["**"]\nRead `missing.md`.', + }, + "claude", + 1, + "always-loaded-rule", + ), + ( + { + "CLAUDE.md": "# Root", + ".claude/rules/api.md": 'paths: ["api/**"]\n# Rules', + }, + "claude", + 0, + "findings:\n - none", + ), + ( + {"AGENTS.md": "# Shared", "CLAUDE.md": "# Shared"}, + "both", + 0, + "duplicate-content", + ), + ({"AGENTS.md": "x" * 34000}, "codex", 1, "bytes=34000 limit=32768"), + ( + { + "AGENTS.md": "x" * 20, + ".codex/config.toml": "project_doc_max_bytes = 10 # comment", + }, + "codex", + 1, + "bytes=20 limit=10", + ), + ( + { + "AGENTS.md": "# Root", + ".codex/config.toml": "project_doc_max_bytes = 'invalid'", + }, + "codex", + 0, + "unreadable=0", + ), + ( + {"AGENTS.md": "\ufeff```md\r\nRead `missing.md`.\r\n```\r\n"}, + "codex", + 0, + "broken=0", + ), + ( + { + "AGENTS.md": "Read `.agents/skills/check/SKILL.md`.", + ".agents/skills/check/SKILL.md": "Read `references/a.md`.", + ".agents/skills/check/references/a.md": "Read `references/b.md`.", + ".agents/skills/check/references/b.md": "# Details", + }, + "codex", + 0, + "routes=3", + ), + ( + { + "AGENTS.md": "Read `.agents/skills/check/SKILL.md`.", + ".agents/skills/check/SKILL.md": "Read `references/a.md`.", + "references/a.md": "# Root lookalike", + }, + "codex", + 1, + "broken=1", + ), + ( + {"CLAUDE.md": "@src/local.md", "local.md": "# Root lookalike"}, + "claude", + 1, + str(Path("src/local.md")), + ), + ], +) +def test_graph_cases( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], + files: dict[str, str], + runtime: str, + status: int, + finding: str, +) -> None: + write(tmp_path, files) + before = {path: path.read_bytes() for path in tmp_path.rglob("*") if path.is_file()} + actual, output = invoke(tmp_path, capsys, "verify", "--runtime", runtime) + assert (actual, finding in output) == (status, True), output + assert before == { + path: path.read_bytes() for path in tmp_path.rglob("*") if path.is_file() + } + assert invoke(tmp_path, capsys, "inspect", "--runtime", runtime)[0] == 0 + + +@pytest.mark.parametrize( + "args", + [ + [], + ["wrong"], + ["inspect", "--unknown"], + ["verify", "--runtime", "other"], + ["inspect", "a", "b"], + ["inspect", "--mirror"], + ["inspect", "--mirror", "invalid"], + ["inspect", "--mirror", "../a=b"], + ["inspect", "--mirror", "a=C:\\out"], + ["inspect", "--mirror", "a=/out"], + ], +) +def test_doctor_usage( + tmp_path: Path, capsys: pytest.CaptureFixture[str], args: list[str] +) -> None: + assert cli.run(cli.doctor, [*args, str(tmp_path)]) == 64 + assert capsys.readouterr().err + + +def test_setup_inventory_and_limits( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + files = {f"src/{i}/AGENTS.md": "# Nested" for i in range(45)} + files.update( + { + "AGENTS.md": "# Root", + "package.json": "{}", + ".github/workflows/ci.yml": "name: ci", + ".agents/skills/a/SKILL.md": "# Skill", + "docs/a.md": "# Docs", + "rules/a.md": "# Rule", + "agent-guidance/a.md": "# Agent", + "lefthook.yml": "{}", + "deep/a/b/c/package.json": "{}", + "src/code.py": "pass", + ".worktrees/a/AGENTS.md": "ignore", + ".claude/worktrees/a/CLAUDE.md": "ignore", + "node_modules/a/AGENTS.md": "ignore", + } + ) + write(tmp_path, files) + before = {path: path.read_bytes() for path in tmp_path.rglob("*") if path.is_file()} + directories = set(tmp_path.rglob("*")) + assert cli.run(cli.setup_command, ["inspect", str(tmp_path)]) == 0 + output = capsys.readouterr().out + for expected in ( + "runtime_adapters:", + "skills:", + "guidance_candidates:", + "package.json", + "lefthook.yml", + f"{tmp_path / 'AGENTS.md'} | bytes=6 | approx_tokens=2", + str(tmp_path / ".github/workflows/ci.yml"), + str(tmp_path / ".agents/skills/a/SKILL.md"), + " - ...", + ): + assert expected in output + assert str(tmp_path / "deep/a/b/c/package.json") not in output + assert str(tmp_path / ".worktrees/a/AGENTS.md") not in output + assert before == { + path: path.read_bytes() for path in tmp_path.rglob("*") if path.is_file() + } + assert directories == set(tmp_path.rglob("*")) + assert cli.run(cli.setup_command, ["verify", str(tmp_path)]) == 64 + + +def link_or_skip(link: Path, target: Path, directory: bool = False) -> None: + try: + link.symlink_to(target, target_is_directory=directory) + except OSError as error: + if os.name == "nt": + pytest.skip(f"native symlink privilege unavailable: {error}") + raise + + +@pytest.mark.parametrize("reverse", [False, True]) +def test_root_adapter( + tmp_path: Path, capsys: pytest.CaptureFixture[str], reverse: bool +) -> None: + source, adapter = ( + ("CLAUDE.md", "AGENTS.md") if reverse else ("AGENTS.md", "CLAUDE.md") + ) + write(tmp_path, {source: "# Shared\n"}) + link_or_skip(tmp_path / adapter, Path(source)) + status, output = invoke(tmp_path, capsys, "verify", "--runtime", "both") + assert status == 0 + assert "status=symlink" in output and "duplicate-content" not in output + assert f"{tmp_path / source} -> {tmp_path / adapter} | status=symlink" in output + assert "codex_root_bytes=9" in output and "claude_root_bytes=9" in output + updates.replace( + tmp_path, adapter, b"# Changed\n", updates.fingerprint(tmp_path / source) + ) + assert (tmp_path / adapter).is_symlink() + assert (tmp_path / source).read_bytes() == b"# Changed\n" + (tmp_path / adapter).unlink() + (tmp_path / adapter).write_bytes((tmp_path / source).read_bytes()) + status, output = invoke(tmp_path, capsys, "verify", "--runtime", "both") + assert status == 0 and "duplicate-content" in output + assert "status=symlink" not in output + + +@pytest.mark.parametrize("target_kind", ["missing", "directory", "external", "loop"]) +def test_bad_selected_adapter( + tmp_path: Path, capsys: pytest.CaptureFixture[str], target_kind: str +) -> None: + targets = { + "missing": tmp_path / "missing", + "directory": tmp_path, + "external": tmp_path.parent / (tmp_path.name + ".md"), + "loop": tmp_path / "AGENTS.md", + } + target = targets[target_kind] + if target_kind == "external": + target.write_text("# Outside") + link_or_skip(tmp_path / "AGENTS.md", target, target_kind == "directory") + status, output = invoke(tmp_path, capsys, "verify", "--runtime", "codex") + assert status == 1, output + assert "unreadable-guidance" in output + assert cli.run(cli.setup_command, ["inspect", str(tmp_path)]) == 2 + + +@pytest.mark.parametrize( + "difference", ["aligned", "content", "files", "missing", "broken-route"] +) +def test_mirrors( + tmp_path: Path, capsys: pytest.CaptureFixture[str], difference: str +) -> None: + write( + tmp_path, + { + "AGENTS.md": "Read `left/a.md`.", + "left/a.md": "# Policy", + "right/a.md": "# Policy", + }, + ) + if difference == "content": + write(tmp_path, {"right/a.md": "# Changed"}) + if difference == "files": + write(tmp_path, {"right/extra.md": "# Extra"}) + if difference == "missing": + (tmp_path / "right/a.md").unlink() + (tmp_path / "right").rmdir() + if difference == "broken-route": + write( + tmp_path, + {"left/a.md": "Read `missing.md`.", "right/a.md": "Read `missing.md`."}, + ) + status, output = invoke(tmp_path, capsys, "verify", "--mirror", "left=right") + assert status == int(difference != "aligned"), output + assert "declared=true" in output + assert ( + f"status={'aligned' if difference in {'aligned', 'broken-route'} else 'drift'}" + in output + ) + assert difference != "aligned" or "duplicate-content" not in output + + +def test_worktree_identity(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: + repo = tmp_path / "main ü with spaces" + repo.mkdir() + subprocess.run(["git", "init", "-q", str(repo)], check=True) + subprocess.run( + [ + "git", + "-C", + str(repo), + "-c", + "user.name=Test", + "-c", + "user.email=test@example.invalid", + "commit", + "--allow-empty", + "-qm", + "initial", + ], + check=True, + ) + linked = tmp_path / "linked" + subprocess.run( + ["git", "-C", str(repo), "worktree", "add", "-qb", "task", str(linked)], + check=True, + capture_output=True, + ) + assert resolve_root(str(linked)) == repo.resolve() + nested = repo / "sub" + nested.mkdir() + assert resolve_root(str(nested)) == repo.resolve() + write(repo, {"AGENTS.md": "# Root"}) + assert cli.run(cli.setup_command, ["inspect", str(nested)]) == 0 + assert f"root: {repo.resolve()}" in capsys.readouterr().out + + +def test_missing_git_and_repository( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + monkeypatch.setenv("PATH", "") + assert resolve_root(str(tmp_path)) == tmp_path.resolve() + assert cli.run(cli.doctor, ["inspect", str(tmp_path / "absent")]) == 2 + assert "not a readable directory" in capsys.readouterr().err + + +def test_case_and_absolute_routes( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + write( + tmp_path, {"AGENTS.md": "Read `docs/Policy.md`.", "docs/policy.md": "# Policy"} + ) + assert invoke(tmp_path, capsys, "verify")[0] == 1 + assert existing(tmp_path / "docs/Policy.md", tmp_path) is None + write(tmp_path, {"AGENTS.md": f"Read `{tmp_path}/docs/policy.md`."}) + assert "nonroot-path" in invoke(tmp_path, capsys, "verify")[1] + + +def test_long_graph_and_caps( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + files = {"AGENTS.md": "Read `docs/0.md`."} + files.update({f"docs/{i}.md": f"Read `docs/{i + 1}.md`." for i in range(150)}) + files.update({f"src/{i}/AGENTS.md": "# Nested" for i in range(45)}) + write(tmp_path, files) + status, output = invoke(tmp_path, capsys, "verify") + assert status == 1 and str(tmp_path / "docs/150.md") in output + assert ( + f"critical broken-reference | {tmp_path / 'docs/149.md'} -> {tmp_path / 'docs/150.md'}" + in output + ) + assert "additional entrypoints omitted" in output + assert ( + "additional routes omitted" in output + and "additional findings omitted" in output + ) + + +@pytest.mark.parametrize("newline", [b"\n", b"\r\n"]) +@pytest.mark.parametrize("bom", [b"", b"\xef\xbb\xbf"]) +def test_atomic_replacement(tmp_path: Path, newline: bytes, bom: bytes) -> None: + path = tmp_path / "AGENTS.md" + original = bom + b"# Before" + newline + path.write_bytes(original) + path.chmod(0o644) + digest = hashlib.sha256(original).hexdigest() + assert updates.replace(tmp_path, "AGENTS.md", b"# After\n", digest) == path + assert path.read_bytes() == bom + b"# After" + newline + with pytest.raises(ValueError, match="changed since approval"): + updates.replace(tmp_path, "AGENTS.md", b"stale", digest) + assert sorted(p.name for p in tmp_path.iterdir()) == ["AGENTS.md"] + + +def test_atomic_failure_and_creation( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + updates.replace(tmp_path, "AGENTS.md", b"original", "missing") + + def fail_replace(source: str, destination: Path) -> None: + raise PermissionError("locked") + + monkeypatch.setattr(os, "replace", fail_replace) + with pytest.raises(PermissionError, match="locked"): + updates.replace( + tmp_path, "AGENTS.md", b"new", updates.fingerprint(tmp_path / "AGENTS.md") + ) + assert (tmp_path / "AGENTS.md").read_bytes() == b"original" + assert len(list(tmp_path.iterdir())) == 1 + with pytest.raises(ValueError, match="stay inside"): + updates.replace(tmp_path, "../escape.md", b"outside", "missing") + + +def test_public_entrypoints( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + write(tmp_path, {"AGENTS.md": "# Root", "content.txt": "# New"}) + monkeypatch.setattr(sys, "argv", ["ia-doctor", "verify", str(tmp_path)]) + with pytest.raises(SystemExit) as result: + cli.doctor_entrypoint() + assert result.value.code == 0 + monkeypatch.setattr(sys, "argv", ["ia-setup", "inspect", str(tmp_path)]) + with pytest.raises(SystemExit) as result: + cli.setup_entrypoint() + assert result.value.code == 0 + monkeypatch.setattr( + sys, + "argv", + [ + "ia-write", + "--expected", + updates.fingerprint(tmp_path / "AGENTS.md"), + "--content-file", + str(tmp_path / "content.txt"), + "AGENTS.md", + str(tmp_path), + ], + ) + with pytest.raises(SystemExit) as result: + updates.entrypoint() + assert result.value.code == 0 + assert f"updated: {tmp_path / 'AGENTS.md'}" in capsys.readouterr().out + + +def test_unicode_paths_with_legacy_pipe_encoding(tmp_path: Path) -> None: + root = tmp_path / "repository 漢字" + root.mkdir() + write(root, {"AGENTS.md": "# Root"}) + result = subprocess.run( + [ + sys.executable, + "-c", + "from darrow_ia.cli import doctor_entrypoint; doctor_entrypoint()", + "verify", + str(root), + ], + env={**os.environ, "PYTHONIOENCODING": "ascii", "PYTHONUTF8": "0"}, + capture_output=True, + check=False, + ) + assert result.returncode == 0, result.stderr + assert str(root) in result.stdout.decode("utf-8") diff --git a/plugins/foundation/darrow-information-architecture/backend/tests/test_failures.py b/plugins/foundation/darrow-information-architecture/backend/tests/test_failures.py new file mode 100644 index 00000000..bc6dbfac --- /dev/null +++ b/plugins/foundation/darrow-information-architecture/backend/tests/test_failures.py @@ -0,0 +1,207 @@ +"""Fail-closed operating-system boundaries and parsing edge cases.""" + +import os +from pathlib import Path + +import pytest + +from darrow_ia import checks, filesystem, report, updates +from darrow_ia.graph import Audit, cycles +from darrow_ia.references import Reference, clean_reference, owning_skill, resolve +from test_commands import invoke, link_or_skip, write + + +def test_permission_errors( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + write(tmp_path, {"AGENTS.md": "# Root", ".codex/config.toml": "# config"}) + real_access = os.access + monkeypatch.setattr( + os, + "access", + lambda path, mode: ( + False if Path(path).name == "config.toml" else real_access(path, mode) + ), + ) + status, output = invoke(tmp_path, capsys, "verify") + assert status == 1 and "unreadable-guidance" in output + monkeypatch.setattr( + os, + "access", + lambda path, mode: ( + False if Path(path).name == "AGENTS.md" else real_access(path, mode) + ), + ) + assert invoke(tmp_path, capsys, "verify")[0] == 1 + audit = Audit(tmp_path, "codex") + assert "unreadable" in report.file_record(tmp_path / "AGENTS.md", audit) + + +def test_traversal_failures( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + write(tmp_path, {"left/a.md": "# Policy", "right/a.md": "# Policy"}) + + def inaccessible(path: Path) -> dict[str, bytes]: + raise PermissionError("cannot traverse") + + monkeypatch.setattr(checks, "mirror_files", inaccessible) + assert ( + "traversal failed" + in checks.compare_mirror(tmp_path / "left", tmp_path / "right")[0] + ) + with pytest.raises(PermissionError): + filesystem.raise_walk_error(PermissionError("denied")) + monkeypatch.setattr(Path, "read_bytes", lambda self: b"\xff") + assert filesystem.unreadable_reason(tmp_path / "left/a.md", tmp_path) + + +def test_external_and_broken_routes( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + write(tmp_path, {"AGENTS.md": "Read `outside.md`.", "CLAUDE.md": "@outside.md"}) + outside = tmp_path.parent / (tmp_path.name + "-outside") + outside.mkdir() + write(outside, {"outside.md": "# External"}) + link_or_skip(tmp_path / "outside.md", outside / "outside.md") + assert invoke(tmp_path, capsys, "verify", "--runtime", "codex")[0] == 1 + # Preserve native host imports outside repository verification scope. + assert invoke(tmp_path, capsys, "verify", "--runtime", "claude")[0] == 0 + link_or_skip(tmp_path / "mirror", outside, True) + with pytest.raises(ValueError, match="stay inside"): + checks.mirror_paths("mirror=local", tmp_path) + link_or_skip(tmp_path / "broken", tmp_path / "missing") + with pytest.raises(ValueError, match="broken"): + updates.destination(tmp_path, "broken") + assert filesystem.existing(tmp_path / "outside.md", tmp_path) is None + + +def test_path_resolution_failure( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + write(tmp_path, {"AGENTS.md": "# Root"}) + + def fail(self: Path, strict: bool = False) -> Path: + raise OSError("unreadable ancestor") + + monkeypatch.setattr(Path, "resolve", fail) + assert filesystem.existing(tmp_path / "AGENTS.md", tmp_path) is None + + +def test_non_guidance_references( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + write( + tmp_path, + { + "AGENTS.md": "Read `src/check.py`.\nSee `background.md`.\nRead `docs/a.md`.\nRead `docs/b.md`.", + "src/check.py": "# Read `missing.md`.", + "background.md": "Read `missing.md`.", + "docs/a.md": "Read `shared.md`.", + "docs/b.md": "Read `shared.md`.", + "shared.md": "# Common", + }, + ) + status, output = invoke(tmp_path, capsys, "verify") + assert status == 0 and "broken=0" in output + assert clean_reference("https://example.test/a.md") is None + assert clean_reference("") == "variable/path.md" + assert clean_reference("${ROOT}/a.md") is None + assert clean_reference(" ") is None + assert clean_reference("command") is None + assert report.scope(".claude/rules/a.md") == "claude-native-rule" + assert report.scope(".agents/rules/a.md") == "adapter-rule" + assert report.scope("docs/a.md") == "referenced" + assert ( + resolve(Reference("docs/a.md", True), tmp_path / "AGENTS.md", tmp_path).target + == tmp_path / "docs/a.md" + ) + + +def test_cycle_depth_and_shared_nodes() -> None: + edges = {(Path(str(index)), Path(str(index + 1))) for index in range(1500)} + edges.add((Path("1500"), Path("0"))) + edges.add((Path("2"), Path("4"))) + assert len(cycles(edges)) == 1 + + +def test_detect_change_during_staging( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + write(tmp_path, {"AGENTS.md": "# Original"}) + digest = updates.fingerprint(tmp_path / "AGENTS.md") + original_fsync = os.fsync + + def concurrent_write(descriptor: int) -> None: + (tmp_path / "AGENTS.md").write_bytes(b"concurrent edit") + original_fsync(descriptor) + + monkeypatch.setattr(os, "fsync", concurrent_write) + with pytest.raises(ValueError, match="changed since approval"): + updates.replace(tmp_path, "AGENTS.md", b"replacement", digest) + assert (tmp_path / "AGENTS.md").read_bytes() == b"concurrent edit" + assert len(list(tmp_path.iterdir())) == 1 + + +def test_parent_relative_native_and_skill_resources( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + write( + tmp_path, + { + "AGENTS.md": "Read `.agents/skills/check/SKILL.md`.", + "CLAUDE.md": "@.claude/../AGENTS.md", + ".claude/CLAUDE.md": "@../AGENTS.md", + ".agents/skills/check/SKILL.md": "Read `../../references/file-updates.md`.", + ".agents/references/file-updates.md": "# Update contract", + }, + ) + status, output = invoke(tmp_path, capsys, "verify", "--runtime", "both") + assert status == 0, output + assert "routes=4" in output + + +@pytest.mark.parametrize("runtime, status", [("codex", 0), ("claude", 1), ("both", 1)]) +def test_invalid_encoding_respects_runtime_scope( + tmp_path: Path, capsys: pytest.CaptureFixture[str], runtime: str, status: int +) -> None: + write( + tmp_path, + {"AGENTS.md": "# Root", "CLAUDE.md": "# Claude", ".claude/rules/bad.md": ""}, + ) + (tmp_path / ".claude/rules/bad.md").write_bytes(b"\xff") + actual, output = invoke(tmp_path, capsys, "verify", "--runtime", runtime) + assert actual == status, output + assert ("critical unreadable-guidance" in output) == bool(status) + + +def test_entrypoint_case_is_explicit( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + write(tmp_path, {"claude.md": "# Wrong spelling"}) + status, output = invoke(tmp_path, capsys, "verify", "--runtime", "claude") + assert status == 1 and "missing-entrypoint" in output + write(tmp_path, {"AGENTS.md": "# Root", "agents.override.md": "Read `missing.md`."}) + status, output = invoke(tmp_path, capsys, "verify") + assert status == 0 and "runtime=codex" in output + + +def test_binary_source_reference_is_not_parsed_as_guidance( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + write( + tmp_path, + { + "AGENTS.md": "Before visual work, read `assets/logo.png`.", + "assets/logo.png": "", + }, + ) + (tmp_path / "assets/logo.png").write_bytes(b"\x89PNG\r\n\xff") + status, output = invoke(tmp_path, capsys, "verify") + assert status == 0 and "unreadable=0" in output + + +def test_skill_owner_stops_at_filesystem_root(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(Path, "is_file", lambda self: False) + root = Path(Path.cwd().anchor) + assert owning_skill(root / "AGENTS.md", root) is None diff --git a/plugins/foundation/darrow-information-architecture/backend/tests/test_properties.py b/plugins/foundation/darrow-information-architecture/backend/tests/test_properties.py new file mode 100644 index 00000000..d8d0739d --- /dev/null +++ b/plugins/foundation/darrow-information-architecture/backend/tests/test_properties.py @@ -0,0 +1,57 @@ +"""Generated invariants for portable parsing, graph closure, and updates.""" + +from pathlib import Path + +from hypothesis import given, settings +from hypothesis import strategies as st + +from darrow_ia.checks import valid_mirror_path +from darrow_ia.graph import reachable +from darrow_ia.references import clean_reference, references +from darrow_ia.updates import replacement_bytes + +settings.register_profile( + "deterministic", derandomize=True, deadline=None, max_examples=60 +) +settings.load_profile("deterministic") + + +@given( + st.text(alphabet="abcdefghijklmnopqrstuvwxyz0123456789_-", min_size=1, max_size=30) +) +def test_portable_reference_roundtrip(name: str) -> None: + value = f"docs/{name}.md" + assert clean_reference(f"<{value}#anchor>") == value + assert references(f"Before changes, read `{value}`.\r\n")[0].value == value + assert references(f"```md\nRead `{value}`.\n```\n") == [] + assert valid_mirror_path(value) + assert not valid_mirror_path("../" + value) + assert not valid_mirror_path("C:\\" + value) + + +@given( + st.sets(st.tuples(st.integers(0, 15), st.integers(0, 15))), + st.sets(st.integers(0, 15)), +) +def test_graph_closure(edges: set[tuple[int, int]], seeds: set[int]) -> None: + pairs = {(Path(str(left)), Path(str(right))) for left, right in edges} + start = {Path(str(node)) for node in seeds} + closure = reachable(start, pairs) + assert start <= closure + assert all(target in closure for source, target in pairs if source in closure) + assert reachable(closure, pairs) == closure + + +@given( + st.text( + alphabet=st.characters( + blacklist_categories=["Cs"], blacklist_characters="\r\ufeff" + ) + ) +) +def test_newline_update_idempotence(text: str) -> None: + content = text.encode() + result = replacement_bytes(content, b"\xef\xbb\xbf# Root\r\n") + assert result.startswith(b"\xef\xbb\xbf") + assert result.decode("utf-8-sig").replace("\r\n", "\n") == text + assert replacement_bytes(result, result) == result diff --git a/plugins/foundation/darrow-information-architecture/backend/tests/test_regressions.py b/plugins/foundation/darrow-information-architecture/backend/tests/test_regressions.py new file mode 100644 index 00000000..5fc40ecd --- /dev/null +++ b/plugins/foundation/darrow-information-architecture/backend/tests/test_regressions.py @@ -0,0 +1,370 @@ +"""Portable command regressions carried over from the original shell suites.""" + +import os +import subprocess +import sys +from pathlib import Path + +import pytest + +from darrow_ia import cli +from test_commands import invoke, link_or_skip, write + + +@pytest.mark.parametrize("state", ["missing", "repaired", "cycle"]) +def test_deferred_guidance_chain( + tmp_path: Path, capsys: pytest.CaptureFixture[str], state: str +) -> None: + write( + tmp_path, + { + "AGENTS.md": "Read `docs/agent-guidance/map.md`.", + "docs/agent-guidance/map.md": "Read `ordinary/map.md`.", + "ordinary/map.md": "Read `docs/agent-guidance/end.md`.", + }, + ) + if state != "missing": + content = "Read `docs/agent-guidance/map.md`." if state == "cycle" else "# End" + write(tmp_path, {"docs/agent-guidance/end.md": content}) + status, output = invoke(tmp_path, capsys, "verify", "--runtime", "codex") + assert status == int(state != "repaired"), output + assert ("instruction-cycle" in output) == (state == "cycle") + assert ("broken-reference" in output) == (state == "missing") + assert ( + f"{tmp_path / 'ordinary/map.md'} -> {tmp_path / 'docs/agent-guidance/end.md'}" + in output + ) + + +@pytest.mark.parametrize("present", [False, True]) +def test_arrow_rule_index( + tmp_path: Path, capsys: pytest.CaptureFixture[str], present: bool +) -> None: + target = ".agent-shared/rules/api.md" + write(tmp_path, {"AGENTS.md": f"- `apps/api/**` -> `{target}`"}) + if present: + write(tmp_path, {target: "# API"}) + status, output = invoke(tmp_path, capsys, "verify", "--runtime", "codex") + assert status == int(not present), output + assert f"{tmp_path / 'AGENTS.md'} -> {tmp_path / target}" in output + assert ("broken-reference" in output) != present + assert "unrouted-guidance" not in output + + +@pytest.mark.parametrize( + "example", + [ + "```md\nRead `docs/missing.md`.\n```", + " ~~~~md\nRead `docs/missing.md`.\n ~~~~", + "```md\n```not-a-close\nRead `docs/missing.md`.\n```", + "\ufeff```md\nRead `docs/missing.md`.\n```", + ], + ids=["backticks", "indented-tildes", "false-closing-fence", "bom"], +) +def test_fenced_examples_are_not_routes( + tmp_path: Path, capsys: pytest.CaptureFixture[str], example: str +) -> None: + write(tmp_path, {"AGENTS.md": example}) + status, output = invoke(tmp_path, capsys, "verify") + assert status == 0, output + assert "missing.md" not in output + assert "routes=0" in output + + +@pytest.mark.parametrize("present", [False, True]) +def test_nested_native_import_ignores_root_lookalike( + tmp_path: Path, capsys: pytest.CaptureFixture[str], present: bool +) -> None: + write( + tmp_path, + { + "AGENTS.md": "# Root", + "CLAUDE.md": "@AGENTS.md", + "src/CLAUDE.md": "@local.md", + "local.md": "Read `wrong-target.md`.", + }, + ) + if present: + write(tmp_path, {"src/local.md": "# Local policy"}) + status, output = invoke(tmp_path, capsys, "verify", "--runtime", "both") + assert status == int(not present), output + assert f"{tmp_path / 'CLAUDE.md'} -> {tmp_path / 'AGENTS.md'}" in output + assert f"{tmp_path / 'src/CLAUDE.md'} -> {tmp_path / 'src/local.md'}" in output + assert "wrong-target.md" not in output + + +@pytest.mark.parametrize("present", [False, True]) +def test_skill_resources_ignore_root_lookalike( + tmp_path: Path, capsys: pytest.CaptureFixture[str], present: bool +) -> None: + skill = ".agents/skills/check" + write( + tmp_path, + { + "AGENTS.md": f"Read `{skill}/SKILL.md`.", + f"{skill}/SKILL.md": "Read `references/guide.md`.", + f"{skill}/references/detail.md": "# Details", + "references/guide.md": "Read `wrong-target.md`.", + }, + ) + if present: + write( + tmp_path, {f"{skill}/references/guide.md": "Read `references/detail.md`."} + ) + status, output = invoke(tmp_path, capsys, "verify") + assert status == int(not present), output + assert ( + f"{tmp_path / skill / 'SKILL.md'} -> {tmp_path / skill / 'references/guide.md'}" + in output + ) + assert "wrong-target.md" not in output and "nonroot-path" not in output + + +@pytest.mark.parametrize( + "runtime, imported, expected", + [("claude", False, 0), ("both", False, 1), ("claude", True, 1)], +) +def test_imported_codex_defects_are_selected( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], + runtime: str, + imported: bool, + expected: int, +) -> None: + write( + tmp_path, + { + "CLAUDE.md": "@AGENTS.md" if imported else "# Claude", + "AGENTS.md": "Read `docs/codex-missing.md`.", + "src/AGENTS.md": "Read `docs/nested-codex-missing.md`.", + }, + ) + status, output = invoke(tmp_path, capsys, "verify", "--runtime", runtime) + assert status == expected, output + assert f"{tmp_path / 'AGENTS.md'} | scope=root" in output + assert ("codex-missing.md" in output) == bool(expected) + + +@pytest.mark.parametrize("runtime, expected", [("codex", 0), ("claude", 1)]) +def test_native_claude_defects_follow_selected_runtime( + tmp_path: Path, capsys: pytest.CaptureFixture[str], runtime: str, expected: int +) -> None: + write( + tmp_path, + { + "AGENTS.md": "# Root", + "CLAUDE.md": "Read `docs/root-missing.md`.", + "src/CLAUDE.md": "Read `docs/nested-missing.md`.", + ".claude/rules/api.md": "Read `docs/native-missing.md`.", + }, + ) + status, output = invoke(tmp_path, capsys, "verify", "--runtime", runtime) + assert status == expected, output + assert ("nested-missing.md" in output) == bool(expected) + assert ("native-missing.md" in output) == bool(expected) + + +def test_unselected_broken_adapter_is_only_inventoried( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + write(tmp_path, {"AGENTS.md": "# Root"}) + link_or_skip(tmp_path / "CLAUDE.md", Path("missing.md")) + assert invoke(tmp_path, capsys, "verify", "--runtime", "codex")[0] == 0 + status, output = invoke(tmp_path, capsys, "verify", "--runtime", "claude") + assert status == 1 + assert ( + f"critical unreadable-guidance | {tmp_path / 'CLAUDE.md'} (broken symlink)" + in output + ) + + +def test_incidental_relative_mention_is_not_a_route_error( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + write( + tmp_path, + { + "AGENTS.md": "Read `docs/nested/AGENTS.md`.", + "docs/nested/AGENTS.md": "See `local.md` for background.", + "docs/nested/local.md": "# Background", + }, + ) + status, output = invoke(tmp_path, capsys, "verify") + assert status == 0 and "nonroot-path" not in output + + +def test_ordinary_route_cannot_escape_repository( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + root = tmp_path / "repo" + write( + tmp_path, {"outside.md": "# Outside", "repo/AGENTS.md": "Read `../outside.md`."} + ) + status, output = invoke(root, capsys, "verify") + assert status == 1 + assert f"critical broken-reference | {root / 'AGENTS.md'}" in output + + +def test_declared_mirror_dependencies_outside_selected_runtime( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + write( + tmp_path, + { + "AGENTS.md": "# Root", + ".claude/rules/api.md": "Read `docs/policy.md`.", + "mirror/api.md": "Read `docs/policy.md`.", + }, + ) + args = ("verify", "--runtime", "codex", "--mirror", ".claude/rules=mirror") + status, output = invoke(tmp_path, capsys, *args) + assert status == 1 + assert ( + f"critical broken-reference | {tmp_path / '.claude/rules/api.md'} -> {tmp_path / 'docs/policy.md'}" + in output + ) + write(tmp_path, {"docs/policy.md": "# Policy"}) + assert invoke(tmp_path, capsys, *args)[0] == 0 + + +def test_unsearchable_mirror_fails_closed( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + write( + tmp_path, + {"AGENTS.md": "# Root", "source/a.md": "# Rule", "target/a.md": "# Rule"}, + ) + access = os.access + monkeypatch.setattr( + os, + "access", + lambda path, mode: ( + False if Path(path) == tmp_path / "source" else access(path, mode) + ), + ) + status, output = invoke(tmp_path, capsys, "verify", "--mirror", "source=target") + assert status == 1 + assert "critical adapter-drift" in output and "unsearchable" in output + + +def test_drifted_native_mirror_preserves_resident_rule_warning( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + write( + tmp_path, + { + "AGENTS.md": "# Root", + ".agent-shared/rules/all.md": '---\npaths: ["**"]\n---\n# Shared', + ".claude/rules/all.md": '---\npaths: ["**"]\n---\n# Different', + }, + ) + status, output = invoke( + tmp_path, + capsys, + "verify", + "--runtime", + "codex", + "--mirror", + ".agent-shared/rules=.claude/rules", + ) + assert status == 1 and "critical adapter-drift" in output + assert ( + f"advisory always-loaded-rule | {tmp_path / '.claude/rules/all.md'}" in output + ) + + +def test_shadowed_budget_and_unselected_config( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + write( + tmp_path, + { + "AGENTS.md": "x" * 34000, + "AGENTS.override.md": "# Override", + "CLAUDE.md": "# Claude", + }, + ) + assert invoke(tmp_path, capsys, "verify", "--runtime", "codex")[0] == 0 + write( + tmp_path, + { + "AGENTS.override.md": "x" * 1500, + ".codex/config.toml": "project_doc_max_bytes = 1024 # limit", + }, + ) + status, output = invoke(tmp_path, capsys, "verify", "--runtime", "codex") + assert status == 1 and "bytes=1500 limit=1024" in output + access = os.access + monkeypatch.setattr( + os, + "access", + lambda path, mode: ( + False if Path(path).name == "config.toml" else access(path, mode) + ), + ) + status, output = invoke(tmp_path, capsys, "verify", "--runtime", "claude") + assert ( + status == 0 + and "unreadable-guidance" not in output + and "root-budget" not in output + ) + + +def test_duplicate_shared_guidance_is_reported( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + write( + tmp_path, + { + "AGENTS.md": "Read `.agent-shared/rules/a.md` and `.agent-shared/rules/b.md`.", + ".agent-shared/rules/a.md": "# Same", + ".agent-shared/rules/b.md": "# Same", + }, + ) + status, output = invoke(tmp_path, capsys, "verify") + assert status == 0 + assert ( + f"advisory duplicate-content | {tmp_path / '.agent-shared/rules/a.md'} = {tmp_path / '.agent-shared/rules/b.md'}" + in output + ) + + +@pytest.mark.parametrize("command", ["doctor", "setup"]) +@pytest.mark.parametrize("separator", ["nul", "newline"]) +def test_large_worktree_output( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], + command: str, + separator: str, +) -> None: + write(tmp_path, {"AGENTS.md": "# Root"}) + nested = tmp_path / "nested" + nested.mkdir() + run = subprocess.run + script = ( + "import os, sys; " + "separator = b'\\0' if sys.argv[2] == 'nul' else b'\\n'; " + "sys.stdout.buffer.write(os.fsencode(sys.argv[1]) + separator " + "+ (b'HEAD ' + b'0' * 40 + b'\\n') * 9000)" + ) + + def git_output( + args: list[str], *, capture_output: bool, check: bool + ) -> subprocess.CompletedProcess[bytes]: + assert args[0] == "git" and args[3:] == [ + "worktree", + "list", + "--porcelain", + "-z", + ] + return run( + [sys.executable, "-c", script, f"worktree {tmp_path}", separator], + capture_output=capture_output, + check=check, + ) + + monkeypatch.setattr(subprocess, "run", git_output) + operation = cli.doctor if command == "doctor" else cli.setup_command + assert cli.run(operation, ["inspect", str(nested)]) == 0 + assert f"root: {tmp_path}" in capsys.readouterr().out diff --git a/plugins/foundation/darrow-information-architecture/backend/uv.lock b/plugins/foundation/darrow-information-architecture/backend/uv.lock new file mode 100644 index 00000000..239544e0 --- /dev/null +++ b/plugins/foundation/darrow-information-architecture/backend/uv.lock @@ -0,0 +1,462 @@ +version = 1 +revision = 3 +requires-python = ">=3.10, <3.14" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/2d/c738872f477f5687152acae68635790387425d407ae37dd3d3a8a6692307/coverage-7.16.1.tar.gz", hash = "sha256:f83981779bcf9dfa06fa0a8d4cb43e0faec1706328ce07aa3e7b665b4ac0f210", size = 969651, upload-time = "2026-09-13T19:12:21.422Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/62/a8b4dd53308a4b1571e6bdd77ee562c9b1c1742db9886c0aef2f69463067/coverage-7.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f12a9e27ca7b65e40a8475d27899b2d45064d9020e6a89148939e01987b5853", size = 223178, upload-time = "2026-09-13T19:08:21.805Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7a/b9325b8d486a5c05fd4a9eb1cd9c9f697c361ddeda0e73567d5c2d8959f6/coverage-7.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f590d46c30d9e4c1fda3efefe5443f4c2f6a4192c5ca2ba403653e9ebadf097", size = 223701, upload-time = "2026-09-13T19:08:23.391Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ee/3ab3ac6e9e7165a623695b73a83d1bfbdf5069681a68a012cb5abb096060/coverage-7.16.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3df82f0a3cef4e1bcfc799436056f0b978dda319d0bfd4460c6e479b2802d98a", size = 250434, upload-time = "2026-09-13T19:08:24.948Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a9/31ffc231e9879876f611cf1ac7622cf092d76762c07ca64dce47b29a200b/coverage-7.16.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb462d59146656e278d1e8ed913ce374d0ba68a4e081acde1867f6d3377fc881", size = 252264, upload-time = "2026-09-13T19:08:26.734Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f4/4fb4f6b16de07dbae3d3a111ae28d32ad657ce6ff4d364c49c10962daad5/coverage-7.16.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7db888dd0a1df1a653cae7f99d4047430d2187a3626eda51bc847b0fd6b9b43", size = 254127, upload-time = "2026-09-13T19:08:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3d/048e88eb610f44b7542dac10fa45bb2ffad5fbfcdab0f03289a843da934b/coverage-7.16.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:38a7e16f061504ac2b45370bf5bf97e8250d8d3f25e37385bae884978166554b", size = 256043, upload-time = "2026-09-13T19:08:30.019Z" }, + { url = "https://files.pythonhosted.org/packages/97/59/d27b113c80b04c7fc64c69457132648e1d287b83a1de41189a504b50b52b/coverage-7.16.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b26b55b18e1a53e1a159dd743728c4ddbbef28ba19000a91aaff5ce023197ec", size = 251092, upload-time = "2026-09-13T19:08:31.57Z" }, + { url = "https://files.pythonhosted.org/packages/45/42/b7eafaceff08dc1dcbfc924fa9233031fd00c8b8287cc4a1a0dcdedcb0f9/coverage-7.16.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fdb2f528b50953e29d22033b3256c396a700193c6e45b2490222ef9c333cbbf9", size = 252170, upload-time = "2026-09-13T19:08:33.191Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8f/57dcc5360aa6d8d24370c12fe855f6ebf5fbf2718c154fb9fdec7161de07/coverage-7.16.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3284754371dc78592aa3ae4d661d30ee20e02b2b6b0de3590a181a936d0d3b38", size = 250173, upload-time = "2026-09-13T19:08:34.75Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/379a8c7ac0f3063769dc43fd2475e9fc9b2722cb88eabe71f4d862660c93/coverage-7.16.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:802d1246c540e07486d4ee1adfa19a797e4b33529ffc371dc140644e8f27da0a", size = 253986, upload-time = "2026-09-13T19:08:36.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/25/70218d2a6077730843b75ee2525fa64a750283d71cc4c063dc0b75ba6979/coverage-7.16.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6fc735d6fe6d57f803e7ba021be4dde48e43e6aff94e6954350555d5332b0594", size = 250444, upload-time = "2026-09-13T19:08:38.434Z" }, + { url = "https://files.pythonhosted.org/packages/58/ac/6c41c441baa6c8a1fed5615e4e1e7d0ca649b33b472ee28591a92d70a1ff/coverage-7.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed5ade1bb18f62edace1bd198c66f9d4c75a8385d5fd24e87d917ea1a5958773", size = 251046, upload-time = "2026-09-13T19:08:40.133Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/01166d6f8aa42acd148b4c2c88173d60eefdd0270ea2bcbba79be35bc193/coverage-7.16.1-cp310-cp310-win32.whl", hash = "sha256:0c309096926b119543dc16438a11ef4c80783d2f4e59ff94f7f462651a944cdc", size = 225242, upload-time = "2026-09-13T19:08:41.672Z" }, + { url = "https://files.pythonhosted.org/packages/d2/28/a76060478919becf0fbea2df2053a6e1a7195f11edf8dfd51c58c1636b43/coverage-7.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:7562f8067ed9360e8b9739e5703403a7686dd1b36bf0f89fc538047c54cdea90", size = 225868, upload-time = "2026-09-13T19:08:43.22Z" }, + { url = "https://files.pythonhosted.org/packages/af/3a/d09495dfd5191b5593852bbe2f037afae768157443378d066d9ecea69a49/coverage-7.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72e013665e25cf9d44779f01f340af26319756f9a76822b7c94ce6b1d93813da", size = 223307, upload-time = "2026-09-13T19:08:44.914Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/310196a80258e28ebf6e5aa814a5a6ffbb18c03cf3c0c0c04191ed893e88/coverage-7.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb05c0ff98b56ba6969adf35556bc43bcb8d094df8bc9cb403acff53460c4e07", size = 223817, upload-time = "2026-09-13T19:08:46.738Z" }, + { url = "https://files.pythonhosted.org/packages/73/29/ed0fda1fcd440cdfc07e72a07f3c9c3b43f65a6b3d53bff2cba9b9de50e8/coverage-7.16.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d0ececb32090e3fbb03e0d352b973a0485879b4de6c58daf47227b9988b99e5", size = 254222, upload-time = "2026-09-13T19:08:48.358Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4e/5e2507fbd71ec1047620978f39568b0d2ed6c4468d08495a0c24f5677f65/coverage-7.16.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f0ba3892d81aacf36996c52f16bca04e39af31a6c5de930b7688ab617f4a6475", size = 256134, upload-time = "2026-09-13T19:08:49.926Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b1/a9f97846554bc240473656f9c3874591105f29c08eccf4282daadc6bc281/coverage-7.16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6410b75fe07d5271eaa95fc24bd0a9177ed588d9d1c10c0cf67829adb8f0567", size = 258240, upload-time = "2026-09-13T19:08:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/2b1ab208d06719394851cdd7dc322157870a93b178410b1bda6cac7755f9/coverage-7.16.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1039cb2de093225d597109342ce1675626bd127565e80b3044f4eca07c15b2e", size = 260202, upload-time = "2026-09-13T19:08:53.204Z" }, + { url = "https://files.pythonhosted.org/packages/af/14/ff4db31d6e3126d5b72aab1e868d35ec94e1b71dee46f5c3b54cb1b2438e/coverage-7.16.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cf047bc39fde5425be2628666d0f435ed8817859111c3aacc84b32d858069f5d", size = 254305, upload-time = "2026-09-13T19:08:54.833Z" }, + { url = "https://files.pythonhosted.org/packages/ce/27/c28faa4818f61c49ca3bfb4a77e1c191e79a40447504b89af0bd859cd6fc/coverage-7.16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c05913d0d5badf7ac83200f35dcf9514cce5df16a7cc89e7d1d7fff0461813b", size = 255935, upload-time = "2026-09-13T19:08:56.453Z" }, + { url = "https://files.pythonhosted.org/packages/46/5a/42e64e5b716048cae33e2e15f8c6fe04a927467056c533b8e88900da895e/coverage-7.16.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4027bf6d7bc0a16df058ce913b69f10c5687f8e1ca668f08caa659ce101744bf", size = 253995, upload-time = "2026-09-13T19:08:58.159Z" }, + { url = "https://files.pythonhosted.org/packages/84/e5/860287acd5a1e29acd337f302b476b54148203590091b1a1555b2e914b50/coverage-7.16.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4f48b345f831eaf4402ab6333c2dc3e2e2b5bc7b9c1b8fe12680dee3f0538f01", size = 257767, upload-time = "2026-09-13T19:08:59.958Z" }, + { url = "https://files.pythonhosted.org/packages/a5/c5/4b68006da567b4b413352f891776c35776b3557a0479a28187ab57c21659/coverage-7.16.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8643baeb590726c558b2faed6cd59b0917480f9367fcc692026f1a86d824fd08", size = 253715, upload-time = "2026-09-13T19:09:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/a1/0e/b2d0c47cfbf11770e197f1d0f11a92864eb29fd7eaca45bc9915573d9725/coverage-7.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d06dcc420b570bf683cdb647cc8fe62b672d9e429ef711c3cbbb7a6880ca1572", size = 254624, upload-time = "2026-09-13T19:09:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/2e/14/3bab8821b2b942155578fc34e88e21b0670d922f039ab23651f6035ba7f7/coverage-7.16.1-cp311-cp311-win32.whl", hash = "sha256:946f58aa59b08bcd6afcc6a7bd0ff54ed5eee844f32ad69fe6814836d15856a2", size = 225404, upload-time = "2026-09-13T19:09:05.174Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ce/4f4ce667a97d1c538b46a79b89161395ad2beffc248b6c782afc9f521927/coverage-7.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:684c7ee9b4c04358fe6ac8b517ab51ec35fcd79d08ff0f105dd8bcd96885bbb7", size = 225879, upload-time = "2026-09-13T19:09:06.81Z" }, + { url = "https://files.pythonhosted.org/packages/26/17/f3dce5e47351ab34522dd037fd64b111742958e24861bdd2820971bc1a26/coverage-7.16.1-cp311-cp311-win_arm64.whl", hash = "sha256:1b24f79e25bcf6c73931aeca7a3dfc7595c0cb5e9364aba3fdf387a3de4b1c22", size = 225428, upload-time = "2026-09-13T19:09:08.502Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f7/7cd4c9f2a3b7574414222ec425d5eee21cc690d867a013315e3be8ba185c/coverage-7.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b7f2c26ce6ce0b1e0ca0d5fae96ea510e3a2e78b7207f06e76b7f2c87fa3d0af", size = 223475, upload-time = "2026-09-13T19:09:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f2/9ac65f9cedd43f91e2d3657c11f13aef82aebae89b2243dc9e44fe58a740/coverage-7.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070acb9da788dff743a4d36fc015feee12d68f0349959017542017c79f59c21c", size = 223845, upload-time = "2026-09-13T19:09:11.988Z" }, + { url = "https://files.pythonhosted.org/packages/62/4d/f13db452d4367fe1122abfd98ae330596f65a3b40498f8e1a5811f68f123/coverage-7.16.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e366587b370bc9b8b51b7b7272c610c56db5d5b4795b9e4a29d28ff2f440f809", size = 255341, upload-time = "2026-09-13T19:09:13.708Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6b/85ed86e82835a96ddfbe7705c387c28ce1cbbecd1b6d606f5ddfeeb712df/coverage-7.16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e82e10b9d290f60b63459cfb245a841aec347603997206296b93881463a93dcf", size = 258078, upload-time = "2026-09-13T19:09:15.368Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d2/f66945853d850b9c05f4e012e37396f1f31d0cd40d062394b229c22e7b56/coverage-7.16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d73bb1f85c4150ac208fb0755beb04b2e44897bad81414de9380f98dd74729f", size = 259191, upload-time = "2026-09-13T19:09:17.046Z" }, + { url = "https://files.pythonhosted.org/packages/77/a1/7b907abe62461f289035c7ac60ab3e6334efb8c425151aac4abfa0c97820/coverage-7.16.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3397b9032553d281ad6a9253b12675b65e0cc8cd7a3b0633cf48872c9eb13360", size = 261452, upload-time = "2026-09-13T19:09:18.756Z" }, + { url = "https://files.pythonhosted.org/packages/51/e6/e26f4d6b1069a2a2fee3238a132f85c5b4c1dc25aebd88e015451ff0bd68/coverage-7.16.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77890395cf37026a5907d3ad32376aa51f41c0f163b7477fdbd4f94966cc1d08", size = 255698, upload-time = "2026-09-13T19:09:20.786Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ec/99db7450e813050ebce300e31bbd4f997c76a8c6e3a6d168c5dffc104109/coverage-7.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b0944dc3bee3091039bf970d73caaf930c906013128a421bdc132e797494d941", size = 257111, upload-time = "2026-09-13T19:09:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9d/500df9d3cd8c541ac84b5e0bcab00646be75654e05aa34e8410ec851282d/coverage-7.16.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b89d22a89d5bc05dd95b64e08295b8394aa96dc88e08f8ba210c9ebfebbe0489", size = 255258, upload-time = "2026-09-13T19:09:24.429Z" }, + { url = "https://files.pythonhosted.org/packages/c0/24/53318d96da332bb4946f8ac646fa19fd37fecbd0c49144735c716ac69bf0/coverage-7.16.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:550a2a1faf7559f13d5344f12d1eb886ad87955155d7dfab2a3fe5c8ec8fe776", size = 259326, upload-time = "2026-09-13T19:09:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ce/abc0462b2e6ae96ae22197d2bc30fe33b6b4e52937e782745436ceb2a761/coverage-7.16.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:55eb268e5b81aefac759766c9162625b06c1bedb7b77d936225bafc4f038a6f6", size = 254827, upload-time = "2026-09-13T19:09:28.191Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e1/0a04eedaaf19196b51f0180968134228c7646e469ed29914e48690a7cf3e/coverage-7.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65a8fc80898c9ce59f04349fe8b4849b1f9787f14e52ead990e5f849ff4727a0", size = 256698, upload-time = "2026-09-13T19:09:29.922Z" }, + { url = "https://files.pythonhosted.org/packages/de/1d/d441a55cf22ce9d8e9e34814806c47441ab844bd534e0f4b64e1c6ae8bd1/coverage-7.16.1-cp312-cp312-win32.whl", hash = "sha256:528a61be40977c340cf201d23b69bd6a6bab507da60e9dbda85f8b30e935d70d", size = 225541, upload-time = "2026-09-13T19:09:31.752Z" }, + { url = "https://files.pythonhosted.org/packages/73/27/ec3d032375735dd331477caa051419678079ff90fcb53d0284a6c2bfb757/coverage-7.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:d0f02c633630e2b74522108ee95a84ad6e1204a8016a6cca5297f335ea27147e", size = 226075, upload-time = "2026-09-13T19:09:33.556Z" }, + { url = "https://files.pythonhosted.org/packages/94/00/90e9f5c4434878494306b9c0ee8068ebbd829483737d8cefccc58884d728/coverage-7.16.1-cp312-cp312-win_arm64.whl", hash = "sha256:2959978f9d1d20a2c0c15d0a68baaeccf615ac1aa214cf4a05a10d6f568926c8", size = 225461, upload-time = "2026-09-13T19:09:35.48Z" }, + { url = "https://files.pythonhosted.org/packages/aa/74/c08c0c4dc9fa6bcd1d90728a63660aa1b17b488a806948598456c48f75d1/coverage-7.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ee5465db6e9152a7d09f3215309326878c6aa3ac509195a369f9d264ff4bfbd9", size = 223501, upload-time = "2026-09-13T19:09:37.276Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c8/784986d326663285258a8e39835c46fb730cc85284f0dbcd82078586dd22/coverage-7.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b8256f8b525ba233d2e4cdcdce0d6673c66fc9bf70df1fd5e67c54a74e2d245", size = 223876, upload-time = "2026-09-13T19:09:39.385Z" }, + { url = "https://files.pythonhosted.org/packages/15/76/73fb792928872bbb07e553f920ff55c65ee962c469265feb5d1d4ae5f97a/coverage-7.16.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57cc400275b9a2892e905fc893f732b21ddb95271bf96406c88e2f6367848b5", size = 254863, upload-time = "2026-09-13T19:09:41.326Z" }, + { url = "https://files.pythonhosted.org/packages/ca/8d/1fd78899513244b065ccecdd1cfc6aa8b8f1bdee796d8c4f2aa8e8e5397d/coverage-7.16.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b3fd0f3435ebb7a7183b32a6062a8b755f08242ced1f3f22761d30b56b3c2a5", size = 257460, upload-time = "2026-09-13T19:09:43.086Z" }, + { url = "https://files.pythonhosted.org/packages/47/1c/b23ddfcb7ef9bd7b3fdb7be9a5dccf9925ae88e556df7aa5b655283c78f2/coverage-7.16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5eb1762e7eb5fad34ef913e8107c7788a66f19d328e598ce95bf7217f9e5c8f", size = 258697, upload-time = "2026-09-13T19:09:45.161Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c9/19d0c6ca35778a7e9415c30c46a0c64c9d4374219d004a35563132296896/coverage-7.16.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46cd3a73e9140410de62cceb66214bce0e08fb3922b9176fbfc1522fec151b41", size = 260827, upload-time = "2026-09-13T19:09:47.061Z" }, + { url = "https://files.pythonhosted.org/packages/97/fc/5862f7344382c62b85df43d483e579168cc062e007f54d895dfa51fe3e7d/coverage-7.16.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1ba5142d68dd2cb775cbd0ac8601819298152047803c8efe4eec6d7d7aa7878", size = 255038, upload-time = "2026-09-13T19:09:48.945Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9f/5c26583199a68df8d15124b4590520903f8eb7cef17db293fea712bb0783/coverage-7.16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c389c6f9d1d518e1249ddcb8a7f158135644ce2c508fa6cc17b680777dad5bf2", size = 256827, upload-time = "2026-09-13T19:09:50.738Z" }, + { url = "https://files.pythonhosted.org/packages/95/13/605198bff079b107336710f28a797312ab132587168600d70a290e7ecd2e/coverage-7.16.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cdc57746c7ac0ea063351b4d651c3bb4dd4fd35e64dbb8e90c10e14eb03c4080", size = 254794, upload-time = "2026-09-13T19:09:52.577Z" }, + { url = "https://files.pythonhosted.org/packages/43/b7/0bbb32dc5ccdac766a13763fdfbffd7d73fc80349a69230c46c6b71e7508/coverage-7.16.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5597180ed7670cc94c04c65347418a467d3a43d5f0cf52fcac647f5425f42037", size = 258947, upload-time = "2026-09-13T19:09:54.372Z" }, + { url = "https://files.pythonhosted.org/packages/13/bd/65c31ddd43ff4e61b63721b3dad17cca412dba6e2ce89f92abdf75734339/coverage-7.16.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9647a0ac46255b8fef59a433a2161f03e5483f3a35e1cbd9dfe4600baff0c6b", size = 254613, upload-time = "2026-09-13T19:09:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/25/20/d278115f2ba522b3e3128c6852be265f4e0df7202b2effca4db96cf0217d/coverage-7.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e306e98186b9cd109121f3583aeb7978797ad21d948f22944c5c08845cd554d0", size = 256388, upload-time = "2026-09-13T19:09:58.095Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ca/d43aa396fb3a2f71c9411b99d926475f5acaf5c124f605c592865c80c8d5/coverage-7.16.1-cp313-cp313-win32.whl", hash = "sha256:48a78a66fcce49d7f6156524bf979c0ac633d584199717c68c6ffa949fc14e6a", size = 225550, upload-time = "2026-09-13T19:09:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/0e/30/df2f6114f6a17cffe94721bd1d298f77f86aad493717fa5bc03cb291a1a6/coverage-7.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af03247d598a353bbbbe1b925deb735276e4d845e7197c4073dc89352b236fa", size = 226091, upload-time = "2026-09-13T19:10:02.331Z" }, + { url = "https://files.pythonhosted.org/packages/ee/31/6f90d8aab72a112492bd50e3b5485b53b772b1f5fa70da0a620e723caae6/coverage-7.16.1-cp313-cp313-win_arm64.whl", hash = "sha256:166adae25b05b04c9a84135912066d9c97482115af38df1a419a38aacc6b6f5d", size = 225481, upload-time = "2026-09-13T19:10:04.151Z" }, + { url = "https://files.pythonhosted.org/packages/96/1a/d6d16babd0a5fe4c3fae40702158c570351694e74516d8d81b86c5637448/coverage-7.16.1-py3-none-any.whl", hash = "sha256:3d8bd4e58b6a5c2018d808f297905393c6c61da466a48c3f0596a76a4900ebe4", size = 215264, upload-time = "2026-09-13T19:12:18.895Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "darrow-information-architecture" +version = "0.2.1" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "coverage", extra = ["toml"] }, + { name = "hypothesis" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "coverage", extras = ["toml"], specifier = ">=7.10,<8" }, + { name = "hypothesis", specifier = ">=6.138,<7" }, + { name = "mypy", specifier = ">=1.18,<2" }, + { name = "pytest", specifier = ">=8.4,<9" }, + { name = "ruff", specifier = ">=0.12,<1" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "hypothesis" +version = "6.168.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/ce/c0946bebffb99b62426a6a7643d4272cc6c5cf777a488b3b4d0ee724e960/hypothesis-6.168.0.tar.gz", hash = "sha256:72af51087b7b5ab21c49f0d502f803c20897678652835596bd2a8b169a39135e", size = 510805, upload-time = "2026-09-08T18:48:36.072Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/f8/8b2cc9ae7b439538f6f2d32a92892340b6343a6b04d171c516666901dedc/hypothesis-6.168.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:47b89491ff02e3ae9b302c440457938e87b47a45b9a1d98ff5575b6910d779e2", size = 791358, upload-time = "2026-09-08T18:47:37.076Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f4/4d7d897310cde5085779fb96feadb8529d98cb8e51ed7b24f7da9b6c6bdc/hypothesis-6.168.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:1f4cd0ff11bd470a1a846296ed5fe55e84214194850370994fd1370fe73d3099", size = 787081, upload-time = "2026-09-08T18:47:16.227Z" }, + { url = "https://files.pythonhosted.org/packages/26/7b/9d52066d363faba7f3ac20ee60a3c696a475feb2c477d235d0d649d41cc1/hypothesis-6.168.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:732ae5d47482f99d8028cca096729625f05690a83f5e7ce31466e266155792f4", size = 1123850, upload-time = "2026-09-08T18:48:11.504Z" }, + { url = "https://files.pythonhosted.org/packages/50/cf/aa46d76fa7df43caf2c372e394fda84ce1dc08421814f8674a6b9295e2ea/hypothesis-6.168.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2085ee74ac3ab6b70e2f7ffae9b4cb74c246da2f574b2de81a0818a8a30f659f", size = 1147685, upload-time = "2026-09-08T18:46:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/84/c2/78ed8c8d5aa37e4baae2a4b3e29687ee3d9b7f1a5e56ea5ea5ec7ec71ecb/hypothesis-6.168.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1894782fae5d9a7bb44e6dcf848ccb09ccb5babab48d8b5c31a0a7fc025b82a1", size = 1149294, upload-time = "2026-09-08T18:47:04.757Z" }, + { url = "https://files.pythonhosted.org/packages/78/7f/d57440f19e9de70e85359cf179ce786f309ee17609bd3c5a0113272875c0/hypothesis-6.168.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecf0ab13cef899efb816ffdd7963e0679f372520884ce06756c7642f3df94213", size = 1169729, upload-time = "2026-09-08T18:47:44.056Z" }, + { url = "https://files.pythonhosted.org/packages/a1/86/dc74410a186990bb22c2a3eea0e77804f2eb0f300860b46d7d8948073674/hypothesis-6.168.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:3f6dcf66270278d078bed01b401f47db4e26456cd909d8e23c6b9366a6c0b131", size = 1129182, upload-time = "2026-09-08T18:46:33.382Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5e/be048fc4f6dac831625e155bdf11e4233caf46bb54030391d6fba8e19449/hypothesis-6.168.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bfef4d46dbf1704a7b8fa3a78778651a2cb18870ca0a70da19c381646822b149", size = 1160180, upload-time = "2026-09-08T18:47:26.459Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4a/15a34498a5f08720fbbdbbe4668a8050fe4e17c16c9eeb6f56a017f2fa6b/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1d1aa5b3484e329295d88488a5ba06243909e65c2ab616513c2d36721de4ed1d", size = 1299711, upload-time = "2026-09-08T18:47:28.34Z" }, + { url = "https://files.pythonhosted.org/packages/77/09/5354e0dae302ab98c4f0046b7e8699c2186ae4349397bda5d5c852bda68b/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:3bc00fd8cda04b58e37a1163e8a65389b247b4f5ee547ae37d244a4960995517", size = 1425341, upload-time = "2026-09-08T18:46:56.785Z" }, + { url = "https://files.pythonhosted.org/packages/59/7c/3c0e1f59043ff128c70a51d298d3d6b5973525c357a1e1d0542dbc05ac90/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:990026952d5b2eca290c88f639ac639233f47e13dae338c6dfb6e4774bcab349", size = 1281063, upload-time = "2026-09-08T18:46:55.401Z" }, + { url = "https://files.pythonhosted.org/packages/40/dd/db884db9a7d42ae6b72a00c13c725940b638dfb263e618b22af59d5dfa2f/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:a74b0945acbbd552c7c2d0a99a3b5232962b8848c8eed1829451800a9bfcf00b", size = 1300247, upload-time = "2026-09-08T18:47:02.949Z" }, + { url = "https://files.pythonhosted.org/packages/99/8a/4ee9769e1d48676efb6a78a130f82e0d52d3f87b2055294102272a08615c/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a380b521b5a76a9e8917d64adcf7f861a45a4360a34b1579af14c5df8eb0377", size = 1336084, upload-time = "2026-09-08T18:48:05.675Z" }, + { url = "https://files.pythonhosted.org/packages/61/17/d4ed11bc99d205d6d2651a0f1a4874f377150f836b5a0849bd511d68a2eb/hypothesis-6.168.0-cp310-abi3-win32.whl", hash = "sha256:2264f15a1c80329e3ad48e39c44bd5c9429b7b04c9ee62cdd72f4b10aaac9f29", size = 677989, upload-time = "2026-09-08T18:47:24.722Z" }, + { url = "https://files.pythonhosted.org/packages/77/51/abf1fde7b8afab87db30afb73b3847e62440551d146472111cabeba2fe00/hypothesis-6.168.0-cp310-abi3-win_amd64.whl", hash = "sha256:5b54769033b84477931d2072e7133a7555e0de5c53fd5ca3bbde960762d7d31b", size = 684692, upload-time = "2026-09-08T18:46:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/12/e2/64d79aed47a95186ce9edcef60eb4684870c72542da3b7027c2483d8ee8c/hypothesis-6.168.0-cp310-abi3-win_arm64.whl", hash = "sha256:112b0900059bf9d7d6528ed729770629ab146e0d133c4143b9bd4a01dc002bcc", size = 682709, upload-time = "2026-09-08T18:48:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/95/67/bfacadcc6cd4bd179d1cd2f6c85f580136019b38af7db71e3159ebb3ce33/hypothesis-6.168.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cb10aa59b0af45badca76911f5323f40d24fdbe00d01b7b67fef8648c99411b5", size = 792193, upload-time = "2026-09-08T18:47:42.392Z" }, + { url = "https://files.pythonhosted.org/packages/fc/29/4abab4114035f54438e3b07bdab3f68fd8769fe0fb84f8ffd6656eaa58fd/hypothesis-6.168.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e21e30b76b6d3adb87c550576132a3204f4c257ec43353f6c09b9d59bb762abc", size = 788121, upload-time = "2026-09-08T18:47:09.398Z" }, + { url = "https://files.pythonhosted.org/packages/85/d3/c54242439dcb59c7e3274a2952188bebbc605be397c3a6ff027fc81e1726/hypothesis-6.168.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:527452b43e79e6dfbf9cb69145a940547a3cd177c556698a3fc939ed2354c4b3", size = 1124222, upload-time = "2026-09-08T18:46:28.345Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8b/1c9f54be7040fe7d94e4cd5d9343da5a36240bee2fa856f44109f9622251/hypothesis-6.168.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:489d5c060f49f495b64215cae627c71730cffd5ef59dc4d7f431932e6e6d2e67", size = 1170346, upload-time = "2026-09-08T18:46:52.784Z" }, + { url = "https://files.pythonhosted.org/packages/af/d4/f6b1557bd3cf2976d56465fcbc483214bc6e7f1aa7e4db9f8c5f776d547a/hypothesis-6.168.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c3af200b322f710c76c2189866246cdcff2039165dd77edff1a7bf1157162fb0", size = 1300267, upload-time = "2026-09-08T18:46:45.644Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ad/a345588acc038deb09a43610328004d31fe5a53289627e03b0199978dc45/hypothesis-6.168.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73084b76e4a79cd0f7883ce80fc60c9f374ce7dcad8f520b39db40470ce1852f", size = 1336724, upload-time = "2026-09-08T18:46:44.179Z" }, + { url = "https://files.pythonhosted.org/packages/97/e9/c5e380fd29fd552ff442bcfb1090926287e2911d40212ae0602a0736347b/hypothesis-6.168.0-cp310-cp310-win_amd64.whl", hash = "sha256:8067e6b4b48e5cfdc849a1a20c9d4972b3f532b3e3edb5e2b5dfd106045a5236", size = 684644, upload-time = "2026-09-08T18:47:45.814Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7a/7a28d9afd52c9a89c4d8e3170c92fe1ffbd4a4ce3f905ca15fa7ddaaa581/hypothesis-6.168.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4d7d29dd63ad9fdc4aa1d65fa272449e14aaf6c6bb8451091818c2945533a43a", size = 792045, upload-time = "2026-09-08T18:46:29.612Z" }, + { url = "https://files.pythonhosted.org/packages/0a/40/e2d6fe12b54abbc8b802b234ec4974d0a420dde4098f57a324e9c791ac27/hypothesis-6.168.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a72ed7afa1f7e30488b8a5754fca0ad9755518bdb77d6f0b003cadf7437a5f9", size = 787932, upload-time = "2026-09-08T18:47:11.145Z" }, + { url = "https://files.pythonhosted.org/packages/ff/89/cc78e05f3248949c6d9ecb489d0e422247d11b08533ab02e177a10a01610/hypothesis-6.168.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcc5bad4300a751804ce41f0e10d77f85272668160708ce39ec579bca8984843", size = 1123994, upload-time = "2026-09-08T18:47:12.647Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e3/7d483a79e2ed9a6c868cac8783b3c6c2bb82303f7db364c9da9a406df10d/hypothesis-6.168.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53469a1a7c4861b12c9a8622f762d7d1fd7bcf171884e1018ed5a8f063a5c063", size = 1170271, upload-time = "2026-09-08T18:47:54.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/bffaf5b5e6b4566aa40d8a4ae25d8d84b47dfe0d9e771f454e4974cdb428/hypothesis-6.168.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a9650c4882fdbdd8e90bdae602a8bfa8c6f09dc5d06afec5b9b23982e8f60a04", size = 1300162, upload-time = "2026-09-08T18:46:27.022Z" }, + { url = "https://files.pythonhosted.org/packages/25/06/6c30a00fc2f5c58546e858259cf2261c6a3ef6a060c373e00d00311267be/hypothesis-6.168.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:348d9b93fd4129f67f9bab94f3d70709a9372bbe0e0d22731325ce85d5eb409f", size = 1336294, upload-time = "2026-09-08T18:46:37.324Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/501910e5a6a9c245f2394fda016376f6c7e209c5dca8c056eaece64ad10e/hypothesis-6.168.0-cp311-cp311-win_amd64.whl", hash = "sha256:719b45b0512e3535a6a0077c2f7c6053b02ac0e72d60693f66f98790a33855b2", size = 684464, upload-time = "2026-09-08T18:47:19.779Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5e/0035896c101f0484c364353f8ee30175eef8936b49171618677287fdd85d/hypothesis-6.168.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6b750390dac4429da0cb70ab3fe758457f0cea3d9c843d48c59d0690d1189fda", size = 793115, upload-time = "2026-09-08T18:48:21.352Z" }, + { url = "https://files.pythonhosted.org/packages/11/5c/938173e27df771cc6e92bc47f117a1b1be4a88fc6dc214f7fc65f9c7ad93/hypothesis-6.168.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e4b2d434e0dd134f3d31ac1efc1825bf99730dfe70fec005ff66d7211836d79", size = 784634, upload-time = "2026-09-08T18:47:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/00/0b2c6ac07d519131f97712f2533750ffe9b2490eec8f518ef3a5ed2dd514/hypothesis-6.168.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d4d36ed2fd62de11382f1d608169c1ffa9a49d3b9351146d8ff87cb81a66f7", size = 1122855, upload-time = "2026-09-08T18:46:21.967Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/dde930fe43171cab37572bd993d70c2a271f240f428f8ccd64ec4c2d661b/hypothesis-6.168.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5920d267f7d8cfd376672f2bde5905cdf284d47519582e41ce7c142d48ee46c4", size = 1168932, upload-time = "2026-09-08T18:46:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/4c/5d/92b83c3d06194ec626e92723d0b0f70221ebf42d7cb355ed36929df6d735/hypothesis-6.168.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fb8cdf45361e259df86e19f8cd042ce2d6c7e6ad88fa631b78a4e3a83c2e572d", size = 1298566, upload-time = "2026-09-08T18:48:19.485Z" }, + { url = "https://files.pythonhosted.org/packages/9c/67/52de8bf3446e3d2d555b812d96673b31bb213b5b9d5804a666e5d0bba76e/hypothesis-6.168.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b3ce1cce70b25a37ed1a38a53ce7204785726c675c0f41a0f83c338a7e47b3d", size = 1335074, upload-time = "2026-09-08T18:48:01.561Z" }, + { url = "https://files.pythonhosted.org/packages/14/fd/e592773c1c0ce55e35d26ec55f75546bf1fd72ef5a5c520ed685969b40cb/hypothesis-6.168.0-cp312-cp312-win_amd64.whl", hash = "sha256:f62bdabf278db9ff61df5f3203d608949f0d893d0e30cdac3f2330e67e41ae68", size = 682026, upload-time = "2026-09-08T18:47:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e9/39bb8fcccfbafd10fcc777d583c6ebad5d2148e7d743cb350562f28e974f/hypothesis-6.168.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d55562bf8d41cfa18559c33f30cadf44ceac8e517509d7a022a9feace621f28", size = 793050, upload-time = "2026-09-08T18:47:56.045Z" }, + { url = "https://files.pythonhosted.org/packages/f7/dd/00fd32e8ec470535e0065cb8d6e175f9fc54b4d3a6f1269f6c487f8bd79d/hypothesis-6.168.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92cff497b92e2285ff6a94193fdee04aba483a4115d501c1f9a570bd103fcd20", size = 784558, upload-time = "2026-09-08T18:46:32.054Z" }, + { url = "https://files.pythonhosted.org/packages/d2/4d/553c47093f68bdbac0438e16c024ce97649b5804dc6972ef86b9bd2db8a1/hypothesis-6.168.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ff259260015f9be3756dcd4bc11c08e007314dec6b43d9a89084c4f34f94475", size = 1122841, upload-time = "2026-09-08T18:48:09.374Z" }, + { url = "https://files.pythonhosted.org/packages/43/d6/0b5940aa75e617c8fd12200bae24d1b71347362514e8210735c581d4d3d1/hypothesis-6.168.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35f1262831b5acc74ded15f629965daffcd657f6016ee04fc9605f6eb2b334c0", size = 1168825, upload-time = "2026-09-08T18:47:33.702Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/800de1231b2869b51409bbf85799d6f1bf49a00e0afff0aabc097aa8f1b7/hypothesis-6.168.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:046fe4bcfce2a2fa186ba9d96bbb62c25c2f6c2e4071f0783ed6b5cc481d0669", size = 1298433, upload-time = "2026-09-08T18:46:38.53Z" }, + { url = "https://files.pythonhosted.org/packages/be/35/9907667a30c1dbabbc44a09b4c25a0f575570937fcbbada924ae3a1dbf2a/hypothesis-6.168.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:24b52a2b1c8db6e1e516f9295c8e4ef7ef63303ff24fbbc5b35f4ff71dcd732c", size = 1334988, upload-time = "2026-09-08T18:48:29.395Z" }, + { url = "https://files.pythonhosted.org/packages/98/8a/7bf214e703fff532ed47cb52ab93ce4b7ea41e4e7084593fa02b740828b7/hypothesis-6.168.0-cp313-cp313-win_amd64.whl", hash = "sha256:ec0886fe0be9091669937989f9a662beca42ae14a4a6dab25491c2c63365f88d", size = 681990, upload-time = "2026-09-08T18:47:49.173Z" }, + { url = "https://files.pythonhosted.org/packages/16/f7/5adae1bf1d4877aca2e8c8e007e57077237e9ac3765e430ffda490b17e19/hypothesis-6.168.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:754016594fe78cef91790e0922f60d183c52f531255fbfa30dac495b813e2128", size = 791056, upload-time = "2026-09-08T18:48:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/f2/93/b1b2770b87591db5cf564b9aa0265cf21e9207d28645e0abdeb8df63225b/hypothesis-6.168.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:6f0dd437ec01140676192422b61f2f833b3ce6a3213da9b7e196ad6b3777e795", size = 782980, upload-time = "2026-09-08T18:48:34.087Z" }, + { url = "https://files.pythonhosted.org/packages/de/bd/673171c1d2423379a7d4a0f9f009cca735a422d0c4a4ac6422d1d1736cab/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f77af7721ff35a58fa8797decd14c932c350a2548686c6e9b844db710a3a2441", size = 1120964, upload-time = "2026-09-08T18:46:59.92Z" }, + { url = "https://files.pythonhosted.org/packages/7d/00/33a9bd941b22a4fd8a8c805b1563e0db17efc020422f5bd15bdd0fdf258f/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a0d28418c104d7268fdebcc09bc49f7b6569b5eb942430c6859f53ec8d4edf63", size = 1143869, upload-time = "2026-09-08T18:46:49.875Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c1/963460976f41721eff8f67f30d31059ea407cc1b41c737c8859aabf37197/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:812a84c4cc7f7ae4fcb39a5647cc2698e6c18254f8423126425578f1dcdac782", size = 1146453, upload-time = "2026-09-08T18:46:25.757Z" }, + { url = "https://files.pythonhosted.org/packages/ce/53/09db238098ad66f4e6d2fe883f26c270c2595b90e21c8f969d4cb21cad7a/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6de30e559eb151de14a5f74bceb4d97792a9315ada2a1816b5da825cd7d28edc", size = 1166918, upload-time = "2026-09-08T18:48:23.436Z" }, + { url = "https://files.pythonhosted.org/packages/b9/31/e1b7b452c8a6166e445ba2ad80a864f6a9eee0fe4c8cecdb9af5c1ee0aa5/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:9018b20acdb061b2ef4b2fa7f558ca5db97ffea316e0a528bc003a24b2ac996e", size = 1126637, upload-time = "2026-09-08T18:47:50.878Z" }, + { url = "https://files.pythonhosted.org/packages/97/2b/4eceed248afb46fb6b2df21cf2239362de5b25d295c2dc67a82ec8657d5e/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bc935a5d5f86fd8f5af951b8fbe00307f6f7c596f82a9a27c17d974f6ab0a26c", size = 1155682, upload-time = "2026-09-08T18:47:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3e/f3414cda4f325004d5774485e8983b7d1b99e4b91013f013dd088fd778cf/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:45fcfa05f746e253350f55f216bcef59754f5f2b85745f1fc2bb8ba81dd517a9", size = 1296464, upload-time = "2026-09-08T18:46:34.833Z" }, + { url = "https://files.pythonhosted.org/packages/aa/40/ca79cf96545e1f172b36b8df56bfeb02b61f351f283026f9a57c4631e368/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:f89d8e998d3c936ffbbd1c3686c96f0378f6558aecc5967a3035a857f2bab0ad", size = 1421853, upload-time = "2026-09-08T18:47:23.083Z" }, + { url = "https://files.pythonhosted.org/packages/77/bc/657d5386740c1f4ac518ff05e4c5b132f1e6df2e7c865ba8fda4279adfa5/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0620fa320fa66649e6bfd71e94f3f86115fffebb7e3c6dcece19d1aaff8e07f", size = 1278216, upload-time = "2026-09-08T18:46:30.871Z" }, + { url = "https://files.pythonhosted.org/packages/51/54/2328cdb70489a36634534478d9b238594a269d8bec4630ea0e6897048347/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:4085b61e25d3dcc6c9151d4115269870aee8cdb921611ee5c989b2786449be09", size = 1297593, upload-time = "2026-09-08T18:46:41.395Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ca/d803fa57e3ff7f460f6e262d2b74822cf143343d378501fd3da601b12040/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:b5449a64eb37d9a4aa6ac9cd2ab0fd1a24145adf421ef1536884f73f39824887", size = 1333785, upload-time = "2026-09-08T18:48:25.434Z" }, + { url = "https://files.pythonhosted.org/packages/86/8f/b9799ae6ba6074f151db2c63f9f6f12d844512821ffaba1a3672d7e59f07/hypothesis-6.168.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:91e3de666a6c4f7543000d1710e25055d63ef3032c98bd2ab338b3087bdaa780", size = 675173, upload-time = "2026-09-08T18:47:52.601Z" }, + { url = "https://files.pythonhosted.org/packages/61/54/14c3e277b451ff24128ecc2673cac59dd7e535bce1a433c466912fd682e1/hypothesis-6.168.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:9a2079cd09919956dd388f1a1f8ea5a79f2b2437650fbeda31d8661217ffefef", size = 681491, upload-time = "2026-09-08T18:46:51.437Z" }, + { url = "https://files.pythonhosted.org/packages/99/f3/827e4a48ffee7e40244b0bf064ba47c2171e053ab7edf1cf770105e23401/hypothesis-6.168.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:085c9aa246487c56a40ca89003d285cbffdbb5be4097ba6d0139f9c21003c04a", size = 679197, upload-time = "2026-09-08T18:47:32.112Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/7f15e1b10d13e266b0f09cf3117dd2206104a1f6ac3b3baf6ec4c43b7c2e/hypothesis-6.168.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:16864797de4b024e4c6cebd44598af932f870aad811341bc5bc24c738801ff76", size = 792946, upload-time = "2026-09-08T18:47:35.438Z" }, + { url = "https://files.pythonhosted.org/packages/10/fb/32487bdcf68b3805ec5efcb7f92fb002b573ad3f90c252d2f2913c5d3124/hypothesis-6.168.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:283eda952bcb1987ccba1c8b634db0e8a960e1e92e2daa7003bc2392f19cea01", size = 788783, upload-time = "2026-09-08T18:46:42.862Z" }, + { url = "https://files.pythonhosted.org/packages/13/26/1aa42e0069ebc68d29d980d47420af324fa002494e01c1c0321c34f609ba/hypothesis-6.168.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5427a3c951080c18170486f775df6a82153882b819eca6b8e7ed77693634e5ab", size = 1124762, upload-time = "2026-09-08T18:48:07.512Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4e/99509045ed55aaa05d8408663051a93168d1a8a5bfc132d92ec1584ce47a/hypothesis-6.168.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a838218ff1eab8d7b4bf66b96037fce0a802f61f2fa5fd4b784696cac365ce7", size = 1171748, upload-time = "2026-09-08T18:46:47.029Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c9/96e57dfd89913322b5180b031b9e36a29bfe67842108b19904466476cd24/hypothesis-6.168.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:34e3c8b66047ba92f8b8df5e427074058d92db58038f007da4bf9d14e934ad3c", size = 685447, upload-time = "2026-09-08T18:46:35.97Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "librt" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/9b/356320fbae2ac8467e21c5e73e1389c80468e4998c62cc7d3536cc51b614/librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162", size = 214338, upload-time = "2026-08-07T10:49:42.663Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/12/e2e9ca532cf5a0e08c9489826c4a35c6958c92ba0313fda70e8c6c3912be/librt-0.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1a49adf16a7c9d9646816c2946135527197b6fcf4347c7b8b761cf1bfbf4489", size = 148673, upload-time = "2026-08-07T10:46:22.569Z" }, + { url = "https://files.pythonhosted.org/packages/6d/7c/02005e23478bd5950618d9712e0fd2b4c511657857f3efd8ba6a5feabcdd/librt-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81a398f45b45a59200e13cd5ad1ae1d3f44334de98b148331afe2cdfee701c52", size = 153547, upload-time = "2026-08-07T10:46:23.931Z" }, + { url = "https://files.pythonhosted.org/packages/a0/90/d8848a735f5642077fc4b3b4bebcdb08edf10178e3add45597f5201a368f/librt-0.15.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4eafbaff06b9563f8b1c850621ce51605de05208e09d4d71ce490bc972b7b9e8", size = 494355, upload-time = "2026-08-07T10:46:25.122Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0b/8604f41ea02feace490e9e405a338a15f9905369f55b239a9ce31c946f24/librt-0.15.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b0411b4066db926b80258c60dcb0e6db4c9cee312eab45b7e8866b17ddf9ada1", size = 485459, upload-time = "2026-08-07T10:46:26.447Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ac/84153bda1ce0da609182527ab92b40d961809e544eefdc5a1c2422971416/librt-0.15.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:febb1ce6cac545a54e6b769982824e955a700fdd9fbf3a08a3d82c990968b57d", size = 498398, upload-time = "2026-08-07T10:46:27.701Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3a/5ca6cd282b2c244bec8ec84102e09773264e9c02891d56ab3a8f0e4d7083/librt-0.15.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b230acc1c3bfe2d6f2627ba2b95dc92e58aa494600e9722d0e6ccbc931e59702", size = 515474, upload-time = "2026-08-07T10:46:28.9Z" }, + { url = "https://files.pythonhosted.org/packages/73/d3/bd34110234779eb843c6ed66aba7c9b2091d3dd85989f1fb9922f564cb7a/librt-0.15.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6da110e5f314c19ab8478464d02ae18808ae73d522c15260fa4918acdcd64da9", size = 509484, upload-time = "2026-08-07T10:46:30.124Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/43c3f7f071d71631a7daa3b835ef2168ea39f20692d81464d4e47fbaa6d6/librt-0.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:eab9208b00ca55bf75983ec99f7bf13acc746a36102e98953addaad7f7ea1e1b", size = 532534, upload-time = "2026-08-07T10:46:31.511Z" }, + { url = "https://files.pythonhosted.org/packages/c5/1c/b854adf036ea817c40408873a5b794d65a91d9f0f39826f2ad2a2d5d7f48/librt-0.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6c013cd3a1721e69e14380ada97eaa4b7b0cdf1c6b96fa765d4ea47c875088db", size = 537087, upload-time = "2026-08-07T10:46:32.734Z" }, + { url = "https://files.pythonhosted.org/packages/25/5c/c9a890e244e7dd725d3bd8b560e41f0aec787eaf343b46956a290ab7b841/librt-0.15.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:567b1c430f8bd560e689421468278ac5941bab4a05303b5d95b6ae10db03f451", size = 536575, upload-time = "2026-08-07T10:46:33.965Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c5/c8e70b60b704299555f55db468eb46b1c81bfc60201ffbfe20407d89870c/librt-0.15.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:29c4cab9df457b19672c39be7f384ebb2bc925c4e2684b8780c222b43eb36389", size = 517142, upload-time = "2026-08-07T10:46:35.577Z" }, + { url = "https://files.pythonhosted.org/packages/56/d1/767a90c41f5d381b3195bc88ac0ec4afda35777c9c781e1f9848fedd965e/librt-0.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bccbd8e5b0bffb7106cf18eb1baa3d7194b1cebb3b4b1cdbd4bdb19382a6ee6c", size = 558714, upload-time = "2026-08-07T10:46:36.829Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b4/3c0624b8dc8301ab808f2b3a910995bcabe28df070fb9a0e5505ae997dae/librt-0.15.0-cp310-cp310-win32.whl", hash = "sha256:8ae493ed5f659a7761c43d42f183db514536073ded9bcf671d2d1df47e29a07e", size = 104426, upload-time = "2026-08-07T10:46:38.594Z" }, + { url = "https://files.pythonhosted.org/packages/31/98/e91c0382304bedb2db9c6801897319a9dcb68daac5e975819b562362f20d/librt-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc25fb356d0c7810bb49ff3df908ad1fda6995d660ab099ded69244ed7ab6053", size = 125057, upload-time = "2026-08-07T10:46:40.052Z" }, + { url = "https://files.pythonhosted.org/packages/59/52/06790ced2ac7117f890c21bda43c39c958ec82aa665c0718e821d33ff939/librt-0.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:823b92cf3c18ecd08afc70c42473888b41b6e8ef5046f3b82c05c154a2fa3d22", size = 148039, upload-time = "2026-08-07T10:46:41.165Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1d/8e150b7fc449a1f33c8a760965cc1f43b14fc1577d9d0b50ab2701420e74/librt-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70bc1b602cf59917e8f0c7a2cbc8bcc6fbc14d5486136b00707a79619121d63", size = 153067, upload-time = "2026-08-07T10:46:42.418Z" }, + { url = "https://files.pythonhosted.org/packages/51/87/a162bc5a66a35599dc619ecb215145f4de7d68e886b479b6d12593139f7c/librt-0.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:814ff83a25b5fce8b9c80c4dd803153fb5c5599fc74db9e022466938368957ef", size = 493087, upload-time = "2026-08-07T10:46:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3a/aeea1fc620cf48060d3065b37614edbf97043c099d0f50782bc8ca61d897/librt-0.15.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:57f5eeb6ad4c180de583b1038e61fe5fbd9796bb69a8a1c1a0c7ddbec4c8c60f", size = 485608, upload-time = "2026-08-07T10:46:45.038Z" }, + { url = "https://files.pythonhosted.org/packages/52/ff/fe571ad416f0856fd0d5578ffc2e6dc531891e586e36b647bcf50569cab8/librt-0.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82909c8f7eb9952656b65d3147afde4cf8e6d5a991eebc86418b5e65843b0ab8", size = 498723, upload-time = "2026-08-07T10:46:46.35Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e1/7a65eb5dedb1f00aebd948cdd8e17add48bf066cab3514e9daf84ab45a6c/librt-0.15.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f779070399f991400fc451719e0ea388eb7de313388bada2c127a35de05f798a", size = 516002, upload-time = "2026-08-07T10:46:47.599Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/59832b0ebfbd08c2742e6ece372ceb53f18bf1faef5d33c8daf3abebf749/librt-0.15.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bac89069bc496ebdf4f79ebb57bbd10d0b214c8454225deb672d91002bd17e18", size = 508607, upload-time = "2026-08-07T10:46:48.873Z" }, + { url = "https://files.pythonhosted.org/packages/ea/0d/37fa73f3b43ebd8259f91ae9102a15e5a54e65d581e48dea72df3e81d7a4/librt-0.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e0d00c708fb2f5822b152429b1ac80a58dbbbc3f6c232c4d13a3f7fcf2ea5b4c", size = 530422, upload-time = "2026-08-07T10:46:50.45Z" }, + { url = "https://files.pythonhosted.org/packages/26/02/e046c6fe7a5881ac34623242192f484426ba8a75595fd18f22c53a3f530f/librt-0.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c6624fe268625869485553dd7cc1daf30d22558215bb2a4ff16f67a9801a31a", size = 534303, upload-time = "2026-08-07T10:46:51.693Z" }, + { url = "https://files.pythonhosted.org/packages/95/32/d5e6d861ab0366f3edf74f887ab0c9eb9f535aaf01d32b80b4f734daa179/librt-0.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f56b397858a23dacf35ede366ed2212fdc03a6a57a1ad36468ad6e9dc5fac091", size = 536084, upload-time = "2026-08-07T10:46:52.951Z" }, + { url = "https://files.pythonhosted.org/packages/2a/de/d69d725513fe53fc90c6d7a1f86e4428939bad2fb905b17fe4c18d413dde/librt-0.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4388184646efe2054911c5b00a1077d6d1ee86a95b7e8ba96dc7850a809f3f40", size = 514307, upload-time = "2026-08-07T10:46:54.194Z" }, + { url = "https://files.pythonhosted.org/packages/36/93/f8aded0d6682b4f25820fa86e0690f87f01df9fd7bd09ddb04d9167ad021/librt-0.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97335f59082f9fe2ce6c2a9cc6433a0114bbb6cd4d5c09dd76c95c68b9f9a8b0", size = 557686, upload-time = "2026-08-07T10:46:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/ffeb6bdeb6cd862b4272fddc8ad05f938dd25d020ed517e631813917d80a/librt-0.15.0-cp311-cp311-win32.whl", hash = "sha256:83380ffde38062a2e9bb55d83e74474f6614665528b98a6928720fc006dfffbb", size = 104917, upload-time = "2026-08-07T10:46:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/96/28/7e2313a3ffbf0b4de7ba3da58a09e488507b4bd1ea2b5e69378354a23415/librt-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:f75720477ee05d509a310e856cacc8d909adc182f7b91193c207bcc26d7ee6db", size = 125886, upload-time = "2026-08-07T10:46:57.729Z" }, + { url = "https://files.pythonhosted.org/packages/39/9e/04b8c3cde014ef255ee785730425268354543acc38902093a40afa0dc164/librt-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:256237037a3ab001ae8d9803b2d43562a4c3aa38739843694349e4d5ebb0fd56", size = 111885, upload-time = "2026-08-07T10:46:58.787Z" }, + { url = "https://files.pythonhosted.org/packages/ba/39/99c25030e782bdfb7a21be8c05254806a2e4bbb05c8d50c2a2130acbfa05/librt-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e87bc679f86a99aa3b26e3c78eeb821a247c9a28eae48eaafcc32c3bf4c3bb9e", size = 151021, upload-time = "2026-08-07T10:47:00.057Z" }, + { url = "https://files.pythonhosted.org/packages/14/43/f4b1bd1b2888798a1409808889a25ea1ba49eaabce7d681ed27734c2df9d/librt-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71599e011ac880e8e45d46047d714871894c7d4ab6f25626f8d4f89da21f368d", size = 155267, upload-time = "2026-08-07T10:47:01.311Z" }, + { url = "https://files.pythonhosted.org/packages/0c/db/3ad9c965c72f1e1d6beeec44ec10a54e17be8ae042fbb4baade16cbadced/librt-0.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c802434092b769b1d613ed2e13fac15fbfce1934a74bd10283b03c0fae231cd1", size = 503136, upload-time = "2026-08-07T10:47:02.45Z" }, + { url = "https://files.pythonhosted.org/packages/4b/07/5888a6d76acd62ebce66c61b74d94e9370b9c32929f111e487bb6546f8ed/librt-0.15.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5500eeae393a184d14e1f35645962c27129d20c81afa4069e6ef826ebc2b3aaa", size = 496670, upload-time = "2026-08-07T10:47:03.675Z" }, + { url = "https://files.pythonhosted.org/packages/29/39/ab57cc2f5b276156da02bb7f5a8921bada1cb1993ffec99acf811c602c23/librt-0.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ecfc32dfb46fb7b565bcd6abf9412acf978775a998273d22888a6d7953730dd", size = 513688, upload-time = "2026-08-07T10:47:04.981Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/bdbb0b648b5c2befb031f4c6f3b1dd857415e8fb492a25a3c764a6681e6c/librt-0.15.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cc46cfd15022e35084355478c9ac809d90b1152222706ac9a7655ec21df6fa", size = 531904, upload-time = "2026-08-07T10:47:06.211Z" }, + { url = "https://files.pythonhosted.org/packages/93/26/473c2e4b6c104e9e58e27ce95fc8005c8bd4fc36cae4f254371125a92db8/librt-0.15.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5f51401d102c885b9ca509e62c79b1dbff286e1b9b047fde6f763780789356d", size = 524427, upload-time = "2026-08-07T10:47:07.592Z" }, + { url = "https://files.pythonhosted.org/packages/26/60/03b3abb82b41714671b907bf6989b228e31e6a8af52dec82b5b0728dc250/librt-0.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cc30523e3f1a23fb7511cc659834a0d01a1042bb9de359bc1c131cc4ec6c9656", size = 543155, upload-time = "2026-08-07T10:47:08.866Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0e/9bb1f0a4affbd0a1888f4f79dc03ed2a299d9a2c26c59ab2a97dcbf11903/librt-0.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:59fe030d8ae4a57e3fb7756bf35a858de74e04066fc8555c53d0af979132af81", size = 546890, upload-time = "2026-08-07T10:47:10.327Z" }, + { url = "https://files.pythonhosted.org/packages/dc/84/6937a280d461f7de6e031ffb02edc2b7c3c90d49d630565ce8ff27cbc5f2/librt-0.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a6526a2a956bbb1e4ae3568c82e650fc99119c66bb011ea60715744955a2b4d", size = 555163, upload-time = "2026-08-07T10:47:11.798Z" }, + { url = "https://files.pythonhosted.org/packages/bc/95/2a2853c1ee014bf102116e7f897a04beeaeb2461b45b79af98bdfb95f1ef/librt-0.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:85ea21ec6730194d67156b0e0b5430ccb1d61f8b8b907e39b37f9812b74a13f0", size = 535812, upload-time = "2026-08-07T10:47:13.279Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4c/cf9601c1b4c5f09280acd5d83abdb2e68527a2be8257136eb42304218622/librt-0.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e47b8ba865d7ede071a91a7163073bbaeb72541f1ef8a07d512c45c7b5007f2", size = 573688, upload-time = "2026-08-07T10:47:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/47/6d/9ac7cbec46189a7625af4b5acbd25f10d827f4141b2002181848c8418923/librt-0.15.0-cp312-cp312-win32.whl", hash = "sha256:a5207ec414d1c4a2a7231b2086970dc036f94293cdf338190984958a013a42f1", size = 106138, upload-time = "2026-08-07T10:47:15.973Z" }, + { url = "https://files.pythonhosted.org/packages/38/d0/2ae99c83be86ce23f925ac1aeeedc777e97f427c4a8d190c70d0a16e9a87/librt-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:73b30cfa976659b3917c8f6153bdb0591c6a9ec6583599fd24a689b690622022", size = 126974, upload-time = "2026-08-07T10:47:17.049Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ef/dd24f9635c730b86b87587967dda7516b1845e8b17684603d31607fed598/librt-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:a54cf9e0ef47b96af580849db5471142200568ce1e02cbf416addab551369570", size = 112292, upload-time = "2026-08-07T10:47:18.222Z" }, + { url = "https://files.pythonhosted.org/packages/e7/42/467b53a601b406ccd7b97c1fd54b59cb34f9185ad5ce7e9d5c3c4e8961c8/librt-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26", size = 151029, upload-time = "2026-08-07T10:47:19.312Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e6/36c2299b7a94b84fdd01220d8a777a71be5be0925bb0dbdf71c0a06a34d9/librt-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801", size = 155194, upload-time = "2026-08-07T10:47:20.398Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/ed5071f9325845e670bd36012757419767fbf56af77ed483077b9e4db541/librt-0.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc", size = 502568, upload-time = "2026-08-07T10:47:21.652Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/6450c67c3615d87704bcbc21323fafc69c799b06a044c447529f725d4b01/librt-0.15.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95", size = 496153, upload-time = "2026-08-07T10:47:22.925Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d6/5f52b722bc75076954b3bfd49be15ea362df4d580c6fb315d0f617100d30/librt-0.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b", size = 513336, upload-time = "2026-08-07T10:47:24.213Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e2/c08fd1d36ce63ea5a12b85c5d37f4550b5f86a692167e41e5a74222607ae/librt-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2", size = 531661, upload-time = "2026-08-07T10:47:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d8/d9482fcbeb177b9eb87bb3899eeb3b42be690313c652f9e146b1d0681fb2/librt-0.15.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3", size = 524487, upload-time = "2026-08-07T10:47:26.79Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/075171517b41f861753034fbb151b42cfc83bcc853849f24f5e66fd60ccf/librt-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785", size = 543201, upload-time = "2026-08-07T10:47:27.999Z" }, + { url = "https://files.pythonhosted.org/packages/b0/03/42c2330f37eeb475b6affeedd06518f60035f323af3a839335e3fc9fef2d/librt-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6", size = 546467, upload-time = "2026-08-07T10:47:29.396Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/1ad4c5638f7e64d8560328bd25c54b409a661bdb6ff254b38ff90744288d/librt-0.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101", size = 555139, upload-time = "2026-08-07T10:47:30.815Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/39fa7d15db1204cd1cbe6514680fbdc243adf754a0885061308f43afc013/librt-0.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218", size = 536050, upload-time = "2026-08-07T10:47:32.222Z" }, + { url = "https://files.pythonhosted.org/packages/1e/88/c6dcf0dd8e26dc0c9a499a2abab8646c86dcaf9ecea9524cb46d3686331a/librt-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b", size = 573700, upload-time = "2026-08-07T10:47:33.527Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9b/ab54c71a7918a7c34fa5327fb61390a77446a07a146fbfb1165250a61035/librt-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab", size = 82194, upload-time = "2026-08-07T10:47:34.835Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b2/4f9a243bb892395f3becb80789ade13771701091f9f07ab8230247953ba8/librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890", size = 106231, upload-time = "2026-08-07T10:47:36.251Z" }, + { url = "https://files.pythonhosted.org/packages/bf/af/64aff4885a40b93132382f2c314647d722574605416504379184ef3045ea/librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8", size = 126996, upload-time = "2026-08-07T10:47:37.453Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/335bccf6c7cb9028cb0b54aead27d9ece3f01f83bc6baa2abace5da655c1/librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad", size = 112188, upload-time = "2026-08-07T10:47:38.729Z" }, +] + +[[package]] +name = "mypy" +version = "1.20.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/af/e3d4b3e9ec91a0ff9aabfdb38692952acf49bbb899c2e4c29acb3a6da3ae/mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665", size = 3817349, upload-time = "2026-04-21T17:12:28.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/97/ce2502df2cecf2ef997b6c6527c4a223b92feb9e7b790cdc8dcd683f3a8a/mypy-1.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cf5a4db6dca263010e2c7bff081c89383c72d187ba2cf4c44759aac970e2f0c4", size = 14457059, upload-time = "2026-04-21T17:06:14.935Z" }, + { url = "https://files.pythonhosted.org/packages/c9/34/417ee60b822cc80c0f3dc9f495ad7fd8dbb8d8b2cf4baf22d4046d25d01d/mypy-1.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7b0e817b518bff7facd7f85ea05b643ad8bdcce684cf29784987b0a7c8e1f997", size = 13346816, upload-time = "2026-04-21T17:10:41.433Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/e20951978702df58379d0bcc2e8f7ccdca4e78cd7dc66dd3ddbf9b29d517/mypy-1.20.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97d7b9a485b40f8ca425460e89bf1da2814625b2da627c0dcc6aa46c92631d14", size = 13772593, upload-time = "2026-04-21T17:08:11.24Z" }, + { url = "https://files.pythonhosted.org/packages/63/a5/5441a13259ec516c56fd5de0fd96a69a9590ae6c5e5d3e5174aa84b97973/mypy-1.20.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e1c12f6d2db3d78b909b5f77513c11eb7f2dd2782b96a3ab6dffc7d44575c99", size = 14656635, upload-time = "2026-04-21T17:09:54.042Z" }, + { url = "https://files.pythonhosted.org/packages/3b/51/b89c69157c5e1f19fd125a65d991166a26906e7902f026f00feebbcfa2b9/mypy-1.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:89dce27e142d25ffbc154c1819383b69f2e9234dc4ed4766f42e0e8cb264ab5c", size = 14943278, upload-time = "2026-04-21T17:09:15.599Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/6b0eeecfe96d7cce1d71c66b8e03cb304aa70ec11f1955dc1d6b46aca3c3/mypy-1.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:f376e37f9bf2a946872fc5fd1199c99310748e3c26c7a26683f13f8bdb756cbd", size = 10851915, upload-time = "2026-04-21T17:06:03.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/6593dc88545d75fb96416184be5392da5e2a8e8c2802a8597913e16ae25c/mypy-1.20.2-cp310-cp310-win_arm64.whl", hash = "sha256:6e2b469efd811707bc530fd1effef0f5d6eebcb7fe376affae69025da4b979a2", size = 9786676, upload-time = "2026-04-21T17:07:02.035Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4d/9ebeae211caccbdaddde7ed5e31dfcf57faac66be9b11deb1dc6526c8078/mypy-1.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4077797a273e56e8843d001e9dfe4ba10e33323d6ade647ff260e5cd97d9758c", size = 14371307, upload-time = "2026-04-21T17:08:56.442Z" }, + { url = "https://files.pythonhosted.org/packages/95/d7/93473d34b61f04fac1aecc01368485c89c5c4af7a4b9a0cab5d77d04b63f/mypy-1.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cdecf62abcc4292500d7858aeae87a1f8f1150f4c4dd08fb0b336ee79b2a6df3", size = 13258917, upload-time = "2026-04-21T17:05:50.978Z" }, + { url = "https://files.pythonhosted.org/packages/e2/30/3dd903e8bafb7b5f7bf87fcd58f8382086dea2aa19f0a7b357f21f63071b/mypy-1.20.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c566c3a88b6ece59b3d70f65bedef17304f48eb52ff040a6a18214e1917b3254", size = 13700516, upload-time = "2026-04-21T17:11:33.161Z" }, + { url = "https://files.pythonhosted.org/packages/07/05/c61a140aba4c729ac7bc99ae26fc627c78a6e08f5b9dd319244ea71a3d7e/mypy-1.20.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0deb80d062b2479f2c87ae568f89845afc71d11bc41b04179e58165fd9f31e98", size = 14562889, upload-time = "2026-04-21T17:05:27.674Z" }, + { url = "https://files.pythonhosted.org/packages/fd/87/da78243742ffa8a36d98c3010f0d829f93d5da4e6786f1a1a6f2ad616502/mypy-1.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bba9ad231e92a3e424b3e56b65aa17704993425bba97e302c832f9466bb85bac", size = 14803844, upload-time = "2026-04-21T17:10:06.2Z" }, + { url = "https://files.pythonhosted.org/packages/37/52/10a1ddf91b40f843943a3c6db51e2df59c9e237f29d355e95eaab427461f/mypy-1.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:baf593f2765fa3a6b1ef95807dbaa3d25b594f6a52adcc506a6b9cb115e1be67", size = 10846300, upload-time = "2026-04-21T17:12:23.886Z" }, + { url = "https://files.pythonhosted.org/packages/20/02/f9a4415b664c53bd34d6709be59da303abcae986dc4ac847b402edb6fa1e/mypy-1.20.2-cp311-cp311-win_arm64.whl", hash = "sha256:20175a1c0f49863946ec20b7f63255768058ac4f07d2b9ded6a6b46cfb5a9100", size = 9779498, upload-time = "2026-04-21T17:09:23.695Z" }, + { url = "https://files.pythonhosted.org/packages/71/4e/7560e4528db9e9b147e4c0f22660466bf30a0a1fe3d63d1b9d3b0fd354ee/mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b", size = 14539393, upload-time = "2026-04-21T17:07:12.52Z" }, + { url = "https://files.pythonhosted.org/packages/32/d9/34a5efed8124f5a9234f55ac6a4ced4201e2c5b81e1109c49ad23190ec8c/mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4", size = 13361642, upload-time = "2026-04-21T17:06:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/d1/14/eb377acf78c03c92d566a1510cda8137348215b5335085ef662ab82ecd3a/mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6", size = 13740347, upload-time = "2026-04-21T17:12:04.73Z" }, + { url = "https://files.pythonhosted.org/packages/b9/94/7e4634a32b641aa1c112422eed1bbece61ee16205f674190e8b536f884de/mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066", size = 14734042, upload-time = "2026-04-21T17:07:43.16Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f3/f7e62395cb7f434541b4491a01149a4439e28ace4c0c632bbf5431e92d1f/mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102", size = 14964958, upload-time = "2026-04-21T17:11:00.665Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0d/47e3c3a0ec2a876e35aeac365df3cac7776c36bbd4ed18cc521e1b9d255b/mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9", size = 10911340, upload-time = "2026-04-21T17:10:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b2/6c852d72e0ea8b01f49da817fb52539993cde327e7d010e0103dc12d0dac/mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58", size = 9833947, upload-time = "2026-04-21T17:09:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c4/b93812d3a192c9bcf5df405bd2f30277cd0e48106a14d1023c7f6ed6e39b/mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026", size = 14524670, upload-time = "2026-04-21T17:10:30.737Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/42c122501bff18eaf1e8f457f5c017933452d8acdc52918a9f59f6812955/mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943", size = 13336218, upload-time = "2026-04-21T17:08:44.069Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/75bbc92f41725fbd585fb17b440b1119b576105df1013622983e18640a93/mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517", size = 13724906, upload-time = "2026-04-21T17:08:01.02Z" }, + { url = "https://files.pythonhosted.org/packages/a1/32/4c49da27a606167391ff0c39aa955707a00edc500572e562f7c36c08a71f/mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15", size = 14726046, upload-time = "2026-04-21T17:11:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/7f/fc/4e354a1bd70216359deb0c9c54847ee6b32ef78dfb09f5131ff99b494078/mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee", size = 14955587, upload-time = "2026-04-21T17:12:16.033Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/c0f2056e9eb8f08c62cafd9715e4584b89132bdc832fcf85d27d07b5f3e5/mypy-1.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f", size = 10922681, upload-time = "2026-04-21T17:06:35.842Z" }, + { url = "https://files.pythonhosted.org/packages/e5/14/065e333721f05de8ef683d0aa804c23026bcc287446b61cac657b902ccac/mypy-1.20.2-cp313-cp313-win_arm64.whl", hash = "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330", size = 9830560, upload-time = "2026-04-21T17:07:51.023Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/f23c163e25b11074188251b0b5a0342625fc1cdb6af604757174fa9acc9b/mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563", size = 2637314, upload-time = "2026-04-21T17:05:54.5Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/78/449cb84790bd5cc3823b2652ee405a4558856e5c4195aee3a16bf7b3eb5d/ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b", size = 4938814, upload-time = "2026-09-16T15:54:46.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/25/6071aabc530e9be7e2c195e8fe3f7aea2735405b6cf447212832d7811831/ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38", size = 10048966, upload-time = "2026-09-16T15:53:57.605Z" }, + { url = "https://files.pythonhosted.org/packages/54/98/07f90ecbc74dd5fb5764f11f2bc774d6a7cffef92d2ff5f5b4e9e23c754e/ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e", size = 10165498, upload-time = "2026-09-16T15:54:01.14Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1f/e6a712e3b47cad4a40600134105ed193cb773f618a42eb7ba323cb812cc0/ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170", size = 9830004, upload-time = "2026-09-16T15:54:03.998Z" }, + { url = "https://files.pythonhosted.org/packages/23/f2/311a08776d75d81c7676e20b6b020ae63cbe881fcdc7a8dd64e6e18bdd93/ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659", size = 9986558, upload-time = "2026-09-16T15:54:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ed/37b6cb3d3ba8c73e68ae3eb1d502383beb5aa05a582bb7bb3a922f929f54/ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2", size = 9877332, upload-time = "2026-09-16T15:54:09.552Z" }, + { url = "https://files.pythonhosted.org/packages/22/cc/40873a8f36ad084cc540d55fcca7077264d5b13b24659e9180c176fb2b08/ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812", size = 10507125, upload-time = "2026-09-16T15:54:12.152Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e4/fc91a642b78ccbab6b9477720f3644ae7a10a9bcce69a934679cd64f62bc/ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b", size = 11336694, upload-time = "2026-09-16T15:54:15.489Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/bbd2a9a600a4e73dc3e7548a249c8d1671273464b55822c6fae50f602dff/ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f", size = 10774448, upload-time = "2026-09-16T15:54:18.16Z" }, + { url = "https://files.pythonhosted.org/packages/1a/41/d83af9879a7b6e8bf5fe16b1da0b134049d2f5d3afac12defb0897cb84bd/ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df", size = 10323796, upload-time = "2026-09-16T15:54:20.743Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/cefd07bfe914b84943ea769ade8d607bd22750b965d3228eefd7cebd15d0/ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e", size = 10514115, upload-time = "2026-09-16T15:54:23.497Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9d/76a2e26c79a23be6e6e3664c57bec9e9fc8de155cfb9e4b67ea91b64f9d7/ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f", size = 10072582, upload-time = "2026-09-16T15:54:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d4/f42edddb39668af1a559ceafa3823aedd65633a48dc9768e775485faa2c1/ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2", size = 9879644, upload-time = "2026-09-16T15:54:29.278Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/913e3195d95e0378786c6656945c865f534a3560e29139da4882aff630d1/ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9", size = 10231569, upload-time = "2026-09-16T15:54:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c4/8aa6ea0bdcedbd1bf87397e2fc4ed8406448ea5842f8660bc6e5f163039d/ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773", size = 10663666, upload-time = "2026-09-16T15:54:34.838Z" }, + { url = "https://files.pythonhosted.org/packages/3d/02/7f10ef4700bc223c30a3fdd10631a29830c45524b810a3c7ed947af64591/ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f", size = 10093472, upload-time = "2026-09-16T15:54:37.47Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5d/a509c07d714b6da88f2c518b4637cf6f1d46b074be8f0f1e5fb9ff5126fe/ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa", size = 10586899, upload-time = "2026-09-16T15:54:40.488Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316, upload-time = "2026-09-16T15:54:43.332Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] diff --git a/plugins/foundation/darrow-information-architecture/bin/ia-doctor b/plugins/foundation/darrow-information-architecture/bin/ia-doctor deleted file mode 100755 index c2d38240..00000000 --- a/plugins/foundation/darrow-information-architecture/bin/ia-doctor +++ /dev/null @@ -1,685 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -usage() { - echo "usage: ia-doctor inspect|verify [--runtime auto|both|codex|claude] [--mirror source=target]... [repository]" >&2 - exit 64 -} - -resolve_root() { - local target=${1:-.} line worktrees - if [[ ! -d "$target" || ! -r "$target" ]]; then - echo "error: repository path is not a readable directory: $target" >&2 - exit 2 - fi - worktrees=$(git -C "$target" worktree list --porcelain 2>/dev/null || :) - line=$(awk 'NR==1 && $1=="worktree" {sub(/^worktree /, ""); print}' <<< "$worktrees") - if [[ -n "$line" ]]; then - printf '%s' "$line" - else - (cd "$target" && pwd -P) - fi -} - -relpath() { - case "$1" in - "$ROOT") printf '.' ;; - "$ROOT"/*) printf '%s' "${1#"$ROOT"/}" ;; - *) printf '%s' "$1" ;; - esac -} - -abspath() { - case "$1" in - /*) printf '%s' "$1" ;; - .) printf '%s' "$ROOT" ;; - *) printf '%s/%s' "$ROOT" "$1" ;; - esac -} - -inside_root() { - case "$1" in "$ROOT"|"$ROOT"/*) return 0 ;; *) return 1 ;; esac -} - -inventory_file() { - if [[ -f "$1" || -L "$1" ]]; then printf '%s\n' "$1" >> "$FILES"; fi -} - -add_file() { - local file=$1 - inventory_file "$file" - if [[ -L "$file" && ! -e "$file" ]]; then - printf '%s\n' "$file (broken symlink)" >> "$UNREADABLE" - return - fi - if [[ -L "$file" && ! -f "$file" ]]; then - printf '%s\n' "$file (non-file symlink)" >> "$UNREADABLE" - return - fi - if [[ -e "$file" && ! -r "$file" ]]; then - printf '%s\n' "$file" >> "$UNREADABLE" - return - fi -} - -extract_references() { - LC_ALL=C awk ' - function emit(value, kind, origin) { - gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) - if (value != "") print kind "\t" origin "\t" value - } - function fence_marker(line, text,ch,n,i) { - n=0 - while (n < length(line) && substr(line, n + 1, 1) == " ") n++ - if (n > 3) return "" - text=substr(line, n + 1) - ch=substr(text, 1, 1) - if (ch != "`" && ch != "~") return "" - n=0 - for (i=1; i<=length(text) && substr(text, i, 1)==ch; i++) n++ - return n >= 3 ? ch n : "" - } - function fence_closes(line, ch, need, spaces,text,n,tail) { - spaces=0 - while (spaces < length(line) && substr(line, spaces + 1, 1) == " ") spaces++ - if (spaces > 3) return 0 - text=substr(line, spaces + 1) - n=0 - while (n < length(text) && substr(text, n + 1, 1) == ch) n++ - tail=substr(text, n + 1) - return n >= need && tail ~ /^[[:space:]]*$/ - } - { - line=$0 - if (NR == 1 && substr(line, 1, 3) == sprintf("%c%c%c", 239, 187, 191)) line=substr(line, 4) - marker=fence_marker(line) - if (in_fence) { - if (marker != "" && fence_closes(line, fence_ch, fence_n)) {in_fence=0; fence_ch=""; fence_n=0} - next - } - if (marker != "") { - fence_ch=substr(marker, 1, 1); fence_n=substr(marker, 2) + 0; in_fence=1 - next - } - lower=tolower(line) - kind=(lower ~ /(^|[^a-z])(read|consult|load|follow)([^a-z]|$)/ || lower ~ /before .*(edit|chang|work)/ || line ~ /(^|[[:space:]])(->|→)([[:space:]]|$)/) ? "explicit" : "mention" - rest=line - while (match(rest, /\[[^][]*\]\([^()]*\)/)) { - value=substr(rest, RSTART, RLENGTH) - sub(/^.*\]\(/, "", value); sub(/\)$/, "", value) - emit(value, kind, "ordinary") - rest=substr(rest, RSTART + RLENGTH) - } - rest=line - while (match(rest, /`[^`]+`/)) { - value=substr(rest, RSTART + 1, RLENGTH - 2) - emit(value, kind, "ordinary") - rest=substr(rest, RSTART + RLENGTH) - } - rest=line - gsub(/`[^`]*`/, "", rest) - while (match(rest, /(^|[[:space:]])@[.A-Za-z0-9_\/-]+\.md/)) { - value=substr(rest, RSTART, RLENGTH) - sub(/^[[:space:]]*@/, "", value) - emit(value, "explicit", "import") - rest=substr(rest, RSTART + RLENGTH) - } - } - ' "$1" -} - -clean_reference() { - local ref=$1 - ref=${ref#<}; ref=${ref%>} - ref=${ref%%#*} - case "$ref" in - http://*|https://*|mailto:*|codex:*|file:*|'') return 1 ;; - *' '*|*$'\t'*|*'<'*|*'>'*|*'$'*|*'{'*|*'}'*|*'*'*) return 1 ;; - esac - case "$ref" in - */*.*|*.md) printf '%s' "${ref%/}" ;; - *) return 1 ;; - esac -} - -canonical_existing() { - local candidate=$1 dir base - [[ -f "$candidate" ]] || return 1 - dir=$(dirname "$candidate") - base=$(basename "$candidate") - dir=$(cd "$dir" 2>/dev/null && pwd -P) || return 1 - printf '%s/%s' "$dir" "$base" -} - -resolve_reference() { - local ref=$1 candidate resolved - candidate="$ROOT/${ref#./}" - if resolved=$(canonical_existing "$candidate"); then - printf '%s' "$resolved" - return - fi - return 1 -} - -resolve_source_relative() { - local source=$1 ref=$2 candidate resolved - case "$ref" in - /*) candidate=$ref ;; - *) candidate="$(dirname "$source")/$ref" ;; - esac - if resolved=$(canonical_existing "$candidate"); then - printf '%s' "$resolved" - return - fi - return 1 -} - -owning_skill() { - local dir - dir=$(dirname "$1") - while inside_root "$dir"; do - if [[ -f "$dir/SKILL.md" ]]; then printf '%s' "$dir"; return 0; fi - [[ "$dir" != "$ROOT" ]] || break - dir=$(dirname "$dir") - done - return 1 -} - -print_file_record() { - local file=$1 bytes rel scope - if [[ ! -f "$file" || ! -r "$file" ]]; then - printf ' - %s | unreadable\n' "$file" - return - fi - bytes=$(wc -c < "$file" | tr -d ' ') - rel=$(relpath "$file") - scope=referenced - case "$rel" in - AGENTS.md|AGENTS.override.md|CLAUDE.md|CLAUDE.local.md|.claude/CLAUDE.md) scope=root ;; - */AGENTS.md|*/AGENTS.override.md|*/CLAUDE.md|*/CLAUDE.local.md) scope=nested ;; - .claude/rules/*) scope=claude-native-rule ;; - .agent-shared/rules/*|.agents/rules/*|.codex/rules/*|.pi/rules/*) scope=adapter-rule ;; - esac - printf ' - %s | scope=%s | bytes=%s | approx_tokens=%s\n' "$file" "$scope" "$bytes" "$(( (bytes + 3) / 4 ))" -} - -is_route_reference() { - case "$1" in - .agent-shared/*|.claude/*|.agents/*|.codex/*|.pi/*|AGENTS.md|AGENTS.override.md|CLAUDE.md|CLAUDE.local.md|*/AGENTS.md|*/AGENTS.override.md|*/CLAUDE.md|*/CLAUDE.local.md) return 0 ;; - *) return 1 ;; - esac -} - -print_line_findings() { - local label=$1 file=$2 count=0 limit=40 - while IFS= read -r line; do - count=$((count + 1)) - if [[ $count -le $limit ]]; then printf ' - %s | %s\n' "$label" "$line"; fi - done < "$file" - if [[ $count -gt $limit ]]; then printf ' - %s | ... %s additional findings omitted\n' "$label" "$((count - limit))"; fi -} - -validate_mirror_spec() { - local spec=$1 source target - case "$spec" in *=*) ;; *) echo "error: mirror must be source=target: $spec" >&2; exit 64 ;; esac - source=${spec%%=*} - target=${spec#*=} - case "$source:$target" in *:|:*|/*:*|*:/*|../*:*|*:../*|*/../*:*|*:*/../*) - echo "error: mirror paths must stay inside the repository: $spec" >&2 - exit 64 - ;; - esac -} - -[[ ${1:-} == "inspect" || ${1:-} == "verify" ]] || usage -MODE=$1 -shift -RUNTIME=auto -REPOSITORY=. -MIRROR_SPECS="" -while [[ $# -gt 0 ]]; do - case "$1" in - --runtime) - [[ $# -ge 2 ]] || usage - RUNTIME=$2 - shift 2 - ;; - --runtime=*) - RUNTIME=${1#*=} - shift - ;; - --mirror) - [[ $# -ge 2 ]] || usage - MIRROR_SPECS="$MIRROR_SPECS${MIRROR_SPECS:+$'\n'}$2" - shift 2 - ;; - --mirror=*) - MIRROR_SPECS="$MIRROR_SPECS${MIRROR_SPECS:+$'\n'}${1#*=}" - shift - ;; - -*) usage ;; - *) - [[ "$REPOSITORY" == "." ]] || usage - REPOSITORY=$1 - shift - ;; - esac -done -case "$RUNTIME" in auto|both|codex|claude) ;; *) usage ;; esac -ROOT=$(resolve_root "$REPOSITORY") -if [[ "$RUNTIME" == "auto" ]]; then - has_codex=0 - has_claude=0 - [[ -f "$ROOT/AGENTS.md" || -f "$ROOT/AGENTS.override.md" ]] && has_codex=1 - [[ -f "$ROOT/CLAUDE.md" || -f "$ROOT/.claude/CLAUDE.md" ]] && has_claude=1 - if [[ $has_codex -eq 1 && $has_claude -eq 1 ]]; then RUNTIME=both - elif [[ $has_claude -eq 1 ]]; then RUNTIME=claude - else RUNTIME=codex - fi -fi -TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT -FILES="$TMP/files" -ENTRYPOINTS="$TMP/entrypoints" -SEEDS="$TMP/seeds" -CODEX_SEEDS="$TMP/codex-seeds" -CLAUDE_SEEDS="$TMP/claude-seeds" -EDGES="$TMP/edges" -EXPLICIT_EDGES="$TMP/explicit-edges" -BROKEN="$TMP/broken" -UNREADABLE="$TMP/unreadable" -NONROOT="$TMP/nonroot" -MISSING_RUNTIME="$TMP/missing-runtime" -CYCLES="$TMP/cycles" -UNROUTED="$TMP/unrouted" -DUPLICATES="$TMP/duplicates" -: > "$FILES"; : > "$ENTRYPOINTS"; : > "$SEEDS"; : > "$CODEX_SEEDS"; : > "$CLAUDE_SEEDS" -: > "$EDGES"; : > "$EXPLICIT_EDGES"; : > "$BROKEN"; : > "$UNREADABLE"; : > "$NONROOT" -: > "$MISSING_RUNTIME"; : > "$CYCLES"; : > "$UNROUTED"; : > "$DUPLICATES" -ROOT_ADAPTER_STATUS=none -ROOT_ADAPTER_SOURCE="$ROOT/AGENTS.md" -ROOT_ADAPTER_TARGET="$ROOT/CLAUDE.md" -if [[ -e "$ROOT/AGENTS.md" && -L "$ROOT/CLAUDE.md" && "$ROOT/AGENTS.md" -ef "$ROOT/CLAUDE.md" ]]; then - ROOT_ADAPTER_STATUS=symlink -elif [[ -e "$ROOT/CLAUDE.md" && -L "$ROOT/AGENTS.md" && "$ROOT/CLAUDE.md" -ef "$ROOT/AGENTS.md" ]]; then - ROOT_ADAPTER_STATUS=symlink - ROOT_ADAPTER_SOURCE="$ROOT/CLAUDE.md" - ROOT_ADAPTER_TARGET="$ROOT/AGENTS.md" -fi - -find "$ROOT" \( -type f -o -type l \) \ - \( -name 'AGENTS.md' -o -name 'AGENTS.override.md' -o -name 'CLAUDE.md' -o -name 'CLAUDE.local.md' \) \ - -not -path '*/.git/*' -not -path '*/node_modules/*' -not -path '*/.claude/worktrees/*' -not -path '*/.worktrees/*' -print 2>/dev/null | - LC_ALL=C sort > "$ENTRYPOINTS" - -while IFS= read -r file; do - inventory_file "$file" - rel=$(relpath "$file") - case "$rel" in - AGENTS.override.md) - [[ -s "$file" ]] && printf '%s\n' "$file" >> "$CODEX_SEEDS" - ;; - AGENTS.md) - [[ ! -s "$ROOT/AGENTS.override.md" ]] && printf '%s\n' "$file" >> "$CODEX_SEEDS" - ;; - CLAUDE.md|.claude/CLAUDE.md|CLAUDE.local.md|*/CLAUDE.md|*/CLAUDE.local.md) - printf '%s\n' "$file" >> "$CLAUDE_SEEDS" - ;; - esac -done < "$ENTRYPOINTS" - -if [[ "$RUNTIME" != "claude" && ! -s "$CODEX_SEEDS" ]]; then printf '%s\n' codex >> "$MISSING_RUNTIME"; fi -if [[ "$RUNTIME" != "codex" && ! -f "$ROOT/CLAUDE.md" && ! -f "$ROOT/.claude/CLAUDE.md" ]]; then - printf '%s\n' claude >> "$MISSING_RUNTIME" -fi - -for dir in .claude/rules .agent-shared/rules .agents/rules .codex/rules .pi/rules; do - if [[ -d "$ROOT/$dir" ]]; then - find "$ROOT/$dir" \( -type f -o -type l \) -name '*.md' -print 2>/dev/null | - while IFS= read -r file; do inventory_file "$file"; done - fi -done -if [[ -d "$ROOT/.claude/rules" ]]; then - find "$ROOT/.claude/rules" \( -type f -o -type l \) -name '*.md' -print 2>/dev/null >> "$CLAUDE_SEEDS" -fi -LC_ALL=C sort -u "$FILES" -o "$FILES" -LC_ALL=C sort -u "$CODEX_SEEDS" -o "$CODEX_SEEDS" -LC_ALL=C sort -u "$CLAUDE_SEEDS" -o "$CLAUDE_SEEDS" -case "$RUNTIME" in - codex) cp "$CODEX_SEEDS" "$SEEDS" ;; - claude) cp "$CLAUDE_SEEDS" "$SEEDS" ;; - both) cat "$CODEX_SEEDS" "$CLAUDE_SEEDS" | LC_ALL=C sort -u > "$SEEDS" ;; -esac -cp "$SEEDS" "$TMP/parse-files" -if [[ -n "$MIRROR_SPECS" ]]; then - while IFS= read -r spec; do - validate_mirror_spec "$spec" - for dir in "${spec%%=*}" "${spec#*=}"; do - if [[ -d "$ROOT/$dir" && -r "$ROOT/$dir" && -x "$ROOT/$dir" ]]; then - find "$ROOT/$dir" \( -type f -o -type l \) -name '*.md' -print >> "$TMP/parse-files" - fi - done - done <<< "$MIRROR_SPECS" - LC_ALL=C sort -u "$TMP/parse-files" -o "$TMP/parse-files" -fi -: > "$TMP/parsed-files" - -while [[ -s "$TMP/parse-files" ]]; do - cp "$TMP/parse-files" "$TMP/pass-files" - : > "$TMP/parse-files" - while IFS= read -r source; do - add_file "$source" - [[ -f "$source" && -r "$source" ]] || continue - skill_root=$(owning_skill "$source") || skill_root="" - while IFS=$'\t' read -r kind origin raw; do - ref=$(clean_reference "$raw") || continue - source_rel=$(relpath "$source") - target="" - skip_broken=0 - broken_ref=$ref - if [[ "$origin" == "import" ]]; then - target=$(resolve_source_relative "$source" "$ref") || true - case "$ref" in - /*) broken_ref=$ref ;; - *) broken_ref=$(relpath "$(dirname "$source")/$ref") ;; - esac - if [[ -n "$target" ]] && ! inside_root "$target"; then target=""; skip_broken=1; fi - elif [[ -n "$skill_root" && "$ref" != /* ]]; then - target=$(resolve_source_relative "$skill_root/SKILL.md" "$ref") || true - broken_ref=$(relpath "$skill_root/$ref") - if [[ -n "$target" ]] && ! inside_root "$target"; then target=""; fi - else - case "$ref" in - /*) - if [[ "$kind" == "explicit" ]]; then printf '%s\t%s\n' "$source_rel" "$ref" >> "$NONROOT"; fi - target=$(canonical_existing "$ref") || true - ;; - *) target=$(resolve_reference "$ref") || true ;; - esac - if [[ -n "$target" ]] && ! inside_root "$target"; then target=""; fi - if [[ -z "$target" ]] && relative_target=$(resolve_source_relative "$source" "$ref"); then - if inside_root "$relative_target"; then - if [[ "$kind" == "explicit" ]]; then printf '%s\t%s\n' "$source_rel" "$ref" >> "$NONROOT"; fi - target=$relative_target - fi - fi - fi - if [[ -n "$target" ]]; then - target_rel=$(relpath "$target") - printf '%s\t%s\n' "$source_rel" "$target_rel" >> "$EDGES" - inventory_file "$target" - if [[ "$kind" == "explicit" ]]; then - add_file "$target" - printf '%s\t%s\n' "$source_rel" "$target_rel" >> "$EXPLICIT_EDGES" - case "$target" in *.md) printf '%s\n' "$target" >> "$TMP/parse-files" ;; esac - fi - elif [[ $skip_broken -eq 0 ]]; then - if [[ "$kind" == "explicit" ]] || is_route_reference "$ref"; then - printf '%s\t%s\n' "$source_rel" "$broken_ref" >> "$BROKEN" - fi - fi - done < <(extract_references "$source") - done < "$TMP/pass-files" - cat "$TMP/pass-files" >> "$TMP/parsed-files" - LC_ALL=C sort -u "$TMP/parsed-files" -o "$TMP/parsed-files" - LC_ALL=C sort -u "$TMP/parse-files" -o "$TMP/parse-files" - LC_ALL=C comm -23 "$TMP/parse-files" "$TMP/parsed-files" > "$TMP/pending-files" - mv "$TMP/pending-files" "$TMP/parse-files" -done -for records in "$FILES" "$EDGES" "$EXPLICIT_EDGES" "$BROKEN" "$NONROOT" "$UNREADABLE"; do - LC_ALL=C sort -u "$records" -o "$records" -done - -while IFS= read -r file; do relpath "$file"; echo; done < "$FILES" > "$TMP/relative-files" -while IFS= read -r file; do relpath "$file"; echo; done < "$SEEDS" > "$TMP/relative-seeds" -LC_ALL=C sort -u "$TMP/relative-files" -o "$TMP/relative-files" -LC_ALL=C sort -u "$TMP/relative-seeds" -o "$TMP/relative-seeds" - -reachable_for_runtime() { - local runtime=$1 seeds=$2 unreachable - unreachable="$TMP/unreachable-$runtime" - awk -F '\t' ' - FILENAME==ARGV[1] {seed[$0]=1; reach[$0]=1; node[$0]=1; next} - FILENAME==ARGV[2] {node[$0]=1; next} - {edge[$1 SUBSEP $2]=1; node[$1]=1; node[$2]=1} - END { - changed=1 - while (changed) { - changed=0 - for (key in edge) { - split(key, pair, SUBSEP) - if (reach[pair[1]] && !reach[pair[2]]) {reach[pair[2]]=1; changed=1} - } - } - for (n in node) if (!reach[n]) print n - } -' "$seeds" "$TMP/relative-files" "$EXPLICIT_EDGES" | LC_ALL=C sort > "$unreachable" - - while IFS= read -r rel; do - case "$runtime:$rel" in - codex:.agent-shared/rules/*|codex:.agents/rules/*|codex:.codex/rules/*|codex:.pi/rules/*|codex:*/AGENTS.md|codex:*/AGENTS.override.md) - printf '%s\t%s\n' "$runtime" "$rel" >> "$UNROUTED" - ;; - claude:.agent-shared/rules/*) - printf '%s\t%s\n' "$runtime" "$rel" >> "$UNROUTED" - ;; - esac - done < "$unreachable" -} - -while IFS= read -r file; do relpath "$file"; echo; done < "$CODEX_SEEDS" | LC_ALL=C sort -u > "$TMP/relative-codex-seeds" -while IFS= read -r file; do relpath "$file"; echo; done < "$CLAUDE_SEEDS" | LC_ALL=C sort -u > "$TMP/relative-claude-seeds" -case "$RUNTIME" in - codex) reachable_for_runtime codex "$TMP/relative-codex-seeds" ;; - claude) reachable_for_runtime claude "$TMP/relative-claude-seeds" ;; - both) - reachable_for_runtime codex "$TMP/relative-codex-seeds" - reachable_for_runtime claude "$TMP/relative-claude-seeds" - ;; -esac -LC_ALL=C sort -u "$UNROUTED" -o "$UNROUTED" - -awk -F '\t' ' - function dfs(n, key,pair,t) { - state[n]=1 - for (key in edge) { - split(key, pair, SUBSEP) - if (pair[1] != n) continue - t=pair[2] - if (state[t] == 1) print n " -> " t - else if (!state[t]) dfs(t) - } - state[n]=2 - } - {edge[$1 SUBSEP $2]=1; node[$1]=1; node[$2]=1} - END {for (n in node) if (!state[n]) dfs(n)} -' "$EXPLICIT_EDGES" | LC_ALL=C sort -u > "$CYCLES" - -while IFS= read -r file; do - [[ -f "$file" && -r "$file" ]] || continue - read -r checksum size _ < <(cksum "$file") - printf '%s:%s\t%s\n' "$checksum" "$size" "$file" -done < "$FILES" | - LC_ALL=C sort | - awk -F '\t' ' - $1==last {if (!shown) {group=first; shown=1}; group=group " = " $2; next} - last!="" && shown {print group} - {last=$1; first=$2; shown=0; group=""} - END {if (shown) print group} - ' > "$DUPLICATES" - -BUDGET=32768 -if [[ "$RUNTIME" != "claude" && ( -e "$ROOT/.codex/config.toml" || -L "$ROOT/.codex/config.toml" ) ]]; then - if [[ ! -f "$ROOT/.codex/config.toml" || ! -r "$ROOT/.codex/config.toml" ]]; then - printf '%s\n' "$ROOT/.codex/config.toml" >> "$UNREADABLE" - else - add_file "$ROOT/.codex/config.toml" - fi -fi -if [[ "$RUNTIME" != "claude" && -f "$ROOT/.codex/config.toml" && -r "$ROOT/.codex/config.toml" ]]; then - configured=$(awk -F= '/^[[:space:]]*project_doc_max_bytes[[:space:]]*=/ {value=$2; sub(/[[:space:]]*#.*/, "", value); gsub(/[[:space:]]/, "", value); if (value ~ /^[0-9]+$/) {print value; exit}}' "$ROOT/.codex/config.toml") - [[ -n "$configured" ]] && BUDGET=$configured -fi - -CODEX_ROOT_BYTES=0 -CLAUDE_ROOT_BYTES=0 -BUDGET_FINDINGS="$TMP/budget" -ALWAYS_FINDINGS="$TMP/always" -DRIFT_FINDINGS="$TMP/drift" -: > "$BUDGET_FINDINGS"; : > "$ALWAYS_FINDINGS"; : > "$DRIFT_FINDINGS" -MIRROR_RECORDS="$TMP/mirror-records" -: > "$MIRROR_RECORDS" -while IFS= read -r file; do - [[ -f "$file" && -r "$file" ]] || continue - rel=$(relpath "$file") - bytes=$(wc -c < "$file" | tr -d ' ') - case "$rel" in - AGENTS.md|AGENTS.override.md) - if grep -Fqx "$file" "$CODEX_SEEDS"; then CODEX_ROOT_BYTES=$((CODEX_ROOT_BYTES + bytes)); fi - if [[ "$RUNTIME" != "claude" ]] && grep -Fqx "$file" "$CODEX_SEEDS" && [[ $bytes -gt $BUDGET ]]; then printf '%s\t%s\t%s\n' "$file" "$bytes" "$BUDGET" >> "$BUDGET_FINDINGS"; fi - ;; - CLAUDE.md|.claude/CLAUDE.md|CLAUDE.local.md) - if grep -Fqx "$file" "$CLAUDE_SEEDS"; then - CLAUDE_ROOT_BYTES=$((CLAUDE_ROOT_BYTES + bytes)) - fi - ;; - esac - case "$rel" in - .claude/rules/*) - if ! grep -Eq '^[[:space:]]*paths[[:space:]]*:' "$file" || grep -Eq 'paths:[[:space:]]*\[[^]]*"\*\*"|^[[:space:]]*-[[:space:]]*"\*\*"' "$file"; then - printf '%s\n' "$file" >> "$ALWAYS_FINDINGS" - fi - ;; - esac -done < "$FILES" - -check_declared_mirror() { - local spec=$1 source target source_abs target_abs status=aligned rel index - validate_mirror_spec "$spec" - source=${spec%%=*} - target=${spec#*=} - source_abs="$ROOT/$source" - target_abs="$ROOT/$target" - if [[ ! -d "$source_abs" || ! -r "$source_abs" || ! -x "$source_abs" || ! -d "$target_abs" || ! -r "$target_abs" || ! -x "$target_abs" ]]; then - printf '%s -> %s | declared mirror source or target is unreadable, unsearchable, or missing\n' "$source_abs" "$target_abs" >> "$DRIFT_FINDINGS" - printf '%s\t%s\tdrift\n' "$source_abs" "$target_abs" >> "$MIRROR_RECORDS" - return - fi - index=$(wc -l < "$MIRROR_RECORDS" | tr -d ' ') - if ! (cd "$source_abs" && find . ! -type d ! -name .DS_Store -print | LC_ALL=C sort) > "$TMP/mirror-source-$index" 2> "$TMP/mirror-source-error-$index"; then - printf '%s -> %s | source traversal failed: %s\n' "$source_abs" "$target_abs" "$(sed -n '1p' "$TMP/mirror-source-error-$index")" >> "$DRIFT_FINDINGS" - printf '%s\t%s\tdrift\n' "$source_abs" "$target_abs" >> "$MIRROR_RECORDS" - return - fi - if ! (cd "$target_abs" && find . ! -type d ! -name .DS_Store -print | LC_ALL=C sort) > "$TMP/mirror-target-$index" 2> "$TMP/mirror-target-error-$index"; then - printf '%s -> %s | target traversal failed: %s\n' "$source_abs" "$target_abs" "$(sed -n '1p' "$TMP/mirror-target-error-$index")" >> "$DRIFT_FINDINGS" - printf '%s\t%s\tdrift\n' "$source_abs" "$target_abs" >> "$MIRROR_RECORDS" - return - fi - if ! cmp -s "$TMP/mirror-source-$index" "$TMP/mirror-target-$index"; then - status=drift - printf '%s -> %s | file sets differ\n' "$source_abs" "$target_abs" >> "$DRIFT_FINDINGS" - else - while IFS= read -r rel; do - if ! cmp -s "$source_abs/$rel" "$target_abs/$rel"; then - status=drift - printf '%s -> %s | content differs: %s\n' "$source_abs" "$target_abs" "${rel#./}" >> "$DRIFT_FINDINGS" - fi - done < "$TMP/mirror-source-$index" - fi - printf '%s\t%s\t%s\n' "$source_abs" "$target_abs" "$status" >> "$MIRROR_RECORDS" - if [[ "$status" == "aligned" ]]; then - SOURCE_PREFIX="$source_abs/" TARGET_PREFIX="$target_abs/" awk ' - index($0, ENVIRON["SOURCE_PREFIX"]) && index($0, ENVIRON["TARGET_PREFIX"]) {next} - {print} - ' "$DUPLICATES" > "$TMP/duplicates-filtered" - mv "$TMP/duplicates-filtered" "$DUPLICATES" - fi -} - -if [[ -n "$MIRROR_SPECS" ]]; then - while IFS= read -r spec; do [[ -n "$spec" ]] && check_declared_mirror "$spec"; done <<< "$MIRROR_SPECS" -fi -if [[ "$ROOT_ADAPTER_STATUS" == "symlink" ]]; then - LEFT="$ROOT/AGENTS.md" RIGHT="$ROOT/CLAUDE.md" awk '$0 != ENVIRON["LEFT"] " = " ENVIRON["RIGHT"] && $0 != ENVIRON["RIGHT"] " = " ENVIRON["LEFT"]' "$DUPLICATES" > "$TMP/duplicates-filtered" - mv "$TMP/duplicates-filtered" "$DUPLICATES" -fi - -FILE_COUNT=$(wc -l < "$FILES" | tr -d ' ') -ENTRYPOINT_COUNT=$(wc -l < "$ENTRYPOINTS" | tr -d ' ') -EDGE_COUNT=$(wc -l < "$EXPLICIT_EDGES" | tr -d ' ') -BROKEN_COUNT=$(wc -l < "$BROKEN" | tr -d ' ') -NONROOT_COUNT=$(wc -l < "$NONROOT" | tr -d ' ') -MISSING_RUNTIME_COUNT=$(wc -l < "$MISSING_RUNTIME" | tr -d ' ') -CYCLE_COUNT=$(wc -l < "$CYCLES" | tr -d ' ') -UNROUTED_COUNT=$(wc -l < "$UNROUTED" | tr -d ' ') -DUPLICATE_COUNT=$(wc -l < "$DUPLICATES" | tr -d ' ') -UNREADABLE_COUNT=$(wc -l < "$UNREADABLE" | tr -d ' ') -DRIFT_COUNT=$(wc -l < "$DRIFT_FINDINGS" | tr -d ' ') -BUDGET_COUNT=$(wc -l < "$BUDGET_FINDINGS" | tr -d ' ') - -echo "root: $ROOT" -printf 'summary: runtime=%s entrypoints=%s files=%s routes=%s broken=%s nonroot_paths=%s cycles=%s unrouted=%s missing_runtime=%s duplicate_files=%s unreadable=%s adapter_drift=%s codex_root_bytes=%s codex_root_approx_tokens=%s claude_root_bytes=%s claude_root_approx_tokens=%s\n' \ - "$RUNTIME" "$ENTRYPOINT_COUNT" "$FILE_COUNT" "$EDGE_COUNT" "$BROKEN_COUNT" "$NONROOT_COUNT" "$CYCLE_COUNT" "$UNROUTED_COUNT" "$MISSING_RUNTIME_COUNT" "$DUPLICATE_COUNT" "$UNREADABLE_COUNT" "$DRIFT_COUNT" "$CODEX_ROOT_BYTES" "$(( (CODEX_ROOT_BYTES + 3) / 4 ))" "$CLAUDE_ROOT_BYTES" "$(( (CLAUDE_ROOT_BYTES + 3) / 4 ))" -echo "entrypoints:" -entrypoint_number=0 -while IFS= read -r file; do - entrypoint_number=$((entrypoint_number + 1)) - if [[ $entrypoint_number -le 40 ]]; then print_file_record "$file"; fi -done < "$ENTRYPOINTS" -if [[ $entrypoint_number -gt 40 ]]; then printf ' - ... %s additional entrypoints omitted\n' "$((entrypoint_number - 40))"; fi -echo "adapter_mirrors:" -if [[ ! -s "$MIRROR_RECORDS" && "$ROOT_ADAPTER_STATUS" == "none" ]]; then - echo " - none detected" -fi -if [[ "$ROOT_ADAPTER_STATUS" != "none" ]]; then - printf ' - %s -> %s | status=%s\n' "$ROOT_ADAPTER_SOURCE" "$ROOT_ADAPTER_TARGET" "$ROOT_ADAPTER_STATUS" -fi -while IFS=$'\t' read -r source target status; do printf ' - %s -> %s | status=%s | declared=true\n' "$source" "$target" "$status"; done < "$MIRROR_RECORDS" - -echo "findings:" -if [[ $MISSING_RUNTIME_COUNT -eq 0 && $BROKEN_COUNT -eq 0 && $NONROOT_COUNT -eq 0 && $CYCLE_COUNT -eq 0 && $UNROUTED_COUNT -eq 0 && $DUPLICATE_COUNT -eq 0 && $UNREADABLE_COUNT -eq 0 && $DRIFT_COUNT -eq 0 && $BUDGET_COUNT -eq 0 && ! -s "$ALWAYS_FINDINGS" ]]; then - echo " - none" -fi -while IFS= read -r runtime; do printf ' - critical missing-entrypoint | runtime=%s\n' "$runtime"; done < "$MISSING_RUNTIME" -broken_number=0 -while IFS=$'\t' read -r source target; do - broken_number=$((broken_number + 1)) - if [[ $broken_number -le 40 ]]; then printf ' - critical broken-reference | %s -> %s\n' "$(abspath "$source")" "$(abspath "$target")"; fi -done < "$BROKEN" -if [[ $broken_number -gt 40 ]]; then printf ' - critical broken-reference | ... %s additional findings omitted\n' "$((broken_number - 40))"; fi -nonroot_number=0 -while IFS=$'\t' read -r source target; do - nonroot_number=$((nonroot_number + 1)) - if [[ $nonroot_number -le 40 ]]; then printf ' - critical nonroot-path | %s -> %s | expected session-root-relative path\n' "$(abspath "$source")" "$(abspath "$target")"; fi -done < "$NONROOT" -if [[ $nonroot_number -gt 40 ]]; then printf ' - critical nonroot-path | ... %s additional findings omitted\n' "$((nonroot_number - 40))"; fi -while IFS= read -r cycle; do - source=${cycle%% -> *} - target=${cycle#* -> } - printf ' - critical instruction-cycle | %s -> %s\n' "$(abspath "$source")" "$(abspath "$target")" -done < "$CYCLES" -unrouted_number=0 -while IFS=$'\t' read -r runtime path; do - unrouted_number=$((unrouted_number + 1)) - if [[ $unrouted_number -le 40 ]]; then printf ' - critical unrouted-guidance | runtime=%s | %s\n' "$runtime" "$(abspath "$path")"; fi -done < "$UNROUTED" -if [[ $unrouted_number -gt 40 ]]; then printf ' - critical unrouted-guidance | ... %s additional findings omitted\n' "$((unrouted_number - 40))"; fi -print_line_findings "critical unreadable-guidance" "$UNREADABLE" -while IFS=$'\t' read -r file bytes limit; do printf ' - critical root-budget | %s bytes=%s limit=%s\n' "$file" "$bytes" "$limit"; done < "$BUDGET_FINDINGS" -print_line_findings "critical adapter-drift" "$DRIFT_FINDINGS" -print_line_findings "advisory always-loaded-rule" "$ALWAYS_FINDINGS" -print_line_findings "advisory duplicate-content" "$DUPLICATES" - -echo "routes:" -if [[ $EDGE_COUNT -eq 0 ]]; then echo " - none"; fi -route_number=0 -while IFS=$'\t' read -r source target; do - route_number=$((route_number + 1)) - if [[ $route_number -le 120 ]]; then printf ' - %s -> %s\n' "$(abspath "$source")" "$(abspath "$target")"; fi -done < "$EXPLICIT_EDGES" -if [[ $route_number -gt 120 ]]; then printf ' - ... %s additional routes omitted\n' "$((route_number - 120))"; fi - -if [[ "$MODE" == "verify" && $(( MISSING_RUNTIME_COUNT + BROKEN_COUNT + NONROOT_COUNT + CYCLE_COUNT + UNROUTED_COUNT + UNREADABLE_COUNT + DRIFT_COUNT + BUDGET_COUNT )) -ne 0 ]]; then - exit 1 -fi diff --git a/plugins/foundation/darrow-information-architecture/references/file-updates.md b/plugins/foundation/darrow-information-architecture/references/file-updates.md new file mode 100644 index 00000000..c403c135 --- /dev/null +++ b/plugins/foundation/darrow-information-architecture/references/file-updates.md @@ -0,0 +1,28 @@ +# Approved guidance updates + +Use the writer only after the skill's proposal and authority checks. It receives +complete approved UTF-8 content; it makes no placement, policy, or edit decision. + +Read the current source and record its SHA-256 (`missing` for a new file). Stage +the complete replacement in a temporary content file outside the guidance graph. +Create an approved parent directory first if needed. Then invoke: + +```text +uv run --quiet --frozen --no-dev --project "/../../backend" ia-write --expected --content-file [repository] +``` + +The writer resolves an existing file adapter to its in-repository source, +preserves its permissions, UTF-8 BOM, and LF/CRLF convention, and stages complete +bytes in the destination directory before `os.replace`. It checks the expected +digest and adapter destination again immediately before publication. A refused +or failed replacement leaves the original intact and removes the temporary file. +The caller must coordinate concurrent writers: the digest check detects observed +changes but is not an operating-system compare-and-swap or a multi-file +transaction. Re-read and re-evaluate the proposal after a conflict. + +On native Windows, a locked destination can refuse replacement; report that +failure rather than falling back to truncation. Symlink creation may require +host privileges. Preserve existing adapters; for an approved new graph where +symlinks are unavailable, use a native import or explicit route. The writer does +not create, replace, or delete symlinks. Apply approved moves/deletions separately, +then verify the entire resulting graph using `ia-doctor verify`. diff --git a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/SKILL.md b/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/SKILL.md index 95c3d403..41cab838 100644 --- a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/SKILL.md +++ b/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/SKILL.md @@ -8,9 +8,10 @@ description: Audit repository agent guidance and repair diagnosed problems acros Return a smaller, correctly routed instruction graph without deleting behavioral knowledge or turning repository patterns into policy. Begin read-only. -Run every structural operation through `/scripts/ia-doctor.sh`, where -`` contains this file. Execute it with Bash. Its findings establish -structure, not edit decisions. +Run structural operations through the frozen UV package at +`/../../backend`, where `` contains this file. Requires +UV and Python 3.10–3.13 on Linux, macOS, or native Windows. Its findings +establish structure, not edit decisions. ## Working model @@ -33,7 +34,7 @@ structure, not edit decisions. Use both runtimes unless the user explicitly selected one: ```sh -bash /scripts/ia-doctor.sh inspect --runtime both [repository] +uv run --quiet --frozen --no-dev --project "/../../backend" ia-doctor inspect --runtime both [repository] ``` Pass `--mirror source=target` only for adapter relationships declared by @@ -94,6 +95,10 @@ Apply runtime semantics independently: version-control-eligible `SKILL.md` with valid discovery metadata and runtime reachability. Preserve an explicit intent route when native discovery is not sufficient for every selected runtime. + Such a route names the exact session-root-relative `SKILL.md` path and says + when to read it. A skill-name mention alone is insufficient. Confirm that + the route appears in the inspector's `routes` records; a passing structural + status alone does not prove that a newly moved procedure is reachable. For every rule that chooses among live implementation patterns, identify the arbiter. Counts, recency, directory names, apparent completeness, and your @@ -133,6 +138,10 @@ response asks one compact grouped question. ### 4. Apply and verify +For approved guidance-file replacements, read +[`../../references/file-updates.md`](../../references/file-updates.md) and use +the bundled atomic writer. Keep existing adapter direction and newline style. + Apply only confirmed actions. Preserve each settled decision, behavioral constraint, gate, scope, and route. Use the repository's declared sync mechanism after editing a source of truth. Do not commit or push. @@ -140,7 +149,7 @@ mechanism after editing a source of truth. Do not commit or push. Verify the same runtimes and declared mirrors used for inspection: ```sh -bash /scripts/ia-doctor.sh verify --runtime both \ +uv run --quiet --frozen --no-dev --project "/../../backend" ia-doctor verify --runtime both \ [--mirror source=target]... [repository] ``` diff --git a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/claude-only-scope.yaml b/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/claude-only-scope.yaml index 467bb20a..d9529766 100644 --- a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/claude-only-scope.yaml +++ b/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/claude-only-scope.yaml @@ -18,7 +18,7 @@ fixture: Preserve keyboard navigation. src/web/app.ts: "export const app = true;\n" - setup: cp "{{case_dir}}/../../../bin/ia-doctor" .git/ia-doctor + setup: mkdir -p .git/ia-backend; cp -R "{{case_dir}}/../../../backend/src" .git/ia-backend/; cp "{{case_dir}}/../../../backend/pyproject.toml" "{{case_dir}}/../../../backend/uv.lock" .git/ia-backend/ checks: - name: Claude guidance remains intact run: cat .claude/rules/web.md @@ -30,7 +30,7 @@ checks: expect_regex: "^absent$" - name: Claude-only verification passes run: >- - bash .git/ia-doctor verify --runtime claude . >/dev/null && echo verified + uv run --quiet --frozen --no-dev --project .git/ia-backend ia-doctor verify --runtime claude . >/dev/null && echo verified expect_regex: "^verified$" - name: doctor creates no commit run: git rev-list --all --reflog --count diff --git a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/ignore-unselected-guidance.yaml b/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/ignore-unselected-guidance.yaml index f61c484a..57358001 100644 --- a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/ignore-unselected-guidance.yaml +++ b/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/ignore-unselected-guidance.yaml @@ -17,7 +17,7 @@ fixture: --- Keep API errors typed. - setup: cp "{{case_dir}}/../../../bin/ia-doctor" .git/ia-doctor + setup: mkdir -p .git/ia-backend; cp -R "{{case_dir}}/../../../backend/src" .git/ia-backend/; cp "{{case_dir}}/../../../backend/pyproject.toml" "{{case_dir}}/../../../backend/uv.lock" .git/ia-backend/ checks: - name: unrelated Codex guidance is unchanged run: git diff HEAD -- AGENTS.md @@ -26,7 +26,7 @@ checks: run: test ! -e docs/codex-policy.md && echo absent expect_regex: "^absent$" - name: selected runtime verifies - run: bash .git/ia-doctor verify --runtime claude . >/dev/null && echo verified + run: uv run --quiet --frozen --no-dev --project .git/ia-backend ia-doctor verify --runtime claude . >/dev/null && echo verified expect_regex: "^verified$" - name: doctor creates no commit run: git rev-list --all --reflog --count @@ -34,6 +34,8 @@ checks: semantic_output_checks: - name: unrelated Codex routes do not block Claude verification proposition: >- - The response reports successful Claude structural verification. It may - mention the Codex file as inventory, but does not require fixing its - missing policy before declaring the selected Claude graph verified. + In the context of the requested Claude-only audit, the response reports + a successful or issue-free structural outcome. Wording such as "no + structural issues found" is sufficient; the literal word "verified" is + not required. It may mention Codex inventory, but does not require fixing + its missing policy as a condition of the Claude outcome. diff --git a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/move-procedure-to-skill.yaml b/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/move-procedure-to-skill.yaml index 8c03c727..85d221c7 100644 --- a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/move-procedure-to-skill.yaml +++ b/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/move-procedure-to-skill.yaml @@ -27,7 +27,7 @@ fixture: tools/build-release: "#!/bin/sh\nexit 0\n" tools/publish: "#!/bin/sh\nexit 0\n" dist/checksums.txt: "fixture\n" - setup: cp "{{case_dir}}/../../../bin/ia-doctor" .git/ia-doctor + setup: mkdir -p .git/ia-backend; cp -R "{{case_dir}}/../../../backend/src" .git/ia-backend/; cp "{{case_dir}}/../../../backend/pyproject.toml" "{{case_dir}}/../../../backend/uv.lock" .git/ia-backend/ checks: - name: release procedure becomes an intent-triggered project skill run: >- @@ -79,7 +79,7 @@ checks: | sed -n '1p'); target=${skill#./}; test -n "$target"; skill_abs=$(cd "$(dirname "$target")" && pwd -P)/$(basename "$target"); - root=$(pwd -P); routes=$(bash .git/ia-doctor inspect --runtime codex .); + root=$(pwd -P); routes=$(uv run --quiet --frozen --no-dev --project .git/ia-backend ia-doctor inspect --runtime codex .); graph_route=false; case "$routes" in *"$root/AGENTS.md -> $skill_abs"*) graph_route=true;; esac; intent_route=false; @@ -97,11 +97,12 @@ checks: flat=$(tr '\n' ' ' < "$native"); if printf '%s\n' "$flat" | grep -Ei 'preflight.*package\.json.*build-release.*dist/checksums\.txt.*tools/publish.*explicit confirmation' >/dev/null; then return 0; fi; done; return 1; }; - valid_native "$codex" && valid_native "$claude" && echo discoverable + if valid_native "$codex" && valid_native "$claude"; then echo discoverable; + else printf '%s\n' "$routes" >&2; cat AGENTS.md >&2; exit 1; fi expect_regex: "^discoverable$" - name: procedure move passes deterministic dual-runtime verification run: >- - bash .git/ia-doctor verify --runtime both . >/dev/null && echo verified + uv run --quiet --frozen --no-dev --project .git/ia-backend ia-doctor verify --runtime both . >/dev/null && echo verified expect_regex: "^verified$" - name: doctor creates no commit run: git rev-list --all --reflog --count diff --git a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/preserve-native-resource-paths.yaml b/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/preserve-native-resource-paths.yaml index 8220ca8a..ed8002ae 100644 --- a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/preserve-native-resource-paths.yaml +++ b/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/preserve-native-resource-paths.yaml @@ -27,13 +27,13 @@ fixture: # API checks Preserve error codes in compatibility checks. - setup: cp "{{case_dir}}/../../../bin/ia-doctor" .git/ia-doctor + setup: mkdir -p .git/ia-backend; cp -R "{{case_dir}}/../../../backend/src" .git/ia-backend/; cp "{{case_dir}}/../../../backend/pyproject.toml" "{{case_dir}}/../../../backend/uv.lock" .git/ia-backend/ checks: - name: valid root routes, native adapter, and skill bundle remain unchanged run: git diff HEAD -- AGENTS.md CLAUDE.md .agents/skills/api-check not_regex: "\\S" - name: native adapter and bundled resource verification passes - run: bash .git/ia-doctor verify --runtime both . >/dev/null && echo verified + run: uv run --quiet --frozen --no-dev --project .git/ia-backend ia-doctor verify --runtime both . >/dev/null && echo verified expect_regex: "^verified$" - name: doctor creates no commit run: git rev-list --all --reflog --count diff --git a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/preserve-rare-safety-route.yaml b/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/preserve-rare-safety-route.yaml index b4e66cc1..faa088b0 100644 --- a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/preserve-rare-safety-route.yaml +++ b/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/preserve-rare-safety-route.yaml @@ -23,7 +23,7 @@ fixture: Never mutate production while diagnosing. Collect evidence first and require explicit operator confirmation before any remediation. package.json: "{\"private\":true}\n" - setup: cp "{{case_dir}}/../../../bin/ia-doctor" .git/ia-doctor + setup: mkdir -p .git/ia-backend; cp -R "{{case_dir}}/../../../backend/src" .git/ia-backend/; cp "{{case_dir}}/../../../backend/pyproject.toml" "{{case_dir}}/../../../backend/uv.lock" .git/ia-backend/ checks: - name: rare safety guidance is not deleted for lack of usage run: test -f .agent-shared/rules/production-incident.md && echo kept @@ -34,7 +34,7 @@ checks: flags: i - name: rare route passes deterministic dual-runtime verification run: >- - bash .git/ia-doctor verify --runtime both . >/dev/null && echo verified + uv run --quiet --frozen --no-dev --project .git/ia-backend ia-doctor verify --runtime both . >/dev/null && echo verified expect_regex: "^verified$" - name: doctor creates no commit run: git rev-list --all --reflog --count diff --git a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/preserve-runtime-specific-guidance.yaml b/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/preserve-runtime-specific-guidance.yaml index 0b1f1fb6..80621838 100644 --- a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/preserve-runtime-specific-guidance.yaml +++ b/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/preserve-runtime-specific-guidance.yaml @@ -26,7 +26,7 @@ fixture: # Web Preserve keyboard navigation. - setup: cp "{{case_dir}}/../../../bin/ia-doctor" .git/ia-doctor + setup: mkdir -p .git/ia-backend; cp -R "{{case_dir}}/../../../backend/src" .git/ia-backend/; cp "{{case_dir}}/../../../backend/pyproject.toml" "{{case_dir}}/../../../backend/uv.lock" .git/ia-backend/ checks: - name: shared API rule remains run: cat .agent-shared/rules/api.md @@ -44,7 +44,7 @@ checks: expect_regex: "^unchanged$" - name: deterministic dual-runtime verification passes run: >- - bash .git/ia-doctor verify --runtime both . >/dev/null && echo verified + uv run --quiet --frozen --no-dev --project .git/ia-backend ia-doctor verify --runtime both . >/dev/null && echo verified expect_regex: "^verified$" - name: doctor creates no commit run: git rev-list --all --reflog --count diff --git a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/preserve-settled-abandoned-experiment.yaml b/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/preserve-settled-abandoned-experiment.yaml index 13804ffd..4edc20c8 100644 --- a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/preserve-settled-abandoned-experiment.yaml +++ b/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/preserve-settled-abandoned-experiment.yaml @@ -23,7 +23,7 @@ fixture: src/persistence/sql/a.go: "package sql\n" src/persistence/sql/b.go: "package sql\n" src/persistence/sql/c.go: "package sql\n" - setup: cp "{{case_dir}}/../../../bin/ia-doctor" .git/ia-doctor + setup: mkdir -p .git/ia-backend; cp -R "{{case_dir}}/../../../backend/src" .git/ia-backend/; cp "{{case_dir}}/../../../backend/pyproject.toml" "{{case_dir}}/../../../backend/uv.lock" .git/ia-backend/ checks: - name: settled ORM direction survives despite pattern frequency run: cat .agent-shared/rules/storage.md @@ -34,7 +34,7 @@ checks: flags: i - name: settled deferred decision remains reachable in both runtimes run: >- - bash .git/ia-doctor verify --runtime both . >/dev/null && echo verified + uv run --quiet --frozen --no-dev --project .git/ia-backend ia-doctor verify --runtime both . >/dev/null && echo verified expect_regex: "^verified$" - name: doctor creates no commit run: git rev-list --all --reflog --count diff --git a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/restore-cross-runtime-reachability.yaml b/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/restore-cross-runtime-reachability.yaml index da416f8a..de248aab 100644 --- a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/restore-cross-runtime-reachability.yaml +++ b/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/restore-cross-runtime-reachability.yaml @@ -20,7 +20,7 @@ fixture: # Security policy Scope: all automated coding runtimes. - setup: cp "{{case_dir}}/../../../bin/ia-doctor" .git/ia-doctor + setup: mkdir -p .git/ia-backend; cp -R "{{case_dir}}/../../../backend/src" .git/ia-backend/; cp "{{case_dir}}/../../../backend/pyproject.toml" "{{case_dir}}/../../../backend/uv.lock" .git/ia-backend/ checks: - name: cross-runtime security rule remains intact run: find . -type f -name '*.md' -not -path './.git/*' -exec cat {} + @@ -34,12 +34,12 @@ checks: -exec grep -il 'never copy production secrets into local fixtures' {} + | sed -n '1p'); target=${rule#./}; test -n "$target"; root=$(pwd -P); policy=$(cd "$(dirname "$target")" && pwd -P)/$(basename "$target"); - bash .git/ia-doctor inspect --runtime codex . | + uv run --quiet --frozen --no-dev --project .git/ia-backend ia-doctor inspect --runtime codex . | grep -F "$root/AGENTS.md -> $policy" >/dev/null && echo reachable; fi expect_regex: "^reachable$" - name: deterministic dual-runtime verification passes run: >- - bash .git/ia-doctor verify --runtime both . >/dev/null && echo verified + uv run --quiet --frozen --no-dev --project .git/ia-backend ia-doctor verify --runtime both . >/dev/null && echo verified expect_regex: "^verified$" - name: doctor creates no commit run: git rev-list --all --reflog --count diff --git a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/trim-derived-keep-contracts.yaml b/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/trim-derived-keep-contracts.yaml index 3c6637f1..f3cf5fbc 100644 --- a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/trim-derived-keep-contracts.yaml +++ b/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/evals/trim-derived-keep-contracts.yaml @@ -34,7 +34,7 @@ fixture: .agent-shared/rules/web.md: "# Web\n\nPreserve keyboard navigation.\n" package.json: "{\"private\":true,\"packageManager\":\"bun@1.3.0\",\"scripts\":{\"test\":\"vitest\",\"lint\":\"biome check .\"}}\n" tools/guard: "#!/bin/sh\nexit 0\n" - setup: cp "{{case_dir}}/../../../bin/ia-doctor" .git/ia-doctor + setup: mkdir -p .git/ia-backend; cp -R "{{case_dir}}/../../../backend/src" .git/ia-backend/; cp "{{case_dir}}/../../../backend/pyproject.toml" "{{case_dir}}/../../../backend/uv.lock" .git/ia-backend/ checks: - name: self-merge prohibition remains run: cat AGENTS.md @@ -59,7 +59,7 @@ checks: flags: i - name: deterministic dual-runtime routing verification passes run: >- - bash .git/ia-doctor verify --runtime both . >/dev/null && echo verified + uv run --quiet --frozen --no-dev --project .git/ia-backend ia-doctor verify --runtime both . >/dev/null && echo verified expect_regex: "^verified$" - name: response reports exact per-runtime before and after bytes and tokens run: >- @@ -67,7 +67,7 @@ checks: before_codex_tokens=$(( (before_codex + 3) / 4 )); before_claude=$(git show HEAD:CLAUDE.md | wc -c | tr -d ' '); before_claude_tokens=$(( (before_claude + 3) / 4 )); - summary=$(bash .git/ia-doctor inspect --runtime both . | sed -n '/^summary:/p'); + summary=$(uv run --quiet --frozen --no-dev --project .git/ia-backend ia-doctor inspect --runtime both . | sed -n '/^summary:/p'); after_codex=$(printf '%s\n' "$summary" | sed -n 's/.*codex_root_bytes=\([0-9][0-9]*\).*/\1/p'); after_codex_tokens=$(printf '%s\n' "$summary" | sed -n 's/.*codex_root_approx_tokens=\([0-9][0-9]*\).*/\1/p'); after_claude=$(printf '%s\n' "$summary" | sed -n 's/.*claude_root_bytes=\([0-9][0-9]*\).*/\1/p'); diff --git a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/scripts/ia-doctor.sh b/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/scripts/ia-doctor.sh deleted file mode 100755 index 86d14fcd..00000000 --- a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/scripts/ia-doctor.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) -exec bash "$SCRIPT_DIR/../../../bin/ia-doctor" "$@" diff --git a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/scripts/ia-doctor.test.sh b/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/scripts/ia-doctor.test.sh deleted file mode 100755 index 2da06100..00000000 --- a/plugins/foundation/darrow-information-architecture/skills/doctor-information-architecture/scripts/ia-doctor.test.sh +++ /dev/null @@ -1,418 +0,0 @@ -#!/usr/bin/env bash -# shellcheck disable=SC2016 # Backticks below are literal Markdown code spans. -set -uo pipefail - -SCRIPT="$(cd "$(dirname "$0")" && pwd)/ia-doctor.sh" -FAILURES=0 -TEMPS="" - -cleanup() { - cd / - local d - for d in $TEMPS; do rm -rf "$d"; done -} -trap cleanup EXIT - -fresh_repo() { - REPO=$(mktemp -d) - REPO=$(cd "$REPO" && pwd -P) - TEMPS="$TEMPS $REPO" - git -C "$REPO" init -qb main -} - -check_contains() { - local name=$1 needle=$2 output=$3 - if [[ "$output" == *"$needle"* ]]; then - echo " ok: $name" - else - echo " FAIL: $name (missing: $needle)" - FAILURES=$((FAILURES + 1)) - fi -} - -check_not_contains() { - local name=$1 needle=$2 output=$3 - if [[ "$output" != *"$needle"* ]]; then - echo " ok: $name" - else - echo " FAIL: $name (unexpected: $needle)" - FAILURES=$((FAILURES + 1)) - fi -} - -check_verify() { - local name=$1 expected=$2 runtime=$3 status - out=$(bash "$SCRIPT" verify --runtime "$runtime" "$REPO" 2>&1) - status=$? - if [[ $status -eq $expected ]]; then - echo " ok: $name" - else - echo " FAIL: $name (exit $status, expected $expected)" - printf '%s\n' "$out" - FAILURES=$((FAILURES + 1)) - fi -} - -echo "deferred guidance outside native directories" -fresh_repo -mkdir -p "$REPO/docs/agent-guidance" "$REPO/ordinary" "$REPO/src" -printf 'Read `docs/agent-guidance/map.md` before API work.\n' > "$REPO/AGENTS.md" -printf 'Read `ordinary/map.md` before API work.\n' > "$REPO/docs/agent-guidance/map.md" -printf 'Read `docs/agent-guidance/missing.md` before API work.\n' > "$REPO/ordinary/map.md" -check_verify "rejects a broken route through deferred maps" 1 codex -check_contains "identifies the transitive missing target" "critical broken-reference | $REPO/ordinary/map.md -> $REPO/docs/agent-guidance/missing.md" "$out" -printf '# API policy\n' > "$REPO/docs/agent-guidance/missing.md" -check_verify "accepts the repaired deferred chain" 0 codex -printf 'Read `docs/agent-guidance/map.md`.\n' > "$REPO/docs/agent-guidance/missing.md" -check_verify "detects a cycle outside native directories" 1 codex -check_contains "reports the deferred cycle" "critical instruction-cycle" "$out" -printf 'Read `src/example.sh`.\nSee `ordinary/background.md` for background.\n' > "$REPO/docs/agent-guidance/missing.md" -printf '# Read `docs/missing-from-source.md`.\n' > "$REPO/src/example.sh" -printf 'Read `docs/missing-from-background.md`.\n' > "$REPO/ordinary/background.md" -check_verify "does not parse source code or incidental documentation as guidance" 0 codex -check_not_contains "source and background routes stay unparsed" "missing-from-" "$out" - -echo "complete route traversal" -fresh_repo -mkdir -p "$REPO/docs/agent-guidance" -printf 'Read `docs/agent-guidance/1.md`.\n' > "$REPO/AGENTS.md" -i=1 -while [[ $i -le 10 ]]; do - printf 'Read `docs/agent-guidance/%s.md`.\n' "$((i + 1))" > "$REPO/docs/agent-guidance/$i.md" - i=$((i + 1)) -done -check_verify "rejects a broken dependency beyond eight hops" 1 codex -check_contains "checks the end of the chain" "critical broken-reference | $REPO/docs/agent-guidance/10.md -> $REPO/docs/agent-guidance/11.md" "$out" - -echo "native imports" -fresh_repo -mkdir -p "$REPO/.agent-shared/rules" "$REPO/src" -printf 'Read `.agent-shared/rules/api.md`.\n' > "$REPO/AGENTS.md" -printf '@AGENTS.md\n' > "$REPO/CLAUDE.md" -printf '# API\n' > "$REPO/.agent-shared/rules/api.md" -check_verify "accepts the documented bare Claude adapter" 0 both -check_contains "records the bare import route" "$REPO/CLAUDE.md -> $REPO/AGENTS.md" "$out" -printf '@local.md\n' > "$REPO/src/CLAUDE.md" -printf '# Local policy\n' > "$REPO/src/local.md" -check_verify "resolves nested bare imports from their importing file" 0 both -check_contains "records a source-relative import" "$REPO/src/CLAUDE.md -> $REPO/src/local.md" "$out" -rm "$REPO/src/local.md" -printf '# Root lookalike\n' > "$REPO/local.md" -check_verify "does not substitute a root lookalike for a missing native import" 1 claude -check_contains "reports the actual missing import path" "$REPO/src/CLAUDE.md -> $REPO/src/local.md" "$out" - -echo "skill bundle resources" -fresh_repo -mkdir -p "$REPO/.agents/skills/check/references" "$REPO/references" -printf 'For checks, read `.agents/skills/check/SKILL.md`.\n' > "$REPO/AGENTS.md" -printf '%s\n' '---' 'name: check' 'description: Check the repository.' '---' 'Read `references/guide.md`.' > "$REPO/.agents/skills/check/SKILL.md" -printf 'Read `references/detail.md`.\n' > "$REPO/.agents/skills/check/references/guide.md" -printf '# Details\n' > "$REPO/.agents/skills/check/references/detail.md" -check_verify "accepts valid skill-relative resources without root counterparts" 0 codex -check_not_contains "does not report bundle resources as ordinary paths" "critical nonroot-path" "$out" -printf 'Read `docs/missing-lookalike.md`.\n' > "$REPO/references/guide.md" -check_verify "uses skill-owned resources including deferred references" 0 codex -check_contains "records the bundle-relative route" "$REPO/.agents/skills/check/SKILL.md -> $REPO/.agents/skills/check/references/guide.md" "$out" -check_not_contains "does not prefer repository lookalikes" "missing-lookalike" "$out" -rm "$REPO/.agents/skills/check/references/guide.md" -check_verify "rejects a missing bundle resource despite a root lookalike" 1 codex -check_contains "reports the missing bundle path" "$REPO/.agents/skills/check/SKILL.md -> $REPO/.agents/skills/check/references/guide.md" "$out" - -echo "selected-runtime verification scope" -fresh_repo -mkdir -p "$REPO/.claude/rules" "$REPO/src" -printf '# Claude\n' > "$REPO/CLAUDE.md" -printf 'Read `docs/codex-missing.md`.\n' > "$REPO/AGENTS.md" -printf 'Read `docs/nested-codex-missing.md`.\n' > "$REPO/src/AGENTS.md" -check_verify "Claude ignores unrelated Codex entrypoint defects" 0 claude -check_contains "still inventories the unselected root" "$REPO/AGENTS.md | scope=root" "$out" -check_verify "both runtimes expose the Codex defect" 1 both -printf '@AGENTS.md\n' > "$REPO/CLAUDE.md" -check_verify "Claude checks Codex guidance reached through an import" 1 claude -check_contains "reports the imported defect" "$REPO/AGENTS.md -> $REPO/docs/codex-missing.md" "$out" -printf '# Agents\n' > "$REPO/AGENTS.md" -rm "$REPO/src/AGENTS.md" -printf 'Read `docs/claude-missing.md`.\n' > "$REPO/CLAUDE.md" -printf 'Read `docs/native-missing.md`.\n' > "$REPO/.claude/rules/api.md" -printf 'Read `docs/nested-claude-missing.md`.\n' > "$REPO/src/CLAUDE.md" -check_verify "Codex ignores unrelated Claude entrypoints and native rules" 0 codex -check_verify "Claude verifies selected native guidance" 1 claude -check_contains "checks Claude native rules" "$REPO/.claude/rules/api.md -> $REPO/docs/native-missing.md" "$out" -check_contains "checks Claude nested guidance" "$REPO/src/CLAUDE.md -> $REPO/docs/nested-claude-missing.md" "$out" -printf '# Override\n' > "$REPO/AGENTS.override.md" -printf 'Read `docs/shadowed-missing.md`.\n' > "$REPO/AGENTS.md" -check_verify "Codex ignores its shadowed root" 0 codex -printf 'Read `AGENTS.md`.\n' > "$REPO/AGENTS.override.md" -check_verify "explicitly routed shadowed guidance is checked" 1 codex -printf '# Override\n' > "$REPO/AGENTS.override.md" -rm "$REPO/CLAUDE.md" -ln -s missing.md "$REPO/CLAUDE.md" -check_verify "unselected broken symlinks remain inventory only" 0 codex -check_verify "selected broken symlinks still block" 1 claude -check_contains "reports unreadable selected guidance" "critical unreadable-guidance | $REPO/CLAUDE.md (broken symlink)" "$out" - -echo "doctor IA structure" -fresh_repo -mkdir -p "$REPO/docs/agent-rules" -printf '# Agents\n\nBefore API work, read `docs/agent-rules/api.md`.\n' > "$REPO/AGENTS.md" -printf '# API rules\n\nKeep errors typed.\n' > "$REPO/docs/agent-rules/api.md" -out=$(bash "$SCRIPT" inspect "$REPO") -check_contains "finds a routed file" "$REPO/AGENTS.md -> $REPO/docs/agent-rules/api.md" "$out" -check_contains "reports approximate tokens" "approx_tokens=" "$out" -check_not_contains "does not invent a broken route" "broken-reference" "$out" - -echo "broken references" -printf '# Agents\n\nBefore API work, read `docs/agent-rules/missing.md`.\n' > "$REPO/AGENTS.md" -out=$(bash "$SCRIPT" inspect "$REPO") -check_contains "reports missing local markdown" "critical broken-reference | $REPO/AGENTS.md -> $REPO/docs/agent-rules/missing.md" "$out" -if bash "$SCRIPT" verify "$REPO" >/dev/null 2>&1; then - echo " FAIL: verify accepted a broken route" - FAILURES=$((FAILURES + 1)) -else - echo " ok: verify rejects a broken route" -fi - -printf '# Agents\n\n- `apps/api/**` -> `.agent-shared/rules/missing.md`\n' > "$REPO/AGENTS.md" -out=$(bash "$SCRIPT" inspect "$REPO") -check_contains "reports a broken rule-index target" "critical broken-reference | $REPO/AGENTS.md -> $REPO/.agent-shared/rules/missing.md" "$out" -mkdir -p "$REPO/.agent-shared/rules" -printf '# Agents\n\n- `apps/api/**` -> `.agent-shared/rules/api.md`\n' > "$REPO/AGENTS.md" -printf '# API\n' > "$REPO/.agent-shared/rules/api.md" -out=$(bash "$SCRIPT" inspect --runtime codex "$REPO") -check_contains "treats an arrow rule-index row as an explicit route" "$REPO/AGENTS.md -> $REPO/.agent-shared/rules/api.md" "$out" -check_not_contains "does not leave an arrow-routed rule unrouted" "unrouted-guidance" "$out" - -echo "session-root paths and fenced examples" -fresh_repo -mkdir -p "$REPO/docs/internal/export" -printf '# Agents\n\nBefore docs work, read `docs/AGENTS.md`.\n' > "$REPO/AGENTS.md" -printf '# Docs\n\nBefore export work, read `internal/export/assembler.go`.\n' > "$REPO/docs/AGENTS.md" -printf 'package export\n' > "$REPO/docs/internal/export/assembler.go" -out=$(bash "$SCRIPT" inspect --runtime codex "$REPO") -check_contains "rejects a containing-file-relative ordinary path" "critical nonroot-path | $REPO/docs/AGENTS.md -> $REPO/internal/export/assembler.go" "$out" - -printf '# Agents\n\n```md\nBefore API work, read `docs/missing.md`.\n```\n' > "$REPO/AGENTS.md" -out=$(bash "$SCRIPT" inspect --runtime codex "$REPO") -check_not_contains "ignores routes inside backtick fences" "docs/missing.md" "$out" -printf '# Agents\n\n ~~~~md\nBefore API work, read `docs/missing.md`.\n ~~~~\n' > "$REPO/AGENTS.md" -out=$(bash "$SCRIPT" inspect --runtime codex "$REPO") -check_not_contains "ignores routes inside indented tilde fences" "docs/missing.md" "$out" -printf '# Agents\n\n```md\n```not-a-close\nBefore API work, read `docs/missing.md`.\n```\n' > "$REPO/AGENTS.md" -out=$(bash "$SCRIPT" inspect --runtime codex "$REPO") -check_not_contains "does not close a fence on a marker with trailing text" "docs/missing.md" "$out" -printf '\357\273\277```md\nBefore API work, read `docs/missing.md`.\n```\n' > "$REPO/AGENTS.md" -out=$(bash "$SCRIPT" inspect --runtime codex "$REPO") -check_not_contains "ignores a route in a BOM-prefixed fenced example" "docs/missing.md" "$out" - -echo "mentions and repository boundaries" -fresh_repo -mkdir -p "$REPO/.agent-shared/rules" "$REPO/docs/nested" -printf '# Agents\n\nSee `.agent-shared/rules/api.md` for background.\n' > "$REPO/AGENTS.md" -printf '# API\n' > "$REPO/.agent-shared/rules/api.md" -out=$(bash "$SCRIPT" inspect --runtime codex "$REPO") -check_contains "a mention does not satisfy reachability" "critical unrouted-guidance | runtime=codex | $REPO/.agent-shared/rules/api.md" "$out" -printf '# Nested\n\nSee `local.md` for background.\n' > "$REPO/docs/nested/AGENTS.md" -printf '# Local\n' > "$REPO/docs/nested/local.md" -out=$(bash "$SCRIPT" inspect --runtime codex "$REPO") -check_not_contains "a mention-only relative path is not a route error" "nonroot-path | $REPO/docs/nested/AGENTS.md" "$out" - -outside="$REPO/../$(basename "$REPO")-outside.md" -printf '# Outside\n' > "$outside" -printf '# Agents\n\nRead `../%s-outside.md`.\n' "$(basename "$REPO")" > "$REPO/AGENTS.md" -out=$(bash "$SCRIPT" inspect --runtime codex "$REPO") -check_contains "rejects an ordinary route that escapes the repository" "critical broken-reference | $REPO/AGENTS.md" "$out" -printf '# Agents\n\nRead `%s/docs/nested/local.md`.\n' "$REPO" > "$REPO/AGENTS.md" -out=$(bash "$SCRIPT" inspect --runtime codex "$REPO") -check_contains "rejects an absolute in-repository route form" "critical nonroot-path | $REPO/AGENTS.md -> $REPO/docs/nested/local.md" "$out" -rm -f "$outside" - -echo "missing entrypoint" -fresh_repo -out=$(bash "$SCRIPT" inspect "$REPO") -check_contains "reports a missing root entrypoint" "critical missing-entrypoint" "$out" -if bash "$SCRIPT" verify "$REPO" >/dev/null 2>&1; then - echo " FAIL: verify accepted a repository without an entrypoint" - FAILURES=$((FAILURES + 1)) -else - echo " ok: verify rejects a repository without an entrypoint" -fi - -echo "cycles and unrouted adapter rules" -fresh_repo -mkdir -p "$REPO/.agent-shared/rules" "$REPO/.agent-shared/shared" -printf '# Agents\n\nRead `.agent-shared/shared/a.md`.\n' > "$REPO/AGENTS.md" -printf '# A\n\nRead `.agent-shared/shared/b.md`.\n' > "$REPO/.agent-shared/shared/a.md" -printf '# B\n\nRead `.agent-shared/shared/a.md`.\n' > "$REPO/.agent-shared/shared/b.md" -printf '# Hidden\n' > "$REPO/.agent-shared/rules/hidden.md" -out=$(bash "$SCRIPT" inspect "$REPO") -check_contains "reports an instruction cycle" "critical instruction-cycle" "$out" -check_contains "reports an unrouted shared rule" "critical unrouted-guidance | runtime=codex | $REPO/.agent-shared/rules/hidden.md" "$out" -if bash "$SCRIPT" verify "$REPO" >/dev/null 2>&1; then - echo " FAIL: verify accepted unrouted shared guidance" - FAILURES=$((FAILURES + 1)) -else - echo " ok: verify rejects unrouted shared guidance" -fi - -echo "runtime-specific reachability" -fresh_repo -mkdir -p "$REPO/src" -printf '# Agents\n' > "$REPO/AGENTS.md" -printf '# Nested\n' > "$REPO/src/AGENTS.md" -out=$(bash "$SCRIPT" inspect --runtime codex "$REPO") -check_contains "does not invent Codex reachability for nested guidance" "critical unrouted-guidance | runtime=codex | $REPO/src/AGENTS.md" "$out" - -fresh_repo -mkdir -p "$REPO/.claude" "$REPO/src" -printf '# Claude\n' > "$REPO/.claude/CLAUDE.md" -printf '# Nested Claude\n' > "$REPO/src/CLAUDE.md" -out=$(bash "$SCRIPT" inspect --runtime claude "$REPO") -check_not_contains "accepts Claude native nested scope" "unrouted-guidance" "$out" -check_not_contains "accepts the .claude project entrypoint" "missing-entrypoint" "$out" - -echo "adapter drift and native all-scope rules" -fresh_repo -mkdir -p "$REPO/.agent-shared/rules" "$REPO/.claude/rules" -printf '# Agents\n' > "$REPO/AGENTS.md" -printf '%s\n' '---' 'paths: ["**"]' '---' '# Shared' > "$REPO/.agent-shared/rules/all.md" -printf '%s\n' '---' 'paths: ["**"]' '---' '# Different' > "$REPO/.claude/rules/all.md" -out=$(bash "$SCRIPT" inspect --runtime codex --mirror .agent-shared/rules=.claude/rules "$REPO") -check_contains "reports mirror drift" "critical adapter-drift" "$out" -check_contains "reports a rule that is effectively resident" "advisory always-loaded-rule | $REPO/.claude/rules/all.md" "$out" - -echo "aligned adapter mirrors" -fresh_repo -mkdir -p "$REPO/.agent-shared/rules" "$REPO/.claude/rules" -printf '# Agents\n\nRead `.agent-shared/rules/api.md`.\n' > "$REPO/AGENTS.md" -printf '# API\n' > "$REPO/.agent-shared/rules/api.md" -cp "$REPO/.agent-shared/rules/api.md" "$REPO/.claude/rules/api.md" -out=$(bash "$SCRIPT" inspect --runtime codex --mirror .agent-shared/rules=.claude/rules "$REPO") -check_contains "recognizes an aligned mirror" "$REPO/.agent-shared/rules -> $REPO/.claude/rules | status=aligned | declared=true" "$out" -check_not_contains "does not call an intentional mirror duplicate" "advisory duplicate-content" "$out" - -echo "declared mirror dependencies" -fresh_repo -mkdir -p "$REPO/.claude/rules" "$REPO/mirror" "$REPO/docs" -printf '# Agents\n' > "$REPO/AGENTS.md" -printf 'Before API work, read `docs/missing.md`.\n' > "$REPO/.claude/rules/api.md" -cp "$REPO/.claude/rules/api.md" "$REPO/mirror/api.md" -if out=$(bash "$SCRIPT" verify --runtime codex --mirror .claude/rules=mirror "$REPO"); then - echo " FAIL: verify accepted a broken dependency in a declared mirror" - FAILURES=$((FAILURES + 1)) -else - echo " ok: declared mirror dependencies block verification" -fi -check_contains "parses declared mirrors even outside the selected native runtime" "critical broken-reference | $REPO/.claude/rules/api.md -> $REPO/docs/missing.md" "$out" -printf '# Shared policy\n' > "$REPO/docs/missing.md" -if bash "$SCRIPT" verify --runtime codex --mirror .claude/rules=mirror "$REPO" >/dev/null; then - echo " ok: repaired declared mirror dependencies verify" -else - echo " FAIL: verify rejected repaired declared mirror dependencies" - FAILURES=$((FAILURES + 1)) -fi - -echo "root symlink adapter" -fresh_repo -printf '# Agents\n' > "$REPO/AGENTS.md" -ln -s AGENTS.md "$REPO/CLAUDE.md" -out=$(bash "$SCRIPT" inspect "$REPO") -check_contains "recognizes the root symlink" "$REPO/AGENTS.md -> $REPO/CLAUDE.md | status=symlink" "$out" -check_contains "counts Codex resident root content once" "codex_root_bytes=9" "$out" -check_contains "reports Claude adapter resident content separately" "claude_root_bytes=9" "$out" -check_not_contains "does not call the symlink duplicate" "advisory duplicate-content" "$out" - -echo "reverse root symlink adapter" -fresh_repo -printf '# Claude\n' > "$REPO/CLAUDE.md" -ln -s CLAUDE.md "$REPO/AGENTS.md" -check_verify "accepts a Claude-canonical symlink adapter" 0 both -check_contains "reports Claude as source and Codex as adapter" "$REPO/CLAUDE.md -> $REPO/AGENTS.md | status=symlink" "$out" -check_contains "counts Codex reverse-adapter content" "codex_root_bytes=9" "$out" -check_contains "counts canonical Claude content" "claude_root_bytes=9" "$out" -check_not_contains "does not call the reverse adapter duplicate" "advisory duplicate-content" "$out" -rm "$REPO/AGENTS.md" -cp "$REPO/CLAUDE.md" "$REPO/AGENTS.md" -out=$(bash "$SCRIPT" inspect "$REPO") -check_contains "independent root copies still count as duplicates" "advisory duplicate-content" "$out" -check_not_contains "independent root copies are not symlink adapters" "status=symlink" "$out" - -echo "root budget" -fresh_repo -awk 'BEGIN {for (i=0; i<34000; i++) printf "x"}' > "$REPO/AGENTS.md" -out=$(bash "$SCRIPT" inspect "$REPO") -check_contains "reports the default 32 KiB budget" "critical root-budget | $REPO/AGENTS.md bytes=34000 limit=32768" "$out" - -echo "active and selected runtime budget" -fresh_repo -awk 'BEGIN {for (i=0; i<34000; i++) printf "x"}' > "$REPO/AGENTS.md" -printf '# Override\n' > "$REPO/AGENTS.override.md" -out=$(bash "$SCRIPT" inspect --runtime codex "$REPO") -check_not_contains "does not budget a shadowed Codex root" "critical root-budget" "$out" -mkdir -p "$REPO/.codex" -printf 'project_doc_max_bytes = 1024 # 32 KiB\n' > "$REPO/.codex/config.toml" -awk 'BEGIN {for (i=0; i<1500; i++) printf "x"}' > "$REPO/AGENTS.override.md" -out=$(bash "$SCRIPT" inspect --runtime codex "$REPO") -check_contains "parses a budget with an inline TOML comment" "bytes=1500 limit=1024" "$out" -printf '# Claude\n' > "$REPO/CLAUDE.md" -chmod 000 "$REPO/.codex/config.toml" -out=$(bash "$SCRIPT" inspect --runtime claude "$REPO") -check_not_contains "Claude-only audit ignores unreadable Codex config" "unreadable-guidance" "$out" -check_not_contains "Claude-only audit ignores Codex budget" "root-budget" "$out" -chmod 600 "$REPO/.codex/config.toml" - -echo "unreadable declared mirror" -fresh_repo -mkdir -p "$REPO/source" "$REPO/target" -printf '# Agents\n' > "$REPO/AGENTS.md" -printf '# Rule\n' > "$REPO/source/rule.md" -cp "$REPO/source/rule.md" "$REPO/target/rule.md" -chmod 400 "$REPO/source" -out=$(bash "$SCRIPT" inspect --runtime codex --mirror source=target "$REPO") -check_contains "reports an unsearchable declared mirror compactly" "critical adapter-drift" "$out" -chmod 700 "$REPO/source" - -echo "duplicate content" -fresh_repo -mkdir -p "$REPO/.agent-shared/rules" -printf '# Agents\n\nRead `.agent-shared/rules/a.md` and `.agent-shared/rules/b.md`.\n' > "$REPO/AGENTS.md" -printf '# Same\n' > "$REPO/.agent-shared/rules/a.md" -cp "$REPO/.agent-shared/rules/a.md" "$REPO/.agent-shared/rules/b.md" -out=$(bash "$SCRIPT" inspect "$REPO") -check_contains "reports identical guidance" "advisory duplicate-content" "$out" - -echo "unreadable required evidence" -fresh_repo -ln -s missing.md "$REPO/AGENTS.md" -out=$(bash "$SCRIPT" inspect --runtime codex "$REPO") -check_contains "reports a broken entrypoint symlink" "critical unreadable-guidance | $REPO/AGENTS.md (broken symlink)" "$out" -if bash "$SCRIPT" verify --runtime codex "$REPO" >/dev/null 2>&1; then - echo " FAIL: verify accepted a broken entrypoint symlink" - FAILURES=$((FAILURES + 1)) -else - echo " ok: verify rejects a broken entrypoint symlink" -fi - -echo "large worktree output" -fresh_repo -printf '# Agents\n' > "$REPO/AGENTS.md" -REAL_GIT=$(command -v git) -mkdir -p "$REPO/bin" -printf '%s\n' '#!/usr/bin/env bash' 'if [[ "$*" == *"worktree list --porcelain"* ]]; then' " printf 'worktree %s\\n' '$REPO'" ' i=0; while [[ $i -lt 9000 ]]; do printf "HEAD %040d\\n" "$i"; i=$((i + 1)); done' ' exit 0' 'fi' "exec '$REAL_GIT' \"\$@\"" > "$REPO/bin/git" -chmod +x "$REPO/bin/git" -out=$(PATH="$REPO/bin:$PATH" bash "$SCRIPT" inspect --runtime codex "$REPO") -check_contains "consumes a large worktree listing without SIGPIPE" "root: $REPO" "$out" - -echo "compact output caps" -fresh_repo -printf '# Agents\n' > "$REPO/AGENTS.md" -i=0 -while [[ $i -lt 45 ]]; do mkdir -p "$REPO/dir-$i"; printf '# Nested\n' > "$REPO/dir-$i/AGENTS.md"; i=$((i + 1)); done -out=$(bash "$SCRIPT" inspect --runtime codex "$REPO") -check_contains "caps the entrypoint listing" "additional entrypoints omitted" "$out" -check_contains "caps unrouted findings" "additional findings omitted" "$out" - -if [[ $FAILURES -ne 0 ]]; then - echo "$FAILURES test(s) failed" >&2 - exit 1 -fi -echo "all doctor IA tests passed" diff --git a/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/SKILL.md b/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/SKILL.md index c7e90ddb..0dc1034f 100644 --- a/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/SKILL.md +++ b/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/SKILL.md @@ -10,8 +10,9 @@ resident and makes every deferred rule reachable when it matters. Begin read-only and preserve existing knowledge; never replace it with a generic template. -Run structural inventory through `/scripts/ia-setup.sh`, where -`` contains this file. Execute bundled scripts with Bash. The script +Run structural inventory through the frozen UV package at +`/../../backend`, where `` contains this file. Requires +UV and Python 3.10–3.13 on Linux, macOS, or native Windows. The inspector finds evidence; it does not decide placement or policy. ## Working model @@ -35,7 +36,7 @@ finds evidence; it does not decide placement or policy. Run: ```sh -bash /scripts/ia-setup.sh inspect [repository] +uv run --quiet --frozen --no-dev --project "/../../backend" ia-setup inspect [repository] ``` Use its compact inventory to locate instruction entrypoints, adapters, @@ -118,6 +119,9 @@ prohibitions from workflow steps: the universal rules stay in the resident root, while the ordered steps and their task-specific gates move to the skill. A route to the skill does not replace a resident prohibition. Keep an explicit intent route when native skill discovery is insufficient for any selected runtime. +That route names the exact session-root-relative `SKILL.md` path and says when +to read it. Confirm it appears in the inspector's `routes` records; a passing +structural status alone does not prove that a newly moved procedure is reachable. Remove cheap manifest/config inventory rather than turning it into `use the existing toolchain`. For a mixed statement such as `Runtime: Bun; never use npm @@ -165,6 +169,10 @@ grouped question. ### 4. Apply and verify +For approved guidance-file replacements, read +[`../../references/file-updates.md`](../../references/file-updates.md) and use +the bundled atomic writer. Keep existing adapter direction and newline style. + Apply only confirmed actions. Edit a declared source of truth, then use the repository's sync mechanism for generated/mirrored adapters. Preserve exact constraints, rationales, unusual command spelling, skill procedures, and @@ -175,7 +183,7 @@ push. Verify the selected runtimes and declared mirrors: ```sh -bash /../../bin/ia-doctor verify --runtime both \ +uv run --quiet --frozen --no-dev --project "/../../backend" ia-doctor verify --runtime both \ [--mirror source=target]... [repository] ``` diff --git a/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/codex-only-scope.yaml b/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/codex-only-scope.yaml index 56d7edce..830cd0e8 100644 --- a/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/codex-only-scope.yaml +++ b/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/codex-only-scope.yaml @@ -9,9 +9,9 @@ fixture: commits: - message: "chore: init" files: - README.md: "# Service\n" + README.md: "# Service\n\n## Service contract\n\nPreserve JSON response bodies for every API error, including authentication failures.\n" package.json: "{\"private\":true}\n" - setup: cp "{{case_dir}}/../../../bin/ia-doctor" .git/ia-doctor + setup: mkdir -p .git/ia-backend; cp -R "{{case_dir}}/../../../backend/src" .git/ia-backend/; cp "{{case_dir}}/../../../backend/pyproject.toml" "{{case_dir}}/../../../backend/uv.lock" .git/ia-backend/ checks: - name: Codex entrypoint is created run: test -f AGENTS.md && echo created @@ -23,7 +23,7 @@ checks: expect_regex: "^absent$" - name: Codex-only verification passes run: >- - bash .git/ia-doctor verify --runtime codex . >/dev/null && echo verified + uv run --quiet --frozen --no-dev --project .git/ia-backend ia-doctor verify --runtime codex . >/dev/null && echo verified expect_regex: "^verified$" - name: setup creates no commit run: git rev-list --all --reflog --count diff --git a/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/move-procedure-to-skill.yaml b/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/move-procedure-to-skill.yaml index af1852b9..83d9737e 100644 --- a/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/move-procedure-to-skill.yaml +++ b/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/move-procedure-to-skill.yaml @@ -28,7 +28,7 @@ fixture: tools/build-release: "#!/bin/sh\nexit 0\n" tools/publish: "#!/bin/sh\nexit 0\n" dist/checksums.txt: "fixture\n" - setup: cp "{{case_dir}}/../../../bin/ia-doctor" .git/ia-doctor + setup: mkdir -p .git/ia-backend; cp -R "{{case_dir}}/../../../backend/src" .git/ia-backend/; cp "{{case_dir}}/../../../backend/pyproject.toml" "{{case_dir}}/../../../backend/uv.lock" .git/ia-backend/ checks: - name: ordered procedure moves to a repository skill run: >- @@ -95,7 +95,7 @@ checks: expect_regex: "^discoverable$" - name: procedure move passes deterministic dual-runtime verification run: >- - bash .git/ia-doctor verify --runtime both . >/dev/null && echo verified + uv run --quiet --frozen --no-dev --project .git/ia-backend ia-doctor verify --runtime both . >/dev/null && echo verified expect_regex: "^verified$" - name: setup creates no commit run: git rev-list --all --reflog --count diff --git a/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/preserve-reverse-adapter.yaml b/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/preserve-reverse-adapter.yaml index f5317b13..a56c61b3 100644 --- a/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/preserve-reverse-adapter.yaml +++ b/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/preserve-reverse-adapter.yaml @@ -18,7 +18,7 @@ fixture: setup: >- ln -s CLAUDE.md AGENTS.md && git add AGENTS.md && git commit -m 'chore: add Codex adapter' >/dev/null && - cp "{{case_dir}}/../../../bin/ia-doctor" .git/ia-doctor + mkdir -p .git/ia-backend; cp -R "{{case_dir}}/../../../backend/src" .git/ia-backend/; cp "{{case_dir}}/../../../backend/pyproject.toml" "{{case_dir}}/../../../backend/uv.lock" .git/ia-backend/ checks: - name: existing Claude canonical direction is preserved run: test -L AGENTS.md && test AGENTS.md -ef CLAUDE.md && echo preserved @@ -31,5 +31,5 @@ checks: expect_regex: "^2$" - name: reverse adapter passes deterministic dual-runtime verification run: >- - bash .git/ia-doctor verify --runtime both . >/dev/null && echo verified + uv run --quiet --frozen --no-dev --project .git/ia-backend ia-doctor verify --runtime both . >/dev/null && echo verified expect_regex: "^verified$" diff --git a/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/preserve-settled-migration.yaml b/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/preserve-settled-migration.yaml index a9044bdd..7657d9b2 100644 --- a/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/preserve-settled-migration.yaml +++ b/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/preserve-settled-migration.yaml @@ -20,14 +20,14 @@ fixture: src/storage/orm/legacy.ts: "export const legacy = true;\n" src/storage/sql/users.ts: "export const users = true;\n" package.json: "{\"private\":true}\n" - setup: cp "{{case_dir}}/../../../bin/ia-doctor" .git/ia-doctor + setup: mkdir -p .git/ia-backend; cp -R "{{case_dir}}/../../../backend/src" .git/ia-backend/; cp "{{case_dir}}/../../../backend/pyproject.toml" "{{case_dir}}/../../../backend/uv.lock" .git/ia-backend/ checks: - name: dual-runtime agent entrypoints are created run: test -f AGENTS.md && test -e CLAUDE.md && echo created expect_regex: "^created$" - name: settled guidance passes deterministic dual-runtime verification run: >- - bash .git/ia-doctor verify --runtime both . >/dev/null && echo verified + uv run --quiet --frozen --no-dev --project .git/ia-backend ia-doctor verify --runtime both . >/dev/null && echo verified expect_regex: "^verified$" - name: setup creates no commit run: git rev-list --all --reflog --count diff --git a/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/rewrite-root-relative-paths.yaml b/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/rewrite-root-relative-paths.yaml index 9bf0b24a..b209d11e 100644 --- a/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/rewrite-root-relative-paths.yaml +++ b/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/rewrite-root-relative-paths.yaml @@ -33,7 +33,7 @@ fixture: Run `scripts/check.sh` from this skill. .agents/skills/export-check/scripts/check.sh: "#!/bin/sh\nexit 0\n" - setup: cp "{{case_dir}}/../../../bin/ia-doctor" .git/ia-doctor + setup: mkdir -p .git/ia-backend; cp -R "{{case_dir}}/../../../backend/src" .git/ia-backend/; cp "{{case_dir}}/../../../backend/pyproject.toml" "{{case_dir}}/../../../backend/uv.lock" .git/ia-backend/ checks: - name: nested-relative path is rewritten from the session root run: cat src/backend/MAP.md @@ -49,7 +49,7 @@ checks: expect_regex: "`scripts/check\\.sh`" - name: deterministic path verification passes run: >- - bash .git/ia-doctor verify --runtime both . >/dev/null && echo verified + uv run --quiet --frozen --no-dev --project .git/ia-backend ia-doctor verify --runtime both . >/dev/null && echo verified expect_regex: "^verified$" - name: setup creates no commit run: git rev-list --all --reflog --count diff --git a/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/scoped-router.yaml b/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/scoped-router.yaml index e398bf5f..661f1bf3 100644 --- a/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/scoped-router.yaml +++ b/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/evals/scoped-router.yaml @@ -15,7 +15,7 @@ fixture: package.json: "{\"private\":true,\"packageManager\":\"bun@1.3.0\"}\n" apps/api/src/db.ts: "export const schemaVersion = 1;\n" apps/web/src/app.tsx: "export const App = () => null;\n" - setup: cp "{{case_dir}}/../../../bin/ia-doctor" .git/ia-doctor + setup: mkdir -p .git/ia-backend; cp -R "{{case_dir}}/../../../backend/src" .git/ia-backend/; cp "{{case_dir}}/../../../backend/pyproject.toml" "{{case_dir}}/../../../backend/uv.lock" .git/ia-backend/ checks: - name: root entrypoint created run: test -f AGENTS.md && echo created @@ -111,7 +111,7 @@ checks: flags: i - name: deterministic dual-runtime verification passes run: >- - bash .git/ia-doctor verify --runtime both . >/dev/null && echo verified + uv run --quiet --frozen --no-dev --project .git/ia-backend ia-doctor verify --runtime both . >/dev/null && echo verified expect_regex: "^verified$" - name: setup creates no commit run: git rev-list --all --reflog --count diff --git a/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/scripts/ia-setup.sh b/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/scripts/ia-setup.sh deleted file mode 100755 index 9d359fb7..00000000 --- a/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/scripts/ia-setup.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -usage() { - echo "usage: ia-setup.sh inspect [repository]" >&2 - exit 64 -} - -resolve_root() { - local target=${1:-.} line worktrees - if [[ ! -d "$target" || ! -r "$target" ]]; then - echo "error: repository path is not a readable directory: $target" >&2 - exit 2 - fi - worktrees=$(git -C "$target" worktree list --porcelain 2>/dev/null || :) - line=$(awk 'NR==1 && $1=="worktree" {sub(/^worktree /, ""); print}' <<< "$worktrees") - if [[ -n "$line" ]]; then - printf '%s' "$line" - else - (cd "$target" && pwd -P) - fi -} - -print_group() { - local label=$1 count=0 path - shift - echo "$label:" - find "$ROOT" "$@" -print 2>/dev/null | - LC_ALL=C sort | - while IFS= read -r path; do - count=$((count + 1)) - if [[ $count -le 40 ]]; then printf ' - %s\n' "$path"; fi - if [[ $count -eq 41 ]]; then echo " - ..."; fi - done -} - -[[ ${1:-} == "inspect" ]] || usage -ROOT=$(resolve_root "${2:-.}") - -validate_required_file() { - local label=$1 path=$2 - [[ -e "$path" || -L "$path" ]] || return 0 - if [[ ! -f "$path" || ! -r "$path" ]]; then - echo "error: $label is unreadable, missing, or not a regular file: $path" >&2 - exit 2 - fi -} - -for file in "$ROOT/AGENTS.md" "$ROOT/AGENTS.override.md" "$ROOT/CLAUDE.md" "$ROOT/CLAUDE.local.md" "$ROOT/.claude/CLAUDE.md"; do - validate_required_file "instruction entrypoint" "$file" -done -validate_required_file "Codex project configuration" "$ROOT/.codex/config.toml" - -echo "root: $ROOT" -echo "entrypoints:" -find "$ROOT" \( -type f -o -type l \) \( -name 'AGENTS.md' -o -name 'AGENTS.override.md' -o -name 'CLAUDE.md' -o -name 'CLAUDE.local.md' \) \ - -not -path '*/.git/*' -not -path '*/node_modules/*' -not -path '*/.claude/worktrees/*' -not -path '*/.worktrees/*' -print 2>/dev/null | - LC_ALL=C sort | - { - entrypoint_number=0 - while IFS= read -r file; do - entrypoint_number=$((entrypoint_number + 1)) - if [[ $entrypoint_number -gt 40 ]]; then - if [[ $entrypoint_number -eq 41 ]]; then echo " - ..."; fi - continue - fi - bytes=$(wc -c < "$file" | tr -d ' ') - printf ' - %s | bytes=%s | approx_tokens=%s\n' "$file" "$bytes" "$(( (bytes + 3) / 4 ))" - done - } - -print_group "runtime_adapters" -type d "(" -name .claude -o -name .agents -o -name .codex -o -name .pi ")" -not -path '*/.git/*' -not -path '*/node_modules/*' -not -path '*/.claude/worktrees/*' -not -path '*/.worktrees/*' -print_group "manifests" -maxdepth 3 -type f "(" -name package.json -o -name pyproject.toml -o -name Cargo.toml -o -name go.mod -o -name pom.xml -o -name build.gradle -o -name Makefile ")" -not -path '*/node_modules/*' -not -path '*/.claude/worktrees/*' -not -path '*/.worktrees/*' -print_group "automation" -maxdepth 4 -type f "(" -path '*/.github/workflows/*' -o -name lefthook.yml -o -name lefthook.yaml -o -name .pre-commit-config.yaml -o -name hooks.json ")" -not -path '*/node_modules/*' -not -path '*/.claude/worktrees/*' -not -path '*/.worktrees/*' -print_group "skills" -type f -name SKILL.md -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/.claude/worktrees/*' -not -path '*/.worktrees/*' -print_group "guidance_candidates" -type f -name '*.md' "(" -path '*/rules/*' -o -path '*/agent*/*' -o -path '*/docs/*' ")" -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/.claude/worktrees/*' -not -path '*/.worktrees/*' diff --git a/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/scripts/ia-setup.test.sh b/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/scripts/ia-setup.test.sh deleted file mode 100755 index d8a453c3..00000000 --- a/plugins/foundation/darrow-information-architecture/skills/setup-information-architecture/scripts/ia-setup.test.sh +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env bash -set -uo pipefail - -SCRIPT="$(cd "$(dirname "$0")" && pwd)/ia-setup.sh" -FAILURES=0 -TEMPS="" - -cleanup() { - cd / - local d - for d in $TEMPS; do rm -rf "$d"; done -} -trap cleanup EXIT - -check_contains() { - local name=$1 needle=$2 output=$3 - if [[ "$output" == *"$needle"* ]]; then - echo " ok: $name" - else - echo " FAIL: $name (missing: $needle)" - FAILURES=$((FAILURES + 1)) - fi -} - -fresh_repo() { - REPO=$(mktemp -d) - REPO=$(cd "$REPO" && pwd -P) - TEMPS="$TEMPS $REPO" - git -C "$REPO" init -qb main - mkdir -p "$REPO/src" "$REPO/.github/workflows" "$REPO/.agents/skills/review" - printf '# Agent instructions\n' > "$REPO/AGENTS.md" - printf '{"private":true}\n' > "$REPO/package.json" - printf 'name: test\n' > "$REPO/.github/workflows/test.yml" - printf '%s\n' '---' 'name: review' 'description: Review code.' '---' > "$REPO/.agents/skills/review/SKILL.md" -} - -echo "setup IA inventory" -fresh_repo -out=$(bash "$SCRIPT" inspect "$REPO/src") -check_contains "resolves main worktree from a subdirectory" "root: $REPO" "$out" -check_contains "finds root guidance" "AGENTS.md | bytes=" "$out" -check_contains "finds manifests" "package.json" "$out" -check_contains "finds CI" ".github/workflows/test.yml" "$out" -check_contains "finds skills" ".agents/skills/review/SKILL.md" "$out" - -before=$(find "$REPO" -not -path '*/.git/*' -print | LC_ALL=C sort) -bash "$SCRIPT" inspect "$REPO" >/dev/null -after=$(find "$REPO" -not -path '*/.git/*' -print | LC_ALL=C sort) -if [[ "$before" == "$after" ]]; then - echo " ok: inventory is read-only" -else - echo " FAIL: inventory changed the repository" - FAILURES=$((FAILURES + 1)) -fi - -echo "setup fails closed on required evidence" -fresh_repo -rm "$REPO/AGENTS.md" -ln -s missing.md "$REPO/AGENTS.md" -if bash "$SCRIPT" inspect "$REPO" > /dev/null 2>&1; then - echo " FAIL: inventory accepted a broken entrypoint symlink" - FAILURES=$((FAILURES + 1)) -else - echo " ok: inventory rejects a broken entrypoint symlink" -fi - -echo "setup handles large worktree output" -fresh_repo -REAL_GIT=$(command -v git) -mkdir -p "$REPO/bin" -# shellcheck disable=SC2016 # $i/$* must stay literal: this emits a git stub script -printf '%s\n' '#!/usr/bin/env bash' 'if [[ "$*" == *"worktree list --porcelain"* ]]; then' " printf 'worktree %s\\n' '$REPO'" ' i=0; while [[ $i -lt 9000 ]]; do printf "HEAD %040d\\n" "$i"; i=$((i + 1)); done' ' exit 0' 'fi' "exec '$REAL_GIT' \"\$@\"" > "$REPO/bin/git" -chmod +x "$REPO/bin/git" -out=$(PATH="$REPO/bin:$PATH" bash "$SCRIPT" inspect "$REPO") -check_contains "consumes a large worktree listing without SIGPIPE" "root: $REPO" "$out" - -echo "setup caps large inventories" -fresh_repo -i=0 -while [[ $i -lt 45 ]]; do mkdir -p "$REPO/dir-$i"; printf '# Nested\n' > "$REPO/dir-$i/AGENTS.md"; i=$((i + 1)); done -out=$(bash "$SCRIPT" inspect "$REPO") -check_contains "caps the entrypoint listing" " - ..." "$out" - -if [[ $FAILURES -ne 0 ]]; then - echo "$FAILURES test(s) failed" >&2 - exit 1 -fi -echo "all setup IA tests passed" diff --git a/python-packages.txt b/python-packages.txt index b1e65a55..c7ace994 100644 --- a/python-packages.txt +++ b/python-packages.txt @@ -6,3 +6,4 @@ plugins/capability/darrow-verification/skills/verify-change/backend plugins/foundation/darrow-skill-authoring/skills/author-agent-skill/backend plugins/capability/darrow-tickets-github/backend plugins/foundation/darrow-decisions/backend +plugins/foundation/darrow-information-architecture/backend