Add semantic configuration resume evidence

This commit is contained in:
2026-08-30 12:44:26 +00:00
parent b97b12da7f
commit 82cb53e107
13 changed files with 761 additions and 37 deletions

View File

@@ -0,0 +1,86 @@
package stage
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"reflect"
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
)
// SemanticConfigFingerprinter is implemented by stages whose reusable result
// depends on Narratio-observable semantic configuration.
type SemanticConfigFingerprinter interface {
SemanticConfigFingerprint(env *Env) (manifest.SemanticConfigFingerprint, error)
}
// FingerprintSemanticConfig hashes deterministic JSON for a stage-owned typed
// semantic configuration value. Callers must exclude secrets and operational
// settings that cannot affect the canonical result.
func FingerprintSemanticConfig(version int, value any) (manifest.SemanticConfigFingerprint, error) {
record := manifest.SemanticConfigFingerprint{Version: version}
if version <= 0 {
return record, fmt.Errorf("semantic configuration fingerprint version must be positive")
}
typeOf := reflect.TypeOf(value)
if typeOf == nil {
return record, fmt.Errorf("semantic configuration value is nil")
}
valueOf := reflect.ValueOf(value)
if valueOf.Kind() == reflect.Pointer && valueOf.IsNil() {
return record, fmt.Errorf("semantic configuration value is nil")
}
for typeOf.Kind() == reflect.Pointer {
typeOf = typeOf.Elem()
}
if typeOf.Kind() != reflect.Struct {
return record, fmt.Errorf("semantic configuration value must be a typed struct, got %s", typeOf.Kind())
}
if err := validateSemanticConfigType(typeOf, map[reflect.Type]bool{}); err != nil {
return record, err
}
payload, err := json.Marshal(value)
if err != nil {
return record, fmt.Errorf("encode semantic configuration: %w", err)
}
digest := sha256.Sum256(payload)
record.Digest = hex.EncodeToString(digest[:])
return record, nil
}
func validateSemanticConfigType(value reflect.Type, visiting map[reflect.Type]bool) error {
for value.Kind() == reflect.Pointer {
value = value.Elem()
}
if visiting[value] {
return nil
}
visiting[value] = true
defer delete(visiting, value)
switch value.Kind() {
case reflect.Interface:
return fmt.Errorf("semantic configuration structs cannot contain arbitrary interface values")
case reflect.Struct:
for index := 0; index < value.NumField(); index++ {
field := value.Field(index)
if field.PkgPath != "" || field.Tag.Get("json") == "-" {
continue
}
if err := validateSemanticConfigType(field.Type, visiting); err != nil {
return fmt.Errorf("semantic configuration field %s: %w", field.Name, err)
}
}
case reflect.Slice, reflect.Array:
return validateSemanticConfigType(value.Elem(), visiting)
case reflect.Map:
if value.Key().Kind() != reflect.String {
return fmt.Errorf("semantic configuration maps must use string keys")
}
return validateSemanticConfigType(value.Elem(), visiting)
case reflect.Chan, reflect.Func, reflect.UnsafePointer:
return fmt.Errorf("semantic configuration contains unsupported %s value", value.Kind())
}
return nil
}

View File

@@ -0,0 +1,61 @@
package stage
import (
"strings"
"testing"
)
type semanticFingerprintFixture struct {
Model string `json:"model"`
Modules []string `json:"modules"`
Enabled bool `json:"enabled"`
}
func TestFingerprintSemanticConfigIsDeterministicAndTyped(t *testing.T) {
value := semanticFingerprintFixture{Model: "quality", Modules: []string{"speaker", "terms"}, Enabled: true}
first, err := FingerprintSemanticConfig(2, value)
if err != nil {
t.Fatal(err)
}
second, err := FingerprintSemanticConfig(2, &value)
if err != nil {
t.Fatal(err)
}
if !first.Equal(second) || first.Version != 2 || len(first.Digest) != 64 || first.Validate() != nil {
t.Fatalf("fingerprints = %#v / %#v", first, second)
}
changed := value
changed.Model = "testing"
different, err := FingerprintSemanticConfig(2, changed)
if err != nil {
t.Fatal(err)
}
if first.Equal(different) {
t.Fatalf("changed semantic value retained digest %q", first.Digest)
}
}
func TestFingerprintSemanticConfigRejectsInvalidInputs(t *testing.T) {
for _, test := range []struct {
name string
version int
value any
want string
}{
{name: "version", value: semanticFingerprintFixture{}, want: "positive"},
{name: "nil", version: 1, value: nil, want: "nil"},
{name: "typed nil", version: 1, value: (*semanticFingerprintFixture)(nil), want: "nil"},
{name: "untyped map", version: 1, value: map[string]any{"model": "quality"}, want: "typed struct"},
{name: "arbitrary field", version: 1, value: struct {
Values map[string]any `json:"values"`
}{Values: map[string]any{"model": "quality"}}, want: "arbitrary interface"},
{name: "slice", version: 1, value: []string{"quality"}, want: "typed struct"},
} {
t.Run(test.name, func(t *testing.T) {
_, err := FingerprintSemanticConfig(test.version, test.value)
if err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("error = %v, want %q", err, test.want)
}
})
}
}