54 """Immutable documentation record for a single lint rule.
56 The ``severity`` field (FS-C4) classifies each rule's failure mode:
58 - ``"blocking"`` (default) — a violation flips the linter exit code
59 to 1 and the quality gate to non-zero.
60 - ``"advisory"`` — a violation is reported in JSON output and logs
61 but does not affect exit codes. AI agents and CI can use this tier
62 to surface improvement opportunities without blocking merges.
63 - ``"info"`` — reserved for low-priority hints (future use).
65 The ``severity_per_profile`` map allows a rule to be ``advisory`` at
66 one profile and ``blocking`` at another (e.g. ``tests_exist`` is
67 advisory at compact, blocking at strict).
78 severity: str = SEVERITY_BLOCKING
79 severity_per_profile: dict[str, str] = field(default_factory=dict)
82RULE_REGISTRY: dict[str, RuleInfo] = {
85 label=
"File header block",
87 "Every Python file must start with a 4-line header: shebang, "
88 "encoding declaration, file-identity comment, and a blank line. "
89 "This makes files self-locating and ensures consistent encoding."
92 "#!/usr/bin/env python3\n"
93 "# -*- coding: utf-8 -*-\n"
94 "# path/to/module.py\n"
98 "Missing shebang, wrong encoding line, or file-identity path "
99 "that does not match the file's actual location in the project."
101 spec_reference=
"Spec S7 — File Identity and Self-Location",
103 profiles=[
"proto",
"compact",
"strict"],
107 label=
"Module docstring structure",
109 "Module docstrings must contain Purpose, Responsibilities, "
110 "Diagnostics, and Contracts sections so that both humans and "
111 "AI assistants can understand the module's role at a glance."
117 "Brief description of what this module does.\n\n"
120 "- Bullet list of responsibilities.\n\n"
123 "Domain: MY-DOMAIN\n\n"
126 "- Invariants and guarantees.\n"
130 "Missing one or more required sections (Purpose, "
131 "Responsibilities, Diagnostics, Contracts) or using "
132 "non-standard section headers."
134 spec_reference=
"Spec S8 — Module Docstring Specification",
136 profiles=[
"proto",
"compact",
"strict"],
139 name=
"dependencies_section",
140 label=
"Module Dependencies docstring section",
142 "At strict profile, every module docstring must include a "
143 "Dependencies section listing project-internal and notable "
144 "third-party imports as ``- module_name (purpose)``. The "
145 "machine-parsable form is what ``oct deps`` consumes to build "
146 "the project-wide dependency graph and Sphinx documentation."
151 "- oct.core.exclusions (directory exclusion lists)\n"
152 "- oc_diagnostics (structured debug logger)"
155 "Omitting the Dependencies section, missing the underline, or "
156 "writing free-form bullets without the ``(purpose)`` parenthetical."
158 spec_reference=
"Spec Appendix C — Dependencies Docstring Format",
164 severity=SEVERITY_ADVISORY,
167 name=
"diagnostics_profile",
168 label=
"Diagnostics profile block",
170 "The Diagnostics section in the module docstring must declare "
171 "a Domain and at least L2/L3/L4 level descriptions so that "
172 "debug output is traceable to a specific subsystem and severity."
177 "Domain: MY-DOMAIN\n"
179 " L2 — lifecycle events\n"
180 " L3 — semantic details\n"
184 "Omitting the Domain line, missing level descriptions, or "
185 "placing the Diagnostics block outside the module docstring."
187 spec_reference=
"Spec S11 — Diagnostics System",
189 profiles=[
"compact",
"strict"],
193 label=
"_dbg() usage in non-trivial modules",
195 "Non-trivial modules (>= 20 lines) must call _dbg() at least "
196 "once to ensure runtime diagnostics coverage. Silent modules "
197 "are invisible to the diagnostics system."
200 'from oc_diagnostics import _dbg\n\n'
201 'func = "my_function"\n'
202 '_dbg("MY-DOMAIN", func, "starting processing", 2)'
205 "Using print() instead of _dbg(), or having a non-trivial "
206 "module with no diagnostics calls at all."
208 spec_reference=
"Spec S11 — Diagnostics System, S14 — Linter Specification",
210 profiles=[
"compact",
"strict"],
214 label=
"_dbg import source",
216 "The _dbg function must be imported from oc_diagnostics, not "
217 "defined locally or imported from another location. This "
218 "ensures a single canonical diagnostics pipeline."
220 correct_pattern=
'from oc_diagnostics import _dbg',
222 "Importing _dbg from a local module, aliasing it "
223 "(import _dbg as dbg), or defining a local _dbg function."
225 spec_reference=
"Spec S11 — Diagnostics System",
227 profiles=[
"compact",
"strict"],
231 label=
'func = "..." variable pattern',
233 "Functions that call _dbg() must define a local "
234 'func = "function_name" variable so that diagnostics output '
235 "includes the originating function name without relying on "
236 "stack introspection."
239 'def process_data(items: list) -> None:\n'
240 ' func = "process_data"\n'
241 ' _dbg("MY-DOMAIN", func, "processing", 2)'
244 "Calling _dbg() without defining func first, or defining func "
245 "with a name that doesn't match the enclosing function."
247 spec_reference=
"Spec S9 — Function Structure and Helper Extraction",
249 profiles=[
"compact",
"strict"],
253 label=
"Regression test exists",
255 "Every module must have a corresponding test file. Testing is "
256 "non-negotiable in Option C — no untested code reaches production."
259 "Module: src/my_module.py\n"
260 "Test: tests/test_my_module.py"
263 "Creating a module without a companion test_<name>.py file, "
264 "or placing the test in the wrong directory."
266 spec_reference=
"Spec S3 — Testing Philosophy and Regression Requirements",
272 severity=SEVERITY_ADVISORY,
276 label=
"Type hint annotations",
278 "All public functions must have full type annotations on "
279 "parameters and return values. This enables static analysis, "
280 "improves documentation, and supports AI-assisted development."
283 "def calculate_total(items: list[dict], tax_rate: float = 0.0) -> float:\n"
287 "Missing return type annotation (use -> None for void), "
288 "omitting parameter types, or using bare 'list' instead of "
291 spec_reference=
"Spec S14 — Type hints enforcement",
293 profiles=[
"compact",
"strict"],
296 name=
"no_hardcoded_secrets",
297 label=
"No hardcoded secrets",
299 "String literals assigned to variables whose names suggest "
300 "secrets (password, token, api_key, etc.) must not contain "
301 "hardcoded values. Secrets belong in environment variables or "
306 'api_key = os.environ["API_KEY"]'
309 'Assigning a literal string to a variable named api_key, '
310 'password, secret, or token: api_key = "sk-abc123..."'
312 spec_reference=
"Spec S6b — Secret Hygiene",
314 profiles=[
"compact",
"strict"],
317 name=
"dbg_assert_safety",
318 label=
"_dbg assert safety",
320 "Calls to _dbg() must not have side effects that would change "
321 "program behaviour if diagnostics are disabled. The level "
322 "argument must be a literal integer, and the message must not "
323 "contain function calls with side effects."
326 '_dbg("DOMAIN", func, f"count={len(items)}", 3)'
329 "Passing a function call with side effects as the message "
330 "argument, or using a variable instead of a literal for the "
333 spec_reference=
"Spec S6a — Safety Rules",
351 """Return the severity tier (``blocking`` / ``advisory`` / ``info``)
352 for *rule_name* under *profile* (FS-C4).
354 Resolution order: profile-specific override → rule's default severity
355 → ``"blocking"`` for unknown rules (fail-safe).
357 info = RULE_REGISTRY.get(rule_name)
359 return SEVERITY_BLOCKING
360 if info.severity_per_profile
and profile
in info.severity_per_profile:
361 candidate = info.severity_per_profile[profile]
362 if candidate
in _VALID_SEVERITIES:
364 return info.severity
if info.severity
in _VALID_SEVERITIES
else SEVERITY_BLOCKING