From c006b163d5c5d7d827b6b41e08d350d7c05a7f44 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Wed, 5 Aug 2026 19:05:42 +0000 Subject: [PATCH] Adopt location registry durable contract --- docs/config.md | 2 +- docs/integrations/dnd-location-artifacts.md | 14 +++---- .../dnd-location-occurrence-artifacts.md | 4 +- .../cli/dnd_enemy_events_contract_test.go | 2 +- internal/cli/example_contract_test.go | 2 +- .../testdata/dnd_location_occurrences.v1.json | 2 +- ....v1.json => dnd_location_registry.v1.json} | 2 +- internal/modules/dnd/codec/locations/codec.go | 34 ++++++++-------- .../modules/dnd/codec/locations/codec_test.go | 28 ++++++------- ....v1.json => dnd_location_registry.v1.json} | 2 +- .../extract/locationoccurrences/extractor.go | 2 +- .../locationoccurrences/extractor_test.go | 10 ++--- .../dnd/extract/locations/canonicalize.go | 6 +-- .../dnd/extract/locations/extractor.go | 18 ++++----- .../dnd/extract/locations/extractor_test.go | 2 +- .../dnd/extract/locations/registry_test.go | 2 +- .../dnd/locations/identity/identity.go | 4 +- .../dnd/locations/identity/identity_test.go | 4 +- .../dnd/locations/registry/registry.go | 26 ++++++------ .../dnd/locations/registry/registry_test.go | 8 ++-- .../locationoccurrences/normalizer.go | 2 +- .../locationoccurrences/normalizer_test.go | 10 ++--- .../dnd/normalize/locations/normalizer.go | 40 +++++++++---------- .../normalize/locations/normalizer_test.go | 14 +++---- .../normalize/locations/test_helpers_test.go | 6 +-- internal/modules/dnd/register/evidence.go | 4 +- internal/modules/dnd/register/merge.go | 6 +-- internal/modules/dnd/register/modules.go | 8 ++-- .../modules/dnd/register/register_test.go | 26 ++++++------ internal/modules/dnd/register/validators.go | 8 ++-- internal/modules/dnd/types.go | 4 +- .../registry/validator_test.go | 2 +- .../validate/locations/identity/validator.go | 6 +-- .../locations/identity/validator_test.go | 10 ++--- .../dnd/validate/locations/shape/validator.go | 10 ++--- .../locations/shape/validator_test.go | 14 +++---- .../locations/source_refs/validator.go | 6 +-- .../locations/source_refs/validator_test.go | 22 +++++----- .../locations/source_relatedness/validator.go | 6 +-- .../source_relatedness/validator_test.go | 12 +++--- .../integration/dnd_locations_runner_test.go | 2 +- 41 files changed, 196 insertions(+), 196 deletions(-) rename internal/modules/dnd/codec/locations/assets/schemas/{dnd_locations.v1.json => dnd_location_registry.v1.json} (96%) rename internal/modules/dnd/codec/locations/testdata/{dnd_locations.v1.json => dnd_location_registry.v1.json} (64%) diff --git a/docs/config.md b/docs/config.md index 6b259b0..5b4f261 100644 --- a/docs/config.md +++ b/docs/config.md @@ -387,7 +387,7 @@ binding contracts. Durable semantics and wire shapes remain in their | `scene_descriptions` | `dnd/scene-description-list` | `application/json` | 1,048,576 bytes | extract only | | `combat_turns` | `dnd/combat-turn-list` | `application/json` | 1,048,576 bytes | extract only | | `npc_occurrences` | `dnd/npc-occurrence-list` | `application/json` | 1,048,576 bytes | extract only | -| `locations` | `dnd/location-list` | `application/json` | 1,048,576 bytes | location-occurrence extract and normalize | +| `locations` | `dnd/location-registry` | `application/json` | 1,048,576 bytes | location-occurrence extract and normalize | Scene descriptions accept **party**, **players**, and **glossary**, but not **roster**. NPC occurrences require **npc_registry** for both extraction and diff --git a/docs/integrations/dnd-location-artifacts.md b/docs/integrations/dnd-location-artifacts.md index d3298b3..62c650e 100644 --- a/docs/integrations/dnd-location-artifacts.md +++ b/docs/integrations/dnd-location-artifacts.md @@ -1,4 +1,4 @@ -# D&D Location Artifact +# D&D Location Registry Artifact This contract defines the durable, source-grounded location registry produced by `dnd/locations`. It records transcript-established physical places for one @@ -9,12 +9,12 @@ registry, or location description. | Property | Value | | --- | --- | -| Artifact kind | `dnd/location-list` | -| Schema ID | `notarius.dnd.locations` | -| Schema name | `notarius_dnd_locations_v1` | +| Artifact kind | `dnd/location-registry` | +| Schema ID | `notarius.dnd.location_registry` | +| Schema name | `notarius_dnd_location_registry_v1` | | Schema version | `v1` | | Media type | `application/json` | -| Identity policy | `dnd.locations.identity.v1` | +| Identity policy | `dnd.location_registry.identity.v1` | `v1` accepts one strict JSON object with required `locations`; the array may be empty. Location and source-reference objects reject unknown fields. An @@ -39,7 +39,7 @@ identifiers, and the start may not follow the end. { "locations": [ { - "id": "location:sha256:5c1a91f15729df0b8c257093865fdf2452b43c215375e8cf2341aa9c37bb99aa", + "id": "location:sha256:fb05475da0fc7debf994b517e1906ffe7209887a6a1ec306356d84de820b1a24", "name": "Moon Gate", "source_refs": [ {"source_id": "session-7", "start_unit_id": 4, "end_unit_id": 5} @@ -55,7 +55,7 @@ normalization, collapsed whitespace, and case folding. It hashes compact JSON for this array, using the earliest canonical source reference as the anchor: ```text -["dnd.locations.identity.v1", comparison_name, source_id, start_unit_id, end_unit_id] +["dnd.location_registry.identity.v1", comparison_name, source_id, start_unit_id, end_unit_id] ``` The canonical ID is the lowercase SHA-256 digest of those bytes with the diff --git a/docs/integrations/dnd-location-occurrence-artifacts.md b/docs/integrations/dnd-location-occurrence-artifacts.md index 8c0c4dc..956ded0 100644 --- a/docs/integrations/dnd-location-occurrence-artifacts.md +++ b/docs/integrations/dnd-location-occurrence-artifacts.md @@ -38,7 +38,7 @@ positive and the start may not follow the end. { "occurrences": [ { - "location_id": "location:sha256:5c1a91f15729df0b8c257093865fdf2452b43c215375e8cf2341aa9c37bb99aa", + "location_id": "location:sha256:fb05475da0fc7debf994b517e1906ffe7209887a6a1ec306356d84de820b1a24", "name": "Moon Gate", "kind": "visited", "source_refs": [ @@ -72,7 +72,7 @@ complete canonical evidence sequence. ## Required grounding and evidence Both extraction and normalization require exactly one `locations` reference of -kind `dnd/location-list`, media type `application/json`, and at most 1 MiB. The +kind `dnd/location-registry`, media type `application/json`, and at most 1 MiB. The registry provides identity grounding only: unknown IDs and mismatched ID/name pairs are rejected rather than guessed or reassigned. The current transcript is the only evidence source for an occurrence; registry evidence and provenance diff --git a/internal/cli/dnd_enemy_events_contract_test.go b/internal/cli/dnd_enemy_events_contract_test.go index cb07410..3b5ba16 100644 --- a/internal/cli/dnd_enemy_events_contract_test.go +++ b/internal/cli/dnd_enemy_events_contract_test.go @@ -143,7 +143,7 @@ func TestMaintainedCompleteExampleProducesEnemyEventsThroughGeneratedHandoffs(t if locationOutput.File != "lanes/locations.json" || locationOutput.SchemaID != locationcodec.SchemaID || locationOutput.SchemaVersion != locationcodec.SchemaVersion { t.Fatalf("location output = %#v, want typed location registry JSON", locationOutput) } - locationsValue := readProductionJSON[dnd.LocationList](t, filepath.Join(runRoot, locationOutput.File)) + locationsValue := readProductionJSON[dnd.LocationRegistry](t, filepath.Join(runRoot, locationOutput.File)) if len(locationsValue.Locations) != 2 || locationsValue.Locations[0].Name != "Moon Gate" || locationsValue.Locations[1].Name != "Moon Gate" || locationsValue.Locations[0].ID == locationsValue.Locations[1].ID { t.Fatalf("location registry = %#v, want distinct source-grounded identities for same-name locations", locationsValue) } diff --git a/internal/cli/example_contract_test.go b/internal/cli/example_contract_test.go index 1ca70e9..c3d99c9 100644 --- a/internal/cli/example_contract_test.go +++ b/internal/cli/example_contract_test.go @@ -62,7 +62,7 @@ func TestMaintainedExamplesLoadResolveAndList(t *testing.T) { t.Fatalf("complete example steps and lanes = %v, want the documented D&D extractor composition", got) } locationLane := referenceContractLane(t, materialized, "locations") - if locationLane.ArtifactKind != dnd.LocationListKind || locationLane.Extract.Module != locationextract.Key || locationLane.Extract.Retries != 2 || locationLane.Merge.Module != pipeline.DefaultMergeModule || locationLane.Normalize.Module != locationnormalize.Key || locationLane.Normalize.Retries != 2 { + if locationLane.ArtifactKind != dnd.LocationRegistryKind || locationLane.Extract.Module != locationextract.Key || locationLane.Extract.Retries != 2 || locationLane.Merge.Module != pipeline.DefaultMergeModule || locationLane.Normalize.Module != locationnormalize.Key || locationLane.Normalize.Retries != 2 { t.Fatalf("location lane = %#v, want typed registry composition", locationLane) } occurrenceLane := referenceContractLane(t, materialized, "location-occurrences") diff --git a/internal/modules/dnd/codec/locationoccurrences/testdata/dnd_location_occurrences.v1.json b/internal/modules/dnd/codec/locationoccurrences/testdata/dnd_location_occurrences.v1.json index d6856a1..2ef18a2 100644 --- a/internal/modules/dnd/codec/locationoccurrences/testdata/dnd_location_occurrences.v1.json +++ b/internal/modules/dnd/codec/locationoccurrences/testdata/dnd_location_occurrences.v1.json @@ -1,7 +1,7 @@ { "occurrences": [ { - "location_id": "location:sha256:cdd2b57615b56a4e92d506cf053cd3985e11d4df4895c5383b30ec2ce9f39ed0", + "location_id": "location:sha256:4ea39088943a692f120ae8740419c3baa1e7026c0e2a26ab03255f0b7e20215c", "name": "The Old Tavern", "kind": "visited", "source_refs": [ diff --git a/internal/modules/dnd/codec/locations/assets/schemas/dnd_locations.v1.json b/internal/modules/dnd/codec/locations/assets/schemas/dnd_location_registry.v1.json similarity index 96% rename from internal/modules/dnd/codec/locations/assets/schemas/dnd_locations.v1.json rename to internal/modules/dnd/codec/locations/assets/schemas/dnd_location_registry.v1.json index b7089da..1d2ae42 100644 --- a/internal/modules/dnd/codec/locations/assets/schemas/dnd_locations.v1.json +++ b/internal/modules/dnd/codec/locations/assets/schemas/dnd_location_registry.v1.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "notarius.dnd.locations", + "$id": "notarius.dnd.location_registry", "type": "object", "additionalProperties": false, "required": ["locations"], diff --git a/internal/modules/dnd/codec/locations/codec.go b/internal/modules/dnd/codec/locations/codec.go index 0fb43f6..e585b8e 100644 --- a/internal/modules/dnd/codec/locations/codec.go +++ b/internal/modules/dnd/codec/locations/codec.go @@ -13,25 +13,25 @@ import ( ) const ( - SchemaID = "notarius.dnd.locations" - SchemaName = "notarius_dnd_locations_v1" + SchemaID = "notarius.dnd.location_registry" + SchemaName = "notarius_dnd_location_registry_v1" SchemaVersion = "v1" MediaType = "application/json" ) -//go:embed assets/schemas/dnd_locations.v1.json +//go:embed assets/schemas/dnd_location_registry.v1.json var schemaAssets embed.FS -var _ contracts.ArtifactCodec[dnd.LocationList] = (*Codec)(nil) +var _ contracts.ArtifactCodec[dnd.LocationRegistry] = (*Codec)(nil) type Codec struct{} func New() *Codec { return &Codec{} } -func (c *Codec) Kind() contracts.ArtifactKind { return dnd.LocationListKind } +func (c *Codec) Kind() contracts.ArtifactKind { return dnd.LocationRegistryKind } func (c *Codec) Schema() contracts.ArtifactSchema { - raw, err := schemaAssets.ReadFile("assets/schemas/dnd_locations.v1.json") + raw, err := schemaAssets.ReadFile("assets/schemas/dnd_location_registry.v1.json") if err != nil { return contracts.ArtifactSchema{} } @@ -45,41 +45,41 @@ func (c *Codec) Schema() contracts.ArtifactSchema { func (c *Codec) MediaType() string { return MediaType } -func (c *Codec) Metadata(value dnd.LocationList) map[string]any { +func (c *Codec) Metadata(value dnd.LocationRegistry) map[string]any { return map[string]any{"location_count": len(value.Locations)} } -func (c *Codec) Encode(value dnd.LocationList) ([]byte, error) { +func (c *Codec) Encode(value dnd.LocationRegistry) ([]byte, error) { if err := validate(value); err != nil { - return nil, fmt.Errorf("encode dnd location list: %w", err) + return nil, fmt.Errorf("encode dnd location registry: %w", err) } return c.EncodeCandidate(value) } // EncodeCandidate provides the durable representation before semantic // validators have approved a value. -func (c *Codec) EncodeCandidate(value dnd.LocationList) ([]byte, error) { - return candidatejson.EncodeCandidate("dnd location list", value) +func (c *Codec) EncodeCandidate(value dnd.LocationRegistry) ([]byte, error) { + return candidatejson.EncodeCandidate("dnd location registry", value) } -func (c *Codec) Decode(content []byte) (dnd.LocationList, error) { +func (c *Codec) Decode(content []byte) (dnd.LocationRegistry, error) { value, err := c.DecodeCandidate(content) if err != nil { - return dnd.LocationList{}, err + return dnd.LocationRegistry{}, err } if err := validate(value); err != nil { - return dnd.LocationList{}, fmt.Errorf("decode dnd location list: %w", err) + return dnd.LocationRegistry{}, fmt.Errorf("decode dnd location registry: %w", err) } return value, nil } // DecodeCandidate reads one strict durable JSON value before semantic // validators have approved it. -func (c *Codec) DecodeCandidate(content []byte) (dnd.LocationList, error) { - return candidatejson.DecodeCandidate[dnd.LocationList]("dnd location list", content) +func (c *Codec) DecodeCandidate(content []byte) (dnd.LocationRegistry, error) { + return candidatejson.DecodeCandidate[dnd.LocationRegistry]("dnd location registry", content) } -func validate(value dnd.LocationList) error { +func validate(value dnd.LocationRegistry) error { if value.Locations == nil { return fmt.Errorf("locations must be present") } diff --git a/internal/modules/dnd/codec/locations/codec_test.go b/internal/modules/dnd/codec/locations/codec_test.go index 605b0de..8c141c7 100644 --- a/internal/modules/dnd/codec/locations/codec_test.go +++ b/internal/modules/dnd/codec/locations/codec_test.go @@ -15,9 +15,9 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/locations/identity" ) -func validList() dnd.LocationList { +func validList() dnd.LocationRegistry { refs := []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 1, EndUnitID: 2}} - return dnd.LocationList{Locations: []dnd.Location{{ + return dnd.LocationRegistry{Locations: []dnd.Location{{ ID: identity.DeriveID("The Old Tavern", refs), Name: "The Old Tavern", SourceRefs: refs, @@ -25,7 +25,7 @@ func validList() dnd.LocationList { } func TestCodecMatchesMaintainedDurableFixture(t *testing.T) { - raw, err := os.ReadFile("testdata/dnd_locations.v1.json") + raw, err := os.ReadFile("testdata/dnd_location_registry.v1.json") if err != nil { t.Fatalf("read durable fixture: %v", err) } @@ -53,7 +53,7 @@ func TestCodecMatchesMaintainedDurableFixture(t *testing.T) { func TestCodecOwnsDurableSchemaAndMetadata(t *testing.T) { codec := New() schema := codec.Schema() - if codec.Kind() != dnd.LocationListKind || codec.MediaType() != MediaType { + if codec.Kind() != dnd.LocationRegistryKind || codec.MediaType() != MediaType { t.Fatalf("codec identity = %q/%q", codec.Kind(), codec.MediaType()) } if schema.ID != SchemaID || schema.Name != SchemaName || schema.Version != SchemaVersion || !json.Valid(schema.JSONSchema) { @@ -67,7 +67,7 @@ func TestCodecOwnsDurableSchemaAndMetadata(t *testing.T) { if err := pipeline.RegisterArtifactCodec(registry, codec); err != nil { t.Fatalf("RegisterArtifactCodec() error = %v", err) } - if spec, ok := registry.Spec(dnd.LocationListKind); !ok || spec.SchemaDigest != contracts.DigestArtifactSchema(schema) { + if spec, ok := registry.Spec(dnd.LocationRegistryKind); !ok || spec.SchemaDigest != contracts.DigestArtifactSchema(schema) { t.Fatalf("registered spec = %#v, %t", spec, ok) } first := schema.JSONSchema @@ -84,11 +84,11 @@ func TestCodecOwnsDurableSchemaAndMetadata(t *testing.T) { func TestCodecSupportsEmptyListsAndCandidateSemanticFailures(t *testing.T) { codec := New() - empty := dnd.LocationList{Locations: []dnd.Location{}} + empty := dnd.LocationRegistry{Locations: []dnd.Location{}} if content, err := codec.Encode(empty); err != nil || string(content) != `{"locations":[]}` { t.Fatalf("Encode() = %s, %v", content, err) } - for _, candidate := range []dnd.LocationList{ + for _, candidate := range []dnd.LocationRegistry{ {}, empty, {Locations: []dnd.Location{{ID: "bad", Name: " ", SourceRefs: nil}}}, @@ -109,7 +109,7 @@ func TestCodecSupportsEmptyListsAndCandidateSemanticFailures(t *testing.T) { func TestCodecRejectsStructuralJSONBeforeSemanticApproval(t *testing.T) { valid := `{"locations":[{"id":"location:sha256:0000000000000000000000000000000000000000000000000000000000000000","name":"The Tavern","source_refs":[{"source_id":"session","start_unit_id":1,"end_unit_id":1}]}]}` for _, test := range []struct{ name, raw, want string }{ - {"malformed", `{`, "decode dnd location list"}, + {"malformed", `{`, "decode dnd location registry"}, {"unknown top-level", `{"locations":[],"unexpected":true}`, "unknown field"}, {"unknown location field", strings.Replace(valid, `"name":"The Tavern"`, `"name":"The Tavern","unexpected":true`, 1), "unknown field"}, {"unknown source reference field", strings.Replace(valid, `"end_unit_id":1`, `"end_unit_id":1,"unexpected":true`, 1), "unknown field"}, @@ -128,14 +128,14 @@ func TestCodecRejectsApprovedShapeBoundaries(t *testing.T) { base := validList().Locations[0] for _, test := range []struct { name string - value dnd.LocationList + value dnd.LocationRegistry want string }{ - {"nil locations", dnd.LocationList{}, "locations must be present"}, - {"invalid ID", dnd.LocationList{Locations: []dnd.Location{{ID: "bad", Name: base.Name, SourceRefs: base.SourceRefs}}}, "id must match location ID pattern"}, - {"blank name", dnd.LocationList{Locations: []dnd.Location{{ID: base.ID, Name: " ", SourceRefs: base.SourceRefs}}}, "name must not be empty"}, - {"empty source refs", dnd.LocationList{Locations: []dnd.Location{{ID: base.ID, Name: base.Name, SourceRefs: nil}}}, "source_refs must contain"}, - {"malformed source reference", dnd.LocationList{Locations: []dnd.Location{{ID: base.ID, Name: base.Name, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 0, EndUnitID: 1}}}}}, "start_unit_id must be positive"}, + {"nil locations", dnd.LocationRegistry{}, "locations must be present"}, + {"invalid ID", dnd.LocationRegistry{Locations: []dnd.Location{{ID: "bad", Name: base.Name, SourceRefs: base.SourceRefs}}}, "id must match location ID pattern"}, + {"blank name", dnd.LocationRegistry{Locations: []dnd.Location{{ID: base.ID, Name: " ", SourceRefs: base.SourceRefs}}}, "name must not be empty"}, + {"empty source refs", dnd.LocationRegistry{Locations: []dnd.Location{{ID: base.ID, Name: base.Name, SourceRefs: nil}}}, "source_refs must contain"}, + {"malformed source reference", dnd.LocationRegistry{Locations: []dnd.Location{{ID: base.ID, Name: base.Name, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 0, EndUnitID: 1}}}}}, "start_unit_id must be positive"}, } { t.Run(test.name, func(t *testing.T) { if _, err := New().Encode(test.value); err == nil || !strings.Contains(err.Error(), test.want) { diff --git a/internal/modules/dnd/codec/locations/testdata/dnd_locations.v1.json b/internal/modules/dnd/codec/locations/testdata/dnd_location_registry.v1.json similarity index 64% rename from internal/modules/dnd/codec/locations/testdata/dnd_locations.v1.json rename to internal/modules/dnd/codec/locations/testdata/dnd_location_registry.v1.json index abb6354..dc7fd9b 100644 --- a/internal/modules/dnd/codec/locations/testdata/dnd_locations.v1.json +++ b/internal/modules/dnd/codec/locations/testdata/dnd_location_registry.v1.json @@ -1,7 +1,7 @@ { "locations": [ { - "id": "location:sha256:cdd2b57615b56a4e92d506cf053cd3985e11d4df4895c5383b30ec2ce9f39ed0", + "id": "location:sha256:4ea39088943a692f120ae8740419c3baa1e7026c0e2a26ab03255f0b7e20215c", "name": "The Old Tavern", "source_refs": [ {"source_id": "session-alpha", "start_unit_id": 1, "end_unit_id": 2} diff --git a/internal/modules/dnd/extract/locationoccurrences/extractor.go b/internal/modules/dnd/extract/locationoccurrences/extractor.go index cbce8e5..61fce72 100644 --- a/internal/modules/dnd/extract/locationoccurrences/extractor.go +++ b/internal/modules/dnd/extract/locationoccurrences/extractor.go @@ -40,7 +40,7 @@ func referenceSlots() []contracts.ReferenceSlot { Description: "Required normalized location registry used only for location identity grounding, never as occurrence evidence.", Required: true, AcceptedMediaTypes: []string{"application/json"}, - AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.LocationListKind}, + AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.LocationRegistryKind}, MaxBytes: LocationRegistryMaxBytes, }) sort.Slice(slots, func(left, right int) bool { return slots[left].Name < slots[right].Name }) diff --git a/internal/modules/dnd/extract/locationoccurrences/extractor_test.go b/internal/modules/dnd/extract/locationoccurrences/extractor_test.go index 349c609..3b2d186 100644 --- a/internal/modules/dnd/extract/locationoccurrences/extractor_test.go +++ b/internal/modules/dnd/extract/locationoccurrences/extractor_test.go @@ -111,7 +111,7 @@ func TestExtractRequiresRegistryAndAcceptsEmptyRegistryWithNoOccurrences(t *test if len(client.requests) != 0 { t.Fatalf("LLM calls = %d", len(client.requests)) } - empty := dnd.LocationList{Locations: []dnd.Location{}} + empty := dnd.LocationRegistry{Locations: []dnd.Location{}} references := registryReferences(t, empty) req := extractionRequest() req.References = references @@ -196,7 +196,7 @@ func TestExtractorContractsMetadataAndFailures(t *testing.T) { slot = candidate } } - if !slot.Required || !reflect.DeepEqual(slot.AcceptedArtifactKinds, []contracts.ArtifactKind{dnd.LocationListKind}) || slot.MaxBytes != LocationRegistryMaxBytes { + if !slot.Required || !reflect.DeepEqual(slot.AcceptedArtifactKinds, []contracts.ArtifactKind{dnd.LocationRegistryKind}) || slot.MaxBytes != LocationRegistryMaxBytes { t.Fatalf("location registry slot = %#v", slot) } registry := pipeline.NewExtractorRegistry() @@ -220,17 +220,17 @@ func TestExtractorContractsMetadataAndFailures(t *testing.T) { } } -func locationRegistry(t *testing.T, names ...string) dnd.LocationList { +func locationRegistry(t *testing.T, names ...string) dnd.LocationRegistry { t.Helper() locations := make([]dnd.Location, len(names)) for index, name := range names { refs := []source.SourceRef{{SourceID: "other-session", StartUnitID: index + 1, EndUnitID: index + 1}} locations[index] = dnd.Location{ID: identity.DeriveID(name, refs), Name: name, SourceRefs: refs} } - return dnd.LocationList{Locations: locations} + return dnd.LocationRegistry{Locations: locations} } -func registryReferences(t *testing.T, locations dnd.LocationList) contracts.ReferenceSet { +func registryReferences(t *testing.T, locations dnd.LocationRegistry) contracts.ReferenceSet { t.Helper() content, err := locationcodec.New().Encode(locations) if err != nil { diff --git a/internal/modules/dnd/extract/locations/canonicalize.go b/internal/modules/dnd/extract/locations/canonicalize.go index 301bc02..7de8a0d 100644 --- a/internal/modules/dnd/extract/locations/canonicalize.go +++ b/internal/modules/dnd/extract/locations/canonicalize.go @@ -47,9 +47,9 @@ func canonicalizeLocation(location *locationResponse, order shared.SourceRefOrde return order.EarliestValid(refs) } -func canonicalLocationList(response extractionResponse, sourceID string) dnd.LocationList { +func canonicalLocationRegistry(response extractionResponse, sourceID string) dnd.LocationRegistry { if response.Locations == nil { - return dnd.LocationList{Locations: nil} + return dnd.LocationRegistry{Locations: nil} } locations := make([]dnd.Location, len(response.Locations)) for index, location := range response.Locations { @@ -60,7 +60,7 @@ func canonicalLocationList(response extractionResponse, sourceID string) dnd.Loc SourceRefs: refs, } } - return dnd.LocationList{Locations: locations} + return dnd.LocationRegistry{Locations: locations} } func canonicalSourceRefs(values []locationSourceRefResponse, sourceID string) []source.SourceRef { diff --git a/internal/modules/dnd/extract/locations/extractor.go b/internal/modules/dnd/extract/locations/extractor.go index a8e5834..63c55d8 100644 --- a/internal/modules/dnd/extract/locations/extractor.go +++ b/internal/modules/dnd/extract/locations/extractor.go @@ -31,7 +31,7 @@ func referenceSlots() []contracts.ReferenceSlot { return shared.ReferenceSlots(referenceSlotDescriptions) } -var _ contracts.Extractor[dnd.LocationList] = (*Extractor)(nil) +var _ contracts.Extractor[dnd.LocationRegistry] = (*Extractor)(nil) var _ contracts.ManifestMetadataProvider = (*Extractor)(nil) var _ pipeline.CheckpointFingerprintProvider = (*Extractor)(nil) @@ -95,16 +95,16 @@ func (e *Extractor) CheckpointFingerprints() []pipeline.CheckpointFingerprint { } } -func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRequest) (contracts.TypedExtractionResult[dnd.LocationList], error) { +func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRequest) (contracts.TypedExtractionResult[dnd.LocationRegistry], error) { if e == nil { - return contracts.TypedExtractionResult[dnd.LocationList]{}, extractorErrorf("extractor must not be nil") + return contracts.TypedExtractionResult[dnd.LocationRegistry]{}, extractorErrorf("extractor must not be nil") } if e.llm == nil { - return contracts.TypedExtractionResult[dnd.LocationList]{}, extractorErrorf("LLM client must not be nil") + return contracts.TypedExtractionResult[dnd.LocationRegistry]{}, extractorErrorf("LLM client must not be nil") } sourceInput, err := shared.PrepareChunkExtraction(ctx, req) if err != nil { - return contracts.TypedExtractionResult[dnd.LocationList]{}, extractorErrorf("%w", err) + return contracts.TypedExtractionResult[dnd.LocationRegistry]{}, extractorErrorf("%w", err) } order := shared.NewSourceRefOrder(req.Source) @@ -114,22 +114,22 @@ func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRe ProfileID: req.LLMProfile, SessionID: req.SessionID, Inputs: shared.PromptInputs(sourceInput, req.References), }, &response); err != nil { - return contracts.TypedExtractionResult[dnd.LocationList]{}, extractorErrorf("complete structured output: %w", err) + return contracts.TypedExtractionResult[dnd.LocationRegistry]{}, extractorErrorf("complete structured output: %w", err) } canonicalizeResponse(&response, order, req.Source.ID) - return contracts.TypedExtractionResult[dnd.LocationList]{Value: canonicalLocationList(response, req.Source.ID)}, nil + return contracts.TypedExtractionResult[dnd.LocationRegistry]{Value: canonicalLocationRegistry(response, req.Source.ID)}, nil } func ModuleSpec() pipeline.ModuleSpec { return pipeline.ModuleSpec{ Key: Key, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), - ArtifactKind: dnd.LocationListKind, ReferenceSlots: referenceSlots(), + ArtifactKind: dnd.LocationRegistryKind, ReferenceSlots: referenceSlots(), } } func Register(registry *pipeline.ExtractorRegistry) error { - return pipeline.RegisterExtractorBuilder(registry, ModuleSpec(), validateOptions, func(request pipeline.BuildRequest) (contracts.Extractor[dnd.LocationList], error) { + return pipeline.RegisterExtractorBuilder(registry, ModuleSpec(), validateOptions, func(request pipeline.BuildRequest) (contracts.Extractor[dnd.LocationRegistry], error) { options, err := DecodeOptions(request.Options) if err != nil { return nil, err diff --git a/internal/modules/dnd/extract/locations/extractor_test.go b/internal/modules/dnd/extract/locations/extractor_test.go index b2c187a..8a90ab9 100644 --- a/internal/modules/dnd/extract/locations/extractor_test.go +++ b/internal/modules/dnd/extract/locations/extractor_test.go @@ -23,7 +23,7 @@ func TestExtractMapsLocationsWithOwnedEvidenceAndDeterministicOrder(t *testing.T t.Fatalf("Extract() error = %v, want nil", err) } refs := []source.SourceRef{{SourceID: "session-locations", StartUnitID: 1, EndUnitID: 1}, {SourceID: "session-locations", StartUnitID: 2, EndUnitID: 2}} - want := dnd.LocationList{Locations: []dnd.Location{ + want := dnd.LocationRegistry{Locations: []dnd.Location{ {ID: identity.DeriveID("Old Mill", refs), Name: "Old Mill", SourceRefs: refs}, {ID: identity.DeriveID("The Tavern", []source.SourceRef{{SourceID: "session-locations", StartUnitID: 3, EndUnitID: 3}}), Name: "The Tavern", SourceRefs: []source.SourceRef{{SourceID: "session-locations", StartUnitID: 3, EndUnitID: 3}}}, }} diff --git a/internal/modules/dnd/extract/locations/registry_test.go b/internal/modules/dnd/extract/locations/registry_test.go index 45899c1..56f25d0 100644 --- a/internal/modules/dnd/extract/locations/registry_test.go +++ b/internal/modules/dnd/extract/locations/registry_test.go @@ -18,7 +18,7 @@ func TestModuleRegistrationAndMetadata(t *testing.T) { if _, err := New(&fakeLocationsLLMClient{}, Options{}, contracts.ReferenceSet{}, contracts.ReferenceSet{}); err == nil || !strings.Contains(err.Error(), "at most one") { t.Fatalf("New() error = %v, want reference-set rejection", err) } - want := pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: []string{"chunks", "source.transcript"}, Provides: []string{"dnd.locations"}, ArtifactKind: dnd.LocationListKind, ReferenceSlots: referenceSlots()} + want := pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: []string{"chunks", "source.transcript"}, Provides: []string{"dnd.locations"}, ArtifactKind: dnd.LocationRegistryKind, ReferenceSlots: referenceSlots()} if got := ModuleSpec(); !reflect.DeepEqual(got, want) { t.Fatalf("ModuleSpec() = %#v, want %#v", got, want) } diff --git a/internal/modules/dnd/locations/identity/identity.go b/internal/modules/dnd/locations/identity/identity.go index e67e4e5..bada3aa 100644 --- a/internal/modules/dnd/locations/identity/identity.go +++ b/internal/modules/dnd/locations/identity/identity.go @@ -16,7 +16,7 @@ import ( const ( // IdentityPolicy identifies the durable location ID derivation policy. - IdentityPolicy = "dnd.locations.identity.v1" + IdentityPolicy = "dnd.location_registry.identity.v1" // Policy is an alias for IdentityPolicy. Policy = IdentityPolicy @@ -109,7 +109,7 @@ func IsValidID(value string) bool { func ValidID(value string) bool { return IsValidID(value) } // ValidateList validates the identity members of list. -func ValidateList(list dnd.LocationList) []Issue { return ValidateRegistry(list.Locations) } +func ValidateList(list dnd.LocationRegistry) []Issue { return ValidateRegistry(list.Locations) } // ValidateRegistry validates location IDs without modifying records or their // source references. Equal comparison names are allowed because their evidence diff --git a/internal/modules/dnd/locations/identity/identity_test.go b/internal/modules/dnd/locations/identity/identity_test.go index 197cce9..ae03575 100644 --- a/internal/modules/dnd/locations/identity/identity_test.go +++ b/internal/modules/dnd/locations/identity/identity_test.go @@ -32,7 +32,7 @@ func TestDeriveIDUsesDocumentedCompactJSONInput(t *testing.T) { {SourceID: "session-alpha", StartUnitID: 3, EndUnitID: 9}, {SourceID: "session-alpha", StartUnitID: 3, EndUnitID: 9}, } - const want = "location:sha256:379bd996e754e143b47e6b613a95166e58c111c7451750df7066ca816bb1866c" + const want = "location:sha256:c099f7780a3cfd8c4094441d3a3e906f4f1e79c27589f95dfad573f86f308845" if got := DeriveID(" The\u2003Tavern ", refs); got != want { t.Fatalf("DeriveID() = %q, want %q", got, want) @@ -120,7 +120,7 @@ func TestValidateRegistryReportsIdentityProblemsDeterministicallyWithoutMutation } wantLocations := cloneLocations(locations) - issues := ValidateList(dnd.LocationList{Locations: locations}) + issues := ValidateList(dnd.LocationRegistry{Locations: locations}) want := []Issue{ {Code: IssueIDMismatch, RecordIndex: 1, Value: validID}, {Code: IssueEmptyCanonicalName, RecordIndex: 2, Value: ""}, diff --git a/internal/modules/dnd/locations/registry/registry.go b/internal/modules/dnd/locations/registry/registry.go index ef979fe..3d68fa0 100644 --- a/internal/modules/dnd/locations/registry/registry.go +++ b/internal/modules/dnd/locations/registry/registry.go @@ -27,7 +27,7 @@ const ( // grounding. All accessors return defensive copies. type Registry struct { bound bool - list dnd.LocationList + list dnd.LocationRegistry canonical []byte digest string projectionDigest string @@ -68,7 +68,7 @@ func (r *Resolver) Resolve(references contracts.ReferenceSet) (*Registry, error) return r.resolver.Resolve(references) } -// Resolve validates an optional location-list reference. An absent reference +// Resolve validates an optional location-registry reference. An absent reference // uses the canonical empty projection and has no durable registry identity. func Resolve(references contracts.ReferenceSet) (*Registry, error) { item, present, err := registryresolver.ResolveOptionalSingleItem(references, locationReferenceSpec()) @@ -102,7 +102,7 @@ func emptyRegistry() *Registry { content := []byte(emptyPrompt) projectionDigest := semanticDigest(content) return &Registry{ - list: dnd.LocationList{Locations: []dnd.Location{}}, + list: dnd.LocationRegistry{Locations: []dnd.Location{}}, canonical: append([]byte(nil), content...), projectionDigest: projectionDigest, promptInput: contracts.NewLLMInputMaterial(ReferenceSlot, locationcodec.MediaType, content, projectionDigest, ""), @@ -124,7 +124,7 @@ func loadRegistry(referenceContent []byte) (*Registry, error) { return nil, fmt.Errorf("encode canonical location registry: approved location value could not be encoded") } - list := cloneLocationList(value) + list := cloneLocationRegistry(value) lookupByID := make(map[string]int, len(list.Locations)) for index, location := range list.Locations { lookupByID[location.ID] = index @@ -157,12 +157,12 @@ func (r *Registry) Locations() []dnd.Location { return cloneLocations(r.list.Locations) } -// List returns a defensive copy of the validated location list. -func (r *Registry) List() dnd.LocationList { +// List returns a defensive copy of the validated location registry. +func (r *Registry) List() dnd.LocationRegistry { if r == nil { - return dnd.LocationList{} + return dnd.LocationRegistry{} } - return cloneLocationList(r.list) + return cloneLocationRegistry(r.list) } // CanonicalBytes returns a defensive copy of the canonical durable JSON. @@ -236,12 +236,12 @@ type projectedLocation struct { Name string `json:"name"` } -type projectedLocationList struct { +type projectedLocationRegistry struct { Locations []projectedLocation `json:"locations"` } -func promptProjection(list dnd.LocationList) ([]byte, error) { - projection := projectedLocationList{Locations: make([]projectedLocation, len(list.Locations))} +func promptProjection(list dnd.LocationRegistry) ([]byte, error) { + projection := projectedLocationRegistry{Locations: make([]projectedLocation, len(list.Locations))} for index, location := range list.Locations { projection.Locations[index] = projectedLocation{ID: location.ID, Name: location.Name} } @@ -256,8 +256,8 @@ func formatIdentityIssues(issues []identity.Issue) string { return diagnostics.Aggregate("validate location registry identity", parts) } -func cloneLocationList(value dnd.LocationList) dnd.LocationList { - return dnd.LocationList{Locations: cloneLocations(value.Locations)} +func cloneLocationRegistry(value dnd.LocationRegistry) dnd.LocationRegistry { + return dnd.LocationRegistry{Locations: cloneLocations(value.Locations)} } func cloneLocations(values []dnd.Location) []dnd.Location { diff --git a/internal/modules/dnd/locations/registry/registry_test.go b/internal/modules/dnd/locations/registry/registry_test.go index 9b0b648..77d0c00 100644 --- a/internal/modules/dnd/locations/registry/registry_test.go +++ b/internal/modules/dnd/locations/registry/registry_test.go @@ -155,16 +155,16 @@ func TestResolverReusesEquivalentCanonicalRegistries(t *testing.T) { } } -func registryFixture() dnd.LocationList { +func registryFixture() dnd.LocationRegistry { firstRefs := []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 1, EndUnitID: 1}} secondRefs := []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 2, EndUnitID: 2}} - return dnd.LocationList{Locations: []dnd.Location{ + return dnd.LocationRegistry{Locations: []dnd.Location{ {ID: identity.DeriveID("The Tavern", firstRefs), Name: "The Tavern", SourceRefs: firstRefs}, {ID: identity.DeriveID("The Tavern", secondRefs), Name: "The Tavern", SourceRefs: secondRefs}, }} } -func resolveList(t *testing.T, list dnd.LocationList) *Registry { +func resolveList(t *testing.T, list dnd.LocationRegistry) *Registry { t.Helper() registry, err := Resolve(referenceSet(item(encodeList(t, list)))) if err != nil { @@ -173,7 +173,7 @@ func resolveList(t *testing.T, list dnd.LocationList) *Registry { return registry } -func encodeList(t *testing.T, list dnd.LocationList) []byte { +func encodeList(t *testing.T, list dnd.LocationRegistry) []byte { t.Helper() content, err := locationcodec.New().Encode(list) if err != nil { diff --git a/internal/modules/dnd/normalize/locationoccurrences/normalizer.go b/internal/modules/dnd/normalize/locationoccurrences/normalizer.go index d6a3e0f..665eefa 100644 --- a/internal/modules/dnd/normalize/locationoccurrences/normalizer.go +++ b/internal/modules/dnd/normalize/locationoccurrences/normalizer.go @@ -327,7 +327,7 @@ func occurrenceScope(index int) string { return fmt.Sprintf("occurrences[%d]", i func referenceSlots() []contracts.ReferenceSlot { return []contracts.ReferenceSlot{{ Name: LocationRegistryReferenceSlot, Description: "Required normalized location registry used only for location identity grounding, never as occurrence evidence.", - Required: true, AcceptedMediaTypes: []string{"application/json"}, AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.LocationListKind}, MaxBytes: LocationRegistryMaxBytes, + Required: true, AcceptedMediaTypes: []string{"application/json"}, AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.LocationRegistryKind}, MaxBytes: LocationRegistryMaxBytes, }} } diff --git a/internal/modules/dnd/normalize/locationoccurrences/normalizer_test.go b/internal/modules/dnd/normalize/locationoccurrences/normalizer_test.go index f5eab31..0fc8f0a 100644 --- a/internal/modules/dnd/normalize/locationoccurrences/normalizer_test.go +++ b/internal/modules/dnd/normalize/locationoccurrences/normalizer_test.go @@ -133,7 +133,7 @@ func TestNormalizerContractsRequiredRegistryAndWarningBounds(t *testing.T) { doc.Units[index].ID = index + 1 input.Occurrences[index] = dnd.LocationOccurrence{LocationID: location.ID, Name: "not canonical", Kind: dnd.LocationOccurrenceKindMentioned, SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: count - index, EndUnitID: count - index}}} } - bounded, err := newNormalizer(t, registryReferences(t, dnd.LocationList{Locations: []dnd.Location{location}})).Normalize(context.Background(), normalizeRequest(input, doc, contracts.ReferenceSet{})) + bounded, err := newNormalizer(t, registryReferences(t, dnd.LocationRegistry{Locations: []dnd.Location{location}})).Normalize(context.Background(), normalizeRequest(input, doc, contracts.ReferenceSet{})) if err != nil || len(bounded.Warnings) != diagnostics.MaxWarnings || bounded.Warnings[len(bounded.Warnings)-1].ReasonCode != ReasonCodeWarningsOmitted { t.Fatalf("bounded warnings = %#v, %v", bounded.Warnings, err) } @@ -151,7 +151,7 @@ func assertLocationRegistryReferenceSlots(t *testing.T, owner string, slots []co Name: LocationRegistryReferenceSlot, Required: true, AcceptedMediaTypes: []string{"application/json"}, - AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.LocationListKind}, + AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.LocationRegistryKind}, MaxBytes: LocationRegistryMaxBytes, } if !reflect.DeepEqual(got, want) || strings.TrimSpace(description) == "" { @@ -172,16 +172,16 @@ func normalizeRequest(value dnd.LocationOccurrenceList, doc *source.SourceDocume return contracts.TypedNormalizeRequest[dnd.LocationOccurrenceList]{Source: doc, MergeOutput: contracts.MergeArtifact[dnd.LocationOccurrenceList]{Value: value}, References: references} } -func registryLocations(names ...string) dnd.LocationList { +func registryLocations(names ...string) dnd.LocationRegistry { locations := make([]dnd.Location, len(names)) for index, name := range names { refs := []source.SourceRef{{SourceID: "registry", StartUnitID: index + 1, EndUnitID: index + 1}} locations[index] = dnd.Location{ID: identity.DeriveID(name, refs), Name: name, SourceRefs: refs} } - return dnd.LocationList{Locations: locations} + return dnd.LocationRegistry{Locations: locations} } -func registryReferences(t *testing.T, locations dnd.LocationList) contracts.ReferenceSet { +func registryReferences(t *testing.T, locations dnd.LocationRegistry) contracts.ReferenceSet { t.Helper() content, err := locationcodec.New().Encode(locations) if err != nil { diff --git a/internal/modules/dnd/normalize/locations/normalizer.go b/internal/modules/dnd/normalize/locations/normalizer.go index 3b08c4e..8485c7d 100644 --- a/internal/modules/dnd/normalize/locations/normalizer.go +++ b/internal/modules/dnd/normalize/locations/normalizer.go @@ -40,7 +40,7 @@ const ( var requiredCapabilities = []string{"merged"} var providedCapabilities = []string{"normalized"} -var _ contracts.Normalizer[dnd.LocationList] = (*Normalizer)(nil) +var _ contracts.Normalizer[dnd.LocationRegistry] = (*Normalizer)(nil) var _ contracts.ManifestMetadataProvider = (*Normalizer)(nil) var _ pipeline.CheckpointFingerprintProvider = (*Normalizer)(nil) @@ -94,18 +94,18 @@ func (n *Normalizer) CheckpointFingerprints() []pipeline.CheckpointFingerprint { } } -func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalizeRequest[dnd.LocationList]) (contracts.TypedNormalizeResult[dnd.LocationList], error) { +func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalizeRequest[dnd.LocationRegistry]) (contracts.TypedNormalizeResult[dnd.LocationRegistry], error) { if n == nil { - return contracts.TypedNormalizeResult[dnd.LocationList]{}, normalizerErrorf("normalizer must not be nil") + return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, normalizerErrorf("normalizer must not be nil") } if n.llm == nil { - return contracts.TypedNormalizeResult[dnd.LocationList]{}, normalizerErrorf("LLM client must not be nil") + return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, normalizerErrorf("LLM client must not be nil") } if ctx == nil { - return contracts.TypedNormalizeResult[dnd.LocationList]{}, normalizerErrorf("context must not be nil") + return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, normalizerErrorf("context must not be nil") } if err := ctx.Err(); err != nil { - return contracts.TypedNormalizeResult[dnd.LocationList]{}, normalizerErrorf("context error before normalize: %w", err) + return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, normalizerErrorf("context error before normalize: %w", err) } order := shared.NewSourceRefOrder(req.Source) @@ -113,10 +113,10 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize deterministic := recordList(records) materials, ready, err := entityreconcile.BuildContext(req.Source, reconciliationCandidates(records), semanticContextRadius) if err != nil { - return contracts.TypedNormalizeResult[dnd.LocationList]{}, normalizerErrorf("build semantic context: %w", err) + return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, normalizerErrorf("build semantic context: %w", err) } if !ready { - return contracts.TypedNormalizeResult[dnd.LocationList]{Value: deterministic, Warnings: limitWarnings(warnings)}, nil + return contracts.TypedNormalizeResult[dnd.LocationRegistry]{Value: deterministic, Warnings: limitWarnings(warnings)}, nil } var response entityreconcile.ProposalResponse @@ -128,26 +128,26 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize if errors.Is(err, contracts.ErrInvalidStructuredOutput) { return n.invalidStructuredResult(deterministic, warnings), nil } - return contracts.TypedNormalizeResult[dnd.LocationList]{}, normalizerErrorf("complete structured output: %w", err) + return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, normalizerErrorf("complete structured output: %w", err) } assessment := materials.Assess(response) applied, semanticWarnings := applySafeGroups(records, reconciliationGroups(assessment, materials.CandidateKeys()), order) warnings = append(warnings, semanticWarnings...) if assessment.DiscardedGroups() == 0 { - return contracts.TypedNormalizeResult[dnd.LocationList]{Value: recordList(applied), Warnings: limitWarnings(warnings)}, nil + return contracts.TypedNormalizeResult[dnd.LocationRegistry]{Value: recordList(applied), Warnings: limitWarnings(warnings)}, nil } return retryResult(recordList(applied), warnings, assessment), nil } -func (n *Normalizer) invalidStructuredResult(value dnd.LocationList, warnings []contracts.Warning) contracts.TypedNormalizeResult[dnd.LocationList] { - return contracts.TypedNormalizeResult[dnd.LocationList]{Value: value, Warnings: limitWarningsForRetry(warnings), Retry: &contracts.NormalizeRetry{ +func (n *Normalizer) invalidStructuredResult(value dnd.LocationRegistry, warnings []contracts.Warning) contracts.TypedNormalizeResult[dnd.LocationRegistry] { + return contracts.TypedNormalizeResult[dnd.LocationRegistry]{Value: value, Warnings: limitWarningsForRetry(warnings), Retry: &contracts.NormalizeRetry{ ReasonCode: ReasonCodeLocationSemanticProposalInvalid, Message: "semantic proposal requires retry: invalid structured output", FallbackWarnings: []contracts.Warning{semanticFallbackWarning(-1)}, }} } -func retryResult(value dnd.LocationList, warnings []contracts.Warning, assessment entityreconcile.Assessment) contracts.TypedNormalizeResult[dnd.LocationList] { - return contracts.TypedNormalizeResult[dnd.LocationList]{Value: value, Warnings: limitWarningsForRetry(warnings), Retry: &contracts.NormalizeRetry{ +func retryResult(value dnd.LocationRegistry, warnings []contracts.Warning, assessment entityreconcile.Assessment) contracts.TypedNormalizeResult[dnd.LocationRegistry] { + return contracts.TypedNormalizeResult[dnd.LocationRegistry]{Value: value, Warnings: limitWarningsForRetry(warnings), Retry: &contracts.NormalizeRetry{ ReasonCode: ReasonCodeLocationSemanticProposalInvalid, Message: diagnostics.Aggregate("semantic proposal requires retry", reconciliationIssues(assessment)), FallbackWarnings: []contracts.Warning{semanticFallbackWarning(assessment.DiscardedGroups())}, }} @@ -183,7 +183,7 @@ type normalizedRecord struct { earliest int } -func preprocessRecords(input dnd.LocationList, order shared.SourceRefOrder) ([]normalizedRecord, []contracts.Warning) { +func preprocessRecords(input dnd.LocationRegistry, order shared.SourceRefOrder) ([]normalizedRecord, []contracts.Warning) { if input.Locations == nil { return nil, nil } @@ -294,11 +294,11 @@ func recordValues(records []normalizedRecord) []dnd.Location { } return values } -func recordList(records []normalizedRecord) dnd.LocationList { +func recordList(records []normalizedRecord) dnd.LocationRegistry { if records == nil { - return dnd.LocationList{} + return dnd.LocationRegistry{} } - return dnd.LocationList{Locations: recordValues(records)} + return dnd.LocationRegistry{Locations: recordValues(records)} } func duplicateWarning(retainedIndex int, removed []int) contracts.Warning { @@ -320,10 +320,10 @@ func duplicateWarning(retainedIndex int, removed []int) contracts.Warning { func locationScope(index int) string { return fmt.Sprintf("locations[%d]", index) } func ModuleSpec() pipeline.ModuleSpec { - return pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), ArtifactKind: dnd.LocationListKind} + return pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), ArtifactKind: dnd.LocationRegistryKind} } func Register(registry *pipeline.NormalizerRegistry) error { - return pipeline.RegisterNormalizerBuilder(registry, ModuleSpec(), validateOptions, func(request pipeline.BuildRequest) (contracts.Normalizer[dnd.LocationList], error) { + return pipeline.RegisterNormalizerBuilder(registry, ModuleSpec(), validateOptions, func(request pipeline.BuildRequest) (contracts.Normalizer[dnd.LocationRegistry], error) { options, err := DecodeOptions(request.Options) if err != nil { return nil, err diff --git a/internal/modules/dnd/normalize/locations/normalizer_test.go b/internal/modules/dnd/normalize/locations/normalizer_test.go index fd1dff4..e7c2120 100644 --- a/internal/modules/dnd/normalize/locations/normalizer_test.go +++ b/internal/modules/dnd/normalize/locations/normalizer_test.go @@ -16,7 +16,7 @@ import ( ) func TestModuleContractAndMetadata(t *testing.T) { - want := pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: []string{"merged"}, Provides: []string{"normalized"}, ArtifactKind: dnd.LocationListKind} + want := pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: []string{"merged"}, Provides: []string{"normalized"}, ArtifactKind: dnd.LocationRegistryKind} if got := ModuleSpec(); !reflect.DeepEqual(got, want) { t.Fatalf("ModuleSpec() = %#v, want %#v", got, want) } @@ -38,13 +38,13 @@ func TestModuleContractAndMetadata(t *testing.T) { } func TestNormalizePreparesOnlyExactDuplicatesAndRetainsSameNameAndNestedPlaces(t *testing.T) { - input := dnd.LocationList{Locations: []dnd.Location{ + input := dnd.LocationRegistry{Locations: []dnd.Location{ {Name: " The Tavern ", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}, {Name: "the tavern", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}, {Name: "The Tavern", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 2, EndUnitID: 2}}}, {Name: "The Tavern Cellar", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 2, EndUnitID: 2}}}, }} - before := dnd.LocationList{Locations: append([]dnd.Location(nil), input.Locations...)} + before := dnd.LocationRegistry{Locations: append([]dnd.Location(nil), input.Locations...)} result, err := newNormalizer(t, &recordingLocationNormalizerClient{}).Normalize(context.Background(), normalizeRequest(input)) if err != nil || len(result.Value.Locations) != 3 { t.Fatalf("Normalize() = %#v, %v; want one exact duplicate removed", result, err) @@ -63,7 +63,7 @@ func TestNormalizePreparesOnlyExactDuplicatesAndRetainsSameNameAndNestedPlaces(t func TestNormalizeAppliesSafeAliasGroupAndUsesOpaqueInputs(t *testing.T) { client := &recordingLocationNormalizerClient{response: `{"duplicate_groups":[{"members":["candidate-000001","candidate-000002"],"canonical":"candidate-000002"}]}`} doc := semanticDocument() - input := dnd.LocationList{Locations: []dnd.Location{ + input := dnd.LocationRegistry{Locations: []dnd.Location{ {Name: "Old Mill", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}}, {Name: "the Greencloak's refuge", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}}, {Name: "The Tavern", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}}}, @@ -84,7 +84,7 @@ func TestNormalizeAppliesSafeAliasGroupAndUsesOpaqueInputs(t *testing.T) { func TestNormalizeRejectsUnsafeAndOverlappingGroupsWithoutLosingCandidates(t *testing.T) { doc := semanticDocument() - input := dnd.LocationList{Locations: []dnd.Location{ + input := dnd.LocationRegistry{Locations: []dnd.Location{ {Name: "Old Mill", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}}, {Name: "Mill", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}}, {Name: "Tavern", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}}}, @@ -101,7 +101,7 @@ func TestNormalizeRejectsUnsafeAndOverlappingGroupsWithoutLosingCandidates(t *te func TestNormalizeHandlesRetryFallbackAndErrors(t *testing.T) { doc := semanticDocument() - input := dnd.LocationList{Locations: []dnd.Location{{Name: "Old Mill", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}}, {Name: "Mill", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}}}} + input := dnd.LocationRegistry{Locations: []dnd.Location{{Name: "Old Mill", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}}, {Name: "Mill", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}}}} invalid, err := newNormalizer(t, &recordingLocationNormalizerClient{err: contracts.ErrInvalidStructuredOutput}).Normalize(context.Background(), normalizeRequestWithSource(input, doc)) if err != nil || invalid.Retry == nil || invalid.Retry.ReasonCode != ReasonCodeLocationSemanticProposalInvalid { t.Fatalf("invalid result = %#v, %v", invalid, err) @@ -119,7 +119,7 @@ func TestNormalizeHandlesRetryFallbackAndErrors(t *testing.T) { func TestNormalizeOrdersEvidenceAndIsIdempotent(t *testing.T) { doc := &source.SourceDocument{ID: "ordered", Units: []source.SourceUnit{{ID: 30}, {ID: 10}}} - input := dnd.LocationList{Locations: []dnd.Location{{Name: "Old Mill", SourceRefs: []source.SourceRef{ + input := dnd.LocationRegistry{Locations: []dnd.Location{{Name: "Old Mill", SourceRefs: []source.SourceRef{ {SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}, {SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}, {SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}, diff --git a/internal/modules/dnd/normalize/locations/test_helpers_test.go b/internal/modules/dnd/normalize/locations/test_helpers_test.go index aed0a49..6e0b8cf 100644 --- a/internal/modules/dnd/normalize/locations/test_helpers_test.go +++ b/internal/modules/dnd/normalize/locations/test_helpers_test.go @@ -39,10 +39,10 @@ func newNormalizer(t *testing.T, client contracts.StructuredLLMClient) *Normaliz } return normalizer } -func normalizeRequest(value dnd.LocationList) contracts.TypedNormalizeRequest[dnd.LocationList] { - return contracts.TypedNormalizeRequest[dnd.LocationList]{MergeOutput: contracts.MergeArtifact[dnd.LocationList]{Value: value}} +func normalizeRequest(value dnd.LocationRegistry) contracts.TypedNormalizeRequest[dnd.LocationRegistry] { + return contracts.TypedNormalizeRequest[dnd.LocationRegistry]{MergeOutput: contracts.MergeArtifact[dnd.LocationRegistry]{Value: value}} } -func normalizeRequestWithSource(value dnd.LocationList, doc *source.SourceDocument) contracts.TypedNormalizeRequest[dnd.LocationList] { +func normalizeRequestWithSource(value dnd.LocationRegistry, doc *source.SourceDocument) contracts.TypedNormalizeRequest[dnd.LocationRegistry] { request := normalizeRequest(value) request.Source = doc return request diff --git a/internal/modules/dnd/register/evidence.go b/internal/modules/dnd/register/evidence.go index ea23227..f4f931d 100644 --- a/internal/modules/dnd/register/evidence.go +++ b/internal/modules/dnd/register/evidence.go @@ -26,7 +26,7 @@ func registerEvidence(registry *pipeline.ArtifactEvidenceRegistry) error { return pipeline.RegisterArtifactEvidence(registry, dnd.SceneDescriptionListKind, sceneDescriptionEvidence) }}, {name: "locations evidence", register: func() error { - return pipeline.RegisterArtifactEvidence(registry, dnd.LocationListKind, locationEvidence) + return pipeline.RegisterArtifactEvidence(registry, dnd.LocationRegistryKind, locationEvidence) }}, {name: "location occurrences evidence", register: func() error { return pipeline.RegisterArtifactEvidence(registry, dnd.LocationOccurrenceListKind, locationOccurrenceEvidence) @@ -90,7 +90,7 @@ func sceneDescriptionEvidence(value dnd.SceneDescriptionList) []source.SourceRef return refs } -func locationEvidence(value dnd.LocationList) []source.SourceRef { +func locationEvidence(value dnd.LocationRegistry) []source.SourceRef { var refs []source.SourceRef for _, location := range value.Locations { refs = append(refs, location.SourceRefs...) diff --git a/internal/modules/dnd/register/merge.go b/internal/modules/dnd/register/merge.go index 65b5546..d936374 100644 --- a/internal/modules/dnd/register/merge.go +++ b/internal/modules/dnd/register/merge.go @@ -152,7 +152,7 @@ func appendSceneDescriptionLists(values []dnd.SceneDescriptionList) (dnd.SceneDe return combined, nil } -func appendLocationLists(values []dnd.LocationList) (dnd.LocationList, error) { +func appendLocationRegistries(values []dnd.LocationRegistry) (dnd.LocationRegistry, error) { count := 0 present := false for _, value := range values { @@ -162,9 +162,9 @@ func appendLocationLists(values []dnd.LocationList) (dnd.LocationList, error) { count += len(value.Locations) } if !present { - return dnd.LocationList{}, nil + return dnd.LocationRegistry{}, nil } - combined := dnd.LocationList{Locations: make([]dnd.Location, 0, count)} + combined := dnd.LocationRegistry{Locations: make([]dnd.Location, 0, count)} for _, value := range values { for _, location := range value.Locations { combined.Locations = append(combined.Locations, cloneLocation(location)) diff --git a/internal/modules/dnd/register/modules.go b/internal/modules/dnd/register/modules.go index 8bee2fe..bed7fef 100644 --- a/internal/modules/dnd/register/modules.go +++ b/internal/modules/dnd/register/modules.go @@ -84,8 +84,8 @@ func registerModules(registries pipeline.Registries) error { {name: "scene-description-list appendorder merger", register: func() error { return appendorder.RegisterTyped(registries.Mergers, dnd.SceneDescriptionListKind, appendSceneDescriptionLists) }}, - {name: "location-list appendorder merger", register: func() error { - return appendorder.RegisterTyped(registries.Mergers, dnd.LocationListKind, appendLocationLists) + {name: "location-registry appendorder merger", register: func() error { + return appendorder.RegisterTyped(registries.Mergers, dnd.LocationRegistryKind, appendLocationRegistries) }}, {name: "location-occurrence-list appendorder merger", register: func() error { return appendorder.RegisterTyped(registries.Mergers, dnd.LocationOccurrenceListKind, appendLocationOccurrenceLists) @@ -120,8 +120,8 @@ func registerModules(registries pipeline.Registries) error { {name: "scene-description-list noop normalizer", register: func() error { return noop.RegisterTyped[dnd.SceneDescriptionList](registries.Normalizers, dnd.SceneDescriptionListKind) }}, - {name: "location-list noop normalizer", register: func() error { - return noop.RegisterTyped[dnd.LocationList](registries.Normalizers, dnd.LocationListKind) + {name: "location-registry noop normalizer", register: func() error { + return noop.RegisterTyped[dnd.LocationRegistry](registries.Normalizers, dnd.LocationRegistryKind) }}, {name: "location-occurrence-list noop normalizer", register: func() error { return noop.RegisterTyped[dnd.LocationOccurrenceList](registries.Normalizers, dnd.LocationOccurrenceListKind) diff --git a/internal/modules/dnd/register/register_test.go b/internal/modules/dnd/register/register_test.go index cbbd864..3e4e385 100644 --- a/internal/modules/dnd/register/register_test.go +++ b/internal/modules/dnd/register/register_test.go @@ -84,10 +84,10 @@ func TestRegisterAddsDNDFamily(t *testing.T) { assertContainsKeys(t, "chunkers", registries.Chunkers.RegisteredKeys(), []string{"dnd/scenes"}) assertContainsKeys(t, "extractors", registries.Extractors.RegisteredKeys(), []string{"dnd/spells", npcextract.Key, combatextract.Key, enemyeventextract.Key, itemeventextract.Key, occurrenceextract.Key, scenedescriptionextract.Key, locationextract.Key, locationoccurrenceextract.Key}) assertContainsKeys(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{spellnormalize.Key, npcnormalize.Key, combatnormalize.Key, enemyeventnormalize.Key, itemeventnormalize.Key, occurrencenormalize.Key, scenedescriptionnormalize.Key, locationnormalize.Key, locationoccurrencenormalize.Key, pipeline.DefaultNormalizeModule}) - assertContainsArtifactKinds(t, registries.ArtifactCodecs.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind}) - assertContainsArtifactKinds(t, registries.ArtifactEvidence.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind}) - assertContainsArtifactKinds(t, registries.Mergers.RegisteredArtifactKinds(pipeline.DefaultMergeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind}) - assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(pipeline.DefaultNormalizeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind}) + assertContainsArtifactKinds(t, registries.ArtifactCodecs.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationRegistryKind, dnd.LocationOccurrenceListKind}) + assertContainsArtifactKinds(t, registries.ArtifactEvidence.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationRegistryKind, dnd.LocationOccurrenceListKind}) + assertContainsArtifactKinds(t, registries.Mergers.RegisteredArtifactKinds(pipeline.DefaultMergeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationRegistryKind, dnd.LocationOccurrenceListKind}) + assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(pipeline.DefaultNormalizeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationRegistryKind, dnd.LocationOccurrenceListKind}) assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(npcnormalize.Key), []contracts.ArtifactKind{dnd.NPCRegistryKind}) assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(combatnormalize.Key), []contracts.ArtifactKind{dnd.CombatTurnListKind}) assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(enemyeventnormalize.Key), []contracts.ArtifactKind{dnd.EnemyEventListKind}) @@ -358,7 +358,7 @@ func TestRegisterAddsDNDFamily(t *testing.T) { } locationExtractSpec, locationExtractOK := registries.Extractors.Spec(locationextract.Key) locationNormalizeSpec, locationNormalizeOK := registries.Normalizers.Spec(locationnormalize.Key) - if !locationExtractOK || locationExtractSpec.ArtifactKind != dnd.LocationListKind || locationExtractSpec.ExecutionClass != contracts.ExecutionClassLLMBacked || !locationNormalizeOK || locationNormalizeSpec.ArtifactKind != dnd.LocationListKind || locationNormalizeSpec.ExecutionClass != contracts.ExecutionClassLLMBacked { + if !locationExtractOK || locationExtractSpec.ArtifactKind != dnd.LocationRegistryKind || locationExtractSpec.ExecutionClass != contracts.ExecutionClassLLMBacked || !locationNormalizeOK || locationNormalizeSpec.ArtifactKind != dnd.LocationRegistryKind || locationNormalizeSpec.ExecutionClass != contracts.ExecutionClassLLMBacked { t.Fatalf("location specs = %#v / %#v", locationExtractSpec, locationNormalizeSpec) } locationOccurrenceExtractSpec, locationOccurrenceExtractOK := registries.Extractors.Spec(locationoccurrenceextract.Key) @@ -371,7 +371,7 @@ func TestRegisterAddsDNDFamily(t *testing.T) { if len(locationOccurrenceExtractSpec.ReferenceSlots) != 5 || len(locationOccurrenceNormalizeSpec.ReferenceSlots) != 1 { t.Fatalf("location occurrence reference slots = %#v / %#v, want extractor campaign context and normalizer registry only", locationOccurrenceExtractSpec.ReferenceSlots, locationOccurrenceNormalizeSpec.ReferenceSlots) } - if !locationRegistrySlot.Required || !reflect.DeepEqual(locationRegistrySlot.AcceptedMediaTypes, []string{"application/json"}) || !reflect.DeepEqual(locationRegistrySlot.AcceptedArtifactKinds, []contracts.ArtifactKind{dnd.LocationListKind}) || locationRegistrySlot.MaxBytes != 1048576 || !sameReferenceSlotContract(locationRegistrySlot, occurrenceNormalizeRegistrySlot) { + if !locationRegistrySlot.Required || !reflect.DeepEqual(locationRegistrySlot.AcceptedMediaTypes, []string{"application/json"}) || !reflect.DeepEqual(locationRegistrySlot.AcceptedArtifactKinds, []contracts.ArtifactKind{dnd.LocationRegistryKind}) || locationRegistrySlot.MaxBytes != 1048576 || !sameReferenceSlotContract(locationRegistrySlot, occurrenceNormalizeRegistrySlot) { t.Fatalf("location registry slots disagree: %#v / %#v", occurrenceExtractSpec.ReferenceSlots, occurrenceNormalizeSpec.ReferenceSlots) } for _, name := range []string{"party", "roster", "players", "glossary"} { @@ -425,7 +425,7 @@ func TestEvidenceProjectorsPreserveDirectReferencesWithIndependentStorage(t *tes return sceneDescriptionEvidence(dnd.SceneDescriptionList{Scenes: []dnd.SceneDescription{{SourceRef: first}, {SourceRef: second}}}) }, want: []source.SourceRef{first, second}}, {name: "locations", project: func() []source.SourceRef { - return locationEvidence(dnd.LocationList{Locations: []dnd.Location{{SourceRefs: []source.SourceRef{first, second}}}}) + return locationEvidence(dnd.LocationRegistry{Locations: []dnd.Location{{SourceRefs: []source.SourceRef{first, second}}}}) }, want: []source.SourceRef{first, second}}, {name: "location occurrences", project: func() []source.SourceRef { return locationOccurrenceEvidence(dnd.LocationOccurrenceList{Occurrences: []dnd.LocationOccurrence{{SourceRefs: []source.SourceRef{first, second}}}}) @@ -490,19 +490,19 @@ func TestAppendNPCRegistriesPreservesOrderAndArrayPresence(t *testing.T) { } } -func TestAppendLocationListsPreserveOrderPresenceAndOwnership(t *testing.T) { +func TestAppendLocationRegistriesPreserveOrderPresenceAndOwnership(t *testing.T) { refs := []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}} - input := []dnd.LocationList{{Locations: []dnd.Location{{ID: "one", Name: "First", SourceRefs: refs}}}, {Locations: []dnd.Location{{ID: "two", Name: "Second", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 2, EndUnitID: 2}}}}}} - got, err := appendLocationLists(input) + input := []dnd.LocationRegistry{{Locations: []dnd.Location{{ID: "one", Name: "First", SourceRefs: refs}}}, {Locations: []dnd.Location{{ID: "two", Name: "Second", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 2, EndUnitID: 2}}}}}} + got, err := appendLocationRegistries(input) if err != nil || !reflect.DeepEqual([]string{got.Locations[0].Name, got.Locations[1].Name}, []string{"First", "Second"}) { - t.Fatalf("appendLocationLists() = %#v, %v", got, err) + t.Fatalf("appendLocationRegistries() = %#v, %v", got, err) } got.Locations[0].SourceRefs[0].StartUnitID = 99 if input[0].Locations[0].SourceRefs[0].StartUnitID != 1 { t.Fatal("merged locations share source-reference storage") } - for _, values := range [][]dnd.LocationList{nil, {{}, {}}} { - result, err := appendLocationLists(values) + for _, values := range [][]dnd.LocationRegistry{nil, {{}, {}}} { + result, err := appendLocationRegistries(values) if err != nil || result.Locations != nil { t.Fatalf("nil-only merge = %#v, %v", result, err) } diff --git a/internal/modules/dnd/register/validators.go b/internal/modules/dnd/register/validators.go index 9ac5f37..07bb97d 100644 --- a/internal/modules/dnd/register/validators.go +++ b/internal/modules/dnd/register/validators.go @@ -129,11 +129,11 @@ func registerValidators(registries pipeline.Registries) error { {name: "scene-description-list always reject validator", register: func() error { return alwaysreject.RegisterTyped[dnd.SceneDescriptionList](registries.Validators, dnd.SceneDescriptionListKind) }}, - {name: "location-list always accept validator", register: func() error { - return alwaysaccept.RegisterTyped[dnd.LocationList](registries.Validators, dnd.LocationListKind) + {name: "location-registry always accept validator", register: func() error { + return alwaysaccept.RegisterTyped[dnd.LocationRegistry](registries.Validators, dnd.LocationRegistryKind) }}, - {name: "location-list always reject validator", register: func() error { - return alwaysreject.RegisterTyped[dnd.LocationList](registries.Validators, dnd.LocationListKind) + {name: "location-registry always reject validator", register: func() error { + return alwaysreject.RegisterTyped[dnd.LocationRegistry](registries.Validators, dnd.LocationRegistryKind) }}, {name: "location-occurrence-list always accept validator", register: func() error { return alwaysaccept.RegisterTyped[dnd.LocationOccurrenceList](registries.Validators, dnd.LocationOccurrenceListKind) diff --git a/internal/modules/dnd/types.go b/internal/modules/dnd/types.go index 3d34849..674fa91 100644 --- a/internal/modules/dnd/types.go +++ b/internal/modules/dnd/types.go @@ -20,7 +20,7 @@ const ItemEventListKind contracts.ArtifactKind = "dnd/item-event-list" const EnemyEventListKind contracts.ArtifactKind = "dnd/enemy-event-list" -const LocationListKind contracts.ArtifactKind = "dnd/location-list" +const LocationRegistryKind contracts.ArtifactKind = "dnd/location-registry" const LocationOccurrenceListKind contracts.ArtifactKind = "dnd/location-occurrence-list" @@ -150,7 +150,7 @@ type EnemyEvent struct { SourceRefs []source.SourceRef `json:"source_refs"` } -type LocationList struct { +type LocationRegistry struct { Locations []Location `json:"locations"` } diff --git a/internal/modules/dnd/validate/locationoccurrences/registry/validator_test.go b/internal/modules/dnd/validate/locationoccurrences/registry/validator_test.go index cc8264a..0d25dec 100644 --- a/internal/modules/dnd/validate/locationoccurrences/registry/validator_test.go +++ b/internal/modules/dnd/validate/locationoccurrences/registry/validator_test.go @@ -103,7 +103,7 @@ func registryReferences(t *testing.T) (contracts.ReferenceSet, dnd.Location, dnd second := dnd.Location{ID: identity.DeriveID("Moon Gate", secondRefs), Name: "Moon Gate", SourceRefs: secondRefs} thirdRefs := []source.SourceRef{{SourceID: "source", StartUnitID: 3, EndUnitID: 3}} third := dnd.Location{ID: identity.DeriveID("Sun Gate", thirdRefs), Name: "Sun Gate", SourceRefs: thirdRefs} - content, err := locationcodec.New().Encode(dnd.LocationList{Locations: []dnd.Location{first, second, third}}) + content, err := locationcodec.New().Encode(dnd.LocationRegistry{Locations: []dnd.Location{first, second, third}}) if err != nil { t.Fatal(err) } diff --git a/internal/modules/dnd/validate/locations/identity/validator.go b/internal/modules/dnd/validate/locations/identity/validator.go index a8ed2d1..d932b68 100644 --- a/internal/modules/dnd/validate/locations/identity/validator.go +++ b/internal/modules/dnd/validate/locations/identity/validator.go @@ -22,7 +22,7 @@ const ( type Options struct{} type Validator struct{} -var _ contracts.TypedValidator[dnd.LocationList] = (*Validator)(nil) +var _ contracts.TypedValidator[dnd.LocationRegistry] = (*Validator)(nil) var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil) func New(Options) *Validator { return &Validator{} } @@ -34,7 +34,7 @@ func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint { return []pipeline.CheckpointFingerprint{{Name: "policy", Value: policy}} } -func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.LocationList]) (contracts.ValidationResult, error) { +func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.LocationRegistry]) (contracts.ValidationResult, error) { if err := locationshape.Validate(req.Value); err != nil { return contracts.ValidationResult{Approved: true}, nil } @@ -57,7 +57,7 @@ func Spec() pipeline.ValidatorSpec { } func Register(registry *pipeline.ValidatorRegistry) error { - return pipeline.RegisterTypedValidatorBuilder(registry, dnd.LocationListKind, Spec(), validateOptions, func(request pipeline.BuildRequest) (contracts.TypedValidator[dnd.LocationList], error) { + return pipeline.RegisterTypedValidatorBuilder(registry, dnd.LocationRegistryKind, Spec(), validateOptions, func(request pipeline.BuildRequest) (contracts.TypedValidator[dnd.LocationRegistry], error) { options, err := DecodeOptions(request.Options) if err != nil { return nil, err diff --git a/internal/modules/dnd/validate/locations/identity/validator_test.go b/internal/modules/dnd/validate/locations/identity/validator_test.go index 3345262..9e18610 100644 --- a/internal/modules/dnd/validate/locations/identity/validator_test.go +++ b/internal/modules/dnd/validate/locations/identity/validator_test.go @@ -16,7 +16,7 @@ import ( func TestValidatorAllowsSameNameAtDistinctEvidence(t *testing.T) { refs := []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}} otherRefs := []source.SourceRef{{SourceID: "session", StartUnitID: 2, EndUnitID: 2}} - value := dnd.LocationList{Locations: []dnd.Location{ + value := dnd.LocationRegistry{Locations: []dnd.Location{ {ID: domainidentity.DeriveID("Watchtower", refs), Name: "Watchtower", SourceRefs: refs}, {ID: domainidentity.DeriveID("Watchtower", otherRefs), Name: "Watchtower", SourceRefs: otherRefs}, }} @@ -28,12 +28,12 @@ func TestValidatorAllowsSameNameAtDistinctEvidence(t *testing.T) { } func TestValidatorDefersShapeAndRejectsDerivationAndDuplicateID(t *testing.T) { - result, err := New(Options{}).Validate(context.Background(), request(dnd.LocationList{Locations: []dnd.Location{{Name: "Missing"}}})) + result, err := New(Options{}).Validate(context.Background(), request(dnd.LocationRegistry{Locations: []dnd.Location{{Name: "Missing"}}})) if err != nil || !result.Approved { t.Fatalf("shape-invalid result = %#v, %v; want deferral", result, err) } refs := []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}} - value := dnd.LocationList{Locations: []dnd.Location{ + value := dnd.LocationRegistry{Locations: []dnd.Location{ {ID: "not-an-id", Name: "Gate", SourceRefs: refs}, {ID: domainidentity.DeriveID("Gate", refs), Name: "Other Gate", SourceRefs: refs}, {ID: domainidentity.DeriveID("Gate", refs), Name: "Gate", SourceRefs: refs}, @@ -65,6 +65,6 @@ func TestValidatorRegistersIdentityPolicy(t *testing.T) { } } -func request(value dnd.LocationList) contracts.TypedValidationRequest[dnd.LocationList] { - return contracts.TypedValidationRequest[dnd.LocationList]{Value: value} +func request(value dnd.LocationRegistry) contracts.TypedValidationRequest[dnd.LocationRegistry] { + return contracts.TypedValidationRequest[dnd.LocationRegistry]{Value: value} } diff --git a/internal/modules/dnd/validate/locations/shape/validator.go b/internal/modules/dnd/validate/locations/shape/validator.go index d303e93..c7ad495 100644 --- a/internal/modules/dnd/validate/locations/shape/validator.go +++ b/internal/modules/dnd/validate/locations/shape/validator.go @@ -21,7 +21,7 @@ const ( type Options struct{} type Validator struct{} -var _ contracts.TypedValidator[dnd.LocationList] = (*Validator)(nil) +var _ contracts.TypedValidator[dnd.LocationRegistry] = (*Validator)(nil) var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil) func New(Options) *Validator { return &Validator{} } @@ -33,7 +33,7 @@ func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint { return []pipeline.CheckpointFingerprint{{Name: "policy", Value: policy}} } -func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.LocationList]) (contracts.ValidationResult, error) { +func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.LocationRegistry]) (contracts.ValidationResult, error) { issues := issuesFor(req.Value) if len(issues) > 0 { return rejection(diagnostics.Aggregate("invalid location shape", issues)), nil @@ -41,7 +41,7 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq return contracts.ValidationResult{Approved: true}, nil } -func Validate(value dnd.LocationList) error { +func Validate(value dnd.LocationRegistry) error { issues := issuesFor(value) if len(issues) == 0 { return nil @@ -49,7 +49,7 @@ func Validate(value dnd.LocationList) error { return fmt.Errorf("%s", diagnostics.Aggregate("invalid location shape", issues)) } -func issuesFor(value dnd.LocationList) []string { +func issuesFor(value dnd.LocationRegistry) []string { if value.Locations == nil { return []string{"locations must be present"} } @@ -74,7 +74,7 @@ func Spec() pipeline.ValidatorSpec { } func Register(registry *pipeline.ValidatorRegistry) error { - return pipeline.RegisterTypedValidatorBuilder(registry, dnd.LocationListKind, Spec(), validateOptions, func(request pipeline.BuildRequest) (contracts.TypedValidator[dnd.LocationList], error) { + return pipeline.RegisterTypedValidatorBuilder(registry, dnd.LocationRegistryKind, Spec(), validateOptions, func(request pipeline.BuildRequest) (contracts.TypedValidator[dnd.LocationRegistry], error) { options, err := DecodeOptions(request.Options) if err != nil { return nil, err diff --git a/internal/modules/dnd/validate/locations/shape/validator_test.go b/internal/modules/dnd/validate/locations/shape/validator_test.go index e3e6d22..0e248ad 100644 --- a/internal/modules/dnd/validate/locations/shape/validator_test.go +++ b/internal/modules/dnd/validate/locations/shape/validator_test.go @@ -14,7 +14,7 @@ import ( ) func TestValidatorRejectsMissingRequiredFieldsWithoutMutation(t *testing.T) { - value := dnd.LocationList{Locations: []dnd.Location{{Name: strings.Repeat("火", 220), SourceRefs: []source.SourceRef{}}}} + value := dnd.LocationRegistry{Locations: []dnd.Location{{Name: strings.Repeat("火", 220), SourceRefs: []source.SourceRef{}}}} before := value result, err := New(Options{}).Validate(context.Background(), requestWithValue(value)) if err != nil || result.Approved || result.ReasonCode != ReasonCode || !strings.Contains(result.Message, "id must not be empty") || !strings.Contains(result.Message, "source_refs must not be empty") { @@ -23,14 +23,14 @@ func TestValidatorRejectsMissingRequiredFieldsWithoutMutation(t *testing.T) { if len(result.Message) > 4096 || !utf8.ValidString(result.Message) || !reflect.DeepEqual(value, before) { t.Fatalf("Validate() produced unsafe diagnostics or mutated value: %#v", value) } - result, err = New(Options{}).Validate(context.Background(), requestWithValue(dnd.LocationList{})) + result, err = New(Options{}).Validate(context.Background(), requestWithValue(dnd.LocationRegistry{})) if err != nil || result.Approved || !strings.Contains(result.Message, "locations must be present") { t.Fatalf("missing locations = %#v, %v; want rejection", result, err) } } func TestValidatorApprovesAndRegisters(t *testing.T) { - result, err := New(Options{}).Validate(context.Background(), requestWithValue(validLocationList())) + result, err := New(Options{}).Validate(context.Background(), requestWithValue(validLocationRegistry())) if err != nil || !result.Approved { t.Fatalf("Validate() = %#v, %v; want approval", result, err) } @@ -49,10 +49,10 @@ func TestValidatorApprovesAndRegisters(t *testing.T) { } } -func requestWithValue(value dnd.LocationList) contracts.TypedValidationRequest[dnd.LocationList] { - return contracts.TypedValidationRequest[dnd.LocationList]{Value: value} +func requestWithValue(value dnd.LocationRegistry) contracts.TypedValidationRequest[dnd.LocationRegistry] { + return contracts.TypedValidationRequest[dnd.LocationRegistry]{Value: value} } -func validLocationList() dnd.LocationList { - return dnd.LocationList{Locations: []dnd.Location{{ID: "candidate", Name: "Moon Gate", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}}} +func validLocationRegistry() dnd.LocationRegistry { + return dnd.LocationRegistry{Locations: []dnd.Location{{ID: "candidate", Name: "Moon Gate", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}}} } diff --git a/internal/modules/dnd/validate/locations/source_refs/validator.go b/internal/modules/dnd/validate/locations/source_refs/validator.go index a5cf403..b3202ad 100644 --- a/internal/modules/dnd/validate/locations/source_refs/validator.go +++ b/internal/modules/dnd/validate/locations/source_refs/validator.go @@ -22,7 +22,7 @@ const ( type Options struct{} type Validator struct{} -var _ contracts.TypedValidator[dnd.LocationList] = (*Validator)(nil) +var _ contracts.TypedValidator[dnd.LocationRegistry] = (*Validator)(nil) var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil) func New(Options) *Validator { return &Validator{} } @@ -34,7 +34,7 @@ func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint { return []pipeline.CheckpointFingerprint{{Name: "policy", Value: policy}} } -func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.LocationList]) (contracts.ValidationResult, error) { +func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.LocationRegistry]) (contracts.ValidationResult, error) { if req.Stage == string(pipeline.StageExtract) && req.Chunk == nil { return contracts.ValidationResult{}, fmt.Errorf("location source-reference validator requires the current extraction chunk") } @@ -78,7 +78,7 @@ func Spec() pipeline.ValidatorSpec { } func Register(registry *pipeline.ValidatorRegistry) error { - return pipeline.RegisterTypedValidatorBuilder(registry, dnd.LocationListKind, Spec(), validateOptions, func(request pipeline.BuildRequest) (contracts.TypedValidator[dnd.LocationList], error) { + return pipeline.RegisterTypedValidatorBuilder(registry, dnd.LocationRegistryKind, Spec(), validateOptions, func(request pipeline.BuildRequest) (contracts.TypedValidator[dnd.LocationRegistry], error) { options, err := DecodeOptions(request.Options) if err != nil { return nil, err diff --git a/internal/modules/dnd/validate/locations/source_refs/validator_test.go b/internal/modules/dnd/validate/locations/source_refs/validator_test.go index daac909..b53084b 100644 --- a/internal/modules/dnd/validate/locations/source_refs/validator_test.go +++ b/internal/modules/dnd/validate/locations/source_refs/validator_test.go @@ -14,7 +14,7 @@ import ( ) func TestValidatorRejectsForeignAndOutOfRangeReferencesWithoutMutation(t *testing.T) { - value := validLocationList() + value := validLocationRegistry() value.Locations[0].SourceRefs = []source.SourceRef{{SourceID: "foreign", StartUnitID: 1, EndUnitID: 1}, {SourceID: "session", StartUnitID: 9, EndUnitID: 9}} before := value result, err := New(Options{}).Validate(context.Background(), request(validDocument(), value)) @@ -27,11 +27,11 @@ func TestValidatorRejectsForeignAndOutOfRangeReferencesWithoutMutation(t *testin } func TestValidatorApprovesValidReferencesAndDefersMalformedShape(t *testing.T) { - result, err := New(Options{}).Validate(context.Background(), request(validDocument(), validLocationList())) + result, err := New(Options{}).Validate(context.Background(), request(validDocument(), validLocationRegistry())) if err != nil || !result.Approved { t.Fatalf("valid references = %#v, %v", result, err) } - result, err = New(Options{}).Validate(context.Background(), request(nil, dnd.LocationList{Locations: []dnd.Location{{Name: "Missing"}}})) + result, err = New(Options{}).Validate(context.Background(), request(nil, dnd.LocationRegistry{Locations: []dnd.Location{{Name: "Missing"}}})) if err != nil || !result.Approved || result.ReasonCode != "" { t.Fatalf("malformed shape = %#v, %v; want deferral", result, err) } @@ -39,8 +39,8 @@ func TestValidatorApprovesValidReferencesAndDefersMalformedShape(t *testing.T) { func TestValidatorRequiresCurrentChunkAndRejectsEvidenceOutsideIt(t *testing.T) { doc := &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 1}, {ID: 2}}} - value := validLocationList() - result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.LocationList]{ + value := validLocationRegistry() + result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.LocationRegistry]{ Stage: string(pipeline.StageExtract), Source: doc, Chunk: &source.Chunk{SourceID: "session", Units: []source.SourceUnit{{ID: 1}}}, Value: value, }) @@ -48,14 +48,14 @@ func TestValidatorRequiresCurrentChunkAndRejectsEvidenceOutsideIt(t *testing.T) t.Fatalf("contained extraction evidence = %#v, %v", result, err) } value.Locations[0].SourceRefs[0].EndUnitID = 2 - result, err = New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.LocationList]{ + result, err = New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.LocationRegistry]{ Stage: string(pipeline.StageExtract), Source: doc, Chunk: &source.Chunk{SourceID: "session", Units: []source.SourceUnit{{ID: 1}}}, Value: value, }) if err != nil || result.Approved || !strings.Contains(result.Message, "outside the current extraction chunk") { t.Fatalf("out-of-chunk extraction evidence = %#v, %v", result, err) } - _, err = New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.LocationList]{Stage: string(pipeline.StageExtract), Source: doc, Value: validLocationList()}) + _, err = New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.LocationRegistry]{Stage: string(pipeline.StageExtract), Source: doc, Value: validLocationRegistry()}) if err == nil || !strings.Contains(err.Error(), "requires the current extraction chunk") { t.Fatalf("missing chunk error = %v", err) } @@ -74,14 +74,14 @@ func TestValidatorRegisters(t *testing.T) { } } -func request(doc *source.SourceDocument, value dnd.LocationList) contracts.TypedValidationRequest[dnd.LocationList] { - return contracts.TypedValidationRequest[dnd.LocationList]{Source: doc, Value: value} +func request(doc *source.SourceDocument, value dnd.LocationRegistry) contracts.TypedValidationRequest[dnd.LocationRegistry] { + return contracts.TypedValidationRequest[dnd.LocationRegistry]{Source: doc, Value: value} } func validDocument() *source.SourceDocument { return &source.SourceDocument{ID: "session", Kind: "transcript", Format: "application/json", Digest: "sha256:session", Units: []source.SourceUnit{{ID: 1, Kind: "message", Text: "Moon Gate opens."}}} } -func validLocationList() dnd.LocationList { - return dnd.LocationList{Locations: []dnd.Location{{ID: "candidate", Name: "Moon Gate", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}}} +func validLocationRegistry() dnd.LocationRegistry { + return dnd.LocationRegistry{Locations: []dnd.Location{{ID: "candidate", Name: "Moon Gate", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}}} } diff --git a/internal/modules/dnd/validate/locations/source_relatedness/validator.go b/internal/modules/dnd/validate/locations/source_relatedness/validator.go index 60a9813..ac447be 100644 --- a/internal/modules/dnd/validate/locations/source_relatedness/validator.go +++ b/internal/modules/dnd/validate/locations/source_relatedness/validator.go @@ -23,7 +23,7 @@ const ( type Options struct{} type Validator struct{} -var _ contracts.TypedValidator[dnd.LocationList] = (*Validator)(nil) +var _ contracts.TypedValidator[dnd.LocationRegistry] = (*Validator)(nil) var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil) func New(Options) *Validator { return &Validator{} } @@ -35,7 +35,7 @@ func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint { return []pipeline.CheckpointFingerprint{{Name: "policy", Value: policy}} } -func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.LocationList]) (contracts.ValidationResult, error) { +func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.LocationRegistry]) (contracts.ValidationResult, error) { if err := locationshape.Validate(req.Value); err != nil { return contracts.ValidationResult{Approved: true}, nil } @@ -69,7 +69,7 @@ func Spec() pipeline.ValidatorSpec { } func Register(registry *pipeline.ValidatorRegistry) error { - return pipeline.RegisterTypedValidatorBuilder(registry, dnd.LocationListKind, Spec(), validateOptions, func(request pipeline.BuildRequest) (contracts.TypedValidator[dnd.LocationList], error) { + return pipeline.RegisterTypedValidatorBuilder(registry, dnd.LocationRegistryKind, Spec(), validateOptions, func(request pipeline.BuildRequest) (contracts.TypedValidator[dnd.LocationRegistry], error) { options, err := DecodeOptions(request.Options) if err != nil { return nil, err diff --git a/internal/modules/dnd/validate/locations/source_relatedness/validator_test.go b/internal/modules/dnd/validate/locations/source_relatedness/validator_test.go index 478b4f9..db5b21d 100644 --- a/internal/modules/dnd/validate/locations/source_relatedness/validator_test.go +++ b/internal/modules/dnd/validate/locations/source_relatedness/validator_test.go @@ -14,7 +14,7 @@ import ( ) func TestValidatorUsesOnlyCitedTranscriptText(t *testing.T) { - value := dnd.LocationList{Locations: []dnd.Location{{ID: "candidate", Name: "O'Rin's Gate", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}}} + value := dnd.LocationRegistry{Locations: []dnd.Location{{ID: "candidate", Name: "O'Rin's Gate", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}}} doc := &source.SourceDocument{ID: "session", Kind: "transcript", Format: "application/json", Digest: "sha256:session", Units: []source.SourceUnit{{ID: 1, Kind: "message", Text: "The party enters o’rin’s gate."}, {ID: 2, Kind: "message", Text: "Unrelated location."}}} result, err := New(Options{}).Validate(context.Background(), request(doc, value, contracts.ReferenceSet{})) if err != nil || !result.Approved || len(result.Warnings) != 0 { @@ -30,12 +30,12 @@ func TestValidatorUsesOnlyCitedTranscriptText(t *testing.T) { } func TestValidatorDefersMalformedOrUnreadableCitations(t *testing.T) { - invalid := dnd.LocationList{Locations: []dnd.Location{{Name: "Missing"}}} + invalid := dnd.LocationRegistry{Locations: []dnd.Location{{Name: "Missing"}}} result, err := New(Options{}).Validate(context.Background(), request(document(), invalid, contracts.ReferenceSet{})) if err != nil || !result.Approved || len(result.Warnings) != 0 { t.Fatalf("shape deferral = %#v, %v", result, err) } - value := dnd.LocationList{Locations: []dnd.Location{{ID: "candidate", Name: "Missing", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 99, EndUnitID: 99}}}}} + value := dnd.LocationRegistry{Locations: []dnd.Location{{ID: "candidate", Name: "Missing", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 99, EndUnitID: 99}}}}} result, err = New(Options{}).Validate(context.Background(), request(document(), value, contracts.ReferenceSet{})) if err != nil || !result.Approved || len(result.Warnings) != 0 { t.Fatalf("unreadable citation = %#v, %v", result, err) @@ -60,14 +60,14 @@ func TestValidatorBoundsWarnings(t *testing.T) { for index := range locations { locations[index] = dnd.Location{ID: "candidate", Name: "Missing", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}} } - result, err := New(Options{}).Validate(context.Background(), request(document(), dnd.LocationList{Locations: locations}, contracts.ReferenceSet{})) + result, err := New(Options{}).Validate(context.Background(), request(document(), dnd.LocationRegistry{Locations: locations}, contracts.ReferenceSet{})) if err != nil || !result.Approved || len(result.Warnings) != diagnostics.MaxWarnings || result.Warnings[len(result.Warnings)-1].ReasonCode != OmittedReasonCode { t.Fatalf("bounded warnings = %#v, %v", result, err) } } -func request(doc *source.SourceDocument, value dnd.LocationList, references contracts.ReferenceSet) contracts.TypedValidationRequest[dnd.LocationList] { - return contracts.TypedValidationRequest[dnd.LocationList]{Source: doc, References: references, Value: value} +func request(doc *source.SourceDocument, value dnd.LocationRegistry, references contracts.ReferenceSet) contracts.TypedValidationRequest[dnd.LocationRegistry] { + return contracts.TypedValidationRequest[dnd.LocationRegistry]{Source: doc, References: references, Value: value} } func document() *source.SourceDocument { diff --git a/internal/modules/integration/dnd_locations_runner_test.go b/internal/modules/integration/dnd_locations_runner_test.go index 719656b..ae7912a 100644 --- a/internal/modules/integration/dnd_locations_runner_test.go +++ b/internal/modules/integration/dnd_locations_runner_test.go @@ -33,7 +33,7 @@ func TestLocationRegistryHandoffProducesOccurrencesAndEvidence(t *testing.T) { } registryOutput := normalizedLane(t, output, "locations") - if registryOutput.Artifact.Kind != dnd.LocationListKind || registryOutput.Artifact.Schema.ID != locationcodec.SchemaID { + if registryOutput.Artifact.Kind != dnd.LocationRegistryKind || registryOutput.Artifact.Schema.ID != locationcodec.SchemaID { t.Fatalf("registry envelope = %#v, want durable location artifact", registryOutput) } registry, err := locationcodec.New().Decode(registryOutput.Artifact.Content)