Scale the blocker window by lane risk, converging with risk-nexus
risk-nexus accepted the offer to match their convention rather than grow a second one, and published it: 14d critical/high, 30d medium, 60d low, nothing auto-closing on staleness alone. Their preference — point warden route gaps at those windows and the two registers agree without a shared mechanism — is better than a joint tool. blocker_stale_days() now maps lane risk onto those windows. A flat 14 would have been wrong in both directions: too aggressive for a low-risk pointer, and it treated an admin PAT lane the same as one. ungraded takes the shortest window, not the longest. ADR-0007 makes an absent grade a defect and ADR-0008 makes a grade cover the whole path, so a lane nobody graded is the one whose blocker deserves least trust. Encoding that as 60 days would have been the fail-open default this repo already fixed once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
55f0f47a02
commit
e18894ee5b
7 changed files with 96 additions and 24 deletions
|
|
@ -126,3 +126,4 @@
|
||||||
| task | WARDEN-WP-0033-T02 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
|
| task | WARDEN-WP-0033-T02 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
|
||||||
| task | WARDEN-WP-0033-T03 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
|
| task | WARDEN-WP-0033-T03 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
|
||||||
| task | WARDEN-WP-0033-T04 | wait | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
|
| task | WARDEN-WP-0033-T04 | wait | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
|
||||||
|
| task | WARDEN-WP-0033-T05 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,11 @@
|
||||||
# declares it, and is null where the field set has not been established --
|
# declares it, and is null where the field set has not been established --
|
||||||
# null means unknown, never 'one field'.
|
# null means unknown, never 'one field'.
|
||||||
|
|
||||||
generated_at: "2026-08-21T11:24:43Z"
|
generated_at: "2026-08-21T11:28:33Z"
|
||||||
source: ops-warden/registry/routing/catalog.yaml
|
source: ops-warden/registry/routing/catalog.yaml
|
||||||
catalog_revision: "675e04e8e67869f0e47a3ae55ab37b00112582f2"
|
catalog_revision: "55f0f47a021375b8b25c924953d1b49a24e002c5"
|
||||||
catalog_revision_date: "2026-08-21T09:04:54+02:00"
|
catalog_revision_date: "2026-08-21T13:26:10+02:00"
|
||||||
catalog_dirty: true
|
catalog_dirty: false
|
||||||
high_risk_lane_count: 19
|
high_risk_lane_count: 19
|
||||||
concrete_path_count: 14
|
concrete_path_count: 14
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -710,7 +710,7 @@ def route_list(
|
||||||
bool, typer.Option("--stale", help="Show entries past review cadence (see --stale-days)")
|
bool, typer.Option("--stale", help="Show entries past review cadence (see --stale-days)")
|
||||||
] = False,
|
] = False,
|
||||||
stale_days: Annotated[
|
stale_days: Annotated[
|
||||||
int,
|
Optional[int],
|
||||||
typer.Option(
|
typer.Option(
|
||||||
"--stale-days",
|
"--stale-days",
|
||||||
help="Days since reviewed before an entry is stale (default 90)",
|
help="Days since reviewed before an entry is stale (default 90)",
|
||||||
|
|
@ -771,10 +771,10 @@ def route_list(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
from warden.routing.catalog import DEFAULT_BLOCKER_STALE_DAYS
|
from warden.routing.catalog import blocker_stale_days
|
||||||
|
|
||||||
|
|
||||||
def _gap_is_stale(delegation, reviewed: str, stale_days: int) -> bool:
|
def _gap_is_stale(entry, delegation, reviewed: str, stale_days) -> bool:
|
||||||
"""An interim lane needs attention on either of two independent grounds.
|
"""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
|
Age is the obvious one. The other is that the review was never a
|
||||||
|
|
@ -784,7 +784,7 @@ def _gap_is_stale(delegation, reviewed: str, stale_days: int) -> bool:
|
||||||
"""
|
"""
|
||||||
from warden.routing.catalog import days_since_review
|
from warden.routing.catalog import days_since_review
|
||||||
|
|
||||||
if days_since_review(reviewed) > stale_days:
|
if days_since_review(reviewed) > blocker_stale_days(entry.risk, stale_days):
|
||||||
return True
|
return True
|
||||||
return delegation.verified is not None and not delegation.is_verified
|
return delegation.verified is not None and not delegation.is_verified
|
||||||
|
|
||||||
|
|
@ -797,11 +797,11 @@ def route_gaps(
|
||||||
int,
|
int,
|
||||||
typer.Option(
|
typer.Option(
|
||||||
"--stale-days",
|
"--stale-days",
|
||||||
help="Days since a blocker was verified before an interim lane is stale "
|
help="Override the risk-scaled blocker window (default: 14d high/ungraded, "
|
||||||
"(default 14 — see DEFAULT_BLOCKER_STALE_DAYS)",
|
"30d standard, 60d low — matches risk-nexus stall windows)",
|
||||||
min=1,
|
min=1,
|
||||||
),
|
),
|
||||||
] = DEFAULT_BLOCKER_STALE_DAYS,
|
] = None,
|
||||||
fail_on_stale: Annotated[
|
fail_on_stale: Annotated[
|
||||||
bool,
|
bool,
|
||||||
typer.Option(
|
typer.Option(
|
||||||
|
|
@ -834,7 +834,8 @@ def route_gaps(
|
||||||
"verified": d.verified,
|
"verified": d.verified,
|
||||||
"is_verified": d.is_verified,
|
"is_verified": d.is_verified,
|
||||||
"implicit": d.implicit,
|
"implicit": d.implicit,
|
||||||
"stale": _gap_is_stale(d, reviewed, stale_days),
|
"window_days": blocker_stale_days(e.risk, stale_days),
|
||||||
|
"stale": _gap_is_stale(e, d, reviewed, stale_days),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
print(json.dumps(payload, indent=2))
|
print(json.dumps(payload, indent=2))
|
||||||
|
|
@ -858,7 +859,7 @@ def route_gaps(
|
||||||
d = e.effective_delegation
|
d = e.effective_delegation
|
||||||
reviewed = d.reviewed or e.reviewed
|
reviewed = d.reviewed or e.reviewed
|
||||||
days = days_since_review(reviewed)
|
days = days_since_review(reviewed)
|
||||||
stale = _gap_is_stale(d, reviewed, stale_days)
|
stale = _gap_is_stale(e, d, reviewed, stale_days)
|
||||||
reviewed_styled = f"[yellow]{reviewed}[/yellow]" if stale else reviewed
|
reviewed_styled = f"[yellow]{reviewed}[/yellow]" if stale else reviewed
|
||||||
days_styled = f"[yellow]{days}[/yellow]" if stale else str(days)
|
days_styled = f"[yellow]{days}[/yellow]" if stale else str(days)
|
||||||
verified_styled = (
|
verified_styled = (
|
||||||
|
|
@ -879,6 +880,7 @@ def route_gaps(
|
||||||
stale_entries = [
|
stale_entries = [
|
||||||
e for e in entries
|
e for e in entries
|
||||||
if _gap_is_stale(
|
if _gap_is_stale(
|
||||||
|
e,
|
||||||
e.effective_delegation,
|
e.effective_delegation,
|
||||||
e.effective_delegation.reviewed or e.reviewed,
|
e.effective_delegation.reviewed or e.reviewed,
|
||||||
stale_days,
|
stale_days,
|
||||||
|
|
@ -890,12 +892,13 @@ def route_gaps(
|
||||||
# asked-and-waiting lane reads as reviewed.
|
# asked-and-waiting lane reads as reviewed.
|
||||||
aged = [
|
aged = [
|
||||||
e for e in stale_entries
|
e for e in stale_entries
|
||||||
if days_since_review(e.effective_delegation.reviewed or e.reviewed) > stale_days
|
if days_since_review(e.effective_delegation.reviewed or e.reviewed)
|
||||||
|
> blocker_stale_days(e.risk, stale_days)
|
||||||
]
|
]
|
||||||
unverified = [e for e in stale_entries if e not in aged]
|
unverified = [e for e in stale_entries if e not in aged]
|
||||||
if aged:
|
if aged:
|
||||||
console.print(
|
console.print(
|
||||||
f"[yellow]{len(aged)} interim lane(s) past the {stale_days}d blocker "
|
f"[yellow]{len(aged)} interim lane(s) past their blocker "
|
||||||
f"cadence — re-check the blocker, do not just bump the date.[/yellow]"
|
f"cadence — re-check the blocker, do not just bump the date.[/yellow]"
|
||||||
)
|
)
|
||||||
if unverified:
|
if unverified:
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,29 @@ DEFAULT_STALE_DAYS = 90
|
||||||
# ~15 interim lanes, surfaces about one lane a day rather than a wall of them.
|
# ~15 interim lanes, surfaces about one lane a day rather than a wall of them.
|
||||||
DEFAULT_BLOCKER_STALE_DAYS = 14
|
DEFAULT_BLOCKER_STALE_DAYS = 14
|
||||||
|
|
||||||
|
# Scaled by the lane's own risk grade, matching risk-nexus's stall windows
|
||||||
|
# (14d critical/high, 30d medium, 60d low — docs/method/check-procedure.md).
|
||||||
|
# They offered the convention rather than a joint tool: point `warden route gaps`
|
||||||
|
# at the same windows and the two registers agree without a shared mechanism.
|
||||||
|
#
|
||||||
|
# `ungraded` gets the shortest window, not the longest. ADR-0007 already decided
|
||||||
|
# an absent grade is a defect and ADR-0008 that a grade covers the whole path;
|
||||||
|
# a lane nobody has graded is exactly the one whose blocker is least trustworthy.
|
||||||
|
BLOCKER_STALE_DAYS_BY_RISK = {
|
||||||
|
"high": 14,
|
||||||
|
"ungraded": 14,
|
||||||
|
"standard": 30,
|
||||||
|
"accepted": 60,
|
||||||
|
"low": 60,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def blocker_stale_days(risk: Optional[str], override: Optional[int] = None) -> int:
|
||||||
|
"""Days a lane's blocker may go unverified, scaled by what the lane holds."""
|
||||||
|
if override is not None:
|
||||||
|
return override
|
||||||
|
return BLOCKER_STALE_DAYS_BY_RISK.get(risk or "ungraded", DEFAULT_BLOCKER_STALE_DAYS)
|
||||||
|
|
||||||
|
|
||||||
def days_since_review(reviewed: str, *, today: Optional[date] = None) -> int:
|
def days_since_review(reviewed: str, *, today: Optional[date] = None) -> int:
|
||||||
"""Calendar days between reviewed date (YYYY-MM-DD) and today."""
|
"""Calendar days between reviewed date (YYYY-MM-DD) and today."""
|
||||||
|
|
@ -229,12 +252,16 @@ class Catalog:
|
||||||
def stale_gaps(
|
def stale_gaps(
|
||||||
self,
|
self,
|
||||||
include_draft: bool = False,
|
include_draft: bool = False,
|
||||||
threshold_days: int = DEFAULT_BLOCKER_STALE_DAYS,
|
threshold_days: Optional[int] = None,
|
||||||
*,
|
*,
|
||||||
today: Optional[date] = None,
|
today: Optional[date] = None,
|
||||||
) -> List[RouteEntry]:
|
) -> List[RouteEntry]:
|
||||||
"""Interim lanes whose blocker is due a re-check.
|
"""Interim lanes whose blocker is due a re-check.
|
||||||
|
|
||||||
|
The window scales with the lane's risk grade unless `threshold_days`
|
||||||
|
overrides it -- a blocker on a lane holding an admin PAT should not go
|
||||||
|
unverified as long as one on a low-risk pointer.
|
||||||
|
|
||||||
A lane counts as stale when its review date is past the threshold **or**
|
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`
|
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
|
entry is the case that motivated this: it looks freshly reviewed on the
|
||||||
|
|
@ -244,7 +271,8 @@ class Catalog:
|
||||||
for e in self.gaps(include_draft=include_draft):
|
for e in self.gaps(include_draft=include_draft):
|
||||||
d = e.effective_delegation
|
d = e.effective_delegation
|
||||||
reviewed = d.reviewed or e.reviewed
|
reviewed = d.reviewed or e.reviewed
|
||||||
if is_review_stale(reviewed, threshold_days=threshold_days, today=today):
|
window = blocker_stale_days(e.risk, threshold_days)
|
||||||
|
if is_review_stale(reviewed, threshold_days=window, today=today):
|
||||||
out.append(e)
|
out.append(e)
|
||||||
elif d.verified is not None and not d.is_verified:
|
elif d.verified is not None and not d.is_verified:
|
||||||
out.append(e)
|
out.append(e)
|
||||||
|
|
@ -287,14 +315,12 @@ class Catalog:
|
||||||
)
|
)
|
||||||
# Interim blockers run on their own, much shorter cadence -- a stale
|
# Interim blockers run on their own, much shorter cadence -- a stale
|
||||||
# pointer and an unanswered blocker are not the same kind of drift.
|
# pointer and an unanswered blocker are not the same kind of drift.
|
||||||
stale_interim = len(self.stale_gaps(
|
stale_interim = len(self.stale_gaps(include_draft=True, today=today))
|
||||||
include_draft=True, threshold_days=DEFAULT_BLOCKER_STALE_DAYS, today=today
|
|
||||||
))
|
|
||||||
if stale_interim:
|
if stale_interim:
|
||||||
warnings.append(
|
warnings.append(
|
||||||
f"{stale_interim} interim delegation"
|
f"{stale_interim} interim delegation"
|
||||||
f"{'' if stale_interim == 1 else 's'} need re-verifying "
|
f"{'' if stale_interim == 1 else 's'} need re-verifying "
|
||||||
f"({DEFAULT_BLOCKER_STALE_DAYS}d blocker cadence) — see `warden route gaps`"
|
f"(risk-scaled blocker cadence) — see `warden route gaps`"
|
||||||
)
|
)
|
||||||
|
|
||||||
return CatalogFreshness(
|
return CatalogFreshness(
|
||||||
|
|
|
||||||
|
|
@ -799,6 +799,25 @@ def test_blocker_cadence_is_separate_from_pointer_cadence():
|
||||||
assert DEFAULT_BLOCKER_STALE_DAYS < DEFAULT_STALE_DAYS
|
assert DEFAULT_BLOCKER_STALE_DAYS < DEFAULT_STALE_DAYS
|
||||||
|
|
||||||
|
|
||||||
|
def test_blocker_window_scales_with_risk_and_matches_risk_nexus():
|
||||||
|
"""risk-nexus stall windows: 14d critical/high, 30d medium, 60d low.
|
||||||
|
|
||||||
|
They offered the convention instead of a joint tool, so the two registers
|
||||||
|
agree only for as long as these numbers do.
|
||||||
|
"""
|
||||||
|
from warden.routing.catalog import blocker_stale_days
|
||||||
|
|
||||||
|
assert blocker_stale_days("high") == 14
|
||||||
|
assert blocker_stale_days("standard") == 30
|
||||||
|
assert blocker_stale_days("low") == 60
|
||||||
|
# An ungraded lane gets the SHORTEST window, not the longest -- ADR-0007 makes
|
||||||
|
# an absent grade a defect, so its blocker is the least trustworthy of all.
|
||||||
|
assert blocker_stale_days("ungraded") == 14
|
||||||
|
assert blocker_stale_days(None) == 14
|
||||||
|
# An explicit --stale-days still wins.
|
||||||
|
assert blocker_stale_days("low", 7) == 7
|
||||||
|
|
||||||
|
|
||||||
def test_asked_and_waiting_is_not_verification():
|
def test_asked_and_waiting_is_not_verification():
|
||||||
"""The failure this whole change exists to catch.
|
"""The failure this whole change exists to catch.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,21 @@ while they shared one 90-day threshold. They should not.
|
||||||
| Claim | Question | Default | Where |
|
| Claim | Question | Default | Where |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Pointer freshness | Is this still the right owner and page? | **90 days** | `warden route list --stale` |
|
| Pointer freshness | Is this still the right owner and page? | **90 days** | `warden route list --stale` |
|
||||||
| Interim blocker | Has the intended owner answered / can they front this yet? | **14 days** | `warden route gaps` |
|
| Interim blocker | Has the intended owner answered / can they front this yet? | **risk-scaled, 14–60 days** | `warden route gaps` |
|
||||||
|
|
||||||
|
The blocker window scales with what the lane holds, matching `risk-nexus`'s stall
|
||||||
|
windows (`docs/method/check-procedure.md`) so the two registers agree without a
|
||||||
|
shared tool:
|
||||||
|
|
||||||
|
| Lane `risk` | Window | risk-nexus equivalent |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `high`, `ungraded` | **14 days** | critical / high |
|
||||||
|
| `standard` | **30 days** | medium |
|
||||||
|
| `low`, `accepted` | **60 days** | low |
|
||||||
|
|
||||||
|
`ungraded` takes the *shortest* window, not the longest. `ADR-0007` makes an
|
||||||
|
absent grade a defect and `ADR-0008` makes a grade cover the whole path — a lane
|
||||||
|
nobody has graded is the one whose blocker deserves least trust.
|
||||||
|
|
||||||
A pointer genuinely is a quarterly question. A blocker is not: it is a claim
|
A pointer genuinely is a quarterly question. A blocker is not: it is a claim
|
||||||
about another repo's state at a date, and this estate invalidates those in days.
|
about another repo's state at a date, and this estate invalidates those in days.
|
||||||
|
|
@ -230,8 +244,8 @@ re-establishing the claim is the failure this section exists to prevent, and it
|
||||||
is cheap to avoid — most of these are answerable by reading the owner's repo.
|
is cheap to avoid — most of these are answerable by reading the owner's repo.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
warden route gaps # 14-day blocker cadence, plus unverified lanes
|
warden route gaps # risk-scaled cadence, plus unverified lanes
|
||||||
warden route gaps --stale-days 30 # looser threshold
|
warden route gaps --stale-days 30 # flat override for every lane
|
||||||
warden route gaps --fail-on-stale # exit 3 — for a cron job or a gate
|
warden route gaps --fail-on-stale # exit 3 — for a cron job or a gate
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -222,6 +222,15 @@ fire before November.
|
||||||
`RISK-F-0001`, ~50 for `FLEX-WP-0007`. At ~15 lanes it surfaces about one a
|
`RISK-F-0001`, ~50 for `FLEX-WP-0007`. At ~15 lanes it surfaces about one a
|
||||||
day rather than a wall, and it fires on zero lanes on age today.
|
day rather than a wall, and it fires on zero lanes on age today.
|
||||||
|
|
||||||
|
**Then scaled by risk, after risk-nexus answered.** They accepted the offer to
|
||||||
|
converge and published their convention: 14 days critical/high, 30 medium, 60
|
||||||
|
low, with fix state *read* from the owning repo rather than remembered. Their
|
||||||
|
framing — "point `warden route gaps` at those windows and the two will agree
|
||||||
|
without either of us building a shared mechanism" — is better than a joint
|
||||||
|
tool, so `blocker_stale_days()` now maps lane `risk` onto exactly those
|
||||||
|
windows. `ungraded` takes the *shortest*, not the longest: `ADR-0007` makes an
|
||||||
|
absent grade a defect, so its blocker is the least trustworthy of all.
|
||||||
|
|
||||||
2. **`verified:` distinguishes re-checking from re-editing.** `reviewed` records
|
2. **`verified:` distinguishes re-checking from re-editing.** `reviewed` records
|
||||||
when someone touched the entry; nothing recorded whether the claim was
|
when someone touched the entry; nothing recorded whether the claim was
|
||||||
re-established. `owner-confirmed` and `source-read` count;
|
re-established. `owner-confirmed` and `source-read` count;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue