Skip to content

Commit 43dafe7

Browse files
merge: resolve conflicts with main, fix prime.md tool names
Co-authored-by: MarkusNeusinger <2921697+MarkusNeusinger@users.noreply.github.com>
2 parents ac3c4cf + 47c210f commit 43dafe7

300 files changed

Lines changed: 30961 additions & 18953 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,28 @@ ENVIRONMENT=development
3535
# API port (default: 8000)
3636
PORT=8000
3737

38+
# ============================================================================
39+
# Admin Auth (for /debug/* endpoints)
40+
# ============================================================================
41+
42+
# GET /debug/status and GET /debug/ping can be authorized either by a valid
43+
# Cloudflare Access JWT (production browser flow) or by ADMIN_TOKEN.
44+
# Leave ADMIN_TOKEN unset if you only want Cloudflare Access auth.
45+
# Set it for local development, CI, or as a break-glass fallback when
46+
# Cloudflare Access is unavailable. Locally, any non-empty string works
47+
# because these endpoints are only reachable on localhost.
48+
# ADMIN_TOKEN=
49+
50+
# Cloudflare Access (Zero Trust) settings — only needed when verifying the
51+
# Cf-Access-Jwt-Assertion header in production. Leave unset locally.
52+
# CF_ACCESS_TEAM_DOMAIN=anyplot.cloudflareaccess.com
53+
# CF_ACCESS_AUD=
54+
55+
# Comma-separated list of allowed Google account email addresses for the
56+
# Cloudflare Access JWT path. Required for the JWT path to authorize anyone;
57+
# requests with a valid JWT but an unlisted email return 403.
58+
# ADMIN_ALLOWED_EMAILS=alice@example.com,bob@example.com
59+
3860
# ============================================================================
3961
# AI Services (optional)
4062
# ============================================================================

.github/copilot-instructions.md

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ This file provides guidance to GitHub Copilot when working with code in this rep
44

55
## Important Rules
66

7-
- **No Co-authored-by in commit messages** - Never add `Co-authored-by:` lines to commit messages. Keep commit messages clean without AI attribution footers.
87
- **Always write in English** - All output text (code comments, commit messages, PR descriptions, issue comments, documentation) must be in English, even if the user writes in another language.
98

109
## Task Suitability
@@ -85,17 +84,22 @@ Everything for one plot type lives in a single directory:
8584

8685
```
8786
plots/{spec-id}/
88-
├── specification.md # Library-agnostic description
89-
├── specification.yaml # Tags, created, issue, suggested
90-
├── metadata/ # Per-library metadata
91-
│ ├── matplotlib.yaml
92-
│ └── ...
93-
└── implementations/ # Library implementations
94-
├── matplotlib.py
95-
├── seaborn.py
96-
└── ...
87+
├── specification.md # Library-agnostic description
88+
├── specification.yaml # Tags, created, issue, suggested
89+
├── metadata/
90+
│ └── python/ # Per-library metadata (one file per library)
91+
│ ├── matplotlib.yaml
92+
│ ├── seaborn.yaml
93+
│ └── ...
94+
└── implementations/
95+
└── python/ # Library implementations (one file per library)
96+
├── matplotlib.py
97+
├── seaborn.py
98+
└── ...
9799
```
98100

101+
The `python/` subdirectory is a deliberate forward-compatibility layer; non-Python implementation languages would live as siblings (e.g. `implementations/r/`) when introduced. All paths in code, prompts, and metadata refer to the language-prefixed form: `plots/{spec-id}/implementations/python/{library}.py` and `plots/{spec-id}/metadata/python/{library}.yaml`.
102+
99103
Example: `plots/scatter-basic/` contains everything for the basic scatter plot.
100104

