Track the editor config, local Claude permissions and the maintenance scripts

.editorconfig fixes indentation and line endings for every editor. The
scripts/ helpers (database import checks, the monolith guard, and the two
refactor migrations) were only ever local.
This commit is contained in:
retoor 2026-07-26 16:05:17 +02:00
parent f996336afb
commit b8277d6351
6 changed files with 334 additions and 0 deletions

View File

@ -0,0 +1,20 @@
{
"permissions": {
"allow": [
"Bash(python *)",
"Bash(DEVPLACE_DISABLE_SERVICES=1 python -)",
"Bash(command -v hawk)",
"Bash(export DEVPLACE_DISABLE_SERVICES=1)",
"Bash(export DEVPLACE_DATABASE_URL=\"sqlite:///tmp/devplace_verify.db\")",
"Bash(rm -f /tmp/devplace_verify.db)",
"Bash(export DEVPLACE_DATABASE_URL=\"sqlite:////tmp/devplace_verify.db\")",
"Bash",
"Edit(/home/retoor/projects/devplacepy/devplacepy/routers/projects/containers/instances.py)",
"Edit(/home/retoor/projects/devplacepy/devplacepy/static/js/components/ContainerTerminal.js)",
"Edit(/home/retoor/projects/devplacepy/devplacepy/services/containers/store.py)",
"Verify",
"Edit(/home/retoor/projects/devplacepy/devplacepy/static/js/MessagesLayout.js)",
"Write(/home/retoor/projects/devplacepy/devplacepy/static/css/messages.css)"
]
}
}

12
.editorconfig Normal file
View File

@ -0,0 +1,12 @@
root = true
[*]
indent_style = space
indent_size = 4
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false

View File

