feat: wire persistence into the web layer (check → archive → dossier)
This is the piece that turns a Pre-Publish-Prüfung into an actual
archived, provable record instead of a one-off form response.
extract.Client.Extract now returns Result{Facts, RawJSON} instead of
just Facts — RawJSON is the model's exact, unmodified JSON, which is
what belongs in extraction.payload (the audit trail), not a re-encoded
view through our own Facts struct. extract.ParsePayload reconstructs
Facts from a stored payload later, reusing the same parsing/validation
path Extract uses (including the enum guard), so a previously-saved
extraction can be read back exactly as it would have been the first
time.
internal/dossier.BuildContent no longer requires AssetHash: most checks
right now are caption-only (no image/video upload wired yet), and
inventing a placeholder hash for a nonexistent asset would itself be an
integrity problem in an evidence tool. Content shows "kein Asset
hinterlegt" instead.
internal/web gains a narrow Store interface (mirroring the Extractor
pattern — only the methods these handlers use, not the full
*store.Store) so its test suite stays network/DB-free via an in-memory
fake:
- POST /pruefen persists submission + extraction + findings and marks
the submission "checked". A needsClarification result persists the
extraction (there's something worth keeping) but no findings and no
status change, and the template omits the archive option entirely.
- POST /veroeffentlichen re-derives Facts from the stored payload, hashes
the canonical submission+facts+findings metadata, gets an RFC-3161
timestamp, generates the PDF dossier to disk, and persists the
evidence_package row before marking the submission "published".
- GET /dossier/{id} serves the generated PDF.
Tested end-to-end offline: a fake Timestamper builds a real, structurally
valid self-signed RFC-3161 response so the full check→archive→download
flow runs against an in-memory store, verifying the downloaded bytes are
an actual PDF and the dossier file lands on disk — without hitting a
real database, TSA, or the Claude API.
cmd/deklarix/main.go now wires store.Store, evidence.NewHTTPTimestamper
(TSA_URL, default FreeTSA), and DOSSIER_DIR (default "dossiers") into
web.NewServer alongside the extractor and rule set.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -101,9 +101,11 @@ func BuildContent(data Data) (Content, error) {
|
||||
if len(data.TimestampToken) == 0 {
|
||||
return Content{}, fmt.Errorf("dossier: timestamp token fehlt")
|
||||
}
|
||||
if len(data.AssetHash) == 0 {
|
||||
return Content{}, fmt.Errorf("dossier: asset hash fehlt")
|
||||
}
|
||||
// AssetHash ist optional: nicht jede Prüfung hat ein hochgeladenes
|
||||
// Bild/Video (aktuell reine Caption-Prüfungen). Ein erfundener
|
||||
// Platzhalter-Hash für ein nicht existierendes Asset wäre selbst ein
|
||||
// Integritätsproblem — also zeigt das Dossier stattdessen "kein
|
||||
// Asset hinterlegt" an, siehe Render.
|
||||
if len(data.MetadataHash) == 0 {
|
||||
return Content{}, fmt.Errorf("dossier: metadata hash fehlt")
|
||||
}
|
||||
|
||||
@@ -115,20 +115,28 @@ func TestBuildContentRejectsMissingTimestampToken(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildContentRejectsMissingHashes(t *testing.T) {
|
||||
func TestBuildContentRejectsMissingMetadataHash(t *testing.T) {
|
||||
data := validData(t)
|
||||
data.AssetHash = nil
|
||||
if _, err := dossier.BuildContent(data); err == nil {
|
||||
t.Fatal("expected error for missing asset hash, got nil")
|
||||
}
|
||||
|
||||
data = validData(t)
|
||||
data.MetadataHash = nil
|
||||
if _, err := dossier.BuildContent(data); err == nil {
|
||||
t.Fatal("expected error for missing metadata hash, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildContentAllowsMissingAssetHash(t *testing.T) {
|
||||
// Reine Caption-Pruefungen ohne hochgeladenes Bild/Video haben kein
|
||||
// Asset zum Hashen — das ist kein Fehlerfall.
|
||||
data := validData(t)
|
||||
data.AssetHash = nil
|
||||
content, err := dossier.BuildContent(data)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildContent: %v", err)
|
||||
}
|
||||
if content.AssetHashHex != "" {
|
||||
t.Fatalf("AssetHashHex = %q, want empty when no asset was provided", content.AssetHashHex)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildContentSuccess(t *testing.T) {
|
||||
data := validData(t)
|
||||
|
||||
|
||||
@@ -89,7 +89,11 @@ func Render(w io.Writer, c Content) error {
|
||||
pdf.Ln(2)
|
||||
|
||||
heading("Beweiskette")
|
||||
line("Asset-Hash (SHA-256): %s", c.AssetHashHex)
|
||||
if c.AssetHashHex != "" {
|
||||
line("Asset-Hash (SHA-256): %s", c.AssetHashHex)
|
||||
} else {
|
||||
line("Asset-Hash: kein Asset hinterlegt (reine Caption-Prüfung)")
|
||||
}
|
||||
line("Metadaten-Hash (SHA-256): %s", c.MetadataHashHex)
|
||||
line("RFC-3161-Zeitstempel: %s", c.TimestampedAt.Format("02.01.2006 15:04:05 MST"))
|
||||
pdf.Ln(4)
|
||||
|
||||
Reference in New Issue
Block a user