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 |
| `combat_turns` | `dnd/combat-turn-list` | `application/json` | 1,048,576 bytes | extract only |
| `npc_occurrences` | `dnd/npc-occurrence-list` | `application/json` | 1,048,576 bytes | extract only |
| `locations` | `dnd/location-list` | `application/json` | 1,048,576 bytes | location-occurrence extract and normalize |
| `locations` | `dnd/location-registry` | `application/json` | 1,048,576 bytes | location-occurrence extract and normalize |
Scene descriptions accept **party**, **players**, and **glossary**, but not
**roster**. NPC occurrences require **npc_registry** for both extraction and

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

View File

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

View File

@@ -143,7 +143,7 @@ func TestMaintainedCompleteExampleProducesEnemyEventsThroughGeneratedHandoffs(t
if locationOutput.File != "lanes/locations.json" || locationOutput.SchemaID != locationcodec.SchemaID || locationOutput.SchemaVersion != locationcodec.SchemaVersion {
t.Fatalf("location output = %#v, want typed location registry JSON", locationOutput)
}
locationsValue := readProductionJSON[dnd.LocationList](t, filepath.Join(runRoot, locationOutput.File))
locationsValue := readProductionJSON[dnd.LocationRegistry](t, filepath.Join(runRoot, locationOutput.File))
if len(locationsValue.Locations) != 2 || locationsValue.Locations[0].Name != "Moon Gate" || locationsValue.Locations[1].Name != "Moon Gate" || locationsValue.Locations[0].ID == locationsValue.Locations[1].ID {
t.Fatalf("location registry = %#v, want distinct source-grounded identities for same-name locations", locationsValue)
}

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)
}
locationLane := referenceContractLane(t, materialized, "locations")
if locationLane.ArtifactKind != dnd.LocationListKind || locationLane.Extract.Module != locationextract.Key || locationLane.Extract.Retries != 2 || locationLane.Merge.Module != pipeline.DefaultMergeModule || locationLane.Normalize.Module != locationnormalize.Key || locationLane.Normalize.Retries != 2 {
if locationLane.ArtifactKind != dnd.LocationRegistryKind || locationLane.Extract.Module != locationextract.Key || locationLane.Extract.Retries != 2 || locationLane.Merge.Module != pipeline.DefaultMergeModule || locationLane.Normalize.Module != locationnormalize.Key || locationLane.Normalize.Retries != 2 {
t.Fatalf("location lane = %#v, want typed registry composition", locationLane)
}
occurrenceLane := referenceContractLane(t, materialized, "location-occurrences")

View File

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

View File

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

View File

@@ -13,25 +13,25 @@ import (
)
const (
SchemaID = "notarius.dnd.locations"
SchemaName = "notarius_dnd_locations_v1"
SchemaID = "notarius.dnd.location_registry"
SchemaName = "notarius_dnd_location_registry_v1"
SchemaVersion = "v1"
MediaType = "application/json"
)
//go:embed assets/schemas/dnd_locations.v1.json
//go:embed assets/schemas/dnd_location_registry.v1.json
var schemaAssets embed.FS
var _ contracts.ArtifactCodec[dnd.LocationList] = (*Codec)(nil)
var _ contracts.ArtifactCodec[dnd.LocationRegistry] = (*Codec)(nil)
type Codec struct{}
func New() *Codec { return &Codec{} }
func (c *Codec) Kind() contracts.ArtifactKind { return dnd.LocationListKind }
func (c *Codec) Kind() contracts.ArtifactKind { return dnd.LocationRegistryKind }
func (c *Codec) Schema() contracts.ArtifactSchema {
raw, err := schemaAssets.ReadFile("assets/schemas/dnd_locations.v1.json")
raw, err := schemaAssets.ReadFile("assets/schemas/dnd_location_registry.v1.json")
if err != nil {
return contracts.ArtifactSchema{}
}
@@ -45,41 +45,41 @@ func (c *Codec) Schema() contracts.ArtifactSchema {
func (c *Codec) MediaType() string { return MediaType }
func (c *Codec) Metadata(value dnd.LocationList) map[string]any {
func (c *Codec) Metadata(value dnd.LocationRegistry) map[string]any {
return map[string]any{"location_count": len(value.Locations)}
}
func (c *Codec) Encode(value dnd.LocationList) ([]byte, error) {
func (c *Codec) Encode(value dnd.LocationRegistry) ([]byte, error) {
if err := validate(value); err != nil {
return nil, fmt.Errorf("encode dnd location list: %w", err)
return nil, fmt.Errorf("encode dnd location registry: %w", err)
}
return c.EncodeCandidate(value)
}
// EncodeCandidate provides the durable representation before semantic
// validators have approved a value.
func (c *Codec) EncodeCandidate(value dnd.LocationList) ([]byte, error) {
return candidatejson.EncodeCandidate("dnd location list", value)
func (c *Codec) EncodeCandidate(value dnd.LocationRegistry) ([]byte, error) {
return candidatejson.EncodeCandidate("dnd location registry", value)
}
func (c *Codec) Decode(content []byte) (dnd.LocationList, error) {
func (c *Codec) Decode(content []byte) (dnd.LocationRegistry, error) {
value, err := c.DecodeCandidate(content)
if err != nil {
return dnd.LocationList{}, err
return dnd.LocationRegistry{}, err
}
if err := validate(value); err != nil {
return dnd.LocationList{}, fmt.Errorf("decode dnd location list: %w", err)
return dnd.LocationRegistry{}, fmt.Errorf("decode dnd location registry: %w", err)
}
return value, nil
}
// DecodeCandidate reads one strict durable JSON value before semantic
// validators have approved it.
func (c *Codec) DecodeCandidate(content []byte) (dnd.LocationList, error) {
return candidatejson.DecodeCandidate[dnd.LocationList]("dnd location list", content)
func (c *Codec) DecodeCandidate(content []byte) (dnd.LocationRegistry, error) {
return candidatejson.DecodeCandidate[dnd.LocationRegistry]("dnd location registry", content)
}
func validate(value dnd.LocationList) error {
func validate(value dnd.LocationRegistry) error {
if value.Locations == nil {
return fmt.Errorf("locations must be present")
}

View File

@@ -15,9 +15,9 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/locations/identity"
)
func validList() dnd.LocationList {
func validList() dnd.LocationRegistry {
refs := []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 1, EndUnitID: 2}}
return dnd.LocationList{Locations: []dnd.Location{{
return dnd.LocationRegistry{Locations: []dnd.Location{{
ID: identity.DeriveID("The Old Tavern", refs),
Name: "The Old Tavern",
SourceRefs: refs,
@@ -25,7 +25,7 @@ func validList() dnd.LocationList {
}
func TestCodecMatchesMaintainedDurableFixture(t *testing.T) {
raw, err := os.ReadFile("testdata/dnd_locations.v1.json")
raw, err := os.ReadFile("testdata/dnd_location_registry.v1.json")
if err != nil {
t.Fatalf("read durable fixture: %v", err)
}
@@ -53,7 +53,7 @@ func TestCodecMatchesMaintainedDurableFixture(t *testing.T) {
func TestCodecOwnsDurableSchemaAndMetadata(t *testing.T) {
codec := New()
schema := codec.Schema()
if codec.Kind() != dnd.LocationListKind || codec.MediaType() != MediaType {
if codec.Kind() != dnd.LocationRegistryKind || codec.MediaType() != MediaType {
t.Fatalf("codec identity = %q/%q", codec.Kind(), codec.MediaType())
}
if schema.ID != SchemaID || schema.Name != SchemaName || schema.Version != SchemaVersion || !json.Valid(schema.JSONSchema) {
@@ -67,7 +67,7 @@ func TestCodecOwnsDurableSchemaAndMetadata(t *testing.T) {
if err := pipeline.RegisterArtifactCodec(registry, codec); err != nil {
t.Fatalf("RegisterArtifactCodec() error = %v", err)
}
if spec, ok := registry.Spec(dnd.LocationListKind); !ok || spec.SchemaDigest != contracts.DigestArtifactSchema(schema) {
if spec, ok := registry.Spec(dnd.LocationRegistryKind); !ok || spec.SchemaDigest != contracts.DigestArtifactSchema(schema) {
t.Fatalf("registered spec = %#v, %t", spec, ok)
}
first := schema.JSONSchema
@@ -84,11 +84,11 @@ func TestCodecOwnsDurableSchemaAndMetadata(t *testing.T) {
func TestCodecSupportsEmptyListsAndCandidateSemanticFailures(t *testing.T) {
codec := New()
empty := dnd.LocationList{Locations: []dnd.Location{}}
empty := dnd.LocationRegistry{Locations: []dnd.Location{}}
if content, err := codec.Encode(empty); err != nil || string(content) != `{"locations":[]}` {
t.Fatalf("Encode() = %s, %v", content, err)
}
for _, candidate := range []dnd.LocationList{
for _, candidate := range []dnd.LocationRegistry{
{},
empty,
{Locations: []dnd.Location{{ID: "bad", Name: " ", SourceRefs: nil}}},
@@ -109,7 +109,7 @@ func TestCodecSupportsEmptyListsAndCandidateSemanticFailures(t *testing.T) {
func TestCodecRejectsStructuralJSONBeforeSemanticApproval(t *testing.T) {
valid := `{"locations":[{"id":"location:sha256:0000000000000000000000000000000000000000000000000000000000000000","name":"The Tavern","source_refs":[{"source_id":"session","start_unit_id":1,"end_unit_id":1}]}]}`
for _, test := range []struct{ name, raw, want string }{
{"malformed", `{`, "decode dnd location list"},
{"malformed", `{`, "decode dnd location registry"},
{"unknown top-level", `{"locations":[],"unexpected":true}`, "unknown field"},
{"unknown location field", strings.Replace(valid, `"name":"The Tavern"`, `"name":"The Tavern","unexpected":true`, 1), "unknown field"},
{"unknown source reference field", strings.Replace(valid, `"end_unit_id":1`, `"end_unit_id":1,"unexpected":true`, 1), "unknown field"},
@@ -128,14 +128,14 @@ func TestCodecRejectsApprovedShapeBoundaries(t *testing.T) {
base := validList().Locations[0]
for _, test := range []struct {
name string
value dnd.LocationList
value dnd.LocationRegistry
want string
}{
{"nil locations", dnd.LocationList{}, "locations must be present"},
{"invalid ID", dnd.LocationList{Locations: []dnd.Location{{ID: "bad", Name: base.Name, SourceRefs: base.SourceRefs}}}, "id must match location ID pattern"},
{"blank name", dnd.LocationList{Locations: []dnd.Location{{ID: base.ID, Name: " ", SourceRefs: base.SourceRefs}}}, "name must not be empty"},
{"empty source refs", dnd.LocationList{Locations: []dnd.Location{{ID: base.ID, Name: base.Name, SourceRefs: nil}}}, "source_refs must contain"},
{"malformed source reference", dnd.LocationList{Locations: []dnd.Location{{ID: base.ID, Name: base.Name, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 0, EndUnitID: 1}}}}}, "start_unit_id must be positive"},
{"nil locations", dnd.LocationRegistry{}, "locations must be present"},
{"invalid ID", dnd.LocationRegistry{Locations: []dnd.Location{{ID: "bad", Name: base.Name, SourceRefs: base.SourceRefs}}}, "id must match location ID pattern"},
{"blank name", dnd.LocationRegistry{Locations: []dnd.Location{{ID: base.ID, Name: " ", SourceRefs: base.SourceRefs}}}, "name must not be empty"},
{"empty source refs", dnd.LocationRegistry{Locations: []dnd.Location{{ID: base.ID, Name: base.Name, SourceRefs: nil}}}, "source_refs must contain"},
{"malformed source reference", dnd.LocationRegistry{Locations: []dnd.Location{{ID: base.ID, Name: base.Name, SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 0, EndUnitID: 1}}}}}, "start_unit_id must be positive"},
} {
t.Run(test.name, func(t *testing.T) {
if _, err := New().Encode(test.value); err == nil || !strings.Contains(err.Error(), test.want) {

View File

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

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.",
Required: true,
AcceptedMediaTypes: []string{"application/json"},
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.LocationListKind},
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.LocationRegistryKind},
MaxBytes: LocationRegistryMaxBytes,
})
sort.Slice(slots, func(left, right int) bool { return slots[left].Name < slots[right].Name })

View File

@@ -111,7 +111,7 @@ func TestExtractRequiresRegistryAndAcceptsEmptyRegistryWithNoOccurrences(t *test
if len(client.requests) != 0 {
t.Fatalf("LLM calls = %d", len(client.requests))
}
empty := dnd.LocationList{Locations: []dnd.Location{}}
empty := dnd.LocationRegistry{Locations: []dnd.Location{}}
references := registryReferences(t, empty)
req := extractionRequest()
req.References = references
@@ -196,7 +196,7 @@ func TestExtractorContractsMetadataAndFailures(t *testing.T) {
slot = candidate
}
}
if !slot.Required || !reflect.DeepEqual(slot.AcceptedArtifactKinds, []contracts.ArtifactKind{dnd.LocationListKind}) || slot.MaxBytes != LocationRegistryMaxBytes {
if !slot.Required || !reflect.DeepEqual(slot.AcceptedArtifactKinds, []contracts.ArtifactKind{dnd.LocationRegistryKind}) || slot.MaxBytes != LocationRegistryMaxBytes {
t.Fatalf("location registry slot = %#v", slot)
}
registry := pipeline.NewExtractorRegistry()
@@ -220,17 +220,17 @@ func TestExtractorContractsMetadataAndFailures(t *testing.T) {
}
}
func locationRegistry(t *testing.T, names ...string) dnd.LocationList {
func locationRegistry(t *testing.T, names ...string) dnd.LocationRegistry {
t.Helper()
locations := make([]dnd.Location, len(names))
for index, name := range names {
refs := []source.SourceRef{{SourceID: "other-session", StartUnitID: index + 1, EndUnitID: index + 1}}
locations[index] = dnd.Location{ID: identity.DeriveID(name, refs), Name: name, SourceRefs: refs}
}
return dnd.LocationList{Locations: locations}
return dnd.LocationRegistry{Locations: locations}
}
func registryReferences(t *testing.T, locations dnd.LocationList) contracts.ReferenceSet {
func registryReferences(t *testing.T, locations dnd.LocationRegistry) contracts.ReferenceSet {
t.Helper()
content, err := locationcodec.New().Encode(locations)
if err != nil {

View File

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

View File

@@ -31,7 +31,7 @@ func referenceSlots() []contracts.ReferenceSlot {
return shared.ReferenceSlots(referenceSlotDescriptions)
}
var _ contracts.Extractor[dnd.LocationList] = (*Extractor)(nil)
var _ contracts.Extractor[dnd.LocationRegistry] = (*Extractor)(nil)
var _ contracts.ManifestMetadataProvider = (*Extractor)(nil)
var _ pipeline.CheckpointFingerprintProvider = (*Extractor)(nil)
@@ -95,16 +95,16 @@ func (e *Extractor) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
}
}
func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRequest) (contracts.TypedExtractionResult[dnd.LocationList], error) {
func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRequest) (contracts.TypedExtractionResult[dnd.LocationRegistry], error) {
if e == nil {
return contracts.TypedExtractionResult[dnd.LocationList]{}, extractorErrorf("extractor must not be nil")
return contracts.TypedExtractionResult[dnd.LocationRegistry]{}, extractorErrorf("extractor must not be nil")
}
if e.llm == nil {
return contracts.TypedExtractionResult[dnd.LocationList]{}, extractorErrorf("LLM client must not be nil")
return contracts.TypedExtractionResult[dnd.LocationRegistry]{}, extractorErrorf("LLM client must not be nil")
}
sourceInput, err := shared.PrepareChunkExtraction(ctx, req)
if err != nil {
return contracts.TypedExtractionResult[dnd.LocationList]{}, extractorErrorf("%w", err)
return contracts.TypedExtractionResult[dnd.LocationRegistry]{}, extractorErrorf("%w", err)
}
order := shared.NewSourceRefOrder(req.Source)
@@ -114,22 +114,22 @@ func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRe
ProfileID: req.LLMProfile, SessionID: req.SessionID,
Inputs: shared.PromptInputs(sourceInput, req.References),
}, &response); err != nil {
return contracts.TypedExtractionResult[dnd.LocationList]{}, extractorErrorf("complete structured output: %w", err)
return contracts.TypedExtractionResult[dnd.LocationRegistry]{}, extractorErrorf("complete structured output: %w", err)
}
canonicalizeResponse(&response, order, req.Source.ID)
return contracts.TypedExtractionResult[dnd.LocationList]{Value: canonicalLocationList(response, req.Source.ID)}, nil
return contracts.TypedExtractionResult[dnd.LocationRegistry]{Value: canonicalLocationRegistry(response, req.Source.ID)}, nil
}
func ModuleSpec() pipeline.ModuleSpec {
return pipeline.ModuleSpec{
Key: Key, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked,
Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...),
ArtifactKind: dnd.LocationListKind, ReferenceSlots: referenceSlots(),
ArtifactKind: dnd.LocationRegistryKind, ReferenceSlots: referenceSlots(),
}
}
func Register(registry *pipeline.ExtractorRegistry) error {
return pipeline.RegisterExtractorBuilder(registry, ModuleSpec(), validateOptions, func(request pipeline.BuildRequest) (contracts.Extractor[dnd.LocationList], error) {
return pipeline.RegisterExtractorBuilder(registry, ModuleSpec(), validateOptions, func(request pipeline.BuildRequest) (contracts.Extractor[dnd.LocationRegistry], error) {
options, err := DecodeOptions(request.Options)
if err != nil {
return nil, err

View File

@@ -23,7 +23,7 @@ func TestExtractMapsLocationsWithOwnedEvidenceAndDeterministicOrder(t *testing.T
t.Fatalf("Extract() error = %v, want nil", err)
}
refs := []source.SourceRef{{SourceID: "session-locations", StartUnitID: 1, EndUnitID: 1}, {SourceID: "session-locations", StartUnitID: 2, EndUnitID: 2}}
want := dnd.LocationList{Locations: []dnd.Location{
want := dnd.LocationRegistry{Locations: []dnd.Location{
{ID: identity.DeriveID("Old Mill", refs), Name: "Old Mill", SourceRefs: refs},
{ID: identity.DeriveID("The Tavern", []source.SourceRef{{SourceID: "session-locations", StartUnitID: 3, EndUnitID: 3}}), Name: "The Tavern", SourceRefs: []source.SourceRef{{SourceID: "session-locations", StartUnitID: 3, EndUnitID: 3}}},
}}

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") {
t.Fatalf("New() error = %v, want reference-set rejection", err)
}
want := pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: []string{"chunks", "source.transcript"}, Provides: []string{"dnd.locations"}, ArtifactKind: dnd.LocationListKind, ReferenceSlots: referenceSlots()}
want := pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: []string{"chunks", "source.transcript"}, Provides: []string{"dnd.locations"}, ArtifactKind: dnd.LocationRegistryKind, ReferenceSlots: referenceSlots()}
if got := ModuleSpec(); !reflect.DeepEqual(got, want) {
t.Fatalf("ModuleSpec() = %#v, want %#v", got, want)
}

View File

@@ -16,7 +16,7 @@ import (
const (
// IdentityPolicy identifies the durable location ID derivation policy.
IdentityPolicy = "dnd.locations.identity.v1"
IdentityPolicy = "dnd.location_registry.identity.v1"
// Policy is an alias for IdentityPolicy.
Policy = IdentityPolicy
@@ -109,7 +109,7 @@ func IsValidID(value string) bool {
func ValidID(value string) bool { return IsValidID(value) }
// ValidateList validates the identity members of list.
func ValidateList(list dnd.LocationList) []Issue { return ValidateRegistry(list.Locations) }
func ValidateList(list dnd.LocationRegistry) []Issue { return ValidateRegistry(list.Locations) }
// ValidateRegistry validates location IDs without modifying records or their
// source references. Equal comparison names are allowed because their evidence

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},
}
const want = "location:sha256:379bd996e754e143b47e6b613a95166e58c111c7451750df7066ca816bb1866c"
const want = "location:sha256:c099f7780a3cfd8c4094441d3a3e906f4f1e79c27589f95dfad573f86f308845"
if got := DeriveID(" The\u2003Tavern ", refs); got != want {
t.Fatalf("DeriveID() = %q, want %q", got, want)
@@ -120,7 +120,7 @@ func TestValidateRegistryReportsIdentityProblemsDeterministicallyWithoutMutation
}
wantLocations := cloneLocations(locations)
issues := ValidateList(dnd.LocationList{Locations: locations})
issues := ValidateList(dnd.LocationRegistry{Locations: locations})
want := []Issue{
{Code: IssueIDMismatch, RecordIndex: 1, Value: validID},
{Code: IssueEmptyCanonicalName, RecordIndex: 2, Value: ""},

View File

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

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

View File

@@ -327,7 +327,7 @@ func occurrenceScope(index int) string { return fmt.Sprintf("occurrences[%d]", i
func referenceSlots() []contracts.ReferenceSlot {
return []contracts.ReferenceSlot{{
Name: LocationRegistryReferenceSlot, Description: "Required normalized location registry used only for location identity grounding, never as occurrence evidence.",
Required: true, AcceptedMediaTypes: []string{"application/json"}, AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.LocationListKind}, MaxBytes: LocationRegistryMaxBytes,
Required: true, AcceptedMediaTypes: []string{"application/json"}, AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.LocationRegistryKind}, MaxBytes: LocationRegistryMaxBytes,
}}
}

View File

@@ -133,7 +133,7 @@ func TestNormalizerContractsRequiredRegistryAndWarningBounds(t *testing.T) {
doc.Units[index].ID = index + 1
input.Occurrences[index] = dnd.LocationOccurrence{LocationID: location.ID, Name: "not canonical", Kind: dnd.LocationOccurrenceKindMentioned, SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: count - index, EndUnitID: count - index}}}
}
bounded, err := newNormalizer(t, registryReferences(t, dnd.LocationList{Locations: []dnd.Location{location}})).Normalize(context.Background(), normalizeRequest(input, doc, contracts.ReferenceSet{}))
bounded, err := newNormalizer(t, registryReferences(t, dnd.LocationRegistry{Locations: []dnd.Location{location}})).Normalize(context.Background(), normalizeRequest(input, doc, contracts.ReferenceSet{}))
if err != nil || len(bounded.Warnings) != diagnostics.MaxWarnings || bounded.Warnings[len(bounded.Warnings)-1].ReasonCode != ReasonCodeWarningsOmitted {
t.Fatalf("bounded warnings = %#v, %v", bounded.Warnings, err)
}
@@ -151,7 +151,7 @@ func assertLocationRegistryReferenceSlots(t *testing.T, owner string, slots []co
Name: LocationRegistryReferenceSlot,
Required: true,
AcceptedMediaTypes: []string{"application/json"},
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.LocationListKind},
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.LocationRegistryKind},
MaxBytes: LocationRegistryMaxBytes,
}
if !reflect.DeepEqual(got, want) || strings.TrimSpace(description) == "" {
@@ -172,16 +172,16 @@ func normalizeRequest(value dnd.LocationOccurrenceList, doc *source.SourceDocume
return contracts.TypedNormalizeRequest[dnd.LocationOccurrenceList]{Source: doc, MergeOutput: contracts.MergeArtifact[dnd.LocationOccurrenceList]{Value: value}, References: references}
}
func registryLocations(names ...string) dnd.LocationList {
func registryLocations(names ...string) dnd.LocationRegistry {
locations := make([]dnd.Location, len(names))
for index, name := range names {
refs := []source.SourceRef{{SourceID: "registry", StartUnitID: index + 1, EndUnitID: index + 1}}
locations[index] = dnd.Location{ID: identity.DeriveID(name, refs), Name: name, SourceRefs: refs}
}
return dnd.LocationList{Locations: locations}
return dnd.LocationRegistry{Locations: locations}
}
func registryReferences(t *testing.T, locations dnd.LocationList) contracts.ReferenceSet {
func registryReferences(t *testing.T, locations dnd.LocationRegistry) contracts.ReferenceSet {
t.Helper()
content, err := locationcodec.New().Encode(locations)
if err != nil {

View File

@@ -40,7 +40,7 @@ const (
var requiredCapabilities = []string{"merged"}
var providedCapabilities = []string{"normalized"}
var _ contracts.Normalizer[dnd.LocationList] = (*Normalizer)(nil)
var _ contracts.Normalizer[dnd.LocationRegistry] = (*Normalizer)(nil)
var _ contracts.ManifestMetadataProvider = (*Normalizer)(nil)
var _ pipeline.CheckpointFingerprintProvider = (*Normalizer)(nil)
@@ -94,18 +94,18 @@ func (n *Normalizer) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
}
}
func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalizeRequest[dnd.LocationList]) (contracts.TypedNormalizeResult[dnd.LocationList], error) {
func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalizeRequest[dnd.LocationRegistry]) (contracts.TypedNormalizeResult[dnd.LocationRegistry], error) {
if n == nil {
return contracts.TypedNormalizeResult[dnd.LocationList]{}, normalizerErrorf("normalizer must not be nil")
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, normalizerErrorf("normalizer must not be nil")
}
if n.llm == nil {
return contracts.TypedNormalizeResult[dnd.LocationList]{}, normalizerErrorf("LLM client must not be nil")
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, normalizerErrorf("LLM client must not be nil")
}
if ctx == nil {
return contracts.TypedNormalizeResult[dnd.LocationList]{}, normalizerErrorf("context must not be nil")
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, normalizerErrorf("context must not be nil")
}
if err := ctx.Err(); err != nil {
return contracts.TypedNormalizeResult[dnd.LocationList]{}, normalizerErrorf("context error before normalize: %w", err)
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, normalizerErrorf("context error before normalize: %w", err)
}
order := shared.NewSourceRefOrder(req.Source)
@@ -113,10 +113,10 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
deterministic := recordList(records)
materials, ready, err := entityreconcile.BuildContext(req.Source, reconciliationCandidates(records), semanticContextRadius)
if err != nil {
return contracts.TypedNormalizeResult[dnd.LocationList]{}, normalizerErrorf("build semantic context: %w", err)
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, normalizerErrorf("build semantic context: %w", err)
}
if !ready {
return contracts.TypedNormalizeResult[dnd.LocationList]{Value: deterministic, Warnings: limitWarnings(warnings)}, nil
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{Value: deterministic, Warnings: limitWarnings(warnings)}, nil
}
var response entityreconcile.ProposalResponse
@@ -128,26 +128,26 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
if errors.Is(err, contracts.ErrInvalidStructuredOutput) {
return n.invalidStructuredResult(deterministic, warnings), nil
}
return contracts.TypedNormalizeResult[dnd.LocationList]{}, normalizerErrorf("complete structured output: %w", err)
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{}, normalizerErrorf("complete structured output: %w", err)
}
assessment := materials.Assess(response)
applied, semanticWarnings := applySafeGroups(records, reconciliationGroups(assessment, materials.CandidateKeys()), order)
warnings = append(warnings, semanticWarnings...)
if assessment.DiscardedGroups() == 0 {
return contracts.TypedNormalizeResult[dnd.LocationList]{Value: recordList(applied), Warnings: limitWarnings(warnings)}, nil
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{Value: recordList(applied), Warnings: limitWarnings(warnings)}, nil
}
return retryResult(recordList(applied), warnings, assessment), nil
}
func (n *Normalizer) invalidStructuredResult(value dnd.LocationList, warnings []contracts.Warning) contracts.TypedNormalizeResult[dnd.LocationList] {
return contracts.TypedNormalizeResult[dnd.LocationList]{Value: value, Warnings: limitWarningsForRetry(warnings), Retry: &contracts.NormalizeRetry{
func (n *Normalizer) invalidStructuredResult(value dnd.LocationRegistry, warnings []contracts.Warning) contracts.TypedNormalizeResult[dnd.LocationRegistry] {
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{Value: value, Warnings: limitWarningsForRetry(warnings), Retry: &contracts.NormalizeRetry{
ReasonCode: ReasonCodeLocationSemanticProposalInvalid, Message: "semantic proposal requires retry: invalid structured output",
FallbackWarnings: []contracts.Warning{semanticFallbackWarning(-1)},
}}
}
func retryResult(value dnd.LocationList, warnings []contracts.Warning, assessment entityreconcile.Assessment) contracts.TypedNormalizeResult[dnd.LocationList] {
return contracts.TypedNormalizeResult[dnd.LocationList]{Value: value, Warnings: limitWarningsForRetry(warnings), Retry: &contracts.NormalizeRetry{
func retryResult(value dnd.LocationRegistry, warnings []contracts.Warning, assessment entityreconcile.Assessment) contracts.TypedNormalizeResult[dnd.LocationRegistry] {
return contracts.TypedNormalizeResult[dnd.LocationRegistry]{Value: value, Warnings: limitWarningsForRetry(warnings), Retry: &contracts.NormalizeRetry{
ReasonCode: ReasonCodeLocationSemanticProposalInvalid, Message: diagnostics.Aggregate("semantic proposal requires retry", reconciliationIssues(assessment)),
FallbackWarnings: []contracts.Warning{semanticFallbackWarning(assessment.DiscardedGroups())},
}}
@@ -183,7 +183,7 @@ type normalizedRecord struct {
earliest int
}
func preprocessRecords(input dnd.LocationList, order shared.SourceRefOrder) ([]normalizedRecord, []contracts.Warning) {
func preprocessRecords(input dnd.LocationRegistry, order shared.SourceRefOrder) ([]normalizedRecord, []contracts.Warning) {
if input.Locations == nil {
return nil, nil
}
@@ -294,11 +294,11 @@ func recordValues(records []normalizedRecord) []dnd.Location {
}
return values
}
func recordList(records []normalizedRecord) dnd.LocationList {
func recordList(records []normalizedRecord) dnd.LocationRegistry {
if records == nil {
return dnd.LocationList{}
return dnd.LocationRegistry{}
}
return dnd.LocationList{Locations: recordValues(records)}
return dnd.LocationRegistry{Locations: recordValues(records)}
}
func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
@@ -320,10 +320,10 @@ func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
func locationScope(index int) string { return fmt.Sprintf("locations[%d]", index) }
func ModuleSpec() pipeline.ModuleSpec {
return pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), ArtifactKind: dnd.LocationListKind}
return pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), ArtifactKind: dnd.LocationRegistryKind}
}
func Register(registry *pipeline.NormalizerRegistry) error {
return pipeline.RegisterNormalizerBuilder(registry, ModuleSpec(), validateOptions, func(request pipeline.BuildRequest) (contracts.Normalizer[dnd.LocationList], error) {
return pipeline.RegisterNormalizerBuilder(registry, ModuleSpec(), validateOptions, func(request pipeline.BuildRequest) (contracts.Normalizer[dnd.LocationRegistry], error) {
options, err := DecodeOptions(request.Options)
if err != nil {
return nil, err

View File

@@ -16,7 +16,7 @@ import (
)
func TestModuleContractAndMetadata(t *testing.T) {
want := pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: []string{"merged"}, Provides: []string{"normalized"}, ArtifactKind: dnd.LocationListKind}
want := pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: []string{"merged"}, Provides: []string{"normalized"}, ArtifactKind: dnd.LocationRegistryKind}
if got := ModuleSpec(); !reflect.DeepEqual(got, want) {
t.Fatalf("ModuleSpec() = %#v, want %#v", got, want)
}
@@ -38,13 +38,13 @@ func TestModuleContractAndMetadata(t *testing.T) {
}
func TestNormalizePreparesOnlyExactDuplicatesAndRetainsSameNameAndNestedPlaces(t *testing.T) {
input := dnd.LocationList{Locations: []dnd.Location{
input := dnd.LocationRegistry{Locations: []dnd.Location{
{Name: " The Tavern ", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}},
{Name: "the tavern", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}},
{Name: "The Tavern", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 2, EndUnitID: 2}}},
{Name: "The Tavern Cellar", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 2, EndUnitID: 2}}},
}}
before := dnd.LocationList{Locations: append([]dnd.Location(nil), input.Locations...)}
before := dnd.LocationRegistry{Locations: append([]dnd.Location(nil), input.Locations...)}
result, err := newNormalizer(t, &recordingLocationNormalizerClient{}).Normalize(context.Background(), normalizeRequest(input))
if err != nil || len(result.Value.Locations) != 3 {
t.Fatalf("Normalize() = %#v, %v; want one exact duplicate removed", result, err)
@@ -63,7 +63,7 @@ func TestNormalizePreparesOnlyExactDuplicatesAndRetainsSameNameAndNestedPlaces(t
func TestNormalizeAppliesSafeAliasGroupAndUsesOpaqueInputs(t *testing.T) {
client := &recordingLocationNormalizerClient{response: `{"duplicate_groups":[{"members":["candidate-000001","candidate-000002"],"canonical":"candidate-000002"}]}`}
doc := semanticDocument()
input := dnd.LocationList{Locations: []dnd.Location{
input := dnd.LocationRegistry{Locations: []dnd.Location{
{Name: "Old Mill", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
{Name: "the Greencloak's refuge", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}},
{Name: "The Tavern", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}}},
@@ -84,7 +84,7 @@ func TestNormalizeAppliesSafeAliasGroupAndUsesOpaqueInputs(t *testing.T) {
func TestNormalizeRejectsUnsafeAndOverlappingGroupsWithoutLosingCandidates(t *testing.T) {
doc := semanticDocument()
input := dnd.LocationList{Locations: []dnd.Location{
input := dnd.LocationRegistry{Locations: []dnd.Location{
{Name: "Old Mill", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
{Name: "Mill", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}},
{Name: "Tavern", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}}},
@@ -101,7 +101,7 @@ func TestNormalizeRejectsUnsafeAndOverlappingGroupsWithoutLosingCandidates(t *te
func TestNormalizeHandlesRetryFallbackAndErrors(t *testing.T) {
doc := semanticDocument()
input := dnd.LocationList{Locations: []dnd.Location{{Name: "Old Mill", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}}, {Name: "Mill", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}}}}
input := dnd.LocationRegistry{Locations: []dnd.Location{{Name: "Old Mill", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}}, {Name: "Mill", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}}}}
invalid, err := newNormalizer(t, &recordingLocationNormalizerClient{err: contracts.ErrInvalidStructuredOutput}).Normalize(context.Background(), normalizeRequestWithSource(input, doc))
if err != nil || invalid.Retry == nil || invalid.Retry.ReasonCode != ReasonCodeLocationSemanticProposalInvalid {
t.Fatalf("invalid result = %#v, %v", invalid, err)
@@ -119,7 +119,7 @@ func TestNormalizeHandlesRetryFallbackAndErrors(t *testing.T) {
func TestNormalizeOrdersEvidenceAndIsIdempotent(t *testing.T) {
doc := &source.SourceDocument{ID: "ordered", Units: []source.SourceUnit{{ID: 30}, {ID: 10}}}
input := dnd.LocationList{Locations: []dnd.Location{{Name: "Old Mill", SourceRefs: []source.SourceRef{
input := dnd.LocationRegistry{Locations: []dnd.Location{{Name: "Old Mill", SourceRefs: []source.SourceRef{
{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10},
{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30},
{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30},

View File

@@ -39,10 +39,10 @@ func newNormalizer(t *testing.T, client contracts.StructuredLLMClient) *Normaliz
}
return normalizer
}
func normalizeRequest(value dnd.LocationList) contracts.TypedNormalizeRequest[dnd.LocationList] {
return contracts.TypedNormalizeRequest[dnd.LocationList]{MergeOutput: contracts.MergeArtifact[dnd.LocationList]{Value: value}}
func normalizeRequest(value dnd.LocationRegistry) contracts.TypedNormalizeRequest[dnd.LocationRegistry] {
return contracts.TypedNormalizeRequest[dnd.LocationRegistry]{MergeOutput: contracts.MergeArtifact[dnd.LocationRegistry]{Value: value}}
}
func normalizeRequestWithSource(value dnd.LocationList, doc *source.SourceDocument) contracts.TypedNormalizeRequest[dnd.LocationList] {
func normalizeRequestWithSource(value dnd.LocationRegistry, doc *source.SourceDocument) contracts.TypedNormalizeRequest[dnd.LocationRegistry] {
request := normalizeRequest(value)
request.Source = doc
return request

View File

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

View File

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

View File

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

View File

@@ -84,10 +84,10 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
assertContainsKeys(t, "chunkers", registries.Chunkers.RegisteredKeys(), []string{"dnd/scenes"})
assertContainsKeys(t, "extractors", registries.Extractors.RegisteredKeys(), []string{"dnd/spells", npcextract.Key, combatextract.Key, enemyeventextract.Key, itemeventextract.Key, occurrenceextract.Key, scenedescriptionextract.Key, locationextract.Key, locationoccurrenceextract.Key})
assertContainsKeys(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{spellnormalize.Key, npcnormalize.Key, combatnormalize.Key, enemyeventnormalize.Key, itemeventnormalize.Key, occurrencenormalize.Key, scenedescriptionnormalize.Key, locationnormalize.Key, locationoccurrencenormalize.Key, pipeline.DefaultNormalizeModule})
assertContainsArtifactKinds(t, registries.ArtifactCodecs.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind})
assertContainsArtifactKinds(t, registries.ArtifactEvidence.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind})
assertContainsArtifactKinds(t, registries.Mergers.RegisteredArtifactKinds(pipeline.DefaultMergeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind})
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(pipeline.DefaultNormalizeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind})
assertContainsArtifactKinds(t, registries.ArtifactCodecs.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationRegistryKind, dnd.LocationOccurrenceListKind})
assertContainsArtifactKinds(t, registries.ArtifactEvidence.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationRegistryKind, dnd.LocationOccurrenceListKind})
assertContainsArtifactKinds(t, registries.Mergers.RegisteredArtifactKinds(pipeline.DefaultMergeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationRegistryKind, dnd.LocationOccurrenceListKind})
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(pipeline.DefaultNormalizeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCOccurrenceListKind, dnd.SceneDescriptionListKind, dnd.LocationRegistryKind, dnd.LocationOccurrenceListKind})
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(npcnormalize.Key), []contracts.ArtifactKind{dnd.NPCRegistryKind})
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(combatnormalize.Key), []contracts.ArtifactKind{dnd.CombatTurnListKind})
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(enemyeventnormalize.Key), []contracts.ArtifactKind{dnd.EnemyEventListKind})
@@ -358,7 +358,7 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
}
locationExtractSpec, locationExtractOK := registries.Extractors.Spec(locationextract.Key)
locationNormalizeSpec, locationNormalizeOK := registries.Normalizers.Spec(locationnormalize.Key)
if !locationExtractOK || locationExtractSpec.ArtifactKind != dnd.LocationListKind || locationExtractSpec.ExecutionClass != contracts.ExecutionClassLLMBacked || !locationNormalizeOK || locationNormalizeSpec.ArtifactKind != dnd.LocationListKind || locationNormalizeSpec.ExecutionClass != contracts.ExecutionClassLLMBacked {
if !locationExtractOK || locationExtractSpec.ArtifactKind != dnd.LocationRegistryKind || locationExtractSpec.ExecutionClass != contracts.ExecutionClassLLMBacked || !locationNormalizeOK || locationNormalizeSpec.ArtifactKind != dnd.LocationRegistryKind || locationNormalizeSpec.ExecutionClass != contracts.ExecutionClassLLMBacked {
t.Fatalf("location specs = %#v / %#v", locationExtractSpec, locationNormalizeSpec)
}
locationOccurrenceExtractSpec, locationOccurrenceExtractOK := registries.Extractors.Spec(locationoccurrenceextract.Key)
@@ -371,7 +371,7 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
if len(locationOccurrenceExtractSpec.ReferenceSlots) != 5 || len(locationOccurrenceNormalizeSpec.ReferenceSlots) != 1 {
t.Fatalf("location occurrence reference slots = %#v / %#v, want extractor campaign context and normalizer registry only", locationOccurrenceExtractSpec.ReferenceSlots, locationOccurrenceNormalizeSpec.ReferenceSlots)
}
if !locationRegistrySlot.Required || !reflect.DeepEqual(locationRegistrySlot.AcceptedMediaTypes, []string{"application/json"}) || !reflect.DeepEqual(locationRegistrySlot.AcceptedArtifactKinds, []contracts.ArtifactKind{dnd.LocationListKind}) || locationRegistrySlot.MaxBytes != 1048576 || !sameReferenceSlotContract(locationRegistrySlot, occurrenceNormalizeRegistrySlot) {
if !locationRegistrySlot.Required || !reflect.DeepEqual(locationRegistrySlot.AcceptedMediaTypes, []string{"application/json"}) || !reflect.DeepEqual(locationRegistrySlot.AcceptedArtifactKinds, []contracts.ArtifactKind{dnd.LocationRegistryKind}) || locationRegistrySlot.MaxBytes != 1048576 || !sameReferenceSlotContract(locationRegistrySlot, occurrenceNormalizeRegistrySlot) {
t.Fatalf("location registry slots disagree: %#v / %#v", occurrenceExtractSpec.ReferenceSlots, occurrenceNormalizeSpec.ReferenceSlots)
}
for _, name := range []string{"party", "roster", "players", "glossary"} {
@@ -425,7 +425,7 @@ func TestEvidenceProjectorsPreserveDirectReferencesWithIndependentStorage(t *tes
return sceneDescriptionEvidence(dnd.SceneDescriptionList{Scenes: []dnd.SceneDescription{{SourceRef: first}, {SourceRef: second}}})
}, want: []source.SourceRef{first, second}},
{name: "locations", project: func() []source.SourceRef {
return locationEvidence(dnd.LocationList{Locations: []dnd.Location{{SourceRefs: []source.SourceRef{first, second}}}})
return locationEvidence(dnd.LocationRegistry{Locations: []dnd.Location{{SourceRefs: []source.SourceRef{first, second}}}})
}, want: []source.SourceRef{first, second}},
{name: "location occurrences", project: func() []source.SourceRef {
return locationOccurrenceEvidence(dnd.LocationOccurrenceList{Occurrences: []dnd.LocationOccurrence{{SourceRefs: []source.SourceRef{first, second}}}})
@@ -490,19 +490,19 @@ func TestAppendNPCRegistriesPreservesOrderAndArrayPresence(t *testing.T) {
}
}
func TestAppendLocationListsPreserveOrderPresenceAndOwnership(t *testing.T) {
func TestAppendLocationRegistriesPreserveOrderPresenceAndOwnership(t *testing.T) {
refs := []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}
input := []dnd.LocationList{{Locations: []dnd.Location{{ID: "one", Name: "First", SourceRefs: refs}}}, {Locations: []dnd.Location{{ID: "two", Name: "Second", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 2, EndUnitID: 2}}}}}}
got, err := appendLocationLists(input)
input := []dnd.LocationRegistry{{Locations: []dnd.Location{{ID: "one", Name: "First", SourceRefs: refs}}}, {Locations: []dnd.Location{{ID: "two", Name: "Second", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 2, EndUnitID: 2}}}}}}
got, err := appendLocationRegistries(input)
if err != nil || !reflect.DeepEqual([]string{got.Locations[0].Name, got.Locations[1].Name}, []string{"First", "Second"}) {
t.Fatalf("appendLocationLists() = %#v, %v", got, err)
t.Fatalf("appendLocationRegistries() = %#v, %v", got, err)
}
got.Locations[0].SourceRefs[0].StartUnitID = 99
if input[0].Locations[0].SourceRefs[0].StartUnitID != 1 {
t.Fatal("merged locations share source-reference storage")
}
for _, values := range [][]dnd.LocationList{nil, {{}, {}}} {
result, err := appendLocationLists(values)
for _, values := range [][]dnd.LocationRegistry{nil, {{}, {}}} {
result, err := appendLocationRegistries(values)
if err != nil || result.Locations != nil {
t.Fatalf("nil-only merge = %#v, %v", result, err)
}

View File

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

View File

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

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}
thirdRefs := []source.SourceRef{{SourceID: "source", StartUnitID: 3, EndUnitID: 3}}
third := dnd.Location{ID: identity.DeriveID("Sun Gate", thirdRefs), Name: "Sun Gate", SourceRefs: thirdRefs}
content, err := locationcodec.New().Encode(dnd.LocationList{Locations: []dnd.Location{first, second, third}})
content, err := locationcodec.New().Encode(dnd.LocationRegistry{Locations: []dnd.Location{first, second, third}})
if err != nil {
t.Fatal(err)
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ const (
type Options struct{}
type Validator struct{}
var _ contracts.TypedValidator[dnd.LocationList] = (*Validator)(nil)
var _ contracts.TypedValidator[dnd.LocationRegistry] = (*Validator)(nil)
var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil)
func New(Options) *Validator { return &Validator{} }
@@ -34,7 +34,7 @@ func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
return []pipeline.CheckpointFingerprint{{Name: "policy", Value: policy}}
}
func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.LocationList]) (contracts.ValidationResult, error) {
func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.LocationRegistry]) (contracts.ValidationResult, error) {
if req.Stage == string(pipeline.StageExtract) && req.Chunk == nil {
return contracts.ValidationResult{}, fmt.Errorf("location source-reference validator requires the current extraction chunk")
}
@@ -78,7 +78,7 @@ func Spec() pipeline.ValidatorSpec {
}
func Register(registry *pipeline.ValidatorRegistry) error {
return pipeline.RegisterTypedValidatorBuilder(registry, dnd.LocationListKind, Spec(), validateOptions, func(request pipeline.BuildRequest) (contracts.TypedValidator[dnd.LocationList], error) {
return pipeline.RegisterTypedValidatorBuilder(registry, dnd.LocationRegistryKind, Spec(), validateOptions, func(request pipeline.BuildRequest) (contracts.TypedValidator[dnd.LocationRegistry], error) {
options, err := DecodeOptions(request.Options)
if err != nil {
return nil, err

View File

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

View File

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

View File

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

View File

@@ -33,7 +33,7 @@ func TestLocationRegistryHandoffProducesOccurrencesAndEvidence(t *testing.T) {
}
registryOutput := normalizedLane(t, output, "locations")
if registryOutput.Artifact.Kind != dnd.LocationListKind || registryOutput.Artifact.Schema.ID != locationcodec.SchemaID {
if registryOutput.Artifact.Kind != dnd.LocationRegistryKind || registryOutput.Artifact.Schema.ID != locationcodec.SchemaID {
t.Fatalf("registry envelope = %#v, want durable location artifact", registryOutput)
}
registry, err := locationcodec.New().Decode(registryOutput.Artifact.Content)