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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches: [main]
pull_request:
workflow_dispatch:
schedule:
- cron: "0 6 * * 1"

Expand Down Expand Up @@ -76,6 +77,9 @@ jobs:
- name: Run linters and type checks (pre-push stage)
run: make prepush-check

- name: Verify public API type completeness
run: make type-completeness

- name: Run CI/CD policy contract tests
run: make ci-contracts

Expand Down
526 changes: 526 additions & 0 deletions .local/public-api-surface-improvements.md

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ repos:
rev: v0.15.1
hooks:
- id: ruff
args: [--fix]
args: [--fix, --ignore, TID251]
stages: [pre-commit]
- id: ruff
args: [--ignore, TID251]
stages: [pre-push]

- repo: https://github.com/psf/black
Expand Down Expand Up @@ -44,6 +45,12 @@ repos:

- repo: local
hooks:
- id: import-lint
name: import-lint
entry: bash scripts/run_import_lint.sh
language: system
pass_filenames: false
stages: [pre-push]
- id: pyright
name: pyright
entry: uv run --frozen --extra dev pyright --pythonversion 3.12 ser tests
Expand Down
11 changes: 8 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help setup setup-runtime fmt lint type test test-cov check ci train predict optin-all-restricted quality-gate-full prepush prepush-check prepush-hook import-lint lock-check workflow-lint ci-contracts clean
.PHONY: help setup setup-runtime fmt lint type type-completeness test test-cov check ci train predict optin-all-restricted quality-gate-full prepush prepush-check prepush-hook import-lint lock-check workflow-lint ci-contracts clean

.DEFAULT_GOAL := help

Expand All @@ -11,6 +11,7 @@ help:
@echo " fmt - format code"
@echo " lint - run linters"
@echo " type - run type checks"
@echo " type-completeness - enforce pyright verifytypes public API completeness"
@echo " test - run tests"
@echo " test-cov - run tests with branch coverage gating"
@echo " check - lint + type + test"
Expand All @@ -35,19 +36,23 @@ setup-runtime:

fmt:
uv run --frozen --extra dev pyupgrade --py312-plus --exit-zero-even-if-changed $$(rg --files ser tests -g '*.py')
uv run --frozen --extra dev ruff check --fix ser tests
uv run --frozen --extra dev ruff check --fix --ignore TID251 ser tests
uv run --frozen --extra dev isort ser tests
uv run --frozen --extra dev black ser tests

lint:
uv run --frozen --extra dev ruff check ser tests
uv run --frozen --extra dev ruff check --ignore TID251 ser tests
bash ./scripts/run_import_lint.sh
uv run --frozen --extra dev black --check ser tests
uv run --frozen --extra dev isort --check-only ser tests

type:
uv run --frozen --extra dev mypy ser tests
uv run --frozen --extra dev pyright --pythonversion 3.12 ser tests

type-completeness:
uv run --frozen --extra dev python scripts/check_type_completeness.py

test:
uv run --frozen --extra dev pytest -q

