2026-08-03 20:44:44 +00:00
package enemyevents
import (
"context"
"fmt"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
sceneregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/scenedescriptions/registry"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
)
const (
Key = "dnd/enemy-events"
2026-08-28 13:45:21 +00:00
mappingPolicy = "dnd.enemy_events.extract_mapping.v2"
2026-08-03 20:44:44 +00:00
sceneGatePolicy = "dnd.enemy_events.scene_gate.v1"
)
var requiredCapabilities = [ ] string {
"chunks" ,
"source.transcript" ,
}
var providedCapabilities = [ ] string {
"dnd.enemy_events" ,
}
var _ contracts . Extractor [ dnd . EnemyEventList ] = ( * Extractor ) ( nil )
var _ contracts . ManifestMetadataProvider = ( * Extractor ) ( nil )
var _ pipeline . CheckpointFingerprintProvider = ( * Extractor ) ( nil )
type Options struct { }
type Extractor struct {
llm contracts . StructuredLLMClient
grounding * groundingResolver
promptSHA string
responseSchemaSHA string
}
func New ( llmClient contracts . StructuredLLMClient , _ Options , references ... contracts . ReferenceSet ) ( * Extractor , error ) {
if llmClient == nil {
return nil , extractorErrorf ( "LLM client must not be nil" )
}
if len ( references ) > 1 {
return nil , extractorErrorf ( "at most one reference set may be supplied" )
}
var referenceSet contracts . ReferenceSet
if len ( references ) == 1 {
referenceSet = references [ 0 ]
}
grounding , err := newGroundingResolver ( referenceSet )
if err != nil {
return nil , extractorErrorf ( "prepare grounding: %w" , err )
}
promptSHA , err := promptAssetMetadata ( )
if err != nil {
return nil , extractorErrorf ( "load prompt metadata: %w" , err )
}
responseSchema , err := loadResponseSchema ( )
if err != nil {
return nil , extractorErrorf ( "load response schema: %w" , err )
}
return & Extractor {
llm : llmClient ,
grounding : grounding ,
promptSHA : promptSHA ,
responseSchemaSHA : responseSchema . SHA256 ,
} , nil
}
func ( e * Extractor ) Key ( ) string { return Key }
func ( e * Extractor ) ReferenceSlots ( ) [ ] contracts . ReferenceSlot { return referenceSlots ( ) }
func ( e * Extractor ) ManifestMetadata ( ) map [ string ] any {
if e == nil {
return nil
}
return map [ string ] any {
"prompt_id" : PromptID ,
"prompt_version" : SchemaVersion ,
"prompt_sha256" : e . promptSHA ,
"mapping_policy" : mappingPolicy ,
"scene_gate_policy" : sceneGatePolicy ,
"response_schema_key" : string ( ResponseSchemaKey ) ,
"response_schema_id" : ResponseSchemaID ,
"response_schema_name" : ResponseSchemaName ,
"response_schema_version" : SchemaVersion ,
"response_schema_sha256" : e . responseSchemaSHA ,
}
}
func ( e * Extractor ) CheckpointFingerprints ( ) [ ] pipeline . CheckpointFingerprint {
if e == nil {
return nil
}
return [ ] pipeline . CheckpointFingerprint {
{ Name : "prompt" , Value : e . promptSHA } ,
{ Name : "response_schema" , Value : e . responseSchemaSHA } ,
{ Name : "mapping_policy" , Value : mappingPolicy } ,
{ Name : "scene_gate_policy" , Value : sceneGatePolicy } ,
}
}
func ( e * Extractor ) Extract ( ctx context . Context , req contracts . TypedExtractionRequest ) ( contracts . TypedExtractionResult [ dnd . EnemyEventList ] , error ) {
if e == nil {
return contracts . TypedExtractionResult [ dnd . EnemyEventList ] { } , extractorErrorf ( "extractor must not be nil" )
}
if e . llm == nil {
return contracts . TypedExtractionResult [ dnd . EnemyEventList ] { } , extractorErrorf ( "LLM client must not be nil" )
}
if e . grounding == nil {
return contracts . TypedExtractionResult [ dnd . EnemyEventList ] { } , extractorErrorf ( "grounding resolver must not be nil" )
}
sourceInput , err := shared . PrepareChunkExtraction ( ctx , req )
if err != nil {
return contracts . TypedExtractionResult [ dnd . EnemyEventList ] { } , extractorErrorf ( "%w" , err )
}
match , err := e . grounding . SceneMatch ( req . References , req . Chunk )
if err != nil {
return contracts . TypedExtractionResult [ dnd . EnemyEventList ] { } , extractorErrorf ( "resolve scene eligibility: %w" , err )
}
switch match . State {
case sceneregistry . MatchExact :
if match . Kind != dnd . SceneKindCombat {
return emptyResult ( ) , nil
}
case sceneregistry . MatchMissing , sceneregistry . MatchMismatched :
return unavailableSceneResult ( ) , nil
default :
return contracts . TypedExtractionResult [ dnd . EnemyEventList ] { } , extractorErrorf ( "unsupported scene eligibility match state %q" , match . State )
}
grounding , err := e . grounding . Resolve ( req . References )
if err != nil {
return contracts . TypedExtractionResult [ dnd . EnemyEventList ] { } , extractorErrorf ( "resolve enemy-event grounding: %w" , err )
}
inputs := shared . PromptInputs ( sourceInput , req . References )
for name , input := range grounding . PromptInputs ( ) {
inputs [ name ] = input
}
var response extractionResponse
2026-08-26 23:59:05 +00:00
completion , err := e . llm . CompleteStructured ( ctx , contracts . StructuredCompletionRequest {
2026-08-25 19:40:44 +00:00
StageName : Key ,
PromptID : PromptID ,
PromptVersion : SchemaVersion ,
ProfileID : req . LLMProfile ,
SessionID : req . SessionID ,
StructuredOutputRepairAttempts : req . StructuredOutputRepairAttempts ,
2026-08-26 23:59:05 +00:00
Correction : req . Correction ,
2026-08-25 19:40:44 +00:00
Inputs : inputs ,
2026-08-26 23:59:05 +00:00
} , & response )
if err != nil {
2026-08-03 20:44:44 +00:00
return contracts . TypedExtractionResult [ dnd . EnemyEventList ] { } , extractorErrorf ( "complete structured output: %w" , err )
}
2026-08-26 23:59:05 +00:00
candidate , err := shared . ModelCandidateFromResponse ( completion )
if err != nil {
return contracts . TypedExtractionResult [ dnd . EnemyEventList ] { } , extractorErrorf ( "capture model candidate: %w" , err )
}
2026-08-03 20:44:44 +00:00
return contracts . TypedExtractionResult [ dnd . EnemyEventList ] {
2026-08-26 23:59:05 +00:00
Value : canonicalEnemyEventList ( response , shared . NewSourceRefOrder ( req . Source ) , req . Source . ID ) ,
ModelCandidate : candidate ,
2026-08-03 20:44:44 +00:00
} , nil
}
func emptyResult ( ) contracts . TypedExtractionResult [ dnd . EnemyEventList ] {
return contracts . TypedExtractionResult [ dnd . EnemyEventList ] { Value : dnd . EnemyEventList { Events : [ ] dnd . EnemyEvent { } } }
}
func unavailableSceneResult ( ) contracts . TypedExtractionResult [ dnd . EnemyEventList ] {
result := emptyResult ( )
2026-08-27 16:04:32 +00:00
result . Diagnostics = [ ] contracts . ProducerDiagnostic { {
Disposition : contracts . DiagnosticDispositionWarning ,
Category : contracts . DiagnosticCategoryDegradation ,
ReasonCode : "scene_classification_unavailable" ,
OccurrenceCount : 1 ,
Samples : [ ] contracts . DiagnosticSample { { Scope : SceneDescriptionReferenceSlot , Message : "No exact scene classification was available; enemy-event extraction was skipped." } } ,
2026-08-03 20:44:44 +00:00
} }
return result
}
func ModuleSpec ( ) pipeline . ModuleSpec {
return pipeline . ModuleSpec {
2026-08-26 23:59:05 +00:00
Key : Key ,
Stage : pipeline . StageExtract ,
ExecutionClass : contracts . ExecutionClassLLMBacked ,
CorrectionProtocol : contracts . CorrectionProtocolSingleResponseV1 ,
Requires : append ( [ ] string ( nil ) , requiredCapabilities ... ) ,
Provides : append ( [ ] string ( nil ) , providedCapabilities ... ) ,
ArtifactKind : dnd . EnemyEventListKind ,
ReferenceSlots : referenceSlots ( ) ,
2026-08-03 20:44:44 +00:00
}
}
func Register ( registry * pipeline . ExtractorRegistry ) error {
return pipeline . RegisterExtractorBuilder ( registry , ModuleSpec ( ) , validateOptions , func ( request pipeline . BuildRequest ) ( contracts . Extractor [ dnd . EnemyEventList ] , error ) {
options , err := DecodeOptions ( request . Options )
if err != nil {
return nil , err
}
return New ( request . Dependencies . LLM , options , request . References )
} )
}
func validateOptions ( options map [ string ] any ) error {
_ , err := DecodeOptions ( options )
return err
}
func DecodeOptions ( options map [ string ] any ) ( Options , error ) {
if err := pipeline . RejectUnknownOptions ( options ) ; err != nil {
return Options { } , extractorErrorf ( "%w" , err )
}
return Options { } , nil
}
func extractorErrorf ( format string , args ... any ) error {
return fmt . Errorf ( "dnd enemy events extractor: " + format , args ... )
}