Adopt location registry durable contract

This commit is contained in:
2026-08-05 19:05:42 +00:00
parent 2f61118e78
commit c006b163d5
41 changed files with 196 additions and 196 deletions

View File

@@ -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 | | `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 | | `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 | | `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 Scene descriptions accept **party**, **players**, and **glossary**, but not
**roster**. NPC occurrences require **npc_registry** for both extraction and **roster**. NPC occurrences require **npc_registry** for both extraction and

View File

@@ -1,4 +1,4 @@
# D&D Location Artifact # D&D Location Registry Artifact
This contract defines the durable, source-grounded location registry produced This contract defines the durable, source-grounded location registry produced
by `dnd/locations`. It records transcript-established physical places for one by `dnd/locations`. It records transcript-established physical places for one
@@ -9,12 +9,12 @@ registry, or location description.
| Property | Value | | Property | Value |
| --- | --- | | --- | --- |
| Artifact kind | `dnd/location-list` | | Artifact kind | `dnd/location-registry` |
| Schema ID | `notarius.dnd.locations` | | Schema ID | `notarius.dnd.location_registry` |
| Schema name | `notarius_dnd_locations_v1` | | Schema name | `notarius_dnd_location_registry_v1` |
| Schema version | `v1` | | Schema version | `v1` |
| Media type | `application/json` | | 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 `v1` accepts one strict JSON object with required `locations`; the array may be
empty. Location and source-reference objects reject unknown fields. An empty. Location and source-reference objects reject unknown fields. An
@@ -39,7 +39,7 @@ identifiers, and the start may not follow the end.
{ {
"locations": [ "locations": [
{ {
"id": "location:sha256:5c1a91f15729df0b8c257093865fdf2452b43c215375e8cf2341aa9c37bb99aa", "id": "location:sha256:fb05475da0fc7debf994b517e1906ffe7209887a6a1ec306356d84de820b1a24",
"name": "Moon Gate", "name": "Moon Gate",
"source_refs": [ "source_refs": [
{"source_id": "session-7", "start_unit_id": 4, "end_unit_id": 5} {"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: for this array, using the earliest canonical source reference as the anchor:
```text ```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 The canonical ID is the lowercase SHA-256 digest of those bytes with the

View File

@@ -38,7 +38,7 @@ positive and the start may not follow the end.
{ {
"occurrences": [ "occurrences": [
{ {
"location_id": "location:sha256:5c1a91f15729df0b8c257093865fdf2452b43c215375e8cf2341aa9c37bb99aa", "location_id": "location:sha256:fb05475da0fc7debf994b517e1906ffe7209887a6a1ec306356d84de820b1a24",
"name": "Moon Gate", "name": "Moon Gate",
"kind": "visited", "kind": "visited",
"source_refs": [ "source_refs": [
@@ -72,7 +72,7 @@ complete canonical evidence sequence.
## Required grounding and evidence ## Required grounding and evidence
Both extraction and normalization require exactly one `locations` reference of 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 registry provides identity grounding only: unknown IDs and mismatched ID/name
pairs are rejected rather than guessed or reassigned. The current transcript is pairs are rejected rather than guessed or reassigned. The current transcript is
the only evidence source for an occurrence; registry evidence and provenance the only evidence source for an occurrence; registry evidence and provenance

View File

@@ -143,7 +143,7 @@ func TestMaintainedCompleteExampleProducesEnemyEventsThroughGeneratedHandoffs(t
if locationOutput.File != "lanes/locations.json" || locationOutput.SchemaID != locationcodec.SchemaID || locationOutput.SchemaVersion != locationcodec.SchemaVersion { 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) 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 { 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) t.Fatalf("location registry = %#v, want distinct source-grounded identities for same-name locations", locationsValue)
} }

View File

@@ -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) t.Fatalf("complete example steps and lanes = %v, want the documented D&D extractor composition", got)
} }
locationLane := referenceContractLane(t, materialized, "locations") 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) t.Fatalf("location lane = %#v, want typed registry composition", locationLane)
} }
occurrenceLane := referenceContractLane(t, materialized, "location-occurrences") occurrenceLane := referenceContractLane(t, materialized, "location-occurrences")

View File

@@ -1,7 +1,7 @@
{ {
"occurrences": [ "occurrences": [
{ {
"location_id": "location:sha256:cdd2b57615b56a4e92d506cf053cd3985e11d4df4895c5383b30ec2ce9f39ed0", "location_id": "location:sha256:4ea39088943a692f120ae8740419c3baa1e7026c0e2a26ab03255f0b7e20215c",
"name": "The Old Tavern", "name": "The Old Tavern",
"kind": "visited", "kind": "visited",
"source_refs": [ "source_refs": [

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "notarius.dnd.locations", "$id": "notarius.dnd.location_registry",
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["locations"], "required": ["locations"],

View File

@@ -13,25 +13,25 @@ import (
) )
const ( const (
SchemaID = "notarius.dnd.locations" SchemaID = "notarius.dnd.location_registry"
SchemaName = "notarius_dnd_locations_v1" SchemaName = "notarius_dnd_location_registry_v1"
SchemaVersion = "v1" SchemaVersion = "v1"
MediaType = "application/json" 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 schemaAssets embed.FS
var _ contracts.ArtifactCodec[dnd.LocationList] = (*Codec)(nil) var _ contracts.ArtifactCodec[dnd.LocationRegistry] = (*Codec)(nil)
type Codec struct{} type Codec struct{}
func New() *Codec { return &Codec{} } 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 { 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 { if err != nil {
return contracts.ArtifactSchema{} return contracts.ArtifactSchema{}
} }
@@ -45,41 +45,41 @@ func (c *Codec) Schema() contracts.ArtifactSchema {
func (c *Codec) MediaType() string { return MediaType } 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)} 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 { 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) return c.EncodeCandidate(value)
} }
// EncodeCandidate provides the durable representation before semantic // EncodeCandidate provides the durable representation before semantic
// validators have approved a value. // validators have approved a value.
func (c *Codec) EncodeCandidate(value dnd.LocationList) ([]byte, error) { func (c *Codec) EncodeCandidate(value dnd.LocationRegistry) ([]byte, error) {
return candidatejson.EncodeCandidate("dnd location list", value) 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) value, err := c.DecodeCandidate(content)
if err != nil { if err != nil {
return dnd.LocationList{}, err return dnd.LocationRegistry{}, err
} }
if err := validate(value); err != nil { 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 return value, nil
} }
// DecodeCandidate reads one strict durable JSON value before semantic // DecodeCandidate reads one strict durable JSON value before semantic
// validators have approved it. // validators have approved it.
func (c *Codec) DecodeCandidate(content []byte) (dnd.LocationList, error) { func (c *Codec) DecodeCandidate(content []byte) (dnd.LocationRegistry, error) {
return candidatejson.DecodeCandidate[dnd.LocationList]("dnd location list", content) 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 { if value.Locations == nil {
return fmt.Errorf("locations must be present") return fmt.Errorf("locations must be present")
} }

View File

@@ -15,9 +15,9 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/locations/identity" "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}} 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), ID: identity.DeriveID("The Old Tavern", refs),
Name: "The Old Tavern", Name: "The Old Tavern",
SourceRefs: refs, SourceRefs: refs,
@@ -25,7 +25,7 @@ func validList() dnd.LocationList {
} }
func TestCodecMatchesMaintainedDurableFixture(t *testing.T) { 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 { if err != nil {
t.Fatalf("read durable fixture: %v", err) t.Fatalf("read durable fixture: %v", err)
} }
@@ -53,7 +53,7 @@ func TestCodecMatchesMaintainedDurableFixture(t *testing.T) {
func TestCodecOwnsDurableSchemaAndMetadata(t *testing.T) { func TestCodecOwnsDurableSchemaAndMetadata(t *testing.T) {
codec := New() codec := New()
schema := codec.Schema() 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()) t.Fatalf("codec identity = %q/%q", codec.Kind(), codec.MediaType())
} }
if schema.ID != SchemaID || schema.Name != SchemaName || schema.Version != SchemaVersion || !json.Valid(schema.JSONSchema) { 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 { if err := pipeline.RegisterArtifactCodec(registry, codec); err != nil {
t.Fatalf("RegisterArtifactCodec() error = %v", err) 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) t.Fatalf("registered spec = %#v, %t", spec, ok)
} }
first := schema.JSONSchema first := schema.JSONSchema
@@ -84,11 +84,11 @@ func TestCodecOwnsDurableSchemaAndMetadata(t *testing.T) {
func TestCodecSupportsEmptyListsAndCandidateSemanticFailures(t *testing.T) { func TestCodecSupportsEmptyListsAndCandidateSemanticFailures(t *testing.T) {
codec := New() 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":[]}` { if content, err := codec.Encode(empty); err != nil || string(content) != `{"locations":[]}` {
t.Fatalf("Encode() = %s, %v", content, err) t.Fatalf("Encode() = %s, %v", content, err)
} }
for _, candidate := range []dnd.LocationList{ for _, candidate := range []dnd.LocationRegistry{
{}, {},
empty, empty,
{Locations: []dnd.Location{{ID: "bad", Name: " ", SourceRefs: nil}}}, {Locations: []dnd.Location{{ID: "bad", Name: " ", SourceRefs: nil}}},
@@ -109,7 +109,7 @@ func TestCodecSupportsEmptyListsAndCandidateSemanticFailures(t *testing.T) {
func TestCodecRejectsStructuralJSONBeforeSemanticApproval(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}]}]}` 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 }{ 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 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 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"}, {"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] base := validList().Locations[0]
for _, test := range []struct { for _, test := range []struct {
name string name string
value dnd.LocationList value dnd.LocationRegistry
want string want string
}{ }{
{"nil locations", dnd.LocationList{}, "locations must be present"}, {"nil locations", dnd.LocationRegistry{}, "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"}, {"invalid ID", dnd.LocationRegistry{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"}, {"blank name", dnd.LocationRegistry{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"}, {"empty source refs", dnd.LocationRegistry{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"}, {"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) { t.Run(test.name, func(t *testing.T) {
if _, err := New().Encode(test.value); err == nil || !strings.Contains(err.Error(), test.want) { if _, err := New().Encode(test.value); err == nil || !strings.Contains(err.Error(), test.want) {

View File

@@ -1,7 +1,7 @@
{ {
"locations": [ "locations": [
{ {
"id": "location:sha256:cdd2b57615b56a4e92d506cf053cd3985e11d4df4895c5383b30ec2ce9f39ed0", "id": "location:sha256:4ea39088943a692f120ae8740419c3baa1e7026c0e2a26ab03255f0b7e20215c",
"name": "The Old Tavern", "name": "The Old Tavern",
"source_refs": [ "source_refs": [
{"source_id": "session-alpha", "start_unit_id": 1, "end_unit_id": 2} {"source_id": "session-alpha", "start_unit_id": 1, "end_unit_id": 2}

View File

@@ -40,7 +40,7 @@ func referenceSlots() []contracts.ReferenceSlot {
Description: "Required normalized location registry used only for location identity grounding, never as occurrence evidence.", Description: "Required normalized location registry used only for location identity grounding, never as occurrence evidence.",
Required: true, Required: true,
AcceptedMediaTypes: []string{"application/json"}, AcceptedMediaTypes: []string{"application/json"},
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.LocationListKind}, AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.LocationRegistryKind},
MaxBytes: LocationRegistryMaxBytes, MaxBytes: LocationRegistryMaxBytes,
}) })
sort.Slice(slots, func(left, right int) bool { return slots[left].Name < slots[right].Name }) sort.Slice(slots, func(left, right int) bool { return slots[left].Name < slots[right].Name })

View File

@@ -111,7 +111,7 @@ func TestExtractRequiresRegistryAndAcceptsEmptyRegistryWithNoOccurrences(t *test
if len(client.requests) != 0 { if len(client.requests) != 0 {
t.Fatalf("LLM calls = %d", len(client.requests)) t.Fatalf("LLM calls = %d", len(client.requests))
} }
empty := dnd.LocationList{Locations: []dnd.Location{}} empty := dnd.LocationRegistry{Locations: []dnd.Location{}}
references := registryReferences(t, empty) references := registryReferences(t, empty)
req := extractionRequest() req := extractionRequest()
req.References = references req.References = references
@@ -196,7 +196,7 @@ func TestExtractorContractsMetadataAndFailures(t *testing.T) {
slot = candidate 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) t.Fatalf("location registry slot = %#v", slot)
} }
registry := pipeline.NewExtractorRegistry() 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() t.Helper()
locations := make([]dnd.Location, len(names)) locations := make([]dnd.Location, len(names))
for index, name := range names { for index, name := range names {
refs := []source.SourceRef{{SourceID: "other-session", StartUnitID: index + 1, EndUnitID: index + 1}} 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} 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() t.Helper()
content, err := locationcodec.New().Encode(locations) content, err := locationcodec.New().Encode(locations)
if err != nil { if err != nil {

View File

@@ -47,9 +47,9 @@ func canonicalizeLocation(location *locationResponse, order shared.SourceRefOrde
return order.EarliestValid(refs) return order.EarliestValid(refs)
} }
func canonicalLocationList(response extractionResponse, sourceID string) dnd.LocationList { func canonicalLocationRegistry(response extractionResponse, sourceID string) dnd.LocationRegistry {
if response.Locations == nil { if response.Locations == nil {
return dnd.LocationList{Locations: nil} return dnd.LocationRegistry{Locations: nil}
} }
locations := make([]dnd.Location, len(response.Locations)) locations := make([]dnd.Location, len(response.Locations))
for index, location := range response.Locations { for index, location := range response.Locations {
@@ -60,7 +60,7 @@ func canonicalLocationList(response extractionResponse, sourceID string) dnd.Loc
SourceRefs: refs, SourceRefs: refs,
} }
} }
return dnd.LocationList{Locations: locations} return dnd.LocationRegistry{Locations: locations}
} }
func canonicalSourceRefs(values []locationSourceRefResponse, sourceID string) []source.SourceRef { func canonicalSourceRefs(values []locationSourceRefResponse, sourceID string) []source.SourceRef {

View File

@@ -31,7 +31,7 @@ func referenceSlots() []contracts.ReferenceSlot {
return shared.ReferenceSlots(referenceSlotDescriptions) return shared.ReferenceSlots(referenceSlotDescriptions)
} }
var _ contracts.Extractor[dnd.LocationList] = (*Extractor)(nil) var _ contracts.Extractor[dnd.LocationRegistry] = (*Extractor)(nil)
var _ contracts.ManifestMetadataProvider = (*Extractor)(nil) var _ contracts.ManifestMetadataProvider = (*Extractor)(nil)
var _ pipeline.CheckpointFingerprintProvider = (*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 { 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 { 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) sourceInput, err := shared.PrepareChunkExtraction(ctx, req)
if err != nil { if err != nil {
return contracts.TypedExtractionResult[dnd.LocationList]{}, extractorErrorf("%w", err) return contracts.TypedExtractionResult[dnd.LocationRegistry]{}, extractorErrorf("%w", err)
} }
order := shared.NewSourceRefOrder(req.Source) 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, ProfileID: req.LLMProfile, SessionID: req.SessionID,
Inputs: shared.PromptInputs(sourceInput, req.References), Inputs: shared.PromptInputs(sourceInput, req.References),
}, &response); err != nil { }, &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) 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 { func ModuleSpec() pipeline.ModuleSpec {
return pipeline.ModuleSpec{ return pipeline.ModuleSpec{
Key: Key, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked, Key: Key, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked,
Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), 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 { 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) options, err := DecodeOptions(request.Options)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -23,7 +23,7 @@ func TestExtractMapsLocationsWithOwnedEvidenceAndDeterministicOrder(t *testing.T
t.Fatalf("Extract() error = %v, want nil", err) 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}} 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("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}}}, {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}}},
}} }}

View File

@@ -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") { 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) 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) { if got := ModuleSpec(); !reflect.DeepEqual(got, want) {
t.Fatalf("ModuleSpec() = %#v, want %#v", got, want) t.Fatalf("ModuleSpec() = %#v, want %#v", got, want)
} }

View File

@@ -16,7 +16,7 @@ import (
const ( const (
// IdentityPolicy identifies the durable location ID derivation policy. // 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 is an alias for IdentityPolicy.
Policy = IdentityPolicy Policy = IdentityPolicy
@@ -109,7 +109,7 @@ func IsValidID(value string) bool {
func ValidID(value string) bool { return IsValidID(value) } func ValidID(value string) bool { return IsValidID(value) }
// ValidateList validates the identity members of list. // 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 // ValidateRegistry validates location IDs without modifying records or their
// source references. Equal comparison names are allowed because their evidence // source references. Equal comparison names are allowed because their evidence

View File

@@ -32,7 +32,7 @@ func TestDeriveIDUsesDocumentedCompactJSONInput(t *testing.T) {
{SourceID: "session-alpha", StartUnitID: 3, EndUnitID: 9}, {SourceID: "session-alpha", StartUnitID: 3, EndUnitID: 9},
{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 { if got := DeriveID(" The\u2003Tavern ", refs); got != want {
t.Fatalf("DeriveID() = %q, want %q", got, want) t.Fatalf("DeriveID() = %q, want %q", got, want)
@@ -120,7 +120,7 @@ func TestValidateRegistryReportsIdentityProblemsDeterministicallyWithoutMutation
} }
wantLocations := cloneLocations(locations) wantLocations := cloneLocations(locations)
issues := ValidateList(dnd.LocationList{Locations: locations}) issues := ValidateList(dnd.LocationRegistry{Locations: locations})
want := []Issue{ want := []Issue{
{Code: IssueIDMismatch, RecordIndex: 1, Value: validID}, {Code: IssueIDMismatch, RecordIndex: 1, Value: validID},
{Code: IssueEmptyCanonicalName, RecordIndex: 2, Value: ""}, {Code: IssueEmptyCanonicalName, RecordIndex: 2, Value: ""},

View File

@@ -27,7 +27,7 @@ const (
// grounding. All accessors return defensive copies. // grounding. All accessors return defensive copies.
type Registry struct { type Registry struct {
bound bool bound bool
list dnd.LocationList list dnd.LocationRegistry
canonical []byte canonical []byte
digest string digest string
projectionDigest string projectionDigest string
@@ -68,7 +68,7 @@ func (r *Resolver) Resolve(references contracts.ReferenceSet) (*Registry, error)
return r.resolver.Resolve(references) 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. // uses the canonical empty projection and has no durable registry identity.
func Resolve(references contracts.ReferenceSet) (*Registry, error) { func Resolve(references contracts.ReferenceSet) (*Registry, error) {
item, present, err := registryresolver.ResolveOptionalSingleItem(references, locationReferenceSpec()) item, present, err := registryresolver.ResolveOptionalSingleItem(references, locationReferenceSpec())
@@ -102,7 +102,7 @@ func emptyRegistry() *Registry {
content := []byte(emptyPrompt) content := []byte(emptyPrompt)
projectionDigest := semanticDigest(content) projectionDigest := semanticDigest(content)
return &Registry{ return &Registry{
list: dnd.LocationList{Locations: []dnd.Location{}}, list: dnd.LocationRegistry{Locations: []dnd.Location{}},
canonical: append([]byte(nil), content...), canonical: append([]byte(nil), content...),
projectionDigest: projectionDigest, projectionDigest: projectionDigest,
promptInput: contracts.NewLLMInputMaterial(ReferenceSlot, locationcodec.MediaType, content, 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") 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)) lookupByID := make(map[string]int, len(list.Locations))
for index, location := range list.Locations { for index, location := range list.Locations {
lookupByID[location.ID] = index lookupByID[location.ID] = index
@@ -157,12 +157,12 @@ func (r *Registry) Locations() []dnd.Location {
return cloneLocations(r.list.Locations) return cloneLocations(r.list.Locations)
} }
// List returns a defensive copy of the validated location list. // List returns a defensive copy of the validated location registry.
func (r *Registry) List() dnd.LocationList { func (r *Registry) List() dnd.LocationRegistry {
if r == nil { 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. // CanonicalBytes returns a defensive copy of the canonical durable JSON.
@@ -236,12 +236,12 @@ type projectedLocation struct {
Name string `json:"name"` Name string `json:"name"`
} }
type projectedLocationList struct { type projectedLocationRegistry struct {
Locations []projectedLocation `json:"locations"` Locations []projectedLocation `json:"locations"`
} }
func promptProjection(list dnd.LocationList) ([]byte, error) { func promptProjection(list dnd.LocationRegistry) ([]byte, error) {
projection := projectedLocationList{Locations: make([]projectedLocation, len(list.Locations))} projection := projectedLocationRegistry{Locations: make([]projectedLocation, len(list.Locations))}
for index, location := range list.Locations { for index, location := range list.Locations {
projection.Locations[index] = projectedLocation{ID: location.ID, Name: location.Name} 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) return diagnostics.Aggregate("validate location registry identity", parts)
} }
func cloneLocationList(value dnd.LocationList) dnd.LocationList { func cloneLocationRegistry(value dnd.LocationRegistry) dnd.LocationRegistry {
return dnd.LocationList{Locations: cloneLocations(value.Locations)} return dnd.LocationRegistry{Locations: cloneLocations(value.Locations)}
} }
func cloneLocations(values []dnd.Location) []dnd.Location { func cloneLocations(values []dnd.Location) []dnd.Location {

View File

@@ -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}} firstRefs := []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 1, EndUnitID: 1}}
secondRefs := []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 2, EndUnitID: 2}} 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", firstRefs), Name: "The Tavern", SourceRefs: firstRefs},
{ID: identity.DeriveID("The Tavern", secondRefs), Name: "The Tavern", SourceRefs: secondRefs}, {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() t.Helper()
registry, err := Resolve(referenceSet(item(encodeList(t, list)))) registry, err := Resolve(referenceSet(item(encodeList(t, list))))
if err != nil { if err != nil {
@@ -173,7 +173,7 @@ func resolveList(t *testing.T, list dnd.LocationList) *Registry {
return registry return registry
} }
func encodeList(t *testing.T, list dnd.LocationList) []byte { func encodeList(t *testing.T, list dnd.LocationRegistry) []byte {
t.Helper() t.Helper()
content, err := locationcodec.New().Encode(list) content, err := locationcodec.New().Encode(list)
if err != nil { if err != nil {

View File

@@ -327,7 +327,7 @@ func occurrenceScope(index int) string { return fmt.Sprintf("occurrences[%d]", i
func referenceSlots() []contracts.ReferenceSlot { func referenceSlots() []contracts.ReferenceSlot {
return []contracts.ReferenceSlot{{ return []contracts.ReferenceSlot{{
Name: LocationRegistryReferenceSlot, Description: "Required normalized location registry used only for location identity grounding, never as occurrence evidence.", 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,
}} }}
} }

View File

@@ -133,7 +133,7 @@ func TestNormalizerContractsRequiredRegistryAndWarningBounds(t *testing.T) {
doc.Units[index].ID = index + 1 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}}} 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 { 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) t.Fatalf("bounded warnings = %#v, %v", bounded.Warnings, err)
} }
@@ -151,7 +151,7 @@ func assertLocationRegistryReferenceSlots(t *testing.T, owner string, slots []co
Name: LocationRegistryReferenceSlot, Name: LocationRegistryReferenceSlot,
Required: true, Required: true,
AcceptedMediaTypes: []string{"application/json"}, AcceptedMediaTypes: []string{"application/json"},
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.LocationListKind}, AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.LocationRegistryKind},
MaxBytes: LocationRegistryMaxBytes, MaxBytes: LocationRegistryMaxBytes,
} }
if !reflect.DeepEqual(got, want) || strings.TrimSpace(description) == "" { 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} 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)) locations := make([]dnd.Location, len(names))
for index, name := range names { for index, name := range names {
refs := []source.SourceRef{{SourceID: "registry", StartUnitID: index + 1, EndUnitID: index + 1}} refs := []source.SourceRef{{SourceID: "registry", StartUnitID: index + 1, EndUnitID: index + 1}}
locations[index] = dnd.Location{ID: identity.DeriveID(name, refs), Name: name, SourceRefs: refs} 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() t.Helper()
content, err := locationcodec.New().Encode(locations) content, err := locationcodec.New().Encode(locations)
if err != nil { if err != nil {

View File

@@ -40,7 +40,7 @@ const (
var requiredCapabilities = []string{"merged"} var requiredCapabilities = []string{"merged"}
var providedCapabilities = []string{"normalized"} var providedCapabilities = []string{"normalized"}
var _ contracts.Normalizer[dnd.LocationList] = (*Normalizer)(nil) var _ contracts.Normalizer[dnd.LocationRegistry] = (*Normalizer)(nil)
var _ contracts.ManifestMetadataProvider = (*Normalizer)(nil) var _ contracts.ManifestMetadataProvider = (*Normalizer)(nil)
var _ pipeline.CheckpointFingerprintProvider = (*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 { 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 { 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 { 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 { 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) order := shared.NewSourceRefOrder(req.Source)
@@ -113,10 +113,10 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
deterministic := recordList(records) deterministic := recordList(records)
materials, ready, err := entityreconcile.BuildContext(req.Source, reconciliationCandidates(records), semanticContextRadius) materials, ready, err := entityreconcile.BuildContext(req.Source, reconciliationCandidates(records), semanticContextRadius)
if err != nil { 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 { 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 var response entityreconcile.ProposalResponse
@@ -128,26 +128,26 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
if errors.Is(err, contracts.ErrInvalidStructuredOutput) { if errors.Is(err, contracts.ErrInvalidStructuredOutput) {
return n.invalidStructuredResult(deterministic, warnings), nil 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) assessment := materials.Assess(response)
applied, semanticWarnings := applySafeGroups(records, reconciliationGroups(assessment, materials.CandidateKeys()), order) applied, semanticWarnings := applySafeGroups(records, reconciliationGroups(assessment, materials.CandidateKeys()), order)
warnings = append(warnings, semanticWarnings...) warnings = append(warnings, semanticWarnings...)
if assessment.DiscardedGroups() == 0 { 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 return retryResult(recordList(applied), warnings, assessment), nil
} }
func (n *Normalizer) invalidStructuredResult(value dnd.LocationList, warnings []contracts.Warning) contracts.TypedNormalizeResult[dnd.LocationList] { func (n *Normalizer) invalidStructuredResult(value dnd.LocationRegistry, warnings []contracts.Warning) contracts.TypedNormalizeResult[dnd.LocationRegistry] {
return contracts.TypedNormalizeResult[dnd.LocationList]{Value: value, Warnings: limitWarningsForRetry(warnings), Retry: &contracts.NormalizeRetry{ return contracts.TypedNormalizeResult[dnd.LocationRegistry]{Value: value, Warnings: limitWarningsForRetry(warnings), Retry: &contracts.NormalizeRetry{
ReasonCode: ReasonCodeLocationSemanticProposalInvalid, Message: "semantic proposal requires retry: invalid structured output", ReasonCode: ReasonCodeLocationSemanticProposalInvalid, Message: "semantic proposal requires retry: invalid structured output",
FallbackWarnings: []contracts.Warning{semanticFallbackWarning(-1)}, FallbackWarnings: []contracts.Warning{semanticFallbackWarning(-1)},
}} }}
} }
func retryResult(value dnd.LocationList, warnings []contracts.Warning, assessment entityreconcile.Assessment) contracts.TypedNormalizeResult[dnd.LocationList] { func retryResult(value dnd.LocationRegistry, warnings []contracts.Warning, assessment entityreconcile.Assessment) contracts.TypedNormalizeResult[dnd.LocationRegistry] {
return contracts.TypedNormalizeResult[dnd.LocationList]{Value: value, Warnings: limitWarningsForRetry(warnings), Retry: &contracts.NormalizeRetry{ return contracts.TypedNormalizeResult[dnd.LocationRegistry]{Value: value, Warnings: limitWarningsForRetry(warnings), Retry: &contracts.NormalizeRetry{
ReasonCode: ReasonCodeLocationSemanticProposalInvalid, Message: diagnostics.Aggregate("semantic proposal requires retry", reconciliationIssues(assessment)), ReasonCode: ReasonCodeLocationSemanticProposalInvalid, Message: diagnostics.Aggregate("semantic proposal requires retry", reconciliationIssues(assessment)),
FallbackWarnings: []contracts.Warning{semanticFallbackWarning(assessment.DiscardedGroups())}, FallbackWarnings: []contracts.Warning{semanticFallbackWarning(assessment.DiscardedGroups())},
}} }}
@@ -183,7 +183,7 @@ type normalizedRecord struct {
earliest int 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 { if input.Locations == nil {
return nil, nil return nil, nil
} }
@@ -294,11 +294,11 @@ func recordValues(records []normalizedRecord) []dnd.Location {
} }
return values return values
} }
func recordList(records []normalizedRecord) dnd.LocationList { func recordList(records []normalizedRecord) dnd.LocationRegistry {
if records == nil { 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 { 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 locationScope(index int) string { return fmt.Sprintf("locations[%d]", index) }
func ModuleSpec() pipeline.ModuleSpec { 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 { 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) options, err := DecodeOptions(request.Options)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -16,7 +16,7 @@ import (
) )
func TestModuleContractAndMetadata(t *testing.T) { 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) { if got := ModuleSpec(); !reflect.DeepEqual(got, want) {
t.Fatalf("ModuleSpec() = %#v, want %#v", got, want) t.Fatalf("ModuleSpec() = %#v, want %#v", got, want)
} }
@@ -38,13 +38,13 @@ func TestModuleContractAndMetadata(t *testing.T) {
} }
func TestNormalizePreparesOnlyExactDuplicatesAndRetainsSameNameAndNestedPlaces(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: 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", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 2, EndUnitID: 2}}},
{Name: "The Tavern Cellar", 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)) result, err := newNormalizer(t, &recordingLocationNormalizerClient{}).Normalize(context.Background(), normalizeRequest(input))
if err != nil || len(result.Value.Locations) != 3 { if err != nil || len(result.Value.Locations) != 3 {
t.Fatalf("Normalize() = %#v, %v; want one exact duplicate removed", result, err) t.Fatalf("Normalize() = %#v, %v; want one exact duplicate removed", result, err)
@@ -63,7 +63,7 @@ func TestNormalizePreparesOnlyExactDuplicatesAndRetainsSameNameAndNestedPlaces(t
func TestNormalizeAppliesSafeAliasGroupAndUsesOpaqueInputs(t *testing.T) { func TestNormalizeAppliesSafeAliasGroupAndUsesOpaqueInputs(t *testing.T) {
client := &recordingLocationNormalizerClient{response: `{"duplicate_groups":[{"members":["candidate-000001","candidate-000002"],"canonical":"candidate-000002"}]}`} client := &recordingLocationNormalizerClient{response: `{"duplicate_groups":[{"members":["candidate-000001","candidate-000002"],"canonical":"candidate-000002"}]}`}
doc := semanticDocument() 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: "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 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}}}, {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) { func TestNormalizeRejectsUnsafeAndOverlappingGroupsWithoutLosingCandidates(t *testing.T) {
doc := semanticDocument() 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: "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: "Mill", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}},
{Name: "Tavern", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}}}, {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) { func TestNormalizeHandlesRetryFallbackAndErrors(t *testing.T) {
doc := semanticDocument() 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)) invalid, err := newNormalizer(t, &recordingLocationNormalizerClient{err: contracts.ErrInvalidStructuredOutput}).Normalize(context.Background(), normalizeRequestWithSource(input, doc))
if err != nil || invalid.Retry == nil || invalid.Retry.ReasonCode != ReasonCodeLocationSemanticProposalInvalid { if err != nil || invalid.Retry == nil || invalid.Retry.ReasonCode != ReasonCodeLocationSemanticProposalInvalid {
t.Fatalf("invalid result = %#v, %v", invalid, err) t.Fatalf("invalid result = %#v, %v", invalid, err)
@@ -119,7 +119,7 @@ func TestNormalizeHandlesRetryFallbackAndErrors(t *testing.T) {
func TestNormalizeOrdersEvidenceAndIsIdempotent(t *testing.T) { func TestNormalizeOrdersEvidenceAndIsIdempotent(t *testing.T) {
doc := &source.SourceDocument{ID: "ordered", Units: []source.SourceUnit{{ID: 30}, {ID: 10}}} 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: 10, EndUnitID: 10},
{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}, {SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30},
{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}, {SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30},

View File

@@ -39,10 +39,10 @@ func newNormalizer(t *testing.T, client contracts.StructuredLLMClient) *Normaliz
} }
return normalizer return normalizer
} }
func normalizeRequest(value dnd.LocationList) contracts.TypedNormalizeRequest[dnd.LocationList] { func normalizeRequest(value dnd.LocationRegistry) contracts.TypedNormalizeRequest[dnd.LocationRegistry] {
return contracts.TypedNormalizeRequest[dnd.LocationList]{MergeOutput: contracts.MergeArtifact[dnd.LocationList]{Value: value}} 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 := normalizeRequest(value)
request.Source = doc request.Source = doc
return request return request

View File

@@ -26,7 +26,7 @@ func registerEvidence(registry *pipeline.ArtifactEvidenceRegistry) error {
return pipeline.RegisterArtifactEvidence(registry, dnd.SceneDescriptionListKind, sceneDescriptionEvidence) return pipeline.RegisterArtifactEvidence(registry, dnd.SceneDescriptionListKind, sceneDescriptionEvidence)
}}, }},
{name: "locations evidence", register: func() error { {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 { {name: "location occurrences evidence", register: func() error {
return pipeline.RegisterArtifactEvidence(registry, dnd.LocationOccurrenceListKind, locationOccurrenceEvidence) return pipeline.RegisterArtifactEvidence(registry, dnd.LocationOccurrenceListKind, locationOccurrenceEvidence)
@@ -90,7 +90,7 @@ func sceneDescriptionEvidence(value dnd.SceneDescriptionList) []source.SourceRef
return refs return refs
} }
func locationEvidence(value dnd.LocationList) []source.SourceRef { func locationEvidence(value dnd.LocationRegistry) []source.SourceRef {
var refs []source.SourceRef var refs []source.SourceRef
for _, location := range value.Locations { for _, location := range value.Locations {
refs = append(refs, location.SourceRefs...) refs = append(refs, location.SourceRefs...)

View File

@@ -152,7 +152,7 @@ func appendSceneDescriptionLists(values []dnd.SceneDescriptionList) (dnd.SceneDe
return combined, nil return combined, nil
} }
func appendLocationLists(values []dnd.LocationList) (dnd.LocationList, error) { func appendLocationRegistries(values []dnd.LocationRegistry) (dnd.LocationRegistry, error) {
count := 0 count := 0
present := false present := false
for _, value := range values { for _, value := range values {
@@ -162,9 +162,9 @@ func appendLocationLists(values []dnd.LocationList) (dnd.LocationList, error) {
count += len(value.Locations) count += len(value.Locations)
} }
if !present { 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 _, value := range values {
for _, location := range value.Locations { for _, location := range value.Locations {
combined.Locations = append(combined.Locations, cloneLocation(location)) combined.Locations = append(combined.Locations, cloneLocation(location))

View File

@@ -84,8 +84,8 @@ func registerModules(registries pipeline.Registries) error {
{name: "scene-description-list appendorder merger", register: func() error { {name: "scene-description-list appendorder merger", register: func() error {
return appendorder.RegisterTyped(registries.Mergers, dnd.SceneDescriptionListKind, appendSceneDescriptionLists) return appendorder.RegisterTyped(registries.Mergers, dnd.SceneDescriptionListKind, appendSceneDescriptionLists)
}}, }},
{name: "location-list appendorder merger", register: func() error { {name: "location-registry appendorder merger", register: func() error {
return appendorder.RegisterTyped(registries.Mergers, dnd.LocationListKind, appendLocationLists) return appendorder.RegisterTyped(registries.Mergers, dnd.LocationRegistryKind, appendLocationRegistries)
}}, }},
{name: "location-occurrence-list appendorder merger", register: func() error { {name: "location-occurrence-list appendorder merger", register: func() error {
return appendorder.RegisterTyped(registries.Mergers, dnd.LocationOccurrenceListKind, appendLocationOccurrenceLists) 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 { {name: "scene-description-list noop normalizer", register: func() error {
return noop.RegisterTyped[dnd.SceneDescriptionList](registries.Normalizers, dnd.SceneDescriptionListKind) return noop.RegisterTyped[dnd.SceneDescriptionList](registries.Normalizers, dnd.SceneDescriptionListKind)
}}, }},
{name: "location-list noop normalizer", register: func() error { {name: "location-registry noop normalizer", register: func() error {
return noop.RegisterTyped[dnd.LocationList](registries.Normalizers, dnd.LocationListKind) return noop.RegisterTyped[dnd.LocationRegistry](registries.Normalizers, dnd.LocationRegistryKind)
}}, }},
{name: "location-occurrence-list noop normalizer", register: func() error { {name: "location-occurrence-list noop normalizer", register: func() error {
return noop.RegisterTyped[dnd.LocationOccurrenceList](registries.Normalizers, dnd.LocationOccurrenceListKind) return noop.RegisterTyped[dnd.LocationOccurrenceList](registries.Normalizers, dnd.LocationOccurrenceListKind)

View File

@@ -84,10 +84,10 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
assertContainsKeys(t, "chunkers", registries.Chunkers.RegisteredKeys(), []string{"dnd/scenes"}) 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, "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}) 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.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.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.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.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.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.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.LocationRegistryKind, dnd.LocationOccurrenceListKind})
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(npcnormalize.Key), []contracts.ArtifactKind{dnd.NPCRegistryKind}) 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(combatnormalize.Key), []contracts.ArtifactKind{dnd.CombatTurnListKind})
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(enemyeventnormalize.Key), []contracts.ArtifactKind{dnd.EnemyEventListKind}) 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) locationExtractSpec, locationExtractOK := registries.Extractors.Spec(locationextract.Key)
locationNormalizeSpec, locationNormalizeOK := registries.Normalizers.Spec(locationnormalize.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) t.Fatalf("location specs = %#v / %#v", locationExtractSpec, locationNormalizeSpec)
} }
locationOccurrenceExtractSpec, locationOccurrenceExtractOK := registries.Extractors.Spec(locationoccurrenceextract.Key) 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 { 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) 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) t.Fatalf("location registry slots disagree: %#v / %#v", occurrenceExtractSpec.ReferenceSlots, occurrenceNormalizeSpec.ReferenceSlots)
} }
for _, name := range []string{"party", "roster", "players", "glossary"} { 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}}}) return sceneDescriptionEvidence(dnd.SceneDescriptionList{Scenes: []dnd.SceneDescription{{SourceRef: first}, {SourceRef: second}}})
}, want: []source.SourceRef{first, second}}, }, want: []source.SourceRef{first, second}},
{name: "locations", project: func() []source.SourceRef { {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}}, }, want: []source.SourceRef{first, second}},
{name: "location occurrences", project: func() []source.SourceRef { {name: "location occurrences", project: func() []source.SourceRef {
return locationOccurrenceEvidence(dnd.LocationOccurrenceList{Occurrences: []dnd.LocationOccurrence{{SourceRefs: []source.SourceRef{first, second}}}}) 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}} 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}}}}}} 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 := appendLocationLists(input) got, err := appendLocationRegistries(input)
if err != nil || !reflect.DeepEqual([]string{got.Locations[0].Name, got.Locations[1].Name}, []string{"First", "Second"}) { 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 got.Locations[0].SourceRefs[0].StartUnitID = 99
if input[0].Locations[0].SourceRefs[0].StartUnitID != 1 { if input[0].Locations[0].SourceRefs[0].StartUnitID != 1 {
t.Fatal("merged locations share source-reference storage") t.Fatal("merged locations share source-reference storage")
} }
for _, values := range [][]dnd.LocationList{nil, {{}, {}}} { for _, values := range [][]dnd.LocationRegistry{nil, {{}, {}}} {
result, err := appendLocationLists(values) result, err := appendLocationRegistries(values)
if err != nil || result.Locations != nil { if err != nil || result.Locations != nil {
t.Fatalf("nil-only merge = %#v, %v", result, err) t.Fatalf("nil-only merge = %#v, %v", result, err)
} }

View File

@@ -129,11 +129,11 @@ func registerValidators(registries pipeline.Registries) error {
{name: "scene-description-list always reject validator", register: func() error { {name: "scene-description-list always reject validator", register: func() error {
return alwaysreject.RegisterTyped[dnd.SceneDescriptionList](registries.Validators, dnd.SceneDescriptionListKind) return alwaysreject.RegisterTyped[dnd.SceneDescriptionList](registries.Validators, dnd.SceneDescriptionListKind)
}}, }},
{name: "location-list always accept validator", register: func() error { {name: "location-registry always accept validator", register: func() error {
return alwaysaccept.RegisterTyped[dnd.LocationList](registries.Validators, dnd.LocationListKind) return alwaysaccept.RegisterTyped[dnd.LocationRegistry](registries.Validators, dnd.LocationRegistryKind)
}}, }},
{name: "location-list always reject validator", register: func() error { {name: "location-registry always reject validator", register: func() error {
return alwaysreject.RegisterTyped[dnd.LocationList](registries.Validators, dnd.LocationListKind) return alwaysreject.RegisterTyped[dnd.LocationRegistry](registries.Validators, dnd.LocationRegistryKind)
}}, }},
{name: "location-occurrence-list always accept validator", register: func() error { {name: "location-occurrence-list always accept validator", register: func() error {
return alwaysaccept.RegisterTyped[dnd.LocationOccurrenceList](registries.Validators, dnd.LocationOccurrenceListKind) return alwaysaccept.RegisterTyped[dnd.LocationOccurrenceList](registries.Validators, dnd.LocationOccurrenceListKind)

View File

@@ -20,7 +20,7 @@ const ItemEventListKind contracts.ArtifactKind = "dnd/item-event-list"
const EnemyEventListKind contracts.ArtifactKind = "dnd/enemy-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" const LocationOccurrenceListKind contracts.ArtifactKind = "dnd/location-occurrence-list"
@@ -150,7 +150,7 @@ type EnemyEvent struct {
SourceRefs []source.SourceRef `json:"source_refs"` SourceRefs []source.SourceRef `json:"source_refs"`
} }
type LocationList struct { type LocationRegistry struct {
Locations []Location `json:"locations"` Locations []Location `json:"locations"`
} }

View File

@@ -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} second := dnd.Location{ID: identity.DeriveID("Moon Gate", secondRefs), Name: "Moon Gate", SourceRefs: secondRefs}
thirdRefs := []source.SourceRef{{SourceID: "source", StartUnitID: 3, EndUnitID: 3}} thirdRefs := []source.SourceRef{{SourceID: "source", StartUnitID: 3, EndUnitID: 3}}
third := dnd.Location{ID: identity.DeriveID("Sun Gate", thirdRefs), Name: "Sun Gate", SourceRefs: thirdRefs} 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -22,7 +22,7 @@ const (
type Options struct{} type Options struct{}
type Validator struct{} type Validator struct{}
var _ contracts.TypedValidator[dnd.LocationList] = (*Validator)(nil) var _ contracts.TypedValidator[dnd.LocationRegistry] = (*Validator)(nil)
var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil) var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil)
func New(Options) *Validator { return &Validator{} } func New(Options) *Validator { return &Validator{} }
@@ -34,7 +34,7 @@ func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
return []pipeline.CheckpointFingerprint{{Name: "policy", Value: policy}} 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 { if err := locationshape.Validate(req.Value); err != nil {
return contracts.ValidationResult{Approved: true}, nil return contracts.ValidationResult{Approved: true}, nil
} }
@@ -57,7 +57,7 @@ func Spec() pipeline.ValidatorSpec {
} }
func Register(registry *pipeline.ValidatorRegistry) error { 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) options, err := DecodeOptions(request.Options)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -16,7 +16,7 @@ import (
func TestValidatorAllowsSameNameAtDistinctEvidence(t *testing.T) { func TestValidatorAllowsSameNameAtDistinctEvidence(t *testing.T) {
refs := []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}} refs := []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}
otherRefs := []source.SourceRef{{SourceID: "session", StartUnitID: 2, EndUnitID: 2}} 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", refs), Name: "Watchtower", SourceRefs: refs},
{ID: domainidentity.DeriveID("Watchtower", otherRefs), Name: "Watchtower", SourceRefs: otherRefs}, {ID: domainidentity.DeriveID("Watchtower", otherRefs), Name: "Watchtower", SourceRefs: otherRefs},
}} }}
@@ -28,12 +28,12 @@ func TestValidatorAllowsSameNameAtDistinctEvidence(t *testing.T) {
} }
func TestValidatorDefersShapeAndRejectsDerivationAndDuplicateID(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 { if err != nil || !result.Approved {
t.Fatalf("shape-invalid result = %#v, %v; want deferral", result, err) t.Fatalf("shape-invalid result = %#v, %v; want deferral", result, err)
} }
refs := []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}} 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: "not-an-id", Name: "Gate", SourceRefs: refs},
{ID: domainidentity.DeriveID("Gate", refs), Name: "Other Gate", SourceRefs: refs}, {ID: domainidentity.DeriveID("Gate", refs), Name: "Other Gate", SourceRefs: refs},
{ID: domainidentity.DeriveID("Gate", refs), Name: "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] { func request(value dnd.LocationRegistry) contracts.TypedValidationRequest[dnd.LocationRegistry] {
return contracts.TypedValidationRequest[dnd.LocationList]{Value: value} return contracts.TypedValidationRequest[dnd.LocationRegistry]{Value: value}
} }

View File

@@ -21,7 +21,7 @@ const (
type Options struct{} type Options struct{}
type Validator struct{} type Validator struct{}
var _ contracts.TypedValidator[dnd.LocationList] = (*Validator)(nil) var _ contracts.TypedValidator[dnd.LocationRegistry] = (*Validator)(nil)
var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil) var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil)
func New(Options) *Validator { return &Validator{} } func New(Options) *Validator { return &Validator{} }
@@ -33,7 +33,7 @@ func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
return []pipeline.CheckpointFingerprint{{Name: "policy", Value: policy}} 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) issues := issuesFor(req.Value)
if len(issues) > 0 { if len(issues) > 0 {
return rejection(diagnostics.Aggregate("invalid location shape", issues)), nil 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 return contracts.ValidationResult{Approved: true}, nil
} }
func Validate(value dnd.LocationList) error { func Validate(value dnd.LocationRegistry) error {
issues := issuesFor(value) issues := issuesFor(value)
if len(issues) == 0 { if len(issues) == 0 {
return nil return nil
@@ -49,7 +49,7 @@ func Validate(value dnd.LocationList) error {
return fmt.Errorf("%s", diagnostics.Aggregate("invalid location shape", issues)) 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 { if value.Locations == nil {
return []string{"locations must be present"} return []string{"locations must be present"}
} }
@@ -74,7 +74,7 @@ func Spec() pipeline.ValidatorSpec {
} }
func Register(registry *pipeline.ValidatorRegistry) error { 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) options, err := DecodeOptions(request.Options)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -14,7 +14,7 @@ import (
) )
func TestValidatorRejectsMissingRequiredFieldsWithoutMutation(t *testing.T) { 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 before := value
result, err := New(Options{}).Validate(context.Background(), requestWithValue(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") { 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) { if len(result.Message) > 4096 || !utf8.ValidString(result.Message) || !reflect.DeepEqual(value, before) {
t.Fatalf("Validate() produced unsafe diagnostics or mutated value: %#v", value) 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") { if err != nil || result.Approved || !strings.Contains(result.Message, "locations must be present") {
t.Fatalf("missing locations = %#v, %v; want rejection", result, err) t.Fatalf("missing locations = %#v, %v; want rejection", result, err)
} }
} }
func TestValidatorApprovesAndRegisters(t *testing.T) { 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 { if err != nil || !result.Approved {
t.Fatalf("Validate() = %#v, %v; want approval", result, err) 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] { func requestWithValue(value dnd.LocationRegistry) contracts.TypedValidationRequest[dnd.LocationRegistry] {
return contracts.TypedValidationRequest[dnd.LocationList]{Value: value} return contracts.TypedValidationRequest[dnd.LocationRegistry]{Value: value}
} }
func validLocationList() dnd.LocationList { func validLocationRegistry() dnd.LocationRegistry {
return dnd.LocationList{Locations: []dnd.Location{{ID: "candidate", Name: "Moon Gate", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}}} return dnd.LocationRegistry{Locations: []dnd.Location{{ID: "candidate", Name: "Moon Gate", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}}}
} }

View File

@@ -22,7 +22,7 @@ const (
type Options struct{} type Options struct{}
type Validator struct{} type Validator struct{}
var _ contracts.TypedValidator[dnd.LocationList] = (*Validator)(nil) var _ contracts.TypedValidator[dnd.LocationRegistry] = (*Validator)(nil)
var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil) var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil)
func New(Options) *Validator { return &Validator{} } func New(Options) *Validator { return &Validator{} }
@@ -34,7 +34,7 @@ func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
return []pipeline.CheckpointFingerprint{{Name: "policy", Value: policy}} 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 { if req.Stage == string(pipeline.StageExtract) && req.Chunk == nil {
return contracts.ValidationResult{}, fmt.Errorf("location source-reference validator requires the current extraction chunk") 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 { 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) options, err := DecodeOptions(request.Options)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -14,7 +14,7 @@ import (
) )
func TestValidatorRejectsForeignAndOutOfRangeReferencesWithoutMutation(t *testing.T) { 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}} value.Locations[0].SourceRefs = []source.SourceRef{{SourceID: "foreign", StartUnitID: 1, EndUnitID: 1}, {SourceID: "session", StartUnitID: 9, EndUnitID: 9}}
before := value before := value
result, err := New(Options{}).Validate(context.Background(), request(validDocument(), value)) result, err := New(Options{}).Validate(context.Background(), request(validDocument(), value))
@@ -27,11 +27,11 @@ func TestValidatorRejectsForeignAndOutOfRangeReferencesWithoutMutation(t *testin
} }
func TestValidatorApprovesValidReferencesAndDefersMalformedShape(t *testing.T) { 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 { if err != nil || !result.Approved {
t.Fatalf("valid references = %#v, %v", result, err) 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 != "" { if err != nil || !result.Approved || result.ReasonCode != "" {
t.Fatalf("malformed shape = %#v, %v; want deferral", result, err) t.Fatalf("malformed shape = %#v, %v; want deferral", result, err)
} }
@@ -39,8 +39,8 @@ func TestValidatorApprovesValidReferencesAndDefersMalformedShape(t *testing.T) {
func TestValidatorRequiresCurrentChunkAndRejectsEvidenceOutsideIt(t *testing.T) { func TestValidatorRequiresCurrentChunkAndRejectsEvidenceOutsideIt(t *testing.T) {
doc := &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 1}, {ID: 2}}} doc := &source.SourceDocument{ID: "session", Units: []source.SourceUnit{{ID: 1}, {ID: 2}}}
value := validLocationList() value := validLocationRegistry()
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, Stage: string(pipeline.StageExtract), Source: doc,
Chunk: &source.Chunk{SourceID: "session", Units: []source.SourceUnit{{ID: 1}}}, Value: value, 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) t.Fatalf("contained extraction evidence = %#v, %v", result, err)
} }
value.Locations[0].SourceRefs[0].EndUnitID = 2 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, Stage: string(pipeline.StageExtract), Source: doc,
Chunk: &source.Chunk{SourceID: "session", Units: []source.SourceUnit{{ID: 1}}}, Value: value, 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") { 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) 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") { if err == nil || !strings.Contains(err.Error(), "requires the current extraction chunk") {
t.Fatalf("missing chunk error = %v", err) 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] { func request(doc *source.SourceDocument, value dnd.LocationRegistry) contracts.TypedValidationRequest[dnd.LocationRegistry] {
return contracts.TypedValidationRequest[dnd.LocationList]{Source: doc, Value: value} return contracts.TypedValidationRequest[dnd.LocationRegistry]{Source: doc, Value: value}
} }
func validDocument() *source.SourceDocument { 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."}}} 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 { func validLocationRegistry() dnd.LocationRegistry {
return dnd.LocationList{Locations: []dnd.Location{{ID: "candidate", Name: "Moon Gate", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}}} return dnd.LocationRegistry{Locations: []dnd.Location{{ID: "candidate", Name: "Moon Gate", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}}}
} }

View File

@@ -23,7 +23,7 @@ const (
type Options struct{} type Options struct{}
type Validator struct{} type Validator struct{}
var _ contracts.TypedValidator[dnd.LocationList] = (*Validator)(nil) var _ contracts.TypedValidator[dnd.LocationRegistry] = (*Validator)(nil)
var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil) var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil)
func New(Options) *Validator { return &Validator{} } func New(Options) *Validator { return &Validator{} }
@@ -35,7 +35,7 @@ func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
return []pipeline.CheckpointFingerprint{{Name: "policy", Value: policy}} 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 { if err := locationshape.Validate(req.Value); err != nil {
return contracts.ValidationResult{Approved: true}, nil return contracts.ValidationResult{Approved: true}, nil
} }
@@ -69,7 +69,7 @@ func Spec() pipeline.ValidatorSpec {
} }
func Register(registry *pipeline.ValidatorRegistry) error { 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) options, err := DecodeOptions(request.Options)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -14,7 +14,7 @@ import (
) )
func TestValidatorUsesOnlyCitedTranscriptText(t *testing.T) { 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 orins gate."}, {ID: 2, Kind: "message", Text: "Unrelated location."}}} doc := &source.SourceDocument{ID: "session", Kind: "transcript", Format: "application/json", Digest: "sha256:session", Units: []source.SourceUnit{{ID: 1, Kind: "message", Text: "The party enters orins gate."}, {ID: 2, Kind: "message", Text: "Unrelated location."}}}
result, err := New(Options{}).Validate(context.Background(), request(doc, value, contracts.ReferenceSet{})) result, err := New(Options{}).Validate(context.Background(), request(doc, value, contracts.ReferenceSet{}))
if err != nil || !result.Approved || len(result.Warnings) != 0 { if err != nil || !result.Approved || len(result.Warnings) != 0 {
@@ -30,12 +30,12 @@ func TestValidatorUsesOnlyCitedTranscriptText(t *testing.T) {
} }
func TestValidatorDefersMalformedOrUnreadableCitations(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{})) result, err := New(Options{}).Validate(context.Background(), request(document(), invalid, contracts.ReferenceSet{}))
if err != nil || !result.Approved || len(result.Warnings) != 0 { if err != nil || !result.Approved || len(result.Warnings) != 0 {
t.Fatalf("shape deferral = %#v, %v", result, err) 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{})) result, err = New(Options{}).Validate(context.Background(), request(document(), value, contracts.ReferenceSet{}))
if err != nil || !result.Approved || len(result.Warnings) != 0 { if err != nil || !result.Approved || len(result.Warnings) != 0 {
t.Fatalf("unreadable citation = %#v, %v", result, err) t.Fatalf("unreadable citation = %#v, %v", result, err)
@@ -60,14 +60,14 @@ func TestValidatorBoundsWarnings(t *testing.T) {
for index := range locations { for index := range locations {
locations[index] = dnd.Location{ID: "candidate", Name: "Missing", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}} 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 { 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) t.Fatalf("bounded warnings = %#v, %v", result, err)
} }
} }
func request(doc *source.SourceDocument, value dnd.LocationList, references contracts.ReferenceSet) contracts.TypedValidationRequest[dnd.LocationList] { func request(doc *source.SourceDocument, value dnd.LocationRegistry, references contracts.ReferenceSet) contracts.TypedValidationRequest[dnd.LocationRegistry] {
return contracts.TypedValidationRequest[dnd.LocationList]{Source: doc, References: references, Value: value} return contracts.TypedValidationRequest[dnd.LocationRegistry]{Source: doc, References: references, Value: value}
} }
func document() *source.SourceDocument { func document() *source.SourceDocument {

View File

@@ -33,7 +33,7 @@ func TestLocationRegistryHandoffProducesOccurrencesAndEvidence(t *testing.T) {
} }
registryOutput := normalizedLane(t, output, "locations") 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) t.Fatalf("registry envelope = %#v, want durable location artifact", registryOutput)
} }
registry, err := locationcodec.New().Decode(registryOutput.Artifact.Content) registry, err := locationcodec.New().Decode(registryOutput.Artifact.Content)