package cli import ( "context" "encoding/json" "fmt" "reflect" "sort" "strings" "sync" "testing" "gitea.maximumdirect.net/eric/notarius/internal/core/artifacts" "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" spellnormalize "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/normalize/spells" spellcatalog "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/spells/catalog" ) const assembledSpellExtractorKey = "test/dnd/spell-casts" const assembledCorrectingSpellExtractorKey = "test/dnd/correcting-spell-casts" const assembledDirectSpellValidatorKey = "test/dnd/direct-spell-correction" func TestAssembledSpellPipelineNormalizesMergedCasts(t *testing.T) { registries, resolved, extractor := assembledSpellPipeline(t, assembledSpellPipelineOptions{}) prepared, err := pipeline.Prepare(resolved, registries, pipeline.ModuleDependencies{}) if err != nil { t.Fatalf("Prepare() error = %v, want nil", err) } output, err := pipeline.New().Run(context.Background(), pipeline.RunInput{ Prepared: prepared, RawInput: readRepositoryFile(t, "examples", "seriatim-minimal-transcript.json"), ChunkCacheMode: pipeline.ChunkCacheBypass, }) if err != nil { t.Fatalf("Run() error = %v, want nil", err) } chunkIndexes := extractor.chunkIndexesSnapshot() sort.Ints(chunkIndexes) if !reflect.DeepEqual(chunkIndexes, []int{0, 1}) { t.Fatalf("extractor chunk indexes = %#v, want two chunk-boundary calls", chunkIndexes) } if output.Manifest.ValidationStatus != "approved" || len(output.Rejected) != 0 || len(output.NormalizeOutputs) != 1 { t.Fatalf("run output = %#v, want approved normalized output without rejections", output) } if output.NormalizeOutputs[0].NormalizerKey != spellnormalize.Key { t.Fatalf("normalized output module = %q, want %q", output.NormalizeOutputs[0].NormalizerKey, spellnormalize.Key) } var normalized dnd.SpellList if err := json.Unmarshal(output.NormalizeOutputs[0].Artifact.Content, &normalized); err != nil { t.Fatalf("decode normalized output: %v", err) } if len(normalized.SpellCasts) != 2 { t.Fatalf("normalized casts = %#v, want collapsed duplicate plus distinct evidence", normalized.SpellCasts) } first, distinct := normalized.SpellCasts[0], normalized.SpellCasts[1] if first.Spell != "Cure Wounds" || first.Caster != " Aria \t" { t.Fatalf("retained cast = %#v, want canonical spell with first occurrence caster", first) } if !reflect.DeepEqual(first.SourceRefs, []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 1, EndUnitID: 1}, {SourceID: "session-alpha", StartUnitID: 2, EndUnitID: 2}}) { t.Fatalf("retained refs = %#v, want sorted complete evidence", first.SourceRefs) } if distinct.Spell != "Cure Wounds" || distinct.Caster != "aria" || !reflect.DeepEqual(distinct.SourceRefs, []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 2, EndUnitID: 2}}) { t.Fatalf("distinct cast = %#v, want separate evidence event", distinct) } wantDiagnosticReasons := []string{ spellnormalize.ReasonCodeSpellNameCanonicalized, spellnormalize.ReasonCodeSourceReferencesNormalized, spellnormalize.ReasonCodeDuplicateSpellCastCollapsed, "spell_not_near_source", } gotDiagnosticReasons := make([]string, len(output.Diagnostics.Groups)) for index, group := range output.Diagnostics.Groups { gotDiagnosticReasons[index] = group.ReasonCode } if !reflect.DeepEqual(gotDiagnosticReasons, wantDiagnosticReasons) { t.Fatalf("diagnostics = %#v, want deterministic normalize and validation diagnostics", output.Diagnostics) } if output.Diagnostics.Groups[2].Samples[0].Scope != "spell_casts[0]" || !strings.Contains(output.Diagnostics.Groups[2].Samples[0].Message, "retained input index 0") || !strings.Contains(output.Diagnostics.Groups[2].Samples[0].Message, "removed input indices [1]") { t.Fatalf("duplicate diagnostic = %#v, want retained and removed merged indices", output.Diagnostics.Groups[2]) } warningsFile := decodeAssembledOutput[struct { Groups []contracts.DiagnosticGroup `json:"groups"` }](t, output.OutputFiles, "warnings.json") if len(warningsFile.Groups) != 0 { t.Fatalf("warnings file = %#v, want no process warnings for advisory-only diagnostics", warningsFile.Groups) } diagnosticsFile := decodeAssembledOutput[struct { SchemaVersion string `json:"schema_version"` GroupCount int `json:"group_count"` OccurrenceCount int `json:"occurrence_count"` Truncated bool `json:"truncated"` UnrepresentedOccurrenceCount int `json:"unrepresented_occurrence_count"` Groups []contracts.DiagnosticGroup `json:"groups"` }](t, output.OutputFiles, "diagnostics.json") if diagnosticsFile.SchemaVersion != "notarius.diagnostics.v1" || diagnosticsFile.GroupCount != len(output.Diagnostics.Groups) || !reflect.DeepEqual(diagnosticsFile.Groups, output.Diagnostics.Groups) || diagnosticsFile.OccurrenceCount != diagnosticGroupOccurrences(output.Diagnostics.Groups)+output.Diagnostics.UnrepresentedOccurrenceCount || diagnosticsFile.Truncated != output.Diagnostics.Truncated || diagnosticsFile.UnrepresentedOccurrenceCount != output.Diagnostics.UnrepresentedOccurrenceCount { t.Fatalf("diagnostics file = %#v, run diagnostics = %#v", diagnosticsFile, output.Diagnostics) } manifest := decodeAssembledOutput[artifacts.RunManifest](t, output.OutputFiles, "manifest.json") if len(manifest.ArtifactLanes) != 1 || manifest.ArtifactLanes[0].Normalizer != spellnormalize.Key { t.Fatalf("manifest lanes = %#v, want assembled spell normalizer", manifest.ArtifactLanes) } normalizerMetadata, ok := manifest.ArtifactLanes[0].Metadata["normalizer"].(map[string]any) _, hasOverlayIDs := normalizerMetadata["catalog_overlay_ids"] if !ok || normalizerMetadata["catalog_base_id"] != spellcatalog.SRD5E2014ID || !strings.HasPrefix(stringValue(normalizerMetadata["catalog_digest"]), "sha256:") || !hasOverlayIDs { t.Fatalf("normalizer manifest metadata = %#v, want base ID, digest, and overlay IDs", manifest.ArtifactLanes[0].Metadata) } } func TestAssembledSpellPipelineCorrectsRejectedDirectExtraction(t *testing.T) { registries, resolved, extractor := assembledCorrectingSpellPipeline(t) prepared, err := pipeline.Prepare(resolved, registries, pipeline.ModuleDependencies{}) if err != nil { t.Fatalf("Prepare() error = %v, want nil", err) } output, err := pipeline.New().Run(context.Background(), pipeline.RunInput{ Prepared: prepared, RawInput: readRepositoryFile(t, "examples", "seriatim-minimal-transcript.json"), ChunkCacheMode: pipeline.ChunkCacheBypass, }) if err != nil { t.Fatalf("Run() error = %v, want nil", err) } if len(output.Rejected) != 0 || output.Manifest.ValidationStatus != "approved" || len(output.NormalizeOutputs) != 1 { t.Fatalf("run output = %#v, want corrected accepted spell output", output) } correction := extractor.correctionSnapshot() if correction == nil || string(correction.AssistantResponse) != `{"spell":"Mysterious Burst"}` || !strings.Contains(correction.UserGuidance, "use a known spell name") || !strings.Contains(correction.UserGuidance, "complete corrected replacement") || strings.Contains(correction.UserGuidance, "unknown_spell") || strings.Contains(correction.UserGuidance, "spell is not in the catalog") { t.Fatalf("extract correction = %#v, want exact rejected model response and semantic replacement guidance only", correction) } } func TestAssembledSpellPipelineHonorsNormalizeValidatorOverride(t *testing.T) { registries, resolved, _ := assembledSpellPipeline(t, assembledSpellPipelineOptions{normalizeValidatorOverride: true}) var normalizeChain *pipeline.ResolvedValidatorChain for index := range resolved.ValidatorChains { chain := &resolved.ValidatorChains[index] if chain.Stage == pipeline.StageNormalize && chain.ModuleKey == spellnormalize.Key && chain.LaneID == "spells" { normalizeChain = chain break } } if normalizeChain == nil || len(normalizeChain.Validators) != 1 || normalizeChain.Validators[0].Binding.Module != "generic/always_accept" { t.Fatalf("normalize validator chain = %#v, want explicit always-accept override", normalizeChain) } prepared, err := pipeline.Prepare(resolved, registries, pipeline.ModuleDependencies{}) if err != nil { t.Fatalf("Prepare() error = %v, want nil", err) } output, err := pipeline.New().Run(context.Background(), pipeline.RunInput{ Prepared: prepared, RawInput: readRepositoryFile(t, "examples", "seriatim-minimal-transcript.json"), ChunkCacheMode: pipeline.ChunkCacheBypass, }) if err != nil || output.Manifest.ValidationStatus != "approved" || len(output.Rejected) != 0 || len(output.NormalizeOutputs) != 1 { t.Fatalf("Run() error = %v output = %#v, want approved override run", err, output) } for _, group := range output.Diagnostics.Groups { if group.ReasonCode == "spell_not_near_source" { t.Fatalf("diagnostics = %#v, want explicit validator override to replace default relatedness chain", output.Diagnostics) } } } func TestAssembledSpellPipelinePromotesTerminalUnknownSpellDiagnostics(t *testing.T) { registries, resolved, _ := assembledSpellPipeline(t, assembledSpellPipelineOptions{unknownSpell: true}) resolved.Steps[0].ArtifactLanes[0].NormalizeValidationPolicy.SemanticRejection = pipeline.SemanticRejectionRejectOutput prepared, err := pipeline.Prepare(resolved, registries, pipeline.ModuleDependencies{}) if err != nil { t.Fatalf("Prepare() error = %v, want nil", err) } output, err := pipeline.New().Run(context.Background(), pipeline.RunInput{ Prepared: prepared, RawInput: readRepositoryFile(t, "examples", "seriatim-minimal-transcript.json"), ChunkCacheMode: pipeline.ChunkCacheBypass, }) if err != nil { t.Fatalf("Run() error = %v, want nil", err) } if output.Manifest.ValidationStatus != "rejected" || len(output.NormalizeOutputs) != 0 || len(output.Rejected) != 1 { t.Fatalf("run output = %#v, want one rejected normalize candidate and no normalized output", output) } rejection := output.Rejected[0] if rejection.Stage != string(pipeline.StageNormalize) || rejection.LaneID != "spells" || rejection.ModuleKey != spellnormalize.Key || rejection.ValidatorName != "extract/dnd/spells/catalog" || rejection.ReasonCode != "unknown_spell" { t.Fatalf("rejection = %#v, want durable normalize catalog rejection", rejection) } rejectedFile := decodeAssembledOutput[struct { Rejected []contracts.RejectedOutput `json:"rejected"` }](t, output.OutputFiles, "rejected.json") if !reflect.DeepEqual(rejectedFile.Rejected, output.Rejected) { t.Fatalf("rejected file = %#v, run rejections = %#v, want durable rejection diagnostic", rejectedFile.Rejected, output.Rejected) } if len(output.Diagnostics.Groups) != 2 || output.Diagnostics.Groups[0].ReasonCode != spellnormalize.ReasonCodeSpellNameUnresolved || output.Diagnostics.Groups[0].Samples[0].Scope != "spell_casts[0]" || output.Diagnostics.Groups[1].ReasonCode != "spell_not_near_source" { t.Fatalf("diagnostics = %#v, want complete terminal normalize validation diagnostics", output.Diagnostics) } } func TestAssembledSpellPipelinePromotesUnknownSpellAdvisoryWhenOverrideAccepts(t *testing.T) { registries, resolved, _ := assembledSpellPipeline(t, assembledSpellPipelineOptions{normalizeValidatorOverride: true, unknownSpell: true}) prepared, err := pipeline.Prepare(resolved, registries, pipeline.ModuleDependencies{}) if err != nil { t.Fatalf("Prepare() error = %v, want nil", err) } output, err := pipeline.New().Run(context.Background(), pipeline.RunInput{ Prepared: prepared, RawInput: readRepositoryFile(t, "examples", "seriatim-minimal-transcript.json"), ChunkCacheMode: pipeline.ChunkCacheBypass, }) if err != nil { t.Fatalf("Run() error = %v, want nil", err) } if output.Manifest.ValidationStatus != "approved" || len(output.Rejected) != 0 || len(output.NormalizeOutputs) != 1 { t.Fatalf("run output = %#v, want accepted unknown spell with explicit validator override", output) } var normalized dnd.SpellList if err := json.Unmarshal(output.NormalizeOutputs[0].Artifact.Content, &normalized); err != nil { t.Fatalf("decode normalized output: %v", err) } if len(normalized.SpellCasts) != 1 || normalized.SpellCasts[0].Spell != "Mysterious Burst" { t.Fatalf("normalized casts = %#v, want unresolved name preserved", normalized.SpellCasts) } if len(output.Diagnostics.Groups) != 1 || output.Diagnostics.Groups[0].ReasonCode != spellnormalize.ReasonCodeSpellNameUnresolved || output.Diagnostics.Groups[0].Samples[0].Scope != "spell_casts[0]" { t.Fatalf("diagnostics = %#v, want promoted scoped unresolved-name diagnostic", output.Diagnostics) } warningsFile := decodeAssembledOutput[struct { Groups []contracts.DiagnosticGroup `json:"groups"` }](t, output.OutputFiles, "warnings.json") if len(warningsFile.Groups) != 0 { t.Fatalf("warnings file = %#v, want no process warnings for an advisory diagnostic", warningsFile.Groups) } diagnosticsFile := decodeAssembledOutput[struct { Groups []contracts.DiagnosticGroup `json:"groups"` }](t, output.OutputFiles, "diagnostics.json") if !reflect.DeepEqual(diagnosticsFile.Groups, output.Diagnostics.Groups) { t.Fatalf("diagnostics file = %#v, run diagnostics = %#v", diagnosticsFile.Groups, output.Diagnostics) } } type assembledSpellPipelineOptions struct { normalizeValidatorOverride bool unknownSpell bool } func assembledCorrectingSpellPipeline(t *testing.T) (pipeline.Registries, pipeline.ResolvedPipeline, *assembledCorrectingSpellExtractor) { t.Helper() components := productionTestComponents(t) extractor := &assembledCorrectingSpellExtractor{} if err := pipeline.RegisterExtractor[dnd.SpellList](components.registries.Extractors, pipeline.ModuleSpec{ Key: assembledCorrectingSpellExtractorKey, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked, CorrectionProtocol: contracts.CorrectionProtocolSingleResponseV1, Requires: []string{"chunks", "source.transcript"}, Provides: []string{"dnd.spell_casts"}, ArtifactKind: dnd.SpellListKind, }, func() (contracts.Extractor[dnd.SpellList], error) { return extractor, nil }); err != nil { t.Fatalf("register correcting extractor: %v", err) } if err := pipeline.RegisterTypedValidator[dnd.SpellList](components.registries.Validators, dnd.SpellListKind, pipeline.ValidatorSpec{Key: assembledDirectSpellValidatorKey, ExecutionClass: contracts.ExecutionClassDeterministic}, func() (contracts.TypedValidator[dnd.SpellList], error) { return assembledDirectSpellValidator{}, nil }); err != nil { t.Fatalf("register direct spell validator: %v", err) } extract := pipeline.Binding(assembledCorrectingSpellExtractorKey) extract.Retries = 1 extract.Validators = pipeline.ValidatorOverride{Set: true, Validators: []pipeline.ModuleBinding{{Module: assembledDirectSpellValidatorKey}}} resolved, err := pipeline.ResolvePipeline(pipeline.PipelineProfile{ ID: "assembled-dnd-correcting-spells", Input: pipeline.Binding("seriatim"), Chunk: pipeline.ModuleBinding{Module: "generic", Options: map[string]any{"max_units": 1}}, Artifacts: map[string]pipeline.ArtifactLaneProfile{ "spells": {Extract: extract, Normalize: pipeline.Binding(spellnormalize.Key)}, }, Output: pipeline.Binding("json"), }, pipeline.ResolveOptions{}, catalogFromRegistries(components.registries)) if err != nil { t.Fatalf("ResolvePipeline() error = %v, want nil", err) } return components.registries, resolved, extractor } func assembledSpellPipeline(t *testing.T, options assembledSpellPipelineOptions) (pipeline.Registries, pipeline.ResolvedPipeline, *assembledSpellExtractor) { t.Helper() components := productionTestComponents(t) extractor := &assembledSpellExtractor{unknownSpell: options.unknownSpell} if err := pipeline.RegisterExtractor[dnd.SpellList](components.registries.Extractors, pipeline.ModuleSpec{ Key: assembledSpellExtractorKey, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassDeterministic, Requires: []string{"chunks", "source.transcript"}, Provides: []string{"dnd.spell_casts"}, ArtifactKind: dnd.SpellListKind, }, func() (contracts.Extractor[dnd.SpellList], error) { return extractor, nil }); err != nil { t.Fatalf("register assembled extractor: %v", err) } normalize := pipeline.Binding(spellnormalize.Key) if options.normalizeValidatorOverride { normalize.Validators = pipeline.ValidatorOverride{ Set: true, Validators: []pipeline.ModuleBinding{pipeline.Binding("generic/always_accept")}, } } resolved, err := pipeline.ResolvePipeline(pipeline.PipelineProfile{ ID: "assembled-dnd-spells", Input: pipeline.Binding("seriatim"), Chunk: pipeline.ModuleBinding{Module: "generic", Options: map[string]any{"max_units": 1}}, Artifacts: map[string]pipeline.ArtifactLaneProfile{ "spells": {Extract: pipeline.Binding(assembledSpellExtractorKey), Normalize: normalize}, }, Output: pipeline.Binding("json"), }, pipeline.ResolveOptions{}, catalogFromRegistries(components.registries)) if err != nil { t.Fatalf("ResolvePipeline() error = %v, want nil", err) } return components.registries, resolved, extractor } type assembledSpellExtractor struct { mu sync.Mutex chunkIndexes []int unknownSpell bool } type assembledCorrectingSpellExtractor struct { mu sync.Mutex correction *contracts.SemanticCorrection } func (*assembledCorrectingSpellExtractor) Key() string { return assembledCorrectingSpellExtractorKey } func (*assembledCorrectingSpellExtractor) ReferenceSlots() []contracts.ReferenceSlot { return nil } func (e *assembledCorrectingSpellExtractor) Extract(_ context.Context, req contracts.TypedExtractionRequest) (contracts.TypedExtractionResult[dnd.SpellList], error) { if req.Source == nil || req.Chunk == nil { return contracts.TypedExtractionResult[dnd.SpellList]{}, fmt.Errorf("correcting assembled extractor requires source and chunk") } response := `{"spell":"accepted"}` value := dnd.SpellList{SpellCasts: []dnd.SpellCast{}} if req.Chunk.Index == 0 && req.Correction == nil { response = `{"spell":"Mysterious Burst"}` value.SpellCasts = []dnd.SpellCast{{Caster: "Aria", Spell: "Mysterious Burst", SourceRefs: []source.SourceRef{{SourceID: req.Source.ID, StartUnitID: 1, EndUnitID: 1}}}} } if req.Chunk.Index == 0 && req.Correction != nil { correction, err := contracts.CloneSemanticCorrection(req.Correction) if err != nil { return contracts.TypedExtractionResult[dnd.SpellList]{}, err } e.mu.Lock() e.correction = correction e.mu.Unlock() value.SpellCasts = []dnd.SpellCast{{Caster: "Aria", Spell: "Cure Wounds", SourceRefs: []source.SourceRef{{SourceID: req.Source.ID, StartUnitID: 1, EndUnitID: 1}}}} } candidate, err := contracts.NewModelCandidate([]byte(response), contracts.CorrectionProtocolSingleResponseV1) if err != nil { return contracts.TypedExtractionResult[dnd.SpellList]{}, err } return contracts.TypedExtractionResult[dnd.SpellList]{Value: value, ModelCandidate: candidate}, nil } func (e *assembledCorrectingSpellExtractor) correctionSnapshot() *contracts.SemanticCorrection { e.mu.Lock() defer e.mu.Unlock() correction, err := contracts.CloneSemanticCorrection(e.correction) if err != nil { return nil } return correction } type assembledDirectSpellValidator struct{} func (assembledDirectSpellValidator) Name() string { return assembledDirectSpellValidatorKey } func (assembledDirectSpellValidator) ExecutionClass() contracts.ExecutionClass { return contracts.ExecutionClassDeterministic } func (assembledDirectSpellValidator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.SpellList]) (contracts.ValidationResult, error) { for _, cast := range req.Value.SpellCasts { if cast.Spell == "Mysterious Burst" { return contracts.ValidationResult{Approved: false, ReasonCode: "unknown_spell", Message: "spell is not in the catalog", CorrectionGuidance: "use a known spell name"}, nil } } return contracts.ValidationResult{Approved: true}, nil } func (e *assembledSpellExtractor) Key() string { return assembledSpellExtractorKey } func (*assembledSpellExtractor) ReferenceSlots() []contracts.ReferenceSlot { return nil } func (e *assembledSpellExtractor) Extract(ctx context.Context, req contracts.TypedExtractionRequest) (contracts.TypedExtractionResult[dnd.SpellList], error) { if err := ctx.Err(); err != nil { return contracts.TypedExtractionResult[dnd.SpellList]{}, err } if req.Chunk == nil || req.Source == nil { return contracts.TypedExtractionResult[dnd.SpellList]{}, fmt.Errorf("assembled extractor requires source and chunk") } e.mu.Lock() e.chunkIndexes = append(e.chunkIndexes, req.Chunk.Index) e.mu.Unlock() refOne := source.SourceRef{SourceID: req.Source.ID, StartUnitID: 1, EndUnitID: 1} refTwo := source.SourceRef{SourceID: req.Source.ID, StartUnitID: 2, EndUnitID: 2} if e.unknownSpell { if req.Chunk.Index == 0 { return contracts.TypedExtractionResult[dnd.SpellList]{Value: dnd.SpellList{SpellCasts: []dnd.SpellCast{{ Caster: "Aria", Spell: "Mysterious Burst", SourceRefs: []source.SourceRef{refOne}, }}}}, nil } return contracts.TypedExtractionResult[dnd.SpellList]{Value: dnd.SpellList{SpellCasts: []dnd.SpellCast{}}}, nil } switch req.Chunk.Index { case 0: return contracts.TypedExtractionResult[dnd.SpellList]{Value: dnd.SpellList{SpellCasts: []dnd.SpellCast{{ Caster: " Aria \t", Spell: " cure wounds ", SourceRefs: []source.SourceRef{refTwo, refOne}, }}}}, nil case 1: return contracts.TypedExtractionResult[dnd.SpellList]{Value: dnd.SpellList{SpellCasts: []dnd.SpellCast{ {Caster: "aria", Spell: "Cure Wounds", SourceRefs: []source.SourceRef{refOne, refTwo}}, {Caster: "aria", Spell: "Cure Wounds", SourceRefs: []source.SourceRef{refTwo}}, }}}, nil default: return contracts.TypedExtractionResult[dnd.SpellList]{}, fmt.Errorf("unexpected assembled chunk index %d", req.Chunk.Index) } } func (e *assembledSpellExtractor) chunkIndexesSnapshot() []int { e.mu.Lock() defer e.mu.Unlock() return append([]int(nil), e.chunkIndexes...) } func diagnosticGroupOccurrences(groups []contracts.DiagnosticGroup) int { count := 0 for _, group := range groups { count += group.OccurrenceCount } return count } func decodeAssembledOutput[T any](t *testing.T, files []contracts.OutputFile, name string) T { t.Helper() for _, file := range files { if file.Name != name { continue } var value T if err := json.Unmarshal(file.Bytes, &value); err != nil { t.Fatalf("decode %s: %v", name, err) } return value } t.Fatalf("output files = %#v, want %q", files, name) return *new(T) }