Files
notarius/internal/framework/semanticreconcile/identity.go

102 lines
3.7 KiB
Go

package semanticreconcile
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"strconv"
"strings"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
)
// Policy identifies the framework-owned reconciliation and assessment rules.
const Policy = "semantic_reconciliation.v1"
var _ contracts.ManifestMetadataProvider = (*Engine)(nil)
var _ pipeline.CheckpointFingerprintProvider = (*Engine)(nil)
// ManifestMetadata returns fresh, content-free identity for the complete core
// reconciliation mechanism.
func (engine *Engine) ManifestMetadata() map[string]any {
if engine == nil || engine.validate() != nil {
return nil
}
return map[string]any{
"prompt_id": engine.prompt.ID,
"prompt_version": engine.prompt.Version,
"prompt_sha256": engine.prompt.SHA256,
"response_schema_key": string(engine.schema.Key),
"response_schema_id": engine.schema.ID,
"response_schema_name": engine.schema.Name,
"response_schema_version": engine.schema.Version,
"response_schema_sha256": engine.schema.SHA256,
"semantic_reconciliation_policy": Policy,
"semantic_reconciliation_limits": map[string]any{
"context_radius": engine.limits.ContextRadius,
"maximum_candidates": engine.limits.MaximumCandidates,
"maximum_material_bytes": engine.limits.MaximumMaterialBytes,
},
}
}
// CheckpointFingerprints returns fresh canonical identities for prompt,
// schema, reconciliation policy, and the complete limit policy.
func (engine *Engine) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
if engine == nil || engine.validate() != nil {
return nil
}
return []pipeline.CheckpointFingerprint{
{Name: "prompt", Value: identityDigest(engine.prompt.ID, engine.prompt.Version, engine.prompt.SHA256)},
{Name: "response_schema", Value: identityDigest(string(engine.schema.Key), engine.schema.ID, engine.schema.Version, engine.schema.Name, engine.schema.SHA256)},
{Name: "semantic_reconciliation_policy", Value: Policy},
{Name: "semantic_reconciliation_limits", Value: limitPolicyDigest(engine.limits)},
}
}
func limitPolicyDigest(limits Limits) string {
return identityDigest(
strconv.Itoa(limits.ContextRadius),
strconv.Itoa(limits.MaximumCandidates),
strconv.Itoa(limits.MaximumMaterialBytes),
)
}
func identityDigest(parts ...string) string {
hash := sha256.New()
for _, part := range parts {
_, _ = hash.Write([]byte(strconv.Itoa(len(part))))
_, _ = hash.Write([]byte{':'})
_, _ = hash.Write([]byte(part))
}
return "sha256:" + hex.EncodeToString(hash.Sum(nil))
}
func validateResponseSchemaIdentity(schema llm.ResponseSchema) error {
if strings.TrimSpace(string(schema.Key)) == "" || strings.TrimSpace(schema.ID) == "" || strings.TrimSpace(schema.Version) == "" || strings.TrimSpace(schema.Name) == "" {
return fmt.Errorf("response schema identity must be complete")
}
if err := validateSHA256(schema.SHA256); err != nil {
return fmt.Errorf("response schema digest: %w", err)
}
return nil
}
func validateSHA256(value string) error {
const prefix = "sha256:"
if !strings.HasPrefix(value, prefix) {
return fmt.Errorf("must use sha256: prefix")
}
hexValue := strings.TrimPrefix(value, prefix)
if len(hexValue) != sha256.Size*2 || hexValue != strings.ToLower(hexValue) {
return fmt.Errorf("must contain 64 lowercase hexadecimal characters")
}
decoded, err := hex.DecodeString(hexValue)
if err != nil || len(decoded) != sha256.Size {
return fmt.Errorf("must contain 64 lowercase hexadecimal characters")
}
return nil
}