Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions .github/workflows/maint-77-model-registry-freshness.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jobs:
runs-on: ubuntu-latest
outputs:
rc: ${{ steps.gate.outputs.rc }}
has_advisory: ${{ steps.gate.outputs.has_advisory }}
discovery_drift: ${{ steps.discovery.outputs.drift || 'false' }}
permissions:
contents: read
Expand All @@ -47,6 +48,8 @@ jobs:
python-version: '3.14'

# On PRs, fail the job on staleness so registry/slot changes are gated.
# The default gate fails only on structural findings (danger); a merely
# overdue review is advisory and never fails, so it cannot block work.
# On schedule/dispatch, never fail the run — open a tracking issue instead.
- name: Run freshness gate
id: gate
Expand All @@ -55,6 +58,8 @@ jobs:
python3 tools/check_model_registry_freshness.py --json > freshness.json
rc=$?
echo "rc=$rc" >> "$GITHUB_OUTPUT"
has_advisory=$(python3 -c "import json;print('true' if json.load(open('freshness.json')).get('advisory') else 'false')")
echo "has_advisory=$has_advisory" >> "$GITHUB_OUTPUT"
cat freshness.json
{
echo '### Model registry freshness'
Expand All @@ -67,10 +72,12 @@ jobs:
exit 2
fi

- name: Fail PRs on staleness
# Only structural/dangerous findings fail a model-config PR. An overdue
# review is advisory and is surfaced by the scheduled tracking issue.
- name: Fail PRs on structural findings
if: github.event_name == 'pull_request' && steps.gate.outputs.rc == '1'
run: |
echo "::error::Model registry/slots are stale — see job summary."
echo "::error::Model registry/slots have a structural problem — see job summary."
exit 1
Comment on lines +75 to 81

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail

echo "== Workflow file outline =="
fd -a 'maint-77-model-registry-freshness.yml' .github templates || true

echo "== Search for strict usage and model-config path checks =="
rg -n --hidden --glob '.github/workflows/*.yml' --glob '.github/workflows/*.yaml' --glob 'templates/consumer-repo/.github/workflows/*.yml' --glob 'templates/consumer-repo/.github/workflows/*.yaml' -- '--strict|model configuration|model-config|registry/slots|structural findings|advisory' .

echo "== Read target workflow around relevant lines =="
file=".github/workflows/maint-77-model-registry-freshness.yml"
if [ -f "$file" ]; then
  nl -ba "$file" | sed -n '1,140p'
fi

echo "== Read template counterpart if present =="
tfile="templates/consumer-repo/.github/workflows/maint-77-model-registry-freshness.yml"
if [ -f "$tfile" ]; then
  nl -ba "$tfile" | sed -n '1,140p'
fi

Repository: stranske/Workflows

Length of output: 2909


🏁 Script executed:

set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path('.github/workflows/maint-77-model-registry-freshness.yml')
print(p.exists())
if p.exists():
    text = p.read_text()
    for i, line in enumerate(text.splitlines(), 1):
        if '--strict' in line or 'gate' in line or 'pull_request' in line or 'model' in line:
            print(f'{i}: {line}')
PY

Repository: stranske/Workflows

Length of output: 2002


🏁 Script executed:

set -euo pipefail

echo "== template counterpart presence =="
fd -a 'maint-77-model-registry-freshness.yml' templates .github || true

echo "== workflow snippet with line numbers =="
sed -n '45,90p' .github/workflows/maint-77-model-registry-freshness.yml | cat -n

echo "== strict support in freshness tool =="
rg -n --hidden --glob 'tools/check_model_registry_freshness.py' -- '--strict|argparse|add_argument|rc|advisory|structural' tools/check_model_registry_freshness.py

echo "== tool file outline =="
python3 - <<'PY'
from pathlib import Path
p = Path('tools/check_model_registry_freshness.py')
print("exists:", p.exists())
if p.exists():
    for i, line in enumerate(p.read_text().splitlines(), 1):
        if any(k in line for k in ['--strict','argparse','add_argument','rc','advisory','structural','parser']):
            print(f'{i}: {line}')
PY

Repository: stranske/Workflows

Length of output: 9231


🏁 Script executed:

set -euo pipefail
rg -n --hidden --glob '.github/workflows/*' --glob 'templates/consumer-repo/.github/workflows/*' -- 'check_model_registry_freshness\.py|--strict' .

Repository: stranske/Workflows

Length of output: 847


Add --strict to the PR gate step. The freshness tool already supports strict mode, but this PR path still runs it without that flag, so advisory-only findings won’t fail model-config PRs. Keep the scheduled tracking-issue path non-strict.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/maint-77-model-registry-freshness.yml around lines 75 -
81, Add the freshness tool’s --strict flag to the pull-request gate step that
produces steps.gate.outputs.rc, ensuring advisory findings fail model-config
PRs. Leave the scheduled tracking-issue invocation non-strict, and preserve the
existing structural-finding failure behavior in “Fail PRs on structural
findings.”

Source: Path instructions


- name: Discover provider catalog drift
Expand Down Expand Up @@ -111,7 +118,9 @@ jobs:
tracking-issue:
if: >-
github.event_name != 'pull_request' &&
(needs.freshness.outputs.rc == '1' || needs.freshness.outputs.discovery_drift == 'true')
(needs.freshness.outputs.rc == '1' ||
needs.freshness.outputs.has_advisory == 'true' ||
needs.freshness.outputs.discovery_drift == 'true')
needs: freshness
runs-on: ubuntu-latest
permissions:
Expand Down
19 changes: 19 additions & 0 deletions docs/MODEL_SELECTION_POLICY.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,25 @@ records. Do not hand-enter aggregate rates or recommendation rankings.
An approved evidence record uses `kind: workload-benchmark` and
`status: passed`. The freshness gate rejects an approved decision without it.

### Advisory vs. blocking findings

`tools/check_model_registry_freshness.py` separates its findings into two
classes, and by default only one of them fails the gate:

- **Blocking (structural):** a malformed registry, a selection or slot that
points at an absent or blocked model, or an *approved* selection with no
passing workload-benchmark evidence. These mean work could be wrong, so they
fail the gate (exit 1).
- **Advisory (cadence):** a review whose `review_by` date has simply passed
(`review_overdue`, `provisional_overdue`, `selection_review_overdue`). A due
review is not a danger signal — the provisional incumbents remain a valid
runtime baseline — so it never fails the default gate and never blocks
unrelated work. It is surfaced instead by the `maint-77` scheduled run, which
opens a non-blocking tracking issue.

Pass `--strict` to fail on *any* finding (used where a PR itself edits model
configuration and should be proven fresh before merging).

## Incumbents and Candidates

The existing OpenAI, Anthropic, and GitHub Models verifier choices are recorded
Expand Down
9 changes: 9 additions & 0 deletions tests/test_check_model_registry_freshness.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,16 @@ def test_main_exit_codes(tmp_path: Path):
"2026-07-10",
]
assert gate.main(common) == 0
# An overdue review is advisory: it must NOT fail the default gate (so it
# cannot block unrelated fleet-wide work)...
registry_path.write_text(json.dumps(_registry(review_by="2026-07-01")), encoding="utf-8")
assert gate.main(common) == 0
# ...but --strict still fails on it, for callers gating a model-config change.
assert gate.main([*common, "--strict"]) == 1
# A structural finding (selection references an absent model) always blocks.
structural = _registry()
structural["selections"][0]["model_id"] = "missing"
registry_path.write_text(json.dumps(structural), encoding="utf-8")
assert gate.main(common) == 1
Comment on lines +322 to 332

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add coverage for the new JSON and finding-category contract.