Expand Down
32 changes: 23 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ dev = [
"pre-commit>=4.0.0,<5.0.0",
"pytest>=9.1.1,<10.0.0",
"coverage[toml]>=7.6.0,<8.0.0",
"griffe>=1.14.0,<2.0.0",
"hypothesis>=6.130.0,<7.0.0",
"ruff>=0.9.0,<1.0.0",
"black>=26.3.1,<27.0.0",
Expand Down Expand Up @@ -102,7 +103,7 @@ exclude = [

[tool.hatch.build.targets.wheel]
packages = ["ser"]
include = ["ser/profile_defs.yaml"]
include = ["ser/profile_defs.yaml", "ser/py.typed"]
exclude = [
"ser/.gitignore",
"ser/configure",
Expand Down Expand Up @@ -141,17 +142,27 @@ select = ["E", "F", "B", "TID251"]
ignore = ["E501"]

[tool.ruff.lint.flake8-tidy-imports.banned-api]
"ser._internal.api".msg = "Internal API modules are private; import from `ser.api`."
"ser._internal.api.data".msg = "Internal API modules are private; import from `ser.api`."
"ser._internal.api.diagnostics".msg = "Internal API modules are private; import from `ser.api`."
"ser._internal.api.runtime".msg = "Internal API modules are private; import from `ser.api`."
"ser._internal".msg = "Internal modules are private; public facades importing them must be declared in boundary_policy.toml."

[tool.ruff.lint.per-file-ignores]
# TID251 exceptions mirror boundary_policy.toml; keep the policy file authoritative.
"ser/__main__.py" = ["TID251"]
"ser/api.py" = ["TID251"]
"ser/_internal/cli/data.py" = ["TID251"]
"ser/_internal/cli/diagnostics.py" = ["TID251"]
"ser/_internal/cli/runtime.py" = ["TID251"]
"tests/suites/integration/api/test_api.py" = ["TID251"]
"ser/config.py" = ["TID251"]
"ser/data/application.py" = ["TID251"]
"ser/diagnostics/service.py" = ["TID251"]
"ser/models/emotion_model.py" = ["TID251"]
"ser/models/profile_runtime.py" = ["TID251"]
"ser/models/training_entrypoints.py" = ["TID251"]
"ser/repr/emotion2vec.py" = ["TID251"]
"ser/runtime/accurate_inference.py" = ["TID251"]
"ser/runtime/fast_inference.py" = ["TID251"]
"ser/runtime/medium_inference.py" = ["TID251"]
"ser/runtime/pipeline.py" = ["TID251"]
"ser/runtime/profile_quality_gate.py" = ["TID251"]
"ser/transcript/backends/stable_whisper.py" = ["TID251"]
"ser/transcript/profiling.py" = ["TID251"]
"ser/transcript/transcript_extractor.py" = ["TID251"]

[tool.ruff.format]
quote-style = "double"
Expand Down Expand Up @@ -182,6 +193,9 @@ exclude = ["**/__pycache__", "dist", "build", ".venv", ".*"]
reportMissingImports = "warning"
reportMissingTypeStubs = "none"

[tool.ser.type_completeness]
threshold = 0.9788235294117648

[tool.coverage.run]
branch = true
concurrency = ["multiprocessing"]
Expand Down
93 changes: 93 additions & 0 deletions scripts/check_type_completeness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""Run pyright verifytypes and enforce the configured completeness ratchet."""

from __future__ import annotations

import json
import subprocess
import sys
import tomllib
from pathlib import Path
from typing import Any

PYRIGHT_VERIFYTYPES_COMMAND = (
"pyright",
"--verifytypes",
"ser",
"--ignoreexternal",
"--outputjson",
)


def _load_threshold(repo_root: Path) -> float:
"""Loads the configured type-completeness threshold from pyproject."""
pyproject = tomllib.loads((repo_root / "pyproject.toml").read_text(encoding="utf-8"))
threshold = pyproject["tool"]["ser"]["type_completeness"]["threshold"]
if not isinstance(threshold, int | float):
raise TypeError("[tool.ser.type_completeness].threshold must be a number.")
return float(threshold)


def _run_pyright(repo_root: Path) -> dict[str, Any]:
"""Runs pyright verifytypes and returns its JSON payload."""
completed = subprocess.run(
PYRIGHT_VERIFYTYPES_COMMAND,
cwd=repo_root,
capture_output=True,
text=True,
)
try:
payload = json.loads(completed.stdout)
except json.JSONDecodeError as err:
sys.stderr.write(completed.stdout)
sys.stderr.write(completed.stderr)
raise RuntimeError("pyright --verifytypes did not emit valid JSON.") from err
if not isinstance(payload, dict):
raise RuntimeError("pyright --verifytypes JSON payload must be an object.")
return payload


def _read_score(payload: dict[str, Any]) -> float:
"""Reads the verifytypes completeness score with shape validation."""
completeness = payload.get("typeCompleteness")
if not isinstance(completeness, dict):
raise RuntimeError("pyright JSON missing typeCompleteness object.")
score = completeness.get("completenessScore")
if not isinstance(score, int | float):
raise RuntimeError("pyright JSON missing numeric completenessScore.")
return float(score)


def _read_error_count(payload: dict[str, Any]) -> int:
"""Reads the pyright summary error count."""
summary = payload.get("summary")
if not isinstance(summary, dict):
raise RuntimeError("pyright JSON missing summary object.")
error_count = summary.get("errorCount")
if not isinstance(error_count, int):
raise RuntimeError("pyright JSON missing integer summary.errorCount.")
return error_count


def main() -> int:
"""CLI entry point."""
repo_root = Path.cwd()
threshold = _load_threshold(repo_root)
payload = _run_pyright(repo_root)
error_count = _read_error_count(payload)
score = _read_score(payload)
print(f"pyright verifytypes completeness: {score:.10f} (threshold {threshold:.10f})")
if error_count:
print(f"pyright verifytypes reported {error_count} errors.", file=sys.stderr)
return 1
if score < threshold:
print(
f"pyright verifytypes completeness {score:.10f} is below threshold {threshold:.10f}.",
file=sys.stderr,
)
return 1
return 0


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