From 9c4cd636b5f8302519bc2587926673df199839c7 Mon Sep 17 00:00:00 2001 From: JNHFlow21 Date: Tue, 11 Aug 2026 01:11:42 -0400 Subject: [PATCH] feat: chart rolling clone activity near README footer --- .github/repository-metrics-traffic.json | 60 ++++++++++++++++- README.md | 34 +++++----- README.zh-CN.md | 34 +++++----- scripts/render_repository_metrics.py | 89 ++++++++++++------------- tests/test_documentation_contract.py | 6 +- tests/test_repository_metrics.py | 21 +++--- 6 files changed, 154 insertions(+), 90 deletions(-) diff --git a/.github/repository-metrics-traffic.json b/.github/repository-metrics-traffic.json index fe147d9..f46924e 100644 --- a/.github/repository-metrics-traffic.json +++ b/.github/repository-metrics-traffic.json @@ -3,5 +3,63 @@ "views_14d": 0, "unique_cloners_14d": 15, "clones_14d": 21, - "traffic_as_of": "2026-08-10T00:00:00Z" + "traffic_as_of": "2026-08-10T00:00:00Z", + "clone_series_14d": [ + { + "count": 0, + "timestamp": "2026-07-27T00:00:00Z" + }, + { + "count": 15, + "timestamp": "2026-07-28T00:00:00Z" + }, + { + "count": 2, + "timestamp": "2026-07-29T00:00:00Z" + }, + { + "count": 1, + "timestamp": "2026-07-30T00:00:00Z" + }, + { + "count": 0, + "timestamp": "2026-07-31T00:00:00Z" + }, + { + "count": 0, + "timestamp": "2026-08-01T00:00:00Z" + }, + { + "count": 0, + "timestamp": "2026-08-02T00:00:00Z" + }, + { + "count": 0, + "timestamp": "2026-08-03T00:00:00Z" + }, + { + "count": 1, + "timestamp": "2026-08-04T00:00:00Z" + }, + { + "count": 0, + "timestamp": "2026-08-05T00:00:00Z" + }, + { + "count": 0, + "timestamp": "2026-08-06T00:00:00Z" + }, + { + "count": 0, + "timestamp": "2026-08-07T00:00:00Z" + }, + { + "count": 1, + "timestamp": "2026-08-08T00:00:00Z" + }, + { + "count": 1, + "timestamp": "2026-08-09T00:00:00Z" + } + ] } diff --git a/README.md b/README.md index 6685e54..5ff8cac 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,19 @@ The scanners are guardrails, not a substitute for human review. See | `skills` | Outcome-oriented agent Skills and generated manifest | | `scripts` | Build, test, privacy, release, benchmark, and migration gates | +## Documentation + +- [Architecture](docs/architecture.md) +- [MCP packs and trust boundary](docs/mcp.md) +- [Capability reference](docs/capability-map.md) +- [Protocol](docs/protocol.md) +- [Provider SDK](docs/provider-sdk.md) +- [WeChat Provider](docs/providers/wechat.md) +- [Operations and recovery](docs/operations.md) +- [Testing](docs/testing.md) +- [Release model](docs/release.md) +- [Roadmap](docs/roadmap.md) + ## Project activity | Public signal | Live or latest owner-visible value | @@ -181,28 +194,15 @@ The scanners are guardrails, not a substitute for human review. See Traffic snapshot: 2026-08-10. GitHub exposes clone and unique-visitor analytics only to maintainers, so those values are a dated, transparent snapshot rather than a token-backed public badge. -### Repository growth +### Clone activity

- - TROVE repository growth and traffic metrics + + TROVE rolling 14-day clone activity and repository metrics

