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
60 changes: 59 additions & 1 deletion .github/repository-metrics-traffic.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
34 changes: 17 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -181,28 +194,15 @@ The scanners are guardrails, not a substitute for human review. See

<sub>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.</sub>

### Repository growth
### Clone activity

<p align="center">
<a href="https://github.com/JNHFlow21/trove/stargazers">
<img width="800" src="https://raw.githubusercontent.com/JNHFlow21/trove/metrics/repository-metrics.svg" alt="TROVE repository growth and traffic metrics">
<a href="https://github.com/JNHFlow21/trove">
<img width="800" src="https://raw.githubusercontent.com/JNHFlow21/trove/metrics/repository-metrics.svg" alt="TROVE rolling 14-day clone activity and repository metrics">
</a>
</p>

<sub>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.</sub>

## 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)
<sub>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.</sub>

## Contributing

Expand Down
34 changes: 17 additions & 17 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

## 项目数据

| 公开指标 | 实时数据或最近一次维护者可见数据 |
Expand All @@ -145,28 +158,15 @@ gitleaks git --redact

<sub>数据快照日期:2026-08-10。GitHub 只向仓库维护者提供克隆与独立访客数据,因此这里采用注明日期的透明快照,而不是需要私密 Token 的公开徽章。</sub>

### 仓库增长曲线
### 克隆趋势

<p align="center">
<a href="https://github.com/JNHFlow21/trove/stargazers">
<img width="800" src="https://raw.githubusercontent.com/JNHFlow21/trove/metrics/repository-metrics.svg" alt="TROVE 仓库增长与流量指标">
<a href="https://github.com/JNHFlow21/trove">
<img width="800" src="https://raw.githubusercontent.com/JNHFlow21/trove/metrics/repository-metrics.svg" alt="TROVE 最近 14 天克隆趋势与仓库指标">
</a>
</p>

<sub>Star、Fork 与 Commit 会在每次获得新 Star 后及每周自动刷新。由于 GitHub 不允许 Actions Token 读取仓库 Traffic,流量卡片使用图中注明日期的仓库所有者聚合快照;README 中不嵌入任何长期 Token。</sub>

## 文档

- [架构](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)
<sub>曲线展示 GitHub Traffic 最近 14 天滚动窗口内的累计克隆次数。Traffic 序列采用注明日期的仓库所有者聚合快照;侧边的 Star、Fork 与 Commit 会在每次获得新 Star 后及每周自动刷新。README 中不嵌入任何长期 Token。</sub>

## 参与贡献

Expand Down
89 changes: 43 additions & 46 deletions scripts/render_repository_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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())
Expand All @@ -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)
Expand All @@ -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"],
Expand All @@ -113,7 +106,6 @@ def fetch_snapshot(
"commits": commit_count,
**traffic,
"traffic_live": traffic_live,
"starred_at": sorted(starred_at),
}


Expand All @@ -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"

Expand Down Expand Up @@ -202,20 +200,19 @@ def y_for(value: int) -> float:
)

empty_note = ""
if stars == 0:
empty_note = '<text x="334" y="292" text-anchor="middle" class="empty">Waiting for the first star</text>'
if total_clones == 0:
empty_note = '<text x="334" y="292" text-anchor="middle" class="empty">No clones in this 14-day window</text>'

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'''<svg xmlns="http://www.w3.org/2000/svg" width="960" height="560" viewBox="0 0 960 560" role="img" aria-labelledby="title desc">
<title id="title">{repository} repository metrics</title>
<desc id="desc">Star growth curve with stars, forks, commits, visitors, cloners, and clone totals.</desc>
<desc id="desc">Cumulative clone curve for the rolling 14-day Traffic window with stars, forks, commits, visitors, cloners, and clone totals.</desc>
<style>
.background {{ fill: #ffffff; }}
.outline {{ fill: none; stroke: #171717; stroke-width: 2.4; }}
Expand All @@ -239,8 +236,8 @@ def y_for(value: int) -> float:
<rect class="outline" x="12" y="12" width="936" height="536" rx="22"/>
<text x="54" y="61" class="title">Repository Pulse</text>
<text x="54" y="88" class="subtitle">{repository} · privacy-safe public activity</text>
<path d="M 844 47 l 8 16 18 3 -13 12 3 18 -16 -9 -16 9 3 -18 -13 -12 18 -3 z" fill="#18a558" opacity=".9"/>
<text x="64" y="128" class="section">Stars over time</text>
<path d="M 844 45 v34 m-13 -13 13 13 13 -13 M 825 92 h38" fill="none" stroke="#18a558" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<text x="64" y="128" class="section">Total clones over time · rolling 14 days</text>
{''.join(grid)}
<line x1="{chart_x:.1f}" y1="{baseline:.1f}" x2="{chart_x + chart_w:.1f}" y2="{baseline:.1f}" class="outline"/>
<path d="{area_path}" class="area"/>
Expand Down
6 changes: 5 additions & 1 deletion tests/test_documentation_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ def test_readme_is_ordered_artifact_walkthrough(self):
headings = (
'## Why TROVE', '## Architecture', '## Core capabilities',
'## Quick start from source', '## Privacy and safety boundary',
'## Repository map', '## Documentation',
'## Repository map', '## Documentation', '## Project activity',
'## Contributing', '## License',
)
positions = [readme.index(item) for item in headings]
self.assertEqual(positions, sorted(positions))
Expand All @@ -142,6 +143,9 @@ def test_readme_is_ordered_artifact_walkthrough(self):
self.assertIn(chart, chinese)
self.assertNotIn('api.star-history.com', readme)
self.assertNotIn('api.star-history.com', chinese)
chinese_headings = ('## 文档', '## 项目数据', '## 参与贡献', '## 许可证')
chinese_positions = [chinese.index(item) for item in chinese_headings]
self.assertEqual(chinese_positions, sorted(chinese_positions))

def test_generated_reference_is_byte_identical_to_catalog(self):
self.assertEqual(
Expand Down
Loading