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 }