Add a framework mechanism for prepared modules and validators to contribute checkpoint identity fingerprints

This commit is contained in:
2026-07-20 15:14:41 -05:00
parent 87c57681f6
commit 0f25e7339f
15 changed files with 539 additions and 26 deletions

View File

@@ -13,9 +13,11 @@ import (
)
const (
Key = "extract/dnd/spells/catalog"
ReasonCode = "unknown_spell"
maxIssues = 20
Key = "extract/dnd/spells/catalog"
ReasonCode = "unknown_spell"
maxIssues = 20
maxDisplayedNameRunes = 128
maxMessageBytes = 4096
)
type Options struct{}
@@ -25,6 +27,7 @@ type Validator struct {
}
var _ contracts.TypedValidator[dnd.SpellList] = (*Validator)(nil)
var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil)
func New(_ Options, references ...contracts.ReferenceSet) (*Validator, error) {
if len(references) > 1 {
@@ -47,6 +50,13 @@ func (v *Validator) ExecutionClass() contracts.ExecutionClass {
return contracts.ExecutionClassDeterministic
}
func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
if v == nil {
return nil
}
return []pipeline.CheckpointFingerprint{{Name: "effective_catalog", Value: v.catalog.Digest()}}
}
func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.SpellList]) (contracts.ValidationResult, error) {
if err := spellshape.Validate(req.Value); err != nil {
return contracts.ValidationResult{Approved: true}, nil
@@ -74,19 +84,37 @@ type unknownSpell struct {
}
func rejection(unknown []unknownSpell) contracts.ValidationResult {
displayed := unknown
if len(displayed) > maxIssues {
displayed = displayed[:maxIssues]
limit := len(unknown)
if limit > maxIssues {
limit = maxIssues
}
issues := make([]string, len(displayed))
for index, item := range displayed {
issues[index] = fmt.Sprintf("spell_casts[%d].spell %q", item.index, item.name)
issues := make([]string, 0, limit)
for _, item := range unknown[:limit] {
issue := fmt.Sprintf("spell_casts[%d].spell %q", item.index, truncateDisplayedName(item.name))
candidate := rejectionMessage(append(issues, issue), len(unknown)-len(issues)-1)
if len(candidate) > maxMessageBytes {
break
}
issues = append(issues, issue)
}
message := fmt.Sprintf("unknown spell names: %s", strings.Join(issues, ", "))
if omitted := len(unknown) - len(displayed); omitted > 0 {
message := rejectionMessage(issues, len(unknown)-len(issues))
return contracts.ValidationResult{Approved: false, ReasonCode: ReasonCode, Message: message}
}
func truncateDisplayedName(name string) string {
runes := []rune(name)
if len(runes) <= maxDisplayedNameRunes {
return name
}
return string(runes[:maxDisplayedNameRunes-1]) + "…"
}
func rejectionMessage(issues []string, omitted int) string {
message := "unknown spell names: " + strings.Join(issues, ", ")
if omitted > 0 {
message += fmt.Sprintf("; %d additional issue(s) omitted", omitted)
}
return contracts.ValidationResult{Approved: false, ReasonCode: ReasonCode, Message: message}
return message
}
func Spec() pipeline.ValidatorSpec {

View File

@@ -6,6 +6,7 @@ import (
"reflect"
"strings"
"testing"
"unicode/utf8"
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
@@ -29,6 +30,17 @@ func TestValidatorApprovesCanonicalNormalizedAndOverlayAliasNames(t *testing.T)
}
}
func TestValidatorCheckpointFingerprintUsesEffectiveCatalogDigest(t *testing.T) {
validator, err := New(Options{}, overlayReferences())
if err != nil {
t.Fatal(err)
}
fingerprints := validator.CheckpointFingerprints()
if len(fingerprints) != 1 || fingerprints[0].Name != "effective_catalog" || fingerprints[0].Value != validator.catalog.Digest() {
t.Fatalf("checkpoint fingerprints = %#v, want effective catalog digest %q", fingerprints, validator.catalog.Digest())
}
}
func TestValidatorRejectsMultipleUnknownCastsInStableOrder(t *testing.T) {
value := spellList("Unknown First", "Cure Wounds", "Unknown Second")
validator, err := New(Options{})
@@ -71,6 +83,50 @@ func TestValidatorBoundsUnknownCastMessage(t *testing.T) {
}
}
func TestValidatorBoundsUnknownSpellNamesAndTotalMessage(t *testing.T) {
longName := strings.Repeat("火", maxDisplayedNameRunes+100) + "\n\t"
value := dnd.SpellList{SpellCasts: make([]dnd.SpellCast, maxIssues)}
for index := range value.SpellCasts {
value.SpellCasts[index] = validCast(fmt.Sprintf("%s-%d", longName, index))
}
validator, err := New(Options{})
if err != nil {
t.Fatal(err)
}
result, err := validator.Validate(context.Background(), validationRequest(value))
if err != nil {
t.Fatal(err)
}
if result.Approved || len([]byte(result.Message)) > maxMessageBytes || !strings.Contains(result.Message, "…") {
t.Fatalf("message length/content = %d/%q, want bounded message with truncation", len([]byte(result.Message)), result.Message)
}
if !strings.Contains(result.Message, "additional issue(s) omitted") {
t.Fatalf("message = %q, want byte-budget omitted count", result.Message)
}
displayed := strings.Count(result.Message, "spell_casts[")
wantOmitted := fmt.Sprintf("%d additional issue(s) omitted", len(value.SpellCasts)-displayed)
if !strings.Contains(result.Message, wantOmitted) {
t.Fatalf("message = %q, want omitted count %q", result.Message, wantOmitted)
}
if !utf8.ValidString(result.Message) {
t.Fatal("bounded message is not valid UTF-8")
}
}
func TestValidatorQuotesControlCharactersInUnknownSpellName(t *testing.T) {
validator, err := New(Options{})
if err != nil {
t.Fatal(err)
}
result, err := validator.Validate(context.Background(), validationRequest(spellList("Unknown\nSpell\tName")))
if err != nil {
t.Fatal(err)
}
if strings.Contains(result.Message, "Unknown\nSpell") || !strings.Contains(result.Message, `Unknown\nSpell\tName`) {
t.Fatalf("message = %q, want safely quoted control characters", result.Message)
}
}
func TestValidatorDefersInvalidShape(t *testing.T) {
value := dnd.SpellList{SpellCasts: []dnd.SpellCast{{Spell: "Unknown Spell"}}}
validator, err := New(Options{})