This test covers exit codes, but never invokes --json or asserts ok, blocking, and advisory. It also exercises only review_overdue; regressions affecting provisional_overdue or selection_review_overdue would pass unnoticed.

As per path instructions, changed Python behavior must have accompanying test coverage.

🧰 Tools
🪛 ast-grep (0.44.1)

[info] 323-323: use jsonify instead of json.dumps for JSON output
Context: json.dumps(_registry(review_by="2026-07-01"))
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 330-330: use jsonify instead of json.dumps for JSON output
Context: json.dumps(structural)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_check_model_registry_freshness.py` around lines 322 - 332, Extend
the freshness-gate tests around gate.main to invoke --json and assert the
response contract fields ok, blocking, and advisory. Add cases covering
provisional_overdue and selection_review_overdue in addition to review_overdue,
verifying their advisory behavior by default and blocking behavior under
--strict where applicable, while preserving the existing structural finding
assertion.

Source: Path instructions

assert gate.main([*common[:-1], "not-a-date"]) == 2

Expand Down
58 changes: 53 additions & 5 deletions tools/check_model_registry_freshness.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,30 @@
DEFAULT_MAX_AGE_DAYS = 30
VALID_SELECTION_STATUSES = {"provisional", "approved"}

# Time-cadence findings mean "a review is due", NOT "this work is dangerous".
# They are advisory: reported and surfaced as a tracking issue, but they never
# fail the gate (and so never block unrelated fleet-wide work). Only structural
# findings — a malformed registry, an absent/blocked model, an APPROVED
# selection with no passing evidence — indicate work could be wrong, and those
# still block. Use --strict to fail on any finding (e.g. gating a PR that itself
# edits model config).
ADVISORY_FINDING_KINDS = frozenset(
{
"review_overdue",
"provisional_overdue",
"selection_review_overdue",
Comment on lines +42 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep invalid review dates blocking

When the registry date is missing or unparseable, _review_date emits review_overdue; a missing or invalid selection date similarly emits selection_review_overdue. Classifying these kinds wholesale as advisory therefore makes malformed model configuration exit 0 under the default gate, allowing a model-config PR with no valid review deadline to pass. Reserve these advisory kinds for successfully parsed dates that have merely elapsed, or emit distinct blocking kinds for missing/invalid dates.

Useful? React with 👍 / 👎.

}
)


def partition_findings(
findings: list[dict[str, str]],
) -> tuple[list[dict[str, str]], list[dict[str, str]]]:
"""Split findings into (blocking, advisory)."""
advisory = [f for f in findings if f.get("kind") in ADVISORY_FINDING_KINDS]
blocking = [f for f in findings if f.get("kind") not in ADVISORY_FINDING_KINDS]
return blocking, advisory
Comment on lines +40 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Do not classify malformed review dates as advisory.

_review_date uses review_overdue for unparseable dates and missing review_by/as_of values at Lines [87-95]. Adding that kind to ADVISORY_FINDING_KINDS makes malformed or incomplete registries return ok: true and exit successfully, contradicting the blocking contract for malformed registries.

Use distinct structural finding kinds for invalid/missing dates and reserve review_overdue for a valid date that has merely elapsed. Add regression tests for both cases.

As per path instructions, changed Python behavior must prioritize correctness and test coverage.

Proposed fix
-        findings.append(_finding("review_overdue", f"unparseable registry date: {exc}"))
+        findings.append(_finding("registry_invalid", f"unparseable registry date: {exc}"))
...
-            "review_overdue",
+            "registry_invalid",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ADVISORY_FINDING_KINDS = frozenset(
{
"review_overdue",
"provisional_overdue",
"selection_review_overdue",
}
)
def partition_findings(
findings: list[dict[str, str]],
) -> tuple[list[dict[str, str]], list[dict[str, str]]]:
"""Split findings into (blocking, advisory)."""
advisory = [f for f in findings if f.get("kind") in ADVISORY_FINDING_KINDS]
blocking = [f for f in findings if f.get("kind") not in ADVISORY_FINDING_KINDS]
return blocking, advisory
ADVISORY_FINDING_KINDS = frozenset(
{
"registry_invalid",
"provisional_overdue",
"selection_review_overdue",
}
)
def partition_findings(
findings: list[dict[str, str]],
) -> tuple[list[dict[str, str]], list[dict[str, str]]]:
"""Split findings into (blocking, advisory)."""
advisory = [f for f in findings if f.get("kind") in ADVISORY_FINDING_KINDS]
blocking = [f for f in findings if f.get("kind") not in ADVISORY_FINDING_KINDS]
return blocking, advisory
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/check_model_registry_freshness.py` around lines 40 - 55, Update
_review_date to emit distinct structural finding kinds for unparseable dates and
missing review_by/as_of values, while reserving review_overdue for valid dates
that have elapsed. Keep only genuinely temporal overdue findings in
ADVISORY_FINDING_KINDS so partition_findings treats malformed or incomplete
registries as blocking, and add regression tests covering both invalid and
missing date cases.

