Files
computer-playbook/utils/claude/settings_sort.py
kevinveenbirkenbachandClaude Opus 4.7 525699a9ab test(lint/cache-read): extend scope to utils, cli, plugins, library
Modules imported by tests run inside the same pytest process as the
tests themselves. Raw ``path.read_text(...)`` calls in those modules
bypass the cache the tests just populated. ``roles/`` stays excluded
(deployed scripts have no ``utils/`` on PYTHONPATH); ``utils/cache/``
stays excluded (cache implementation itself).

Five conversions to ``utils.cache.files.read_text``:
- ``utils/handler/yaml.py`` (YAML loader, read-only)
- ``utils/annotations/summarize.py`` (CI log scanner, with
  ``UnicodeDecodeError`` fallback replacing ``errors="replace"``)
- ``cli/meta/roles/applications/resolution/combined/__main__.py``
  (single-shot CLI JSON input)
- ``cli/core/help.py`` (read_folder_title + read_folder_description;
  same README is read many times across CLI help iteration)

Eight ``# nocheck: cache-read`` markers, each annotated inline:
- ``utils/claude/settings_sort.py`` (read then rewrite sorted)
- ``utils/docker/version_updater.py`` (read then rewrite config)
- ``cli/administration/deploy/development/network.py`` (tests mock
  ``Path.read_text`` directly; caching would bypass the mock)
- ``cli/administration/deploy/development/log_utils.py`` (append flow)
- ``cli/contributing/changelog/archive/changelog_md.py``,
  ``__main__.py``, ``package_mirror.py`` (file mutated in same flow)

Two callers refactored to variable extraction so ruff's 88-char wrap
keeps the ``# nocheck`` marker on the ``read_text(...)`` AST span line
(``cli/.../network.py`` and ``package_mirror.py:105``).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:23:32 +02:00

80 lines
2.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Sort the curated string arrays in ``.claude/settings.json``.
The lint test in
``tests/lint/repository/settings/test_claude_sorted.py`` requires every
hand-curated array to stay ASCII-sorted ascending so diffs stay
reviewable and merge conflicts stay minimal. Tools that auto-extend the
file (e.g. permission auto-allowlists) tend to append unsorted; this
script puts everything back in order.
Exit code is ``0`` if the file was already sorted (no write), ``0``
after a successful rewrite, ``1`` on read/parse failure.
"""
from __future__ import annotations
import json
import sys
from . import PROJECT_ROOT
SETTINGS_PATH = PROJECT_ROOT / ".claude" / "settings.json"
SORTED_ARRAYS: list[tuple[str, ...]] = [
("permissions", "allow"),
("permissions", "deny"),
("permissions", "ask"),
("sandbox", "network", "allowedDomains"),
("sandbox", "filesystem", "allowWrite"),
("sandbox", "filesystem", "denyRead"),
]
def _resolve(data: dict, path: tuple[str, ...]):
obj = data
for key in path:
if not isinstance(obj, dict) or key not in obj:
return None
obj = obj[key]
return obj
def main() -> int:
try:
raw = SETTINGS_PATH.read_text(
encoding="utf-8"
) # nocheck: cache-read — CLI reads SETTINGS_PATH then writes back sorted; cache would mask the rewrite
except OSError as exc:
print(f"error: cannot read {SETTINGS_PATH}: {exc}", file=sys.stderr)
return 1
try:
data = json.loads(raw)
except json.JSONDecodeError as exc:
print(f"error: cannot parse {SETTINGS_PATH}: {exc}", file=sys.stderr)
return 1
changed = False
for path in SORTED_ARRAYS:
arr = _resolve(data, path)
if not isinstance(arr, list):
continue
ordered = sorted(arr)
if ordered != arr:
arr[:] = ordered
changed = True
print(f"sorted: {'.'.join(path)} ({len(arr)} entries)")
if not changed:
print(f"{SETTINGS_PATH.relative_to(PROJECT_ROOT)} already sorted.")
return 0
SETTINGS_PATH.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
print(f"updated {SETTINGS_PATH.relative_to(PROJECT_ROOT)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())