Add shared semantic reconciliation engine
This commit is contained in:
101
internal/framework/semanticreconcile/identity.go
Normal file
101
internal/framework/semanticreconcile/identity.go
Normal file
@@ -0,0 +1,101 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user