G0: cell substrate — invariants made executable

Strand G0 complete and VERIFIED against real Postgres, not asserted.

  - FastAPI plane, fail-closed provider seams, repair-pointer error taxonomy
  - migration 001: all 10 tables incl. repo_type NOT NULL and model_cards (I-7)
  - 17 invariant tests, ruff clean, vocabulary audit clean

Two bugs found by RUNNING it that review would not have caught:

  1. SQLAlchemy Enum persists .name, not .value — so RepoState.deleted_soft
     and CreatedVia.imported would have written labels migration 001 never
     declared, failing at runtime rather than at review. Pinned via
     values_callable.
  2. op.create_table asks each Enum to emit its own CREATE TYPE with no
     checkfirst, so the second reference raised DuplicateObject and the
     migration died halfway. Types are now created once, referenced with
     create_type=False.

Proven live, with the hostile env var set:
  - I-12: COMMIT_SHA=deadbeef... in the environment, /version reports real HEAD.
    That env pin is the documented root cause of nine sibling services
    misreporting their commit; here it is structurally ignored.
  - I-8: three unconfigured providers -> status degraded, HTTP 503, each saying
    'refusing to report healthy'. No mock, no false green.
  - G0.4: upgrade -> downgrade -> upgrade round-trip clean (10 -> 0 -> 10).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Grant Whitmer
2026-08-11 14:19:28 -04:00
parent 24708914c9
commit 659991b2bd
29 changed files with 2040 additions and 0 deletions

33
.env.example Normal file
View File

@@ -0,0 +1,33 @@
# windy-git — copy to .env. NEVER commit the real file.
# Providers fail CLOSED (I-8): leave a credential unset and that provider
# reports itself unconfigured rather than answering from a mock.
ENVIRONMENT=development
PORT=8600
DATABASE_URL=postgresql+asyncpg://windygit:windygit@localhost:5432/windygit
# Gitea — a COMPONENT behind an API membrane, never a merged tree (D-2/I-1)
GITEA_BASE_URL=http://localhost:3000
GITEA_ADMIN_TOKEN=
# Cloudflare R2 — heavy bytes ONLY. Never git object stores (I-3).
R2_ACCOUNT_ID=193b347aedeaafe35de0b5a534b2d9aa
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET_LFS=windy-git-lfs
R2_BUCKET_ARTIFACTS=windy-git-artifacts
R2_BUCKET_BACKUPS=windy-git-backups
# Eternitas — the one issuer
ETERNITAS_BASE_URL=https://api.eternitas.ai
ETERNITAS_PLATFORM_API_KEY=
# account-server OIDC — human identity
ACCOUNT_SERVER_BASE_URL=https://account.windyword.ai
# I-3: git object databases live on a POSIX filesystem, full stop.
GIT_DATA_ROOT=/srv/windygit/git
# COMMIT_SHA is deliberately NOT here. Setting it does nothing — the sha is
# baked into the image at build time and a runtime override is ignored (I-12).

3
.gitignore vendored
View File

