Files
Clayton Kim 4cd3ea0a1e Make the adversarial-refresh promise operational
Plan 013: the Growth promise in docs/PRODUCT.md relied on someone
remembering to run wiki_sync, re-run the parity bench, and funnel misses
through contribute. This lands the three missing pieces:

- references/refresh.md: the agent-runnable refresh procedure — staleness
  check, wiki-sync flow, parity-bench flow with the eval-gating rule,
  every-miss-becomes-a-row routing through contribute, and result-recording
  conventions (pipeline.md tables, TUNE-RESULTS.md style).
- scripts/refresh_status.py: stdlib, network-free staleness reporter.
  JSON with wiki_sync / parity_bench / newest_pattern_row, each carrying
  last/days/stale. Sources: .wiki_sync_state.json (last_timestamp, mtime
  fallback), the "Live matrix recorded **YYYY-MM-DD**" line in
  references/pipeline.md, and git log -1 --format=%cs on
  evals/adversarial-evals.json (argv list, no shell). Thresholds (90/180/60
  days) are commented guidance; always exits 0 — reporter, not gate.
- docs/DECISIONS.md: durable home for decided tradeoffs, each entry with
  Decision / Why / Revisit-when / Source. Seeds the PRODUCT.md decisions
  (agent-invoked only, English-only, no packaging, no rights checks,
  removal-dominant axis) plus the commit-message-only ones: WP8 fixture
  word-floor/length-balance queue (05f2363), voice impostor/background
  calibration queue (41b60d1), protects-grain + build_report deferrals
  (d6f6321), and FP-06 as the single intentional xfail (efd7e0e,
  evals/CRITIQUE.md).
- One-line pointers to references/refresh.md from references/maintenance.md
  and docs/PRODUCT.md (Growth).

All four touched/new docs pass banned_phrase_scan and
structure_scan --genre docs. Suite: 439 PASS / 1 XFAIL (FP-06) / 0 FAIL.

New decisions go in DECISIONS.md at decision time; the reporter is the
building block if the operator ever wants scheduled refresh outside this
repo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6CYksdLbXbTAxcAQjvHz5
2026-07-07 06:37:22 -07:00

155 lines
4.7 KiB
Python

#!/usr/bin/env python3
"""
Report staleness of the three inputs the adversarial-refresh promise depends
on: the Wikipedia wiki-sync state, the model-parity bench, and the newest
adversarial-eval row. Pure stdlib, network-free, always exits 0 — this is a
reporter an agent reads before deciding what to run next
(see references/refresh.md), not a gate that blocks anything itself.
Usage:
python3 scripts/refresh_status.py
"""
from __future__ import annotations
import json
import re
import subprocess
import sys
from datetime import date, datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
WIKI_STATE_FILE = ROOT / "scripts" / ".wiki_sync_state.json"
PIPELINE_DOC = ROOT / "references" / "pipeline.md"
ADVERSARIAL_EVALS = ROOT / "evals" / "adversarial-evals.json"
# Guidance thresholds, not hard limits -- the numbers below are the point at
# which an agent should proactively check, not a required cadence. Every
# field is a plain reporter value; nothing here fails the build.
#
# Wikipedia's "Signs of AI writing" page is a slow-moving reference page;
# quarterly is enough to catch drift without chasing noise.
WIKI_STALE_DAYS = 90
# The GPT/Anthropic model spectrums shift on the order of months, and the
# rule in references/pipeline.md already forces a re-run on any co-writer,
# mimic, or detector-pack change regardless of this threshold -- this is a
# backstop for the case where no such change happened but time still passed.
BENCH_STALE_DAYS = 180
# The adversarial-eval catalog is the product's growth signal (docs/PRODUCT.md,
# Growth); a two-month gap with no new row is worth a look even if nothing
# else prompted one.
ROW_STALE_DAYS = 60
# The convention this script relies on in references/pipeline.md: the
# recorded parity date is written as "Live matrix recorded **YYYY-MM-DD**."
# If that sentence's wording changes, this regex needs to change with it.
PIPELINE_DATE_RE = re.compile(
r"Live matrix recorded \*\*(\d{4}-\d{2}-\d{2})\*\*"
)
def _today() -> date:
return datetime.now(timezone.utc).date()
def _days_since(d: date | None) -> int | None:
if d is None:
return None
return (_today() - d).days
def _status(last: date | None, stale_days: int) -> dict:
days = _days_since(last)
stale = days is None or days > stale_days
return {
"last": last.isoformat() if last else None,
"days": days,
"stale": stale,
}
def _parse_iso_date(value: str) -> date | None:
"""Best-effort parse of an ISO-8601 timestamp (MediaWiki style, 'Z'
suffix included) down to a plain date."""
try:
return datetime.fromisoformat(value.replace("Z", "+00:00")).date()
except (ValueError, AttributeError):
return None
def wiki_sync_status() -> dict:
if not WIKI_STATE_FILE.exists():
# Never synced. Treat as stale so it surfaces for attention.
return _status(None, WIKI_STALE_DAYS)
last: date | None = None
try:
state = json.loads(WIKI_STATE_FILE.read_text())
last = _parse_iso_date(state.get("last_timestamp", ""))
except (json.JSONDecodeError, OSError):
last = None
if last is None:
# Content didn't parse; fall back to the state file's mtime.
try:
mtime = WIKI_STATE_FILE.stat().st_mtime
last = datetime.fromtimestamp(mtime, tz=timezone.utc).date()
except OSError:
last = None
return _status(last, WIKI_STALE_DAYS)
def parity_bench_status() -> dict:
last: date | None = None
try:
text = PIPELINE_DOC.read_text()
match = PIPELINE_DATE_RE.search(text)
if match:
last = date.fromisoformat(match.group(1))
except OSError:
last = None
return _status(last, BENCH_STALE_DAYS)
def newest_pattern_row_status() -> dict:
last: date | None = None
try:
result = subprocess.run(
[
"git",
"log",
"-1",
"--format=%cs",
"--",
str(ADVERSARIAL_EVALS),
],
cwd=str(ROOT),
capture_output=True,
text=True,
timeout=10,
)
out = result.stdout.strip()
if result.returncode == 0 and out:
last = date.fromisoformat(out)
except (OSError, subprocess.SubprocessError, ValueError):
last = None
return _status(last, ROW_STALE_DAYS)
def main() -> None:
output = {
"wiki_sync": wiki_sync_status(),
"parity_bench": parity_bench_status(),
"newest_pattern_row": newest_pattern_row_status(),
}
print(json.dumps(output, indent=2))
sys.exit(0)
if __name__ == "__main__":
main()