Skip to content

Commit fb19e56

Browse files
author
Arturo R Montesinos
committed
Fix: lint_incidents variable rename & indentation
1 parent 346d1e7 commit fb19e56

4 files changed

Lines changed: 101 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,6 @@ jobs:
4343

4444
- name: Lint ADRs
4545
run: python3 scripts/lint_adrs.py
46+
47+
- name: Lint Incidents
48+
run: python3 scripts/lint_incidents.py

.pre-commit-config.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ repos:
2525
language: system
2626
pass_filenames: false
2727
stages: [pre-commit]
28+
- id: incidents-lint
29+
name: Incident log lint
30+
entry: python scripts/lint_incidents.py
31+
language: system
32+
pass_filenames: false
33+
stages: [pre-commit]
2834
- id: no-debug-prints
2935
name: Disallow raw print without file= in src
3036
entry: bash scripts/check_prints.sh

AI_CURATOR_RECIPE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ Suggested Hooks:
177177
- Raw `print(` blocker (allow explicit `file=` usage for intentional streams)
178178
- Fast test subset (pre-push): 1–3 sentinel tests covering critical paths
179179
- (Optional) Incident guard hooks (e.g., verify `pyproject.toml` static version matches module `__version__` if dynamic disabled)
180+
- Incident log linter (sequential IDs, required sections)
180181

181182
Escalation: full suite always in CI.
182183

@@ -294,6 +295,7 @@ preferred-citation:
294295
- Audit README for accuracy vs features.
295296
- Verify `pyproject.toml` authors & metadata still correct.
296297
- Review Incident Log: close resolved ones, consolidate patterns, ensure added guardrails are still active.
298+
- Run incident linter (automated in CI + pre-commit) to ensure structural integrity.
297299

298300
## 23. Human Curator Quick Reference
299301
| Task | Command Examples |

scripts/lint_incidents.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/usr/bin/env python3
2+
"""Lint curatorial incident log entries.
3+
4+
Checks:
5+
- Directory exists: docs/incidents
6+
- Files follow naming: INC-XXXX-*.md (XXXX numeric, zero-padded)
7+
- IDs strictly sequential without gaps.
8+
- Each file contains required section headings:
9+
## Context / Trigger
10+
## Symptom
11+
## Root Cause
12+
## Resolution
13+
## Prevention / Guardrail
14+
## References
15+
- Status line present near top: **Status:**
16+
- Tags line present: **Tags:**
17+
- If Closed, must mention at least one commit hash pattern (7+ hex) in body.
18+
Exit non-zero on failure.
19+
"""
20+
21+
from __future__ import annotations
22+
import re
23+
import sys
24+
from pathlib import Path
25+
26+
RE_FILE = re.compile(r'^INC-(\d{4})-[a-z0-9-]+\.md$')
27+
RE_COMMIT = re.compile(r'\b[0-9a-f]{7,40}\b')
28+
RE_REQUIRED_SECTIONS = [
29+
'## Context / Trigger',
30+
'## Symptom',
31+
'## Root Cause',
32+
'## Resolution',
33+
'## Prevention / Guardrail',
34+
'## References',
35+
]
36+
37+
RE_STATUS = re.compile(r'^\*\*Status:\*\*\s*(.+)$', re.IGNORECASE)
38+
RE_TAGS = re.compile(r'^\*\*Tags:\*\*\s*(.+)$', re.IGNORECASE)
39+
40+
41+
def fail(msg: str):
42+
print(f'[INC-LINT] FAIL: {msg}', file=sys.stderr)
43+
raise SystemExit(1)
44+
45+
46+
def main() -> int:
47+
root = Path(__file__).resolve().parent.parent
48+
idir = root / 'docs' / 'incidents'
49+
if not idir.exists():
50+
print('[INC-LINT] No incidents directory (skip).')
51+
return 0
52+
files = [p for p in idir.glob('INC-*.md') if p.is_file() and p.name != 'TEMPLATE.md']
53+
if not files:
54+
print('[INC-LINT] No incident files.')
55+
return 0
56+
files.sort()
57+
ids = []
58+
for p in files:
59+
m = RE_FILE.match(p.name)
60+
if not m:
61+
fail(f'Bad filename: {p.name} (expected INC-XXXX-kebab.md)')
62+
ids.append(int(m.group(1)))
63+
# Check sequential
64+
for previous, current in zip(ids, ids[1:]):
65+
if current != previous + 1:
66+
fail(f'ID gap between {previous:04d} and {current:04d}')
67+
# Lint content
68+
for p in files:
69+
text = p.read_text(encoding='utf-8')
70+
# Headings
71+
for sec in RE_REQUIRED_SECTIONS:
72+
if sec not in text:
73+
fail(f"Missing section '{sec}' in {p.name}")
74+
# Status & Tags near top (first 25 lines)
75+
lines = text.splitlines()[:25]
76+
status = any(RE_STATUS.search(line) for line in lines)
77+
tags = any(RE_TAGS.search(line) for line in lines)
78+
if not status:
79+
fail(f'Missing **Status:** line near top of {p.name}')
80+
if not tags:
81+
fail(f'Missing **Tags:** line near top of {p.name}')
82+
if '**Status:** Closed' in text:
83+
if not RE_COMMIT.search(text):
84+
fail(f'Closed incident {p.name} missing commit reference')
85+
print('[INC-LINT] OK: incidents lint passed.')
86+
return 0
87+
88+
89+
if __name__ == '__main__': # pragma: no cover
90+
raise SystemExit(main())

0 commit comments

Comments
 (0)