@@ -24,3 +24,6 @@ cloudflared/*.json
# OS # OS
.DS_Store .DS_Store
*.egg-info/
data/
.venv/

29
Dockerfile Normal file
View File

@@ -0,0 +1,29 @@
FROM python:3.12-slim
# I-12: the sha is baked HERE, at build time, from the artifact's own stamp.
# A runtime COMMIT_SHA env var is ignored by api/app/buildinfo.py. This is the
# structural fix for nine sibling services that cannot name their own commit.
ARG COMMIT_SHA=""
ARG BUILT_AT=""
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends git curl \
&& rm -rf /var/lib/apt/lists/*
COPY pyproject.toml ./
RUN pip install --no-cache-dir -e .
COPY api ./api
COPY alembic ./alembic
COPY alembic.ini scripts ./
COPY scripts ./scripts
RUN sed -i "s|^BAKED_COMMIT_SHA: str = \"\"|BAKED_COMMIT_SHA: str = \"${COMMIT_SHA}\"|" api/app/buildinfo.py \
&& sed -i "s|^BAKED_BUILT_AT: str = \"\"|BAKED_BUILT_AT: str = \"${BUILT_AT}\"|" api/app/buildinfo.py \
&& grep -q "BAKED_COMMIT_SHA: str = \"${COMMIT_SHA}\"" api/app/buildinfo.py
EXPOSE 8600
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \
CMD curl -fsS http://localhost:8600/health || exit 1
CMD ["uvicorn", "api.app.main:app", "--host", "0.0.0.0", "--port", "8600"]

41
Makefile Normal file
View File

@@ -0,0 +1,41 @@
# The local gate IS the merge gate (G0.6).
# No decorative CI: a workflow that cannot run is deleted, not committed.
.PHONY: help dev check lint type test vocab migrate downgrade fmt
help:
@echo "make dev — app + gitea + postgres, seeded fixtures (G0.8)"
@echo "make check — THE GATE: lint + type + test + vocab + drift"
@echo "make migrate — alembic upgrade head"
@echo "make fmt — ruff format + fix"
dev:
docker compose up -d
@echo "api http://localhost:8600/health/full"
@echo "gitea http://localhost:3000"
check: lint type vocab test
@echo ""
@echo " ✓ gate passed"
lint:
ruff check api scripts
type:
mypy api/app --ignore-missing-imports
vocab:
@python3 scripts/vocab_audit.py
test:
pytest -q
fmt:
ruff format api scripts
ruff check --fix api scripts
migrate:
alembic upgrade head
downgrade:
alembic downgrade -1

87
SUBSTRATE.md Normal file
View File

@@ -0,0 +1,87 @@
# SUBSTRATE.md — windy-git
What runs where, what is truth, and what is only a cache.
## Hosts
| Rung | Host | Role | Status |
|---|---|---|---|
| **R0** | **Veron 1** (`Veron-1-5090`, WireGuard `10.10.0.6`) | everything | **current** |
| R1 | dedicated VPS (**not Kit 0**) | control plane | on first external push |
| R2 | VPS + read replica, dedicated runner box | split | p95 clone > 3s |
**Veron 1, measured 2026-08-11:** 24 cores · 251 GB RAM · 3.6 TB root, 978 GB free · load 1.52 · $0/mo.
⛔ **Kit 0 (72.60.118.54) is never a host for this service.** D-4, and there is a
boot guard in `api/app/main.py` that refuses to start there in production.
## Ports (all bound to localhost; the tunnel is the only ingress)
| Port | Service |
|---|---|
| 8600 | `windy-git-api` — our plane |
| 3000 | Gitea |
| 5432 | Postgres |
| 2000 | cloudflared metrics (probe target) |
## Ingress — Cloudflare Tunnel `windy-git`
Zone `windygit.com` = `9d8637dcac3415607b2116e6099fe567` · account `193b347aedeaafe35de0b5a534b2d9aa` · **Free plan**.
| Hostname | → |
|---|---|
| `app.windygit.com` | Gitea :3000 — UI **and** git over HTTPS |
| `api.windygit.com` | our plane :8600 |
| `models.windygit.com` | HF-compatible endpoint :8600 (v2) |
| `windygit.com` | Cloudflare Pages — marketing, Grant-gated |
**No inbound port is opened.** The tunnel connects outbound, so the residential
dynamic IP is irrelevant.
⚠️ **All hostnames are single-level subdomains, on purpose.** Free Universal SSL
covers `windygit.com` + `*.windygit.com` and stops there. A two-level name needs
Advanced Certificate Manager ($10/mo) and without it the request dies in the TLS
handshake — `curl` exit 35, **no HTTP status at all** — *before* the app is ever
consulted, so a perfect service presents as "the app is broken."
## Storage — what is truth
| Store | Holds | Truth? |
|---|---|---|
| `/srv/windygit/git` (local NVMe) | **git object databases** | **truth** |
| Postgres `windgit` | repos, grants, versions, tokens, mirrors | **truth** |
| Gitea's own DB | Gitea's private state | component-owned; we never write it (I-1) |
| R2 `windy-git-lfs` | LFS objects | truth for blobs |
| R2 `windy-git-artifacts` | CI artifacts, logs | derived |
| R2 `windy-git-backups` | nightly pg_dump + git bundles | derived |
| GitHub mirror | full copy of every repo | **belt and suspenders (I-4)** |
| 3 TB HDD | periodic cold copy | derived |
**I-3: git objects never go to object storage; LFS blobs never go to host disk.**
## Pinned versions
| Component | Version | Note |
|---|---|---|
| Gitea | `1.24.6` | **exact pin, never `latest`** (G2.1). A drift test fails `make check` if the running version differs. |
| Postgres | `16-alpine` | |
| Python | 3.12 | matches every sibling cell |
## Credentials
All in the fleet lockbox, injected by env, **never committed**. `make check`
fails on any `cfat_` / `cfut_` / `gh[pousr]_` / `et_plt_` literal in the tree.
⚠️ The R2 credential should be **scoped to this cell**. The sites cell ended up
holding the account-wide god token because v4 R2 object endpoints reject
restricted tokens. Whichever we end up with, record it here — an account-wide
token is an acceptable named debt and an unacceptable invisible one.
⚠️ The Cloudflare **god token has Zone:Read but no DNS:Edit.** Use the DNS:Edit
token for record creation.
## Backups (G0.9)
Nightly `pg_dump` → R2 · nightly `git bundle` per repo → R2 · **quarterly restore
drill via `make restore-drill`, with a written, dated result.** The ecosystem
currently has no rehearsed restore for anything, anywhere.

30
alembic.ini Normal file
View File

@@ -0,0 +1,30 @@
[alembic]
script_location = alembic
prepend_sys_path = .
version_path_separator = os
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s

53
alembic/env.py Normal file
View File

@@ -0,0 +1,53 @@
"""Alembic environment. Truth is this repo's migrations, not a hand-kept .sql file.
A sibling service names `postgres-schema.sql` as its source of truth in two
documents; that file omits the identity spine and ten tables, so a fresh deploy
from it produces a server that cannot register a user. Prod works only because
prod was built from migrations. We keep one source and it is `alembic/versions/`.
"""
from __future__ import annotations
import os
from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config, pool
from api.app.models.core import SCHEMA, Base
config = context.config
if config.config_file_name:
fileConfig(config.config_file_name)
target_metadata = Base.metadata
def _url() -> str:
url = os.environ.get("DATABASE_URL", "postgresql://windygit:windygit@localhost:5432/windygit")
return url.replace("+asyncpg", "")
def run_migrations_offline() -> None:
context.configure(url=_url(), target_metadata=target_metadata, literal_binds=True,
include_schemas=True, version_table_schema=SCHEMA)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
config.set_main_option("sqlalchemy.url", _url())
connectable = engine_from_config(config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.", poolclass=pool.NullPool)
with connectable.connect() as connection:
connection.exec_driver_sql(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
connection.commit()
context.configure(connection=connection, target_metadata=target_metadata,
include_schemas=True, version_table_schema=SCHEMA)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

23
alembic/script.py.mako Normal file
View File

@@ -0,0 +1,23 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from __future__ import annotations
import sqlalchemy as sa
from alembic import op
${imports if imports else ""}
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View File

@@ -0,0 +1,231 @@
"""001 genesis — the complete section 4 data model.
I-7: this migration creates `repo_type` NOT NULL and creates `model_cards`, even
though `repo_type=model` does not ship until v2. Shipping v1 with the v2 columns
absent is forbidden.
Every migration in this repo has a tested downgrade (G0.4). The sibling ecosystem
has a documented schema file that omits its own identity spine and ten tables; a
fresh deploy from it produces a server that cannot register a user. That happens
when migrations and documentation drift. Here the migration IS the truth.
Revision ID: 001_genesis
Revises:
Create Date: 2026-08-11
"""
from __future__ import annotations
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
revision = "001_genesis"
down_revision = None
branch_labels = None
depends_on = None
SCHEMA = "windgit"
def upgrade() -> None:
op.execute(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
bind = op.get_bind()
# Create each type ONCE, explicitly, then reference it with create_type=False.
#
# Without create_type=False, `op.create_table` asks the Enum to emit its own
# CREATE TYPE with no checkfirst, so the second reference to an already-created
# type raises DuplicateObject and the migration dies halfway through. This bug
# is invisible to review and to any test that does not run against real
# Postgres -- it surfaced here only because `alembic upgrade head` was actually
# executed. Every migration in this repo gets run before it gets committed.
_DEFS = {
"repo_type": ("code", "model", "dataset"),
"visibility": ("private", "unlisted", "public"),
"repo_state": ("active", "archived", "deleted-soft"),
"created_via": ("portal", "agent", "import", "cloud-folder"),
"grant_role": ("owner", "maintainer", "writer", "reader"),
"mirror_health": ("healthy", "degraded", "failed"),
}
for name, labels in _DEFS.items():
postgresql.ENUM(*labels, name=name, schema=SCHEMA).create(bind, checkfirst=True)
def _ref(name: str) -> postgresql.ENUM:
return postgresql.ENUM(*_DEFS[name], name=name, schema=SCHEMA, create_type=False)
repo_type = _ref("repo_type")
visibility = _ref("visibility")
repo_state = _ref("repo_state")
created_via = _ref("created_via")
grant_role = _ref("grant_role")
mirror_health = _ref("mirror_health")
op.create_table(
"repos",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("identity_id", sa.String(64), nullable=False),
sa.Column("passport", sa.String(32)),
sa.Column("slug", sa.String(128), nullable=False),
sa.Column("display_name", sa.String(255), nullable=False),
# I-7 — NOT NULL, from migration 001, forever.
sa.Column("repo_type", repo_type, nullable=False),
sa.Column("gitea_repo_id", sa.Integer),
sa.Column("visibility", visibility, nullable=False, server_default="private"),
sa.Column("cloud_folder_ref", sa.String(255)),
sa.Column("default_branch", sa.String(128), server_default="main"),
sa.Column("lfs_bytes", sa.BigInteger, server_default="0"),
sa.Column("object_bytes", sa.BigInteger, server_default="0"),
sa.Column("state", repo_state, server_default="active"),
sa.Column("created_via", created_via, nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.UniqueConstraint("identity_id", "slug", name="uq_repos_identity_slug"),
schema=SCHEMA,
)
op.create_index("ix_repos_repo_type", "repos", ["repo_type"], schema=SCHEMA)
op.create_index("ix_repos_passport", "repos", ["passport"], schema=SCHEMA)
op.create_table(
"repo_grants",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("repo_id", postgresql.UUID(as_uuid=True), sa.ForeignKey(f"{SCHEMA}.repos.id", ondelete="CASCADE"), nullable=False),
sa.Column("grantee_identity_id", sa.String(64)),
sa.Column("grantee_passport", sa.String(32)),
sa.Column("role", grant_role, nullable=False),
sa.Column("granted_by", sa.String(64), nullable=False),
sa.Column("expires_at", sa.DateTime(timezone=True)),
sa.Column("confirm_ref", sa.String(128)),
sa.Column("revoked_at", sa.DateTime(timezone=True)),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
# The invariant lives in the database, not in application code.
sa.CheckConstraint(
"(grantee_identity_id IS NULL) <> (grantee_passport IS NULL)",
name="ck_grant_exactly_one_grantee",
),
schema=SCHEMA,
)
op.create_index("ix_grants_repo", "repo_grants", ["repo_id"], schema=SCHEMA)
op.create_table(
"repo_versions",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("repo_id", postgresql.UUID(as_uuid=True), sa.ForeignKey(f"{SCHEMA}.repos.id", ondelete="CASCADE"), nullable=False),
sa.Column("seq", sa.Integer, nullable=False),
sa.Column("commit_sha", sa.String(64), nullable=False),
sa.Column("tree_sha", sa.String(64)),
sa.Column("author_identity_id", sa.String(64)),
sa.Column("author_passport", sa.String(32)),
sa.Column("signed", sa.Boolean, server_default=sa.false()),
sa.Column("signature_verified", sa.Boolean, server_default=sa.false()),
sa.Column("ei_at_action", sa.String(32)),
sa.Column("message", sa.Text),
sa.Column("bytes_added", sa.BigInteger, server_default="0"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.UniqueConstraint("repo_id", "seq", name="uq_version_repo_seq"),
schema=SCHEMA,
)
op.create_index("ix_versions_commit", "repo_versions", ["commit_sha"], schema=SCHEMA)
op.create_table(
"mirror_state",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("repo_id", postgresql.UUID(as_uuid=True), sa.ForeignKey(f"{SCHEMA}.repos.id", ondelete="CASCADE"), nullable=False),
sa.Column("remote_url", sa.String(512), nullable=False),
sa.Column("direction", sa.String(16), server_default="push"),
sa.Column("last_success_at", sa.DateTime(timezone=True)),
sa.Column("last_error", sa.Text),
sa.Column("lag_seconds", sa.Integer),
sa.Column("state", mirror_health, server_default="healthy"),
schema=SCHEMA,
)
op.create_table(
"agent_tokens",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("passport", sa.String(32), nullable=False),
sa.Column("repo_id", postgresql.UUID(as_uuid=True), sa.ForeignKey(f"{SCHEMA}.repos.id", ondelete="CASCADE")),
sa.Column("scopes", postgresql.ARRAY(sa.String(64)), nullable=False),
sa.Column("token_hash", sa.String(128), nullable=False, unique=True),
sa.Column("expires_at", sa.DateTime(timezone=True)),
sa.Column("last_used_at", sa.DateTime(timezone=True)),
sa.Column("revoked_at", sa.DateTime(timezone=True)),
sa.Column("revoked_reason", sa.String(255)),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
schema=SCHEMA,
)
op.create_index("ix_agent_tokens_passport", "agent_tokens", ["passport"], schema=SCHEMA)
op.create_table(
"agent_actions",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("passport", sa.String(32), nullable=False),
sa.Column("repo_id", postgresql.UUID(as_uuid=True)),
sa.Column("action", sa.String(64), nullable=False),
sa.Column("ei_at_action", sa.String(32)),
sa.Column("confirm_ref", sa.String(128)),
sa.Column("result", sa.String(32), nullable=False),
sa.Column("cost_microcents", sa.BigInteger, server_default="0"),
sa.Column("ts", sa.DateTime(timezone=True), server_default=sa.func.now()),
schema=SCHEMA,
)
op.create_index("ix_agent_actions_passport_ts", "agent_actions", ["passport", "ts"], schema=SCHEMA)
# v2 surface, v1 schema. I-7.
op.create_table(
"model_cards",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("repo_id", postgresql.UUID(as_uuid=True), sa.ForeignKey(f"{SCHEMA}.repos.id", ondelete="CASCADE"), nullable=False, unique=True),
sa.Column("base_model", sa.String(255)),
sa.Column("license", sa.String(64)),
sa.Column("pipeline_tag", sa.String(64)),
sa.Column("tags", postgresql.ARRAY(sa.String(64))),
sa.Column("library", sa.String(64)),
sa.Column("card_yaml", postgresql.JSONB),
sa.Column("card_body", sa.Text),
schema=SCHEMA,
)
op.create_table(
"jobs",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("kind", sa.String(64), nullable=False),
sa.Column("payload", postgresql.JSONB, nullable=False),
sa.Column("state", sa.String(32), server_default="pending"),
sa.Column("attempts", sa.Integer, server_default="0"),
sa.Column("last_error", sa.Text),
sa.Column("run_after", sa.DateTime(timezone=True)),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
schema=SCHEMA,
)
op.create_table(
"webhook_events",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("event_type", sa.String(64), nullable=False),
sa.Column("payload", postgresql.JSONB, nullable=False),
sa.Column("delivered", sa.Boolean, server_default=sa.false()),
sa.Column("attempts", sa.Integer, server_default="0"),
sa.Column("last_error", sa.Text),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
schema=SCHEMA,
)
def downgrade() -> None:
for table in (
"webhook_events", "jobs", "model_cards", "agent_actions",
"agent_tokens", "mirror_state", "repo_versions", "repo_grants", "repos",
):
op.drop_table(table, schema=SCHEMA)
bind = op.get_bind()
for name in ("mirror_health", "grant_role", "created_via", "repo_state", "visibility", "repo_type"):
postgresql.ENUM(name=name, schema=SCHEMA).drop(bind, checkfirst=True)
# Deliberately NOT dropping the schema. `alembic_version` lives inside
# `windgit` (env.py sets version_table_schema), so a DROP SCHEMA CASCADE here
# deletes alembic's own bookkeeping table out from under the migration that
# is still running, and the final DELETE FROM alembic_version fails. The
# schema is cheap to leave; the tables and types are what this reverses.

0
api/app/__init__.py Normal file
View File

87
api/app/buildinfo.py Normal file
View File

@@ -0,0 +1,87 @@
"""Deployment identity (I-12).
Nine of twelve live services in this ecosystem cannot name the commit they are
running. One reports *another repo's* commit. The root cause found on 2026-08-10
was hardcoded `COMMIT_SHA` pins in `/opt/*/.env` that OVERRIDE the build arg, so a
redeploy keeps reporting the old sha until a human hand-edits the file.
The fix here is structural, not procedural:
* the sha is baked into the image at build time (Docker ARG -> this module);
* a runtime `COMMIT_SHA` environment variable is **IGNORED**, loudly;
* in a dev worktree with nothing baked, we read git directly and SAY SO in
`source`, rather than reporting a value we cannot stand behind (I-8).
`make check` fails if /version disagrees with `git rev-parse HEAD` in CI.
"""
from __future__ import annotations
import logging
import os
import subprocess
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
log = logging.getLogger(__name__)
# Rewritten at image build time by the Dockerfile. Do not edit by hand, and do
# not "helpfully" default it to something plausible.
BAKED_COMMIT_SHA: str = ""
BAKED_BUILT_AT: str = ""
VERSION = "0.1.0"
@dataclass(frozen=True)
class BuildInfo:
version: str
commit_sha: str | None
built_at: str | None
source: str # "baked" | "git-worktree" | "unknown"
def _git_head() -> str | None:
try:
root = Path(__file__).resolve().parents[2]
out = subprocess.run(
["git", "-C", str(root), "rev-parse", "HEAD"],
capture_output=True,
text=True,
timeout=5,
check=False,
)
return out.stdout.strip() or None if out.returncode == 0 else None
except Exception: # pragma: no cover - defensive
return None
@lru_cache
def get_build_info() -> BuildInfo:
override = os.environ.get("COMMIT_SHA")
if BAKED_COMMIT_SHA:
if override and override != BAKED_COMMIT_SHA:
log.warning(
"IGNORING COMMIT_SHA environment override (%s). This service "
"reports the sha baked into its own artifact (%s). See I-12 — an "
"env pin overriding the build arg is exactly why nine sibling "
"services misreport their commit.",
override[:12],
BAKED_COMMIT_SHA[:12],
)
return BuildInfo(VERSION, BAKED_COMMIT_SHA, BAKED_BUILT_AT or None, "baked")
head = _git_head()
if head:
if override:
log.warning(
"IGNORING COMMIT_SHA environment override (%s); reading the "
"worktree instead.",
override[:12],
)
return BuildInfo(VERSION, head, None, "git-worktree")
# Nothing baked, no git. Say so. Do not invent a sha (I-8).
return BuildInfo(VERSION, None, None, "unknown")

119
api/app/config.py Normal file
View File

@@ -0,0 +1,119 @@
"""Settings for the windy-git plane.
Every `env:` default in DNA_STRAND_MASTER_PLAN.md is shipped here as an actual
default, not a suggestion. Providers are FAIL-CLOSED (I-8): a provider whose
credentials are absent reports itself unconfigured and refuses to answer, rather
than answering from a mock. The domains cell shipped a portal on a mock registrar
and told the public that google.com was available for $18.00 a year. That failure
mode is banned here by construction.
"""
from __future__ import annotations
from functools import lru_cache
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env", env_file_encoding="utf-8", extra="ignore"
)
# ---- service identity -------------------------------------------------
environment: str = "development"
service_name: str = "windy-git"
port: int = 8600
# ---- database ---------------------------------------------------------
# Postgres schema `windgit`, own alembic (G0.4).
database_url: str = "postgresql+asyncpg://windygit:windygit@localhost:5432/windygit"
db_schema: str = "windgit"
# ---- Gitea (a COMPONENT behind an API membrane, never a merged tree) ---
gitea_base_url: str = "http://localhost:3000"
gitea_admin_token: str = ""
# ---- Cloudflare R2 (I-3: heavy bytes only, never git objects) ---------
r2_account_id: str = ""
r2_access_key_id: str = ""
r2_secret_access_key: str = ""
r2_bucket_lfs: str = "windy-git-lfs"
r2_bucket_artifacts: str = "windy-git-artifacts"
r2_bucket_backups: str = "windy-git-backups"
# ---- Eternitas (agent identity + trust) -------------------------------
eternitas_base_url: str = "https://api.eternitas.ai"
eternitas_platform_api_key: str = ""
# ---- account-server OIDC (human identity) -----------------------------
account_server_base_url: str = "https://account.windyword.ai"
# ---- storage law (I-3, G4.4) ------------------------------------------
# Git object databases MUST live on a POSIX filesystem. A test asserts this
# path does not resolve to a network mount.
git_data_root: str = "/srv/windygit/git"
# ---- LFS threshold (G4.5) ---------------------------------------------
# Small text files stay in git proper. LFS-for-everything makes clones slow
# and operations heavy.
lfs_threshold_bytes: int = 5 * 1024 * 1024 # env: 5 MB
lfs_extensions: tuple[str, ...] = (
".safetensors", ".bin", ".gguf", ".pt", ".ckpt", ".onnx",
".zip", ".tar", ".gz", ".mp4", ".wav", ".mov", ".psd",
)
# ---- velocity bases, multiplied by EI band (G3.4) ---------------------
# Platinum x10 / Gold x4 / Standard x1 / Watch x0.5 / Untrusted read-only
rate_pushes_per_day: int = 500
rate_repo_creates_per_day: int = 50
rate_grants_per_day: int = 100
rate_force_pushes_per_day: int = 10
# ---- mirror health (I-4) ----------------------------------------------
mirror_lag_p2_seconds: int = 3600 # env: 60 min -> P2
# ---- agent grants (G5.3) ----------------------------------------------
agent_grant_default_days: int = 90
# ---- repo types (I-7: first-class from migration 001) -----------------
repo_types_enabled: tuple[str, ...] = ("code",) # model/dataset are v2
# ---- feature gates ----------------------------------------------------
# Mirrors the sibling `edge_live` pattern (windy-cloud-sites, a8ff948):
# never claim live while a provider is mock.
hf_compat_enabled: bool = False # Grant-gated, DNA plan section 7.7
kit0_host: str = Field(
default="72.60.118.54",
description="Recorded ONLY so the G1 guard can refuse to deploy here (D-4).",
)
# ---- derived ----------------------------------------------------------
@property
def r2_configured(self) -> bool:
return bool(
self.r2_account_id and self.r2_access_key_id and self.r2_secret_access_key
)
@property
def gitea_configured(self) -> bool:
return bool(self.gitea_base_url and self.gitea_admin_token)
@property
def eternitas_configured(self) -> bool:
return bool(self.eternitas_base_url and self.eternitas_platform_api_key)
@property
def r2_endpoint_url(self) -> str:
return f"https://{self.r2_account_id}.r2.cloudflarestorage.com"
@property
def is_production(self) -> bool:
return self.environment.lower() in {"production", "prod"}
@lru_cache
def get_settings() -> Settings:
return Settings()

108
api/app/errors.py Normal file
View File

@@ -0,0 +1,108 @@
"""Repair-pointer error taxonomy (section 0.6, G8.3).
EVERY error this service emits is a 4-field repair pointer:
{code, speak, machine_cause, remediation_tool}
No exceptions, including validation errors. `speak` is grandma-words (I-9) and
obeys the D-9 vocabulary law (see scripts/vocab_audit.py), and never uses the
word "commit" on a shelter surface.
`remediation_tool` names the tool an agent should call next, or null when a human
must act.
"""
from __future__ import annotations
from typing import Any
from fastapi import HTTPException
class RepairPointer(HTTPException):
"""An error an agent can act on without guessing."""
def __init__(
self,
status_code: int,
code: str,
speak: str,
machine_cause: str,
remediation_tool: str | None = None,
**extra: Any,
) -> None:
self.code = code
self.speak = speak
self.machine_cause = machine_cause
self.remediation_tool = remediation_tool
super().__init__(
status_code=status_code,
detail={
"code": code,
"speak": speak,
"machine_cause": machine_cause,
"remediation_tool": remediation_tool,
**extra,
},
)
def provider_unconfigured(provider: str, missing: str) -> RepairPointer:
"""I-8: fail closed. Never answer from a mock, never claim live.
This is the guard the domains cell did not have on its public quote route.
"""
return RepairPointer(
status_code=503,
code="provider_unconfigured",
speak=(
"That part of Windy Git isn't switched on yet, so we're not going to "
"guess at an answer. Nothing you have is affected."
),
machine_cause=f"{provider} is not configured: {missing} is unset",
remediation_tool=None,
provider=provider,
)
def passport_unresolvable(passport: str, upstream_status: int) -> RepairPointer:
"""G3.6: 404 and 400 REFUSE. There is no soft-allow path here.
windy-chat maps 400/429 to "unreachable" and then soft-ALLOWS, which is a
live residual bypass because 429 is trivially inducible at 100/min/IP.
"""
return RepairPointer(
status_code=403,
code="passport_unresolvable",
speak="We couldn't confirm that helper's ID, so we didn't let it make changes.",
machine_cause=(
f"eternitas trust lookup for {passport} returned {upstream_status}; "
"policy is REFUSE on 400/404 and REFUSE after backoff on 429/5xx"
),
remediation_tool="windy_git.reissue_agent_token",
passport=passport,
)
def quota_exceeded(repo_id: str, used: int, limit: int) -> RepairPointer:
"""G4.6: we emit the cross-sell hook; the KERNEL owns the price (I-11)."""
return RepairPointer(
status_code=413,
code="quota_exceeded",
speak=(
"Your projects have outgrown the space on your plan. Nothing was lost — "
"the newest save just didn't go through."
),
machine_cause=f"repo {repo_id} would use {used} bytes against a limit of {limit}",
remediation_tool="windy_cloud.upgrade_storage",
repo_id=repo_id,
)
def kit_zero_refused(host: str) -> RuntimeError:
"""D-4 / section 7.8: only Grant may overturn a never."""
return RuntimeError(
f"REFUSING TO START: this service is pointed at Kit 0 ({host}). "
"Windy Git never runs on Kit 0 — CI executes untrusted code and Kit 0 "
"holds identity, the certificate authority, mail, Matrix and the broker. "
"See DNA_STRAND_MASTER_PLAN.md D-4."
)

132
api/app/main.py Normal file
View File

@@ -0,0 +1,132 @@
"""windy-git — the version, permission and provenance plane over Windy Cloud.
Strand G0. This process is OUR service. Gitea runs beside it as an unforked
component and is reached only over its REST API (D-2 / I-1).
"""
from __future__ import annotations
import logging
import socket
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from sqlalchemy.ext.asyncio import create_async_engine
from api.app.buildinfo import get_build_info
from api.app.config import get_settings
from api.app.errors import RepairPointer, kit_zero_refused
from api.app.providers.registry import (
DatabaseProvider,
EternitasProvider,
GiteaProvider,
R2Provider,
TunnelProvider,
)
from api.app.routes import health
logging.basicConfig(
level=logging.INFO,
format='{"ts":"%(asctime)s","level":"%(levelname)s","logger":"%(name)s","msg":"%(message)s"}',
)
log = logging.getLogger("windy-git")
def _refuse_kit_zero(settings) -> None:
"""D-4 / section 7.8 — only Grant may overturn a never.
Kit 0 is disqualified on four independent grounds, any one sufficient. The
strongest: CI executes arbitrary workflow code, and Kit 0 holds identity, the
certificate authority, inbound SMTP, Matrix, the broker and the admin console.
A guard in a document is a preference; a guard in the boot path is a rule.
"""
if not settings.is_production:
return
try:
local_ips = {
info[4][0] for info in socket.getaddrinfo(socket.gethostname(), None)
}
except socket.gaierror:
return
if settings.kit0_host in local_ips:
raise kit_zero_refused(settings.kit0_host)
@asynccontextmanager
async def lifespan(app: FastAPI):
settings = get_settings()
_refuse_kit_zero(settings)
info = get_build_info()
log.info(
"starting windy-git %s commit=%s source=%s env=%s",
info.version,
(info.commit_sha or "unknown")[:12],
info.source,
settings.environment,
)
if info.source == "unknown":
log.warning(
"This process cannot name its own commit. It will report null rather "
"than guess (I-12), but a production deploy in this state is a defect."
)
engine = None
try:
engine = create_async_engine(settings.database_url, pool_pre_ping=True)
except Exception as exc: # noqa: BLE001
log.warning("database engine not created: %s", exc)
app.state.settings = settings
app.state.engine = engine
app.state.providers = [
DatabaseProvider(engine),
GiteaProvider(settings),
R2Provider(settings),
EternitasProvider(settings),
TunnelProvider(settings),
]
yield
if engine is not None:
await engine.dispose()
app = FastAPI(
title="Windy Git",
description=(
"The version, permission and provenance plane over Windy Cloud. "
"Agents are citizens here, not tourists wearing a human's token."
),
version=get_build_info().version,
lifespan=lifespan,
)
app.include_router(health.router)
@app.exception_handler(RepairPointer)
async def _repair_pointer_handler(_, exc: RepairPointer) -> JSONResponse:
return JSONResponse(status_code=exc.status_code, content=exc.detail)
@app.exception_handler(RequestValidationError)
async def _validation_handler(_, exc: RequestValidationError) -> JSONResponse:
"""G8.3: EVERY error is a repair pointer. Including validation errors.
FastAPI's default 422 body is machine-readable and human-hostile. It is also
the single most common error an agent will hit, so it is the last place to
drop the contract.
"""
return JSONResponse(
status_code=422,
content={
"code": "invalid_request",
"speak": "Something in that request didn't look right, so we didn't act on it.",
"machine_cause": f"request validation failed: {exc.errors()}",
"remediation_tool": None,
},
)

View File

323
api/app/models/core.py Normal file
View File

@@ -0,0 +1,323 @@
"""Data model, section 4 of the DNA plan.
I-7 is enforced here structurally: `repo_type` is NOT NULL from migration 001,
and `model_cards` exists in v1 even though `repo_type=model` does not ship until
v2. Shipping v1 with the v2 columns absent is forbidden — cheap now,
near-impossible to retrofit.
Postgres is truth. Gitea's own database is a component's private state and this
service never writes to it directly (I-1).
"""
from __future__ import annotations
import enum
import uuid
from datetime import datetime
from sqlalchemy import (
ARRAY,
BigInteger,
Boolean,
CheckConstraint,
DateTime,
Enum,
ForeignKey,
Index,
Integer,
String,
Text,
UniqueConstraint,
func,
)
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
SCHEMA = "windgit"
class Base(DeclarativeBase):
pass
class RepoType(enum.StrEnum):
"""I-7 / D-6. A Hugging Face repo IS a git repo with LFS and a model card —
the consolidation is metadata and UI, not infrastructure."""
code = "code"
model = "model"
dataset = "dataset"
class Visibility(enum.StrEnum):
private = "private"
unlisted = "unlisted"
public = "public"
class RepoState(enum.StrEnum):
active = "active"
archived = "archived"
deleted_soft = "deleted-soft"
class CreatedVia(enum.StrEnum):
portal = "portal"
agent = "agent"
imported = "import"
cloud_folder = "cloud-folder"
class GrantRole(enum.StrEnum):
owner = "owner"
maintainer = "maintainer"
writer = "writer"
reader = "reader"
class MirrorState(enum.StrEnum):
healthy = "healthy"
degraded = "degraded"
failed = "failed"
def _pg_enum(enum_cls, name: str):
"""SQLAlchemy's Enum persists `.name` by default, not `.value`.
Three members here have a name that differs from its value
(`deleted_soft`/"deleted-soft", `imported`/"import", `cloud_folder`/
"cloud-folder"), so the default would write a label migration 001 does not
declare and the insert would fail at runtime, not at review. Pin values
explicitly.
"""
return Enum(
enum_cls,
name=name,
schema=SCHEMA,
values_callable=lambda cls: [member.value for member in cls],
)
def _pk() -> Mapped[uuid.UUID]:
return mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
class Repo(Base):
__tablename__ = "repos"
__table_args__ = (
UniqueConstraint("identity_id", "slug", name="uq_repos_identity_slug"),
Index("ix_repos_repo_type", "repo_type"),
Index("ix_repos_passport", "passport"),
{"schema": SCHEMA},
)
id: Mapped[uuid.UUID] = _pk()
identity_id: Mapped[str] = mapped_column(String(64), nullable=False)
# Agent-owned repos. An agent is a citizen here, not a guest on a human's row.
passport: Mapped[str | None] = mapped_column(String(32))
slug: Mapped[str] = mapped_column(String(128), nullable=False)
display_name: Mapped[str] = mapped_column(String(255), nullable=False)
# I-7: first-class, NOT NULL, never inferred, never defaulted at read time.
repo_type: Mapped[RepoType] = mapped_column(
_pg_enum(RepoType, "repo_type"), nullable=False
)
gitea_repo_id: Mapped[int | None] = mapped_column(Integer)
visibility: Mapped[Visibility] = mapped_column(
_pg_enum(Visibility, "visibility"),
nullable=False,
default=Visibility.private,
)
# D-8: set when this repo was git-enabled from a Windy Cloud folder. The
# user's files stay first-class Cloud objects; we never take custody (I-13).
cloud_folder_ref: Mapped[str | None] = mapped_column(String(255))
default_branch: Mapped[str] = mapped_column(String(128), default="main")
lfs_bytes: Mapped[int] = mapped_column(BigInteger, default=0)
object_bytes: Mapped[int] = mapped_column(BigInteger, default=0)
state: Mapped[RepoState] = mapped_column(
_pg_enum(RepoState, "repo_state"), default=RepoState.active
)
created_via: Mapped[CreatedVia] = mapped_column(
_pg_enum(CreatedVia, "created_via"), nullable=False
)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class RepoGrant(Base):
"""The shelter (D-8/G5.3). Windy Cloud has no sharing of any kind today —
verified 2026-08-11 against routes/storage.py and its models."""
__tablename__ = "repo_grants"
__table_args__ = (
# Exactly one grantee. Enforced by the database, not by application code,
# because application-enforced invariants are how this ecosystem got a
# double-mint and a unique constraint living in two files.
CheckConstraint(
"(grantee_identity_id IS NULL) <> (grantee_passport IS NULL)",
name="ck_grant_exactly_one_grantee",
),
Index("ix_grants_repo", "repo_id"),
{"schema": SCHEMA},
)
id: Mapped[uuid.UUID] = _pk()
repo_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey(f"{SCHEMA}.repos.id", ondelete="CASCADE"), nullable=False
)
grantee_identity_id: Mapped[str | None] = mapped_column(String(64))
grantee_passport: Mapped[str | None] = mapped_column(String(32))
role: Mapped[GrantRole] = mapped_column(
_pg_enum(GrantRole, "grant_role"), nullable=False
)
granted_by: Mapped[str] = mapped_column(String(64), nullable=False)
# Agent grants expire by default (env: 90 days). A permanent agent credential
# is a standing liability nobody chose.
expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
confirm_ref: Mapped[str | None] = mapped_column(String(128))
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class RepoVersion(Base):
"""Our own view of history, independent of Gitea (I-1)."""
__tablename__ = "repo_versions"
__table_args__ = (
UniqueConstraint("repo_id", "seq", name="uq_version_repo_seq"),
Index("ix_versions_commit", "commit_sha"),
{"schema": SCHEMA},
)
id: Mapped[uuid.UUID] = _pk()
repo_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey(f"{SCHEMA}.repos.id", ondelete="CASCADE"), nullable=False
)
seq: Mapped[int] = mapped_column(Integer, nullable=False)
commit_sha: Mapped[str] = mapped_column(String(64), nullable=False)
tree_sha: Mapped[str | None] = mapped_column(String(64))
author_identity_id: Mapped[str | None] = mapped_column(String(64))
author_passport: Mapped[str | None] = mapped_column(String(32))
signed: Mapped[bool] = mapped_column(Boolean, default=False)
signature_verified: Mapped[bool] = mapped_column(Boolean, default=False)
# G9.1: frozen at push time, NEVER recomputed. A band is a statement about
# what was known then, not a live lookup.
ei_at_action: Mapped[str | None] = mapped_column(String(32))
message: Mapped[str | None] = mapped_column(Text)
bytes_added: Mapped[int] = mapped_column(BigInteger, default=0)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class Mirror(Base):
"""I-4: never a one-way door. This table is what the alerting reads."""
__tablename__ = "mirror_state"
__table_args__ = ({"schema": SCHEMA},)
id: Mapped[uuid.UUID] = _pk()
repo_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey(f"{SCHEMA}.repos.id", ondelete="CASCADE"), nullable=False
)
remote_url: Mapped[str] = mapped_column(String(512), nullable=False)
direction: Mapped[str] = mapped_column(String(16), default="push")
last_success_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
last_error: Mapped[str | None] = mapped_column(Text)
lag_seconds: Mapped[int | None] = mapped_column(Integer)
state: Mapped[MirrorState] = mapped_column(
_pg_enum(MirrorState, "mirror_health"), default=MirrorState.healthy
)
class AgentToken(Base):
"""G6.3. Never a human's PAT wearing an agent's name — that is the entire
GitHub grievance and the reason this product exists."""
__tablename__ = "agent_tokens"
__table_args__ = (
Index("ix_agent_tokens_passport", "passport"),
{"schema": SCHEMA},
)
id: Mapped[uuid.UUID] = _pk()
passport: Mapped[str] = mapped_column(String(32), nullable=False)
repo_id: Mapped[uuid.UUID | None] = mapped_column(
ForeignKey(f"{SCHEMA}.repos.id", ondelete="CASCADE")
)
scopes: Mapped[list[str]] = mapped_column(ARRAY(String(64)), nullable=False)
# We store a hash. Never the token.
token_hash: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
revoked_reason: Mapped[str | None] = mapped_column(String(255))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class AgentAction(Base):
__tablename__ = "agent_actions"
__table_args__ = (
Index("ix_agent_actions_passport_ts", "passport", "ts"),
{"schema": SCHEMA},
)
id: Mapped[uuid.UUID] = _pk()
passport: Mapped[str] = mapped_column(String(32), nullable=False)
repo_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True))
action: Mapped[str] = mapped_column(String(64), nullable=False)
ei_at_action: Mapped[str | None] = mapped_column(String(32))
confirm_ref: Mapped[str | None] = mapped_column(String(128))
result: Mapped[str] = mapped_column(String(32), nullable=False)
# G3.7: actually populated. windy-chat emits nothing here and contributes $0
# to the cost dashboard despite real spend.
cost_microcents: Mapped[int] = mapped_column(BigInteger, default=0)
ts: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class ModelCard(Base):
"""v2 surface, v1 schema (I-7). Populated from README.md YAML frontmatter."""
__tablename__ = "model_cards"
__table_args__ = ({"schema": SCHEMA},)
id: Mapped[uuid.UUID] = _pk()
repo_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey(f"{SCHEMA}.repos.id", ondelete="CASCADE"), nullable=False, unique=True
)
base_model: Mapped[str | None] = mapped_column(String(255))
license: Mapped[str | None] = mapped_column(String(64))
pipeline_tag: Mapped[str | None] = mapped_column(String(64))
tags: Mapped[list[str] | None] = mapped_column(ARRAY(String(64)))
library: Mapped[str | None] = mapped_column(String(64))
card_yaml: Mapped[dict | None] = mapped_column(JSONB)
card_body: Mapped[str | None] = mapped_column(Text)
class Job(Base):
__tablename__ = "jobs"
__table_args__ = ({"schema": SCHEMA},)
id: Mapped[uuid.UUID] = _pk()
kind: Mapped[str] = mapped_column(String(64), nullable=False)
payload: Mapped[dict] = mapped_column(JSONB, nullable=False)
state: Mapped[str] = mapped_column(String(32), default="pending")
attempts: Mapped[int] = mapped_column(Integer, default=0)
last_error: Mapped[str | None] = mapped_column(Text)
run_after: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class WebhookEvent(Base):
__tablename__ = "webhook_events"
__table_args__ = ({"schema": SCHEMA},)
id: Mapped[uuid.UUID] = _pk()
event_type: Mapped[str] = mapped_column(String(64), nullable=False)
payload: Mapped[dict] = mapped_column(JSONB, nullable=False)
delivered: Mapped[bool] = mapped_column(Boolean, default=False)
attempts: Mapped[int] = mapped_column(Integer, default=0)
last_error: Mapped[str | None] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())

View File

55
api/app/providers/base.py Normal file
View File

@@ -0,0 +1,55 @@
"""Provider seams — all of them fail CLOSED (I-8).
`windy-cloud-sites` ships an `edge_live` gate whose whole job is "never claim
live while the provider is mock" (commit a8ff948). `windy-cloud-domains` has a
registrar seam that literally raises `RuntimeError("Refusing to pretend")` — and
then shipped its public portal without wiring the equivalent gate on the quote
route, which is why production told anyone who asked that google.com was
available for $18.00 a year.
The lesson those two cells paid for: a fail-closed seam is worth nothing if a
route can reach the data without passing through it. So here the probe and the
gate are the SAME object, and `healthy()` can never return True for a provider
that `configured` reports False.
"""
from __future__ import annotations
import abc
from dataclasses import dataclass
@dataclass(frozen=True)
class ProbeResult:
ok: bool
detail: str
# True only when we actually reached the real dependency. A provider that is
# merely "not configured" is ok=False, reachable=False — never ok=True.
reachable: bool = False
class Provider(abc.ABC):
"""A dependency outside this process."""
name: str
@property
@abc.abstractmethod
def configured(self) -> bool:
"""Do we hold every credential needed to talk to the real thing?"""
@abc.abstractmethod
async def probe(self) -> ProbeResult:
"""Reach the real dependency. Never simulate."""
async def healthy(self) -> ProbeResult:
if not self.configured:
return ProbeResult(
ok=False,
detail=f"{self.name} is not configured; refusing to report healthy",
reachable=False,
)
try:
return await self.probe()
except Exception as exc: # noqa: BLE001 - a probe must never raise upward
return ProbeResult(ok=False, detail=f"{self.name} probe failed: {exc}")

View File

@@ -0,0 +1,128 @@
"""Concrete providers: Gitea, R2, Eternitas, Postgres, tunnel.
Each is a real probe against the real dependency (I-8). None of them has a mock
mode. If you find yourself adding one, add it behind `configured` returning False
instead — an unconfigured provider is honest; a mock provider is a liar with a
green light.
"""
from __future__ import annotations
import httpx
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncEngine
from api.app.config import Settings
from api.app.providers.base import ProbeResult, Provider
_TIMEOUT = httpx.Timeout(5.0, connect=3.0)
class GiteaProvider(Provider):
"""Gitea is a COMPONENT behind an API membrane, never a merged tree (I-1)."""
name = "gitea"
def __init__(self, settings: Settings) -> None:
self._s = settings
@property
def configured(self) -> bool:
return self._s.gitea_configured
async def probe(self) -> ProbeResult:
async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
r = await client.get(
f"{self._s.gitea_base_url}/api/v1/version",
headers={"Authorization": f"token {self._s.gitea_admin_token}"},
)
if r.status_code != 200:
return ProbeResult(False, f"gitea /api/v1/version -> {r.status_code}", True)
return ProbeResult(True, f"gitea {r.json().get('version', '?')}", True)
class R2Provider(Provider):
"""I-3: LFS, releases, artifacts, archives. NEVER git object stores."""
name = "r2"
def __init__(self, settings: Settings) -> None:
self._s = settings
@property
def configured(self) -> bool:
return self._s.r2_configured
async def probe(self) -> ProbeResult:
# HEAD the bucket via the S3 endpoint. boto3 is sync, so we keep the
# probe to a plain reachability check here and let G4 wire the signed
# client; a 400/403 still proves the endpoint is real and answering.
async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
r = await client.head(f"{self._s.r2_endpoint_url}/{self._s.r2_bucket_lfs}")
reachable = r.status_code < 500
return ProbeResult(
ok=r.status_code in (200, 400, 403),
detail=f"r2 endpoint -> {r.status_code}",
reachable=reachable,
)
class EternitasProvider(Provider):
"""The one issuer. Every agent identity in the ecosystem terminates here."""
name = "eternitas"
def __init__(self, settings: Settings) -> None:
self._s = settings
@property
def configured(self) -> bool:
return self._s.eternitas_configured
async def probe(self) -> ProbeResult:
async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
r = await client.get(f"{self._s.eternitas_base_url}/health")
return ProbeResult(r.status_code == 200, f"eternitas /health -> {r.status_code}", True)
class DatabaseProvider(Provider):
"""Postgres is truth. Gitea's own DB is a component's private state."""
name = "db"
def __init__(self, engine: AsyncEngine | None) -> None:
self._engine = engine
@property
def configured(self) -> bool:
return self._engine is not None
async def probe(self) -> ProbeResult:
assert self._engine is not None
async with self._engine.connect() as conn:
await conn.execute(text("SELECT 1"))
return ProbeResult(True, "postgres reachable", True)
class TunnelProvider(Provider):
"""cloudflared is the only ingress. No inbound port is ever opened (G1.2)."""
name = "tunnel"
def __init__(self, settings: Settings) -> None:
self._s = settings
@property
def configured(self) -> bool:
# The tunnel is a host-level concern, not a credential we hold, so there
# is nothing to "configure" here. The probe alone decides health, and in
# dev it will honestly say cloudflared is not running (I-8).
return True
async def probe(self) -> ProbeResult:
async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
try:
r = await client.get("http://localhost:2000/metrics")
except httpx.RequestError as exc:
return ProbeResult(False, f"cloudflared metrics unreachable: {exc}")
return ProbeResult(r.status_code == 200, f"cloudflared metrics -> {r.status_code}", True)

View File

75
api/app/routes/health.py Normal file
View File

@@ -0,0 +1,75 @@
"""G0.2 / G0.3 — /version and /health/full.
The two endpoints this ecosystem most needs to be honest, because both audits
found them lying elsewhere: nine of twelve services cannot name their commit, and
a sibling cell reported healthy while serving from a mock.
`/health/full` returns `ok` ONLY when every provider it depends on proved itself
against the real dependency. Anything else is `degraded`. There is no code path
that returns `ok` from an unconfigured provider (I-8).
"""
from __future__ import annotations
from fastapi import APIRouter, Request, Response
from api.app.buildinfo import get_build_info
from api.app.config import get_settings
router = APIRouter(tags=["system"])
@router.get("/version")
async def version() -> dict:
"""I-12. A runtime COMMIT_SHA override is ignored; see buildinfo.py."""
info = get_build_info()
s = get_settings()
return {
"service": s.service_name,
"version": info.version,
"commit_sha": info.commit_sha,
"built_at": info.built_at,
"source": info.source,
"environment": s.environment,
"repo_types_enabled": list(s.repo_types_enabled),
}
@router.get("/health")
async def health() -> dict:
"""Cheap liveness. Says nothing about dependencies — /health/full does that."""
return {"status": "ok"}
@router.get("/health/full")
async def health_full(request: Request, response: Response) -> dict:
providers = request.app.state.providers
checks: dict[str, dict] = {}
for provider in providers:
result = await provider.healthy()
checks[provider.name] = {
"ok": result.ok,
"configured": provider.configured,
"reachable": result.reachable,
"detail": result.detail,
}
all_ok = all(c["ok"] for c in checks.values())
status = "ok" if all_ok else "degraded"
if not all_ok:
# Degraded is a real answer, not a 500. But it must never read as ok.
response.status_code = 503
info = get_build_info()
return {
"status": status,
"commit_sha": info.commit_sha,
"checks": checks,
# Grandma-words, and the D-9 vocabulary law binds this string.
"speak": (
"Everything is working."
if all_ok
else "Some parts of Windy Git aren't switched on. Nothing you have is lost."
),
}

View File

0
api/tests/__init__.py Normal file
View File

View File

@@ -0,0 +1,236 @@
"""Invariant guards.
DNA plan section 8: "Every invariant I-1..I-13 has a named test. A test file
header cites the invariant it defends." This file defends I-1, I-3, I-7, I-8,
I-12 and D-9.
These are not unit tests of convenience. Each one encodes a failure that already
happened somewhere in this ecosystem and cost real time.
"""
from __future__ import annotations
import re
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
# --------------------------------------------------------------------------
# I-12 — deployment identity must be honest
# --------------------------------------------------------------------------
def test_i12_env_override_cannot_change_reported_sha(monkeypatch):
"""Nine of twelve sibling services misreport their commit; the root cause was
a hardcoded COMMIT_SHA pin in /opt/*/.env overriding the build arg."""
from api.app import buildinfo
monkeypatch.setattr(buildinfo, "BAKED_COMMIT_SHA", "a" * 40)
monkeypatch.setattr(buildinfo, "BAKED_BUILT_AT", "2026-08-11T00:00:00Z")
monkeypatch.setenv("COMMIT_SHA", "b" * 40)
buildinfo.get_build_info.cache_clear()
info = buildinfo.get_build_info()
assert info.commit_sha == "a" * 40, "env override must be IGNORED (I-12)"
assert info.source == "baked"
buildinfo.get_build_info.cache_clear()
def test_i12_never_invents_a_sha(monkeypatch):
"""With nothing baked and no git, report null. Never guess (I-8)."""
from api.app import buildinfo
monkeypatch.setattr(buildinfo, "BAKED_COMMIT_SHA", "")
monkeypatch.setattr(buildinfo, "_git_head", lambda: None)
buildinfo.get_build_info.cache_clear()
info = buildinfo.get_build_info()
assert info.commit_sha is None
assert info.source == "unknown"
buildinfo.get_build_info.cache_clear()
# --------------------------------------------------------------------------
# I-8 — fail closed. Never claim live while a provider is mock.
# --------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_i08_unconfigured_provider_is_never_ok():
"""The domains cell told the public google.com was available for $18 because
a public route reached a mock provider without passing a gate."""
from api.app.providers.base import ProbeResult, Provider
class Unconfigured(Provider):
name = "test"
@property
def configured(self) -> bool:
return False
async def probe(self) -> ProbeResult: # pragma: no cover - must not run
raise AssertionError("probe() must never be reached when unconfigured")
result = await Unconfigured().healthy()
assert result.ok is False
assert result.reachable is False
@pytest.mark.asyncio
async def test_i08_probe_exception_is_not_ok():
from api.app.providers.base import ProbeResult, Provider
class Exploding(Provider):
name = "test"
@property
def configured(self) -> bool:
return True
async def probe(self) -> ProbeResult:
raise RuntimeError("upstream on fire")
result = await Exploding().healthy()
assert result.ok is False
assert "on fire" in result.detail
def test_i08_no_provider_has_a_mock_mode():
"""An unconfigured provider is honest. A mock provider is a liar with a
green light."""
src = (ROOT / "api" / "app" / "providers" / "registry.py").read_text()
for banned in ("MockProvider", "class Mock", "if mock", "USE_MOCK"):
assert banned not in src, f"{banned!r} found — see I-8"
# --------------------------------------------------------------------------
# I-7 — repo_type is first-class from migration 001
# --------------------------------------------------------------------------
def test_i07_repo_type_not_null_in_genesis_migration():
src = (ROOT / "alembic" / "versions" / "001_genesis.py").read_text()
assert 'sa.Column("repo_type", repo_type, nullable=False)' in src
def test_i07_model_cards_exists_in_v1_schema():
"""v2 surface, v1 schema. Cheap now, near-impossible to retrofit."""
src = (ROOT / "alembic" / "versions" / "001_genesis.py").read_text()
assert '"model_cards"' in src
def test_i07_all_three_repo_types_declared():
from api.app.models.core import RepoType
assert {t.value for t in RepoType} == {"code", "model", "dataset"}
# --------------------------------------------------------------------------
# I-1 — Gitea is a component, never a merged tree
# --------------------------------------------------------------------------
def test_i01_patch_ceiling():
"""D-2: a hard fork means owning merge conflicts forever against a project
that ships every 2-3 months including security fixes."""
patches = ROOT / "patches"
diffs = [p for p in patches.glob("*") if p.suffix in {".patch", ".diff"}]
assert len(diffs) <= 3, (
f"{len(diffs)} Gitea patches — the I-1 ceiling is 3 without an ADR. "
"Reach for the REST API before reaching for the source tree."
)
# --------------------------------------------------------------------------
# I-3 — git objects local, heavy bytes remote. Never the reverse.
# --------------------------------------------------------------------------
def test_i03_git_root_is_not_object_storage():
from api.app.config import Settings
root = Settings().git_data_root
for banned in ("s3://", "r2://", "https://", "gs://"):
assert not root.startswith(banned), (
"git object databases must live on a POSIX filesystem — a clone "
"touches thousands of small objects (I-3)"
)
# --------------------------------------------------------------------------
# D-4 — never Kit 0. A guard in a document is a preference.
# --------------------------------------------------------------------------
def test_d04_boot_guard_exists():
src = (ROOT / "api" / "app" / "main.py").read_text()
assert "_refuse_kit_zero" in src
assert "kit_zero_refused" in src
# --------------------------------------------------------------------------
# D-9 — vocabulary law
# --------------------------------------------------------------------------
def test_d09_vocabulary_audit_passes():
result = subprocess.run(
[sys.executable, str(ROOT / "scripts" / "vocab_audit.py")],
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stdout
# --------------------------------------------------------------------------
# G8.3 — every error is a 4-field repair pointer
# --------------------------------------------------------------------------
def test_g83_errors_carry_all_four_fields():
from api.app.errors import provider_unconfigured
err = provider_unconfigured("r2", "R2_ACCESS_KEY_ID")
for field in ("code", "speak", "machine_cause", "remediation_tool"):
assert field in err.detail
def test_g83_speak_strings_are_grandma_words():
"""I-9: hardest on failure. No stack traces, no jargon, and it names the fix."""
from api.app.errors import provider_unconfigured, quota_exceeded
for err in (provider_unconfigured("r2", "KEY"), quota_exceeded("r", 2, 1)):
speak = err.detail["speak"]
assert speak[0].isupper() and speak.endswith(".")
for jargon in ("null", "None", "500", "traceback", "exception", "repo_id"):
assert jargon not in speak
# --------------------------------------------------------------------------
# G3.7 — telemetry actor_type enum
# --------------------------------------------------------------------------
def test_g37_service_is_not_a_legal_actor_type():
"""windy-chat sends actor_type='service'; the ingest Literal allows only
human|agent|system, so the whole batch 422s and is dropped with one warn."""
legal = {"human", "agent", "system"}
assert "service" not in legal
# --------------------------------------------------------------------------
# G7.5 — runner labels are pinned
# --------------------------------------------------------------------------
def test_g75_no_ubuntu_latest_in_workflows():
"""All four windy-registry workflows use ubuntu-latest and every run fails."""
for wf in ROOT.rglob(".github/workflows/*.y*ml"):
assert "ubuntu-latest" not in wf.read_text(), f"{wf}: see G7.5"
# --------------------------------------------------------------------------
# G0.5 — compose files are committed, secrets stripped
# --------------------------------------------------------------------------
def test_g05_no_secret_literals_committed():
"""Three compose files that wire prod to its databases currently exist in
exactly one place on earth. This cell will not add a fourth."""
patterns = [
re.compile(r"cfat_[A-Za-z0-9]{20,}"),
re.compile(r"cfut_[A-Za-z0-9]{20,}"),
re.compile(r"gh[pousr]_[A-Za-z0-9]{30,}"),
re.compile(r"et_plt_[A-Za-z0-9]{10,}"),
]
for path in ROOT.rglob("*"):
if not path.is_file() or ".git/" in path.as_posix():
continue
if path.suffix not in {".py", ".yml", ".yaml", ".toml", ".ini", ".md", ".env", ".example"}:
continue
text = path.read_text(encoding="utf-8", errors="ignore")
for pattern in patterns:
assert not pattern.search(text), f"credential literal in {path}"

60
docker-compose.yml Normal file
View File

@@ -0,0 +1,60 @@
# G0.5 — committed, secrets stripped.
#
# Three compose files that wire production to its databases currently exist in
# exactly one place on earth (SOTU section 5.6.3): windy-pro's postgres override,
# Mind's WireGuard override, and WindyCloud's kit0 override — the last of which
# had to be reconstructed after an `rsync --delete` ate it once. This cell will
# not add a fourth. Real values come from .env, which is gitignored.
name: windy-git
services:
api:
build:
context: .
args:
# I-12: baked at build time. A runtime COMMIT_SHA override is ignored.
COMMIT_SHA: ${COMMIT_SHA_BUILD:-}
BUILT_AT: ${BUILT_AT:-}
env_file: [.env]
environment:
DATABASE_URL: postgresql+asyncpg://windygit:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@db:5432/windygit
GITEA_BASE_URL: http://gitea:3000
ports: ["8600:8600"]
depends_on: {db: {condition: service_healthy}}
restart: unless-stopped
gitea:
# G2.1 — PIN AN EXACT VERSION. Never `latest`. Record it in SUBSTRATE.md.
image: docker.io/gitea/gitea:1.24.6
environment:
GITEA__database__DB_TYPE: postgres
GITEA__database__HOST: db:5432
GITEA__database__NAME: gitea
GITEA__database__USER: gitea
GITEA__database__PASSWD: ${GITEA_DB_PASSWORD:?set GITEA_DB_PASSWORD}
GITEA__repository__DEFAULT_BRANCH: main
GITEA__server__ROOT_URL: ${GITEA_ROOT_URL:-http://localhost:3000/}
# G2.2 — OIDC only. No local password login, no self-registration.
GITEA__service__DISABLE_REGISTRATION: "true"
GITEA__service__ALLOW_ONLY_EXTERNAL_REGISTRATION: "true"
GITEA__lfs__PATH: /data/lfs
volumes:
# I-3: git object databases on a POSIX filesystem. Never object storage.
- ${GIT_DATA_ROOT:-./data/gitea}:/data
ports: ["3000:3000"]
depends_on: {db: {condition: service_healthy}}
restart: unless-stopped
db:
image: docker.io/library/postgres:16-alpine
environment:
POSTGRES_USER: windygit
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
POSTGRES_DB: windygit
volumes: ["./data/pg:/var/lib/postgresql/data"]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U windygit"]
interval: 5s
retries: 10
restart: unless-stopped

45
docs/MEMBRANE.v1.md Normal file
View File

@@ -0,0 +1,45 @@
# MEMBRANE.v1 — windy-git
**This file mirrors invariant I-2 and is the complete surface.** Adding a call
means editing I-2 in `DNA_STRAND_MASTER_PLAN.md` **first**, then this file, then
the code. Not the other way round.
Mirrored into `windy-cloud` and `eternitas` on change.
## Calls OUT
| Target | Route | Why |
|---|---|---|
| windy-cloud kernel | `GET /api/v1/storage/objects`, `HEAD` | read user objects in order to version them (D-8) |
| windy-cloud kernel | `POST /api/v1/storage/quota/check` | G4.6 — we ask, the kernel decides and owns the price (I-11) |
| eternitas | `GET /api/v1/trust/{passport}` | band + allowed_actions |
| eternitas | `GET /api/v1/registry/{passport}/integrity` | ⚠️ note the path — `windy-registry` calls `/api/v1/passports/{p}/status`, which 404s, which is why the integrity index has never been populated |
| account-server | OIDC discovery + JWKS | human identity (G3.1) |
| windy-cloud-sites | `POST /api/v1/sites/{id}/versions` | publish docs from a repo |
## Calls IN
| Route | Caller | Why |
|---|---|---|
| `POST /internal/repo-from-folder` | Cloud portal | git-enable a Windy Cloud folder (G5.1) |
| `POST /internal/mirror-status` | ops | I-4 mirror health |
## Events OUT
`repo.created` · `repo.pushed` · `release.published` · `model.published` · `ci.completed`
## Events IN
`passport.revoked` (**fail-closed**, G3.5) · `storage.quota.exceeded` · `identity.created`
## Webhook contract
⚠️ Four consumers in this ecosystem currently disagree in four ways on the
webhook contract, and two integrations have **never once delivered successfully**
— account-server sends `X-Windy-Signature` while Windy-Clone requires
`X-Windy-Pro-Signature` plus a timestamp header the producer never sends at all,
and the payload field names differ too.
**This cell adopts the `windy-contracts` shape and does not invent a fifth.**
One header name, one timestamp header, one payload field name, HMAC both
directions, and a producer→consumer conformance test in the shared suite.

10
patches/README.md Normal file
View File

@@ -0,0 +1,10 @@
# patches/ — Gitea source patches (I-1)
EMPTY, and it should stay that way.
Windy Git runs STOCK Gitea and builds beside it against its REST API (D-2).
Any patch here must be a numbered, rebasable diff with a one-line justification.
**`make check` fails if this directory holds more than 3 patches without an ADR
naming the decision it overturns.** A hard fork requires a written list of the
specific files that must change and why an API cannot reach them.

42
pyproject.toml Normal file
View File

@@ -0,0 +1,42 @@
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
include = ["api*"]
[project]
name = "windy-git"
version = "0.1.0"
description = "Windy Git — the version, permission and provenance plane over Windy Cloud."
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115",
"uvicorn[standard]>=0.32",
"pydantic>=2.9",
"pydantic-settings>=2.6",
"sqlalchemy[asyncio]>=2.0",
"asyncpg>=0.30",
"alembic>=1.14",
"httpx>=0.27",
"boto3>=1.35",
]
[project.optional-dependencies]
dev = ["pytest>=8.3", "pytest-asyncio>=0.24", "ruff>=0.7", "mypy>=1.13"]
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM"]
ignore = ["E501"]
[tool.mypy]
python_version = "3.12"
ignore_missing_imports = true
[tool.pytest.ini_options]
testpaths = ["api/tests"]
asyncio_mode = "auto"

90
scripts/vocab_audit.py Executable file
View File

@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""D-9 vocabulary audit (codon G12.6).
"Git" is never a countable noun. There is no such thing as "a Git". Git is the
program; a moment in time is a *commit*; user-facing it is a *version* or a
*save point*; the container is a *repo*.
This is not pedantry. It is the tell that separates people who use git from
people who have read about it, and it will cost credibility with exactly the
developer audience this product is built for. There is a second edge: *git* is
British slang for a contemptible person -- which is why Torvalds chose it, self
-deprecatingly -- so "tracking your Gits" reads badly to a Commonwealth ear.
Doctrine files are exempt because they QUOTE the forbidden forms in order to
forbid them. Any other file may exempt a single line with a `vocab-ok` marker.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
# These files define the law and must quote what they prohibit.
EXEMPT_FILES = {
"DNA_STRAND_MASTER_PLAN.md",
"README.md",
"AGENTS.md",
"scripts/vocab_audit.py",
"api/tests/test_d09_vocabulary.py",
}
SKIP_DIRS = {".git", ".venv", "venv", "__pycache__", ".ruff_cache", ".mypy_cache",
".pytest_cache", "node_modules", "LICENSES", "patches"}
SCAN_SUFFIXES = {".py", ".md", ".html", ".js", ".ts", ".jsx", ".tsx", ".json",
".yaml", ".yml", ".toml", ".txt", ".ini"}
# "a Git" / "an Git" / "the Git" as a countable thing, plus the plural.
# Negative lookahead keeps GitHub, Gitea, GitLab, Gitpod and "Git " as a proper
# noun in phrases like "Windy Git is".
PATTERNS = [
(re.compile(r"\b(?:a|an|one|each|every|another)\s+Git\b(?!Hub|ea|Lab|pod|Kraken)"),
'countable "a Git"'),
(re.compile(r"\bGits\b(?!Hub|ea)"), 'plural "Gits"'),
(re.compile(r"\b(?:my|your|their|his|her|its)\s+Git\b(?!Hub|ea|Lab|pod)"),
'possessive "your Git"'),
(re.compile(r"\bgits\b"), 'lowercase plural "gits"'),
]
def violations() -> list[tuple[str, int, str, str]]:
found: list[tuple[str, int, str, str]] = []
for path in ROOT.rglob("*"):
if not path.is_file() or path.suffix not in SCAN_SUFFIXES:
continue
if any(part in SKIP_DIRS for part in path.parts):
continue
rel = path.relative_to(ROOT).as_posix()
if rel in EXEMPT_FILES:
continue
try:
text = path.read_text(encoding="utf-8")
except (UnicodeDecodeError, OSError):
continue
for lineno, line in enumerate(text.splitlines(), 1):
if "vocab-ok" in line:
continue
for pattern, label in PATTERNS:
if pattern.search(line):
found.append((rel, lineno, label, line.strip()[:100]))
return found
def main() -> int:
found = violations()
if not found:
print("vocab audit: clean (D-9)")
return 0
print("D-9 VOCABULARY LAW VIOLATION -- 'Git' is not a countable noun.\n")
for rel, lineno, label, line in found:
print(f" {rel}:{lineno} {label}\n {line}")
print("\nSay 'a commit' (developers) or 'a version' / 'a save point' (everyone else).")
return 1
if __name__ == "__main__":
sys.exit(main())