diff --git a/internal/cli/spell_catalog_identity_contract_test.go b/internal/cli/spell_catalog_identity_contract_test.go index b98dc12..3e1558f 100644 --- a/internal/cli/spell_catalog_identity_contract_test.go +++ b/internal/cli/spell_catalog_identity_contract_test.go @@ -263,12 +263,25 @@ func TestChangedSemanticSpellCatalogFingerprintCannotResumeRecordedCheckpoint(t if _, decision := changedLoader.Normalize("spells", spellnormalize.Key, normalizeDependencies); decision.Reused { t.Fatalf("changed normalize fingerprint decision = %#v, want normalize checkpoint cold miss", decision) } + changedMapping := replaceCheckpointFingerprintValue(t, fingerprints, extractSpellMappingFingerprintName(), "dnd.spells.extract_mapping.v2") + assertOnlyCheckpointFingerprintChanged(t, fingerprints, changedMapping, extractSpellMappingFingerprintName()) + _, mappingLoader, err := checkpointHandlersForRun(settings, Options{}, materialized, changedMapping, []byte("same input"), nil, nil, "", "", true) + if err != nil { + t.Fatal(err) + } + if _, decision := mappingLoader.Source(materialized.Input.Module); decision.Reused { + t.Fatalf("changed mapping policy decision = %#v, want cold miss", decision) + } } func normalizeSpellCatalogFingerprintName() string { return "normalize:spells:" + spellnormalize.Key + ":effective_catalog" } +func extractSpellMappingFingerprintName() string { + return "extract:spells:" + spells.Key + ":mapping_policy" +} + func replaceCheckpointFingerprintValue(t *testing.T, fingerprints []pipeline.CheckpointFingerprint, name, value string) []pipeline.CheckpointFingerprint { t.Helper() changed := append([]pipeline.CheckpointFingerprint(nil), fingerprints...) diff --git a/internal/modules/dnd/extract/npcs/extractor.go b/internal/modules/dnd/extract/npcs/extractor.go index 4201105..60946a6 100644 --- a/internal/modules/dnd/extract/npcs/extractor.go +++ b/internal/modules/dnd/extract/npcs/extractor.go @@ -13,6 +13,8 @@ import ( const Key = "dnd/npcs" +const mappingPolicy = "dnd.npcs.extract_mapping.v1" + var requiredCapabilities = []string{ "chunks", "source.transcript", @@ -85,6 +87,7 @@ func (e *Extractor) ManifestMetadata() map[string]any { "response_schema_version": SchemaVersion, "response_schema_sha256": e.responseSchemaSHA, "identity_policy": identity.Policy, + "mapping_policy": mappingPolicy, } } @@ -96,6 +99,7 @@ func (e *Extractor) CheckpointFingerprints() []pipeline.CheckpointFingerprint { {Name: "prompt", Value: e.promptSHA}, {Name: "response_schema", Value: e.responseSchemaSHA}, {Name: "identity_policy", Value: identity.Policy}, + {Name: "mapping_policy", Value: mappingPolicy}, } } diff --git a/internal/modules/dnd/extract/npcs/registry_test.go b/internal/modules/dnd/extract/npcs/registry_test.go index 3e6dbdc..9cab7f4 100644 --- a/internal/modules/dnd/extract/npcs/registry_test.go +++ b/internal/modules/dnd/extract/npcs/registry_test.go @@ -77,6 +77,7 @@ func TestExtractorMetadataAndCheckpointIdentity(t *testing.T) { "response_schema_key": string(ResponseSchemaKey), "response_schema_id": ResponseSchemaID, "response_schema_name": ResponseSchemaName, "response_schema_version": SchemaVersion, "identity_policy": "dnd.npcs.identity.v1", + "mapping_policy": mappingPolicy, } { if metadata[key] != want { t.Fatalf("metadata[%q] = %#v, want %q", key, metadata[key], want) @@ -88,7 +89,7 @@ func TestExtractorMetadataAndCheckpointIdentity(t *testing.T) { } } fingerprints := extractor.CheckpointFingerprints() - want := map[string]string{"prompt": metadata["prompt_sha256"].(string), "response_schema": metadata["response_schema_sha256"].(string), "identity_policy": "dnd.npcs.identity.v1"} + want := map[string]string{"prompt": metadata["prompt_sha256"].(string), "response_schema": metadata["response_schema_sha256"].(string), "identity_policy": "dnd.npcs.identity.v1", "mapping_policy": mappingPolicy} if len(fingerprints) != len(want) { t.Fatalf("CheckpointFingerprints() = %#v, want %d entries", fingerprints, len(want)) } diff --git a/internal/modules/dnd/extract/spells/extractor.go b/internal/modules/dnd/extract/spells/extractor.go index 284f05f..64801a2 100644 --- a/internal/modules/dnd/extract/spells/extractor.go +++ b/internal/modules/dnd/extract/spells/extractor.go @@ -17,6 +17,8 @@ const Key = "dnd/spells" const ArtifactType = "dnd.spell_cast" const SchemaVersion = "v1" +const mappingPolicy = "dnd.spells.extract_mapping.v1" + const ( NPCRegistryReferenceSlot = npcregistry.ReferenceSlot NPCRegistryMaxBytes = npcregistry.MaxBytes @@ -127,6 +129,7 @@ func (e *Extractor) ManifestMetadata() map[string]any { "catalog_base_id": e.effectiveCatalog.BaseID(), "catalog_digest": e.effectiveCatalog.Digest(), "catalog_overlay_ids": e.effectiveCatalog.OverlayIDs(), + "mapping_policy": mappingPolicy, "response_schema_key": string(ResponseSchemaKey), "response_schema_id": ResponseSchemaID, "response_schema_name": ResponseSchemaName, @@ -147,6 +150,7 @@ func (e *Extractor) CheckpointFingerprints() []pipeline.CheckpointFingerprint { } fingerprints := []pipeline.CheckpointFingerprint{ {Name: "effective_catalog", Value: e.effectiveCatalog.Digest()}, + {Name: "mapping_policy", Value: mappingPolicy}, {Name: "prompt", Value: e.promptSHA}, {Name: "response_schema", Value: e.responseSchemaSHA}, } diff --git a/internal/modules/dnd/extract/spells/extractor_test.go b/internal/modules/dnd/extract/spells/extractor_test.go index 1702426..7e9b65f 100644 --- a/internal/modules/dnd/extract/spells/extractor_test.go +++ b/internal/modules/dnd/extract/spells/extractor_test.go @@ -104,6 +104,9 @@ func TestExtractPromptUsesCanonicalOverlayNamesWithoutAliasesOrMetadata(t *testi } metadata := newExtractor(t, &fakeSpellsLLMClient{}, overlaySpellCatalogReference()).ManifestMetadata() + if metadata["mapping_policy"] != mappingPolicy { + t.Fatalf("mapping policy metadata = %#v, want %q", metadata["mapping_policy"], mappingPolicy) + } if metadata["catalog_base_id"] != spellcatalog.SRD5E2014ID { t.Fatalf("catalog base metadata = %#v", metadata["catalog_base_id"]) } @@ -116,6 +119,7 @@ func TestExtractPromptUsesCanonicalOverlayNamesWithoutAliasesOrMetadata(t *testi fingerprints := newExtractor(t, &fakeSpellsLLMClient{}, overlaySpellCatalogReference()).CheckpointFingerprints() wantFingerprints := map[string]any{ "effective_catalog": metadata["catalog_digest"], + "mapping_policy": mappingPolicy, "prompt": metadata["prompt_sha256"], "response_schema": metadata["response_schema_sha256"], "npc_registry": checkpointFingerprintMap(newExtractor(t, &fakeSpellsLLMClient{}).CheckpointFingerprints())["npc_registry"], diff --git a/internal/modules/integration/dnd_npc_grounded_test.go b/internal/modules/integration/dnd_npc_grounded_test.go index 9688cf6..e2abb6f 100644 --- a/internal/modules/integration/dnd_npc_grounded_test.go +++ b/internal/modules/integration/dnd_npc_grounded_test.go @@ -40,6 +40,12 @@ func TestNPCOutputGroundsSpellAndCombatConsumersThroughOneOperation(t *testing.T if err != nil { t.Fatalf("Prepare() error = %v", err) } + for name, value := range map[string]string{ + "extract:npcs:dnd/npcs:mapping_policy": "dnd.npcs.extract_mapping.v1", + "extract:spells:dnd/spells:mapping_policy": "dnd.spells.extract_mapping.v1", + } { + assertFingerprintValue(t, prepared.CheckpointFingerprints(), name, value) + } for _, name := range []string{ "extract:npcs:dnd/npcs:prompt", "extract:npcs:dnd/npcs:response_schema", @@ -173,6 +179,16 @@ func TestNPCOutputGroundsSpellAndCombatConsumersThroughOneOperation(t *testing.T assertCurrentEvidence(t, combatValue.CombatTurns[0].SourceRefs) } +func assertFingerprintValue(t *testing.T, fingerprints []pipeline.CheckpointFingerprint, name, want string) { + t.Helper() + for _, fingerprint := range fingerprints { + if fingerprint.Name == name && fingerprint.Value == want { + return + } + } + t.Fatalf("fingerprints = %#v, want %q = %q", fingerprints, name, want) +} + func assertCurrentEvidence(t *testing.T, references []source.SourceRef) { t.Helper() for _, reference := range references {