Skip to content

Commit 6639f08

Browse files
helebestLe Heclaude
authored
ci: add lint/type/test + dataset & eval-baseline gates (Milestone A) (#5)
Close the CI/CD gap so dikw-data has the same deterministic floor as dikw-core before Phase 1 dataset construction begins. - pyproject: ruff + mypy (strict) config, mirroring dikw-core; ignore RUF001/2/3 (false positives on this bilingual zh/en codebase's embedded CJK text) and scope-ignore E702 to the one procedural-pictogram generator. - .pre-commit-config.yaml: local ruff + mypy hooks (uv run), matching CI. - .github/workflows/ci.yml: uv sync -> ruff -> mypy src -> pytest -> validate every dataset (shape gate, $0, no provider keys). Matrix 3.12/3.13. - .github/workflows/eval-gate.yml + tools/check_baselines.py: a dataset change (datasets/**) must land a new dated reports/BASELINES.md entry naming a retrieval metric; override with the `no-baseline-needed` label. Unit-tested. - .gitignore: track reports/BASELINES.md (the baseline log) while keeping per-run artifacts ignored; ignore .impeccable/. - Apply ruff autofixes (import sorting, unused-import / whitespace cleanup) across scripts/, src/, web/, tests/ so the existing tree passes the new gate. Fix 3 real lint findings (RUF005 in run_eval.py, dead code in generate_queries_local) and 2 mypy findings (unused ignore; int**int Any-widening in llm_client). All green locally: ruff clean, mypy clean, 50 tests pass, 3 datasets validate. Co-authored-by: Le He <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
1 parent b3fdaee commit 6639f08

34 files changed

Lines changed: 782 additions & 52 deletions

.github/workflows/ci.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: CI
2+
3+
# Deterministic floor for dikw-data: lint, type-check, unit tests, and a $0
4+
# dataset shape-gate. Mirrors dikw-core's reusable-ci lint-type-test job, minus
5+
# the engine-specific legs (Postgres / wheel / e2e). NO provider keys are used —
6+
# this workflow never calls `dikw client eval`, so it makes no API requests.
7+
8+
on:
9+
push:
10+
branches: [main]
11+
pull_request:
12+
13+
concurrency:
14+
group: ${{ github.workflow }}-${{ github.ref }}
15+
cancel-in-progress: true
16+
17+
permissions:
18+
contents: read
19+
20+
jobs:
21+
lint-type-test:
22+
runs-on: ubuntu-latest
23+
timeout-minutes: 15
24+
strategy:
25+
fail-fast: false
26+
matrix:
27+
python-version: ["3.12", "3.13"]
28+
steps:
29+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
30+
31+
- name: Install uv
32+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
33+
with:
34+
enable-cache: true
35+
36+
- name: Set up Python ${{ matrix.python-version }}
37+
run: uv python install ${{ matrix.python-version }}
38+
39+
- name: Sync dependencies
40+
run: uv sync
41+
42+
- name: Ruff
43+
run: uv run ruff check .
44+
45+
- name: Mypy
46+
run: uv run mypy src
47+
48+
- name: Pytest
49+
run: uv run pytest
50+
51+
- name: Validate datasets (shape gate, no API)
52+
# scripts/validate_dataset.py exits non-zero on the first invalid dataset,
53+
# so under `bash -e` a bad dataset fails the job. Costs nothing — pure
54+
# file-shape checks (required files, corpus refs, target relationships).
55+
run: |
56+
for d in datasets/*/; do
57+
echo "== validating $d =="
58+
uv run python scripts/validate_dataset.py "$d"
59+
done

.github/workflows/eval-gate.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Eval gate
2+
3+
# A dataset change (corpus / queries / thresholds) shifts the engine's retrieval
4+
# numbers, so it must land with a baseline entry recording the real-vector
5+
# outcome. This is the dikw-data analog of dikw-core's eval-gate: a *content*
6+
# check (parse the added reports/BASELINES.md lines, assert a new dated entry that
7+
# names a retrieval metric), not a presence check — a blank-line edit won't pass.
8+
# Re-running the eval to verify the numbers is separate (needs provider keys).
9+
#
10+
# Override: label the PR `no-baseline-needed` for a dataset edit that genuinely
11+
# shifts no numbers (a corpus typo fix, a rename).
12+
13+
on:
14+
pull_request:
15+
paths:
16+
- 'datasets/**'
17+
18+
permissions:
19+
contents: read
20+
21+
jobs:
22+
baseline-must-update:
23+
runs-on: ubuntu-latest
24+
timeout-minutes: 5
25+
steps:
26+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
27+
with:
28+
fetch-depth: 0
29+
30+
# Override via a native expression (no shell): a label containing a quote
31+
# can't break detection the way an inline grep on the label could.
32+
- name: Skip note (no-baseline-needed)
33+
if: ${{ contains(github.event.pull_request.labels.*.name, 'no-baseline-needed') }}
34+
run: |
35+
echo "::notice::PR labeled 'no-baseline-needed' — skipping baseline content check."
36+
echo "Reviewer is expected to confirm the dataset change shifts no numbers."
37+
38+
# SHAs come in via env (not inline ${{ }} in the script) — the safe pattern
39+
# for workflow inputs. check_baselines.py is stdlib-only, so no uv/setup-python.
40+
- name: Content-check reports/BASELINES.md
41+
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-baseline-needed') }}
42+
env:
43+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
44+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
45+
run: |
46+
python3 tools/check_baselines.py --base-sha "$BASE_SHA" --head-sha "$HEAD_SHA"

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ __pycache__/
66
.pytest_cache/
77
.ruff_cache/
88
generated/
9-
reports/
9+
# Eval run artifacts are disposable, but the human-readable baseline LOG is the
10+
# tracked source of truth (the eval-gate workflow asserts a new entry on dataset
11+
# changes). Ignore everything under reports/ EXCEPT that log — a file inside a
12+
# fully-ignored dir cannot be re-included, so ignore the contents, not the dir.
13+
reports/*
14+
!reports/BASELINES.md
1015
bases/
1116
datasets/markdown-books/
17+
.impeccable/

.pre-commit-config.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Pre-commit hooks for dikw-data. Install once: `uv run pre-commit install`.
2+
#
3+
# Local hooks shell out to `uv run` so they use the EXACT ruff / mypy pinned in
4+
# pyproject — no version drift between the hook and CI (.github/workflows/ci.yml).
5+
# These are the cheap deterministic stages; the full floor (incl. pytest +
6+
# dataset validation) runs in CI.
7+
repos:
8+
- repo: local
9+
hooks:
10+
- id: ruff
11+
name: ruff check
12+
entry: uv run ruff check --force-exclude
13+
language: system
14+
types_or: [python, pyi]
15+
require_serial: true
16+
- id: mypy
17+
name: mypy (strict, src)
18+
entry: uv run mypy src
19+
language: system
20+
pass_filenames: false
21+
types: [python]

pyproject.toml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,57 @@ dependencies = [
1919
dev = [
2020
"pytest>=8.0",
2121
"pytest-asyncio>=0.23",
22+
"ruff>=0.15",
23+
"mypy>=1.20",
24+
"types-PyYAML",
25+
# Local git pre-commit hooks (ruff + mypy) mirroring the CI floor. Wire
26+
# them once with `uv run pre-commit install`; config in .pre-commit-config.yaml.
27+
"pre-commit>=4.0",
2228
]
2329

30+
[tool.ruff]
31+
line-length = 100
32+
target-version = "py312"
33+
# This repo keeps runnable code outside src/ too (scripts/, web/), so lint the lot.
34+
src = ["src", "scripts", "web", "tests"]
35+
36+
[tool.ruff.lint]
37+
select = ["E", "F", "W", "I", "UP", "B", "SIM", "C4", "RUF"]
38+
ignore = [
39+
"E501", # line length is handled by formatter
40+
# This is a bilingual zh/en data factory: prompts, sample corpora, and
41+
# query strings embed Chinese text and full-width punctuation directly in
42+
# .py files. RUF001/002/003 (ambiguous-unicode) then fire on every CJK
43+
# character — pure false positives here, so silence them repo-wide.
44+
"RUF001",
45+
"RUF002",
46+
"RUF003",
47+
]
48+
49+
[tool.ruff.lint.per-file-ignores]
50+
"tests/*" = ["SIM117"]
51+
# FastAPI's idiomatic ``Query(default=...)`` / ``Depends(...)`` in parameter
52+
# defaults is exactly the B008 anti-pattern, but here it's the framework's
53+
# contract — silence the rule scope-locally rather than refactoring endpoints.
54+
"web/*.py" = ["B008"]
55+
# This generator draws pictograms procedurally: each icon is one line of
56+
# grouped canvas draw-ops (``c.rect(...); c.line(...); wheel(...)``). The
57+
# semicolon grouping is deliberate — one line == one icon — so splitting it
58+
# per E702 would bloat and obscure the file. Scope the allowance to this file.
59+
"scripts/generate_additional_multimodal_datasets.py" = ["E702"]
60+
61+
[tool.mypy]
62+
python_version = "3.12"
63+
strict = true
64+
packages = ["dikw_data"]
65+
mypy_path = "src"
66+
explicit_package_bases = true
67+
68+
[[tool.mypy.overrides]]
69+
# Third-party modules without bundled type stubs in this repo's install.
70+
module = ["anthropic.*", "yaml.*"]
71+
ignore_missing_imports = true
72+
2473
[tool.pytest.ini_options]
2574
testpaths = ["tests"]
2675
addopts = "-ra -q"

reports/BASELINES.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# dikw-data eval baselines
2+
3+
Dated log of real-vector eval runs against the `dikw-core` engine — the tracked
4+
source of truth that mirrors `dikw-core/evals/BASELINES.md`. Everything else under
5+
`reports/` (per-run NDJSON + `summary.json`) is disposable and gitignored; this
6+
file is kept under version control via the `!reports/BASELINES.md` exception in
7+
`.gitignore`.
8+
9+
The `eval-gate` workflow (`.github/workflows/eval-gate.yml` +
10+
`tools/check_baselines.py`) requires a **new** entry here whenever a PR changes
11+
`datasets/**`: it must be a new dated header and name at least one retrieval
12+
metric. That keeps a dataset change from shifting the engine's numbers without a
13+
recorded, reviewable outcome.
14+
15+
## Entry template
16+
17+
```
18+
## <YYYY-MM-DD> — <short title>
19+
20+
- dikw-core: <version> provider: <llm>+<embedder> retrieval: <hybrid|all> cache: <mode>
21+
- <dataset>: ndcg_at_10 <v>, hit_at_3 <v>, hit_at_10 <v>, mrr <v>, recall_at_100 <v>
22+
- notes: <anchor delta / saturation / per-language split / std across reruns>
23+
```
24+
25+
## Entries
26+
27+
_None yet._ The first real entries come from the Phase 0→1 public-anchor
28+
calibration (`scifact` + `cmteb-t2-subset`); see `docs/dikw-eval-plan.md` §2.3 and
29+
`docs/phase0-smoke-results.md`. Phase 0 set **no gates** — the synthetic sets
30+
saturate at 1.0, so thresholds wait for non-saturated, anchored data.

scripts/add_multi_image_chunks.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
import yaml
88

9-
109
ROOT = Path(__file__).resolve().parents[1]
1110
DATASET = "synthetic-multimodal-datasets-v1"
1211
DATASET_DIR = ROOT / "datasets" / DATASET

scripts/apply_imagegen_multimodal_sheets.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
from PIL import Image
77

8-
98
ROOT = Path(__file__).resolve().parents[1]
109
DATASET = "synthetic-multimodal-datasets-v1"
1110
SHEET_DIR = Path.home() / ".codex" / "generated_images" / "019dca28-eaa6-77f0-8a22-0e9235befec2"

scripts/audit_corpus_quality.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import sys
77
from pathlib import Path
88

9-
109
BAD_PATTERNS = [
1110
"The user wants",
1211
"We need",

scripts/augment_multimodal_dataset.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
from generate_multimodal_asset_chunk_dataset import (
99
CATEGORIES as BASE_CATEGORIES,
10+
)
11+
from generate_multimodal_asset_chunk_dataset import (
1012
CORPUS_DIR,
1113
DATASET,
1214
DATASET_DIR,
@@ -15,7 +17,6 @@
1517
yaml_scalar,
1618
)
1719

18-
1920
SHEET_DIR = Path.home() / ".codex" / "generated_images" / "019dca28-eaa6-77f0-8a22-0e9235befec2"
2021

2122

0 commit comments

Comments
 (0)