oct.git.audit module#

Purpose#

Audit JSONL logger and AI-Trace decorator for Phase 4B oct git commands. Writes one JSON object per line to <project>/logs/git-audit-YYYYMMDD-HHMMSS.jsonl with size + count rotation, and provides the audited() decorator that every oct git Click command is wrapped in so that entry/exit trace events and a persistent audit row are recorded even if the command crashes.

Responsibilities#

  • Define AuditRecord — the exact on-disk schema (blueprint §12.2, verbatim).

  • Implement AuditLogger with size-based rotation (5 MB) and count-based pruning (keep the 20 newest files).

  • Redact credentials from any string value written to the log by deferring to oct.core.git._redact_git_url().

  • Emit AI-Trace shadow events via _dbg("GIT", msg, 4) — file-only, off by default; see blueprint §12.3 and Phase 4B design decision D7.

Diagnostics#

Domain: GIT Levels:

L1 — errors: rotation IO failures, directory-create failures L2 — lifecycle: file rotated, files pruned L4 — deep trace: every write() call and every command start/end

Contracts#

  • This module does not import click, does not shell out, and does not touch any git state. It only reads config-less arguments passed in from the caller.

  • AuditLogger.write() must never raise. On any IO failure it logs an error via _dbg and returns silently — the audit log is observability, not a correctness path.

  • Filenames follow git-audit-YYYYMMDD-HHMMSS.jsonl strictly so that lexical ordering matches chronological ordering for pruning.

  • Every AuditRecord field is JSON-serialisable. args is always passed through credential redaction before serialisation.

oct.git.audit.AUDIT_MAX_FILES: int = 20#

Maximum number of git-audit-*.jsonl files kept on disk. When the 21st file is created by a rotation event, the oldest is deleted.

oct.git.audit.AUDIT_MAX_SIZE_BYTES: int = 5242880#

Rotation threshold. When the current audit file reaches this size in bytes, the next AuditLogger.write() call closes it and opens a new timestamped file.

class oct.git.audit.AuditLogger(project_root: Path)[source]#

Bases: object

Append-only rotating JSONL logger for oct git commands.

One instance corresponds to one project root. The constructor does not open a file — the first write() call lazily materialises the logs/ directory and the current audit file. This lets a command that never reaches the write stage (e.g. a CLI parse error) leave no side-effects on disk.

Rotation strategy#

  • Size rotation: before each write, if the current file is at or above AUDIT_MAX_SIZE_BYTES, close it and open a new timestamped file. The rotating write itself goes into the new file, not the old one.

  • Count pruning: after any rotation, if the number of git-audit-*.jsonl files in the logs directory exceeds AUDIT_MAX_FILES, delete the oldest (lexical order) until the count is within the limit.

Error handling#

Any IO error during write() — creating the directory, opening the file, serialising the record, writing the line, or rotating — is caught and logged via _dbg("GIT", ..., 1). The method never raises; audit logging is best-effort observability and must not mask command failures.

property current_path: Path | None#

Current audit filename (None before first write).

write(record: AuditRecord) None[source]#

Serialise record and append it as one JSONL line.

Never raises. On IO failure, logs an error via _dbg at level 1 and returns. The caller is never blocked by audit log misbehaviour.

class oct.git.audit.AuditRecord(timestamp: str = '', command: str = '', args: list[str] = <factory>, branch: str | None = None, staged_files: int = 0, checks_passed: bool = False, secrets_detected: bool = False, lint_violations: int = 0, format_violations: int = 0, exit_code: int | None = None, duration_ms: int = 0, per_subproject_results: list[dict] = <factory>)[source]#

Bases: object

Schema for one audit row. Serialised via dataclasses.asdict().

Every field is either JSON-primitive or a list/dict of JSON-primitives so that json.dumps(asdict(record)) is guaranteed to succeed. args is credential-redacted by the caller before being assigned (see _redact_args()).

Fields whose value is unknown at record-construction time may be left at their default; missing fields are emitted as null in the JSON output.

args: list[str]#
branch: str | None = None#
checks_passed: bool = False#
command: str = ''#
duration_ms: int = 0#
exit_code: int | None = None#
format_violations: int = 0#
lint_violations: int = 0#
per_subproject_results: list[dict]#

per-subproject quality-gate results when oct git commit ran a workspace fan-out. Empty list when fan-out was not used. Each entry: {name, profile, files, lint_violations, format_violations, secrets}.

Type:

F1.3

secrets_detected: bool = False#
staged_files: int = 0#
timestamp: str = ''#
oct.git.audit.audited(cmd_name: str) Callable[[Callable[[...], Any]], Callable[[...], Any]][source]#

Decorator that wraps an oct git Click command.

Responsibilities:

  1. Emit _dbg("GIT", "command_start ...", 4) before the command.

  2. Run the wrapped function.

  3. In a finally block, emit _dbg("GIT", "command_end ...", 4) and write an AuditRecord into the project’s audit log.

The wrapped command is responsible for populating ctx.obj["_audit_record"] during its run — the decorator reads that slot at the end and writes whatever is there, falling back to a minimal record (with exit_code=None and checks_passed=False) if the command crashed before populating it. This guarantees every command invocation leaves exactly one audit row.

Parameters:

cmd_name – Human-readable command identifier for the trace and audit row, e.g. "oct git check".