Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87f3cda5ad |
No files matched your search
@@ -1,2 +0,0 @@
|
||||
# common/ ist modulübergreifend: Änderungen brauchen Review (Auswirkungsliste im PR)
|
||||
/common/ @patrick
|
||||
@@ -1,23 +0,0 @@
|
||||
name: common Pflichttest-Gate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "common/**"
|
||||
|
||||
jobs:
|
||||
pflichttest-gate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
- name: Gate bauen
|
||||
working-directory: common
|
||||
run: go build -o /tmp/pflichttestgate ./cmd/pflichttestgate
|
||||
- name: Geänderte Dateien gegen Pflichttest-Regel pruefen
|
||||
run: |
|
||||
git diff --name-only "origin/${{ github.base_ref }}...HEAD" -- common/ | /tmp/pflichttestgate
|
||||
@@ -1,33 +0,0 @@
|
||||
name: common Versions-Tag
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "common/**"
|
||||
|
||||
jobs:
|
||||
tag:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
- name: Build, Vet, Test
|
||||
working-directory: common
|
||||
run: |
|
||||
go build ./...
|
||||
go vet ./...
|
||||
go test ./... -count=1
|
||||
- name: Naechsten Tag setzen
|
||||
run: |
|
||||
minor=$(tr -d '[:space:]' < common/MINOR)
|
||||
last=$(git tag -l "common/v0.${minor}.*" --sort=-v:refname | head -n1)
|
||||
if [ -z "$last" ]; then next="common/v0.${minor}.0"; else
|
||||
patch=${last##*.}; base=${last%.*}; next="${base}.$((patch + 1))"
|
||||
fi
|
||||
git tag "$next"
|
||||
git push origin "$next"
|
||||
@@ -1,37 +0,0 @@
|
||||
name: DB-Integrationstests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "common/**"
|
||||
- "archive/**"
|
||||
- "ai/**"
|
||||
- "connect/**"
|
||||
- "scripts/ci-pg-cluster.sh"
|
||||
- ".gitea/workflows/db-tests.yml"
|
||||
|
||||
jobs:
|
||||
db-tests:
|
||||
runs-on: nexarch-test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
- name: Wegwerf-Postgres starten
|
||||
run: scripts/ci-pg-cluster.sh start
|
||||
- name: common
|
||||
working-directory: common
|
||||
run: go test ./... -count=1 -p 1
|
||||
- name: archive
|
||||
working-directory: archive
|
||||
run: go test ./... -count=1 -p 1
|
||||
- name: ai
|
||||
working-directory: ai
|
||||
run: go test ./... -count=1 -p 1
|
||||
- name: connect
|
||||
working-directory: connect
|
||||
run: go test ./... -count=1 -p 1
|
||||
- name: Wegwerf-Postgres stoppen
|
||||
if: always()
|
||||
run: scripts/ci-pg-cluster.sh stop
|
||||
@@ -604,567 +604,3 @@ Keine Commits in dieser Session.
|
||||
- scripts/qa02-foundation-gate.sh | 2 +-
|
||||
|
||||
---
|
||||
## 2026-10-19 12:07 – 12:07 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/audit-log/app/page.tsx | 6 +++---
|
||||
- web/dms-browser/app/layout.tsx | 6 +++---
|
||||
- web/lic-admin/app/page.tsx | 18 +++++++++---------
|
||||
- web/portal/app/layout.tsx | 12 ++----------
|
||||
- web/portal/app/page.tsx | 4 ++--
|
||||
- web/shl/components/ModuleNavShell.tsx | 18 +++++++++++++++---
|
||||
- web/shl/components/Shell.tsx | 2 ++
|
||||
- web/shl/index.ts | 2 +-
|
||||
- web/shl/tokens/tokens.ts | 24 ++++++++++++++++++++++++
|
||||
- web/status-page/app/page.tsx | 12 ++++++------
|
||||
- web/tenant-admin/app/page.tsx | 8 ++++----
|
||||
|
||||
---
|
||||
## 2026-10-19 12:08 – 12:08 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/audit-log/app/page.tsx | 6 +++---
|
||||
- web/dms-browser/app/layout.tsx | 6 +++---
|
||||
- web/lic-admin/app/page.tsx | 18 +++++++++---------
|
||||
- web/portal/app/layout.tsx | 12 ++----------
|
||||
- web/portal/app/page.tsx | 4 ++--
|
||||
- web/shl/components/ModuleNavShell.tsx | 18 +++++++++++++++---
|
||||
- web/shl/components/Shell.tsx | 2 ++
|
||||
- web/shl/index.ts | 2 +-
|
||||
- web/shl/tokens/tokens.ts | 24 ++++++++++++++++++++++++
|
||||
- web/status-page/app/page.tsx | 12 ++++++------
|
||||
- web/tenant-admin/app/page.tsx | 8 ++++----
|
||||
|
||||
---
|
||||
## 2026-10-19 12:14 – 12:15 (1m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/audit-log/app/page.tsx | 6 +++---
|
||||
- web/dms-browser/app/layout.tsx | 6 +++---
|
||||
- web/lic-admin/app/page.tsx | 18 +++++++++---------
|
||||
- web/portal/app/layout.tsx | 12 ++----------
|
||||
- web/portal/app/page.tsx | 4 ++--
|
||||
- web/shl/components/ModuleNavShell.tsx | 18 +++++++++++++++---
|
||||
- web/shl/components/Shell.tsx | 2 ++
|
||||
- web/shl/index.ts | 2 +-
|
||||
- web/shl/tokens/tokens.ts | 24 ++++++++++++++++++++++++
|
||||
- web/status-page/app/page.tsx | 12 ++++++------
|
||||
- web/tenant-admin/app/page.tsx | 8 ++++----
|
||||
|
||||
---
|
||||
## 2026-10-19 12:29 – 12:30 (1m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- dms/internal/upload/service.go | 38 ++++++++++++++++++++++++++++++---
|
||||
- dms/internal/upload/service_test.go | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
---
|
||||
## 2026-10-19 12:31 – 12:31 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- dms/internal/upload/service.go | 25 ++++++++++++++++++++++++-
|
||||
- dms/internal/upload/service_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
---
|
||||
## 2026-10-19 12:44 – 12:53 (8m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** banking
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- dms/internal/upload/service.go | 25 ++++++++++++++++++++++++-
|
||||
- dms/internal/upload/service_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
---
|
||||
## 2026-10-19 12:54 – 12:54 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- dms/internal/upload/service.go | 25 ++++++++++++++++++++++++-
|
||||
- dms/internal/upload/service_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
---
|
||||
## 2026-10-19 13:47 – 13:50 (2m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** banking
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/mail-postfach/app/layout.tsx | 8 +++++---
|
||||
- web/shl/components/ModuleNavShell.tsx | 4 ++++
|
||||
|
||||
---
|
||||
## 2026-10-19 14:49 – 14:50 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** archivmail
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- MIGRATIONSKONZEPT.md | 2 +-
|
||||
- PRODUKTARCHITEKTUR.md | 2 +-
|
||||
- SICHERHEITSKONZEPT.md | 16 ++++++++--------
|
||||
- SKALIERUNGSKONZEPT.md | 2 +-
|
||||
- STORAGE-KONZEPT.md | 4 ++--
|
||||
- dms/internal/searchsync/searchsync_test.go | 4 ++--
|
||||
- web/portal/app/ModuleGrid.test.tsx | 3 ++-
|
||||
- web/portal/app/ModuleGrid.tsx | 17 +----------------
|
||||
- web/portal/lib/modules.test.ts | 4 +++-
|
||||
- web/shl/components/ModuleNavShell.tsx | 27 +++++++++++++++++++--------
|
||||
- web/shl/index.ts | 2 +-
|
||||
|
||||
---
|
||||
## 2026-10-19 14:58 – 23:11 (8h 12m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-20 04:03 – 04:03 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-20 04:03 – 04:03 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-20 04:03 – 04:03 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-20 04:03 – 04:03 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-20 04:03 – 04:03 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 11:18 – 11:18 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 11:20 – 11:20 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 11:54 – 11:54 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 11:55 – 11:55 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 11:57 – 11:57 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 11:58 – 11:58 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 12:37 – 12:38 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 12:38 – 12:38 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 12:40 – 12:40 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 12:42 – 12:42 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 12:42 – 12:44 (1m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 12:47 – 12:52 (4m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 12:53 – 12:54 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 12:55 – 12:56 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 13:00 – 13:01 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** Trad-Bot
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 13:35 – 13:39 (4m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 13:42 – 13:52 (10m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 13:53 – 13:53 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 13:54 – 14:06 (12m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 14:08 – 14:41 (32m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 15:03 – 15:25 (22m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 15:27 – 15:43 (15m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
@@ -6,17 +6,6 @@ Stand: 2026-08-26. Zusammenführung aller bisherigen Entscheidungen aus `nexarch
|
||||
|
||||
NEXARCH ist die Nachfolgeplattform von **archivmail** (E-Mail-Archivierung) und **archivdms** (Dokumentenmanagement) — nicht als Umbenennung, sondern als modulare Neuentwicklung unter der Positionierung **"Next Generation Information Management"**: E-Mails, Dokumente, digitale Archivierung, OCR, Volltextsuche, Workflows, Compliance, Integrationen, KI — als ein zusammenhängendes System, nicht als zwei getrennte Werkzeuge nebeneinander.
|
||||
|
||||
## 1a. Vorbildsysteme
|
||||
|
||||
NEXARCH vereint die Stärken bestehender Systeme und vermeidet deren bekannte Schwächen (Details: `dms-kanban/known-issues-archivdms.md`, `dms-kanban/known-issues-archivmail.md`).
|
||||
|
||||
| Bereich | Vorbildsysteme |
|
||||
|---|---|
|
||||
| Mail | **archivmail** (eigenes Produkt, Vorgänger), **MailPiler** |
|
||||
| DMS | **paperless-ngx**, **Alfresco**, **Docspell**, **ecoDMS**, **archivdms** (eigenes Produkt, Vorgänger) |
|
||||
|
||||
Weitere Vergleichsprojekte (z. B. OpenKM, Nuxeo, Mayan EDMS, Seafile) sind noch zu prüfen. Für das DMS-Modul siehe auch `dms/README.md`.
|
||||
|
||||
## 2. Architekturprinzip
|
||||
|
||||
**Ein modularer Monolith aus sieben eigenständigen, aber koordinierten Diensten** — kein Microservice-Zwang, aber klare Dienstgrenzen mit eigenem Datenmodell, eigenem Go-Binary, eigenem Kanban-Board:
|
||||
@@ -75,8 +64,6 @@ Kein Modul kopiert die Daten eines anderen — stattdessen ein wiederkehrendes *
|
||||
|
||||
Gemeinsamer Nenner aller drei: **Referenz statt Kopie, Adapter statt Direktkopplung.**
|
||||
|
||||
Go-`internal/`-Pakete sind zwischen den Modulen nicht sichtbar; identische Querschnittspakete liegen deshalb im versionierten Modul `common/` (Git-Tags `common/vX.Y.Z`, siehe `common/README.md`). So bleiben Module einzeln baubar (Docker, eigenes Repo, Partner-Build). Fachlich verschiedene Pakete (`audittrail`, `cutoverstage`, `crypto`) bleiben modul-lokal.
|
||||
|
||||
Dasselbe Prinzip gilt auch innerhalb eines Moduls, auf Code-Ebene: Hauptdateien/Entry-Points bleiben klein, Fachlogik wandert in eigene Module. Vor einer neuen Funktions-/Modul-Datei wird geprüft, ob dieselbe Funktionalität nicht schon woanders existiert (auch modulübergreifend, z. B. in Core) — keine doppelten Funktionsdateien. Das eigene Kanban-Planungswerkzeug wurde nach genau diesem Muster korrigiert (sieben `kanban.html` enthielten identisches JS/CSS als Volltext-Duplikat, damals in `_shared/kanban.js`/`kanban.css` ausgelagert) — die Boards selbst sind inzwischen komplett aus dem Repo entfernt (Ticket-Historie liegt in der Gitea-PR-Historie), das Prinzip "keine doppelten Funktionsdateien" gilt unverändert für den eigentlichen NEXARCH-Code.
|
||||
|
||||
## 6. Technische Architektur (Kurzfassung, Details in eigenen Dokumenten)
|
||||
|
||||
+17
-45
@@ -41,10 +41,9 @@ import (
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/semanticsearchapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/summarize"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/summaryapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/tenantregistryclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/threadsummarize"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/vectorindex"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/policyclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/tenantregistryclient"
|
||||
)
|
||||
|
||||
const workerPollInterval = 2 * time.Second
|
||||
@@ -53,24 +52,13 @@ const defaultListenAddr = "127.0.0.1:9600"
|
||||
const defaultModel = "default"
|
||||
|
||||
type config struct {
|
||||
dsn string
|
||||
serviceToken string
|
||||
addr string
|
||||
model string
|
||||
moduleURLs map[string]string
|
||||
coreBaseURL string
|
||||
coreServiceToken string
|
||||
policyURL string
|
||||
policyServiceToken string
|
||||
policyDisabled bool
|
||||
}
|
||||
|
||||
// policyClient liefert nil nur mit NEXARCH_AI_APP_POLICY_DISABLED=1.
|
||||
func (c config) policyClient() *policyclient.Client {
|
||||
if c.policyURL == "" {
|
||||
return nil
|
||||
}
|
||||
return policyclient.New(c.policyURL, c.policyServiceToken)
|
||||
dsn string
|
||||
serviceToken string
|
||||
addr string
|
||||
model string
|
||||
moduleURLs map[string]string
|
||||
coreBaseURL string
|
||||
coreServiceToken string
|
||||
}
|
||||
|
||||
// parseModuleBaseURLs liest "modul=url,modul2=url2" (EXT-05: je Modul eine
|
||||
@@ -96,19 +84,13 @@ func parseModuleBaseURLs(raw string) map[string]string {
|
||||
// Programmabbruchs weiter unten im Code.
|
||||
func loadConfig(getenv func(string) string) (config, error) {
|
||||
cfg := config{
|
||||
dsn: getenv("NEXARCH_AI_APP_TENANT_DSN"),
|
||||
serviceToken: getenv("NEXARCH_AI_APP_SERVICE_TOKEN"),
|
||||
addr: getenv("NEXARCH_AI_APP_LISTEN_ADDR"),
|
||||
model: getenv("NEXARCH_AI_APP_MODEL"),
|
||||
moduleURLs: parseModuleBaseURLs(getenv("NEXARCH_AI_APP_MODULE_BASE_URLS")),
|
||||
coreBaseURL: getenv("NEXARCH_AI_APP_CORE_BASE_URL"),
|
||||
coreServiceToken: getenv("NEXARCH_AI_APP_CORE_SERVICE_TOKEN"),
|
||||
policyURL: getenv("NEXARCH_AI_APP_POLICY_URL"),
|
||||
policyServiceToken: getenv("NEXARCH_AI_APP_POLICY_SERVICE_TOKEN"),
|
||||
policyDisabled: getenv("NEXARCH_AI_APP_POLICY_DISABLED") == "1",
|
||||
}
|
||||
if cfg.policyURL != "" && cfg.policyServiceToken == "" {
|
||||
return config{}, errors.New("ai-app: NEXARCH_AI_APP_POLICY_SERVICE_TOKEN ist erforderlich, wenn NEXARCH_AI_APP_POLICY_URL gesetzt ist")
|
||||
dsn: getenv("NEXARCH_AI_APP_TENANT_DSN"),
|
||||
serviceToken: getenv("NEXARCH_AI_APP_SERVICE_TOKEN"),
|
||||
addr: getenv("NEXARCH_AI_APP_LISTEN_ADDR"),
|
||||
model: getenv("NEXARCH_AI_APP_MODEL"),
|
||||
moduleURLs: parseModuleBaseURLs(getenv("NEXARCH_AI_APP_MODULE_BASE_URLS")),
|
||||
coreBaseURL: getenv("NEXARCH_AI_APP_CORE_BASE_URL"),
|
||||
coreServiceToken: getenv("NEXARCH_AI_APP_CORE_SERVICE_TOKEN"),
|
||||
}
|
||||
if cfg.dsn == "" {
|
||||
return config{}, errors.New("ai-app: NEXARCH_AI_APP_TENANT_DSN ist erforderlich")
|
||||
@@ -116,9 +98,6 @@ func loadConfig(getenv func(string) string) (config, error) {
|
||||
if cfg.serviceToken == "" {
|
||||
return config{}, errors.New("ai-app: NEXARCH_AI_APP_SERVICE_TOKEN ist erforderlich")
|
||||
}
|
||||
if cfg.policyURL == "" && !cfg.policyDisabled {
|
||||
return config{}, errors.New("ai-app: NEXARCH_AI_APP_POLICY_URL ist erforderlich (oder NEXARCH_AI_APP_POLICY_DISABLED=1 als Uebergang)")
|
||||
}
|
||||
if cfg.addr == "" {
|
||||
cfg.addr = defaultListenAddr
|
||||
}
|
||||
@@ -150,11 +129,7 @@ func build(pool *pgxpool.Pool, cfg config, prov provider.Provider, workerID stri
|
||||
).RegisterRoutes(mux)
|
||||
|
||||
searchSvc := semanticsearch.New(prov, vectorindex.New(pool), cfg.model)
|
||||
policy := cfg.policyClient()
|
||||
if policy == nil {
|
||||
log.Print("ai-app: WARNUNG: Rechtepruefung ai.search.read deaktiviert (NEXARCH_AI_APP_POLICY_DISABLED=1)")
|
||||
}
|
||||
semanticsearchapi.NewServer(searchSvc, cfg.serviceToken).WithPolicy(policy).RegisterRoutes(mux)
|
||||
semanticsearchapi.NewServer(searchSvc, cfg.serviceToken).RegisterRoutes(mux)
|
||||
|
||||
chunkStore := vectorindex.New(pool)
|
||||
docchatapi.NewServer(
|
||||
@@ -202,9 +177,6 @@ func build(pool *pgxpool.Pool, cfg config, prov provider.Provider, workerID stri
|
||||
return wiring{mux: mux, queue: q, worker: worker, quotaStore: quotaStore}
|
||||
}
|
||||
|
||||
// Muss ueber dem Provider-Timeout (120s) liegen, sonst kappt der Server langsame Inferenz-Antworten.
|
||||
const serverWriteTimeout = 130 * time.Second
|
||||
|
||||
func main() {
|
||||
cfg, err := loadConfig(os.Getenv)
|
||||
if err != nil {
|
||||
@@ -229,7 +201,7 @@ func main() {
|
||||
Handler: w.mux,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: serverWriteTimeout,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider/mock"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/queue"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
// AK3 + AK4: eine fehlende Pflicht-Umgebungsvariable liefert einen klaren
|
||||
@@ -44,36 +44,10 @@ func TestLoadConfig_MissingRequiredVars(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Issue #369: Policy-URL ohne Service-Token darf nicht still ohne
|
||||
// Rechtepruefung starten.
|
||||
func TestLoadConfig_PolicyURLRequiresServiceToken(t *testing.T) {
|
||||
func TestLoadConfig_Defaults(t *testing.T) {
|
||||
env := map[string]string{
|
||||
"NEXARCH_AI_APP_TENANT_DSN": "postgres://x",
|
||||
"NEXARCH_AI_APP_SERVICE_TOKEN": "tok",
|
||||
"NEXARCH_AI_APP_POLICY_URL": "http://127.0.0.1:8093",
|
||||
}
|
||||
get := func(k string) string { return env[k] }
|
||||
if _, err := loadConfig(get); err == nil || !strings.Contains(err.Error(), "NEXARCH_AI_APP_POLICY_SERVICE_TOKEN") {
|
||||
t.Fatalf("erwartete fehler zur fehlenden NEXARCH_AI_APP_POLICY_SERVICE_TOKEN, bekam %v", err)
|
||||
}
|
||||
env["NEXARCH_AI_APP_POLICY_SERVICE_TOKEN"] = "policy-tok"
|
||||
cfg, err := loadConfig(get)
|
||||
if err != nil {
|
||||
t.Fatalf("unerwarteter fehler: %v", err)
|
||||
}
|
||||
if cfg.policyClient() == nil {
|
||||
t.Fatal("policy-client muss bei gesetzter URL aktiv sein")
|
||||
}
|
||||
if (config{}).policyClient() != nil {
|
||||
t.Fatal("ohne URL darf kein policy-client entstehen")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_Defaults(t *testing.T) {
|
||||
env := map[string]string{
|
||||
"NEXARCH_AI_APP_TENANT_DSN": "postgres://x",
|
||||
"NEXARCH_AI_APP_SERVICE_TOKEN": "tok",
|
||||
"NEXARCH_AI_APP_POLICY_DISABLED": "1",
|
||||
}
|
||||
cfg, err := loadConfig(func(k string) string { return env[k] })
|
||||
if err != nil {
|
||||
@@ -87,12 +61,6 @@ func TestLoadConfig_Defaults(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerWriteTimeoutCoversProviderTimeout(t *testing.T) {
|
||||
if serverWriteTimeout <= 120*time.Second {
|
||||
t.Fatalf("serverWriteTimeout = %v, muss ueber dem Provider-Timeout von 120s liegen", serverWriteTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
func newTestPool(t *testing.T) *pgxpool.Pool {
|
||||
t.Helper()
|
||||
dsn := os.Getenv("TEST_TENANT_DSN")
|
||||
@@ -216,18 +184,3 @@ func TestBuild_QueueWorkerProcessesJobWithQuota(t *testing.T) {
|
||||
t.Errorf("quota-zaehlerstand = %d, want 1 (GOV-04-Verbrauchszaehlung nicht verdrahtet)", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_PolicyRequiredUnlessDisabled(t *testing.T) {
|
||||
env := map[string]string{
|
||||
"NEXARCH_AI_APP_TENANT_DSN": "postgres://x",
|
||||
"NEXARCH_AI_APP_SERVICE_TOKEN": "tok",
|
||||
}
|
||||
get := func(k string) string { return env[k] }
|
||||
if _, err := loadConfig(get); err == nil || !strings.Contains(err.Error(), "NEXARCH_AI_APP_POLICY_URL") {
|
||||
t.Fatalf("erwartete fehler zur fehlenden NEXARCH_AI_APP_POLICY_URL, bekam %v", err)
|
||||
}
|
||||
env["NEXARCH_AI_APP_POLICY_DISABLED"] = "1"
|
||||
if _, err := loadConfig(get); err != nil {
|
||||
t.Fatalf("mit POLICY_DISABLED=1 darf der Start nicht scheitern: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
fixtureTenant = "beispiel-mandant"
|
||||
fixtureTenant = "beispiel-mandant"
|
||||
fixtureSourceType = "dms_document"
|
||||
fixtureSourceID = "beispiel-dokument-1"
|
||||
)
|
||||
@@ -77,6 +77,5 @@ func main() {
|
||||
docchatapi.NewServer(chatSvc, "test-token").RegisterRoutes(mux)
|
||||
|
||||
log.Println("searchchatapi-mockserver listening on 127.0.0.1:19603")
|
||||
// archtest:allow Einmaliges lokales pa11y-Testwerkzeug, kein Produktdienst
|
||||
log.Fatal(http.ListenAndServe("127.0.0.1:19603", mux))
|
||||
}
|
||||
@@ -4,10 +4,7 @@ go 1.25.0
|
||||
|
||||
toolchain go1.25.13
|
||||
|
||||
require (
|
||||
gitea.perlbach24.de/scripte/nexarch/common v0.2.0
|
||||
github.com/jackc/pgx/v5 v5.11.0
|
||||
)
|
||||
require github.com/jackc/pgx/v5 v5.11.0
|
||||
|
||||
require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
gitea.perlbach24.de/scripte/nexarch/common v0.2.0 h1:gcOjoOvUkT0Zmqq3m7WQp1MfXSgmzrGTF2E/+NkO2bw=
|
||||
gitea.perlbach24.de/scripte/nexarch/common v0.2.0/go.mod h1:CiEImG++QMt1xZqSMeDJhPiB7sfWTLZOwjS6OY/Iguc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// Package auditclient ist ein schlanker HTTP-Client fuer Core AUD-08
|
||||
// (internal/audit, POST /audit/ingest). AI ist ein physisch getrenntes
|
||||
// Go-Modul und kann Cores internal/audit daher nicht direkt importieren -
|
||||
// nur ueber dessen HTTP-Schnittstelle. Gleiches Muster wie
|
||||
// connect/internal/auditclient (ENG-06) und archive/internal/auditclient.
|
||||
package auditclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/httpclient"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
ServiceToken string
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
func New(baseURL, serviceToken string) *Client {
|
||||
return &Client{BaseURL: baseURL, ServiceToken: serviceToken, HTTPClient: httpclient.Default()}
|
||||
}
|
||||
|
||||
type ingestRequest struct {
|
||||
TenantSlug string `json:"tenant_slug"`
|
||||
Actor string `json:"actor"`
|
||||
Action string `json:"action"`
|
||||
Target string `json:"target"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
}
|
||||
|
||||
// Ingest meldet EIN Audit-Ereignis an Core. Absichtlich keine
|
||||
// Update-/Delete-Methode: der Ingest-Endpunkt (AUD-08) ruft denselben
|
||||
// alleinigen, anfuegenden Schreibpfad wie core-interne Aufrufer auf
|
||||
// (audit.Log.Record), gegen den Core selbst per Datenbank-Trigger jede
|
||||
// nachtraegliche Aenderung/Loeschung verweigert (AUD-02).
|
||||
func (c *Client) Ingest(ctx context.Context, tenantSlug, actor, action, target string, metadata map[string]any) error {
|
||||
body, err := json.Marshal(ingestRequest{
|
||||
TenantSlug: tenantSlug, Actor: actor, Action: action, Target: target, Metadata: metadata,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("auditclient: request kodieren: %w", err)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/audit/ingest", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("auditclient: request bauen: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Service-Token", c.ServiceToken)
|
||||
|
||||
resp, err := c.httpClient().Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("auditclient: aufruf fehlgeschlagen: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
return fmt.Errorf("auditclient: unerwarteter status %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) httpClient() *http.Client {
|
||||
if c.HTTPClient != nil {
|
||||
return c.HTTPClient
|
||||
}
|
||||
return httpclient.Default()
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package auditclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIngest_SendsExpectedRequest(t *testing.T) {
|
||||
var gotToken string
|
||||
var gotBody ingestRequest
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotToken = r.Header.Get("X-Service-Token")
|
||||
_ = json.NewDecoder(r.Body).Decode(&gotBody)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := New(srv.URL, "test-token")
|
||||
err := c.Ingest(context.Background(), "tenant_a", "svc:ai", "ai.decision.classify", "doc:1",
|
||||
map[string]any{"model": "llama3", "prompt_version": 2})
|
||||
if err != nil {
|
||||
t.Fatalf("Ingest: %v", err)
|
||||
}
|
||||
if gotToken != "test-token" {
|
||||
t.Errorf("token = %q, want test-token", gotToken)
|
||||
}
|
||||
if gotBody.Action != "ai.decision.classify" || gotBody.Target != "doc:1" {
|
||||
t.Errorf("unerwarteter request: %+v", gotBody)
|
||||
}
|
||||
if gotBody.Metadata["model"] != "llama3" {
|
||||
t.Errorf("metadata.model = %v, want llama3", gotBody.Metadata["model"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngest_UnexpectedStatusIsError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "zugriff verweigert", http.StatusForbidden)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := New(srv.URL, "test-token")
|
||||
if err := c.Ingest(context.Background(), "tenant_a", "svc:ai", "ai.decision.classify", "doc:1", nil); err == nil {
|
||||
t.Fatal("erwartet fehler bei status 403, habe nil")
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/decisionaudit"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider/mock"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider/ollama"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
|
||||
)
|
||||
|
||||
var testSchema = Schema{Categories: []string{"rechnung", "vertrag", "kuendigung"}}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
func newStore(t *testing.T) *Store {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/moduletype"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
// --- Vertrag 1: GOV-01 registrierter Objekttyp (moduletype.ObjectType) ---
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
func newRecorder(t *testing.T) *Recorder {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/auditclient"
|
||||
)
|
||||
|
||||
// Decision ist eine einzelne, abgeschlossene KI-gestuetzte Entscheidung
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/auditclient"
|
||||
)
|
||||
|
||||
type recordedRequest struct {
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/loadtest"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider/ollama"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/semanticsearch"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/loadtest"
|
||||
)
|
||||
|
||||
// QA-08 Pruefung 1 (AK1): echte parallele Chat-Anfragen gegen den realen,
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/docchat"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/semanticsearch"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/vectorindex"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/classify"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/queue"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
func newPool(t *testing.T) *pgxpool.Pool {
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/extractvalidate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/invoiceextract"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/extractmapping"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/metadatapushclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/extractmapping"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/metadatapushclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
func newMappingRegistry(t *testing.T) *extractmapping.Registry {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
func newRegistry(t *testing.T) *Registry {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Package httpclient liefert den HTTP-Client fuer ausgehende Dienstaufrufe
|
||||
// (Kopie des Musters aus connect/internal/httpclient, ENG-06): Zeitlimit
|
||||
// statt endlosem Warten, keine Weiterleitungen (ein Service-Token wuerde
|
||||
// sonst an das Weiterleitungsziel mitgesendet).
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const DefaultTimeout = 15 * time.Second
|
||||
|
||||
var shared = New(DefaultTimeout)
|
||||
|
||||
func Default() *http.Client { return shared }
|
||||
|
||||
func New(timeout time.Duration) *http.Client {
|
||||
return &http.Client{
|
||||
Timeout: timeout,
|
||||
CheckRedirect: func(*http.Request, []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNew_DoesNotFollowRedirects(t *testing.T) {
|
||||
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("weiterleitungsziel haette nie erreicht werden duerfen")
|
||||
}))
|
||||
defer target.Close()
|
||||
|
||||
redirector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, target.URL, http.StatusFound)
|
||||
}))
|
||||
defer redirector.Close()
|
||||
|
||||
resp, err := Default().Get(redirector.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("Get: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusFound {
|
||||
t.Fatalf("status = %d, want %d (keine weiterleitung gefolgt)", resp.StatusCode, http.StatusFound)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// Package loadtest implementiert die fuer QA-07/QA-08 gemeinsam genutzte
|
||||
// Last- und Leistungsmessung: parallele Sessions ausfuehren, Latenzen
|
||||
// aufzeichnen, Durchsatz bestimmen. k6/vegeta sind auf diesem Rechner nicht
|
||||
// installierbar (Projektregel: keine zusaetzlichen Toolchains/Dienste
|
||||
// installieren) — echter Ersatz ohne externe Abhaengigkeit: reale
|
||||
// nebenlaeufige Aufrufe ueber die Go-Standardbibliothek, gegen den echten,
|
||||
// in dieser Sitzung gebauten Code. Eigenstaendige Kopie aus
|
||||
// mail/internal/loadtest, da Go-Module keine internal/-Pakete teilen.
|
||||
package loadtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Result fasst Latenz- und Durchsatzwerte eines Lasttest-Laufs zusammen.
|
||||
type Result struct {
|
||||
Protocol string
|
||||
Concurrency int
|
||||
Sessions int
|
||||
Errors int
|
||||
Duration time.Duration
|
||||
Latencies []time.Duration // aufsteigend sortiert
|
||||
}
|
||||
|
||||
// Throughput liefert Sessions pro Sekunde über die Gesamtlaufzeit.
|
||||
func (r Result) Throughput() float64 {
|
||||
if r.Duration <= 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(r.Sessions) / r.Duration.Seconds()
|
||||
}
|
||||
|
||||
// Percentile liefert die Latenz am p-ten Perzentil (0.0–1.0).
|
||||
func (r Result) Percentile(p float64) time.Duration {
|
||||
if len(r.Latencies) == 0 {
|
||||
return 0
|
||||
}
|
||||
idx := int(p * float64(len(r.Latencies)-1))
|
||||
if idx < 0 {
|
||||
idx = 0
|
||||
}
|
||||
if idx >= len(r.Latencies) {
|
||||
idx = len(r.Latencies) - 1
|
||||
}
|
||||
return r.Latencies[idx]
|
||||
}
|
||||
|
||||
// Max liefert die höchste gemessene Latenz.
|
||||
func (r Result) Max() time.Duration {
|
||||
if len(r.Latencies) == 0 {
|
||||
return 0
|
||||
}
|
||||
return r.Latencies[len(r.Latencies)-1]
|
||||
}
|
||||
|
||||
// Run führt totalSessions Sessions über maximal concurrency parallele
|
||||
// Worker aus und misst je Session die Latenz von sessionFunc.
|
||||
func Run(ctx context.Context, protocol string, concurrency, totalSessions int, sessionFunc func(ctx context.Context) error) Result {
|
||||
var mu sync.Mutex
|
||||
latencies := make([]time.Duration, 0, totalSessions)
|
||||
errCount := 0
|
||||
|
||||
sem := make(chan struct{}, concurrency)
|
||||
var wg sync.WaitGroup
|
||||
start := time.Now()
|
||||
for i := 0; i < totalSessions; i++ {
|
||||
sem <- struct{}{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }()
|
||||
t0 := time.Now()
|
||||
err := sessionFunc(ctx)
|
||||
elapsed := time.Since(t0)
|
||||
mu.Lock()
|
||||
latencies = append(latencies, elapsed)
|
||||
if err != nil {
|
||||
errCount++
|
||||
}
|
||||
mu.Unlock()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
duration := time.Since(start)
|
||||
|
||||
sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] })
|
||||
return Result{
|
||||
Protocol: protocol,
|
||||
Concurrency: concurrency,
|
||||
Sessions: totalSessions,
|
||||
Errors: errCount,
|
||||
Duration: duration,
|
||||
Latencies: latencies,
|
||||
}
|
||||
}
|
||||
|
||||
// ResourceSample ist eine Momentaufnahme des Ressourcenverbrauchs DIESES
|
||||
// Prozesses: Heap über die Go-Runtime, kumulierte CPU-Zeit über
|
||||
// /proc/self/stat (Linux, kein externes Werkzeug nötig).
|
||||
type ResourceSample struct {
|
||||
HeapAllocBytes uint64
|
||||
CPUTimeSeconds float64
|
||||
}
|
||||
|
||||
func SampleResources() ResourceSample {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
return ResourceSample{HeapAllocBytes: m.HeapAlloc, CPUTimeSeconds: readSelfCPUTimeSeconds()}
|
||||
}
|
||||
|
||||
// readSelfCPUTimeSeconds liefert die kumulierte User+System-CPU-Zeit dieses
|
||||
// Prozesses in Sekunden. Liefert 0, wenn /proc/self/stat nicht lesbar ist
|
||||
// (z. B. auf Nicht-Linux-Systemen) — Aufrufer behandeln das als "keine
|
||||
// CPU-Messung verfügbar", kein harter Fehler.
|
||||
func readSelfCPUTimeSeconds() float64 {
|
||||
data, err := os.ReadFile("/proc/self/stat")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
fields := strings.Fields(string(data))
|
||||
if len(fields) < 15 {
|
||||
return 0
|
||||
}
|
||||
utime, err1 := strconv.ParseFloat(fields[13], 64)
|
||||
stime, err2 := strconv.ParseFloat(fields[14], 64)
|
||||
if err1 != nil || err2 != nil {
|
||||
return 0
|
||||
}
|
||||
const clockTicksPerSecond = 100 // Linux-Standard-USER_HZ
|
||||
return (utime + stime) / clockTicksPerSecond
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"net/url"
|
||||
"sort"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/httpclient"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
|
||||
@@ -81,7 +81,7 @@ func TestPush_UnknownFieldKey_ReportedNotDropped(t *testing.T) {
|
||||
|
||||
c := New(srv.URL, "svc-token-123")
|
||||
result, err := c.Push(context.Background(), "doc-1", "ai", map[string]string{
|
||||
"rechnungsdatum": "2026-01-15",
|
||||
"rechnungsdatum": "2026-01-15",
|
||||
"nicht_existent_feld": "wert",
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
+205
-11
@@ -1,27 +1,221 @@
|
||||
// Package migrate bindet die gemeinsame Migrationslogik aus common an den
|
||||
// modulspezifischen Fortschrittstabellennamen.
|
||||
// Package migrate implements versioned, revertible SQL migrations (no ORM) — a
|
||||
// standalone copy of the Core/DMS pattern, since AI is its own Go module
|
||||
// and cannot import other modules' internal/ packages.
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
cmigrate "gitea.perlbach24.de/scripte/nexarch/common/migrate"
|
||||
)
|
||||
|
||||
type Migration = cmigrate.Migration
|
||||
// Migration ist eine einzelne versionierte Migrationsdatei.
|
||||
type Migration struct {
|
||||
Version string // Dateiname ohne .up.sql/.down.sql, z.B. "0001_documents"
|
||||
UpSQL string
|
||||
DownSQL string
|
||||
}
|
||||
|
||||
// Load liest alle *.up.sql/*.down.sql-Paare aus dir, sortiert nach
|
||||
// Dateiname (Akzeptanzkriterium: Migrationen sind versioniert).
|
||||
func Load(dir string) ([]Migration, error) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("migrationsverzeichnis %q lesen: %w", dir, err)
|
||||
}
|
||||
var versions []string
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".up.sql") {
|
||||
continue
|
||||
}
|
||||
versions = append(versions, strings.TrimSuffix(e.Name(), ".up.sql"))
|
||||
}
|
||||
sort.Strings(versions)
|
||||
|
||||
migrations := make([]Migration, 0, len(versions))
|
||||
for _, v := range versions {
|
||||
up, err := os.ReadFile(filepath.Join(dir, v+".up.sql"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("migration %q: up.sql lesen: %w", v, err)
|
||||
}
|
||||
down, err := os.ReadFile(filepath.Join(dir, v+".down.sql"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("migration %q: down.sql lesen (jede Migration braucht ein Rollback): %w", v, err)
|
||||
}
|
||||
migrations = append(migrations, Migration{Version: v, UpSQL: string(up), DownSQL: string(down)})
|
||||
}
|
||||
return migrations, nil
|
||||
}
|
||||
|
||||
// TrackingTable ist modulspezifisch benannt, damit sich Module in
|
||||
// derselben Datenbank/demselben Schema nicht gegenseitig den
|
||||
// Migrationsfortschritt ueberschreiben (FDN-13).
|
||||
const TrackingTable = "connect_schema_migrations"
|
||||
|
||||
var migrator = cmigrate.MustNew(TrackingTable)
|
||||
const legacyTrackingTable = "schema_migrations"
|
||||
|
||||
func Load(dir string) ([]Migration, error) { return cmigrate.Load(dir) }
|
||||
// ensureTrackingTable legt TrackingTable an. Beim erstmaligen Anlegen wird
|
||||
// ein vorhandener Bestand aus der frueheren, unqualifizierten
|
||||
// schema_migrations uebernommen (nur Versionen, die dieses Modul kennt, und
|
||||
// nur wenn dessen Basisversion dort steht — eine schema_migrations eines
|
||||
// anderen Moduls oder eines anderen Schemas wird weder gelesen noch
|
||||
// veraendert). Die alte Tabelle
|
||||
// bleibt unangetastet.
|
||||
func ensureTrackingTable(ctx context.Context, pool *pgxpool.Pool, migrations []Migration) error {
|
||||
tx, err := pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s anlegen: %w", TrackingTable, err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
|
||||
func Up(ctx context.Context, pool *pgxpool.Pool, migrations []Migration) ([]string, error) {
|
||||
return migrator.Up(ctx, pool, migrations)
|
||||
// Serialisiert gleichzeitige Erstanlage/Uebernahme (mehrere App-Instanzen).
|
||||
if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtext('connect_schema_migrations'))`); err != nil {
|
||||
return fmt.Errorf("%s anlegen: %w", TrackingTable, err)
|
||||
}
|
||||
|
||||
var existed bool
|
||||
if err := tx.QueryRow(ctx, `SELECT to_regclass(current_schema() || '.' || $1::text) IS NOT NULL`, TrackingTable).Scan(&existed); err != nil {
|
||||
return fmt.Errorf("%s anlegen: %w", TrackingTable, err)
|
||||
}
|
||||
if existed {
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(ctx, `
|
||||
CREATE TABLE `+TrackingTable+` (
|
||||
version TEXT PRIMARY KEY,
|
||||
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
)
|
||||
`); err != nil {
|
||||
return fmt.Errorf("%s anlegen: %w", TrackingTable, err)
|
||||
}
|
||||
|
||||
if len(migrations) > 0 {
|
||||
var legacy bool
|
||||
if err := tx.QueryRow(ctx, `SELECT to_regclass(current_schema() || '.' || $1::text) IS NOT NULL`, legacyTrackingTable).Scan(&legacy); err != nil {
|
||||
return fmt.Errorf("%s pruefen: %w", legacyTrackingTable, err)
|
||||
}
|
||||
if legacy {
|
||||
versions := make([]string, len(migrations))
|
||||
for i, m := range migrations {
|
||||
versions[i] = m.Version
|
||||
}
|
||||
var hasBase bool
|
||||
if err := tx.QueryRow(ctx,
|
||||
`SELECT EXISTS (SELECT 1 FROM `+legacyTrackingTable+` WHERE version = $1)`, versions[0],
|
||||
).Scan(&hasBase); err != nil {
|
||||
return fmt.Errorf("%s lesen: %w", legacyTrackingTable, err)
|
||||
}
|
||||
if hasBase {
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO `+TrackingTable+` (version, applied_at)
|
||||
SELECT version, applied_at FROM `+legacyTrackingTable+` WHERE version = ANY($1)
|
||||
`, versions); err != nil {
|
||||
return fmt.Errorf("bestand aus %s uebernehmen: %w", legacyTrackingTable, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func DownOne(ctx context.Context, pool *pgxpool.Pool, migrations []Migration) (string, error) {
|
||||
return migrator.DownOne(ctx, pool, migrations)
|
||||
func appliedVersions(ctx context.Context, pool *pgxpool.Pool) (map[string]bool, error) {
|
||||
rows, err := pool.Query(ctx, `SELECT version FROM `+TrackingTable)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("angewendete migrationen lesen: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
applied := map[string]bool{}
|
||||
for rows.Next() {
|
||||
var v string
|
||||
if err := rows.Scan(&v); err != nil {
|
||||
return nil, fmt.Errorf("migrationsversion lesen: %w", err)
|
||||
}
|
||||
applied[v] = true
|
||||
}
|
||||
return applied, rows.Err()
|
||||
}
|
||||
|
||||
// Up wendet alle noch nicht angewendeten Migrationen in Reihenfolge an
|
||||
// (Akzeptanzkriterium 2: vorwaerts ausfuehrbar) — bereits angewendete
|
||||
// werden uebersprungen, damit Up auf einer leeren UND auf einer bestehenden
|
||||
// DB funktioniert (Pruefung 1).
|
||||
func Up(ctx context.Context, pool *pgxpool.Pool, migrations []Migration) (applied []string, err error) {
|
||||
if err := ensureTrackingTable(ctx, pool, migrations); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
already, err := appliedVersions(ctx, pool)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, m := range migrations {
|
||||
if already[m.Version] {
|
||||
continue
|
||||
}
|
||||
tx, err := pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return applied, fmt.Errorf("transaktion fuer %q starten: %w", m.Version, err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, m.UpSQL); err != nil {
|
||||
_ = tx.Rollback(ctx)
|
||||
return applied, fmt.Errorf("migration %q anwenden: %w", m.Version, err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO `+TrackingTable+` (version) VALUES ($1)`, m.Version); err != nil {
|
||||
_ = tx.Rollback(ctx)
|
||||
return applied, fmt.Errorf("migration %q als angewendet markieren: %w", m.Version, err)
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return applied, fmt.Errorf("migration %q committen: %w", m.Version, err)
|
||||
}
|
||||
applied = append(applied, m.Version)
|
||||
}
|
||||
return applied, nil
|
||||
}
|
||||
|
||||
// DownOne macht die zuletzt angewendete Migration rueckgaengig
|
||||
// (Akzeptanzkriterium 2: rueckwaerts ausfuehrbar) und liefert deren Version,
|
||||
// oder "" falls keine Migration angewendet war.
|
||||
func DownOne(ctx context.Context, pool *pgxpool.Pool, migrations []Migration) (version string, err error) {
|
||||
if err := ensureTrackingTable(ctx, pool, migrations); err != nil {
|
||||
return "", err
|
||||
}
|
||||
already, err := appliedVersions(ctx, pool)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var last *Migration
|
||||
for i := len(migrations) - 1; i >= 0; i-- {
|
||||
if already[migrations[i].Version] {
|
||||
last = &migrations[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if last == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
tx, err := pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("transaktion fuer rollback von %q starten: %w", last.Version, err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, last.DownSQL); err != nil {
|
||||
_ = tx.Rollback(ctx)
|
||||
return "", fmt.Errorf("migration %q zurueckrollen: %w", last.Version, err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `DELETE FROM `+TrackingTable+` WHERE version = $1`, last.Version); err != nil {
|
||||
_ = tx.Rollback(ctx)
|
||||
return "", fmt.Errorf("migration %q aus %s entfernen: %w", last.Version, TrackingTable, err)
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return "", fmt.Errorf("rollback von %q committen: %w", last.Version, err)
|
||||
}
|
||||
return last.Version, nil
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/modelconfig"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
type fakeLister struct {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
// fakeVerifier simuliert Core TEN-01 fuer die Registry-Tests, ohne einen
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/moduletype"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/moduletype"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
type fakeVerifier struct{ validTenants map[string]bool }
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
// sensitivePathPatterns beschreibt die Bereiche aus
|
||||
// TESTSTRATEGIE-AI.md Abschnitt 3.
|
||||
var sensitivePathPatterns = []*regexp.Regexp{
|
||||
regexp.MustCompile(`(^|/)ai/internal/svcauth/`),
|
||||
regexp.MustCompile(`(^|/)ai/internal/tenantfeature/`),
|
||||
regexp.MustCompile(`(^|/)ai/internal/semanticsearch/`),
|
||||
regexp.MustCompile(`(^|/)ai/internal/semanticsearchapi/`),
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
// Negativtest des Gates selbst (QA-01 Prüfung 1): ein Diff mit
|
||||
// geänderter ai/internal/tenantfeature/tenantfeature.go ohne begleitende Testdatei
|
||||
// geänderter ai/internal/svcauth/svcauth.go ohne begleitende Testdatei
|
||||
// muss als Verstoß erkannt werden.
|
||||
package pflichttestgate
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCheckDiff_FlagsSensitiveChangeWithoutTest(t *testing.T) {
|
||||
violations := CheckDiff([]string{"ai/internal/tenantfeature/tenantfeature.go"})
|
||||
violations := CheckDiff([]string{"ai/internal/svcauth/svcauth.go"})
|
||||
if len(violations) != 1 {
|
||||
t.Fatalf("erwartet genau 1 verstoß, habe %d: %+v", len(violations), violations)
|
||||
}
|
||||
if violations[0].Package != "ai/internal/tenantfeature" {
|
||||
if violations[0].Package != "ai/internal/svcauth" {
|
||||
t.Fatalf("falsches package gemeldet: %+v", violations[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDiff_PassesWhenTestFileAccompanies(t *testing.T) {
|
||||
violations := CheckDiff([]string{
|
||||
"ai/internal/tenantfeature/tenantfeature.go",
|
||||
"ai/internal/tenantfeature/tenantfeature_test.go",
|
||||
"ai/internal/svcauth/svcauth.go",
|
||||
"ai/internal/svcauth/svcauth_test.go",
|
||||
})
|
||||
if len(violations) != 0 {
|
||||
t.Fatalf("erwartet keine verstöße, habe: %+v", violations)
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
func newStore(t *testing.T) *Store {
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
// Package classification ist das Prüfgate für QA-03: misst Klassifizierungs- und
|
||||
// Package qa03gate ist das Prüfgate für QA-03: misst Klassifizierungs- und
|
||||
// Extraktionsgenauigkeit am Referenzkorpus (QA-01, ai/internal/refcorpus)
|
||||
// und belegt die Wirkung von Korrektur-Feedback auf künftige Vorschläge.
|
||||
// Reines Prüfgate — ruft nur bestehende, bereits gemergte Pakete auf
|
||||
// (classify CLS-02, extract EXT-05-Grundlage, correction/historysuggest
|
||||
// CLS-05), ohne deren Code zu ändern.
|
||||
package classification
|
||||
package qa03gate
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package searchquality ist das Prüfgate für QA-04: es misst Suchqualität,
|
||||
// Package qa04gate ist das Prüfgate für QA-04: es misst Suchqualität,
|
||||
// RAG-Antwortgüte und Quellenverweis-Nachvollziehbarkeit auf einem festen,
|
||||
// synthetischen Testkorpus (SEM-01..06 bereits gemergt) und ändert keines der
|
||||
// geprüften Pakete. Der Korpus stammt aus refcorpus (QA-01), die "Einbettung"
|
||||
@@ -7,7 +7,7 @@
|
||||
// einem externen Modell abhängt. Reine Termfrequenz ohne IDF-Gewichtung wurde
|
||||
// probiert und verworfen: seltene, fachlich entscheidende Wörter (z.B.
|
||||
// "Gesamtbetrag") gingen gegen häufige, generische Wörter unter.
|
||||
package searchquality
|
||||
package qa04gate
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -231,3 +231,4 @@ func (p *embedProvider) Chat(ctx context.Context, req provider.ChatRequest) (pro
|
||||
func (p *embedProvider) Complete(ctx context.Context, req provider.CompletionRequest) (provider.CompletionResponse, error) {
|
||||
return provider.CompletionResponse{}, errors.New("qa04gate: Complete wird vom Prüfgate nicht genutzt")
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package searchquality
|
||||
package qa04gate
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package searchquality
|
||||
package qa04gate
|
||||
|
||||
// testQuestion ist eine feste Testfrage des QA-04-Prüfgates gegen den
|
||||
// refcorpus-Testkorpus (QA-01). erwarteteSourceID ist leer, wenn die Frage
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
// Package governance ist das Prüfgate für QA-05 (Datenschutz- &
|
||||
// Package qa05gate ist das Prüfgate für QA-05 (Datenschutz- &
|
||||
// Governance-Prüfung). Es bringt keine neue Fachlogik ein, sondern führt
|
||||
// GOV-02/GOV-03/GOV-05 end-to-end gegen eine echte Postgres-Wegwerf-DB bzw.
|
||||
// einen Mock-Core zusammen, um die drei QA-05-Akzeptanzkriterien in der
|
||||
// Praxis (nicht nur auf Store-Ebene) nachzuweisen.
|
||||
package governance
|
||||
package qa05gate
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/decisionaudit"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
|
||||
@@ -23,8 +24,7 @@ import (
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider/openai"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/residency"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/tenantfeature"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
// realPool liefert einen frischen, migrierten Pool auf der Wegwerf-DB aus
|
||||
@@ -36,7 +36,7 @@ func realPool(t *testing.T) *pgxpool.Pool {
|
||||
t.Skip("TEST_TENANT_DSN nicht gesetzt, Integrationstest übersprungen")
|
||||
}
|
||||
pool := testdb.Pool(t, dsn)
|
||||
migrationsDir, err := filepath.Abs("../../../migrations")
|
||||
migrationsDir, err := filepath.Abs("../../migrations")
|
||||
if err != nil {
|
||||
t.Fatalf("migrationsverzeichnis: %v", err)
|
||||
}
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/limits"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/loadtest"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/loadtest"
|
||||
)
|
||||
|
||||
// QA-08 Pruefung 2 (AK2): realistische parallele Klassifizierungs-/
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
func newPool(t *testing.T) *pgxpool.Pool {
|
||||
|
||||
@@ -16,9 +16,9 @@ import (
|
||||
// eine Zeitueberschreitung real simulieren laesst (respektiert ctx wie ein
|
||||
// echter HTTP-Client es tun wuerde).
|
||||
type slowProvider struct {
|
||||
delay time.Duration
|
||||
calls atomic.Int64
|
||||
cancelled atomic.Int64
|
||||
delay time.Duration
|
||||
calls atomic.Int64
|
||||
cancelled atomic.Int64
|
||||
}
|
||||
|
||||
func (s *slowProvider) Chat(ctx context.Context, req provider.ChatRequest) (provider.ChatResponse, error) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
func newPool(t *testing.T) *pgxpool.Pool {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider/openai"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/residency"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
func setup(t *testing.T) *residency.Store {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/residency"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/residency"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/residencyapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
const testToken = "test-service-token"
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/historysuggest"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
func newStore(t *testing.T) *Store {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/historysuggest"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/routingfeedback"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/routingfeedback"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
const testToken = "test-service-token"
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/semanticsearch"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/vectorindex"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
)
|
||||
|
||||
func newRealPool(t *testing.T) *pgxpool.Pool {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Package semanticsearchapi ist die HTTP-Bindung der semantischen Suche
|
||||
// (SEM-03). Aufrufer ist das anfragende Modul (DMS/Mail), nicht der
|
||||
// Endnutzer direkt — Service-Token-Pflicht wie moduleadapter (Workflow
|
||||
// ENG-02), fail closed. Zusaetzlich (Issue #369) Rechtepruefung
|
||||
// ai.search.read gegen Core fuer den Mandanten aus dem Anfrage-Koerper.
|
||||
// ENG-02), fail closed.
|
||||
package semanticsearchapi
|
||||
|
||||
import (
|
||||
@@ -11,30 +10,18 @@ import (
|
||||
"net/http"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/semanticsearch"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/policyclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
|
||||
)
|
||||
|
||||
const permissionSearchRead = "ai.search.read"
|
||||
|
||||
type Server struct {
|
||||
svc *semanticsearch.Service
|
||||
serviceToken string
|
||||
policy *policyclient.Client
|
||||
}
|
||||
|
||||
func NewServer(svc *semanticsearch.Service, serviceToken string) *Server {
|
||||
return &Server{svc: svc, serviceToken: serviceToken}
|
||||
}
|
||||
|
||||
// WithPolicy aktiviert die Rechtepruefung gegen Core (POST /authorize).
|
||||
// Rolle ist fest "user", weil der Modul-zu-Modul-Aufruf keine Nutzerrolle
|
||||
// transportiert (Uebergang, gleiches Muster wie connect/internal/statusapi).
|
||||
func (s *Server) WithPolicy(client *policyclient.Client) *Server {
|
||||
s.policy = client
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Server) RegisterRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("POST /search", s.requireAuth(s.handleSearch))
|
||||
}
|
||||
@@ -50,22 +37,6 @@ func (s *Server) requireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) permitted(w http.ResponseWriter, r *http.Request, tenantSlug, permission string) bool {
|
||||
if s.policy == nil {
|
||||
return true
|
||||
}
|
||||
allowed, err := s.policy.Allowed(r.Context(), tenantSlug, "user", permission)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusServiceUnavailable, "berechtigungsdienst nicht erreichbar")
|
||||
return false
|
||||
}
|
||||
if !allowed {
|
||||
writeError(w, http.StatusForbidden, "keine berechtigung")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type refRequest struct {
|
||||
SourceType string `json:"source_type"`
|
||||
SourceID string `json:"source_id"`
|
||||
@@ -88,9 +59,6 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusBadRequest, "ungueltiger anfrage-koerper")
|
||||
return
|
||||
}
|
||||
if !s.permitted(w, r, req.TenantSlug, permissionSearchRead) {
|
||||
return
|
||||
}
|
||||
|
||||
refs := make([]semanticsearch.Ref, 0, len(req.AllowedRefs))
|
||||
for _, ref := range req.AllowedRefs {
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
package semanticsearchapi_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/policyclient"
|
||||
)
|
||||
|
||||
// Steht fuer Core's POST /authorize-Vertrag ({"allowed": bool}); prueft
|
||||
// die echte Kette requireAuth -> permitted -> policyclient -> 403/200.
|
||||
func newFakePolicyAPI(t *testing.T, allow bool, gotBody *map[string]string) *httptest.Server {
|
||||
t.Helper()
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/authorize" || r.Header.Get("X-Service-Token") == "" {
|
||||
t.Fatalf("unerwartete anfrage: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
if gotBody != nil {
|
||||
_ = json.NewDecoder(r.Body).Decode(gotBody)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]bool{"allowed": allow})
|
||||
}))
|
||||
}
|
||||
|
||||
func searchRequest(t *testing.T) *http.Request {
|
||||
t.Helper()
|
||||
body, _ := json.Marshal(map[string]any{
|
||||
"tenant_slug": "acme",
|
||||
"query": "test",
|
||||
"top_k": 5,
|
||||
"allowed_refs": []map[string]string{{"source_type": "dms_document", "source_id": "doc-1"}},
|
||||
})
|
||||
req := httptest.NewRequest(http.MethodPost, "/search", bytes.NewReader(body))
|
||||
req.Header.Set("X-Service-Token", "geheimes-token")
|
||||
return req
|
||||
}
|
||||
|
||||
func TestHandleSearch_PolicyDenies_Rejected(t *testing.T) {
|
||||
var got map[string]string
|
||||
policyAPI := newFakePolicyAPI(t, false, &got)
|
||||
defer policyAPI.Close()
|
||||
mux := http.NewServeMux()
|
||||
newTestServer().WithPolicy(policyclient.New(policyAPI.URL, "test-token")).RegisterRoutes(mux)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, searchRequest(t))
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("erwartet 403 ohne berechtigung, bekam %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if got["permission"] != "ai.search.read" || got["tenant"] != "acme" {
|
||||
t.Fatalf("falsche policy-anfrage: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleSearch_PolicyAllows_Passes(t *testing.T) {
|
||||
policyAPI := newFakePolicyAPI(t, true, nil)
|
||||
defer policyAPI.Close()
|
||||
mux := http.NewServeMux()
|
||||
newTestServer().WithPolicy(policyclient.New(policyAPI.URL, "test-token")).RegisterRoutes(mux)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, searchRequest(t))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("erwartet 200 mit berechtigung, bekam %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleSearch_PolicyUnavailable_FailsClosed(t *testing.T) {
|
||||
policyAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer policyAPI.Close()
|
||||
mux := http.NewServeMux()
|
||||
newTestServer().WithPolicy(policyclient.New(policyAPI.URL, "test-token")).RegisterRoutes(mux)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, searchRequest(t))
|
||||
if rec.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("erwartet 503 bei ausfall des berechtigungsdienstes, bekam %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleSearch_PolicyCheckedOnlyAfterServiceToken(t *testing.T) {
|
||||
policyAPI := newFakePolicyAPI(t, true, nil)
|
||||
defer policyAPI.Close()
|
||||
mux := http.NewServeMux()
|
||||
newTestServer().WithPolicy(policyclient.New(policyAPI.URL, "test-token")).RegisterRoutes(mux)
|
||||
|
||||
req := searchRequest(t)
|
||||
req.Header.Set("X-Service-Token", "falsch")
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("erwartet 401 bei falschem service-token, bekam %d", rec.Code)
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"net/http"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/summarize"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/threadsummarize"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
||||
@@ -125,10 +125,10 @@ func wordSet(s string) map[string]bool {
|
||||
// TypeMetrics fasst die Bewertungen aller Stichproben eines
|
||||
// Zusammenfassungstyps zusammen (AK1).
|
||||
type TypeMetrics struct {
|
||||
Type string
|
||||
Count int
|
||||
Type string
|
||||
Count int
|
||||
AverageGroundedness float64
|
||||
FlaggedCount int
|
||||
FlaggedCount int
|
||||
}
|
||||
|
||||
// AggregateByType berechnet je Zusammenfassungstyp die Kennzahl über alle
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Package svcauth vergleicht Service-Token in konstanter Zeit (eigene Kopie
|
||||
// je Modul, siehe workflow/internal/svcauth — Go-Module teilen keine
|
||||
// internal/-Pakete über Modulgrenzen; Lehre aus RET-14/F-08: Schutz beim
|
||||
// ersten Entwurf einbauen statt nachzuruesten).
|
||||
package svcauth
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
)
|
||||
|
||||
// Equal vergleicht die SHA-256-Werte beider Eingaben in konstanter Zeit.
|
||||
// Ein leeres erwartetes Token stimmt nie.
|
||||
func Equal(got, want string) bool {
|
||||
if want == "" {
|
||||
return false
|
||||
}
|
||||
g, w := sha256.Sum256([]byte(got)), sha256.Sum256([]byte(want))
|
||||
return subtle.ConstantTimeCompare(g[:], w[:]) == 1
|
||||
}
|
||||
@@ -27,6 +27,7 @@ func (s *stubProvider) Embed(ctx context.Context, req provider.EmbeddingRequest)
|
||||
return provider.EmbeddingResponse{}, nil
|
||||
}
|
||||
|
||||
|
||||
// AK1: Konfidenz wird je Vorschlag mitgeliefert.
|
||||
func TestSuggest_ReturnsConfidencePerSuggestion(t *testing.T) {
|
||||
p := &stubProvider{response: `[{"tag":"rechnung","confidence":0.9,"reasoning":"enthaelt rechnungsnummer"},{"tag":"steuer","confidence":0.4,"reasoning":"erwaehnt umsatzsteuer"}]`}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
func newStore(t *testing.T) *Store {
|
||||
|
||||
File renamed without changes.
File renamed without changes.
@@ -7,7 +7,6 @@ package testdb
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -17,21 +16,6 @@ import (
|
||||
|
||||
var seq atomic.Int64
|
||||
|
||||
// AdminDSN liefert TEST_ADMIN_DSN. Fehlt er, wird uebersprungen; mit
|
||||
// REQUIRE_DB=1 (CI) zaehlt das Fehlen als Fehler.
|
||||
func AdminDSN(t testing.TB) string {
|
||||
t.Helper()
|
||||
dsn := os.Getenv("TEST_ADMIN_DSN")
|
||||
if dsn != "" {
|
||||
return dsn
|
||||
}
|
||||
if os.Getenv("REQUIRE_DB") == "1" {
|
||||
t.Fatal("testdb: TEST_ADMIN_DSN nicht gesetzt, aber REQUIRE_DB=1")
|
||||
}
|
||||
t.Skip("testdb: TEST_ADMIN_DSN nicht gesetzt, Integrationstest uebersprungen")
|
||||
return ""
|
||||
}
|
||||
|
||||
// Pool legt ein Schema an und liefert einen darauf ausgerichteten Pool;
|
||||
// beides wird per t.Cleanup wieder entfernt.
|
||||
func Pool(t testing.TB, dsn string) *pgxpool.Pool {
|
||||
@@ -30,9 +30,9 @@ type Thread struct {
|
||||
|
||||
// Result ist die Zusammenfassung eines Threads.
|
||||
type Result struct {
|
||||
Summary string
|
||||
MessageCount int
|
||||
LastMessageID string
|
||||
Summary string
|
||||
MessageCount int
|
||||
LastMessageID string
|
||||
}
|
||||
|
||||
// Options steuert die Zusammenfassung.
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/vectorindex"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
)
|
||||
|
||||
func newPool(t *testing.T) *pgxpool.Pool {
|
||||
|
||||
@@ -12,9 +12,9 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/complianceapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpharden"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/rbacclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/retentionapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpharden"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/kennzahlenapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/rbacclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/retentionapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -13,10 +13,10 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpharden"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/legalholdapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/moduleadapter"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/tenantregistryclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/tenantregistryclient"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpharden"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/restoretest"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -13,9 +13,9 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpharden"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/rbacclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/retentionapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpharden"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/scrub"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -30,7 +30,7 @@ Verbindlich für jeden Pull Request, der Dateien in einem der folgenden Bereiche
|
||||
- **WORM-Durchsetzung** (`archive/internal/wormstore/`) — die Storage-Object-Lock-Garantie selbst.
|
||||
- **Zugriffsprüfung** (`archive/internal/rbacclient/`, `archive/internal/retentionapi/`) — RBAC-Anbindung
|
||||
und die APIs, die sie durchsetzen.
|
||||
- **Tenant-/Schlüssel-Bezug** (`archive/internal/kekclient/`,
|
||||
- **Tenant-/Schlüssel-Bezug** (`archive/internal/tenantregistryclient/`, `archive/internal/kekclient/`,
|
||||
`archive/internal/backupcrypto/`) — unabhängige Tenant-Validierung und Verschlüsselungspfade.
|
||||
|
||||
Regel: **jede geänderte `.go`-Datei in einem dieser Bereiche muss von einer geänderten oder neuen
|
||||
|
||||
+1
-4
@@ -4,10 +4,7 @@ go 1.25.0
|
||||
|
||||
toolchain go1.25.13
|
||||
|
||||
require (
|
||||
gitea.perlbach24.de/scripte/nexarch/common v0.1.0
|
||||
github.com/jackc/pgx/v5 v5.9.2
|
||||
)
|
||||
require github.com/jackc/pgx/v5 v5.9.2
|
||||
|
||||
require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
gitea.perlbach24.de/scripte/nexarch/common v0.1.0 h1:biNzFr2LOMZC7b1leY3smQtrMmSMp6IW80OvR4LSjLY=
|
||||
gitea.perlbach24.de/scripte/nexarch/common v0.1.0/go.mod h1:CiEImG++QMt1xZqSMeDJhPiB7sfWTLZOwjS6OY/Iguc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
||||
File renamed without changes.
File renamed without changes.
@@ -1,51 +0,0 @@
|
||||
package archtest
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Architekturtest: im Produktivcode dieses Moduls gibt es keine Aufrufe ueber
|
||||
// den Standard-Client ohne Zeitlimit.
|
||||
func TestNoUnboundedDefaultClientInProductionCode(t *testing.T) {
|
||||
root, err := filepath.Abs(filepath.Join("..", ".."))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
forbidden := regexp.MustCompile(`http\.DefaultClient|http\.(Get|Post|Head|PostForm)\(`)
|
||||
err = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
if n := d.Name(); n == "node_modules" || n == "testdata" || n == ".git" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, line := range strings.Split(string(data), "\n") {
|
||||
if t := strings.TrimSpace(line); strings.HasPrefix(t, "//") {
|
||||
continue
|
||||
}
|
||||
if forbidden.MatchString(line) {
|
||||
rel, _ := filepath.Rel(root, path)
|
||||
t.Errorf("%s:%d nutzt den Standard-Client ohne Zeitlimit: %s", rel, i+1, strings.TrimSpace(line))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ func moduleRoot(t *testing.T) string {
|
||||
// Antwort bzw. der rohe ListenAndServe-Aufruf genau die erlaubte
|
||||
// Implementierung der Regel selbst ist (API-20/API-21), kein Verstoss.
|
||||
// Das Archive-Modul hat keine weiteren, physisch verschachtelten Go-Module.
|
||||
var exempt = []string{}
|
||||
var exempt = []string{"internal/apierror", "internal/httpharden"}
|
||||
|
||||
// TestNoErrorTextLeaksIntoHTTPResponses ist Akzeptanzkriterium 1: kein
|
||||
// err.Error() direkt in einer HTTP-Antwort, ausser ueber apierror.
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Package auditclient ist ein schlanker HTTP-Client für Core AUD-08
|
||||
// (internal/audit, POST /audit/ingest). Archive ist ein physisch
|
||||
// getrenntes Go-Modul und kann Cores internal/audit daher nicht direkt
|
||||
// importieren — nur über dessen HTTP-Schnittstelle. Gleiches Muster wie
|
||||
// archive/internal/notifyclient (CFG-05).
|
||||
package auditclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpclient"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
ServiceToken string
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
func New(baseURL, serviceToken string) *Client {
|
||||
return &Client{BaseURL: baseURL, ServiceToken: serviceToken, HTTPClient: httpclient.Default()}
|
||||
}
|
||||
|
||||
type ingestRequest struct {
|
||||
TenantSlug string `json:"tenant_slug"`
|
||||
Actor string `json:"actor"`
|
||||
Action string `json:"action"`
|
||||
Target string `json:"target"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
}
|
||||
|
||||
// Ingest meldet EIN Audit-Ereignis an Core. Jeder Fehler (Transport,
|
||||
// Timeout, unerwarteter Status) wird als Fehler zurückgegeben — der
|
||||
// Aufrufer entscheidet, ob/wie er das nachbehandelt (siehe
|
||||
// archive/internal/audittrail für den outage-toleranten Wrapper).
|
||||
func (c *Client) Ingest(ctx context.Context, tenantSlug, actor, action, target string, metadata map[string]any) error {
|
||||
body, err := json.Marshal(ingestRequest{
|
||||
TenantSlug: tenantSlug, Actor: actor, Action: action, Target: target, Metadata: metadata,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("auditclient: request kodieren: %w", err)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/audit/ingest", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("auditclient: request bauen: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Service-Token", c.ServiceToken)
|
||||
|
||||
resp, err := c.httpClient().Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("auditclient: aufruf fehlgeschlagen: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
return fmt.Errorf("auditclient: unerwarteter status %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) httpClient() *http.Client {
|
||||
if c.HTTPClient != nil {
|
||||
return c.HTTPClient
|
||||
}
|
||||
return httpclient.Default()
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/auditclient"
|
||||
)
|
||||
|
||||
type Recorder struct {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/auditclient"
|
||||
)
|
||||
|
||||
func setupOutboxTest(t *testing.T) *pgxpool.Pool {
|
||||
|
||||
@@ -13,9 +13,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/audittrail"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/referenceset"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"net/url"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/ediscovery"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpclient"
|
||||
)
|
||||
|
||||
const ModuleTypeDMSDocument = "dms_document"
|
||||
|
||||
File renamed without changes.
+5
-11
@@ -57,21 +57,15 @@ func TestClientDoesNotFollowRedirects(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Architekturtest: im Produktivcode von common gibt es keine Aufrufe ueber den
|
||||
// Standard-Client ohne Zeitlimit. Die Verbraucher-Module pruefen ihren eigenen
|
||||
// Code selbst.
|
||||
// Architekturtest: im Produktivcode dieses Moduls gibt es keine Aufrufe ueber
|
||||
// den Standard-Client ohne Zeitlimit.
|
||||
func TestNoUnboundedDefaultClientInProductionCode(t *testing.T) {
|
||||
root, err := filepath.Abs(filepath.Join(".."))
|
||||
root, err := filepath.Abs(filepath.Join("..", ".."))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
scanNoDefaultClient(t, root, root)
|
||||
}
|
||||
|
||||
func scanNoDefaultClient(t *testing.T, repoRoot, root string) {
|
||||
t.Helper()
|
||||
forbidden := regexp.MustCompile(`http\.DefaultClient|http\.(Get|Post|Head|PostForm)\(`)
|
||||
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
||||
err = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -93,7 +87,7 @@ func scanNoDefaultClient(t *testing.T, repoRoot, root string) {
|
||||
continue
|
||||
}
|
||||
if forbidden.MatchString(line) {
|
||||
rel, _ := filepath.Rel(repoRoot, path)
|
||||
rel, _ := filepath.Rel(root, path)
|
||||
t.Errorf("%s:%d nutzt den Standard-Client ohne Zeitlimit: %s", rel, i+1, strings.TrimSpace(line))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// Package httpharden liefert gehaertete http.Server-Grundeinstellungen
|
||||
// (Timeouts, Header-Limit) und ein Body-Limit fuer alle Dienste (QA-15 F-05).
|
||||
// Dieselbe Datei existiert in archive/internal/httpharden und
|
||||
// dms/internal/httpharden, weil die Module getrennte go.mod-Dateien haben.
|
||||
package httpharden
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/apierror"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultReadHeaderTimeout = 10 * time.Second
|
||||
DefaultReadTimeout = 30 * time.Second
|
||||
DefaultWriteTimeout = 60 * time.Second
|
||||
DefaultIdleTimeout = 120 * time.Second
|
||||
DefaultMaxHeaderBytes = 1 << 20
|
||||
DefaultMaxBodyBytes = 1 << 20
|
||||
|
||||
EnvReadHeaderTimeout = "NEXARCH_HTTP_READ_HEADER_TIMEOUT_SECONDS"
|
||||
EnvReadTimeout = "NEXARCH_HTTP_READ_TIMEOUT_SECONDS"
|
||||
EnvWriteTimeout = "NEXARCH_HTTP_WRITE_TIMEOUT_SECONDS"
|
||||
EnvIdleTimeout = "NEXARCH_HTTP_IDLE_TIMEOUT_SECONDS"
|
||||
EnvMaxBodyBytes = "NEXARCH_HTTP_MAX_BODY_BYTES"
|
||||
)
|
||||
|
||||
// Options weicht vom Standard ab. Nullwerte bedeuten Standard bzw. Env-Wert;
|
||||
// StreamingBodies/StreamingResponses setzen ReadTimeout/WriteTimeout auf 0
|
||||
// (unbegrenzt), damit lange Uploads und Downloads nicht abbrechen. Schutz
|
||||
// gegen langsame Header bleibt ueber ReadHeaderTimeout und IdleTimeout.
|
||||
type Options struct {
|
||||
StreamingBodies bool
|
||||
StreamingResponses bool
|
||||
}
|
||||
|
||||
func envSeconds(name string, def time.Duration) time.Duration {
|
||||
v := os.Getenv(name)
|
||||
if v == "" {
|
||||
return def
|
||||
}
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil || n < 1 || n > 3600 {
|
||||
log.Fatalf("%s muss eine ganze Zahl zwischen 1 und 3600 sein, war %q", name, v)
|
||||
}
|
||||
return time.Duration(n) * time.Second
|
||||
}
|
||||
|
||||
// MaxBodyBytes liest die Body-Obergrenze (Standard 1 MiB, Env
|
||||
// NEXARCH_HTTP_MAX_BODY_BYTES, 1 KiB bis 1 GiB).
|
||||
func MaxBodyBytes() int64 {
|
||||
v := os.Getenv(EnvMaxBodyBytes)
|
||||
if v == "" {
|
||||
return DefaultMaxBodyBytes
|
||||
}
|
||||
n, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil || n < 1<<10 || n > 1<<30 {
|
||||
log.Fatalf("%s muss zwischen 1024 und 1073741824 liegen, war %q", EnvMaxBodyBytes, v)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// NewServer baut einen http.Server mit gehaerteten Timeouts.
|
||||
func NewServer(addr string, h http.Handler, opt Options) *http.Server {
|
||||
s := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: h,
|
||||
ReadHeaderTimeout: envSeconds(EnvReadHeaderTimeout, DefaultReadHeaderTimeout),
|
||||
ReadTimeout: envSeconds(EnvReadTimeout, DefaultReadTimeout),
|
||||
WriteTimeout: envSeconds(EnvWriteTimeout, DefaultWriteTimeout),
|
||||
IdleTimeout: envSeconds(EnvIdleTimeout, DefaultIdleTimeout),
|
||||
MaxHeaderBytes: DefaultMaxHeaderBytes,
|
||||
}
|
||||
if opt.StreamingBodies {
|
||||
s.ReadTimeout = 0
|
||||
}
|
||||
if opt.StreamingResponses {
|
||||
s.WriteTimeout = 0
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ListenAndServe ersetzt http.ListenAndServe: JSON-API mit Body-Limit und
|
||||
// (QA-15 F-14) apierror.Normalize aussen um jeden Handler — 5xx-Antworten
|
||||
// verlieren dadurch automatisch jeden Interna-Text (Datenbank-/Pfadfehler),
|
||||
// unabhaengig davon, was der einzelne Handler geschrieben hat.
|
||||
func ListenAndServe(addr string, h http.Handler) error {
|
||||
return NewServer(addr, apierror.Normalize(LimitBody(MaxBodyBytes(), h)), Options{}).ListenAndServe()
|
||||
}
|
||||
|
||||
// ListenAndServeStreaming fuer Dienste mit grossen Uploads/Downloads: keine
|
||||
// globale Body-Grenze und keine Lese-/Schreib-Gesamtzeit, nur Header-Timeout.
|
||||
func ListenAndServeStreaming(addr string, h http.Handler) error {
|
||||
return NewServer(addr, apierror.Normalize(h), Options{StreamingBodies: true, StreamingResponses: true}).ListenAndServe()
|
||||
}
|
||||
|
||||
// LimitBody begrenzt Request-Bodies. Ein Content-Length ueber dem Limit wird
|
||||
// vor dem Handler mit 413 abgewiesen, chunked Bodies ueber MaxBytesReader.
|
||||
func LimitBody(max int64, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.ContentLength > max {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusRequestEntityTooLarge)
|
||||
_, _ = w.Write([]byte(`{"error":{"code":"payload_too_large","message":"Anfrage zu gross"}}`))
|
||||
return
|
||||
}
|
||||
if r.Body != nil && r.Body != http.NoBody {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, max)
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
File renamed without changes.
@@ -11,7 +11,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpclient"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/svcauth"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/svcauth"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpclient"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
var sensitivePathPatterns = []*regexp.Regexp{
|
||||
regexp.MustCompile(`(^|/)archive/internal/wormstore/`),
|
||||
regexp.MustCompile(`(^|/)archive/internal/rbacclient/`),
|
||||
regexp.MustCompile(`(^|/)archive/internal/tenantregistryclient/`),
|
||||
regexp.MustCompile(`(^|/)archive/internal/retentionapi/`),
|
||||
regexp.MustCompile(`(^|/)archive/internal/kekclient/`),
|
||||
regexp.MustCompile(`(^|/)archive/internal/backupcrypto/`),
|
||||
|
||||
@@ -60,6 +60,7 @@ func TestCheckDiff_CoversAllSensitiveAreas(t *testing.T) {
|
||||
cases := []string{
|
||||
"archive/internal/wormstore/wormstore.go",
|
||||
"archive/internal/rbacclient/rbacclient.go",
|
||||
"archive/internal/tenantregistryclient/tenantregistryclient.go",
|
||||
"archive/internal/retentionapi/handler.go",
|
||||
"archive/internal/kekclient/kekclient.go",
|
||||
"archive/internal/backupcrypto/reposecret.go",
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpclient"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Package retentionverify implementiert VAL-03: Stichprobenvergleich von
|
||||
// retain_until (Quelle aus TAR-01 vs. nach der Migration im generischen
|
||||
// Retention-Objektmodell) je Tenant, nach dem Berichtsmuster aus VAL-01
|
||||
// (internal/migration/verify): Abweichung ist entweder erklaert oder
|
||||
// (internal/migrationverify): Abweichung ist entweder erklaert oder
|
||||
// blockierender Befund, nie reine Notiz.
|
||||
package retentionverify
|
||||
|
||||
|
||||
File renamed without changes.
File renamed without changes.
Loaded 100 of 536 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user