Ticket graphs

Every ticket as the root of its provenance graph — goal, patch, approval, runs and (as the ticket-class build lands) mandate, manifestations, guarantees, journey, artifacts and children. Rendered through the <<<TicketGraphList>>> tag.

← all ticket-graphs

DONE#363 forge-refactor

Rewrite code__code__jazz__kokoro__dz_debug:dz_debug/log.py as behavior-preserving, installable, control-config'd Python [forge-rw:4c486357e89341dd]

goal

THIS UNIT SUPERSEDES #300 — it is the SAME module and the same work, re-filed to be SELF-CONTAINED. The superseded unit named its source only as a `git cat-file` command against the forge's bare repos, which the sandboxed pair_loop worker that executes these tickets cannot run, so it could not be done as filed (#346; #259 is the proof). A ticket body is permanent in SQL (tg_tickets_immutable_identity, migrations/0029_ticket_authorization .sql:111-117), so that body could not be repaired — this ticket carries the fix instead, and the original is halted by a stop ticket rather than erased. Do the work HERE. Rewrite ONE unique forge module as entirely new, behavior-preserving, installable, control-config'd Python, per the FORGE-wide refactor standard #200 (#std). This ticket covers ONE module — the one identified below — and nothing else. It was filed against the module's DEDUPLICATED identity, so if that module was copy-pasted across the forge, every copy is covered by this single ticket. MODULE KEY : forge-rw:4c486357e89341dd (the ticketer's idempotency marker — it lives in this ticket's TITLE. Do not edit the title, or the module will be re-filed as a duplicate.) IDENTITY : AST-NORMALIZED — codebean's `_ast_norm` (#183), i.e. ast.dump(ast.parse(ast.unparse(ast.parse(src)))). Copies of this module that differ only in comments, blank lines, quoting or spacing carry the SAME module-sha and are covered by THIS ticket. A differing docstring is real content, so a module whose docstring differs is a DIFFERENT module with its own ticket. SIZE : 8868 bytes EXTENSION : .py CANONICAL SOURCE (the location this ticket is named for): repo : code__code__jazz__kokoro__dz_debug path : dz_debug/log.py ref : refs/heads/main (as recorded when this module was first filed) blob : faeb483cdcf3d7d38aeeeab79ac61f135390eb0c (git blob sha1) REFERENCE ONLY — you do NOT need this command. This module's bytes are embedded COMPLETE in THE ORIGINAL SOURCE block below, and that is what the rewrite is based on. For a reader who does have forge access: git --git-dir=/Users/stevenpeterson/code/installed/jazz-project/config/forge/remotes/code__code__jazz__kokoro__dz_debug.git cat-file blob faeb483cdcf3d7d38aeeeab79ac61f135390eb0c THE SAME MODULE ALSO LIVES AT THESE FORGE TIPS — 6 copy/copies. They SHARE THIS ticket: this module is rewritten ONCE, and every location below is satisfied by that one rewrite. Do not file a ticket per copy. code__code__jazz__litellm__dz_debug:dz_debug/log.py code__code__jazz__notes__dz_debug:dz_debug/log.py dz_debug:dz_debug/log.py gitlab__testmonkeyalpha-group__jazz-forge__dz_debug:dz_debug/log.py gitlab__testmonkeyalpha__dz_debug:dz_debug/log.py testmonkeyalpha-group__jazz-forge__dz_debug:dz_debug/log.py HISTORICAL OCCURRENCES of this exact blob — every OTHER DISTINCT {repo, path} the omni-git index (`occ`, joined on the git blob sha1) records this byte-identical file at, with the index's own `ts` for the first row of each. Any place this run enumerated LIVE is listed under the forge tips above, not here. Context for the rewrite, not extra work: testmonkeyalpha/dz_debug:dz_debug/log.py [occ.ts gitlab] THE ORIGINAL SOURCE, EMBEDDED — YOU NEED NO FORGE ACCESS TO DO THIS TICKET. The module's own bytes are reproduced below, inside this ticket. THEY are the authoritative copy and the behavior-preserving basis: rewrite from them. The `git --git-dir=... cat-file blob` command under CANONICAL SOURCE above is kept only as a REFERENCE for a reader who happens to have forge access — running it is NOT part of this ticket. (Why the bytes are here at all: the pair_loop worker that executes this ticket is sandboxed to the ticket's own congruency worktree and cannot read the forge's bare repos. Ticket #346 is the fix that put them on the ticket.) The block BEGINS at the line ===ORIGINAL SOURCE (verbatim, the behavior-preserving basis)=== and ENDS at the first line ===END SOURCE=== Everything strictly between those two lines is the module — 8868 byte(s), byte for byte, with no re-indentation, no re-wrapping and nothing elided. ===ORIGINAL SOURCE (verbatim, the behavior-preserving basis)=== # SPDX-License-Identifier: Apache-2.0 # Copyright 2026 testmonkeyalpha — Claude & Co. """ Service logger — writes timestamped lines to {log_root}/{build_id}/{service}.log Behaviour is controlled by the CONFIG block below. Set `profile` for the baseline, then flip individual channel booleans as needed. """ import json import sys import traceback from datetime import datetime, timezone from pathlib import Path # ── Profiles ─────────────────────────────────────────────────────────────────── _PROFILES = { "none": {"min_level": "ERROR"}, "standard": {"min_level": "INFO"}, "debug": {"min_level": "DEBUG"}, "trace": {"min_level": "DEBUG"}, } # Which channels are on by default per profile _PROFILE_CHANNELS = { "none": {"payload": False, "perf": False, "process": False, "health": False, "ipc": False, "terminal": False}, "standard": {"payload": False, "perf": False, "process": True, "health": False, "ipc": True, "terminal": False}, "debug": {"payload": False, "perf": True, "process": True, "health": True, "ipc": True, "terminal": True}, "trace": {"payload": True, "perf": True, "process": True, "health": True, "ipc": True, "terminal": True}, } # ── Configuration ────────────────────────────────────────────────────────────── CONFIG = { # Named profile — sets baseline level and channel defaults # One of: "none", "standard", "debug", "trace" "profile": "standard", # Channel overrides — True/False flips a channel regardless of profile. # Set to None to let the profile decide. "payload": None, "perf": None, "process": None, "health": None, "ipc": None, "terminal": None, # Mirror every log line to stderr "mirror_to_stderr": True, # Root directory for log files "log_root": Path.home() / "code" / "run" / "logs", # Where to find build.json to auto-resolve build_id "build_json": Path.home() / "code" / "run" / "clean" / "conf" / "env" / "build.json", # Timestamp format "timestamp_format": "%Y-%m-%dT%H:%M:%S", # fatal(): how many characters of the exception to print to stderr before exit "fatal_preview_chars": 500, # Exit code used by fatal() "fatal_exit_code": 1, # Capture third-party stdlib logging (uvicorn, litellm, fastapi) into our log file. # NOTE: requires touching Python's logging.Handler API — see _attach_stdlib_capture. "capture_stdlib_logging": False, "capture_stdlib_loggers": ["uvicorn", "litellm", "fastapi"], } # ── Level ordering ───────────────────────────────────────────────────────────── _LEVELS = {"DEBUG": 0, "TERM ": 0, "INFO": 1, "WARN": 2, "ERROR": 3} # ── Helpers ──────────────────────────────────────────────────────────────────── def _resolve_build_id(build_json: Path) -> str: if build_json.exists(): return json.loads(build_json.read_text()).get("build_id", "unknown") return "unknown" def _timestamp(fmt: str) -> str: now = datetime.now(timezone.utc) ms = f"{now.microsecond // 1000:03d}" return f"{now.strftime(fmt)}.{ms}Z" def _resolve_channels(profile: str, overrides: dict) -> dict: channels = dict(_PROFILE_CHANNELS[profile]) for ch, val in overrides.items(): if val is not None: channels[ch] = val return channels # ── Logger ───────────────────────────────────────────────────────────────────── class Logger: """ File logger scoped to a service and build ID. Usage: log = Logger("kokoro") log.info("server started on port 4001") log.debug("query took 12ms", tags=["perf"]) log.debug("raw text payload", tags=["payload"]) log.term("raw subprocess output line") log.error("model failed", exc=e) log.fatal("cannot continue", exc=e) Override profile or channels at instantiation: log = Logger("kokoro", config={"profile": "debug", "payload": True}) """ def __init__(self, service: str, build_id: str = None, config: dict = None): cfg = {**CONFIG, **(config or {})} profile = cfg.get("profile", "standard") if profile not in _PROFILES: raise ValueError(f"Unknown profile '{profile}'. Choose: {list(_PROFILES)}") if build_id is None: build_id = _resolve_build_id(cfg["build_json"]) self._service = service self._build_id = build_id self._cfg = cfg self._min_level = _LEVELS[_PROFILES[profile]["min_level"]] self._channels = _resolve_channels(profile, { ch: cfg.get(ch) for ch in ("payload", "perf", "process", "health", "ipc", "terminal") }) log_dir = Path(cfg["log_root"]) / build_id log_dir.mkdir(parents=True, exist_ok=True) self._path = log_dir / f"{service}.log" self._file = self._path.open("a", buffering=1) if cfg.get("capture_stdlib_logging"): self._attach_stdlib_capture(cfg.get("capture_stdlib_loggers", [])) # ── Public interface ─────────────────────────────────────────────── def info(self, msg: str, tags: list = None): self._write("INFO", msg, tags) def warn(self, msg: str, tags: list = None): self._write("WARN", msg, tags) def error(self, msg: str, exc: Exception = None, tags: list = None): self._write("ERROR", msg, tags) if exc: self._write("ERROR", traceback.format_exc().strip(), tags) def debug(self, msg: str, tags: list = None): self._write("DEBUG", msg, tags) def term(self, msg: str): """Log raw terminal/subprocess output. Controlled by the terminal channel.""" self._write("TERM ", msg, tags=["terminal"]) def fatal(self, msg: str, exc: Exception = None): full = msg if exc: full += "\n" + traceback.format_exc().strip() self._write("ERROR", full) preview = full[:self._cfg.get("fatal_preview_chars", 500)] print(f"\nFATAL [{self._service}]: {preview}\n", file=sys.stderr) print(f"Full log: {self._path}", file=sys.stderr) self.close() sys.exit(self._cfg.get("fatal_exit_code", 1)) @property def path(self) -> Path: return self._path @property def channels(self) -> dict: return dict(self._channels) def close(self): self._file.close() # ── Private ──────────────────────────────────────────────────────── def _write(self, level: str, msg: str, tags: list = None): if _LEVELS.get(level, 0) < self._min_level: return if tags: if not any(self._channels.get(t, False) for t in tags): return line = f"{_timestamp(self._cfg['timestamp_format'])} [{self._service}] {level} {msg}\n" self._file.write(line) if self._cfg.get("mirror_to_stderr"): print(line, end="", file=sys.stderr) def _attach_stdlib_capture(self, logger_names: list): # NOTE: This is the one place we must touch the stdlib `logging` API. # uvicorn, litellm, and fastapi call logging.getLogger(__name__) internally # and can only be intercepted by registering a logging.Handler on those loggers. import logging writer = self._write class _Bridge(logging.Handler): _MAP = { logging.DEBUG: "DEBUG", logging.INFO: "INFO", logging.WARNING: "WARN", logging.ERROR: "ERROR", logging.CRITICAL: "ERROR", } def emit(self, record: logging.LogRecord): writer(self._MAP.get(record.levelno, "INFO"), self.format(record)) handler = _Bridge() handler.setFormatter(logging.Formatter("%(name)s: %(message)s")) targets = logger_names if logger_names else [None] for name in targets: logging.getLogger(name).addHandler(handler) ===END SOURCE=== THE CALLABLE SURFACE — every callable in the original, lowered with codebean's `lower_node` (#183). The rewrite must present THIS surface (same names, same parameters, same defaults) so callers of the original keep working: _resolve_build_id(build_json: Path) -> str _timestamp(fmt: str) -> str _resolve_channels(profile: str, overrides: dict) -> dict Logger.__init__(self, service: str, build_id: str=None, config: dict=None) Logger.info(self, msg: str, tags: list=None) Logger.warn(self, msg: str, tags: list=None) Logger.error(self, msg: str, exc: Exception=None, tags: list=None) Logger.debug(self, msg: str, tags: list=None) Logger.term(self, msg: str) Logger.fatal(self, msg: str, exc: Exception=None) Logger.path(self) -> Path Logger.channels(self) -> dict Logger.close(self) Logger._write(self, level: str, msg: str, tags: list=None) Logger._attach_stdlib_capture(self, logger_names: list) CAPTURED BEHAVIORAL EXEMPLARS: none. no function in this module is minable — criteria_miner mines only self-contained pure functions (it refuses anything that mentions a world-touching or non-deterministic name, plus methods and no-arg functions), and nothing here qualified This does NOT weaken the ticket's bar — it MOVES the work: you must write the equivalence exemplars by hand from the original's behavior before rewriting, and they are what settles this ticket. THE STANDARD — #200's four requirements, which this ticket exists to enforce. All four are the standard's own words; none is optional: 1. BEHAVIOR-PRESERVING, VERIFIED NOT ASSERTED — the deliverable is ENTIRELY NEW source code (rewritten, not copied). It must reproduce the ORIGINAL's behavior, and the proof is the exemplars above (captured via codebean lower_* #183 + criteria_miner IO-exemplars, exactly as #200 specifies). 2. A PYTHON MODULE — regardless of the source language. This module's source is `.py`; the rewrite is Python either way. 3. INSTALLABLE — a proper package: pyproject.toml, `pip install -e .` works, console entry points where applicable. 4. CONTROL-CONFIG'D — NO hardcoded paths or params. A JSON/registry config object drives it (the install.json / registry.json idiom of this repo), and per note-for-claude a tool that cannot see its config THROWS rather than guessing a default. ACCEPTANCE (#200's, per rewrite ticket) — this ticket settles when ALL of: a. the new source reproduces every captured exemplar above, plus the edge cases you add for what sampling cannot reach (paste the runs as evidence); b. it presents the callable surface above, so existing callers keep working; c. `pip install -e .` installs it clean from its own pyproject.toml; d. it runs off a config object with ZERO hardcoded settings, and throws without one. Kind is `build` (a pair: coder + adversarial tester). This ticket hangs off #200 by a `spawned` edge, so a stop ticket aimed at #200 brakes every rewrite in this programme. Filed mechanically by checkouts/current/congruency/tools/forge_refactor_ticketer.py under ticket #201, from the forge bare repos at /Users/stevenpeterson/code/installed/jazz-project/config/forge/remotes. Its original source is embedded above (#346), so this ticket is complete on its own.

patch

none

approval

unapproved

runs

no runs recorded

mandate (clauses)

not yet recorded — lands with the ticket-class build

manifestations

not yet recorded — lands with the ticket-class build

guarantees

not yet recorded — lands with the ticket-class build

journey (blunders & successes)

not yet recorded — lands with the ticket-class build

artifacts (forge)

not yet recorded — lands with the ticket-class build

source (forge tree)

browse congruency source (forge-parked tree)

children

#300 Rewrite code__code__jazz__kokoro__dz_debug:dz_debug/log.py as behavior-preserving, installable, control-config'd Python [forge-rw:4c486357e89341dd] (supersedes · REQUESTED)