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>
158 lines
4.5 KiB
Go
158 lines
4.5 KiB
Go
// Package dossier erzeugt das Nachweis-Dossier (PDF) aus Submission,
|
|
// Findings, Verantwortungsmatrix und Beweiskette. Die Aufbereitung des
|
|
// Inhalts (BuildContent) ist von der eigentlichen PDF-Zeichnung
|
|
// (Render) getrennt, damit die fachliche Logik — was steht wo im
|
|
// Dossier, in welcher Form — ohne PDF-Parsing testbar ist.
|
|
package dossier
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/netcell-it/deklarix/internal/evidence"
|
|
"github.com/netcell-it/deklarix/internal/rules"
|
|
)
|
|
|
|
// Disclaimer steht auf jedem erzeugten Dossier. Siehe CLAUDE.md,
|
|
// Leitplanken: Deklarix ist ein Werkzeug, keine Rechtsberatung.
|
|
const Disclaimer = "Dieses Dossier dokumentiert die durchgeführte Prüfung. " +
|
|
"Es ist keine Rechtsberatung und ersetzt keine anwaltliche Prüfung im Einzelfall."
|
|
|
|
// Submission sind die Basisdaten des geprüften Beitrags.
|
|
type Submission struct {
|
|
Platform string
|
|
PostType string
|
|
Caption string
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
// Participant ist ein Beteiligter aus der Verantwortungsmatrix.
|
|
type Participant struct {
|
|
Role string
|
|
Name string
|
|
Vorgegeben bool
|
|
Freigegeben bool
|
|
}
|
|
|
|
// Data ist die Eingabe für Generate/BuildContent — alles, was ein
|
|
// Dossier für einen Beitrag braucht.
|
|
type Data struct {
|
|
Submission Submission
|
|
Facts rules.Facts
|
|
Findings []rules.Finding
|
|
Participants []Participant
|
|
AssetHash []byte
|
|
MetadataHash []byte
|
|
TimestampToken []byte
|
|
GeneratedAt time.Time
|
|
}
|
|
|
|
// FindingRow ist die dossier-taugliche Aufbereitung eines rules.Finding.
|
|
type FindingRow struct {
|
|
RuleID string
|
|
Version int
|
|
Severity string
|
|
Title string
|
|
Fix string
|
|
Sources []string
|
|
}
|
|
|
|
// ParticipantRow ist die dossier-taugliche Aufbereitung eines Participant.
|
|
type ParticipantRow struct {
|
|
Role string
|
|
Name string
|
|
Vorgegeben bool
|
|
Freigegeben bool
|
|
}
|
|
|
|
// Content ist die fertig aufbereitete, PDF-unabhängige Darstellung
|
|
// eines Dossiers.
|
|
type Content struct {
|
|
Title string
|
|
Platform string
|
|
PostType string
|
|
Caption string
|
|
|
|
SubmittedAt time.Time
|
|
GeneratedAt time.Time
|
|
|
|
DisclosurePresent bool
|
|
DisclosureWording string
|
|
DisclosureBeforeCut bool
|
|
|
|
Findings []FindingRow
|
|
Participants []ParticipantRow
|
|
|
|
AssetHashHex string
|
|
MetadataHashHex string
|
|
TimestampedAt time.Time
|
|
|
|
Disclaimer string
|
|
}
|
|
|
|
// BuildContent bereitet Data zu Content auf. Fehlt eine Pflichtangabe
|
|
// (Plattform, Zeitstempel-Token), wird ein Fehler geliefert statt ein
|
|
// Dossier mit stillschweigend leeren Beweisfeldern zu erzeugen.
|
|
func BuildContent(data Data) (Content, error) {
|
|
if data.Submission.Platform == "" {
|
|
return Content{}, fmt.Errorf("dossier: submission.platform fehlt")
|
|
}
|
|
if len(data.TimestampToken) == 0 {
|
|
return Content{}, fmt.Errorf("dossier: timestamp token 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")
|
|
}
|
|
|
|
timestampedAt, err := evidence.TimestampTime(data.TimestampToken)
|
|
if err != nil {
|
|
return Content{}, fmt.Errorf("dossier: timestamp token: %w", err)
|
|
}
|
|
|
|
findings := make([]FindingRow, len(data.Findings))
|
|
for i, f := range data.Findings {
|
|
findings[i] = FindingRow{
|
|
RuleID: f.RuleID,
|
|
Version: f.RuleVersion,
|
|
Severity: string(f.Severity),
|
|
Title: f.Title,
|
|
Fix: f.Fix,
|
|
Sources: f.Sources,
|
|
}
|
|
}
|
|
|
|
participants := make([]ParticipantRow, len(data.Participants))
|
|
for i, p := range data.Participants {
|
|
participants[i] = ParticipantRow{
|
|
Role: p.Role,
|
|
Name: p.Name,
|
|
Vorgegeben: p.Vorgegeben,
|
|
Freigegeben: p.Freigegeben,
|
|
}
|
|
}
|
|
|
|
return Content{
|
|
Title: "Nachweis-Dossier",
|
|
Platform: data.Submission.Platform,
|
|
PostType: data.Submission.PostType,
|
|
Caption: data.Submission.Caption,
|
|
SubmittedAt: data.Submission.CreatedAt,
|
|
GeneratedAt: data.GeneratedAt,
|
|
DisclosurePresent: data.Facts.DisclosurePresent,
|
|
DisclosureWording: data.Facts.DisclosureWording,
|
|
DisclosureBeforeCut: data.Facts.DisclosureBeforeCut,
|
|
Findings: findings,
|
|
Participants: participants,
|
|
AssetHashHex: hex.EncodeToString(data.AssetHash),
|
|
MetadataHashHex: hex.EncodeToString(data.MetadataHash),
|
|
TimestampedAt: timestampedAt,
|
|
Disclaimer: Disclaimer,
|
|
}, nil
|
|
}
|