mirror of
https://github.com/theclaymethod/unslop.git
synced 2026-09-19 01:45:09 +08:00
4cd3ea0a1e
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
155 lines
4.7 KiB
Python
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()
|