Sync: update agents, paper-dynasty skills, sessions
- agents: issue-worker.md and pr-reviewer.md updated for standard branch naming (issue/<number>-<slug> instead of ai/<repo>#<number>) - paper-dynasty: updated SKILL.md, generate_summary, smoke_test, validate_database scripts; added ecosystem_status.sh and plan/ - plugins: updated marketplace submodules and blocklist - sessions: rotate session files, add session-analysis/ - settings: updated settings.json Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
25722c5164
commit
0fa8486e93
@ -30,10 +30,10 @@ You are an autonomous agent that fixes a single Gitea issue and opens a PR for h
|
||||
|
||||
5. **Explore the code.** Read relevant files. Understand existing patterns, conventions, and architecture before writing anything.
|
||||
|
||||
6. **Create a feature branch.**
|
||||
6. **Create a feature branch.** Use the branch name provided in your prompt.
|
||||
```bash
|
||||
# Use -B to handle retries where the branch may already exist
|
||||
git checkout -B ai/<repo>-<issue_number>
|
||||
git checkout -B <branch_name>
|
||||
```
|
||||
|
||||
7. **Implement the fix.** Follow the repo's existing conventions. Keep changes minimal and focused. Check imports. Don't over-engineer.
|
||||
@ -71,7 +71,7 @@ You are an autonomous agent that fixes a single Gitea issue and opens a PR for h
|
||||
|
||||
11. **Push the branch.**
|
||||
```bash
|
||||
git push -u origin ai/<repo>-<issue_number>
|
||||
git push -u origin <branch_name>
|
||||
```
|
||||
|
||||
12. **Create a PR** via `mcp__gitea-mcp__create_pull_request`:
|
||||
@ -80,7 +80,7 @@ You are an autonomous agent that fixes a single Gitea issue and opens a PR for h
|
||||
- `title`: "fix: <concise title> (#<issue_number>)"
|
||||
- `body`: Must start with `Closes #<issue_number>` on its own line (Gitea auto-close keyword), followed by a summary of changes, what was fixed, files changed, test results
|
||||
- `base`: main branch (usually "main")
|
||||
- `head`: "ai/<repo>-<issue_number>"
|
||||
- `head`: the branch name from your prompt
|
||||
|
||||
13. **Update labels.** Remove `status/in-progress` and add `status/pr-open` via the label MCP tools.
|
||||
|
||||
|
||||
@ -140,4 +140,4 @@ Or on failure:
|
||||
- **Be proportionate.** Don't REQUEST_CHANGES for trivial style differences or subjective preferences.
|
||||
- **Stay in scope.** Review only the PR's changes. Don't flag pre-existing issues in surrounding code.
|
||||
- **Respect CLAUDE.md.** The project's CLAUDE.md is the source of truth for conventions. If the code follows CLAUDE.md, approve it even if you'd prefer a different style.
|
||||
- **Consider the author.** PRs from `ai/` branches were created by the issue-worker agent. Be especially thorough on these — you're the safety net.
|
||||
- **Consider the author.** PRs from `issue/` branches were created by the issue-worker agent. Be especially thorough on these — you're the safety net.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
||||
{
|
||||
"fetchedAt": "2026-03-22T07:00:45.125Z",
|
||||
"fetchedAt": "2026-03-24T04:00:47.945Z",
|
||||
"plugins": [
|
||||
{
|
||||
"plugin": "code-review@claude-plugins-official",
|
||||
|
||||
@ -23,10 +23,10 @@
|
||||
"playground@claude-plugins-official": [
|
||||
{
|
||||
"scope": "user",
|
||||
"installPath": "/home/cal/.claude/plugins/cache/claude-plugins-official/playground/61c0597779bd",
|
||||
"version": "61c0597779bd",
|
||||
"installPath": "/home/cal/.claude/plugins/cache/claude-plugins-official/playground/15268f03d2f5",
|
||||
"version": "15268f03d2f5",
|
||||
"installedAt": "2026-02-18T19:51:28.422Z",
|
||||
"lastUpdated": "2026-03-21T01:00:49.236Z",
|
||||
"lastUpdated": "2026-03-23T20:15:51.541Z",
|
||||
"gitCommitSha": "261ce4fba4f2c314c490302158909a32e5889c88"
|
||||
}
|
||||
],
|
||||
@ -43,10 +43,10 @@
|
||||
"frontend-design@claude-plugins-official": [
|
||||
{
|
||||
"scope": "user",
|
||||
"installPath": "/home/cal/.claude/plugins/cache/claude-plugins-official/frontend-design/61c0597779bd",
|
||||
"version": "61c0597779bd",
|
||||
"installPath": "/home/cal/.claude/plugins/cache/claude-plugins-official/frontend-design/15268f03d2f5",
|
||||
"version": "15268f03d2f5",
|
||||
"installedAt": "2026-02-22T05:53:45.091Z",
|
||||
"lastUpdated": "2026-03-21T01:00:49.230Z",
|
||||
"lastUpdated": "2026-03-23T20:15:51.536Z",
|
||||
"gitCommitSha": "aa296ec81e8ccb49c9784f167c2c0aa625a86cec"
|
||||
}
|
||||
],
|
||||
@ -63,10 +63,10 @@
|
||||
"session@agent-toolkit": [
|
||||
{
|
||||
"scope": "user",
|
||||
"installPath": "/home/cal/.claude/plugins/cache/agent-toolkit/session/3.6.1",
|
||||
"version": "3.6.1",
|
||||
"installPath": "/home/cal/.claude/plugins/cache/agent-toolkit/session/3.7.0",
|
||||
"version": "3.7.0",
|
||||
"installedAt": "2026-03-18T23:37:09.034Z",
|
||||
"lastUpdated": "2026-03-21T00:15:51.940Z",
|
||||
"lastUpdated": "2026-03-23T18:00:49.746Z",
|
||||
"gitCommitSha": "8c6e15ce7c51ae53121ec12d8dceee3c8bf936c6"
|
||||
}
|
||||
],
|
||||
@ -173,12 +173,12 @@
|
||||
"atlassian@claude-plugins-official": [
|
||||
{
|
||||
"scope": "project",
|
||||
"projectPath": "/home/cal/work/esb-monorepo",
|
||||
"installPath": "/home/cal/.claude/plugins/cache/claude-plugins-official/atlassian/385c1469c567",
|
||||
"version": "385c1469c567",
|
||||
"installedAt": "2026-03-21T04:15:01.344Z",
|
||||
"lastUpdated": "2026-03-21T04:15:01.344Z",
|
||||
"gitCommitSha": "385c1469c567399970e1d3fc23687a0312aa63dc",
|
||||
"projectPath": "/home/cal/work/esb-monorepo"
|
||||
"gitCommitSha": "385c1469c567399970e1d3fc23687a0312aa63dc"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
"url": "https://github.com/anthropics/claude-plugins-official.git"
|
||||
},
|
||||
"installLocation": "/home/cal/.claude/plugins/marketplaces/claude-plugins-official",
|
||||
"lastUpdated": "2026-03-20T15:01:13.664Z"
|
||||
"lastUpdated": "2026-03-23T15:53:46.553Z"
|
||||
},
|
||||
"claude-code-plugins": {
|
||||
"source": {
|
||||
@ -13,7 +13,7 @@
|
||||
"repo": "anthropics/claude-code"
|
||||
},
|
||||
"installLocation": "/home/cal/.claude/plugins/marketplaces/claude-code-plugins",
|
||||
"lastUpdated": "2026-03-22T07:00:48.599Z"
|
||||
"lastUpdated": "2026-03-24T04:01:11.681Z"
|
||||
},
|
||||
"agent-toolkit": {
|
||||
"source": {
|
||||
@ -21,7 +21,7 @@
|
||||
"repo": "St0nefish/agent-toolkit"
|
||||
},
|
||||
"installLocation": "/home/cal/.claude/plugins/marketplaces/agent-toolkit",
|
||||
"lastUpdated": "2026-03-22T05:00:49.760Z",
|
||||
"lastUpdated": "2026-03-24T03:30:48.201Z",
|
||||
"autoUpdate": true
|
||||
},
|
||||
"cal-claude-plugins": {
|
||||
@ -30,7 +30,7 @@
|
||||
"url": "https://git.manticorum.com/cal/claude-plugins.git"
|
||||
},
|
||||
"installLocation": "/home/cal/.claude/plugins/marketplaces/cal-claude-plugins",
|
||||
"lastUpdated": "2026-03-21T04:01:23.285Z",
|
||||
"lastUpdated": "2026-03-23T17:31:33.371Z",
|
||||
"autoUpdate": true
|
||||
}
|
||||
}
|
||||
@ -1 +1 @@
|
||||
Subproject commit 266237bb258d111433f099d86d735bd9e780569e
|
||||
Subproject commit 070f1d7f7485084a5336c6635593482c22c4387d
|
||||
@ -1 +1 @@
|
||||
Subproject commit 61c0597779bd2d670dcd4fbbf37c66aa19eb2ce6
|
||||
Subproject commit 15268f03d2f560b955584a283927ed175569678c
|
||||
101
session-analysis/state.json
Normal file
101
session-analysis/state.json
Normal file
@ -0,0 +1,101 @@
|
||||
{
|
||||
"version": 1,
|
||||
"last_run": "2026-03-23T17:36:39Z",
|
||||
"analyzed": {
|
||||
"1e366762-a0e9-4620-b5d4-352b18bf4603": {
|
||||
"project_slug": "-home-cal-work",
|
||||
"analyzed_at": "2026-03-23T17:36:39Z",
|
||||
"session_started": "2026-03-23T17:33:09Z"
|
||||
},
|
||||
"16964e75-0eb6-4a9b-ab7c-a1e529ae6ff8": {
|
||||
"project_slug": "-home-cal-work-esb-monorepo",
|
||||
"analyzed_at": "2026-03-23T17:36:39Z",
|
||||
"session_started": "2026-02-24T17:11:58Z"
|
||||
},
|
||||
"f1d7aeee-ea5e-4eb8-b335-1c300c17ae53": {
|
||||
"project_slug": "-home-cal-work-esb-monorepo",
|
||||
"analyzed_at": "2026-03-23T17:36:39Z",
|
||||
"session_started": "2026-02-26T17:40:26Z"
|
||||
},
|
||||
"94e2f9b8-63a8-451d-9c49-3049793dab7a": {
|
||||
"project_slug": "-home-cal-work-esb-monorepo",
|
||||
"analyzed_at": "2026-03-23T17:36:39Z",
|
||||
"session_started": "2026-02-27T12:57:30Z"
|
||||
},
|
||||
"b55a6ece-a2d4-40c8-82bf-ca26fbf321b4": {
|
||||
"project_slug": "-home-cal-work-esb-monorepo",
|
||||
"analyzed_at": "2026-03-23T17:36:39Z",
|
||||
"session_started": "2026-03-04T16:11:14Z"
|
||||
},
|
||||
"a26ce1f8-67d1-46bf-8533-00d75a908e16": {
|
||||
"project_slug": "-home-cal-work-esb-monorepo",
|
||||
"analyzed_at": "2026-03-23T17:36:39Z",
|
||||
"session_started": "2026-03-05T14:23:33Z"
|
||||
},
|
||||
"1f46a730-a955-444b-950b-cf5676cbed44": {
|
||||
"project_slug": "-home-cal-work-esb-monorepo",
|
||||
"analyzed_at": "2026-03-23T17:36:39Z",
|
||||
"session_started": "2026-03-05T17:00:00Z"
|
||||
},
|
||||
"aef3ed6e-c0b8-4440-980a-5eb3fc066395": {
|
||||
"project_slug": "-home-cal-work-esb-monorepo",
|
||||
"analyzed_at": "2026-03-23T17:36:39Z",
|
||||
"session_started": "2026-03-05T18:00:00Z"
|
||||
},
|
||||
"3be575a2-ccf7-419d-b593-28de38900ce6": {
|
||||
"project_slug": "-home-cal-work-esb-monorepo",
|
||||
"analyzed_at": "2026-03-23T17:36:39Z",
|
||||
"session_started": "2026-03-05T19:00:00Z"
|
||||
},
|
||||
"0eb043e8-0c8f-496b-a002-d3df91156dbe": {
|
||||
"project_slug": "-home-cal-work-esb-monorepo",
|
||||
"analyzed_at": "2026-03-23T17:36:39Z",
|
||||
"session_started": "2026-03-09T00:00:00Z"
|
||||
},
|
||||
"76dd16c3-7f35-49a4-8bfe-9ac796dd8ea3": {
|
||||
"project_slug": "-home-cal-work-esb-monorepo",
|
||||
"analyzed_at": "2026-03-23T17:36:39Z",
|
||||
"session_started": "2026-03-10T00:00:00Z"
|
||||
},
|
||||
"a68078d0-88db-4306-bded-f493264760bc": {
|
||||
"project_slug": "-home-cal-work-esb-monorepo",
|
||||
"analyzed_at": "2026-03-23T17:36:39Z",
|
||||
"session_started": "2026-03-11T00:00:00Z"
|
||||
},
|
||||
"d10a068b-d3dc-4da9-b799-9175834eaed5": {
|
||||
"project_slug": "-home-cal-work-esb-monorepo",
|
||||
"analyzed_at": "2026-03-23T17:36:39Z",
|
||||
"session_started": "2026-03-12T00:00:00Z"
|
||||
},
|
||||
"2af0c500-0b5d-4755-9823-b12116704f2c": {
|
||||
"project_slug": "-home-cal-work-esb-monorepo",
|
||||
"analyzed_at": "2026-03-23T17:36:39Z",
|
||||
"session_started": "2026-03-12T18:04:19Z"
|
||||
},
|
||||
"372b5724-f70d-4787-aaed-de6aa157a2dc": {
|
||||
"project_slug": "-home-cal-work-esb-monorepo",
|
||||
"analyzed_at": "2026-03-23T17:36:39Z",
|
||||
"session_started": "2026-03-13T14:30:55Z"
|
||||
},
|
||||
"fb411708-ac74-4485-ab27-be9a46f0dba4": {
|
||||
"project_slug": "-home-cal-work-esb-monorepo",
|
||||
"analyzed_at": "2026-03-23T17:36:39Z",
|
||||
"session_started": "2026-03-18T14:09:15Z"
|
||||
},
|
||||
"cefc895c-cc54-4802-a333-23fe9d249a51": {
|
||||
"project_slug": "-home-cal-work-esb-monorepo",
|
||||
"analyzed_at": "2026-03-23T17:36:39Z",
|
||||
"session_started": "2026-03-19T15:34:10Z"
|
||||
},
|
||||
"583b5f8a-885a-4a0d-b62d-61744cd32cb7": {
|
||||
"project_slug": "-home-cal-work-esb-monorepo",
|
||||
"analyzed_at": "2026-03-23T17:36:39Z",
|
||||
"session_started": "2026-03-19T17:09:10Z"
|
||||
},
|
||||
"74fe7722-54a8-473c-a710-b3efa615d515": {
|
||||
"project_slug": "-home-cal-work-esb-monorepo",
|
||||
"analyzed_at": "2026-03-23T17:36:39Z",
|
||||
"session_started": "2026-03-20T14:20:17Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
sessions/1794866.json
Normal file
1
sessions/1794866.json
Normal file
@ -0,0 +1 @@
|
||||
{"pid":1794866,"sessionId":"0fa5054d-b5c6-4499-b59c-9a0f8fae56f5","cwd":"/mnt/NV2/Development/paper-dynasty","startedAt":1774267485456}
|
||||
1
sessions/1841495.json
Normal file
1
sessions/1841495.json
Normal file
@ -0,0 +1 @@
|
||||
{"pid":1841495,"sessionId":"d582937f-e7b1-4131-8046-993531618bc2","cwd":"/mnt/NV2/Development/claude-home","startedAt":1774271490814}
|
||||
1
sessions/2073728.json
Normal file
1
sessions/2073728.json
Normal file
@ -0,0 +1 @@
|
||||
{"pid":2073728,"sessionId":"1e366762-a0e9-4620-b5d4-352b18bf4603","cwd":"/home/cal/work","startedAt":1774287133224}
|
||||
1
sessions/2085347.json
Normal file
1
sessions/2085347.json
Normal file
@ -0,0 +1 @@
|
||||
{"pid":2085347,"sessionId":"c9852a3c-c4ff-4914-a22f-a6853f93d712","cwd":"/mnt/NV2/Development/major-domo","startedAt":1774287409057}
|
||||
1
sessions/2369320.json
Normal file
1
sessions/2369320.json
Normal file
@ -0,0 +1 @@
|
||||
{"pid":2369320,"sessionId":"5296e222-0748-4619-acbe-b8c7e5b5f297","cwd":"/mnt/NV2/Development/cookbook","startedAt":1774303725948}
|
||||
@ -1 +0,0 @@
|
||||
{"pid":579031,"sessionId":"61fc6bcf-3693-4320-8e96-d4d934dfa0a0","cwd":"/mnt/NV2/Development/claude-home","startedAt":1774067785547}
|
||||
@ -97,5 +97,6 @@
|
||||
}
|
||||
},
|
||||
"autoUpdatesChannel": "latest",
|
||||
"skipDangerousModePermissionPrompt": true
|
||||
"skipDangerousModePermissionPrompt": true,
|
||||
"voiceEnabled": true
|
||||
}
|
||||
|
||||
@ -33,6 +33,13 @@ description: Paper Dynasty baseball card game management. USE WHEN user mentions
|
||||
- "List all teams in season 5"
|
||||
- "Find active gauntlet runs"
|
||||
|
||||
**Ecosystem & Cross-Project**:
|
||||
- "PD status" / "ecosystem status" / "what needs work"
|
||||
- "Show PD ecosystem status" / "What's the status across all projects?"
|
||||
|
||||
**Growth & Engagement**:
|
||||
- "growth roadmap" / "engagement" / "user retention"
|
||||
|
||||
> **For deployment**, use the `deploy` skill instead.
|
||||
|
||||
---
|
||||
@ -274,5 +281,106 @@ $PD gauntlet list/teams/cleanup # Gauntlet operations
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-02-14
|
||||
**Version**: 2.6 (Added live series workflow, PotM documentation for both data sources)
|
||||
---
|
||||
|
||||
## Ecosystem Dashboard
|
||||
|
||||
Provides a cross-project view of all Paper Dynasty Gitea repos in a single terminal dashboard.
|
||||
|
||||
**Script**: `~/.claude/skills/paper-dynasty/scripts/ecosystem_status.sh`
|
||||
|
||||
**Trigger phrases**:
|
||||
- "Show PD ecosystem status"
|
||||
- "What's the status across all projects?"
|
||||
- "PD status" / "ecosystem status" / "what needs work"
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
# Requires GITEA_TOKEN in env (or auto-reads from gitea-mcp config)
|
||||
~/.claude/skills/paper-dynasty/scripts/ecosystem_status.sh
|
||||
```
|
||||
|
||||
**What it shows**:
|
||||
- Open issue count per repo
|
||||
- Open PR count per repo
|
||||
- Latest commit SHA + date per repo
|
||||
- Recent commits (last 3) per repo with author and message
|
||||
- Open issue titles grouped by repo (with labels)
|
||||
- Open PR titles grouped by repo (with branch info)
|
||||
- Cross-repo totals
|
||||
|
||||
**Repos covered**: paper-dynasty-database, paper-dynasty-discord, paper-dynasty-card-creation, paper-dynasty-website
|
||||
|
||||
**Auth**: Uses `GITEA_TOKEN` env var. If unset, attempts to read from `~/.config/claude-code/mcp-servers/gitea-mcp.json`.
|
||||
|
||||
---
|
||||
|
||||
## Initiative Tracker (`pd-plan`)
|
||||
|
||||
Local SQLite database tracking cross-project initiatives, priorities, and status.
|
||||
|
||||
**CLI**: `python ~/.claude/skills/paper-dynasty/plan/cli.py [command]`
|
||||
**Database**: `~/.claude/skills/paper-dynasty/plan/initiatives.db`
|
||||
|
||||
**Trigger phrases**:
|
||||
- "what should I work on" / "what's the priority"
|
||||
- "initiative status" / "pd-plan" / "show priorities"
|
||||
- "update initiative" / "mark done"
|
||||
|
||||
**Quick reference**:
|
||||
```bash
|
||||
PDP="python ~/.claude/skills/paper-dynasty/plan/cli.py"
|
||||
|
||||
$PDP summary # Dashboard — run at session start
|
||||
$PDP list # All active initiatives
|
||||
$PDP list --phase 1 # Phase 1 only
|
||||
$PDP list --repo discord # Filter by repo
|
||||
$PDP next # Highest priority non-blocked item
|
||||
$PDP next --repo discord # Next for a specific repo
|
||||
$PDP show 1 # Full details + activity log
|
||||
$PDP add "Title" --phase 1 --priority 20 --impact retention --size M --repos discord
|
||||
$PDP update 3 --status in_progress --actor pd-discord
|
||||
$PDP update 3 --note "Merged 8 PRs" --actor pd-ops
|
||||
$PDP update 3 --link "discord#104" # Append linked issue
|
||||
$PDP done 3 --actor pd-ops # Mark complete
|
||||
$PDP list --json # Machine-readable output
|
||||
```
|
||||
|
||||
**Session startup**: Always run `pd-plan summary` at the start of a Paper Dynasty session to understand current priorities.
|
||||
|
||||
---
|
||||
|
||||
## Growth Roadmap
|
||||
|
||||
High-level roadmap for Paper Dynasty player growth, engagement, and retention strategies.
|
||||
|
||||
**File**: `/mnt/NV2/Development/paper-dynasty/ROADMAP.md`
|
||||
|
||||
**Trigger phrases**:
|
||||
- "growth roadmap" / "engagement" / "user retention"
|
||||
- "what's planned" / "next features"
|
||||
|
||||
Load the roadmap file for context before discussing growth priorities, feature planning, or retention strategies. Use `pd-plan` for current status of specific initiatives.
|
||||
|
||||
---
|
||||
|
||||
## Specialized Agents
|
||||
|
||||
Dispatch work to these agents for their respective domains. Do not do their work inline — launch them explicitly.
|
||||
|
||||
| Agent | Model | Domain | Dispatch When |
|
||||
|-------|-------|--------|---------------|
|
||||
| `pd-database` | Opus | Database/API | Schema changes, endpoints, migrations, data model |
|
||||
| `pd-discord` | Opus | Discord bot | Commands, gameplay engine, bot UX |
|
||||
| `pd-cards` | Opus | Card pipeline | Card generation, ratings, scouting, rendering |
|
||||
| `pd-growth` | Opus | Product growth | Engagement, retention, roadmap prioritization |
|
||||
| `pd-ops` | Sonnet | Release ops | Merging PRs, deploys, branch cleanup, process |
|
||||
|
||||
PO agents (Opus) decide **what** to build. `pd-ops` ensures it **ships correctly**. Implementation is delegated to `engineer`, `issue-worker`, or `swarm-coder`.
|
||||
|
||||
**How to dispatch**: Mention the agent name explicitly, e.g. "use the pd-cards agent to regenerate scouting" or "dispatch to pd-database for this migration".
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-03-22
|
||||
**Version**: 2.8 (Added pd-plan initiative tracker, pd-ops agent, updated agent table with models)
|
||||
|
||||
1028
skills/paper-dynasty/plan/cli.py
Executable file
1028
skills/paper-dynasty/plan/cli.py
Executable file
File diff suppressed because it is too large
Load Diff
BIN
skills/paper-dynasty/plan/initiatives.db
Normal file
BIN
skills/paper-dynasty/plan/initiatives.db
Normal file
Binary file not shown.
5
skills/paper-dynasty/plan/pd_plan.egg-info/PKG-INFO
Normal file
5
skills/paper-dynasty/plan/pd_plan.egg-info/PKG-INFO
Normal file
@ -0,0 +1,5 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: pd-plan
|
||||
Version: 1.0.0
|
||||
Summary: Paper Dynasty initiative tracker — local SQLite CLI for cross-project priorities
|
||||
Requires-Python: >=3.10
|
||||
7
skills/paper-dynasty/plan/pd_plan.egg-info/SOURCES.txt
Normal file
7
skills/paper-dynasty/plan/pd_plan.egg-info/SOURCES.txt
Normal file
@ -0,0 +1,7 @@
|
||||
cli.py
|
||||
pyproject.toml
|
||||
pd_plan.egg-info/PKG-INFO
|
||||
pd_plan.egg-info/SOURCES.txt
|
||||
pd_plan.egg-info/dependency_links.txt
|
||||
pd_plan.egg-info/entry_points.txt
|
||||
pd_plan.egg-info/top_level.txt
|
||||
@ -0,0 +1 @@
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
[console_scripts]
|
||||
pd-plan = cli:main
|
||||
1
skills/paper-dynasty/plan/pd_plan.egg-info/top_level.txt
Normal file
1
skills/paper-dynasty/plan/pd_plan.egg-info/top_level.txt
Normal file
@ -0,0 +1 @@
|
||||
cli
|
||||
12
skills/paper-dynasty/plan/pyproject.toml
Normal file
12
skills/paper-dynasty/plan/pyproject.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[project]
|
||||
name = "pd-plan"
|
||||
version = "1.0.0"
|
||||
description = "Paper Dynasty initiative tracker — local SQLite CLI for cross-project priorities"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[project.scripts]
|
||||
pd-plan = "cli:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=68.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
273
skills/paper-dynasty/scripts/ecosystem_status.sh
Executable file
273
skills/paper-dynasty/scripts/ecosystem_status.sh
Executable file
@ -0,0 +1,273 @@
|
||||
#!/usr/bin/env bash
|
||||
# ecosystem_status.sh — Paper Dynasty cross-project dashboard
|
||||
# Usage: GITEA_TOKEN=<token> ./ecosystem_status.sh
|
||||
# or: ./ecosystem_status.sh (auto-reads from gitea-mcp config if available)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Auth
|
||||
# ---------------------------------------------------------------------------
|
||||
if [[ -z "${GITEA_TOKEN:-}" ]]; then
|
||||
# Try to pull token from the gitea-mcp config (standard claude-code location)
|
||||
GITEA_MCP_CONFIG="${HOME}/.config/claude-code/mcp-servers/gitea-mcp.json"
|
||||
if [[ -f "$GITEA_MCP_CONFIG" ]]; then
|
||||
GITEA_TOKEN=$(python3 -c "
|
||||
import json, sys, os
|
||||
cfg_path = os.environ.get('GITEA_MCP_CONFIG', '')
|
||||
try:
|
||||
cfg = json.load(open(cfg_path))
|
||||
env = cfg.get('env', {})
|
||||
print(env.get('GITEA_TOKEN', env.get('GITEA_API_TOKEN', '')))
|
||||
except Exception:
|
||||
print('')
|
||||
" 2>/dev/null)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${GITEA_TOKEN:-}" ]]; then
|
||||
echo "ERROR: GITEA_TOKEN not set and could not be read from gitea-mcp config." >&2
|
||||
echo " Set it with: export GITEA_TOKEN=your-token" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
API_BASE="https://git.manticorum.com/api/v1"
|
||||
AUTH_HEADER="Authorization: token ${GITEA_TOKEN}"
|
||||
|
||||
REPOS=(
|
||||
"cal/paper-dynasty-database"
|
||||
"cal/paper-dynasty-discord"
|
||||
"cal/paper-dynasty-card-creation"
|
||||
"cal/paper-dynasty-website"
|
||||
"cal/paper-dynasty-gameplay-webapp"
|
||||
"cal/paper-dynasty-apiproxy"
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
gitea_get() {
|
||||
# gitea_get <endpoint-path> — returns JSON or "null" on error
|
||||
curl -sf -H "$AUTH_HEADER" -H "Content-Type: application/json" \
|
||||
"${API_BASE}/${1}" 2>/dev/null || echo "null"
|
||||
}
|
||||
|
||||
count_items() {
|
||||
local json="$1"
|
||||
if [[ "$json" == "null" || -z "$json" ]]; then
|
||||
echo "?"
|
||||
return
|
||||
fi
|
||||
python3 -c "
|
||||
import json,sys
|
||||
data=json.loads(sys.argv[1])
|
||||
print(len(data) if isinstance(data,list) else '?')
|
||||
" "$json" 2>/dev/null || echo "?"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Banner
|
||||
# ---------------------------------------------------------------------------
|
||||
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════════╗"
|
||||
echo "║ PAPER DYNASTY — ECOSYSTEM STATUS DASHBOARD ║"
|
||||
printf "║ %-56s ║\n" "$TIMESTAMP"
|
||||
echo "╚══════════════════════════════════════════════════════════╝"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Per-repo summary table
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
printf "%-36s %6s %5s %s\n" "REPOSITORY" "ISSUES" "PRs" "LATEST COMMIT"
|
||||
printf "%-36s %6s %5s %s\n" "────────────────────────────────────" "──────" "─────" "────────────────────────────────"
|
||||
|
||||
TOTAL_ISSUES=0
|
||||
TOTAL_PRS=0
|
||||
|
||||
declare -A ALL_ISSUES_JSON
|
||||
declare -A ALL_PRS_JSON
|
||||
|
||||
for REPO in "${REPOS[@]}"; do
|
||||
SHORT_NAME="${REPO#cal/}"
|
||||
|
||||
ISSUES_JSON=$(gitea_get "repos/${REPO}/issues?type=issues&state=open&limit=50")
|
||||
ISSUE_COUNT=$(count_items "$ISSUES_JSON")
|
||||
ALL_ISSUES_JSON["$REPO"]="$ISSUES_JSON"
|
||||
|
||||
PRS_JSON=$(gitea_get "repos/${REPO}/pulls?state=open&limit=50")
|
||||
PR_COUNT=$(count_items "$PRS_JSON")
|
||||
ALL_PRS_JSON["$REPO"]="$PRS_JSON"
|
||||
|
||||
COMMITS_JSON=$(gitea_get "repos/${REPO}/commits?limit=1")
|
||||
LATEST_SHA="n/a"
|
||||
LATEST_DATE=""
|
||||
if [[ "$COMMITS_JSON" != "null" && -n "$COMMITS_JSON" ]]; then
|
||||
LATEST_SHA=$(python3 -c "
|
||||
import json,sys
|
||||
data=json.loads(sys.argv[1])
|
||||
if isinstance(data,list) and data:
|
||||
print(data[0].get('sha','')[:7])
|
||||
else:
|
||||
print('n/a')
|
||||
" "$COMMITS_JSON" 2>/dev/null || echo "n/a")
|
||||
LATEST_DATE=$(python3 -c "
|
||||
import json,sys
|
||||
data=json.loads(sys.argv[1])
|
||||
if isinstance(data,list) and data:
|
||||
ts=data[0].get('commit',{}).get('committer',{}).get('date','')
|
||||
print(ts[:10] if ts else '')
|
||||
else:
|
||||
print('')
|
||||
" "$COMMITS_JSON" 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
if [[ "$ISSUE_COUNT" =~ ^[0-9]+$ ]]; then
|
||||
TOTAL_ISSUES=$((TOTAL_ISSUES + ISSUE_COUNT))
|
||||
fi
|
||||
if [[ "$PR_COUNT" =~ ^[0-9]+$ ]]; then
|
||||
TOTAL_PRS=$((TOTAL_PRS + PR_COUNT))
|
||||
fi
|
||||
|
||||
COMMIT_LABEL="${LATEST_SHA}${LATEST_DATE:+ [${LATEST_DATE}]}"
|
||||
printf "%-36s %6s %5s %s\n" "$SHORT_NAME" "$ISSUE_COUNT" "$PR_COUNT" "$COMMIT_LABEL"
|
||||
done
|
||||
|
||||
echo ""
|
||||
printf " TOTALS: %d open issues, %d open PRs across %d repos\n" \
|
||||
"$TOTAL_ISSUES" "$TOTAL_PRS" "${#REPOS[@]}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Recent commits — last 3 per repo
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo " RECENT COMMITS (last 3 per repo)"
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
|
||||
for REPO in "${REPOS[@]}"; do
|
||||
SHORT_NAME="${REPO#cal/}"
|
||||
echo ""
|
||||
echo " ▸ ${SHORT_NAME}"
|
||||
|
||||
COMMITS_JSON=$(gitea_get "repos/${REPO}/commits?limit=3")
|
||||
if [[ "$COMMITS_JSON" == "null" || -z "$COMMITS_JSON" ]]; then
|
||||
echo " (could not fetch commits)"
|
||||
continue
|
||||
fi
|
||||
|
||||
python3 -c "
|
||||
import json, sys
|
||||
|
||||
data = json.loads(sys.argv[1])
|
||||
if not isinstance(data, list) or not data:
|
||||
print(' (no commits)')
|
||||
sys.exit(0)
|
||||
|
||||
for c in data:
|
||||
sha = c.get('sha', '')[:7]
|
||||
msg = c.get('commit', {}).get('message', '').split('\n')[0][:58]
|
||||
ts = c.get('commit', {}).get('committer', {}).get('date', '')[:10]
|
||||
author = c.get('commit', {}).get('committer', {}).get('name', 'unknown')[:16]
|
||||
print(f' {sha} {ts} {author:<16} {msg}')
|
||||
" "$COMMITS_JSON"
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Open issues detail
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo " OPEN ISSUES"
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
|
||||
FOUND_ISSUES=false
|
||||
for REPO in "${REPOS[@]}"; do
|
||||
SHORT_NAME="${REPO#cal/}"
|
||||
ISSUES_JSON="${ALL_ISSUES_JSON[$REPO]}"
|
||||
ISSUE_COUNT=$(count_items "$ISSUES_JSON")
|
||||
|
||||
if [[ "$ISSUE_COUNT" == "0" || "$ISSUE_COUNT" == "?" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " ▸ ${SHORT_NAME} (${ISSUE_COUNT} open)"
|
||||
FOUND_ISSUES=true
|
||||
|
||||
python3 -c "
|
||||
import json, sys
|
||||
|
||||
data = json.loads(sys.argv[1])
|
||||
if not isinstance(data, list):
|
||||
print(' (error reading issues)')
|
||||
sys.exit(0)
|
||||
|
||||
for i in data[:10]:
|
||||
num = i.get('number', '?')
|
||||
title = i.get('title', '(no title)')[:65]
|
||||
labels = ', '.join(l.get('name','') for l in i.get('labels',[]))
|
||||
lstr = f' [{labels}]' if labels else ''
|
||||
print(f' #{num:<4} {title}{lstr}')
|
||||
|
||||
if len(data) > 10:
|
||||
print(f' ... and {len(data)-10} more')
|
||||
" "$ISSUES_JSON"
|
||||
done
|
||||
|
||||
if [[ "$FOUND_ISSUES" == "false" ]]; then
|
||||
echo ""
|
||||
echo " (no open issues across all repos)"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Open PRs detail
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo " OPEN PULL REQUESTS"
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
|
||||
FOUND_PRS=false
|
||||
for REPO in "${REPOS[@]}"; do
|
||||
SHORT_NAME="${REPO#cal/}"
|
||||
PRS_JSON="${ALL_PRS_JSON[$REPO]}"
|
||||
PR_COUNT=$(count_items "$PRS_JSON")
|
||||
|
||||
if [[ "$PR_COUNT" == "0" || "$PR_COUNT" == "?" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " ▸ ${SHORT_NAME} (${PR_COUNT} open)"
|
||||
FOUND_PRS=true
|
||||
|
||||
python3 -c "
|
||||
import json, sys
|
||||
|
||||
data = json.loads(sys.argv[1])
|
||||
if not isinstance(data, list):
|
||||
print(' (error reading PRs)')
|
||||
sys.exit(0)
|
||||
|
||||
for pr in data:
|
||||
num = pr.get('number', '?')
|
||||
title = pr.get('title', '(no title)')[:65]
|
||||
head = pr.get('head', {}).get('label', '')
|
||||
base = pr.get('base', {}).get('label', '')
|
||||
print(f' #{num:<4} {title}')
|
||||
if head and base:
|
||||
print(f' {head} → {base}')
|
||||
" "$PRS_JSON"
|
||||
done
|
||||
|
||||
if [[ "$FOUND_PRS" == "false" ]]; then
|
||||
echo ""
|
||||
echo " (no open PRs across all repos)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo " Done. Gitea: https://git.manticorum.com/cal"
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
@ -3,140 +3,40 @@
|
||||
Generate summary report for Paper Dynasty card update
|
||||
|
||||
Collects statistics and notable changes for release notes.
|
||||
Uses the Paper Dynasty API instead of direct database access.
|
||||
|
||||
Usage:
|
||||
python generate_summary.py <database_path> [--previous-db <path>]
|
||||
python generate_summary.py [--cardset-id 24] [--env prod]
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
from typing import List, Dict, Tuple
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from api_client import PaperDynastyAPI
|
||||
|
||||
|
||||
RARITY_TIERS = {
|
||||
"Reserve": 1,
|
||||
"Replacement": 2,
|
||||
"Starter": 3,
|
||||
"All-Star": 4,
|
||||
"MVP": 5,
|
||||
"Hall of Fame": 6
|
||||
}
|
||||
def get_card_counts(api: PaperDynastyAPI, cardset_id: int) -> Dict[str, int]:
|
||||
"""Get total card counts from the API"""
|
||||
batting = api.get("battingcards", params=[("cardset_id", cardset_id)])
|
||||
pitching = api.get("pitchingcards", params=[("cardset_id", cardset_id)])
|
||||
b_count = batting.get("count", 0)
|
||||
p_count = pitching.get("count", 0)
|
||||
return {"batting": b_count, "pitching": p_count, "total": b_count + p_count}
|
||||
|
||||
|
||||
def get_card_counts(cursor: sqlite3.Cursor) -> Dict[str, int]:
|
||||
"""Get total card counts"""
|
||||
batting = cursor.execute("SELECT COUNT(*) FROM batting_cards").fetchone()[0]
|
||||
pitching = cursor.execute("SELECT COUNT(*) FROM pitching_cards").fetchone()[0]
|
||||
return {"batting": batting, "pitching": pitching, "total": batting + pitching}
|
||||
|
||||
|
||||
def get_new_players(cursor: sqlite3.Cursor, since_date: Optional[str] = None) -> int:
|
||||
"""Count new players added since date"""
|
||||
if not since_date:
|
||||
# Default to last 7 days
|
||||
query = """
|
||||
SELECT COUNT(DISTINCT player_name)
|
||||
FROM (
|
||||
SELECT player_name, created_date FROM batting_cards
|
||||
WHERE created_date >= date('now', '-7 days')
|
||||
UNION
|
||||
SELECT player_name, created_date FROM pitching_cards
|
||||
WHERE created_date >= date('now', '-7 days')
|
||||
)
|
||||
"""
|
||||
else:
|
||||
query = f"""
|
||||
SELECT COUNT(DISTINCT player_name)
|
||||
FROM (
|
||||
SELECT player_name FROM batting_cards
|
||||
WHERE created_date >= '{since_date}'
|
||||
UNION
|
||||
SELECT player_name FROM pitching_cards
|
||||
WHERE created_date >= '{since_date}'
|
||||
)
|
||||
"""
|
||||
|
||||
def get_player_count(api: PaperDynastyAPI, cardset_id: int) -> int:
|
||||
"""Get total player count for the cardset from the API"""
|
||||
try:
|
||||
return cursor.execute(query).fetchone()[0]
|
||||
except sqlite3.OperationalError:
|
||||
# created_date column might not exist
|
||||
players = api.list_players(cardset_id=cardset_id)
|
||||
return len(players)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def get_rarity_changes(
|
||||
current_cursor: sqlite3.Cursor,
|
||||
previous_db_path: Optional[Path] = None,
|
||||
threshold: int = 2
|
||||
) -> Tuple[List[Dict], List[Dict]]:
|
||||
"""
|
||||
Compare rarity changes between current and previous database.
|
||||
|
||||
Returns (upgrades, downgrades) where each is a list of dicts with:
|
||||
- player_name
|
||||
- card_id
|
||||
- old_rarity
|
||||
- new_rarity
|
||||
- change (tier difference)
|
||||
"""
|
||||
if not previous_db_path or not previous_db_path.exists():
|
||||
return [], []
|
||||
|
||||
prev_conn = sqlite3.connect(previous_db_path)
|
||||
prev_cursor = prev_conn.cursor()
|
||||
|
||||
upgrades = []
|
||||
downgrades = []
|
||||
|
||||
# Compare batting cards
|
||||
query = """
|
||||
SELECT
|
||||
c.player_name,
|
||||
c.card_id,
|
||||
p.rarity as old_rarity,
|
||||
c.rarity as new_rarity
|
||||
FROM batting_cards c
|
||||
JOIN prev.batting_cards p ON c.card_id = p.card_id
|
||||
WHERE c.rarity != p.rarity
|
||||
"""
|
||||
|
||||
try:
|
||||
current_cursor.execute("ATTACH DATABASE ? AS prev", (str(previous_db_path),))
|
||||
changes = current_cursor.execute(query).fetchall()
|
||||
|
||||
for name, card_id, old_rarity, new_rarity in changes:
|
||||
old_tier = RARITY_TIERS.get(old_rarity, 0)
|
||||
new_tier = RARITY_TIERS.get(new_rarity, 0)
|
||||
change = new_tier - old_tier
|
||||
|
||||
if abs(change) >= threshold:
|
||||
record = {
|
||||
"player_name": name,
|
||||
"card_id": card_id,
|
||||
"old_rarity": old_rarity,
|
||||
"new_rarity": new_rarity,
|
||||
"change": change
|
||||
}
|
||||
if change > 0:
|
||||
upgrades.append(record)
|
||||
else:
|
||||
downgrades.append(record)
|
||||
|
||||
current_cursor.execute("DETACH DATABASE prev")
|
||||
except sqlite3.OperationalError as e:
|
||||
print(f"Warning: Could not compare rarity changes: {e}", file=sys.stderr)
|
||||
|
||||
prev_conn.close()
|
||||
|
||||
# Sort by magnitude of change
|
||||
upgrades.sort(key=lambda x: x['change'], reverse=True)
|
||||
downgrades.sort(key=lambda x: x['change'])
|
||||
|
||||
return upgrades, downgrades
|
||||
|
||||
|
||||
def get_date_range(card_creation_dir: Path) -> Tuple[str, str]:
|
||||
"""Extract date range from retrosheet_data.py"""
|
||||
retrosheet_file = card_creation_dir / "retrosheet_data.py"
|
||||
@ -160,11 +60,11 @@ def get_date_range(card_creation_dir: Path) -> Tuple[str, str]:
|
||||
|
||||
def generate_markdown_summary(
|
||||
counts: Dict[str, int],
|
||||
new_players: int,
|
||||
upgrades: List[Dict],
|
||||
downgrades: List[Dict],
|
||||
player_count: int,
|
||||
date_range: Tuple[str, str],
|
||||
csv_files: List[str]
|
||||
csv_files: List[str],
|
||||
cardset_id: int,
|
||||
env: str,
|
||||
) -> str:
|
||||
"""Generate markdown summary report"""
|
||||
|
||||
@ -176,44 +76,14 @@ def generate_markdown_summary(
|
||||
"",
|
||||
"## Overview",
|
||||
f"- **Total Cards**: {counts['batting']} batting, {counts['pitching']} pitching",
|
||||
f"- **New Players**: {new_players}",
|
||||
f"- **Total Players**: {player_count}",
|
||||
f"- **Data Range**: {start_date} to {end_date}",
|
||||
f"- **Cardset ID**: {cardset_id} ({env})",
|
||||
"",
|
||||
]
|
||||
|
||||
if upgrades or downgrades:
|
||||
lines.append("## Notable Rarity Changes")
|
||||
lines.append("")
|
||||
|
||||
if upgrades:
|
||||
lines.append("### Upgrades")
|
||||
for player in upgrades[:10]: # Max 10
|
||||
tier_change = f"+{player['change']}" if player['change'] > 0 else str(player['change'])
|
||||
lines.append(
|
||||
f"- **{player['player_name']}** (ID: {player['card_id']}): "
|
||||
f"{player['old_rarity']} → {player['new_rarity']} ({tier_change} tiers)"
|
||||
)
|
||||
if len(upgrades) > 10:
|
||||
lines.append(f"- *...and {len(upgrades) - 10} more*")
|
||||
lines.append("")
|
||||
|
||||
if downgrades:
|
||||
lines.append("### Downgrades")
|
||||
for player in downgrades[:10]: # Max 10
|
||||
tier_change = str(player['change']) # Already negative
|
||||
lines.append(
|
||||
f"- **{player['player_name']}** (ID: {player['card_id']}): "
|
||||
f"{player['old_rarity']} → {player['new_rarity']} ({tier_change} tiers)"
|
||||
)
|
||||
if len(downgrades) > 10:
|
||||
lines.append(f"- *...and {len(downgrades) - 10} more*")
|
||||
lines.append("")
|
||||
|
||||
lines.extend([
|
||||
"## Files Generated",
|
||||
"- ✅ Card images uploaded to S3",
|
||||
"- ✅ Scouting CSVs transferred to database server",
|
||||
])
|
||||
]
|
||||
|
||||
for csv_file in csv_files:
|
||||
lines.append(f" - {csv_file}")
|
||||
@ -233,28 +103,27 @@ def generate_markdown_summary(
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python generate_summary.py <database_path> [--previous-db <path>]")
|
||||
sys.exit(1)
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate summary report for Paper Dynasty card update"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cardset-id", type=int, default=24,
|
||||
help="Cardset ID to summarize (default: 24, the live cardset)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--env", choices=["prod", "dev"], default="prod",
|
||||
help="API environment (default: prod)"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
db_path = Path(sys.argv[1])
|
||||
previous_db = Path(sys.argv[3]) if len(sys.argv) > 3 and sys.argv[2] == '--previous-db' else None
|
||||
api = PaperDynastyAPI(environment=args.env)
|
||||
|
||||
if not db_path.exists():
|
||||
print(f"❌ Database not found: {db_path}")
|
||||
sys.exit(1)
|
||||
# Collect data from API
|
||||
counts = get_card_counts(api, args.cardset_id)
|
||||
player_count = get_player_count(api, args.cardset_id)
|
||||
|
||||
# Connect to database
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Collect data
|
||||
counts = get_card_counts(cursor)
|
||||
new_players = get_new_players(cursor)
|
||||
upgrades, downgrades = get_rarity_changes(cursor, previous_db, threshold=2)
|
||||
|
||||
# Get date range from retrosheet_data.py
|
||||
card_creation_dir = db_path.parent
|
||||
# Get date range from retrosheet_data.py if present
|
||||
card_creation_dir = Path("/mnt/NV2/Development/paper-dynasty/card-creation")
|
||||
date_range = get_date_range(card_creation_dir)
|
||||
|
||||
# Get CSV files from scouting directory
|
||||
@ -262,7 +131,9 @@ def main():
|
||||
csv_files = sorted([f.name for f in scouting_dir.glob("*.csv")]) if scouting_dir.exists() else []
|
||||
|
||||
# Generate summary
|
||||
summary = generate_markdown_summary(counts, new_players, upgrades, downgrades, date_range, csv_files)
|
||||
summary = generate_markdown_summary(
|
||||
counts, player_count, date_range, csv_files, args.cardset_id, args.env
|
||||
)
|
||||
|
||||
# Print to stdout
|
||||
print(summary)
|
||||
@ -273,8 +144,6 @@ def main():
|
||||
output_file.write_text(summary)
|
||||
print(f"\n✅ Summary saved to: {output_file}", file=sys.stderr)
|
||||
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@ -296,15 +296,14 @@ class SmokeTestRunner:
|
||||
def run_all(self, mode: str = "quick"):
|
||||
"""Run smoke test checks. mode='quick' for core, 'full' for everything."""
|
||||
base = self.api.base_url
|
||||
t = 10 if mode == "quick" else 30 # quick should be fast
|
||||
|
||||
# Pre-fetch IDs for by-ID lookups (full mode only)
|
||||
if mode == "full":
|
||||
team_id = self._fetch_id("teams", params=[("limit", 1)])
|
||||
player_id = self._fetch_id("players", params=[("limit", 1)])
|
||||
card_id = self._fetch_id("cards", params=[("limit", 1)])
|
||||
game_id = self._fetch_id("games", params=[("limit", 1)])
|
||||
result_id = self._fetch_id("results", params=[("limit", 1)])
|
||||
game_id = self._fetch_id("games")
|
||||
result_id = self._fetch_id("results")
|
||||
track_id = self._fetch_id("evolution/tracks", requires_auth=True)
|
||||
else:
|
||||
team_id = player_id = card_id = game_id = result_id = track_id = None
|
||||
@ -380,14 +379,11 @@ class SmokeTestRunner:
|
||||
expect_list=True,
|
||||
min_count=1,
|
||||
),
|
||||
self.check(
|
||||
"Events", "economy", "events", params=[("limit", 5)], expect_list=True
|
||||
),
|
||||
self.check("Events", "economy", "events", expect_list=True),
|
||||
self.check(
|
||||
"Scout opportunities",
|
||||
"scouting",
|
||||
"scout_opportunities",
|
||||
params=[("limit", 5)],
|
||||
expect_list=True,
|
||||
),
|
||||
self.check(
|
||||
@ -493,7 +489,6 @@ class SmokeTestRunner:
|
||||
"Batting card ratings",
|
||||
"cards",
|
||||
"battingcardratings",
|
||||
params=[("limit", 5)],
|
||||
expect_list=True,
|
||||
requires_auth=True,
|
||||
),
|
||||
@ -501,7 +496,6 @@ class SmokeTestRunner:
|
||||
"Pitching card ratings",
|
||||
"cards",
|
||||
"pitchingcardratings",
|
||||
params=[("limit", 5)],
|
||||
expect_list=True,
|
||||
requires_auth=True,
|
||||
),
|
||||
@ -509,7 +503,6 @@ class SmokeTestRunner:
|
||||
"Card positions",
|
||||
"cards",
|
||||
"cardpositions",
|
||||
params=[("limit", 5)],
|
||||
expect_list=True,
|
||||
),
|
||||
*(
|
||||
@ -541,7 +534,6 @@ class SmokeTestRunner:
|
||||
"Games list",
|
||||
"games",
|
||||
"games",
|
||||
params=[("limit", 5)],
|
||||
expect_list=True,
|
||||
min_count=1,
|
||||
),
|
||||
@ -549,7 +541,6 @@ class SmokeTestRunner:
|
||||
"Results list",
|
||||
"games",
|
||||
"results",
|
||||
params=[("limit", 5)],
|
||||
expect_list=True,
|
||||
min_count=1,
|
||||
),
|
||||
@ -619,14 +610,12 @@ class SmokeTestRunner:
|
||||
"Rewards",
|
||||
"economy",
|
||||
"rewards",
|
||||
params=[("limit", 5)],
|
||||
expect_list=True,
|
||||
),
|
||||
self.check(
|
||||
"Game rewards",
|
||||
"economy",
|
||||
"gamerewards",
|
||||
params=[("limit", 5)],
|
||||
expect_list=True,
|
||||
min_count=1,
|
||||
),
|
||||
@ -634,28 +623,24 @@ class SmokeTestRunner:
|
||||
"Gauntlet rewards",
|
||||
"economy",
|
||||
"gauntletrewards",
|
||||
params=[("limit", 5)],
|
||||
expect_list=True,
|
||||
),
|
||||
self.check(
|
||||
"Gauntlet runs",
|
||||
"economy",
|
||||
"gauntletruns",
|
||||
params=[("limit", 5)],
|
||||
expect_list=True,
|
||||
),
|
||||
self.check(
|
||||
"Awards",
|
||||
"economy",
|
||||
"awards",
|
||||
params=[("limit", 5)],
|
||||
expect_list=True,
|
||||
),
|
||||
self.check(
|
||||
"Notifications",
|
||||
"economy",
|
||||
"notifs",
|
||||
params=[("limit", 5)],
|
||||
expect_list=True,
|
||||
),
|
||||
# Scouting extras
|
||||
@ -663,21 +648,18 @@ class SmokeTestRunner:
|
||||
"Scout claims",
|
||||
"scouting",
|
||||
"scout_claims",
|
||||
params=[("limit", 5)],
|
||||
expect_list=True,
|
||||
),
|
||||
self.check(
|
||||
"MLB players",
|
||||
"scouting",
|
||||
"mlbplayers",
|
||||
params=[("limit", 5)],
|
||||
expect_list=True,
|
||||
),
|
||||
self.check(
|
||||
"Paperdex",
|
||||
"scouting",
|
||||
"paperdex",
|
||||
params=[("limit", 5)],
|
||||
expect_list=True,
|
||||
),
|
||||
*(
|
||||
@ -699,14 +681,12 @@ class SmokeTestRunner:
|
||||
"Batting stats",
|
||||
"stats",
|
||||
"batstats",
|
||||
params=[("limit", 5)],
|
||||
expect_list=True,
|
||||
),
|
||||
self.check(
|
||||
"Pitching stats",
|
||||
"stats",
|
||||
"pitstats",
|
||||
params=[("limit", 5)],
|
||||
expect_list=True,
|
||||
),
|
||||
self.check(
|
||||
|
||||
@ -2,30 +2,33 @@
|
||||
"""
|
||||
Database Validation Script for Paper Dynasty Card Generation
|
||||
|
||||
Checks for common errors in card database before uploading:
|
||||
- Negative groundball_b values (causes gameplay crashes)
|
||||
- Invalid percentage ranges (>100 or <0)
|
||||
- NULL values in required fields
|
||||
Checks for common errors in card data via the API before deployment:
|
||||
- Missing batting or pitching cards for a cardset
|
||||
- Players with no corresponding card record
|
||||
- Rarity distribution sanity check
|
||||
|
||||
Usage:
|
||||
python validate_database.py <database_path>
|
||||
python validate_database.py [--cardset-id 24] [--env prod]
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple, Dict
|
||||
from typing import List, Dict
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from api_client import PaperDynastyAPI
|
||||
|
||||
|
||||
class ValidationError:
|
||||
def __init__(self, table: str, issue: str, count: int, examples: List[Dict]):
|
||||
self.table = table
|
||||
def __init__(self, entity: str, issue: str, count: int, examples: List[str]):
|
||||
self.entity = entity
|
||||
self.issue = issue
|
||||
self.count = count
|
||||
self.examples = examples
|
||||
|
||||
def __str__(self):
|
||||
lines = [f"❌ {self.table}: {self.issue} ({self.count} records)"]
|
||||
lines = [f"❌ {self.entity}: {self.issue} ({self.count} records)"]
|
||||
for example in self.examples[:5]: # Show max 5 examples
|
||||
lines.append(f" - {example}")
|
||||
if self.count > 5:
|
||||
@ -33,113 +36,109 @@ class ValidationError:
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def validate_batting_cards(cursor: sqlite3.Cursor) -> List[ValidationError]:
|
||||
"""Validate batting cards table"""
|
||||
def get_card_counts(api: PaperDynastyAPI, cardset_id: int) -> Dict[str, int]:
|
||||
"""Get total card counts for the cardset from the API"""
|
||||
batting = api.get("battingcards", params=[("cardset_id", cardset_id)])
|
||||
pitching = api.get("pitchingcards", params=[("cardset_id", cardset_id)])
|
||||
return {
|
||||
"batting": batting.get("count", 0),
|
||||
"pitching": pitching.get("count", 0),
|
||||
}
|
||||
|
||||
|
||||
def validate_card_counts(api: PaperDynastyAPI, cardset_id: int) -> List[ValidationError]:
|
||||
"""Check that batting and pitching cards exist for the cardset"""
|
||||
errors = []
|
||||
counts = get_card_counts(api, cardset_id)
|
||||
|
||||
if counts["batting"] == 0:
|
||||
errors.append(ValidationError(
|
||||
"battingcards", f"No batting cards found for cardset {cardset_id}", 0, []
|
||||
))
|
||||
if counts["pitching"] == 0:
|
||||
errors.append(ValidationError(
|
||||
"pitchingcards", f"No pitching cards found for cardset {cardset_id}", 0, []
|
||||
))
|
||||
return errors
|
||||
|
||||
|
||||
def validate_player_coverage(api: PaperDynastyAPI, cardset_id: int) -> List[ValidationError]:
|
||||
"""Check that every player in the cardset has at least one card"""
|
||||
errors = []
|
||||
|
||||
# Check 1: Negative groundball_b values
|
||||
cursor.execute("""
|
||||
SELECT player_name, card_id, groundball_b, rarity
|
||||
FROM batting_cards
|
||||
WHERE groundball_b < 0
|
||||
ORDER BY groundball_b ASC
|
||||
LIMIT 10
|
||||
""")
|
||||
negative_gb = cursor.fetchall()
|
||||
if negative_gb:
|
||||
count = cursor.execute("SELECT COUNT(*) FROM batting_cards WHERE groundball_b < 0").fetchone()[0]
|
||||
try:
|
||||
players = api.list_players(cardset_id=cardset_id)
|
||||
except Exception as e:
|
||||
errors.append(ValidationError("players", f"Could not fetch players: {e}", 0, []))
|
||||
return errors
|
||||
|
||||
if not players:
|
||||
errors.append(ValidationError(
|
||||
"players", f"No players found for cardset {cardset_id}", 0, []
|
||||
))
|
||||
return errors
|
||||
|
||||
batting_data = api.get("battingcards", params=[("cardset_id", cardset_id)])
|
||||
pitching_data = api.get("pitchingcards", params=[("cardset_id", cardset_id)])
|
||||
|
||||
batting_player_ids = {
|
||||
c["player"]["player_id"]
|
||||
for c in batting_data.get("cards", [])
|
||||
if c.get("player")
|
||||
}
|
||||
pitching_player_ids = {
|
||||
c["player"]["player_id"]
|
||||
for c in pitching_data.get("cards", [])
|
||||
if c.get("player")
|
||||
}
|
||||
all_card_player_ids = batting_player_ids | pitching_player_ids
|
||||
|
||||
uncovered = [
|
||||
p for p in players
|
||||
if p["player_id"] not in all_card_player_ids
|
||||
]
|
||||
|
||||
if uncovered:
|
||||
examples = [
|
||||
f"{name} (ID: {card_id}, GB-B: {gb}, Rarity: {rarity})"
|
||||
for name, card_id, gb, rarity in negative_gb
|
||||
f"{p.get('p_name', 'Unknown')} (ID: {p['player_id']}, rarity: {p.get('rarity', {}).get('name', '?') if isinstance(p.get('rarity'), dict) else p.get('rarity', '?')})"
|
||||
for p in uncovered
|
||||
]
|
||||
errors.append(ValidationError("batting_cards", "Negative groundball_b values", count, examples))
|
||||
|
||||
# Check 2: Invalid percentage ranges (0-100)
|
||||
# Adjust column names based on actual schema
|
||||
percentage_columns = ['strikeout', 'walk', 'homerun'] # Add more as needed
|
||||
for col in percentage_columns:
|
||||
try:
|
||||
cursor.execute(f"""
|
||||
SELECT player_name, card_id, {col}
|
||||
FROM batting_cards
|
||||
WHERE {col} < 0 OR {col} > 100
|
||||
LIMIT 10
|
||||
""")
|
||||
invalid_pct = cursor.fetchall()
|
||||
if invalid_pct:
|
||||
count = cursor.execute(f"SELECT COUNT(*) FROM batting_cards WHERE {col} < 0 OR {col} > 100").fetchone()[0]
|
||||
examples = [f"{name} (ID: {card_id}, {col}: {val})" for name, card_id, val in invalid_pct]
|
||||
errors.append(ValidationError("batting_cards", f"Invalid {col} percentage", count, examples))
|
||||
except sqlite3.OperationalError:
|
||||
# Column might not exist - skip
|
||||
pass
|
||||
errors.append(ValidationError(
|
||||
"players", "Players with no batting or pitching card", len(uncovered), examples
|
||||
))
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def validate_pitching_cards(cursor: sqlite3.Cursor) -> List[ValidationError]:
|
||||
"""Validate pitching cards table"""
|
||||
errors = []
|
||||
|
||||
# Check: Invalid percentage ranges
|
||||
percentage_columns = ['strikeout', 'walk', 'homerun'] # Add more as needed
|
||||
for col in percentage_columns:
|
||||
try:
|
||||
cursor.execute(f"""
|
||||
SELECT player_name, card_id, {col}
|
||||
FROM pitching_cards
|
||||
WHERE {col} < 0 OR {col} > 100
|
||||
LIMIT 10
|
||||
""")
|
||||
invalid_pct = cursor.fetchall()
|
||||
if invalid_pct:
|
||||
count = cursor.execute(f"SELECT COUNT(*) FROM pitching_cards WHERE {col} < 0 OR {col} > 100").fetchone()[0]
|
||||
examples = [f"{name} (ID: {card_id}, {col}: {val})" for name, card_id, val in invalid_pct]
|
||||
errors.append(ValidationError("pitching_cards", f"Invalid {col} percentage", count, examples))
|
||||
except sqlite3.OperationalError:
|
||||
# Column might not exist - skip
|
||||
pass
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def get_card_counts(cursor: sqlite3.Cursor) -> Dict[str, int]:
|
||||
"""Get total card counts"""
|
||||
batting_count = cursor.execute("SELECT COUNT(*) FROM batting_cards").fetchone()[0]
|
||||
pitching_count = cursor.execute("SELECT COUNT(*) FROM pitching_cards").fetchone()[0]
|
||||
return {"batting": batting_count, "pitching": pitching_count}
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python validate_database.py <database_path>")
|
||||
sys.exit(1)
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Validate Paper Dynasty card data via the API"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cardset-id", type=int, default=24,
|
||||
help="Cardset ID to validate (default: 24, the live cardset)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--env", choices=["prod", "dev"], default="prod",
|
||||
help="API environment (default: prod)"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
db_path = Path(sys.argv[1])
|
||||
if not db_path.exists():
|
||||
print(f"❌ Database not found: {db_path}")
|
||||
sys.exit(1)
|
||||
api = PaperDynastyAPI(environment=args.env)
|
||||
|
||||
print(f"🔍 Validating database: {db_path}")
|
||||
print(f"🔍 Validating cardset {args.cardset_id} ({args.env})")
|
||||
print()
|
||||
|
||||
# Connect to database
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get card counts
|
||||
counts = get_card_counts(cursor)
|
||||
counts = get_card_counts(api, args.cardset_id)
|
||||
print(f"📊 Total Cards:")
|
||||
print(f" - Batting: {counts['batting']}")
|
||||
print(f" - Pitching: {counts['pitching']}")
|
||||
print()
|
||||
|
||||
# Run validations
|
||||
all_errors = []
|
||||
all_errors.extend(validate_batting_cards(cursor))
|
||||
all_errors.extend(validate_pitching_cards(cursor))
|
||||
all_errors.extend(validate_card_counts(api, args.cardset_id))
|
||||
all_errors.extend(validate_player_coverage(api, args.cardset_id))
|
||||
|
||||
# Report results
|
||||
if all_errors:
|
||||
print("❌ VALIDATION FAILED")
|
||||
print()
|
||||
@ -150,7 +149,7 @@ def main():
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("✅ VALIDATION PASSED")
|
||||
print("No errors found in database")
|
||||
print("No errors found")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user