oct
Loading...
Searching...
No Matches
oct_audit.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oct/audit/oct_audit.py
4
5"""
6Purpose
7-------
8FS-539 / FS-501 (stub) - ``oct audit`` reads ``oc_status.json`` and
9reports the project's Option C compliance stage.
10
11This is the minimal landing surface that ``CLAUDE.md``'s compliance
12probe and CI scripts consume. The full audit suite (schema drift,
13scaffold age, doc/version mismatch, MCP readiness) lands in Phase 6.
14
15Responsibilities
16----------------
17- Locate the project root and read ``oc_status.json`` from the
18 ``.option_c/`` directory when one exists.
19- Map the recorded ``stage`` onto the process exit code below, so a CI
20 script or the ``CLAUDE.md`` compliance probe can branch on it without
21 parsing text.
22- Report the stage in human-readable form, or as JSON under
23 ``--json-output`` for machine consumers.
24- Treat an absent ``oc_status.json`` as a distinct outcome (exit 3)
25 rather than an error: an unmigrated project is a legitimate state.
26
27Exit codes
28----------
29 0 - compliant
30 1 - migrating
31 2 - bootstrap
32 3 - no oc_status.json found (unmigrated project)
33
34Diagnostics
35-----------
36Domain: AUDIT
37Levels:
38 L1 - errors: none; an unreadable or absent status file is exit 3
39 L2 - lifecycle: audit entry and the resolved stage
40 L3 - semantic: the status path probed and the stage read from it
41 L4 - deep tracing: the raw status payload
42
43Contracts
44---------
45- Read-only against the project tree; never writes.
46- Exit code is derived from ``oc_status.json`` ``stage`` (or 3 when absent).
47
48Dependencies
49------------
50- oct.core.diagnostics (_dbg structured logger)
51- oct.core.option_c_dir (load_oc_status, option_c_dir)
52- oct.core.project_root (project root detection)
53- click (CLI framework)
54"""
55
56from __future__ import annotations
57
58import json
59from dataclasses import asdict, dataclass
60from pathlib import Path
61
62import click
63
64from oct.core.diagnostics import _dbg
65from oct.core.option_c_dir import load_oc_status, option_c_dir
66from oct.core.project_root import get_project_root
67
68
69_STAGE_EXIT_CODES: dict[str, int] = {
70 "compliant": 0,
71 "migrating": 1,
72 "bootstrap": 2,
73}
74
75
76@dataclass
78 """Structured output from :func:`run_audit`."""
79
80 stage: str | None
81 pillars: dict
82 migrated_from: dict
83 oct_version_at_migration: str
84 exit_code: int
85
86
87def run_audit(project_root: Path) -> AuditResult:
88 """Read ``oc_status.json`` and return structured audit information."""
89 func = "run_audit"
90 _dbg("AUDIT", func, f"start root={project_root}", 2)
91
92 status = load_oc_status(project_root)
93 if status is None:
94 has_dotdir = option_c_dir(project_root).is_dir()
95 _dbg(
96 "AUDIT", func,
97 f"no oc_status.json; .option_c exists={has_dotdir}",
98 2,
99 )
100 return AuditResult(
101 stage=None,
102 pillars={},
103 migrated_from={},
104 oct_version_at_migration="",
105 exit_code=3,
106 )
107
108 exit_code = _STAGE_EXIT_CODES.get(status.stage, 3)
109 _dbg("AUDIT", func, f"end stage={status.stage} exit={exit_code}", 2)
110
111 return AuditResult(
112 stage=status.stage,
113 pillars=status.pillars,
114 migrated_from=status.migrated_from,
115 oct_version_at_migration=status.oct_version_at_migration,
116 exit_code=exit_code,
117 )
118
119
120# -- Click surface -------------------------------------------------------
121
122@click.command()
123@click.option("--json-output", "as_json", is_flag=True, help="Emit JSON instead of human-readable text.")
124@click.pass_context
125def audit(ctx: click.Context, as_json: bool) -> None:
126 """Report Option C compliance stage (stub for FS-501)."""
127 project_root = get_project_root(ctx)
128 result = run_audit(project_root)
129
130 if as_json:
131 payload = {
132 "stage": result.stage,
133 "pillars": result.pillars,
134 "migrated_from": result.migrated_from,
135 "oct_version_at_migration": result.oct_version_at_migration,
136 "exit_code": result.exit_code,
137 }
138 click.echo(json.dumps(payload, indent=2))
139 else:
140 if result.stage is None:
141 click.echo("Stage: unmigrated (no .option_c/oc_status.json)")
142 else:
143 click.echo(f"Stage: {result.stage}")
144 for name, info in result.pillars.items():
145 enabled = info.get("enabled", False)
146 marker = "+" if enabled else "-"
147 click.echo(f" [{marker}] {name}")
148 if result.migrated_from:
149 click.echo("Migrated from:")
150 for label, src in result.migrated_from.items():
151 click.echo(f" {label}: {src}")
152
153 raise SystemExit(result.exit_code)
AuditResult run_audit(Path project_root)
Definition oct_audit.py:87