Option C Tools
Loading...
Searching...
No Matches
lint_baseline.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oct/linter/lint_baseline.py
4
5"""
6Purpose
7-------
8FS-508 — lint baseline: snapshot current violations so subsequent runs
9can report only new vs resolved issues.
10
11Responsibilities
12----------------
13- Save a baseline from lint results to ``.option_c/cache/lint-baseline.json``.
14- Load an existing baseline from disk.
15- Diff current results against a baseline to find new and resolved violations.
16
17Diagnostics
18-----------
19Domain: OCT-LINTER
20Levels:
21 L3 — baseline save/load/diff
22
23Contracts
24---------
25- Baseline file is JSON with ``_schema_version``, ``entries``, metadata.
26- Corrupt or missing file returns ``None`` (no crash).
27- Violations are keyed by ``(path, rule)`` for diff stability.
28"""
29
30from __future__ import annotations
31
32import json
33from dataclasses import dataclass, field, asdict
34from datetime import datetime
35from pathlib import Path
36
37from oct import __version__
38
39
40_SCHEMA_VERSION = "1.0"
41
42
43@dataclass
45 """One baselined violation."""
46
47 path: str
48 rule: str
49 message: str
50
51
52@dataclass
54 """A full baseline snapshot."""
55
56 entries: list[BaselineEntry]
57 oct_version: str
58 timestamp: str
59 profile: str
60
61
63 results: list[dict],
64 baseline_path: Path,
65 profile: str,
66) -> int:
67 """Extract violations from lint *results* and write to *baseline_path*.
68
69 Returns the number of violations saved.
70 """
71 entries: list[BaselineEntry] = []
72 for r in results:
73 detail = r.get("json_detail", {})
74 rel_path = detail.get("path", r.get("rel", ""))
75 checks = detail.get("checks", {})
76 for rule_name, check in checks.items():
77 if not check.get("pass", True):
78 entries.append(BaselineEntry(
79 path=rel_path,
80 rule=rule_name,
81 message=check.get("message", ""),
82 ))
83
84 baseline = LintBaseline(
85 entries=entries,
86 oct_version=__version__,
87 timestamp=datetime.now().isoformat(),
88 profile=profile,
89 )
90
91 baseline_path.parent.mkdir(parents=True, exist_ok=True)
92 baseline_path.write_text(
93 json.dumps(
94 {
95 "_schema_version": _SCHEMA_VERSION,
96 **asdict(baseline),
97 },
98 indent=2,
99 ),
100 encoding="utf-8",
101 )
102 return len(entries)
103
104
105def load_baseline(baseline_path: Path) -> LintBaseline | None:
106 """Load a baseline from disk. Returns ``None`` on missing/corrupt file."""
107 if not baseline_path.is_file():
108 return None
109 try:
110 raw = json.loads(baseline_path.read_text(encoding="utf-8"))
111 if not isinstance(raw, dict):
112 return None
113 if raw.get("_schema_version") != _SCHEMA_VERSION:
114 return None
115 entries_raw = raw.get("entries", [])
116 if not isinstance(entries_raw, list):
117 return None
118 entries = [
119 BaselineEntry(path=e["path"], rule=e["rule"], message=e["message"])
120 for e in entries_raw
121 if isinstance(e, dict) and "path" in e and "rule" in e
122 ]
123 return LintBaseline(
124 entries=entries,
125 oct_version=raw.get("oct_version", ""),
126 timestamp=raw.get("timestamp", ""),
127 profile=raw.get("profile", ""),
128 )
129 except (json.JSONDecodeError, OSError, KeyError):
130 return None
131
132
134 results: list[dict],
135 baseline: LintBaseline,
136) -> tuple[list[tuple[str, str, str]], list[tuple[str, str, str]]]:
137 """Compare current *results* against *baseline*.
138
139 Returns ``(new_violations, resolved_violations)`` where each item is
140 ``(path, rule, message)``.
141
142 A violation is identified by its ``(path, rule)`` pair. If a
143 ``(path, rule)`` appears in current results but not in the baseline,
144 it is "new". If it appears in the baseline but not in current results,
145 it is "resolved".
146 """
147 baseline_set: set[tuple[str, str]] = set()
148 baseline_msgs: dict[tuple[str, str], str] = {}
149 for entry in baseline.entries:
150 key = (entry.path, entry.rule)
151 baseline_set.add(key)
152 baseline_msgs[key] = entry.message
153
154 current_set: set[tuple[str, str]] = set()
155 current_msgs: dict[tuple[str, str], str] = {}
156 for r in results:
157 detail = r.get("json_detail", {})
158 rel_path = detail.get("path", r.get("rel", ""))
159 checks = detail.get("checks", {})
160 for rule_name, check in checks.items():
161 if not check.get("pass", True):
162 key = (rel_path, rule_name)
163 current_set.add(key)
164 current_msgs[key] = check.get("message", "")
165
166 new_keys = current_set - baseline_set
167 resolved_keys = baseline_set - current_set
168
169 new_violations = sorted(
170 [(p, r, current_msgs[(p, r)]) for p, r in new_keys],
171 )
172 resolved_violations = sorted(
173 [(p, r, baseline_msgs[(p, r)]) for p, r in resolved_keys],
174 )
175 return new_violations, resolved_violations
LintBaseline|None load_baseline(Path baseline_path)
int save_baseline(list[dict] results, Path baseline_path, str profile)
tuple[list[tuple[str, str, str]], list[tuple[str, str, str]]] diff_against_baseline(list[dict] results, LintBaseline baseline)