Add deterministic Claude activity surface
This commit is contained in:
1 parent
03e6651f77
commit
52c8f466ea
10 files changed
+5522
-7
No files matched your search
@@ -4,6 +4,18 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
acceptance:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: verify generated Claude surface
|
||||||
|
run: |
|
||||||
|
test -s claude/index.html
|
||||||
|
python3 -m json.tool claude.json >/dev/null
|
||||||
|
grep -q 'claude.json' claude/index.html
|
||||||
|
grep -q 'COPY claude ' Dockerfile
|
||||||
|
- name: build published image
|
||||||
|
run: docker build --tag notplanned-acceptance .
|
||||||
note:
|
note:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
FROM nginx:1.27-alpine
|
FROM nginx:1.27-alpine
|
||||||
COPY index.html /usr/share/nginx/html/index.html
|
COPY index.html /usr/share/nginx/html/index.html
|
||||||
|
COPY claude /usr/share/nginx/html/claude
|
||||||
|
COPY claude.json /usr/share/nginx/html/claude.json
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
# notplanned
|
# notplanned
|
||||||
|
|
||||||
notplanned — we build the features maintainers won't
|
notplanned — we build the features maintainers won't
|
||||||
|
|
||||||
|
## deterministic surfaces
|
||||||
|
|
||||||
|
- `/claude/` is a bounded public GitHub commit scout. Refresh it with `python3 claude.py`; it writes `claude/index.html` and `claude.json`.
|
||||||
|
- The scan uses five fixed commit-search queries over a 30-day window, dedupes by repository + commit SHA, applies fixed keyword categories, and separates `anthropics/*` from independent repositories.
|
||||||
|
- GitHub Search is unauthenticated and rate-limited; each JSON snapshot records query results, rate-limit headers, and errors instead of hiding partial scans.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<!doctype html><html lang="en"><head>
|
||||||
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>notplanned / claude</title>
|
||||||
|
<meta name="description" content="Deterministic GitHub scout for Claude-attributed coding activity.">
|
||||||
|
<style>
|
||||||
|
:root{color-scheme:light dark;--bg:#f3f1eb;--fg:#111;--muted:#6d6961;--line:#cbc7bc;--card:#faf8f2;--accent:#ff5a1f;--warm:#d99416}
|
||||||
|
@media(prefers-color-scheme:dark){:root{--bg:#10100f;--fg:#f2efe7;--muted:#a6a198;--line:#393732;--card:#171715;--accent:#ff7540;--warm:#efad32}}
|
||||||
|
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace}a{color:inherit}.wrap{width:min(1180px,calc(100% - 36px));margin:auto}
|
||||||
|
header{min-height:76px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;gap:16px}.brand{font-weight:900;letter-spacing:-.05em}.nav{font-size:11px;color:var(--muted);display:flex;gap:14px;flex-wrap:wrap;justify-content:flex-end}.nav a{text-decoration:none}.nav a:hover{color:var(--fg)}
|
||||||
|
.hero{padding:74px 0 45px;display:grid;grid-template-columns:1.3fr .7fr;gap:40px;align-items:end}h1{font-family:ui-sans-serif,system-ui,sans-serif;font-size:clamp(58px,10vw,112px);line-height:.85;letter-spacing:-.08em;margin:0}.dek{font-family:ui-sans-serif,system-ui,sans-serif;color:var(--muted);font-size:18px;line-height:1.45;margin:0}
|
||||||
|
.stats{display:grid;grid-template-columns:repeat(5,1fr);border:1px solid var(--line);border-radius:16px;overflow:hidden;margin-bottom:22px}.stat{padding:16px;border-right:1px solid var(--line)}.stat:last-child{border:0}.stat strong{display:block;font-family:ui-sans-serif,system-ui,sans-serif;font-size:22px}.stat span{font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:.1em}
|
||||||
|
.note{font-size:11.5px;color:var(--muted);line-height:1.5;margin:0 0 34px}.section-head{display:flex;justify-content:space-between;gap:24px;align-items:end;margin-bottom:17px}.section-head h2{font-family:ui-sans-serif,system-ui,sans-serif;font-size:30px;letter-spacing:-.04em;margin:0}.section-head p{margin:0;max-width:520px;text-align:right;font-size:12px;line-height:1.45;color:var(--muted)}
|
||||||
|
.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-bottom:58px}.commit-card{display:flex;flex-direction:column;min-height:260px;text-decoration:none;background:var(--card);border:1px solid var(--line);border-radius:17px;padding:19px;transition:.12s ease}.commit-card:hover{transform:translateY(-2px);border-color:var(--fg)}.topline{display:flex;justify-content:space-between;gap:12px;margin-bottom:18px}.repo,.meta,.why{font-size:10.5px;color:var(--muted)}.ecosystem{font-size:9px;text-transform:uppercase;letter-spacing:.08em;border:1px solid currentColor;border-radius:999px;padding:5px 7px}.ecosystem.independent{color:var(--accent)}.ecosystem.official{color:var(--warm)}h3{font-family:ui-sans-serif,system-ui,sans-serif;font-size:21px;line-height:1.08;letter-spacing:-.035em;margin:0 0 8px}.message{font-family:ui-sans-serif,system-ui,sans-serif;font-size:13px;line-height:1.45;color:var(--muted);white-space:pre-wrap;overflow-wrap:anywhere;margin:15px 0 auto;max-height:112px;overflow:hidden}.why{border-top:1px solid var(--line);padding-top:10px;margin-top:15px;line-height:1.45}.empty{border:1px dashed var(--line);padding:28px;border-radius:16px;color:var(--muted);grid-column:1/-1}
|
||||||
|
footer{border-top:1px solid var(--line);padding:24px 0 36px;display:flex;justify-content:space-between;color:var(--muted);font-size:11px}
|
||||||
|
@media(max-width:780px){.hero{grid-template-columns:1fr}.grid{grid-template-columns:1fr}.stats{grid-template-columns:1fr 1fr}.stat:nth-child(2n){border-right:0}.stat:nth-child(-n+3){border-bottom:1px solid var(--line)}.section-head{align-items:start;flex-direction:column}.section-head p{text-align:left}}
|
||||||
|
</style></head><body><div class="wrap">
|
||||||
|
<header><div class="brand">notplanned / claude</div><div class="nav"><a href="/">scout</a><a href="/trending/">trending</a><a href="/claude.json">claude.json</a><a href="https://github.com/search?q=Claude&type=commits" target="_blank" rel="noreferrer">source ↗</a></div></header>
|
||||||
|
<section class="hero"><h1>claude signal.</h1><p class="dek">A bounded, deterministic mirror of public GitHub commits attributed to Claude, Claude Code, or Claude co-authorship. The independent ecosystem stays visible beside official Anthropic work.</p></section>
|
||||||
|
<section class="stats"><div class="stat"><strong>__COUNT__</strong><span>commits</span></div><div class="stat"><strong>__INDEPENDENT__</strong><span>independent</span></div><div class="stat"><strong>__OFFICIAL__</strong><span>official</span></div><div class="stat"><strong>__REPOS__</strong><span>repos</span></div><div class="stat"><strong>__QUERIES__</strong><span>bounded queries</span></div></section>
|
||||||
|
<p class="note">Updated __UPDATED__ UTC. Search window: last 30 days. Results are deduped by repository + commit SHA, sorted newest first, and categorized with fixed keyword rules. GitHub search can be incomplete when its public rate limit is low.</p>
|
||||||
|
<section><div class="section-head"><h2>independent ecosystem</h2><p>Public repositories outside the Anthropic organization: the clearest demand/discovery signal for what people are asking Claude to build.</p></div><div class="grid">__INDEPENDENT_CARDS__</div></section>
|
||||||
|
<section><div class="section-head"><h2>official Anthropic</h2><p>Anthropic-owned repositories are separated so first-party implementation work does not obscure independent activity.</p></div><div class="grid">__OFFICIAL_CARDS__</div></section>
|
||||||
|
<footer><span>notplanned · Claude activity scout</span><span>deterministic refresh · no per-scan model call</span></footer>
|
||||||
|
</div></body></html>
|
||||||
+3185
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,320 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build a bounded, deterministic view of Claude-attributed GitHub commits."""
|
||||||
|
import argparse
|
||||||
|
import datetime as dt
|
||||||
|
import html
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parent
|
||||||
|
LOOKBACK_DAYS = 30
|
||||||
|
MAX_RESULTS_PER_QUERY = 50
|
||||||
|
SEARCHES = [
|
||||||
|
"author:claude[bot]",
|
||||||
|
"committer:claude[bot]",
|
||||||
|
"author:claude",
|
||||||
|
"committer:claude",
|
||||||
|
'"Co-authored-by: Claude"',
|
||||||
|
]
|
||||||
|
COAUTHOR_RE = re.compile(r"(?im)^\s*co-authored-by:\s*(.*?)\s*<([^>]+)>\s*$")
|
||||||
|
|
||||||
|
|
||||||
|
class GithubApiError(RuntimeError):
|
||||||
|
def __init__(self, status, detail, headers):
|
||||||
|
super().__init__(detail)
|
||||||
|
self.status = status
|
||||||
|
self.headers = headers
|
||||||
|
|
||||||
|
|
||||||
|
def api(url):
|
||||||
|
req = urllib.request.Request(url, headers={
|
||||||
|
"User-Agent": "notplanned-claude-scout/0.1",
|
||||||
|
"Accept": "application/vnd.github+json",
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=20) as response:
|
||||||
|
return json.load(response), dict(response.headers)
|
||||||
|
except urllib.error.HTTPError as error:
|
||||||
|
try:
|
||||||
|
body = json.load(error)
|
||||||
|
detail = body.get("message", "GitHub API request failed")
|
||||||
|
except Exception:
|
||||||
|
detail = "GitHub API request failed"
|
||||||
|
raise GithubApiError(error.code, detail, dict(error.headers)) from error
|
||||||
|
|
||||||
|
|
||||||
|
def esc(value):
|
||||||
|
return html.escape(str(value), quote=True)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_date(value):
|
||||||
|
try:
|
||||||
|
return dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||||
|
except Exception:
|
||||||
|
return dt.datetime.min.replace(tzinfo=dt.timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
def age_days(value, now):
|
||||||
|
return max(0, (now - parse_date(value)).days)
|
||||||
|
|
||||||
|
|
||||||
|
def identity_text(identity):
|
||||||
|
if not identity:
|
||||||
|
return ""
|
||||||
|
return " ".join(str(identity.get(key) or "") for key in ("login", "name", "email")).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def is_claude_identity(identity):
|
||||||
|
text = identity_text(identity)
|
||||||
|
return bool(re.search(r"(?:claude\s*\[bot\]|claude\s+code|\bclaude\b)", text))
|
||||||
|
|
||||||
|
|
||||||
|
def item_identity(item, role):
|
||||||
|
commit_identity = (item.get("commit") or {}).get(role) or {}
|
||||||
|
account = item.get(role) or {}
|
||||||
|
return {
|
||||||
|
"login": account.get("login") or "",
|
||||||
|
"name": commit_identity.get("name") or "",
|
||||||
|
"email": commit_identity.get("email") or "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def coauthors(message):
|
||||||
|
return [
|
||||||
|
{"name": name.strip(), "email": email.strip()}
|
||||||
|
for name, email in COAUTHOR_RE.findall(message or "")
|
||||||
|
if is_claude_identity({"name": name, "email": email})
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def category(message):
|
||||||
|
text = (message or "").lower()
|
||||||
|
buckets = [
|
||||||
|
("browser", ["browser", "playwright", "puppeteer", "chrome", "screenshot"]),
|
||||||
|
("integration", ["mcp", "api", "webhook", "integration", "plugin", "connector"]),
|
||||||
|
("workflow", ["workflow", "task", "agent", "command", "hook", "automation"]),
|
||||||
|
("context", ["context", "memory", "session", "history", "resume", "checkpoint"]),
|
||||||
|
("interface", ["ui", "ux", "sidebar", "dashboard", "panel", "menu"]),
|
||||||
|
("testing", ["test", "spec", "fixture", "assert", "coverage"]),
|
||||||
|
("infra", ["ci", "build", "deploy", "docker", "release", "dependency"]),
|
||||||
|
("docs", ["readme", "docs", "documentation", "changelog"]),
|
||||||
|
]
|
||||||
|
for name, words in buckets:
|
||||||
|
if any(word in text for word in words):
|
||||||
|
return name
|
||||||
|
return "implementation"
|
||||||
|
|
||||||
|
|
||||||
|
def attribution_kind(author_match, committer_match, coauthor_match):
|
||||||
|
kinds = []
|
||||||
|
if author_match:
|
||||||
|
kinds.append("author")
|
||||||
|
if committer_match:
|
||||||
|
kinds.append("committer")
|
||||||
|
if coauthor_match:
|
||||||
|
kinds.append("co-author")
|
||||||
|
return " + ".join(kinds)
|
||||||
|
|
||||||
|
|
||||||
|
def candidate(item, query):
|
||||||
|
commit = item.get("commit") or {}
|
||||||
|
message = commit.get("message") or ""
|
||||||
|
author = item_identity(item, "author")
|
||||||
|
committer = item_identity(item, "committer")
|
||||||
|
claude_coauthors = coauthors(message)
|
||||||
|
author_match = is_claude_identity(author)
|
||||||
|
committer_match = is_claude_identity(committer)
|
||||||
|
coauthor_match = bool(claude_coauthors)
|
||||||
|
if not (author_match or committer_match or coauthor_match):
|
||||||
|
return None
|
||||||
|
|
||||||
|
repository = item.get("repository") or {}
|
||||||
|
repo = repository.get("full_name") or ""
|
||||||
|
if not repo:
|
||||||
|
return None
|
||||||
|
timestamp = (committer.get("date") or author.get("date") or "")
|
||||||
|
# GitHub returns dates inside commit.author/commit.committer, not the flattened identities.
|
||||||
|
timestamp = (commit.get("committer") or {}).get("date") or (commit.get("author") or {}).get("date") or timestamp
|
||||||
|
official = repo.lower().startswith("anthropics/")
|
||||||
|
return {
|
||||||
|
"repo": repo,
|
||||||
|
"repo_url": repository.get("html_url") or f"https://github.com/{repo}",
|
||||||
|
"commit_sha": item.get("sha") or "",
|
||||||
|
"commit_url": item.get("html_url") or "",
|
||||||
|
"commit_message": message[:4000],
|
||||||
|
"subject": (message.splitlines() or ["(no commit message)"])[0][:240],
|
||||||
|
"author": author,
|
||||||
|
"committer": committer,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"attribution_kind": attribution_kind(author_match, committer_match, coauthor_match),
|
||||||
|
"coauthors": claude_coauthors,
|
||||||
|
"category": category(message),
|
||||||
|
"ecosystem": "official Anthropic" if official else "independent",
|
||||||
|
"official_anthropic": official,
|
||||||
|
"matched_queries": [query],
|
||||||
|
"_author_match": author_match,
|
||||||
|
"_committer_match": committer_match,
|
||||||
|
"_coauthor_match": coauthor_match,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def merge(existing, incoming):
|
||||||
|
for field in ("_author_match", "_committer_match", "_coauthor_match"):
|
||||||
|
existing[field] = existing[field] or incoming[field]
|
||||||
|
for query in incoming["matched_queries"]:
|
||||||
|
if query not in existing["matched_queries"]:
|
||||||
|
existing["matched_queries"].append(query)
|
||||||
|
if not existing["coauthors"] and incoming["coauthors"]:
|
||||||
|
existing["coauthors"] = incoming["coauthors"]
|
||||||
|
existing["attribution_kind"] = attribution_kind(
|
||||||
|
existing["_author_match"], existing["_committer_match"], existing["_coauthor_match"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def rate_snapshot(headers):
|
||||||
|
if not headers:
|
||||||
|
return {}
|
||||||
|
return {
|
||||||
|
key: headers.get(value)
|
||||||
|
for key, value in (
|
||||||
|
("limit", "X-RateLimit-Limit"),
|
||||||
|
("remaining", "X-RateLimit-Remaining"),
|
||||||
|
("reset", "X-RateLimit-Reset"),
|
||||||
|
)
|
||||||
|
if headers.get(value) is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build():
|
||||||
|
now = dt.datetime.now(dt.timezone.utc)
|
||||||
|
cutoff = (now - dt.timedelta(days=LOOKBACK_DAYS)).date().isoformat()
|
||||||
|
rows = {}
|
||||||
|
errors = []
|
||||||
|
query_log = []
|
||||||
|
last_rate = {}
|
||||||
|
filtered_future = 0
|
||||||
|
|
||||||
|
for term in SEARCHES:
|
||||||
|
today = now.date().isoformat()
|
||||||
|
query = f"{term} committer-date:>={cutoff} committer-date:<={today}"
|
||||||
|
url = "https://api.github.com/search/commits?" + urllib.parse.urlencode({
|
||||||
|
"q": query,
|
||||||
|
"sort": "committer-date",
|
||||||
|
"order": "desc",
|
||||||
|
"per_page": MAX_RESULTS_PER_QUERY,
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
payload, headers = api(url)
|
||||||
|
last_rate = rate_snapshot(headers) or last_rate
|
||||||
|
items = payload.get("items", [])
|
||||||
|
query_log.append({"term": term, "query": query, "returned": len(items), "total_count": payload.get("total_count", 0)})
|
||||||
|
except GithubApiError as error:
|
||||||
|
last_rate = rate_snapshot(error.headers) or last_rate
|
||||||
|
query_log.append({"term": term, "query": query, "returned": 0, "error": str(error)})
|
||||||
|
errors.append({"term": term, "status": error.status, "error": str(error)})
|
||||||
|
continue
|
||||||
|
except Exception as error:
|
||||||
|
query_log.append({"term": term, "query": query, "returned": 0, "error": type(error).__name__})
|
||||||
|
errors.append({"term": term, "error": type(error).__name__})
|
||||||
|
continue
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
row = candidate(item, term)
|
||||||
|
if not row or not row["commit_sha"]:
|
||||||
|
continue
|
||||||
|
if parse_date(row["timestamp"]) > now:
|
||||||
|
filtered_future += 1
|
||||||
|
continue
|
||||||
|
key = (row["repo"].lower(), row["commit_sha"])
|
||||||
|
if key in rows:
|
||||||
|
merge(rows[key], row)
|
||||||
|
else:
|
||||||
|
rows[key] = row
|
||||||
|
|
||||||
|
candidates = list(rows.values())
|
||||||
|
candidates.sort(key=lambda row: (
|
||||||
|
1 if not row["official_anthropic"] else 0,
|
||||||
|
row["timestamp"],
|
||||||
|
row["repo"].lower(),
|
||||||
|
row["commit_sha"],
|
||||||
|
), reverse=True)
|
||||||
|
for row in candidates:
|
||||||
|
for field in ("_author_match", "_committer_match", "_coauthor_match"):
|
||||||
|
del row[field]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"generated_at": now.isoformat(),
|
||||||
|
"source": "https://api.github.com/search/commits",
|
||||||
|
"rules": {
|
||||||
|
"lookback_days": LOOKBACK_DAYS,
|
||||||
|
"max_results_per_query": MAX_RESULTS_PER_QUERY,
|
||||||
|
"dedupe_key": "repo + commit_sha",
|
||||||
|
"searches": SEARCHES,
|
||||||
|
"official_definition": "repo full_name starts with anthropics/",
|
||||||
|
},
|
||||||
|
"api": {"rate_limit": last_rate, "queries": query_log},
|
||||||
|
"filters": {"future_timestamps_excluded": filtered_future},
|
||||||
|
"candidates": candidates,
|
||||||
|
"errors": errors,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def identity_label(identity):
|
||||||
|
return identity.get("login") or identity.get("name") or identity.get("email") or "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def card(row):
|
||||||
|
authors = f"author {identity_label(row['author'])} · committer {identity_label(row['committer'])}"
|
||||||
|
if row["coauthors"]:
|
||||||
|
authors += " · co-author " + ", ".join(x["name"] for x in row["coauthors"])
|
||||||
|
return f"""<a class=\"commit-card\" href=\"{esc(row['commit_url'])}\" target=\"_blank\" rel=\"noreferrer\">
|
||||||
|
<div class=\"topline\"><span class=\"repo\">{esc(row['repo'])}</span><span class=\"ecosystem {'official' if row['official_anthropic'] else 'independent'}\">{esc(row['ecosystem'])}</span></div>
|
||||||
|
<h3>{esc(row['subject'])}</h3>
|
||||||
|
<div class=\"meta\">{esc(row['category'])} · {esc(row['attribution_kind'])} · {esc(row['timestamp'])}</div>
|
||||||
|
<p class=\"message\">{esc(row['commit_message'])}</p>
|
||||||
|
<div class=\"why\">{esc(authors)}</div>
|
||||||
|
</a>"""
|
||||||
|
|
||||||
|
|
||||||
|
def render(out_html, out_json):
|
||||||
|
data = build()
|
||||||
|
independent = [row for row in data["candidates"] if not row["official_anthropic"]]
|
||||||
|
official = [row for row in data["candidates"] if row["official_anthropic"]]
|
||||||
|
template = (ROOT / "claude-template.html").read_text()
|
||||||
|
updated = data["generated_at"][:16].replace("T", " ")
|
||||||
|
page = (template
|
||||||
|
.replace("__UPDATED__", esc(updated))
|
||||||
|
.replace("__COUNT__", str(len(data["candidates"])))
|
||||||
|
.replace("__INDEPENDENT__", str(len(independent)))
|
||||||
|
.replace("__OFFICIAL__", str(len(official)))
|
||||||
|
.replace("__REPOS__", str(len({row["repo"] for row in data["candidates"]})))
|
||||||
|
.replace("__QUERIES__", str(len(data["api"]["queries"])))
|
||||||
|
.replace("__INDEPENDENT_CARDS__", "\n".join(card(row) for row in independent) or '<div class="empty">No independent Claude-attributed commits cleared the bounded search window.</div>')
|
||||||
|
.replace("__OFFICIAL_CARDS__", "\n".join(card(row) for row in official) or '<div class="empty">No official Anthropic commits matched this pass.</div>')
|
||||||
|
)
|
||||||
|
out_html = pathlib.Path(out_html)
|
||||||
|
out_html.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
out_html.write_text(page)
|
||||||
|
pathlib.Path(out_json).write_text(json.dumps(data, indent=2) + "\n")
|
||||||
|
print(json.dumps({
|
||||||
|
"candidates": len(data["candidates"]),
|
||||||
|
"independent": len(independent),
|
||||||
|
"official": len(official),
|
||||||
|
"repos": len({row["repo"] for row in data["candidates"]}),
|
||||||
|
"queries": len(data["api"]["queries"]),
|
||||||
|
"errors": len(data["errors"]),
|
||||||
|
"out": str(out_html),
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--html-out", default=str(ROOT / "claude/index.html"))
|
||||||
|
parser.add_argument("--json-out", default=str(ROOT / "claude.json"))
|
||||||
|
args = parser.parse_args()
|
||||||
|
render(args.html_out, args.json_out)
|
||||||
+1966
File diff suppressed because it is too large.
Load diff
+2
-2
@@ -49,7 +49,7 @@ footer{padding:0 0 44px;display:flex;justify-content:space-between;gap:24px;colo
|
|||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<header>
|
<header>
|
||||||
<div class="brand">notplanned</div>
|
<div class="brand">notplanned</div>
|
||||||
<div class="tiny">a yolopreneur game</div>
|
<div class="tiny"><a href="/claude/">claude signal</a> · a yolopreneur game</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
@@ -92,4 +92,4 @@ footer{padding:0 0 44px;display:flex;justify-content:space-between;gap:24px;colo
|
|||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+2
-2
@@ -25,7 +25,7 @@ footer{border-top:1px solid var(--line);padding:24px 0 36px;display:flex;justify
|
|||||||
<body><div class="wrap">
|
<body><div class="wrap">
|
||||||
<header>
|
<header>
|
||||||
<div class="brand">notplanned</div>
|
<div class="brand">notplanned</div>
|
||||||
<div class="nav"><a href="/trending/">trending</a><a href="#curated">curated</a><a href="#radar">raw radar</a><a href="/trending.json">trending.json</a><a href="/radar.json">radar.json</a><a href="/curated.json">curated.json</a></div>
|
<div class="nav"><a href="/claude/">claude</a><a href="/trending/">trending</a><a href="#curated">curated</a><a href="#radar">raw radar</a><a href="/claude.json">claude.json</a><a href="/trending.json">trending.json</a><a href="/radar.json">radar.json</a><a href="/curated.json">curated.json</a></div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
@@ -60,4 +60,4 @@ footer{border-top:1px solid var(--line);padding:24px 0 36px;display:flex;justify
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer><span>notplanned · GitHub scout</span><span>deterministic radar refreshes every 6h · curation only when asked</span></footer>
|
<footer><span>notplanned · GitHub scout</span><span>deterministic radar refreshes every 6h · curation only when asked</span></footer>
|
||||||
</div></body></html>
|
</div></body></html>
|
||||||
@@ -13,7 +13,7 @@ header{min-height:76px;border-bottom:1px solid var(--line);display:flex;align-it
|
|||||||
footer{border-top:1px solid var(--line);padding:24px 0 36px;display:flex;justify-content:space-between;color:var(--muted);font-size:11px}
|
footer{border-top:1px solid var(--line);padding:24px 0 36px;display:flex;justify-content:space-between;color:var(--muted);font-size:11px}
|
||||||
@media(max-width:780px){.hero{grid-template-columns:1fr}.grid{grid-template-columns:1fr}.stats{grid-template-columns:1fr 1fr}.stat:nth-child(2){border-right:0}.stat:nth-child(-n+2){border-bottom:1px solid var(--line)}}
|
@media(max-width:780px){.hero{grid-template-columns:1fr}.grid{grid-template-columns:1fr}.stats{grid-template-columns:1fr 1fr}.stat:nth-child(2){border-right:0}.stat:nth-child(-n+2){border-bottom:1px solid var(--line)}}
|
||||||
</style></head><body><div class="wrap">
|
</style></head><body><div class="wrap">
|
||||||
<header><div class="brand">notplanned / trending</div><div class="nav"><a href="/">scout</a><a href="/trending.json">trending.json</a><a href="https://github.com/trending" target="_blank">source ↗</a></div></header>
|
<header><div class="brand">notplanned / trending</div><div class="nav"><a href="/">scout</a><a href="/claude/">claude</a><a href="/trending.json">trending.json</a><a href="https://github.com/trending" target="_blank">source ↗</a></div></header>
|
||||||
<section class="hero"><h1>what's hot.</h1><p class="dek">A deterministic mirror of GitHub Trending. Daily, weekly, and monthly lists are fetched directly from GitHub, merged by repository, and left uninterpreted.</p></section>
|
<section class="hero"><h1>what's hot.</h1><p class="dek">A deterministic mirror of GitHub Trending. Daily, weekly, and monthly lists are fetched directly from GitHub, merged by repository, and left uninterpreted.</p></section>
|
||||||
<section class="stats">
|
<section class="stats">
|
||||||
<div class="stat"><strong>__DAILY__</strong><span>daily</span></div>
|
<div class="stat"><strong>__DAILY__</strong><span>daily</span></div>
|
||||||
@@ -24,4 +24,4 @@ footer{border-top:1px solid var(--line);padding:24px 0 36px;display:flex;justify
|
|||||||
<p class="note">Updated __UPDATED__ UTC. Badges preserve GitHub's rank for each time window and, when GitHub exposes it, stars gained in that window. “2/3” or “3/3” means the repo appears across multiple Trending windows; it is not an AI score.</p>
|
<p class="note">Updated __UPDATED__ UTC. Badges preserve GitHub's rank for each time window and, when GitHub exposes it, stars gained in that window. “2/3” or “3/3” means the repo appears across multiple Trending windows; it is not an AI score.</p>
|
||||||
<section class="grid">__CARDS__</section>
|
<section class="grid">__CARDS__</section>
|
||||||
<footer><span>notplanned · trending feed</span><span>source: github.com/trending</span></footer>
|
<footer><span>notplanned · trending feed</span><span>source: github.com/trending</span></footer>
|
||||||
</div></body></html>
|
</div></body></html>
|
||||||
Reference in new issue
Block a user