Skip to content

Commit 954249c

Browse files
ihhclaude
andcommitted
Add complete CI validation pipeline, test entry, GitHub Pages site, and auto-approve workflow
- CI validation pipeline (ci/validate.py + ci/checks/) with modular checks: stockholm parsing, annotation consistency, protein/DNA checks, provenance, structures, and weighted scoring (diversity 40%, annotation 25%, functionality 20%, size 15%) - Test entry (entries/test-entry/) built from real Mos1 transposase data (M14653.1) plus two synthetic variants - GitHub Pages leaderboard site (docs/) with dark theme, expandable detail panels, and submission instructions - Auto-approve workflow for PRs scoped to entries/{username}/ - Branch protection requiring validation + 1 approval - 39 pytest tests covering all check modules and integration - POLICY.md, README.md, .gitignore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d3a1989 commit 954249c

36 files changed

Lines changed: 3352 additions & 2 deletions

.github/workflows/auto-approve.yml

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
name: Auto-approve own entry
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
paths:
7+
- 'entries/**'
8+
9+
permissions:
10+
pull-requests: write
11+
contents: write
12+
13+
jobs:
14+
check-and-approve:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Check if PR only modifies own entry
18+
id: check
19+
uses: actions/github-script@v7
20+
with:
21+
script: |
22+
const pr = context.payload.pull_request;
23+
const author = pr.user.login.toLowerCase();
24+
const expectedDir = `entries/${author}/`;
25+
26+
// Get all files changed in this PR
27+
const files = await github.paginate(
28+
github.rest.pulls.listFiles,
29+
{ owner: context.repo.owner, repo: context.repo.repo, pull_number: pr.number }
30+
);
31+
32+
const changedPaths = files.map(f => f.filename);
33+
core.info(`PR author: ${author}`);
34+
core.info(`Expected directory: ${expectedDir}`);
35+
core.info(`Changed files: ${changedPaths.join(', ')}`);
36+
37+
// Every changed file must be under entries/{author}/
38+
const unauthorized = changedPaths.filter(
39+
p => !p.toLowerCase().startsWith(expectedDir)
40+
);
41+
42+
if (unauthorized.length > 0) {
43+
core.info(`Unauthorized changes: ${unauthorized.join(', ')}`);
44+
core.setOutput('approved', 'false');
45+
core.setOutput('reason',
46+
`PR modifies files outside entries/${author}/: ${unauthorized.join(', ')}`
47+
);
48+
return;
49+
}
50+
51+
if (changedPaths.length === 0) {
52+
core.setOutput('approved', 'false');
53+
core.setOutput('reason', 'No files changed');
54+
return;
55+
}
56+
57+
core.setOutput('approved', 'true');
58+
core.setOutput('reason', `All ${changedPaths.length} files are under ${expectedDir}`);
59+
60+
- name: Approve PR
61+
if: steps.check.outputs.approved == 'true'
62+
uses: actions/github-script@v7
63+
with:
64+
script: |
65+
const pr = context.payload.pull_request;
66+
await github.rest.pulls.createReview({
67+
owner: context.repo.owner,
68+
repo: context.repo.repo,
69+
pull_number: pr.number,
70+
event: 'APPROVE',
71+
body: `Auto-approved: all changes are scoped to \`entries/${pr.user.login.toLowerCase()}/\`.`
72+
});
73+
core.info('PR approved');
74+
75+
- name: Enable auto-merge
76+
if: steps.check.outputs.approved == 'true'
77+
uses: actions/github-script@v7
78+
with:
79+
script: |
80+
const pr = context.payload.pull_request;
81+
try {
82+
await github.graphql(`
83+
mutation($prId: ID!) {
84+
enablePullRequestAutoMerge(input: {
85+
pullRequestId: $prId,
86+
mergeMethod: SQUASH
87+
}) { clientMutationId }
88+
}
89+
`, { prId: pr.node_id });
90+
core.info('Auto-merge enabled');
91+
} catch (e) {
92+
// Auto-merge may not be enabled in repo settings — that's OK
93+
core.warning(`Could not enable auto-merge: ${e.message}`);
94+
}
95+
96+
- name: Post rejection comment
97+
if: steps.check.outputs.approved == 'false'
98+
uses: actions/github-script@v7
99+
with:
100+
script: |
101+
const pr = context.payload.pull_request;
102+
const reason = '${{ steps.check.outputs.reason }}';
103+
await github.rest.issues.createComment({
104+
owner: context.repo.owner,
105+
repo: context.repo.repo,
106+
issue_number: pr.number,
107+
body: `This PR cannot be auto-approved. ${reason}\n\nA maintainer will need to review manually.`
108+
});

