RC-5 Versiopn: 1.6.3
release-tag / release-image (push) Successful in 1m47s

This commit is contained in:
2026-07-22 05:58:38 +02:00
parent 862f86dc3e
commit 133fb10a93
25 changed files with 877 additions and 105 deletions
+64 -7
View File
@@ -13,8 +13,9 @@ import (
)
const (
SchemaVersion = "1.1"
LegacySchemaVersion = "1.0"
SchemaVersion = "1.2"
PreviousSchemaVersion = "1.1"
LegacySchemaVersion = "1.0"
)
var (
@@ -31,8 +32,14 @@ type Component struct {
}
type Responsibility struct {
Name string `json:"name"`
URL string `json:"url,omitempty"`
Assumed bool `json:"assumed,omitempty"`
Role string `json:"role,omitempty"`
Name string `json:"name"`
URL string `json:"url,omitempty"`
}
type LegalContext struct {
Categories []string `json:"categories,omitempty"`
}
type Presentation struct {
@@ -53,6 +60,7 @@ type Declaration struct {
Language string `json:"language"`
Components map[string]Component `json:"components"`
EditorialResponsibility *Responsibility `json:"editorialResponsibility,omitempty"`
LegalContext *LegalContext `json:"legalContext,omitempty"`
Assurance string `json:"assurance"`
Presentation *Presentation `json:"presentation,omitempty"`
}
@@ -90,6 +98,8 @@ var validActivities = map[string]bool{
"transcription": true, "classification": true,
}
var validComponents = map[string]bool{"text": true, "coverImage": true, "image": true, "research": true, "translation": true, "audio": true, "video": true, "code": true, "other": true}
var validLegalCategories = map[string]bool{"deepfake": true, "publicInterestText": true, "artisticCreativeSatiricalFictional": true, "otherVoluntary": true}
var validResponsibilityRoles = map[string]bool{"publisher": true, "other": true}
func NewFromQuery(values url.Values, contextURL string) (Declaration, error) {
return NewFromQueryWithOptions(values, contextURL, ParseOptions{DefaultLanguage: "de"})
@@ -193,8 +203,13 @@ func NewFromQueryWithOptions(values url.Values, contextURL string, options Parse
}
responsibleName := clean(values.Get("responsible"), 200)
responsibleURL := clean(values.Get("responsibleUrl"), 2048)
if responsibleName != "" || responsibleURL != "" {
d.EditorialResponsibility = &Responsibility{Name: responsibleName, URL: responsibleURL}
responsibleRole := clean(values.Get("responsibleRole"), 24)
if responsibleName != "" || responsibleURL != "" || responsibleRole != "" {
d.EditorialResponsibility = &Responsibility{Assumed: responsibleName != "", Role: responsibleRole, Name: responsibleName, URL: responsibleURL}
}
legalCategories := splitCSV(values.Get("legalContext"))
if len(legalCategories) > 0 {
d.LegalContext = &LegalContext{Categories: legalCategories}
}
presentation := &Presentation{
@@ -222,9 +237,16 @@ func Validate(d Declaration) error {
var problems []string
switch d.SchemaVersion {
case SchemaVersion:
case PreviousSchemaVersion:
if d.LegalContext != nil {
problems = append(problems, "legalContext requires schemaVersion 1.2")
}
case LegacySchemaVersion:
if d.Presentation != nil {
problems = append(problems, "presentation requires schemaVersion 1.1")
problems = append(problems, "presentation requires schemaVersion 1.1 or later")
}
if d.LegalContext != nil {
problems = append(problems, "legalContext requires schemaVersion 1.2")
}
if d.Language != "de" && d.Language != "en" {
problems = append(problems, "schemaVersion 1.0 supports only de and en")
@@ -258,12 +280,47 @@ func Validate(d Declaration) error {
if strings.TrimSpace(d.EditorialResponsibility.Name) == "" {
problems = append(problems, "editorialResponsibility.name is required when editorialResponsibility is present")
}
if d.EditorialResponsibility.Role != "" && !validResponsibilityRoles[d.EditorialResponsibility.Role] {
problems = append(problems, "editorialResponsibility.role must be publisher or other")
}
if d.EditorialResponsibility.URL != "" {
if u, err := url.ParseRequestURI(d.EditorialResponsibility.URL); err != nil || u.Scheme == "" || u.Host == "" {
problems = append(problems, "editorialResponsibility.url must be an absolute URL")
}
}
}
if d.LegalContext != nil {
seenLegal := map[string]bool{}
for _, category := range d.LegalContext.Categories {
if !validLegalCategories[category] {
problems = append(problems, "invalid legalContext category: "+category)
}
if seenLegal[category] {
problems = append(problems, "duplicate legalContext category: "+category)
}
seenLegal[category] = true
}
if seenLegal["otherVoluntary"] && len(seenLegal) > 1 {
problems = append(problems, "otherVoluntary cannot be combined with other legalContext categories")
}
if seenLegal["publicInterestText"] {
text, ok := d.Components["text"]
if !ok || text.AIExtent == "none" {
problems = append(problems, "publicInterestText requires a text component with AI involvement")
}
}
if seenLegal["deepfake"] {
hasAIMedia := false
for _, name := range []string{"coverImage", "image", "audio", "video"} {
if c, ok := d.Components[name]; ok && c.AIExtent != "none" {
hasAIMedia = true
}
}
if !hasAIMedia {
problems = append(problems, "deepfake requires an image, audio or video component with AI involvement")
}
}
}
for name, c := range d.Components {
if !validComponents[name] {
problems = append(problems, "invalid component: "+name)
+36
View File
@@ -126,3 +126,39 @@ func TestEditorialResponsibilityRequiresName(t *testing.T) {
t.Fatalf("expected editorial responsibility name validation error, got %v", err)
}
}
func TestLegalContextPublicInterestRequiresAIText(t *testing.T) {
_, err := NewFromQuery(url.Values{
"mode": {"article"}, "lang": {"de"}, "textExtent": {"none"},
"legalContext": {"publicInterestText"},
}, "https://example.org/context/v1")
if err == nil || !strings.Contains(err.Error(), "publicInterestText requires a text component with AI involvement") {
t.Fatalf("expected public-interest consistency error, got %v", err)
}
}
func TestLegalContextDeepfakeRequiresAIMedia(t *testing.T) {
_, err := NewFromQuery(url.Values{
"mode": {"article"}, "lang": {"de"}, "textExtent": {"full"},
"legalContext": {"deepfake"},
}, "https://example.org/context/v1")
if err == nil || !strings.Contains(err.Error(), "deepfake requires an image, audio or video component with AI involvement") {
t.Fatalf("expected deepfake consistency error, got %v", err)
}
}
func TestLegalContextAndResponsibilityAreSerialized(t *testing.T) {
d, err := NewFromQuery(url.Values{
"mode": {"article"}, "lang": {"de"}, "textExtent": {"partial"}, "textReview": {"expert"},
"legalContext": {"publicInterestText"}, "responsibleRole": {"publisher"}, "responsible": {"Beispielredaktion"},
}, "https://example.org/context/v1")
if err != nil {
t.Fatal(err)
}
if d.SchemaVersion != "1.2" || d.LegalContext == nil || len(d.LegalContext.Categories) != 1 {
t.Fatalf("unexpected legal context: %#v", d)
}
if d.EditorialResponsibility == nil || !d.EditorialResponsibility.Assumed || d.EditorialResponsibility.Role != "publisher" {
t.Fatalf("responsibility not explicitly assumed: %#v", d.EditorialResponsibility)
}
}