Option C Tools
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
15Exit codes
16----------
17 0 — compliant
18 1 — migrating
19 2 — bootstrap
20 3 — no oc_status.json found (unmigrated project)
21
22Diagnostics
23-----------
24Domain: AUDIT
25Levels:
26 L2 — lifecycle
27
28Contracts
29---------
30- Read-only against the project tree; never writes.
31- Exit code is derived from ``oc_status.json`` ``stage`` (or 3 when absent).
32
33Dependencies
34------------
35- oct.core.diagnostics (_dbg structured logger)
36- oct.core.option_c_dir (load_oc_status, option_c_dir)
37- oct.core.project_root (project root detection)
38- click (CLI framework)
39"""
40
41from __future__ import annotations
42
43import json
44from dataclasses import asdict, dataclass
45from pathlib import Path
46
47import click
48
49from oct.core.diagnostics import _dbg
50from oct.core.option_c_dir import load_oc_status, option_c_dir
51from oct.core.project_root import get_project_root
52
53
54_STAGE_EXIT_CODES: dict[str, int] = {
55 "compliant": 0,
56 "migrating": 1,
57 "bootstrap": 2,
58}
59
60
61@dataclass
63 """Structured output from :func:`run_audit`."""
64
65 stage: str | None
66 pillars: dict
67 migrated_from: dict
68 oct_version_at_migration: str
69 exit_code: int
70
71
72def run_audit(project_root: Path) -> AuditResult:
73 """Read ``oc_status.json`` and return structured audit information."""
74 func = "run_audit"
75 _dbg("AUDIT", func, f"start root={project_root}", 2)
76
77 status = load_oc_status(project_root)
78 if status is None:
79 has_dotdir = option_c_dir(project_root).is_dir()
80 _dbg(
81 "AUDIT", func,
82 f"no oc_status.json; .option_c exists={has_dotdir}",
83 2,
84 )
85 return AuditResult(
86 stage=None,
87 pillars={},
88 migrated_from={},
89 oct_version_at_migration="",
90 exit_code=3,
91 )
92
93 exit_code = _STAGE_EXIT_CODES.get(status.stage, 3)
94 _dbg("AUDIT", func, f"end stage={status.stage} exit={exit_code}", 2)
95
96 return AuditResult(
97 stage=status.stage,
98 pillars=status.pillars,
99 migrated_from=status.migrated_from,
100 oct_version_at_migration=status.oct_version_at_migration,
101 exit_code=exit_code,
102 )
103
104
105# -- Click surface -------------------------------------------------------
106
107@click.command()
108@click.option("--json-output", "as_json", is_flag=True, help="Emit JSON instead of human-readable text.")
109@click.pass_context
110def audit(ctx, as_json: bool):
111 """Report Option C compliance stage (stub for FS-501)."""
112 project_root = get_project_root(ctx)
113 result = run_audit(project_root)
114
115 if as_json:
116 payload = {
117 "stage": result.stage,
118 "pillars": result.pillars,
119 "migrated_from": result.migrated_from,
120 "oct_version_at_migration": result.oct_version_at_migration,
121 "exit_code": result.exit_code,
122 }
123 click.echo(json.dumps(payload, indent=2))
124 else:
125 if result.stage is None:
126 click.echo("Stage: unmigrated (no .option_c/oc_status.json)")
127 else:
128 click.echo(f"Stage: {result.stage}")
129 for name, info in result.pillars.items():
130 enabled = info.get("enabled", False)
131 marker = "+" if enabled else "-"
132 click.echo(f" [{marker}] {name}")
133 if result.migrated_from:
134 click.echo("Migrated from:")
135 for label, src in result.migrated_from.items():
136 click.echo(f" {label}: {src}")
137
138 raise SystemExit(result.exit_code)
AuditResult run_audit(Path project_root)
Definition oct_audit.py:72