@ -0,0 +1,65 @@
#!/usr/bin/env python3
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
ALLOW_PREFIXES = (
ROOT / "devplacepy" / "database",
ROOT / "devplacepy" / "db_client.py",
ROOT / "devplacepy_services" / "database",
ROOT / "scripts" / "migrate_db_imports.py",
ROOT / "scripts" / "check_database_imports.py",
)
PATTERNS = [
re.compile(r"from devplacepy\.database(?:\.[a-z_]+)? import"),
re.compile(r"import devplacepy\.database\b"),
]
# Only flagged outside tests/: unit tests may legitimately use `local_db` /
# `from devplacepy import database` directly (Section 13.4). Source code must
# always go through `devplacepy.db_client` so the remote broker patch applies.
SOURCE_ONLY_PATTERNS = [
re.compile(r"from devplacepy import(?:\s+\w+,)*\s+database\b"),
]
def allowed(path: Path) -> bool:
for prefix in ALLOW_PREFIXES:
if path == prefix or str(path).startswith(str(prefix) + "/"):
return True
return False
def main() -> int:
errors: list[str] = []
for pattern in ("devplacepy/**/*.py", "devplacepy_services/**/*.py", "tests/**/*.py"):
is_source = not pattern.startswith("tests/")
for path in ROOT.glob(pattern):
if not path.is_file() or allowed(path):
continue
try:
text = path.read_text(encoding="utf-8", errors="ignore")
except OSError:
continue
active_patterns = PATTERNS + SOURCE_ONLY_PATTERNS if is_source else PATTERNS
for lineno, line in enumerate(text.splitlines(), 1):
for rx in active_patterns:
if rx.search(line):
errors.append(f"{path.relative_to(ROOT)}:{lineno}: {line.strip()}")
break
if errors:
print("FORBIDDEN database imports (use devplacepy.db_client):")
for line in sorted(errors)[:80]:
print(f" {line}")
if len(errors) > 80:
print(f" ... and {len(errors) - 80} more")
return 1
print("check_database_imports: ok")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -0,0 +1,85 @@
#!/usr/bin/env python3
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
ALLOWLIST = {
ROOT / "splitup.md",
ROOT / "scripts/check_no_monolith.py",
}
PATTERNS = [
(r"uvicorn\s+devplacepy\.main:app", "uvicorn monolith boot"),
(r"from devplacepy\.main import", "main.py import"),
(r"import devplacepy\.main", "main.py import"),
(r"devplacepy\.main:app", "main app string"),
(r"DEVPLACE_DB_MODE", "hybrid DB mode flag"),
(r"make dev-monolith", "dev-monolith target"),
(r"make prod-monolith", "prod-monolith target"),
(r"DEVPLACE_TEST_PORT=10501", "legacy test port default"),
(r"monolith-dev", "monolith port profile"),
]
PHASE = sys.argv[1] if len(sys.argv) > 1 else "m7"
def scan_file(path: Path) -> list[str]:
if path in ALLOWLIST:
return []
try:
text = path.read_text(encoding="utf-8", errors="ignore")
except OSError:
return []
hits = []
for pattern, label in PATTERNS:
if re.search(pattern, text):
hits.append(f"{path.relative_to(ROOT)}: {label}")
return hits
def main() -> int:
import subprocess
db_check = subprocess.run(
[sys.executable, str(ROOT / "scripts" / "check_database_imports.py")],
capture_output=True,
text=True,
check=False,
)
if db_check.returncode != 0:
print(db_check.stdout or db_check.stderr)
return 1
globs = [
"Makefile",
"Dockerfile",
"docker-compose*.yml",
".gitea/**/*.yaml",
"CLAUDE.md",
"README.md",
"pyproject.toml",
"devplacepy/**/*.py",
"tests/**/*.py",
"orchestrator.py",
]
errors = []
for pattern in globs:
for path in ROOT.glob(pattern):
if path.is_file():
errors.extend(scan_file(path))
if PHASE in ("m3", "m6", "m7") and (ROOT / "devplacepy/main.py").exists():
if PHASE == "m7":
errors.append("devplacepy/main.py: file must be deleted at M7")
elif PHASE == "m3":
pass
if errors:
print("MONOLITH ARTIFACTS FOUND:")
for line in sorted(set(errors)):
print(f" {line}")
return 1
print("check_no_monolith: ok")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -0,0 +1,60 @@
#!/usr/bin/env python3
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SKIP_PREFIXES = (
ROOT / "devplacepy" / "database",
ROOT / "devplacepy_services" / "database",
ROOT / "devplacepy" / "db_client.py",
ROOT / "scripts",
ROOT / "splitup.md",
)
REPLACEMENTS = [
(re.compile(r"\bfrom devplacepy\.database import\b"), "from devplacepy.db_client import"),
(re.compile(r"\bfrom devplacepy\.database\.([a-z_]+) import\b"), r"from devplacepy.db_client import"),
(re.compile(r"\bimport devplacepy\.database\b"), "import devplacepy.db_client"),
]
def should_skip(path: Path) -> bool:
for prefix in SKIP_PREFIXES:
if path == prefix or str(path).startswith(str(prefix) + "/"):
return True
return False
def migrate_file(path: Path) -> bool:
text = path.read_text(encoding="utf-8")
original = text
for pattern, repl in REPLACEMENTS:
text = pattern.sub(repl, text)
if text != original:
path.write_text(text, encoding="utf-8")
return True
return False
def main() -> int:
changed = []
for pattern in ("devplacepy/**/*.py", "tests/**/*.py", "devplacepy_services/**/*.py"):
for path in ROOT.glob(pattern):
if not path.is_file() or should_skip(path):
continue
if migrate_file(path):
changed.append(path.relative_to(ROOT))
if changed:
print(f"migrated {len(changed)} files")
for item in sorted(changed)[:40]:
print(f" {item}")
if len(changed) > 40:
print(f" ... and {len(changed) - 40} more")
else:
print("no files changed")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -0,0 +1,92 @@
#!/usr/bin/env python3
import argparse
import importlib
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SERVICES_DIR = ROOT / "devplacepy_services"
SERVICES = [
"web",
"database",
"pubsub",
"gateway",
"jobs",
"devii",
"bot",
"backup",
"containers",
"telegram",
"email",
"news",
"gitea",
"audit",
"xmlrpc",
]
def list_services() -> int:
for name in SERVICES:
main_py = SERVICES_DIR / name / "main.py"
status = "ok" if main_py.exists() else "missing"
print(f"{name:12} {status}")
return 0
def verify_stubs() -> int:
sys.path.insert(0, str(ROOT))
errors: list[str] = []
for name in SERVICES:
main_py = SERVICES_DIR / name / "main.py"
if not main_py.exists():
errors.append(f"{name}: missing main.py")
continue
module_name = f"devplacepy_services.{name}.main"
try:
module = importlib.import_module(module_name)
except Exception as exc:
errors.append(f"{name}: import failed ({exc})")
continue
app = getattr(module, "app", None)
if app is None:
errors.append(f"{name}: no app export")
continue
try:
from fastapi.testclient import TestClient
client = TestClient(app)
resp = client.get("/health")
if resp.status_code != 200:
errors.append(f"{name}: /health returned {resp.status_code}")
continue
data = resp.json()
for key in ("service", "status", "uptime_s", "version", "deps", "stats"):
if key not in data:
errors.append(f"{name}: health missing key {key}")
except Exception as exc:
errors.append(f"{name}: health check failed ({exc})")
if errors:
print("verify: failed")
for err in errors:
print(f" {err}")
return 1
print("verify: ok")
return 0
def main() -> int:
parser = argparse.ArgumentParser(prog="refactor_to_microservices.py")
parser.add_argument("--list", action="store_true")
parser.add_argument("--verify", action="store_true")
args = parser.parse_args()
if args.list:
return list_services()
if args.verify:
return verify_stubs()
parser.print_help()
return 0
if __name__ == "__main__":
raise SystemExit(main())