.github/workflows/validate.yml

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
name: Validate entries
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'entries/**'
8+
pull_request:
9+
branches: [main]
10+
paths:
11+
- 'entries/**'
12+
13+
permissions:
14+
contents: write
15+
16+
jobs:
17+
validate:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
with:
23+
fetch-depth: 2
24+
25+
- name: Set up Python
26+
uses: actions/setup-python@v5
27+
with:
28+
python-version: '3.11'
29+
30+
- name: Install dependencies
31+
run: pip install -r ci/requirements.txt
32+
33+
- name: Identify modified entries
34+
id: entries
35+
run: |
36+
if [ "${{ github.event_name }}" = "pull_request" ]; then
37+
# For PRs, diff against the base branch
38+
git fetch origin ${{ github.base_ref }} --depth=1
39+
CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD -- entries/ \
40+
| cut -d/ -f2 | sort -u | grep -v '^$' || true)
41+
else
42+
CHANGED=$(git diff --name-only HEAD~1 HEAD -- entries/ \
43+
| cut -d/ -f2 | sort -u | grep -v '^$' || true)
44+
fi
45+
echo "entries=$CHANGED" >> "$GITHUB_OUTPUT"
46+
echo "Modified entries: $CHANGED"
47+
48+
- name: Remove scores for deleted entries
49+
if: github.event_name == 'push' && steps.entries.outputs.entries != ''
50+
run: |
51+
for TEAM in ${{ steps.entries.outputs.entries }}; do
52+
if [ ! -d "entries/$TEAM" ]; then
53+
echo "=== Entry $TEAM was deleted, removing score ==="
54+
rm -f "scores/$TEAM.json"
55+
rm -f "docs/scores/$TEAM.json"
56+
fi
57+
done
58+
59+
- name: Validate entries
60+
if: steps.entries.outputs.entries != ''
61+
run: |
62+
COMMIT=$(git rev-parse --short HEAD)
63+
EXIT=0
64+
for TEAM in ${{ steps.entries.outputs.entries }}; do
65+
if [ ! -d "entries/$TEAM" ]; then
66+
echo "=== Skipping deleted entry $TEAM ==="
67+
continue
68+
fi
69+
echo "=== Validating $TEAM ==="
70+
python ci/validate.py \
71+
--entry "entries/$TEAM" \
72+
--commit "$COMMIT" \
73+
--scores-dir scores \
74+
--leaderboard leaderboard.json || EXIT=1
75+
done
76+
# Rebuild leaderboard to reflect any deletions
77+
python -c "
78+
from pathlib import Path; import sys; sys.path.insert(0, 'ci')
79+
from validate import update_leaderboard
80+
update_leaderboard(Path('scores'), Path('leaderboard.json'))
81+
"
82+
exit $EXIT
83+
84+
- name: Update GitHub Pages data
85+
if: always() && github.event_name == 'push' && steps.entries.outputs.entries != ''
86+
run: |
87+
mkdir -p docs/scores
88+
cp leaderboard.json docs/ 2>/dev/null || true
89+
cp scores/*.json docs/scores/ 2>/dev/null || true
90+
91+
- name: Commit results
92+
if: always() && github.event_name == 'push' && steps.entries.outputs.entries != ''
93+
run: |
94+
git config user.name "github-actions[bot]"
95+
git config user.email "github-actions[bot]@users.noreply.github.com"
96+
git add scores/ leaderboard.json docs/
97+
git diff --cached --quiet || \
98+
git commit -m "Update scores and leaderboard [skip ci]"
99+
git push

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__pycache__/
2+
*.pyc
3+
.pytest_cache/

CLAUDE.md

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ tc1-leaderboard/
2727
│ ├── team-alpha.json # Validation results + score
2828
│ └── team-beta.json
2929
├── leaderboard.json # Aggregated ranked leaderboard
30+
├── docs/ # GitHub Pages site (auto-updated by CI)
31+
│ ├── index.html # Main leaderboard page
32+
│ ├── style.css # Styling
33+
│ ├── leaderboard.js # Client-side rendering from JSON
34+
│ ├── leaderboard.json # Copy of root leaderboard.json
35+
│ └── scores/ # Copies of score files
3036
├── ci/
3137
│ ├── validate.py # Main validation + scoring script
3238
│ ├── checks/ # Modular validation checks
@@ -209,6 +215,8 @@ All checks produce a pass/fail/warn status and a message. An entry with any hard
209215
- `assembly` is a plausible accession format.
210216
- Coordinates are internally consistent (`start` < `end`, `strand` is `+` or `-`).
211217

218+
If it is not beyond the scope of GitHub actions (e.g. if there is a REST service for a fast subsequence retrieval that we can query), provenance should be verified.
219+
212220
### Scoring
213221

214222
The score for an entry is a weighted combination of factors designed to reward phylogenetically diverse, well-annotated, high-confidence collections. The total score is normalized to 0–100.
@@ -241,7 +249,7 @@ Goal: span the phylogenetic space of the ITm superfamily, not pile up near-ident
241249

242250
- Logarithmic scaling: score = min(1, log2(N) / log2(target)), where `target` is a configurable parameter (e.g. 64 sequences).
243251
- This rewards having enough sequences to be useful as a seed dataset, but saturates — 200 sequences is not much better than 64 for a seed. The point is to span space, not to be exhaustive.
244-
- Hard penalty if N < 3 (entry is rejected).
252+
- Hard penalty if N < 3 or N > 300 (entry is rejected).
245253

246254
#### Score normalization
247255

@@ -304,10 +312,63 @@ The GitHub Actions workflow (`.github/workflows/validate.yml`) triggers on pushe
304312

305313
Entries that fail hard validation checks (format errors, missing required files, ID mismatches) receive a score of 0 and a descriptive error in their score file.
306314

315+
307316
## Updating an entry
308317

309318
To update an entry, simply push a new commit modifying files in your `entries/{team}/` directory. The CI will re-run validation and overwrite the previous score. The leaderboard always reflects the latest commit for each team.
310319

320+
## GitHub Pages leaderboard site
321+
322+
The leaderboard is published as a static site via GitHub Pages, automatically updated by CI.
323+
324+
### Site structure
325+
326+
```
327+
docs/
328+
├── index.html # Main leaderboard page (auto-generated)
329+
├── style.css # Minimal styling
330+
└── leaderboard.js # Reads leaderboard.json and renders the table
331+
```
332+
333+
### How it works
334+
335+
1. The validation CI workflow (`.github/workflows/validate.yml`) produces `leaderboard.json` at the repo root as before.
336+
2. A second workflow step (or a dedicated workflow `.github/workflows/pages.yml`) runs after validation completes:
337+
- Copies `leaderboard.json` and all `scores/*.json` into `docs/`.
338+
- Commits any changes to `docs/`.
339+
3. GitHub Pages is configured to serve from the `docs/` folder on the `main` branch (Settings → Pages → Source: `main` / `docs`).
340+
4. `docs/index.html` is a single-page app that fetches `leaderboard.json` at load time and renders a ranked table with columns: rank, team, total score, sub-scores (diversity, annotation, functionality, size), number of sequences, number of families, and last commit.
341+
5. Clicking a team name expands to show per-check status and per-sequence issues from `scores/{team}.json`.
342+
343+
### Design constraints
344+
345+
- **No build step.** The site is plain HTML + CSS + vanilla JS. No bundler, no framework, no Node.
346+
- **Data-driven.** All content comes from the JSON files. The HTML/JS never hard-codes team names or scores.
347+
- **Accessible.** Semantic HTML table, readable without JS (a `<noscript>` fallback links directly to `leaderboard.json`).
348+
- **Mobile-friendly.** Responsive table layout via CSS.
349+
- **Auto-refresh.** The CI keeps `docs/leaderboard.json` up to date; the page always shows the latest data because it fetches from the same repo's GitHub Pages URL.
350+
351+
### CI integration
352+
353+
The pages deployment step should be added to the end of the existing validation workflow:
354+
355+
```yaml
356+
# After the validation and scoring steps:
357+
- name: Update GitHub Pages data
358+
run: |
359+
mkdir -p docs
360+
cp leaderboard.json docs/
361+
cp scores/*.json docs/scores/ 2>/dev/null || true
362+
363+
- name: Commit docs
364+
run: |
365+
git add docs/
366+
git diff --cached --quiet || git commit -m "Update leaderboard site [skip ci]"
367+
git push
368+
```
369+
370+
The `[skip ci]` tag prevents infinite CI loops from the docs commit.
371+
311372
## Reuse plan
312373

313374
The validation logic in `ci/checks/` is designed as a library that can be imported and reused. The "big data" leaderboard (Stage 2) will validate entries stored in S3 using the same annotation format and similar checks, extended with:
@@ -349,4 +410,5 @@ When asked to work on this project, Claude should:
349410
4. **Maintain a clear policy document POLICY.md at the top level, describing the scoring scheme and hard/soft constraints.** The scoring scheme and validation may change, but the current policy should always be transparent.
350411
5. **Write tests.** The `ci/` directory should include test fixtures (minimal valid and invalid entries) and pytest-based tests for every check.
351412
6. **Design for reuse.** The Stockholm parser, annotation validator, and protein/DNA consistency checks will be reused for the big data leaderboard. Keep them modular and dependency-light.
352-
7. **Do not over-engineer.** This is a research tool, not a production service. Clarity and correctness over abstraction.
413+
7. **Do not over-engineer.** This is a research tool, not a production service. Clarity and correctness over abstraction.
414+
8. **Keep the GitHub Pages site working.** When changing the score output format or leaderboard.json schema, update `docs/leaderboard.js` to match. The site must always render correctly from the current JSON structure.

POLICY.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Scoring Policy — ITm Seed Dataset Leaderboard
2+
3+
This document describes the current scoring scheme and validation rules. It is the authoritative reference for what the CI pipeline checks and how scores are computed.
4+
5+
## Hard-fail checks (score = 0 if any fail)
6+
7+
### Format checks
8+
- `protein.sto` must be valid Stockholm 1.0 with a `#=GC catalytic_triad` line.
9+
- `dna.sto` must be valid multi-Stockholm with exactly one sequence per block and a `#=GC element_structure` line per block.
10+
- `provenance.tsv` must be valid TSV with all required columns: `id`, `family`, `host_species`, `host_taxid`, `assembly`, `chrom`, `start`, `end`, `strand`, `source`, `reference`.
11+
- All IDs must be consistent across `protein.sto`, `dna.sto`, and `provenance.tsv`.
12+
- Minimum 3 sequences, maximum 300.
13+
14+
### Annotation consistency checks
15+
- `catalytic_triad` must mark exactly 2–3 positions with D, d, and E (or D for DDD families). Marked columns must contain the expected residue in the majority of sequences.
16+
- `element_structure` must:
17+
- Use only valid characters: `5 3 < > A B 0 1 2 n t .`
18+
- Follow the pattern: `5+AA<+[interior]+>+BB3+`
19+
- Have `A` and `B` positions (TSD) spelling `TA` in the sequence.
20+
- Have TIRs (`<` and `>`) that are approximate reverse complements (>70% match).
21+
- Have ORF annotation (`012`) with correct codon phasing and length divisible by 3.
22+
- Have ORF translation matching the protein in `protein.sto` at >=90% identity.
23+
24+
## Scored checks (reduce score but do not reject)
25+
26+
### Protein-level
27+
- Transposase length within expected range for declared family.
28+
- Catalytic triad spacing matches declared family (e.g. DD34D = 34 residues between 2nd and 3rd).
29+
30+
### DNA-level
31+
- Element length (excluding flanking) within expected range.
32+
- TIR length within expected range.
33+
- ORF GC content between 15% and 75%.
34+
- No ambiguous bases (N) in the ORF.
35+
36+
### Provenance
37+
- `host_taxid` is a valid positive integer.
38+
- `assembly` matches GC[AF]_NNNNNNNNN.N format.
39+
- Coordinates are consistent (`start` < `end`, `strand` is `+` or `-`).
40+
41+
## Scoring weights
42+
43+
| Component | Weight | What it rewards |
44+
|-----------|--------|-----------------|
45+
| Sequence diversity | 40% | Phylogenetic breadth, multi-family coverage |
46+
| Annotation completeness | 25% | Passing all checks, optional metadata |
47+
| Evidence of functionality | 20% | Near-identical paralogs, experimental data, literature |
48+
| Collection size | 15% | Enough sequences to seed a model (target: 64) |
49+
50+
## Scoring details
51+
52+
### Diversity (40%)
53+
- Mean pairwise protein distance (1 − identity) across all sequence pairs.
54+
- Multi-family bonus: +10% per additional family (up to +50%).
55+
- Redundancy penalty: −2% per pair with >95% protein identity.
56+
57+
### Annotation completeness (25%)
58+
- Base score from check pass/warn/fail rates.
59+
- Bonuses for optional metadata: `paralog_hits` (+1% per entry), `confidence_tier` (+0.5% per entry), `pdb_file` (+1% per entry).
60+
61+
### Functionality (20%)
62+
- Per-sequence scoring, averaged:
63+
- `max_paralog_identity` >= 0.99: +40%
64+
- `max_paralog_identity` >= 0.95: +15%
65+
- Experimental PDB: +10%
66+
- Predicted PDB: +3%
67+
- Literature reference with DOI: +5%
68+
- Confidence tier bonus (tier 1 = +10%, tier 2 = +7%, tier 3 = +4%, tier 4 = +2%)
69+
70+
### Size (15%)
71+
- `min(1, log2(N) / log2(64))`
72+
- Saturates at 64 sequences. Hard reject if N < 3 or N > 300.
73+
74+
## Tie-breaking
75+
76+
Entries with equal total scores are ranked by annotation completeness, then by diversity.
77+
78+
## Policy changes
79+
80+
This policy may be updated. The leaderboard always reflects the current policy. When the policy changes, all entries are re-scored on the next CI run.

0 commit comments

Comments
 (0)