Add a framework mechanism for prepared modules and validators to contribute checkpoint identity fingerprints
This commit is contained in:
@@ -43,6 +43,7 @@ func referenceSlots() []contracts.ReferenceSlot {
|
||||
}
|
||||
|
||||
var _ contracts.Extractor[dnd.SpellList] = (*Extractor)(nil)
|
||||
var _ pipeline.CheckpointFingerprintProvider = (*Extractor)(nil)
|
||||
|
||||
type Options struct{}
|
||||
|
||||
@@ -109,6 +110,13 @@ func (e *Extractor) ManifestMetadata() map[string]any {
|
||||
return metadata
|
||||
}
|
||||
|
||||
func (e *Extractor) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return []pipeline.CheckpointFingerprint{{Name: "effective_catalog", Value: e.effectiveCatalog.Digest()}}
|
||||
}
|
||||
|
||||
func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRequest) (contracts.TypedExtractionResult[dnd.SpellList], error) {
|
||||
if e == nil {
|
||||
return contracts.TypedExtractionResult[dnd.SpellList]{}, extractorErrorf("extractor must not be nil")
|
||||
|
||||
@@ -117,6 +117,10 @@ func TestExtractPromptUsesCanonicalOverlayNamesWithoutAliasesOrMetadata(t *testi
|
||||
if got, ok := metadata["catalog_overlay_ids"].([]string); !ok || !reflect.DeepEqual(got, []string{"campaign.example"}) {
|
||||
t.Fatalf("catalog overlay metadata = %#v", metadata["catalog_overlay_ids"])
|
||||
}
|
||||
fingerprints := newExtractor(t, &fakeSpellsLLMClient{}, overlaySpellCatalogReference()).CheckpointFingerprints()
|
||||
if len(fingerprints) != 1 || fingerprints[0].Name != "effective_catalog" || fingerprints[0].Value != metadata["catalog_digest"] {
|
||||
t.Fatalf("checkpoint fingerprints = %#v, want manifest catalog digest %#v", fingerprints, metadata["catalog_digest"])
|
||||
}
|
||||
encoded, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{})
|
||||
|
||||
Reference in New Issue
Block a user