-Stars, forks, and commits refresh after every new star and once a week. Traffic cards use the dated aggregate owner snapshot shown in the chart because GitHub does not grant its Actions token access to repository Traffic. No long-lived token is embedded in this README. - -## Documentation - -- [Architecture](docs/architecture.md) -- [MCP packs and trust boundary](docs/mcp.md) -- [Capability reference](docs/capability-map.md) -- [Protocol](docs/protocol.md) -- [Provider SDK](docs/provider-sdk.md) -- [WeChat Provider](docs/providers/wechat.md) -- [Operations and recovery](docs/operations.md) -- [Testing](docs/testing.md) -- [Release model](docs/release.md) -- [Roadmap](docs/roadmap.md) +The curve shows cumulative clones inside GitHub's rolling 14-day Traffic window. The Traffic series is a dated aggregate owner snapshot; stars, forks, and commits in the side cards refresh after every new star and weekly. No long-lived token is embedded in this README. ## Contributing diff --git a/README.zh-CN.md b/README.zh-CN.md index 32b1a95..15122ea 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -134,6 +134,19 @@ gitleaks git --redact | `skills` | 面向结果的 Agent Skills 与清单 | | `scripts` | 构建、测试、隐私、发行、性能和迁移门禁 | +## 文档 + +- [架构](docs/architecture.md) +- [MCP 能力包与信任边界](docs/mcp.md) +- [能力参考](docs/capability-map.md) +- [协议](docs/protocol.md) +- [Provider SDK](docs/provider-sdk.md) +- [微信 Provider](docs/providers/wechat.md) +- [运维与恢复](docs/operations.md) +- [测试](docs/testing.md) +- [发行模型](docs/release.md) +- [路线图](docs/roadmap.md) + ## 项目数据 | 公开指标 | 实时数据或最近一次维护者可见数据 | @@ -145,28 +158,15 @@ gitleaks git --redact 数据快照日期:2026-08-10。GitHub 只向仓库维护者提供克隆与独立访客数据,因此这里采用注明日期的透明快照,而不是需要私密 Token 的公开徽章。 -### 仓库增长曲线 +### 克隆趋势

- - TROVE 仓库增长与流量指标 + + TROVE 最近 14 天克隆趋势与仓库指标

