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
44 changes: 44 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
repository-gates:
name: repository gates
runs-on: ubuntu-latest
steps:
# Full history is required by scripts/check_file_sizes.py.
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: python scripts/check_repo_assets.py
- run: python scripts/check_file_sizes.py
- run: python scripts/check_project_inventory.py

core:
name: core tests and package
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- run: python -m pip install --upgrade pip build
- run: python -m pip install ".[dev]"
- run: pytest -q skillcorpus/tests
- run: python -m build
- run: python scripts/check_package_contents.py dist/*.whl
58 changes: 58 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Publishes the root ``skillcorpus`` package through PyPI Trusted Publishing,
# then creates a GitHub Release. A tag is the explicit release approval;
# merging a normal pull request never publishes a package.
name: Release

on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+*"

permissions:
contents: read

jobs:
publish:
name: build + publish to PyPI
runs-on: ubuntu-latest
environment: release
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Verify tag matches package version
run: |
tag="${GITHUB_REF_NAME#v}"
version="$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")"
test "$tag" = "$version" || {
echo "::error::tag v$tag does not match pyproject.toml version $version"
exit 1
}
- run: python -m pip install --upgrade pip build
- run: python -m build
- run: python scripts/check_package_contents.py dist/*.whl
- name: Publish to PyPI with OIDC
uses: pypa/gh-action-pypi-publish@release/v1

github-release:
name: create the GitHub Release
needs: publish
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- name: Create a release if one does not already exist
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.ref_name }}
run: |
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Release $TAG already exists; leaving it unchanged."
exit 0
fi
gh release create "$TAG" --title "SkillCorpus ${TAG#v}" --generate-notes
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ This is the concrete inventory of what is public today.
service is closed, and the full hosted catalog is not yet published as a downloadable dataset.*

<div align="center">
<img src="docs/assets/taxonomy.png" alt="16-class distribution over the 96,401 active skills" width="58%">
<img src="https://github.com/user-attachments/assets/71edc5ab-291f-4fb8-8f5c-177d50e5b8f4" alt="16-class distribution over the 96,401 active skills" width="58%">
</div>

The 96,401-skill snapshot measured in the paper, organised by a 16-class taxonomy and three quality facets
Expand Down Expand Up @@ -237,7 +237,7 @@ To curate **your own** sources instead, see [Build your own corpus](#build-your-
## How it works

<div align="center">
<img src="docs/assets/pipeline.png" alt="SkillCorpus: curated skills are matched to a task and injected into an agent before execution" width="100%">
<img src="https://github.com/user-attachments/assets/e0e72150-373b-4381-ad6c-74668d436d49" alt="SkillCorpus: curated skills are matched to a task and injected into an agent before execution" width="100%">
<p><em>The collection pipeline is the foundation; the payoff is task-specific skill retrieval before the agent acts.</em></p>
</div>

Expand Down
4 changes: 2 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ Raven 插件已经可以安装,但要等 Raven 上游合并 `context_segments`
*目前开源:代码、1,000 条 demo 语料和检索模型。托管的 SkillHub 服务不开源,完整在线目录也尚未作为可下载数据集发布。*

<div align="center">
<img src="docs/assets/taxonomy.png" alt="96,401 个有效技能的 16 类分布" width="58%">
<img src="https://github.com/user-attachments/assets/71edc5ab-291f-4fb8-8f5c-177d50e5b8f4" alt="96,401 个有效技能的 16 类分布" width="58%">
</div>

论文所评测的 96,401 条快照,按 16 类体系和三个质量维度(utility / robustness / safety)组织,并带 1024 维
Expand Down Expand Up @@ -222,7 +222,7 @@ EMBEDDING_MODEL=<embedding 检查点目录> RERANKER_MODEL=<reranker 检查点
## 工作原理

<div align="center">
<img src="docs/assets/pipeline.png" alt="SkillCorpus:把策展后的技能匹配到任务,并在执行前注入 agent 上下文" width="100%">
<img src="https://github.com/user-attachments/assets/e0e72150-373b-4381-ad6c-74668d436d49" alt="SkillCorpus:把策展后的技能匹配到任务,并在执行前注入 agent 上下文" width="100%">
<p><em>收集和策展是基础,真正的价值是 agent 执行前能拿到与任务匹配的技能。</em></p>
</div>

Expand Down
Binary file removed docs/assets/pipeline.png
Binary file not shown.
Binary file removed docs/assets/taxonomy.png
Binary file not shown.
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,5 @@ eval = [
# Keeping the package name `skillcorpus` (not renamed) so existing
# `python -m skillcorpus.*` invocations and absolute imports keep working.
[tool.setuptools.packages.find]
include = ["skillcorpus*"]

include = ["skillcorpus", "skillcorpus.*"]
exclude = ["skillcorpus.tests", "skillcorpus.tests.*"]
126 changes: 126 additions & 0 deletions scripts/check_file_sizes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"""Block oversized files introduced or enlarged by the current change.

This mirrors the local ``check-added-large-files`` hook, but covers modified
files too. The check compares the working tree to the merge base, so CI must
check out full history (``fetch-depth: 0``).
"""

from __future__ import annotations

import argparse
import os
import subprocess
from collections.abc import Iterable, Sequence
from dataclasses import dataclass
from pathlib import Path

MAX_KB = 640
DEFAULT_BASE = "origin/main"
EXEMPT_PREFIXES: tuple[str, ...] = ()
EXEMPT_PATHS = frozenset()


class BaseRefError(RuntimeError):
"""The comparison base could not be resolved."""


@dataclass(frozen=True)
class Violation:
path: str
size_bytes: int


def _repo_root() -> Path:
return Path(__file__).resolve().parent.parent


def _git(root: Path, *args: str) -> str:
result = subprocess.run(["git", *args], cwd=root, capture_output=True, text=True)
if result.returncode:
detail = result.stderr.strip() or "unknown error"
raise BaseRefError(f"`git {' '.join(args)}` failed: {detail}")
return result.stdout


def default_base_ref() -> str:
base = os.environ.get("GITHUB_BASE_REF", "").strip()
return f"origin/{base}" if base else DEFAULT_BASE


def changed_paths(root: Path, base_ref: str) -> list[str]:
merge_base = _git(root, "merge-base", base_ref, "HEAD").strip()
if not merge_base:
raise BaseRefError(f"no merge base between {base_ref} and HEAD")

changed = _git(
root,
"diff",
"--name-only",
"-z",
"--diff-filter=ACMR",
merge_base,
)
untracked = _git(root, "ls-files", "--others", "--exclude-standard", "-z")
paths = {path for path in changed.split("\0") if path}
paths.update(path for path in untracked.split("\0") if path)
return sorted(paths)


def is_exempt(path: str, prefixes: Sequence[str] = EXEMPT_PREFIXES) -> bool:
normalised = path.replace("\\", "/")
return normalised in EXEMPT_PATHS or any(
normalised.startswith(prefix) for prefix in prefixes
)


def find_violations(
paths: Iterable[str],
*,
root: Path,
max_kb: int = MAX_KB,
) -> list[Violation]:
violations: list[Violation] = []
for path in paths:
candidate = root / path
if is_exempt(path) or candidate.is_symlink() or not candidate.is_file():
continue
if candidate.stat().st_size > max_kb * 1024:
violations.append(Violation(path=path, size_bytes=candidate.stat().st_size))
return violations


def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base", help="ref to diff against")
args = parser.parse_args(argv)

root = _repo_root()
base_ref = args.base or default_base_ref()
try:
paths = changed_paths(root, base_ref)
except BaseRefError as exc:
print(
f"Repository file-size check could not run: {exc}\n"
"Fetch the base branch and full history first; CI must use fetch-depth: 0."
)
return 1

violations = find_violations(paths, root=root)
if not violations:
print(
f"Repository file-size check passed ({len(paths)} changed file(s) vs "
f"{base_ref}, ceiling {MAX_KB} KB)."
)
return 0

print(
f"Repository file-size check failed: this change adds or grows files above "
f"{MAX_KB} KB. Store large payloads externally or as a release artifact."
)
for violation in sorted(violations, key=lambda item: -item.size_bytes):
print(f"- {violation.path}: {violation.size_bytes / 1024:.1f} KB")
return 1


if __name__ == "__main__":
raise SystemExit(main())
43 changes: 43 additions & 0 deletions scripts/check_package_contents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Verify that the root wheel contains only the root distribution's code."""

from __future__ import annotations

import argparse
import zipfile
from collections.abc import Sequence
from pathlib import Path

FORBIDDEN_PREFIXES = ("skillcorpus_plugin/", "skillcorpus/tests/")


def _check_wheel(path: Path) -> list[str]:
with zipfile.ZipFile(path) as archive:
names = archive.namelist()
failures: list[str] = []
if "skillcorpus/__init__.py" not in names:
failures.append(f"{path.name}: missing skillcorpus/__init__.py")
if not any(name.endswith(".dist-info/licenses/LICENSE") for name in names):
failures.append(f"{path.name}: missing packaged LICENSE")
for prefix in FORBIDDEN_PREFIXES:
leaked = sorted(name for name in names if name.startswith(prefix))
if leaked:
preview = ", ".join(leaked[:3])
failures.append(f"{path.name}: must not include {prefix} ({preview})")
return failures


def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("wheel", nargs="+", type=Path)
args = parser.parse_args(argv)
failures = [failure for path in args.wheel for failure in _check_wheel(path)]
if failures:
print("Package contents check failed:")
print("\n".join(f"- {failure}" for failure in failures))
return 1
print(f"Package contents check passed ({len(args.wheel)} wheel(s)).")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading