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
2 changes: 1 addition & 1 deletion .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Maßgebliche Dokumente: `docs/PRD.md`, `docs/SRS.md`, `.planning/ROADMAP.md`, `.
- `services/verifier`: Java 21, JDK-`HttpServer` (kein Javalin, um Klassenkonflikte mit dem KoSIT-Fat-Jar zu vermeiden), KoSIT Validator 1.6.3 + validator-configuration-xrechnung 2026-08-31 (im Dockerfile mit SHA-256 gepinnt), Build nur in Docker
- `eval/`: Korpus-Builder (Playwright für PDF-Rendering), Mutationen, Benchmark-Harness
- LLM: Nebius Token Factory, OpenAI-kompatibel, `https://api.tokenfactory.nebius.com/v1` (verifiziert 02.10.). Nemotron gibt es dort nur als Textmodell, kein Nano Omni. Preise gepinnt in `packages/core/src/llm/pricing.ts`. Tavily
- Tests: vitest (TS), JUnit (Java). Hosting: Koyeb (web + verifier, verifier nur intern)
- Tests: vitest (TS), JUnit (Java). Hosting: Google Cloud Run Free Tier, ein Container mit web + verifier (verifier nur auf localhost), `deploy/cloud-run.sh` (seit 04.10., vorher Koyeb geplant)

## Architektur
PDF → Rasterisierung + Textlayer → EXTRACT (Nano Omni, JSON mit Evidenz) → EVIDENCE_CHECK → NORMALIZE → DERIVE (Beträge, USt-Aufschlüsselung) → BUILD_CII → VALIDATE (KoSIT) → bei Fehlern REPAIR (Ultra, JSON Patch mit Evidenz) → max. 3 Iterationen → NEEDS_INPUT oder OUTPUT (XRechnung XML, ZUGFeRD PDF/A-3, Audit-Log). Details: `docs/SRS.md`.
Expand Down
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Build context for deploy/hf-space/Dockerfile. Never ship secrets or local state into an image.
# Build context for deploy/Dockerfile. Never ship secrets or local state into an image.
.env
.env.*
!.env.example
Expand Down
2 changes: 1 addition & 1 deletion .planning/REQUIREMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
- [ ] **OPS-02** *(02.10.: Hook + CI-Job angelegt, GitHub-Lauf und Lizenz-Erkennung stehen aus)*: gitleaks in CI und pre-commit; Apache-2.0-LICENSE im Repo-Root, auf GitHub im About-Bereich erkannt
- [x] **OPS-03**: Credits eingelöst (Event-Code, Builder Program, Tavily); Smoke-Test ruft je ein Modell für Extraktion und Reparatur über Token Factory auf
- [x] **OPS-04**: Zentraler LLM-Client zählt Tokens und Kosten je Aufruf und bricht bei Überschreitung von `DAILY_BUDGET_USD` ab
- [ ] **OPS-05** *(02.10.: `docker compose up verifier` läuft lokal; Koyeb offen, Verifier braucht ~570 MB RAM)*: Deployment `web` (öffentlich) und `verifier` (intern oder Secret-geschützt) auf Koyeb; `docker compose up` startet den Verifier lokal
- [x] **OPS-05** *(05.10.: live auf Google Cloud Run, https://verdict-678224659019.europe-west1.run.app — ein Container mit App + Verifier, Verifier nur auf localhost; `deploy/cloud-run.sh`. Koyeb verworfen (Kosten), Hugging Face Docker-Spaces nur mit PRO)*: Deployment `web` (öffentlich) und `verifier` (intern oder Secret-geschützt); `docker compose up` startet den Verifier lokal

### Verifier (VER)
- [x] **VER-01**: `POST /v1/validate/xrechnung` liefert für jede valide Testsuite-Datei `valid: true` und für mutierte Dateien Fehler mit Regel-ID, Meldung und Fundstelle
Expand Down
4 changes: 2 additions & 2 deletions .planning/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Fünf Phasen vom 17.09. bis 28.10.2026, ~50 h Gesamtaufwand. Die riskantesten Te
- Verifier: KoSIT 1.6.3 + Konfiguration 2026-08-31 in Docker, ohne lokales Java. `pnpm verifier:check`: 41/41 CII-Instanzen der Testsuite valide, 4/4 Mutationen mit erwarteter Regel-ID abgelehnt. JDK-`HttpServer` statt Javalin (keine Klassenkonflikte mit dem KoSIT-Fat-Jar).
- Befund: 2 Testsuite-Instanzen (04.05a, 02.01a-cvd) sind ACCEPTABLE trotz Codelisten-Meldungen der Stufe `error`. „Valid“ folgt deshalb ausschließlich der `acceptRecommendation`, nie der Fehlerzählung.
- Cut-Gate ZUGFeRD: ohne Spike verstrichen → ZUGFeRD aus Phase 1 genommen, in Phase 4 verankert (VER-06 + CORE-05, nach Kill-Gate).
- Offen in Phase 1: Koyeb-Deployment (OPS-05), erster CI-Lauf auf GitHub (OPS-01/02).
- Offen in Phase 1: Deployment (OPS-05, erledigt 05.10. auf Google Cloud Run), erster CI-Lauf auf GitHub (OPS-01/02).

### Phase 2: Korpus & Ground Truth
**Goal**: Es gibt einen reproduzierbaren, gemessenen Maßstab, bevor irgendein Modell Rechnungen verarbeitet.
Expand Down Expand Up @@ -98,7 +98,7 @@ Fünf Phasen vom 17.09. bis 28.10.2026, ~50 h Gesamtaufwand. Die riskantesten Te
- Next.js 16: `POST /api/run` (SSE, Limits, Rate-Limit, Tagesbudget, nichts gespeichert), `GET /api/budget`. Galerie: 7 aufgezeichnete echte Läufe (`pnpm --filter @verdict/web gallery:build`), funktioniert ohne Budget.
- Unabhängiger Finish-Reviewer: Runde 1 „fix" (8 Punkte), Runde 2 „fix" (3 Regressionen), Runde 3 „ship". Dabei gefunden und im Kern behoben: Zitate mit wörtlichem `\n` wurden fälschlich abgelehnt; KoSIT-Ablehnungen wie BR-DE-1 werden jetzt zur gezielten Rückfrage.
- Produktions-Build und `next start` mit echter PDF geprüft.
- Offen: Koyeb-Deployment (OPS-05), Galerie-Lauf mit echter Ablehnung fürs Video (Reviewer N2), ZUGFeRD (VER-06/CORE-05, nach Kill-Gate erlaubt), README/Video (Phase 5).
- Deployment erledigt 05.10. (OPS-05, Google Cloud Run). Offen: Galerie-Lauf mit echter Ablehnung fürs Video (Reviewer N2), ZUGFeRD (VER-06/CORE-05, nach Kill-Gate erlaubt), README/Video (Phase 5).

### Phase 5: Einreichung
**Goal**: Vollständige, regelkonforme Einreichung mit belastbaren Zahlen, zwei Tage vor Deadline.
Expand Down
10 changes: 5 additions & 5 deletions deploy/hf-space/Dockerfile → deploy/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1.7
# Hugging Face Space image: the Next.js app and the KoSIT verifier in one container.
# The verifier listens on localhost only; the Space exposes nothing but the app port (7860).
# Verifier pins must match services/verifier/Dockerfile (deploy/hf-space/push.sh checks this).
# Demo image: the Next.js app and the KoSIT verifier in one container.
# The verifier listens on localhost only; the host exposes nothing but the app port (7860).
# Verifier pins must match services/verifier/Dockerfile (deploy/cloud-run.sh checks this).
ARG KOSIT_VALIDATOR_VERSION=1.6.3
ARG XRECHNUNG_CONFIG_VERSION=xrechnung-3.0.2-validator-configuration-2026-08-31

Expand Down Expand Up @@ -46,8 +46,8 @@ COPY --from=verifier /build/lib/validator.jar /opt/verifier/lib/validator.jar
COPY --from=verifier /build/classes /opt/verifier/classes
COPY --from=verifier /build/config /opt/xrechnung-config
COPY --from=web /repo/apps/web/.next/standalone /app
COPY --chmod=755 deploy/hf-space/start.sh /usr/local/bin/start-verdict
# Spaces run the container as uid 1000.
COPY --chmod=755 deploy/start.sh /usr/local/bin/start-verdict
# Unprivileged user; no host needs root here.
USER 1000
WORKDIR /app/apps/web
EXPOSE 7860
Expand Down
75 changes: 75 additions & 0 deletions deploy/cloud-run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
# Deploy the committed tree (HEAD) to Google Cloud Run as one service.
# Needs `gcloud auth login` and a project with billing enabled (the demo stays inside the
# free tier: request-based billing, scale to zero, at most one instance).
# deploy/cloud-run.sh build, push, deploy
# deploy/cloud-run.sh --secret (re)store NEBIUS_API_KEY from .env in Secret Manager first
set -euo pipefail

PROJECT="${GCP_PROJECT:-$(gcloud config get-value project 2>/dev/null)}"
REGION="${GCP_REGION:-europe-west1}"
SERVICE=verdict
REPO=verdict
[ -n "$PROJECT" ] || { echo "No project: gcloud config set project <id>" >&2; exit 1; }

ROOT="$(git rev-parse --show-toplevel)"
cd "$ROOT"

# The demo Dockerfile duplicates the verifier pins; refuse to ship if they drifted.
pins() { grep -oE 'sha256:[0-9a-f]{64}|(VERSION|CONFIG_VERSION)=[^ ]+' "$1" | sort; }
if ! diff <(pins services/verifier/Dockerfile) <(pins deploy/Dockerfile) >/dev/null; then
echo "Verifier pins differ between services/verifier/Dockerfile and deploy/Dockerfile" >&2
exit 1
fi

gcloud services enable run.googleapis.com artifactregistry.googleapis.com \
secretmanager.googleapis.com --project "$PROJECT" --quiet

if [ "${1:-}" = "--secret" ]; then
# The key goes from .env straight into Secret Manager; it is never echoed or logged.
key="$(grep -E '^NEBIUS_API_KEY=' .env | head -1 | cut -d= -f2-)"
[ -n "$key" ] || { echo "NEBIUS_API_KEY is empty in .env" >&2; exit 1; }
if gcloud secrets describe nebius-api-key --project "$PROJECT" >/dev/null 2>&1; then
printf '%s' "$key" | gcloud secrets versions add nebius-api-key --data-file=- --project "$PROJECT" >/dev/null
else
printf '%s' "$key" | gcloud secrets create nebius-api-key --data-file=- \
--replication-policy=automatic --project "$PROJECT" >/dev/null
fi
unset key
sa="$(gcloud projects describe "$PROJECT" --format='value(projectNumber)')-compute@developer.gserviceaccount.com"
gcloud secrets add-iam-policy-binding nebius-api-key --project "$PROJECT" \
--member "serviceAccount:$sa" --role roles/secretmanager.secretAccessor >/dev/null
echo "Secret nebius-api-key stored."
fi

gcloud artifacts repositories describe "$REPO" --location "$REGION" --project "$PROJECT" >/dev/null 2>&1 \
|| gcloud artifacts repositories create "$REPO" --repository-format docker \
--location "$REGION" --project "$PROJECT" --quiet
# Keep only the two newest images so storage stays inside the 0.5 GB free tier.
policy="$(mktemp)"
cat > "$policy" <<'JSON'
[
{"name": "keep-newest-2", "action": {"type": "Keep"}, "mostRecentVersions": {"keepCount": 2}},
{"name": "delete-older", "action": {"type": "Delete"}, "condition": {"tagState": "any"}}
]
JSON
gcloud artifacts repositories set-cleanup-policies "$REPO" --location "$REGION" \
--project "$PROJECT" --policy "$policy" --no-dry-run --quiet >/dev/null
rm -f "$policy"
gcloud auth configure-docker "$REGION-docker.pkg.dev" --quiet >/dev/null

# Build from HEAD only, so uncommitted changes and local files never reach the image.
STAGE="$(mktemp -d)"
trap 'rm -rf "$STAGE"' EXIT
git archive HEAD | tar -x -C "$STAGE"
IMAGE="$REGION-docker.pkg.dev/$PROJECT/$REPO/$SERVICE:$(git rev-parse --short HEAD)"
docker buildx build --platform linux/amd64 -f "$STAGE/deploy/Dockerfile" -t "$IMAGE" --push "$STAGE"

# max-instances 1: the rate limit and the daily model budget live in memory, so a second
# instance would double both. It also caps what a traffic spike can cost.
gcloud run deploy "$SERVICE" --image "$IMAGE" --region "$REGION" --project "$PROJECT" \
--port 7860 --cpu 1 --memory 2Gi --cpu-boost \
--min-instances 0 --max-instances 1 --concurrency 20 --timeout 300 \
--set-secrets NEBIUS_API_KEY=nebius-api-key:latest \
--allow-unauthenticated --quiet
gcloud run services describe "$SERVICE" --region "$REGION" --project "$PROJECT" --format 'value(status.url)'
22 changes: 0 additions & 22 deletions deploy/hf-space/README.md

This file was deleted.

28 changes: 0 additions & 28 deletions deploy/hf-space/push.sh

This file was deleted.

2 changes: 1 addition & 1 deletion deploy/hf-space/start.sh → deploy/start.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Start the verifier on localhost, wait until it is healthy, then run the app.
# If either process exits, the container exits and the Space restarts it.
# If either process exits, the container exits and the host restarts it.
set -euo pipefail

# The secret only guards localhost traffic inside this container, so a fresh one per boot is enough.
Expand Down
Loading