Source: Path instructions



def _normalize_provider(provider: str) -> str:
normalized = (provider or "").strip().lower()
Expand Down Expand Up @@ -403,6 +427,11 @@ def main(argv: list[str] | None = None) -> int:
parser.add_argument("--max-age-days", type=int, default=DEFAULT_MAX_AGE_DAYS)
parser.add_argument("--today", type=str, default=None)
parser.add_argument("--json", action="store_true")
parser.add_argument(
"--strict",
action="store_true",
help="Fail (exit 1) on ANY finding, including advisory cadence findings.",
)
args = parser.parse_args(argv)

try:
Expand All @@ -421,15 +450,34 @@ def main(argv: list[str] | None = None) -> int:
max_age_days=args.max_age_days,
policy=policy,
)
blocking, advisory = partition_findings(findings)
if args.json:
print(json.dumps({"fresh": not findings, "findings": findings}, indent=2))
print(
json.dumps(
{
"fresh": not findings,
"ok": not blocking,
"blocking": blocking,
"advisory": advisory,
"findings": findings,
},
indent=2,
)
)
elif findings:
print(f"Model registry freshness: {len(findings)} finding(s):")
for finding in findings:
print(f" [{finding['kind']}] {finding['detail']}")
print(f"Model registry freshness: {len(blocking)} blocking, {len(advisory)} advisory:")
for finding in blocking:
print(f" [BLOCK] [{finding['kind']}] {finding['detail']}")
for finding in advisory:
print(f" [advisory] [{finding['kind']}] {finding['detail']}")
else:
print("Model registry is fresh: decisions, evidence, and slots are consistent.")
return 1 if findings else 0

if args.strict:
return 1 if findings else 0
# Default: only structural/dangerous findings fail the gate. A merely-overdue
# review is advisory and must never block unrelated work.
return 1 if blocking else 0


if __name__ == "__main__": # pragma: no cover
Expand Down
Loading