Adopt location registry durable contract
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"occurrences": [
|
||||
{
|
||||
"location_id": "location:sha256:cdd2b57615b56a4e92d506cf053cd3985e11d4df4895c5383b30ec2ce9f39ed0",
|
||||
"location_id": "location:sha256:4ea39088943a692f120ae8740419c3baa1e7026c0e2a26ab03255f0b7e20215c",
|
||||
"name": "The Old Tavern",
|
||||
"kind": "visited",
|
||||
"source_refs": [
|
||||
|
||||
@@ -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"],
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}}},
|
||||
}}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: ""},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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...)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}}}}}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}}}}}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
func TestValidatorUsesOnlyCitedTranscriptText(t *testing.T) {
|
||||
value := dnd.LocationList{Locations: []dnd.Location{{ID: "candidate", Name: "O'Rin's Gate", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}}}
|
||||
value := dnd.LocationRegistry{Locations: []dnd.Location{{ID: "candidate", Name: "O'Rin's Gate", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}}}
|
||||
doc := &source.SourceDocument{ID: "session", Kind: "transcript", Format: "application/json", Digest: "sha256:session", Units: []source.SourceUnit{{ID: 1, Kind: "message", Text: "The party enters o’rin’s gate."}, {ID: 2, Kind: "message", Text: "Unrelated location."}}}
|
||||
result, err := New(Options{}).Validate(context.Background(), request(doc, value, contracts.ReferenceSet{}))
|
||||
if err != nil || !result.Approved || len(result.Warnings) != 0 {
|
||||
@@ -30,12 +30,12 @@ func TestValidatorUsesOnlyCitedTranscriptText(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidatorDefersMalformedOrUnreadableCitations(t *testing.T) {
|
||||
invalid := dnd.LocationList{Locations: []dnd.Location{{Name: "Missing"}}}
|
||||
invalid := dnd.LocationRegistry{Locations: []dnd.Location{{Name: "Missing"}}}
|
||||
result, err := New(Options{}).Validate(context.Background(), request(document(), invalid, contracts.ReferenceSet{}))
|
||||
if err != nil || !result.Approved || len(result.Warnings) != 0 {
|
||||
t.Fatalf("shape deferral = %#v, %v", result, err)
|
||||
}
|
||||
value := dnd.LocationList{Locations: []dnd.Location{{ID: "candidate", Name: "Missing", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 99, EndUnitID: 99}}}}}
|
||||
value := dnd.LocationRegistry{Locations: []dnd.Location{{ID: "candidate", Name: "Missing", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 99, EndUnitID: 99}}}}}
|
||||
result, err = New(Options{}).Validate(context.Background(), request(document(), value, contracts.ReferenceSet{}))
|
||||
if err != nil || !result.Approved || len(result.Warnings) != 0 {
|
||||
t.Fatalf("unreadable citation = %#v, %v", result, err)
|
||||
@@ -60,14 +60,14 @@ func TestValidatorBoundsWarnings(t *testing.T) {
|
||||
for index := range locations {
|
||||
locations[index] = dnd.Location{ID: "candidate", Name: "Missing", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}
|
||||
}
|
||||
result, err := New(Options{}).Validate(context.Background(), request(document(), dnd.LocationList{Locations: locations}, contracts.ReferenceSet{}))
|
||||
result, err := New(Options{}).Validate(context.Background(), request(document(), dnd.LocationRegistry{Locations: locations}, contracts.ReferenceSet{}))
|
||||
if err != nil || !result.Approved || len(result.Warnings) != diagnostics.MaxWarnings || result.Warnings[len(result.Warnings)-1].ReasonCode != OmittedReasonCode {
|
||||
t.Fatalf("bounded warnings = %#v, %v", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
func request(doc *source.SourceDocument, value dnd.LocationList, references contracts.ReferenceSet) contracts.TypedValidationRequest[dnd.LocationList] {
|
||||
return contracts.TypedValidationRequest[dnd.LocationList]{Source: doc, References: references, Value: value}
|
||||
func request(doc *source.SourceDocument, value dnd.LocationRegistry, references contracts.ReferenceSet) contracts.TypedValidationRequest[dnd.LocationRegistry] {
|
||||
return contracts.TypedValidationRequest[dnd.LocationRegistry]{Source: doc, References: references, Value: value}
|
||||
}
|
||||
|
||||
func document() *source.SourceDocument {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user