WARDEN-WP-0033-T05: split the stale cadences, and record how a blocker was verified
The 90-day --stale-days default on `warden route gaps` was not a loose threshold, it was an inert one: the delegation register was created 2026-08-15, so it could not have fired before November. It was inherited from the catalog pointer cadence and applied to a claim with a completely different half-life. Two changes. DEFAULT_BLOCKER_STALE_DAYS = 14 now governs interim blockers, while DEFAULT_STALE_DAYS = 90 keeps governing pointer freshness -- "is this the right owner and page" is quarterly, "has the owner answered" is not. 14 is calibrated on blockers that actually cost something: ten days for the secrets-engine lanes, one for RISK-F-0001, roughly fifty for FLEX-WP-0007. The second change matters more. `reviewed` records when someone touched an entry, which is indistinguishable from re-checking it -- six lanes read as freshly reviewed today because I typed in them. `verified:` now says how the claim was established, and asked-and-waiting explicitly does NOT count: that is the state the secrets-engine blocker sat in for ten days while looking current. A lane in that state is stale at zero days old, and key-cape-oidc-login proves it works. 8 of 14 interim lanes are honestly marked unverified rather than given a fresh date they did not earn. --fail-on-stale exits 3 for a cron or gate. No CI test on age: a date-triggered failure breaks the build for whoever commits next instead of whoever owns the blocker. The CI test is structural -- every interim lane must record how it was verified -- so it fails on the commit that introduces the omission. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
a565e62b2f
commit
55f0f47a02
8 changed files with 335 additions and 24 deletions
|
|
@ -771,6 +771,24 @@ def route_list(
|
|||
)
|
||||
|
||||
|
||||
from warden.routing.catalog import DEFAULT_BLOCKER_STALE_DAYS
|
||||
|
||||
|
||||
def _gap_is_stale(delegation, reviewed: str, stale_days: int) -> bool:
|
||||
"""An interim lane needs attention on either of two independent grounds.
|
||||
|
||||
Age is the obvious one. The other is that the review was never a
|
||||
verification: an `asked-and-waiting` lane is fresh on the day the question
|
||||
goes out and stays fresh while nobody answers, which is exactly how the
|
||||
secrets-engine blocker looked current for ten days (WARDEN-WP-0033-T05).
|
||||
"""
|
||||
from warden.routing.catalog import days_since_review
|
||||
|
||||
if days_since_review(reviewed) > stale_days:
|
||||
return True
|
||||
return delegation.verified is not None and not delegation.is_verified
|
||||
|
||||
|
||||
@route_app.command("gaps")
|
||||
def route_gaps(
|
||||
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
|
||||
|
|
@ -779,12 +797,20 @@ def route_gaps(
|
|||
int,
|
||||
typer.Option(
|
||||
"--stale-days",
|
||||
help="Days since delegation review before an interim lane is stale (default 90)",
|
||||
help="Days since a blocker was verified before an interim lane is stale "
|
||||
"(default 14 — see DEFAULT_BLOCKER_STALE_DAYS)",
|
||||
min=1,
|
||||
),
|
||||
] = 90,
|
||||
] = DEFAULT_BLOCKER_STALE_DAYS,
|
||||
fail_on_stale: Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--fail-on-stale",
|
||||
help="Exit 3 if any interim lane needs re-verifying (for cron or a gate)",
|
||||
),
|
||||
] = False,
|
||||
) -> None:
|
||||
"""List interim lanes: intended owner, blocker, and age since review."""
|
||||
"""List interim lanes: intended owner, blocker, and age since verification."""
|
||||
from warden.routing.catalog import days_since_review
|
||||
|
||||
catalog = _load_catalog()
|
||||
|
|
@ -805,11 +831,15 @@ def route_gaps(
|
|||
"blocked_on": d.blocked_on,
|
||||
"reviewed": reviewed,
|
||||
"days_since_review": days_since_review(reviewed),
|
||||
"verified": d.verified,
|
||||
"is_verified": d.is_verified,
|
||||
"implicit": d.implicit,
|
||||
"stale": days_since_review(reviewed) > stale_days,
|
||||
"stale": _gap_is_stale(d, reviewed, stale_days),
|
||||
}
|
||||
)
|
||||
print(json.dumps(payload, indent=2))
|
||||
if fail_on_stale and any(row["stale"] for row in payload):
|
||||
raise typer.Exit(3)
|
||||
return
|
||||
|
||||
if not entries:
|
||||
|
|
@ -822,13 +852,18 @@ def route_gaps(
|
|||
table.add_column("Blocked on")
|
||||
table.add_column("Reviewed")
|
||||
table.add_column("Days")
|
||||
table.add_column("Verified")
|
||||
table.add_column("Status")
|
||||
for e in entries:
|
||||
d = e.effective_delegation
|
||||
reviewed = d.reviewed or e.reviewed
|
||||
days = days_since_review(reviewed)
|
||||
reviewed_styled = f"[yellow]{reviewed}[/yellow]" if days > stale_days else reviewed
|
||||
days_styled = f"[yellow]{days}[/yellow]" if days > stale_days else str(days)
|
||||
stale = _gap_is_stale(d, reviewed, stale_days)
|
||||
reviewed_styled = f"[yellow]{reviewed}[/yellow]" if stale else reviewed
|
||||
days_styled = f"[yellow]{days}[/yellow]" if stale else str(days)
|
||||
verified_styled = (
|
||||
d.verified if d.is_verified else f"[yellow]{d.verified or 'unrecorded'}[/yellow]"
|
||||
)
|
||||
status_styled = e.status if e.status == "active" else f"[yellow]{e.status}[/yellow]"
|
||||
table.add_row(
|
||||
e.id,
|
||||
|
|
@ -836,18 +871,41 @@ def route_gaps(
|
|||
d.blocked_on or "",
|
||||
reviewed_styled,
|
||||
days_styled,
|
||||
verified_styled,
|
||||
status_styled,
|
||||
)
|
||||
console.print(table)
|
||||
stale_n = sum(
|
||||
1
|
||||
for e in entries
|
||||
if days_since_review(e.effective_delegation.reviewed or e.reviewed) > stale_days
|
||||
)
|
||||
if stale_n:
|
||||
console.print(
|
||||
f"[yellow]{stale_n} interim lane(s) past {stale_days}d review cadence.[/yellow]"
|
||||
|
||||
stale_entries = [
|
||||
e for e in entries
|
||||
if _gap_is_stale(
|
||||
e.effective_delegation,
|
||||
e.effective_delegation.reviewed or e.reviewed,
|
||||
stale_days,
|
||||
)
|
||||
]
|
||||
if stale_entries:
|
||||
# Say which of the two reasons applies. "Past cadence" and "never actually
|
||||
# checked" call for different actions, and collapsing them is how an
|
||||
# asked-and-waiting lane reads as reviewed.
|
||||
aged = [
|
||||
e for e in stale_entries
|
||||
if days_since_review(e.effective_delegation.reviewed or e.reviewed) > stale_days
|
||||
]
|
||||
unverified = [e for e in stale_entries if e not in aged]
|
||||
if aged:
|
||||
console.print(
|
||||
f"[yellow]{len(aged)} interim lane(s) past the {stale_days}d blocker "
|
||||
f"cadence — re-check the blocker, do not just bump the date.[/yellow]"
|
||||
)
|
||||
if unverified:
|
||||
console.print(
|
||||
f"[yellow]{len(unverified)} interim lane(s) reviewed but not verified "
|
||||
f"(asked-and-waiting or unverified) — the claim was never "
|
||||
f"re-established.[/yellow]"
|
||||
)
|
||||
if fail_on_stale and stale_entries:
|
||||
raise typer.Exit(3)
|
||||
|
||||
|
||||
@route_app.command("show")
|
||||
|
|
|
|||
|
|
@ -69,9 +69,24 @@ _VALID_STATUS = ("active", "draft")
|
|||
_VALID_LANES = ("secret", "login")
|
||||
_VALID_ROTATION_METHODS = ("rotate", "re-establish")
|
||||
|
||||
# Default review cadence — see wiki/AccessRouting.md#drift-review-cadence
|
||||
# Default review cadence for a catalog pointer — "is this still the right owner
|
||||
# and page?" That is a genuinely quarterly question, so 90 days is right for it.
|
||||
# See wiki/AccessRouting.md#drift-review-cadence
|
||||
DEFAULT_STALE_DAYS = 90
|
||||
|
||||
# Cadence for an interim lane's *blocker*, which is a different kind of claim
|
||||
# with a much shorter half-life: "has the intended owner answered / can they
|
||||
# front this yet?" (WARDEN-WP-0033-T05).
|
||||
#
|
||||
# 14 rather than 90 because 90 was never a loose default, it was an inert one --
|
||||
# the delegation register was created 2026-08-15, so a 90-day threshold could not
|
||||
# fire before November and never had. Calibrated instead against blockers that
|
||||
# actually went stale: the secrets-engine lanes cost ten days, RISK-F-0001
|
||||
# invalidated an ops-warden blocker in one, and the FLEX-WP-0007 claim was
|
||||
# repeated by two repos for roughly fifty. 14 catches the ten-day cases and, at
|
||||
# ~15 interim lanes, surfaces about one lane a day rather than a wall of them.
|
||||
DEFAULT_BLOCKER_STALE_DAYS = 14
|
||||
|
||||
|
||||
def days_since_review(reviewed: str, *, today: Optional[date] = None) -> int:
|
||||
"""Calendar days between reviewed date (YYYY-MM-DD) and today."""
|
||||
|
|
@ -214,16 +229,25 @@ class Catalog:
|
|||
def stale_gaps(
|
||||
self,
|
||||
include_draft: bool = False,
|
||||
threshold_days: int = DEFAULT_STALE_DAYS,
|
||||
threshold_days: int = DEFAULT_BLOCKER_STALE_DAYS,
|
||||
*,
|
||||
today: Optional[date] = None,
|
||||
) -> List[RouteEntry]:
|
||||
"""Interim lanes whose delegation review is past the cadence threshold."""
|
||||
"""Interim lanes whose blocker is due a re-check.
|
||||
|
||||
A lane counts as stale when its review date is past the threshold **or**
|
||||
when the review was never a verification at all. An `asked-and-waiting`
|
||||
entry is the case that motivated this: it looks freshly reviewed on the
|
||||
day the question is asked and stays that way while nobody answers.
|
||||
"""
|
||||
out: List[RouteEntry] = []
|
||||
for e in self.gaps(include_draft=include_draft):
|
||||
reviewed = e.effective_delegation.reviewed or e.reviewed
|
||||
d = e.effective_delegation
|
||||
reviewed = d.reviewed or e.reviewed
|
||||
if is_review_stale(reviewed, threshold_days=threshold_days, today=today):
|
||||
out.append(e)
|
||||
elif d.verified is not None and not d.is_verified:
|
||||
out.append(e)
|
||||
return out
|
||||
|
||||
def freshness(
|
||||
|
|
@ -261,14 +285,16 @@ class Catalog:
|
|||
f"{stale_count} catalog entr{'y' if stale_count == 1 else 'ies'} "
|
||||
f"past {stale_threshold_days}d review cadence"
|
||||
)
|
||||
# Interim blockers run on their own, much shorter cadence -- a stale
|
||||
# pointer and an unanswered blocker are not the same kind of drift.
|
||||
stale_interim = len(self.stale_gaps(
|
||||
include_draft=True, threshold_days=stale_threshold_days, today=today
|
||||
include_draft=True, threshold_days=DEFAULT_BLOCKER_STALE_DAYS, today=today
|
||||
))
|
||||
if stale_interim:
|
||||
warnings.append(
|
||||
f"{stale_interim} interim delegation"
|
||||
f"{'' if stale_interim == 1 else 's'} past "
|
||||
f"{stale_threshold_days}d review — see `warden route gaps`"
|
||||
f"{'' if stale_interim == 1 else 's'} need re-verifying "
|
||||
f"({DEFAULT_BLOCKER_STALE_DAYS}d blocker cadence) — see `warden route gaps`"
|
||||
)
|
||||
|
||||
return CatalogFreshness(
|
||||
|
|
@ -447,11 +473,19 @@ def _parse_delegation(entry_id: str, raw: Optional[dict]) -> Optional[Delegation
|
|||
entry_id, "delegation.blocked_on", blocked_on, prose=True
|
||||
)
|
||||
|
||||
verified = str(raw.get("verified", "")).strip() or None
|
||||
if verified is not None and verified not in Delegation.VERIFICATION_METHODS:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} delegation.verified {verified!r} invalid "
|
||||
f"(expected one of {Delegation.VERIFICATION_METHODS})"
|
||||
)
|
||||
|
||||
return Delegation(
|
||||
mode=mode,
|
||||
intended_owner=intended_owner,
|
||||
blocked_on=blocked_on,
|
||||
reviewed=reviewed,
|
||||
verified=verified,
|
||||
implicit=False,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -64,14 +64,38 @@ class Delegation:
|
|||
intended_owner: Optional[str] = None
|
||||
blocked_on: Optional[str] = None
|
||||
reviewed: Optional[str] = None
|
||||
verified: Optional[str] = None
|
||||
implicit: bool = False
|
||||
|
||||
#: How `reviewed` was established. The distinction exists because a date
|
||||
#: bumped by editing the entry looks identical to one bumped by re-checking
|
||||
#: the blocker, and on 2026-08-21 six lanes read as freshly reviewed when
|
||||
#: only some had actually been re-verified (WARDEN-WP-0033-T05).
|
||||
#:
|
||||
#: `asked-and-waiting` deliberately does NOT count as verification: it is the
|
||||
#: state the secrets-engine lanes sat in for ten days while looking fresh.
|
||||
VERIFICATION_METHODS = (
|
||||
"owner-confirmed", # the intended owner stated the blocker's status
|
||||
"source-read", # re-derived from the owner's code, canon, or CCR
|
||||
"asked-and-waiting", # a question is outstanding — NOT verification
|
||||
"unverified", # carried forward without a check
|
||||
)
|
||||
#: Methods that mean the claim was actually re-established.
|
||||
VERIFYING_METHODS = ("owner-confirmed", "source-read")
|
||||
|
||||
@property
|
||||
def is_verified(self) -> bool:
|
||||
"""True only when the blocker was re-established, not merely re-edited."""
|
||||
return self.verified in self.VERIFYING_METHODS
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"mode": self.mode,
|
||||
"intended_owner": self.intended_owner,
|
||||
"blocked_on": self.blocked_on,
|
||||
"reviewed": self.reviewed,
|
||||
"verified": self.verified,
|
||||
"is_verified": self.is_verified,
|
||||
"implicit": self.implicit,
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue