Adaptive check cadence: the interval is earned, not assigned
Operator ruling 2026-08-20. Severity no longer sets the review interval. A check that comes back clean climbs one rung — instant, 1h, 8h, 24h, 48h, 96h, 7d, 14d, 1mo, 1q — and anything wrong drops straight back to instant. A quarter is the ceiling. The operator may defer an instant finding to a stated date; that is the only other way off the bottom rung. The rung is the point: it says how stable the estate has been on that matter, which is information severity does not carry. Volatile things get attention automatically; quiet things stop consuming it; neither judgement has to be made by a person who might be busy. Escalation trigger 5 rebased onto the ladder — fourteen days at the bottom rung, whether that is failing checks or no checks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
8b204d0411
commit
42bbf5d2dc
17 changed files with 429 additions and 148 deletions
|
|
@ -4,6 +4,7 @@ The finding files are the source of truth. Nothing here writes to them.
|
|||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
import pathlib
|
||||
import yaml
|
||||
|
||||
|
|
@ -12,9 +13,53 @@ FINDINGS = REPO / "findings"
|
|||
NOTES = REPO / "notes"
|
||||
|
||||
SEVERITIES = ["critical", "high", "medium", "low"]
|
||||
REVIEW_INTERVAL_DAYS = {"critical": 7, "high": 30, "medium": 90, "low": 180}
|
||||
STALE_MULTIPLIER = 2
|
||||
GRADED_FIELDS = ["severity", "disclosure", "escalation", "review_by", "last_reviewed"]
|
||||
GRADED_FIELDS = ["severity", "disclosure", "escalation", "next_check", "cadence"]
|
||||
# A finding leaves the watch list only by being genuinely finished. Anything
|
||||
# else — including a word the register has not seen before — stays watched, and
|
||||
# the unknown word is reported. A finding must never fall out of the nag because
|
||||
# somebody used a status the tooling did not recognise.
|
||||
CLOSED_STATUSES = ("fixed", "withdrawn")
|
||||
KNOWN_STATUSES = ("open", "accepted", "mitigated") + CLOSED_STATUSES
|
||||
|
||||
|
||||
def watched(status: str | None) -> bool:
|
||||
return status not in CLOSED_STATUSES
|
||||
|
||||
# The adaptive cadence ladder (operator ruling, 2026-08-20). A clean check
|
||||
# climbs one rung; anything wrong drops straight back to `instant`. The rung a
|
||||
# finding sits on is itself the signal: how stable this matter has been.
|
||||
CADENCE = [
|
||||
("instant", dt.timedelta(0)),
|
||||
("1h", dt.timedelta(hours=1)),
|
||||
("8h", dt.timedelta(hours=8)),
|
||||
("24h", dt.timedelta(hours=24)),
|
||||
("48h", dt.timedelta(hours=48)),
|
||||
("96h", dt.timedelta(hours=96)),
|
||||
("7d", dt.timedelta(days=7)),
|
||||
("14d", dt.timedelta(days=14)),
|
||||
("1mo", dt.timedelta(days=30)),
|
||||
("1q", dt.timedelta(days=90)),
|
||||
]
|
||||
CADENCE_NAMES = [name for name, _ in CADENCE]
|
||||
TOP_RUNG = CADENCE_NAMES[-1]
|
||||
|
||||
|
||||
def rung_index(name: str) -> int:
|
||||
return CADENCE_NAMES.index(name) if name in CADENCE_NAMES else 0
|
||||
|
||||
|
||||
def interval(name: str) -> dt.timedelta:
|
||||
return CADENCE[rung_index(name)][1]
|
||||
|
||||
|
||||
def climb(name: str) -> str:
|
||||
"""One clean check: up one rung, never past the quarter."""
|
||||
return CADENCE_NAMES[min(rung_index(name) + 1, len(CADENCE) - 1)]
|
||||
|
||||
|
||||
def reset() -> str:
|
||||
"""Anything wrong: back to the bottom."""
|
||||
return CADENCE_NAMES[0]
|
||||
|
||||
|
||||
def load(path: pathlib.Path) -> dict:
|
||||
|
|
@ -38,5 +83,20 @@ def notes() -> list[dict]:
|
|||
return sorted((load(p) for p in NOTES.glob("RISK-N-*.md")), key=lambda n: n["id"], reverse=True)
|
||||
|
||||
|
||||
def moment(value) -> dt.datetime | None:
|
||||
"""Parse a date or datetime front-matter value as UTC."""
|
||||
if isinstance(value, dt.datetime):
|
||||
return value if value.tzinfo else value.replace(tzinfo=dt.timezone.utc)
|
||||
if isinstance(value, dt.date):
|
||||
return dt.datetime.combine(value, dt.time(0, 0), tzinfo=dt.timezone.utc)
|
||||
if isinstance(value, str) and value not in ("", "unset"):
|
||||
return dt.datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(dt.timezone.utc)
|
||||
return None
|
||||
|
||||
|
||||
def now() -> dt.datetime:
|
||||
return dt.datetime.now(dt.timezone.utc)
|
||||
|
||||
|
||||
def sev_rank(sev: str) -> int:
|
||||
return SEVERITIES.index(sev) if sev in SEVERITIES else len(SEVERITIES)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue