Imported from fastomop/fastssv (
AGENTS.md). Install upstream withnpx skills add fastomop/fastssv. Copyright stays with the author.
AGENTS.md
Shared guidance for AI coding agents on FastSSV (Fast Semantic Static Validator). Format follows the agents.md standard so every agent (Claude Code, Codex, OpenCode, Cursor, …) reads the same file. See Cross-tool layout for tool-specific entry points.
For the AI-assisted PR policy (Linux-kernel-style disclosure, DCO, library-skills format), see CONTRIBUTING.md.
What this is
A static, semantic validator for SQL written against the OMOP CDM v5.4, built on sqlglot. Catches schema, vocabulary, and modelling errors that pass syntax check but produce silently-wrong analytics — no DB connection. Ships a CLI (fastssv …) and an optional FastAPI service ([api] extra).
Setup
requires-python = ">=3.10". The project uses uv end-to-end (install):
uv sync --frozen --extra dev --extra api --extra mcp # full dev env (API + MCP)
uv sync --frozen --extra docs # docs work
Commands
| Task | Command |
|---|---|
| Tests | uv run --frozen --no-sync pytest tests/ -v |
| Tests + coverage | uv run --frozen --no-sync pytest tests/ --cov |
| Lint | uvx ruff check src/ tests/ |
| Format | uvx ruff format src/ tests/ |
| Pre-commit | uvx prek run --all-files |
| Build sdist + wheel | uv build |
| Serve API locally | uv run --frozen --no-sync fastssv serve --reload |
| Serve docs | uv run --frozen --no-sync zensical serve |
| Build container | docker compose -f deploy/docker-compose.yml build |
| Run container | docker compose -f deploy/docker-compose.yml up |
--no-sync on uv run skips lock revalidation; uv sync --frozen above already established the env.
Layout
src/fastssv/
api/ FastAPI service ([api] extra) — Jinja templates, htmx UI;
api/_validation.py is the shared statement-split + strict +
timeout helper used by both /v1/validate and the MCP tool
mcp/ MCP Streamable HTTP server ([mcp] extra) — mounted by
api/app.py at /mcp; one tool (validate_sql)
core/ base Rule, registry, helpers
rules/<category>/ validation rules; one rule per file, self-registering
schemas/ OMOP CDM column types and standard-concept field set
tests/ pytest (unit + api integration); rule tests in tests/test_rules.py
docs/ zensical source (zensical.toml at repo root)
deploy/ Dockerfile + docker-compose for the API + MCP endpoint
.github/workflows/ tests.yml, docs.yml, publish.yml
.agents/ shared agent assets (skills, prompts) — see Cross-tool layout
Rule categories: anti_patterns, concept_standardization, data_quality, domain_specific, joins, temporal.
Code style
- ruff (
[tool.ruff]inpyproject.toml). Line length 120, target 3.10+. - Project ignores:
E501(pre-existing long lines),E741(l/Inames),E402(intentional in some__init__.pyfor circular-import avoidance),F841(WIP regex/match locals in some rules). src/fastssv/__init__.pywaivesF401— it imports submodules purely for@registerside effects.tests/test_rules.pyis inextend-exclude(skipped by ruff, still run by pytest).- Coverage gate:
fail_under = 79under[tool.coverage.*].
Build & release
[build-system].build-backend = "uv_build".uv buildproduces sdist + wheel.- Tag-driven publish:
.github/workflows/publish.ymlfires onv*and aborts unless the tag matches[project].version.
Adding a validation rule
A dedicated Skill walks through this. Short version:
- Create the rule module in the package matching its category. Five categories are flat — file at
src/fastssv/rules/<category>/<snake_name>.py(norule_prefix; match existing names likedatetime_between_date_literal.py).domain_specificis nested — table-specific rules live atsrc/fastssv/rules/domain_specific/<table>/<table>_<snake_name>.py(e.g.domain_specific/measurement/measurement_cross_unit_comparison.py); cross-cutting domain rules stay flat atsrc/fastssv/rules/domain_specific/<snake_name>.py. - Subclass
Rule(fastssv.core.base) and decorate with@register(fastssv.core.registry). Setrule_id = "<category>.<snake_name>"— 2-segment is the documented stable format and the convention across all 6 categories. The directory nesting underdomain_specific/<table>/is organisational only and must not appear in the id. Also setname,description,severity,suggested_fix. - Wire the class into the closest
__init__.py: flat categories usesrc/fastssv/rules/<category>/__init__.py;domain_specifictable rules usesrc/fastssv/rules/domain_specific/<table>/__init__.py(the parent imports each table subpackage for its@registerside effects). Addfrom .<file> import <Class>and append"<Class>"to__all__. Leavesrc/fastssv/rules/__init__.pyalone — it imports category packages, not individual rules. - Unit-test passing and failing SQL in
tests/test_rules.py. uv run --frozen --no-sync pytest tests/test_rules.py -v.
Changelog
CHANGELOG.md follows Keep a Changelog; semver from 1.0.0 onward. Update ## [Unreleased] for every user-visible change — new rules, rule behaviour shifts, CLI/API surface, build/dependency changes, removals, fixes — under the right ### Added/Changed/Fixed/Removed/Deprecated/Security heading. Skip purely internal changes unless they alter observable behaviour. Match the existing style: bold lead-in summarising the change, then a short paragraph on context, previous vs new behaviour, and user impact.
After making changes
For every kind of change, before reporting done:
- Sweep for stale references. Whenever you rename or remove a public symbol,
rule_id, severity, exception, CLI flag, command, dependency, config key, file, or feature — grep the whole tree and update every hit. Coversrc/,tests/,scripts/,examples/,docs/,README.md,## [Unreleased]inCHANGELOG.md,AGENTS.md(and theCLAUDE.mdsymlink),.github/workflows/,deploy/Dockerfile,deploy/docker-compose.yml. Same applies to behaviour changes that don't rename anything: walk every caller, comment, docstring, and test still describing the old contract. Stale references rot silently. - Run pre-commit hooks:
uvx prek run --all-files(oruvx prek runfor staged files). Prek readsprek.toml— trailing whitespace, EOL, YAML/TOML validity, merge markers, large-file guard,ruff check --fix,ruff format. Fix anything flagged. - Verify end-to-end — backend AND frontend. Green
pytestis necessary but not sufficient: the API web UI (src/fastssv/api/ui.py, Jinja templates undersrc/fastssv/api/templates/, htmx + CSS undersrc/fastssv/api/static/) only has thin smoke tests. For changes touchingsrc/fastssv/api/,src/fastssv/mcp/, dependencies, the deploy bundle, or anything that could affect request handling or asset serving — boot locally withuv run --frozen --no-sync fastssv serve --reloadand click through index, rules listing, and a sample SQL validation. For MCP changes alsocurl -X POST http://localhost:8000/mcp/ -H 'Accept: application/json, text/event-stream' -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"c","version":"0"}}}'and confirm a 200 withserverInfo.name=fastssv(the/mcpmount is a Starlette sub-app and does NOT appear in/openapi.jsonor/docs— that's expected). If you can't verify the UI, say so explicitly in the handoff.
Conventions
uv sync/uv addonly — neveruv pip install. CI and Docker useuv sync --frozen;uv pipis a compat shim.- End-user docs are uv-native too —
README.md,docs/index.md,docs/api.md,docs/mcp.md,docs/plugin_architecture.mduseuv add "fastssv[…]"+uv run fastssv …for install + run examples. Nopip install, nouv tool install(global PATH), nouv pip install(compat shim). HistoricalCHANGELOG.mdentries describing earlier releases keep their original wording — don't rewrite history. - Edit existing files over creating new ones.
[project.optional-dependencies]extras grouped:dev,docs,api,mcp. New optional groups go alongside.- The
[api]extra usesfastapi[standard], which transitively pullsuvicorn[standard],jinja2,python-multipart, andhttpx. Don't re-add those at the top level — let FastAPI manage them. Only add deps FastAPI doesn't pull in (current additions:gunicorn,slowapi,pydantic-settings). - The API's
cors_originssetting accepts an empty string, a comma-separated list, or a JSON list (see thefield_validatorinsrc/fastssv/api/config.py). Don't undo that tolerance —pydantic-settings>=2would otherwise crash on the empty-string default thatdeploy/docker-compose.ymlpasses through. The same_parse_origin_listvalidator is reused formcp_allowed_origins. - The
[mcp]extra is independent of[api]. The MCP server is mounted byapi/app.pyonly whensettings.mcp_enabled=TrueAND themcppackage imports successfully. If the operator opts in (FASTSSV_API_MCP_ENABLED=true) but the extra isn't installed,_maybe_build_mcp_appraisesRuntimeErrorat startup so the misconfiguration fails loudly in non-interactive deployments (CI/docker) instead of silently booting without/mcp. Don't revert that to a warning-and-skip path. Regression guard:tests/api/test_mcp.py::test_mcp_enabled_but_extra_missing_raises. - Middleware ordering in
api/app.pyis load-bearing:MCPOriginMiddlewareshort-circuits with a 403 on disallowed Origins, so it MUST be registered before (and therefore inside)SecurityHeadersMiddlewareandRequestIDMiddlewareso those wrap the rejection response. There's a regression test intests/api/test_mcp.py::test_origin_disallowed_returns_403.
Cross-tool layout
AGENTS.md is the canonical project-wide agent file (agents.md spec). Tool-specific entry points alias into the shared assets via tracked symlinks so every contributor sees the same content regardless of which agent they use:
CLAUDE.md→AGENTS.md(Claude Code).claude/<file>→.agents/<file>for individual shared assets.claude/skills/<name>→../../.agents/skills/<name>for skills
Personal preferences (your own permission allowlist, ad-hoc env vars) belong in .claude/settings.local.json, which is gitignored. Shared, team-level Claude Code settings go in .claude/settings.json.
To add a shared agent asset: put the source-of-truth in .agents/<name> (or .agents/skills/<name>/SKILL.md for skills following the agentskills.io format that tiangolo/library-skills builds on), then symlink:
ln -s ../.agents/<name> .claude/<name>
ln -s ../../.agents/skills/<name> .claude/skills/<name>
(Windows contributors: WSL, "Developer Mode" enabled, or git config core.symlinks true plus admin rights.)