"""One structured record per invocation (D-263). **This is not a logging subsystem.** It is a decorator and an event kind. The record goes out through `core/console` like everything else, because D-263 names console the single output path and two sinks would drift — in format, in destination, in level handling — with the second always being the one nobody remembers to configure. Quiet by default. The record is a `debug` event, so the push hook's output looks exactly as it does today and `--verbose` is what surfaces it. A gate that suddenly printed a line per check would train people to stop reading gate output, which is worse than having no record at all. """ from __future__ import annotations import functools import time from collections.abc import Callable from typing import Any, TypeVar from tooling.core import console F = TypeVar("F", bound=Callable[..., Any]) # Values that should never appear in a log line even at debug level. Repo # tooling is not handling credentials today, but the cost of the guard is one # frozenset and the cost of discovering it was needed is a leaked secret. _REDACT = frozenset({"password", "token", "secret", "api_key", "apikey"}) def logged(func: F) -> F: """Emit command, arguments, duration and outcome for one invocation. Records the outcome in a `finally`, so a command that raises is still reported — with the exception type as its outcome rather than silence. """ @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: started = time.monotonic() outcome = "ok" try: return func(*args, **kwargs) except BaseException as exc: outcome = type(exc).__name__ raise finally: console.event( f"{func.__name__} {outcome}", level="debug", command=func.__name__, args=_safe(kwargs), duration_ms=round((time.monotonic() - started) * 1000, 1), outcome=outcome, ) return wrapper # type: ignore[return-value] def _safe(kwargs: dict[str, Any]) -> dict[str, Any]: """Argument values, with anything secret-shaped replaced.""" return { key: ("***" if key.lower() in _REDACT else value) for key, value in kwargs.items() }