101105
### Spec ID Naming Convention
@@ -137,19 +141,32 @@ Examples: `scatter-basic`, `scatter-color-mapped`, `bar-grouped-horizontal`, `he
137141
### PR Labels (set by workflows)
138142

139143
- **`approved`** - Human approved specification for merge
140-
- **`ai-approved`** - AI quality check passed (score >= 90, or >= 50 after 3 attempts)
141-
- **`ai-rejected`** - AI quality check failed (score < 90), triggers repair loop
144+
- **`ai-approved`** - AI quality check passed at the current cascading threshold (see below)
145+
- **`ai-rejected`** - AI quality check failed at the current threshold, triggers repair loop
142146
- **`quality:XX`** - Quality score (e.g., `quality:92`)
143147

148+
**Quality threshold cascade** (`.github/workflows/impl-review.yml`): the threshold drops by 10 each repair attempt to give partial credit for incremental improvement.
149+
150+
| Review # | Repair attempt | Threshold |
151+
|----------|----------------|-----------|
152+
| 1 (initial) | 0 | ≥ 90 |
153+
| 2 | 1 | ≥ 80 |
154+
| 3 | 2 | ≥ 70 |
155+
| 4 | 3 | ≥ 60 |
156+
| 5 | 4 | ≥ 50 |
157+
158+
If the score is still below 50 after 4 repair attempts, the PR is closed (`gh pr close`) and the workflow posts a comment with next-step options. Regeneration is **manual**, not automatic — re-apply the `generate:{library}` label on the spec issue to start a fresh attempt.
159+
144160
**Specification Lifecycle:**
145161
```
146162
[open] spec-request → approved → spec-ready [implementations can start]
147163
```
148164

149165
**Implementation PR Lifecycle:**
150166
```
151-
[open] → impl-review → ai-approved (≥90) → impl-merge → impl:{library}:done
152-
→ ai-rejected (<90) → impl-repair (×3) → ai-approved (≥50) or failed (<50)
167+
[open] → impl-review → ai-approved (≥ current threshold) → impl-merge → impl:{library}:done
168+
→ ai-rejected (< threshold) → impl-repair (×4) → cascade re-review
169+
→ after 4 failed repairs → close PR; manual regen via generate:{library} label
153170
```
154171

155172
## Code Standards
@@ -212,7 +229,7 @@ plt.savefig('plot.png', dpi=300, bbox_inches='tight')
212229

213230
## Tech Stack
214231

215-
- **Backend**: FastAPI, SQLAlchemy (async), PostgreSQL, Python 3.14+
232+
- **Backend**: FastAPI, SQLAlchemy (async), PostgreSQL, Python 3.13+
216233
- **Frontend**: React 19, TypeScript 6, Vite 8, MUI 9
217234
- **Package Manager**: uv (Python), yarn (Node.js)
218235
- **Linting**: Ruff (Python), ESLint (TypeScript)
@@ -223,8 +240,8 @@ The project runs on **Google Cloud Platform** (europe-west4 region):
223240

224241
| Service | Component | Purpose |
225242
|---------|--------------------|---------|
226-
| **Cloud Run** | `anyplot-backend` | FastAPI API (auto-scaling, serverless) |
227-
| **Cloud Run** | `anyplot-frontend` | React SPA served via nginx |
243+
| **Cloud Run** | `anyplot-api` | FastAPI API (auto-scaling, serverless) |
244+
| **Cloud Run** | `anyplot-app` | React SPA served via nginx |
228245
| **Cloud SQL** | PostgreSQL 18 | Database (Unix socket in production) |
229246
| **Cloud Storage** | `anyplot-images` | Preview images (GCS bucket) |
230247
| **Secret Manager** | `DATABASE_URL` | Secure credential storage |

.github/workflows/ci-lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ jobs:
6363

6464
- name: Install uv
6565
if: steps.check.outputs.should_lint == 'true'
66-
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
66+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
6767

6868
- name: Install dependencies
6969
if: steps.check.outputs.should_lint == 'true'

.github/workflows/ci-tests.yml

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,21 +84,25 @@ jobs:
8484

8585
- name: Install uv
8686
if: steps.check.outputs.should_test == 'true'
87-
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
87+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
8888

8989
- name: Install dependencies
9090
if: steps.check.outputs.should_test == 'true'
9191
run: uv sync --extra test
9292

93+
# Dependabot PRs run from forks and cannot access repository secrets,
94+
# so skip GCP auth + Cloud SQL Proxy + Codecov upload. Only unit and
95+
# integration tests run for Dependabot (no e2e — those need real Postgres).
96+
9397
- name: Authenticate to GCP
94-
if: steps.check.outputs.should_test == 'true'
98+
if: steps.check.outputs.should_test == 'true' && github.actor != 'dependabot[bot]'
9599
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3
96100
with:
97101
project_id: anyplot
98102
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
99103

100104
- name: Start Cloud SQL Proxy
101-
if: steps.check.outputs.should_test == 'true'
105+
if: steps.check.outputs.should_test == 'true' && github.actor != 'dependabot[bot]'
102106
run: |
103107
# Download Cloud SQL Proxy
104108
curl -o cloud-sql-proxy https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.14.3/cloud-sql-proxy.linux.amd64
@@ -114,7 +118,7 @@ jobs:
114118
echo "DATABASE_URL=postgresql+asyncpg://${{ secrets.DB_USER }}:${{ secrets.DB_PASS }}@localhost:5432/${{ secrets.DB_NAME }}" >> $GITHUB_ENV
115119
116120
- name: Run all tests with coverage
117-
if: steps.check.outputs.should_test == 'true'
121+
if: steps.check.outputs.should_test == 'true' && github.actor != 'dependabot[bot]'
118122
run: |
119123
uv run pytest tests/unit tests/integration tests/e2e \
120124
-v --tb=short \
@@ -126,16 +130,21 @@ jobs:
126130
# - Integration tests: SQLite in-memory for repository layer
127131
# - E2E tests: Real PostgreSQL with separate 'test' database
128132

133+
- name: Run unit + integration tests (Dependabot)
134+
if: steps.check.outputs.should_test == 'true' && github.actor == 'dependabot[bot]'
135+
run: |
136+
uv run pytest tests/unit tests/integration -v --tb=short
137+
129138
- name: Upload coverage to GitHub Artifacts
130-
if: steps.check.outputs.should_test == 'true' && always()
139+
if: steps.check.outputs.should_test == 'true' && github.actor != 'dependabot[bot]' && always()
131140
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
132141
with:
133142
name: coverage-report
134143
path: coverage.xml
135144
retention-days: 30
136145

137146
- name: Upload coverage to Codecov
138-
if: steps.check.outputs.should_test == 'true'
147+
if: steps.check.outputs.should_test == 'true' && github.actor != 'dependabot[bot]'
139148
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6
140149
with:
141150
token: ${{ secrets.CODECOV_TOKEN }}

.github/workflows/daily-regen.yml

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
11
name: "Scheduled: Regen oldest specs"
2-
run-name: "Scheduled regen (${{ github.event.inputs.count || '2' }} specs)"
2+
run-name: "Scheduled regen (${{ github.event.inputs.count || '1' }} specs)"
33

44
# Picks the N oldest specs (by most-recent implementation `updated` timestamp)
5-
# and re-dispatches `bulk-generate.yml` for each. Default N=2 per cron tick.
5+
# and re-dispatches `bulk-generate.yml` for each. Default N=1 per cron tick.
66
#
7-
# Schedule: every 2h between 00:00 and 12:00 UTC (02:00–14:00 Berlin CEST).
8-
# The 14:00–22:00 UTC slots (16:00–24:00 Berlin) are intentionally skipped so
9-
# runs never start during the user's 18:00–24:00 Berlin interactive window.
7+
# Schedule: every 2h, skipping the 20:00–24:00 Berlin (CEST) evening window.
8+
# Berlin CEST run hours: 00, 02, 04, 06, 08, 10, 12, 14, 16, 18 → UTC 22, 00,
9+
# 02, 04, 06, 08, 10, 12, 14, 16. The 20:00 and 22:00 Berlin slots (UTC 18, 20)
10+
# are intentionally skipped so runs never start during the user's evening.
1011
#
11-
# bulk-generate is serialised via its own concurrency group, so the 2 dispatched
12-
# specs run sequentially. With Sonnet + reduced bulk-generate pace, a spec
13-
# completes well within the 2h slot, leaving the user window clean.
12+
# bulk-generate is serialised via its own concurrency group. With Sonnet +
13+
# reduced bulk-generate pace, a single spec completes well within the 2h slot,
14+
# leaving the user window clean.
1415
#
1516
# Triggers:
16-
# - schedule: 7× daily (UTC, every 2h up to 12:00)
17+
# - schedule: 10× daily (UTC, every 2h except 18:00 and 20:00 UTC)
1718
# - workflow_dispatch: manual, with inputs for count + dry-run
1819

1920
on:
20-
# Temporarily paused — re-enable in a few days.
21-
# schedule:
22-
# - cron: '0 0,2,4,6,8,10,12 * * *'
21+
schedule:
22+
- cron: '0 0,2,4,6,8,10,12,14,16,22 * * *'
2323
workflow_dispatch:
2424
inputs:
2525
count:
26-
description: "How many of the oldest specs to regen (default 2)"
26+
description: "How many of the oldest specs to regen (default 1)"
2727
required: false
28-
default: '2'
28+
default: '1'
2929
min_age_hours:
3030
description: "Skip specs regen'd within this many hours (default 20)"
3131
required: false
@@ -63,7 +63,7 @@ jobs:
6363
- name: Pick oldest spec(s)
6464
id: pick
6565
env:
66-
COUNT: ${{ inputs.count || '2' }}
66+
COUNT: ${{ inputs.count || '1' }}
6767
MIN_AGE_HOURS: ${{ inputs.min_age_hours || '20' }}
6868
run: |
6969
python3 <<'PY'

.github/workflows/impl-generate.yml

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,6 @@ concurrency:
3939
group: impl-generate-${{ inputs.specification_id || github.event.issue.number }}-${{ inputs.library || github.event.label.name }}
4040
cancel-in-progress: false
4141

42-
env:
43-
# Library dependencies mapping
44-
# Per-library deps now come from `pyproject.toml` `lib-{library}` extras —
45-
# see [project.optional-dependencies] there. Keeping this list in sync was
46-
# painful and silently drifted (this env block lagged the pyproject floors
47-
# by a full major version on plotly/altair). Single source of truth wins.
48-
4942
jobs:
5043
generate:
5144
# Run on label trigger OR workflow_dispatch
@@ -182,7 +175,7 @@ jobs:
182175
python-version: '3.13'
183176

184177
- name: Install uv
185-
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
178+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
186179

187180
- name: Install system dependencies
188181
run: |
@@ -312,7 +305,7 @@ jobs:
312305
id: claude
313306
continue-on-error: true
314307
timeout-minutes: 60
315-
uses: anthropics/claude-code-action@567fe954a4527e81f132d87d1bdbcc94f7737434 # v1
308+
uses: anthropics/claude-code-action@ef50f123a3a9be95b60040d042717517407c7256 # v1
316309
with:
317310
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
318311
claude_args: "--model sonnet"
@@ -331,7 +324,7 @@ jobs:
331324
if: steps.claude.outcome == 'failure'
332325
id: claude_retry
333326
timeout-minutes: 60
334-
uses: anthropics/claude-code-action@567fe954a4527e81f132d87d1bdbcc94f7737434 # v1
327+
uses: anthropics/claude-code-action@ef50f123a3a9be95b60040d042717517407c7256 # v1
335328
with:
336329
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
337330
claude_args: "--model sonnet"

.github/workflows/impl-merge.yml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,10 +178,20 @@ jobs:
178178
- name: Merge PR to main (with retry)
179179
if: steps.check.outputs.should_run == 'true'
180180
env:
181-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
181+
# ADMIN_TOKEN: PAT with admin scope from a repo-admin user, used so
182+
# that `gh pr merge --admin` can bypass the main-branch ruleset
183+
# (required-status-checks). Falls back to GITHUB_TOKEN if not set so
184+
# the workflow still runs and fails with a clear ruleset error
185+
# instead of an opaque auth error.
186+
GH_TOKEN: ${{ secrets.ADMIN_TOKEN || secrets.GITHUB_TOKEN }}
182187
PR_NUM: ${{ steps.check.outputs.pr_number }}
183188
REPOSITORY: ${{ github.repository }}
189+
HAS_ADMIN_TOKEN: ${{ secrets.ADMIN_TOKEN != '' }}
184190
run: |
191+
if [ "$HAS_ADMIN_TOKEN" != "true" ]; then
192+
echo "::warning::ADMIN_TOKEN secret is not set — merge will fail if main ruleset enforces required status checks. Add a fine-grained PAT with Contents:Write + Pull requests:Write + Administration:Read+Write as repo secret ADMIN_TOKEN."
193+
fi
194+
185195
MAX_ATTEMPTS=5
186196
187197
for attempt in $(seq 1 $MAX_ATTEMPTS); do
@@ -191,9 +201,19 @@ jobs:
191201
gh pr update-branch "$PR_NUM" --repo "$REPOSITORY" 2>/dev/null || true
192202
sleep 2
193203
204+
# --admin bypasses the branch ruleset's required-status-check
205+
# gate. Required because impl-generate.yml pushes via GITHUB_TOKEN,
206+
# which by GitHub's anti-recursion design does not trigger
207+
# downstream CI workflows (Run Linting / Run Tests / Run Frontend
208+
# Tests), so impl PRs never get those checks. The pipeline already
209+
# gates merge behind the AI quality review threshold.
210+
#
211+
# Bypass only works if the token has admin role. GITHUB_TOKEN is
212+
# only `write`, so a repo-admin PAT is required (ADMIN_TOKEN).
194213
if gh pr merge "$PR_NUM" \
195214
--repo "$REPOSITORY" \
196215
--squash \
216+
--admin \
197217
--delete-branch; then
198218
echo "::notice::Merge successful on attempt $attempt"
199219
exit 0

.github/workflows/impl-repair.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ jobs:
6262
python-version: '3.13'
6363

6464
- name: Install uv
65-
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
65+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
6666

6767
- name: Install dependencies
6868
env:
@@ -104,7 +104,7 @@ jobs:
104104
id: claude
105105
continue-on-error: true
106106
timeout-minutes: 45
107-
uses: anthropics/claude-code-action@567fe954a4527e81f132d87d1bdbcc94f7737434 # v1
107+
uses: anthropics/claude-code-action@ef50f123a3a9be95b60040d042717517407c7256 # v1
108108
with:
109109
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
110110
claude_args: "--model sonnet"
@@ -123,7 +123,7 @@ jobs:
123123
if: steps.claude.outcome == 'failure'
124124
id: claude_retry
125125
timeout-minutes: 45
126-
uses: anthropics/claude-code-action@567fe954a4527e81f132d87d1bdbcc94f7737434 # v1
126+
uses: anthropics/claude-code-action@ef50f123a3a9be95b60040d042717517407c7256 # v1
127127
with:
128128
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
129129
claude_args: "--model sonnet"

.github/workflows/impl-review.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ jobs:
140140
id: review
141141
continue-on-error: true
142142
timeout-minutes: 30
143-
uses: anthropics/claude-code-action@567fe954a4527e81f132d87d1bdbcc94f7737434 # v1
143+
uses: anthropics/claude-code-action@ef50f123a3a9be95b60040d042717517407c7256 # v1
144144
env:
145145
LIBRARY: ${{ steps.pr.outputs.library }}
146146
SPEC_ID: ${{ steps.pr.outputs.specification_id }}
@@ -154,10 +154,10 @@ jobs:
154154
Read `prompts/workflow-prompts/ai-quality-review.md` and follow those instructions.
155155
156156
Variables for this run:
157-
- LIBRARY: $LIBRARY
158-
- SPEC_ID: $SPEC_ID
159-
- PR_NUMBER: $PR_NUMBER
160-
- ATTEMPT: $ATTEMPT
157+
- LIBRARY: ${{ steps.pr.outputs.library }}
158+
- SPEC_ID: ${{ steps.pr.outputs.specification_id }}
159+
- PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
160+
- ATTEMPT: ${{ steps.attempts.outputs.display }}
161161
162162
- name: Extract quality score
163163
id: score

0 commit comments

Comments
 (0)