-Star、Fork 与 Commit 会在每次获得新 Star 后及每周自动刷新。由于 GitHub 不允许 Actions Token 读取仓库 Traffic,流量卡片使用图中注明日期的仓库所有者聚合快照;README 中不嵌入任何长期 Token。 - -## 文档 - -- [架构](docs/architecture.md) -- [MCP 能力包与信任边界](docs/mcp.md) -- [能力参考](docs/capability-map.md) -- [协议](docs/protocol.md) -- [Provider SDK](docs/provider-sdk.md) -- [微信 Provider](docs/providers/wechat.md) -- [运维与恢复](docs/operations.md) -- [测试](docs/testing.md) -- [发行模型](docs/release.md) -- [路线图](docs/roadmap.md) +曲线展示 GitHub Traffic 最近 14 天滚动窗口内的累计克隆次数。Traffic 序列采用注明日期的仓库所有者聚合快照;侧边的 Star、Fork 与 Commit 会在每次获得新 Star 后及每周自动刷新。README 中不嵌入任何长期 Token。 ## 参与贡献 diff --git a/scripts/render_repository_metrics.py b/scripts/render_repository_metrics.py index 7d2e371..0ad568f 100755 --- a/scripts/render_repository_metrics.py +++ b/scripts/render_repository_metrics.py @@ -4,7 +4,7 @@ from __future__ import annotations import argparse -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from html import escape import json import math @@ -57,6 +57,14 @@ def fetch_snapshot( "views_14d": int(views["count"]), "unique_cloners_14d": int(clones["uniques"]), "clones_14d": int(clones["count"]), + "clone_series_14d": [ + { + "timestamp": str(item["timestamp"]), + "count": int(item["count"]), + } + for item in clones.get("clones", []) + if isinstance(item, dict) and "timestamp" in item and "count" in item + ], "traffic_as_of": generated_at, } except HTTPError as error: @@ -68,6 +76,7 @@ def fetch_snapshot( "views_14d", "unique_cloners_14d", "clones_14d", + "clone_series_14d", "traffic_as_of", } missing = sorted(required - traffic_fallback.keys()) @@ -79,6 +88,7 @@ def fetch_snapshot( "views_14d": int(traffic_fallback["views_14d"]), "unique_cloners_14d": int(traffic_fallback["unique_cloners_14d"]), "clones_14d": int(traffic_fallback["clones_14d"]), + "clone_series_14d": list(traffic_fallback["clone_series_14d"]), "traffic_as_of": str(traffic_fallback["traffic_as_of"]), } _, commit_headers = _request_json(f"/repos/{encoded}/commits?per_page=1", token) @@ -87,23 +97,6 @@ def fetch_snapshot( last_page = re.search(r"[?&]page=(\d+)[^>]*>; rel=\"last\"", link) commit_count = int(last_page.group(1)) if last_page else 1 - starred_at: list[str] = [] - page = 1 - while True: - entries, _ = _request_json( - f"/repos/{encoded}/stargazers?per_page=100&page={page}", - token, - accept="application/vnd.github.star+json", - ) - starred_at.extend( - item["starred_at"] - for item in entries - if isinstance(item, dict) and isinstance(item.get("starred_at"), str) - ) - if len(entries) < 100: - break - page += 1 - return { "repository": repository, "created_at": repo["created_at"], @@ -113,7 +106,6 @@ def fetch_snapshot( "commits": commit_count, **traffic, "traffic_live": traffic_live, - "starred_at": sorted(starred_at), } @@ -131,39 +123,45 @@ def _compact(value: int) -> str: def render_svg(snapshot: dict[str, Any]) -> str: repository = escape(str(snapshot["repository"])) - created = _parse_time(str(snapshot["created_at"])) generated = _parse_time(str(snapshot["generated_at"])) - if generated <= created: - generated = created.replace(microsecond=0) - + traffic_as_of = _parse_time(str(snapshot.get("traffic_as_of", snapshot["generated_at"]))) stars = max(0, int(snapshot["stars"])) - star_dates = sorted( - date for date in (_parse_time(str(value)) for value in snapshot.get("starred_at", [])) - if created <= date <= generated + total_clones = max(0, int(snapshot["clones_14d"])) + clone_series = sorted( + ( + _parse_time(str(item["timestamp"])), + max(0, int(item["count"])), + ) + for item in snapshot.get("clone_series_14d", []) + if isinstance(item, dict) and "timestamp" in item and "count" in item ) + chart_start = clone_series[0][0] if clone_series else traffic_as_of - timedelta(days=13) + chart_end = clone_series[-1][0] if clone_series else traffic_as_of + if chart_end <= chart_start: + chart_end = chart_start + timedelta(days=1) chart_x, chart_y, chart_w, chart_h = 64.0, 150.0, 540.0, 292.0 baseline = chart_y + chart_h - seconds = max((generated - created).total_seconds(), 1.0) - y_max = max(stars, 1) + seconds = max((chart_end - chart_start).total_seconds(), 1.0) + series_total = sum(count for _, count in clone_series) + y_max = max(total_clones, series_total, 1) def x_for(date: datetime) -> float: - return chart_x + chart_w * max(0.0, min(1.0, (date - created).total_seconds() / seconds)) + return chart_x + chart_w * max(0.0, min(1.0, (date - chart_start).total_seconds() / seconds)) def y_for(value: int) -> float: return baseline - chart_h * max(0.0, min(1.0, value / y_max)) path = [f"M {chart_x:.1f} {y_for(0):.1f}"] - count = 0 - for date in star_dates: + cumulative = 0 + for date, count in clone_series: x = x_for(date) - path.append(f"L {x:.1f} {y_for(count):.1f}") - count += 1 - path.append(f"L {x:.1f} {y_for(count):.1f}") - if count < stars: - path.append(f"L {chart_x + chart_w:.1f} {y_for(stars):.1f}") - else: - path.append(f"L {chart_x + chart_w:.1f} {y_for(count):.1f}") + path.append(f"L {x:.1f} {y_for(cumulative):.1f}") + cumulative += count + path.append(f"L {x:.1f} {y_for(cumulative):.1f}") + if cumulative < total_clones: + cumulative = total_clones + path.append(f"L {chart_x + chart_w:.1f} {y_for(cumulative):.1f}") line_path = " ".join(path) area_path = f"{line_path} L {chart_x + chart_w:.1f} {baseline:.1f} Z" @@ -202,20 +200,19 @@ def y_for(value: int) -> float: ) empty_note = "" - if stars == 0: - empty_note = 'Waiting for the first star' + if total_clones == 0: + empty_note = 'No clones in this 14-day window' updated = generated.strftime("%Y-%m-%d UTC") - traffic_as_of = _parse_time(str(snapshot.get("traffic_as_of", snapshot["generated_at"]))) if snapshot.get("traffic_live", False): traffic_note = "GitHub Traffic: rolling 14-day owner view" else: traffic_note = f"GitHub Traffic owner snapshot: {traffic_as_of.strftime('%Y-%m-%d')}" - start_label = created.strftime("%Y-%m") - end_label = generated.strftime("%Y-%m") + start_label = chart_start.strftime("%Y-%m-%d") + end_label = chart_end.strftime("%Y-%m-%d") return f''' {repository} repository metrics - Star growth curve with stars, forks, commits, visitors, cloners, and clone totals. + Cumulative clone curve for the rolling 14-day Traffic window with stars, forks, commits, visitors, cloners, and clone totals.