Migrate NPC registry durable contract
This commit is contained in:
@@ -383,7 +383,7 @@ binding contracts. Durable semantics and wire shapes remain in their
|
|||||||
|
|
||||||
| Slot | Accepted artifact kind | Media type | Maximum size | Required stage |
|
| Slot | Accepted artifact kind | Media type | Maximum size | Required stage |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| `npcs` | `dnd/npc-list` | `application/json` | 1,048,576 bytes | extract and normalize |
|
| `npcs` | `dnd/npc-registry` | `application/json` | 1,048,576 bytes | extract and normalize |
|
||||||
| `scene_descriptions` | `dnd/scene-description-list` | `application/json` | 1,048,576 bytes | extract only |
|
| `scene_descriptions` | `dnd/scene-description-list` | `application/json` | 1,048,576 bytes | extract only |
|
||||||
| `combat_turns` | `dnd/combat-turn-list` | `application/json` | 1,048,576 bytes | extract only |
|
| `combat_turns` | `dnd/combat-turn-list` | `application/json` | 1,048,576 bytes | extract only |
|
||||||
| `npc_interactions` | `dnd/npc-interaction-list` | `application/json` | 1,048,576 bytes | extract only |
|
| `npc_interactions` | `dnd/npc-interaction-list` | `application/json` | 1,048,576 bytes | extract only |
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# D&D NPC Artifact
|
# D&D NPC Registry Artifact
|
||||||
|
|
||||||
This contract defines the durable NPC registry produced by `dnd/npcs`. It is a
|
This contract defines the durable NPC registry produced by `dnd/npcs`. It is a
|
||||||
minimal, source-grounded identity registry for other D&D artifacts, not a
|
minimal, source-grounded identity registry for other D&D artifacts, not a
|
||||||
@@ -8,12 +8,12 @@ character sheet or a relationship summary.
|
|||||||
|
|
||||||
| Property | Value |
|
| Property | Value |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Artifact kind | `dnd/npc-list` |
|
| Artifact kind | `dnd/npc-registry` |
|
||||||
| Schema ID | `notarius.dnd.npcs` |
|
| Schema ID | `notarius.dnd.npc_registry` |
|
||||||
| Schema name | `notarius_dnd_npcs_v1` |
|
| Schema name | `notarius_dnd_npc_registry_v1` |
|
||||||
| Schema version | `v1` |
|
| Schema version | `v1` |
|
||||||
| Media type | `application/json` |
|
| Media type | `application/json` |
|
||||||
| Identity policy | `dnd.npcs.identity.v1` |
|
| Identity policy | `dnd.npc_registry.identity.v1` |
|
||||||
|
|
||||||
`v1` accepts one strict JSON object with required `npcs`; the array may be
|
`v1` accepts one strict JSON object with required `npcs`; the array may be
|
||||||
empty. NPC and source-reference objects reject unknown fields. An incompatible
|
empty. NPC and source-reference objects reject unknown fields. An incompatible
|
||||||
@@ -37,7 +37,7 @@ identifiers, and the start may not follow the end.
|
|||||||
{
|
{
|
||||||
"npcs": [
|
"npcs": [
|
||||||
{
|
{
|
||||||
"id": "npc:sha256:99a16589618a04f535a7d21fdcc71a0b1c05d22f752cd492065b1086d97bc3d7",
|
"id": "npc:sha256:35ba5f679aee69e07ae3bd65c44278f29539d5dc9bb5225db1c0060555b23221",
|
||||||
"name": "Mira Thorn",
|
"name": "Mira Thorn",
|
||||||
"source_refs": [
|
"source_refs": [
|
||||||
{"source_id": "session-7", "start_unit_id": 4, "end_unit_id": 5}
|
{"source_id": "session-7", "start_unit_id": 4, "end_unit_id": 5}
|
||||||
@@ -48,10 +48,12 @@ identifiers, and the start may not follow the end.
|
|||||||
```
|
```
|
||||||
|
|
||||||
The ID is deterministic: normalize the name to Unicode NFKC, normalize the
|
The ID is deterministic: normalize the name to Unicode NFKC, normalize the
|
||||||
supported apostrophe forms, collapse whitespace, case-fold it, SHA-256 the
|
supported apostrophe forms, collapse whitespace, case-fold it, then serialize
|
||||||
result, then prefix the lowercase hexadecimal digest with `npc:sha256:`. Each
|
`["dnd.npc_registry.identity.v1", comparison_name]` as compact JSON. SHA-256
|
||||||
canonical identity and ID appears at most once. Normalization collapses records
|
those UTF-8 bytes and prefix the lowercase hexadecimal digest with
|
||||||
with the same canonical identity, retains their earliest position, and merges
|
`npc:sha256:`. Each canonical identity and ID appears at most once.
|
||||||
|
Normalization collapses records with the same canonical identity, retains their
|
||||||
|
earliest position, and merges
|
||||||
their canonicalized evidence; it does not add aliases, roles, descriptions, or
|
their canonicalized evidence; it does not add aliases, roles, descriptions, or
|
||||||
relationship fields.
|
relationship fields.
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func TestProductionNPCConfigurationResolvesTypedLane(t *testing.T) {
|
|||||||
t.Fatalf("artifact lanes = %#v, want one NPC lane", effective.ResolvedPipeline.Steps[0].ArtifactLanes)
|
t.Fatalf("artifact lanes = %#v, want one NPC lane", effective.ResolvedPipeline.Steps[0].ArtifactLanes)
|
||||||
}
|
}
|
||||||
lane := effective.ResolvedPipeline.Steps[0].ArtifactLanes[0]
|
lane := effective.ResolvedPipeline.Steps[0].ArtifactLanes[0]
|
||||||
if lane.ID != "npcs" || lane.ArtifactKind != dnd.NPCListKind || lane.Extract.Module != npcextract.Key || lane.Extract.Retries != 2 || lane.Merge.Module != pipeline.DefaultMergeModule || lane.Normalize.Module != npcnormalize.Key {
|
if lane.ID != "npcs" || lane.ArtifactKind != dnd.NPCRegistryKind || lane.Extract.Module != npcextract.Key || lane.Extract.Retries != 2 || lane.Merge.Module != pipeline.DefaultMergeModule || lane.Normalize.Module != npcnormalize.Key {
|
||||||
t.Fatalf("resolved NPC lane = %#v, want typed production composition", lane)
|
t.Fatalf("resolved NPC lane = %#v, want typed production composition", lane)
|
||||||
}
|
}
|
||||||
if len(lane.ExtractReferences.Bindings) != 0 || len(lane.NormalizeReferences.Bindings) != 0 {
|
if len(lane.ExtractReferences.Bindings) != 0 || len(lane.NormalizeReferences.Bindings) != 0 {
|
||||||
@@ -39,16 +39,16 @@ func TestProductionNPCConfigurationResolvesTypedLane(t *testing.T) {
|
|||||||
if !ok || !reflect.DeepEqual(extractSpec.Requires, []string{"chunks", "source.transcript"}) || !reflect.DeepEqual(extractSpec.Provides, []string{"dnd.npcs"}) {
|
if !ok || !reflect.DeepEqual(extractSpec.Requires, []string{"chunks", "source.transcript"}) || !reflect.DeepEqual(extractSpec.Provides, []string{"dnd.npcs"}) {
|
||||||
t.Fatalf("NPC extractor spec = %#v, want source and artifact capabilities", extractSpec)
|
t.Fatalf("NPC extractor spec = %#v, want source and artifact capabilities", extractSpec)
|
||||||
}
|
}
|
||||||
mergeSpec, ok := catalog.Mergers.SpecForArtifact(pipeline.DefaultMergeModule, dnd.NPCListKind)
|
mergeSpec, ok := catalog.Mergers.SpecForArtifact(pipeline.DefaultMergeModule, dnd.NPCRegistryKind)
|
||||||
if !ok || !reflect.DeepEqual(mergeSpec.Provides, []string{"merged"}) {
|
if !ok || !reflect.DeepEqual(mergeSpec.Provides, []string{"merged"}) {
|
||||||
t.Fatalf("NPC merger spec = %#v, want merged capability", mergeSpec)
|
t.Fatalf("NPC merger spec = %#v, want merged capability", mergeSpec)
|
||||||
}
|
}
|
||||||
normalizeSpec, ok := catalog.Normalizers.SpecForArtifact(npcnormalize.Key, dnd.NPCListKind)
|
normalizeSpec, ok := catalog.Normalizers.SpecForArtifact(npcnormalize.Key, dnd.NPCRegistryKind)
|
||||||
if !ok || !reflect.DeepEqual(normalizeSpec.Requires, []string{"merged"}) || !reflect.DeepEqual(normalizeSpec.Provides, []string{"normalized"}) {
|
if !ok || !reflect.DeepEqual(normalizeSpec.Requires, []string{"merged"}) || !reflect.DeepEqual(normalizeSpec.Provides, []string{"normalized"}) {
|
||||||
t.Fatalf("NPC normalizer spec = %#v, want merged/normalized capabilities", normalizeSpec)
|
t.Fatalf("NPC normalizer spec = %#v, want merged/normalized capabilities", normalizeSpec)
|
||||||
}
|
}
|
||||||
codecSpec, ok := catalog.ArtifactCodecs.Spec(dnd.NPCListKind)
|
codecSpec, ok := catalog.ArtifactCodecs.Spec(dnd.NPCRegistryKind)
|
||||||
if !ok || codecSpec.Kind != dnd.NPCListKind || codecSpec.Schema.ID != npccodec.SchemaID || codecSpec.Schema.Version != npccodec.SchemaVersion {
|
if !ok || codecSpec.Kind != dnd.NPCRegistryKind || codecSpec.Schema.ID != npccodec.SchemaID || codecSpec.Schema.Version != npccodec.SchemaVersion {
|
||||||
t.Fatalf("NPC codec spec = %#v, want typed v1 durable schema", codecSpec)
|
t.Fatalf("NPC codec spec = %#v, want typed v1 durable schema", codecSpec)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,9 +76,9 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) {
|
|||||||
"generic/valid_json",
|
"generic/valid_json",
|
||||||
"generic/valid_json_schema",
|
"generic/valid_json_schema",
|
||||||
})
|
})
|
||||||
assertProductionContains(t, "artifact codec kinds", registries.ArtifactCodecs.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind, dnd.ItemEventListKind, dnd.EnemyEventListKind})
|
assertProductionContains(t, "artifact codec kinds", registries.ArtifactCodecs.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.ItemEventListKind, dnd.EnemyEventListKind})
|
||||||
assertProductionContains(t, "merger variants", registries.Mergers.RegisteredArtifactKinds(pipeline.DefaultMergeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind, dnd.ItemEventListKind, dnd.EnemyEventListKind})
|
assertProductionContains(t, "merger variants", registries.Mergers.RegisteredArtifactKinds(pipeline.DefaultMergeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.ItemEventListKind, dnd.EnemyEventListKind})
|
||||||
assertProductionContains(t, "normalizer variants", registries.Normalizers.RegisteredArtifactKinds(pipeline.DefaultNormalizeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind, dnd.ItemEventListKind, dnd.EnemyEventListKind})
|
assertProductionContains(t, "normalizer variants", registries.Normalizers.RegisteredArtifactKinds(pipeline.DefaultNormalizeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.ItemEventListKind, dnd.EnemyEventListKind})
|
||||||
assertProductionContains(t, "spell normalizer variants", registries.Normalizers.RegisteredArtifactKinds(spellnormalize.Key), []contracts.ArtifactKind{dnd.SpellListKind})
|
assertProductionContains(t, "spell normalizer variants", registries.Normalizers.RegisteredArtifactKinds(spellnormalize.Key), []contracts.ArtifactKind{dnd.SpellListKind})
|
||||||
assertProductionContains(t, "combat normalizer variants", registries.Normalizers.RegisteredArtifactKinds(combatnormalize.Key), []contracts.ArtifactKind{dnd.CombatTurnListKind})
|
assertProductionContains(t, "combat normalizer variants", registries.Normalizers.RegisteredArtifactKinds(combatnormalize.Key), []contracts.ArtifactKind{dnd.CombatTurnListKind})
|
||||||
assertProductionContains(t, "item event normalizer variants", registries.Normalizers.RegisteredArtifactKinds(itemeventnormalize.Key), []contracts.ArtifactKind{dnd.ItemEventListKind})
|
assertProductionContains(t, "item event normalizer variants", registries.Normalizers.RegisteredArtifactKinds(itemeventnormalize.Key), []contracts.ArtifactKind{dnd.ItemEventListKind})
|
||||||
|
|||||||
@@ -90,10 +90,10 @@ func TestCloneReferenceSlotsCopiesAcceptedMediaTypes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCloneReferenceSlotsCopiesAcceptedArtifactKinds(t *testing.T) {
|
func TestCloneReferenceSlotsCopiesAcceptedArtifactKinds(t *testing.T) {
|
||||||
slots := []ReferenceSlot{{Name: "npcs", AcceptedArtifactKinds: []ArtifactKind{"dnd/npc-list"}}}
|
slots := []ReferenceSlot{{Name: "npcs", AcceptedArtifactKinds: []ArtifactKind{"dnd/npc-registry"}}}
|
||||||
clone := CloneReferenceSlots(slots)
|
clone := CloneReferenceSlots(slots)
|
||||||
clone[0].AcceptedArtifactKinds[0] = "changed"
|
clone[0].AcceptedArtifactKinds[0] = "changed"
|
||||||
if slots[0].AcceptedArtifactKinds[0] != "dnd/npc-list" {
|
if slots[0].AcceptedArtifactKinds[0] != "dnd/npc-registry" {
|
||||||
t.Fatalf("source AcceptedArtifactKinds aliased clone: %#v", slots[0].AcceptedArtifactKinds)
|
t.Fatalf("source AcceptedArtifactKinds aliased clone: %#v", slots[0].AcceptedArtifactKinds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func TestCodecRoundTripAndIdentities(t *testing.T) {
|
|||||||
if !ok || spec.SchemaDigest != contracts.DigestArtifactSchema(schema) {
|
if !ok || spec.SchemaDigest != contracts.DigestArtifactSchema(schema) {
|
||||||
t.Fatalf("registered spec = %#v, %t", spec, ok)
|
t.Fatalf("registered spec = %#v, %t", spec, ok)
|
||||||
}
|
}
|
||||||
if _, err := registry.Encode(dnd.ItemEventListKind, dnd.NPCList{}); err == nil {
|
if _, err := registry.Encode(dnd.ItemEventListKind, dnd.NPCRegistry{}); err == nil {
|
||||||
t.Fatal("Encode() error = nil, want exact type rejection")
|
t.Fatal("Encode() error = nil, want exact type rejection")
|
||||||
} else {
|
} else {
|
||||||
var typeErr *pipeline.ArtifactCodecTypeError
|
var typeErr *pipeline.ArtifactCodecTypeError
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ func TestCodecOwnsDurableSchemaAndRegistersExactType(t *testing.T) {
|
|||||||
if !ok || spec.SchemaDigest != contracts.DigestArtifactSchema(schema) {
|
if !ok || spec.SchemaDigest != contracts.DigestArtifactSchema(schema) {
|
||||||
t.Fatalf("registered spec = %#v, %t", spec, ok)
|
t.Fatalf("registered spec = %#v, %t", spec, ok)
|
||||||
}
|
}
|
||||||
if _, err := registry.Encode(dnd.NPCInteractionListKind, dnd.NPCList{}); err == nil {
|
if _, err := registry.Encode(dnd.NPCInteractionListKind, dnd.NPCRegistry{}); err == nil {
|
||||||
t.Fatal("Encode() error = nil, want exact type rejection")
|
t.Fatal("Encode() error = nil, want exact type rejection")
|
||||||
} else {
|
} else {
|
||||||
var typeErr *pipeline.ArtifactCodecTypeError
|
var typeErr *pipeline.ArtifactCodecTypeError
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "notarius.dnd.npcs",
|
"$id": "notarius.dnd.npc_registry",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["npcs"],
|
"required": ["npcs"],
|
||||||
@@ -12,25 +12,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SchemaID = "notarius.dnd.npcs"
|
SchemaID = "notarius.dnd.npc_registry"
|
||||||
SchemaName = "notarius_dnd_npcs_v1"
|
SchemaName = "notarius_dnd_npc_registry_v1"
|
||||||
SchemaVersion = "v1"
|
SchemaVersion = "v1"
|
||||||
MediaType = "application/json"
|
MediaType = "application/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed assets/schemas/dnd_npcs.v1.json
|
//go:embed assets/schemas/dnd_npc_registry.v1.json
|
||||||
var schemaAssets embed.FS
|
var schemaAssets embed.FS
|
||||||
|
|
||||||
var _ contracts.ArtifactCodec[dnd.NPCList] = (*Codec)(nil)
|
var _ contracts.ArtifactCodec[dnd.NPCRegistry] = (*Codec)(nil)
|
||||||
|
|
||||||
type Codec struct{}
|
type Codec struct{}
|
||||||
|
|
||||||
func New() *Codec { return &Codec{} }
|
func New() *Codec { return &Codec{} }
|
||||||
|
|
||||||
func (c *Codec) Kind() contracts.ArtifactKind { return dnd.NPCListKind }
|
func (c *Codec) Kind() contracts.ArtifactKind { return dnd.NPCRegistryKind }
|
||||||
|
|
||||||
func (c *Codec) Schema() contracts.ArtifactSchema {
|
func (c *Codec) Schema() contracts.ArtifactSchema {
|
||||||
raw, err := schemaAssets.ReadFile("assets/schemas/dnd_npcs.v1.json")
|
raw, err := schemaAssets.ReadFile("assets/schemas/dnd_npc_registry.v1.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return contracts.ArtifactSchema{}
|
return contracts.ArtifactSchema{}
|
||||||
}
|
}
|
||||||
@@ -44,41 +44,41 @@ func (c *Codec) Schema() contracts.ArtifactSchema {
|
|||||||
|
|
||||||
func (c *Codec) MediaType() string { return MediaType }
|
func (c *Codec) MediaType() string { return MediaType }
|
||||||
|
|
||||||
func (c *Codec) Metadata(value dnd.NPCList) map[string]any {
|
func (c *Codec) Metadata(value dnd.NPCRegistry) map[string]any {
|
||||||
return map[string]any{"npc_count": len(value.NPCs)}
|
return map[string]any{"npc_count": len(value.NPCs)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Codec) Encode(value dnd.NPCList) ([]byte, error) {
|
func (c *Codec) Encode(value dnd.NPCRegistry) ([]byte, error) {
|
||||||
if err := validate(value); err != nil {
|
if err := validate(value); err != nil {
|
||||||
return nil, fmt.Errorf("encode dnd npc list: %w", err)
|
return nil, fmt.Errorf("encode dnd npc registry: %w", err)
|
||||||
}
|
}
|
||||||
return c.EncodeCandidate(value)
|
return c.EncodeCandidate(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncodeCandidate provides the durable representation before semantic
|
// EncodeCandidate provides the durable representation before semantic
|
||||||
// validators have approved a value.
|
// validators have approved a value.
|
||||||
func (c *Codec) EncodeCandidate(value dnd.NPCList) ([]byte, error) {
|
func (c *Codec) EncodeCandidate(value dnd.NPCRegistry) ([]byte, error) {
|
||||||
return candidatejson.EncodeCandidate("dnd npc list", value)
|
return candidatejson.EncodeCandidate("dnd npc registry", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Codec) Decode(content []byte) (dnd.NPCList, error) {
|
func (c *Codec) Decode(content []byte) (dnd.NPCRegistry, error) {
|
||||||
value, err := c.DecodeCandidate(content)
|
value, err := c.DecodeCandidate(content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dnd.NPCList{}, err
|
return dnd.NPCRegistry{}, err
|
||||||
}
|
}
|
||||||
if err := validate(value); err != nil {
|
if err := validate(value); err != nil {
|
||||||
return dnd.NPCList{}, fmt.Errorf("decode dnd npc list: %w", err)
|
return dnd.NPCRegistry{}, fmt.Errorf("decode dnd npc registry: %w", err)
|
||||||
}
|
}
|
||||||
return value, nil
|
return value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecodeCandidate reads one strict durable JSON value before semantic
|
// DecodeCandidate reads one strict durable JSON value before semantic
|
||||||
// validators have approved it.
|
// validators have approved it.
|
||||||
func (c *Codec) DecodeCandidate(content []byte) (dnd.NPCList, error) {
|
func (c *Codec) DecodeCandidate(content []byte) (dnd.NPCRegistry, error) {
|
||||||
return candidatejson.DecodeCandidate[dnd.NPCList]("dnd npc list", content)
|
return candidatejson.DecodeCandidate[dnd.NPCRegistry]("dnd npc registry", content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func validate(value dnd.NPCList) error {
|
func validate(value dnd.NPCRegistry) error {
|
||||||
if value.NPCs == nil {
|
if value.NPCs == nil {
|
||||||
return fmt.Errorf("npcs must be present")
|
return fmt.Errorf("npcs must be present")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
||||||
)
|
)
|
||||||
|
|
||||||
func validList() dnd.NPCList {
|
func validList() dnd.NPCRegistry {
|
||||||
return dnd.NPCList{NPCs: []dnd.NPC{{
|
return dnd.NPCRegistry{NPCs: []dnd.NPC{{
|
||||||
ID: identity.DeriveID("Mira Thorn"),
|
ID: identity.DeriveID("Mira Thorn"),
|
||||||
Name: "Mira Thorn",
|
Name: "Mira Thorn",
|
||||||
SourceRefs: []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 1, EndUnitID: 2}},
|
SourceRefs: []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 1, EndUnitID: 2}},
|
||||||
@@ -24,7 +24,7 @@ func validList() dnd.NPCList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCodecMatchesMaintainedDurableFixture(t *testing.T) {
|
func TestCodecMatchesMaintainedDurableFixture(t *testing.T) {
|
||||||
raw, err := os.ReadFile("testdata/dnd_npcs.v1.json")
|
raw, err := os.ReadFile("testdata/dnd_npc_registry.v1.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("read durable fixture: %v", err)
|
t.Fatalf("read durable fixture: %v", err)
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ func TestCodecMatchesMaintainedDurableFixture(t *testing.T) {
|
|||||||
func TestCodecOwnsDurableSchemaAndRegistersExactType(t *testing.T) {
|
func TestCodecOwnsDurableSchemaAndRegistersExactType(t *testing.T) {
|
||||||
codec := New()
|
codec := New()
|
||||||
schema := codec.Schema()
|
schema := codec.Schema()
|
||||||
if codec.Kind() != dnd.NPCListKind || codec.MediaType() != MediaType {
|
if codec.Kind() != dnd.NPCRegistryKind || codec.MediaType() != MediaType {
|
||||||
t.Fatalf("codec identity = %q/%q", codec.Kind(), codec.MediaType())
|
t.Fatalf("codec identity = %q/%q", codec.Kind(), codec.MediaType())
|
||||||
}
|
}
|
||||||
if schema.ID != SchemaID || schema.Name != SchemaName || schema.Version != SchemaVersion || !json.Valid(schema.JSONSchema) {
|
if schema.ID != SchemaID || schema.Name != SchemaName || schema.Version != SchemaVersion || !json.Valid(schema.JSONSchema) {
|
||||||
@@ -67,7 +67,7 @@ func TestCodecOwnsDurableSchemaAndRegistersExactType(t *testing.T) {
|
|||||||
if err := pipeline.RegisterArtifactCodec(registry, codec); err != nil {
|
if err := pipeline.RegisterArtifactCodec(registry, codec); err != nil {
|
||||||
t.Fatalf("RegisterArtifactCodec() error = %v", err)
|
t.Fatalf("RegisterArtifactCodec() error = %v", err)
|
||||||
}
|
}
|
||||||
spec, ok := registry.Spec(dnd.NPCListKind)
|
spec, ok := registry.Spec(dnd.NPCRegistryKind)
|
||||||
if !ok || spec.SchemaDigest != contracts.DigestArtifactSchema(schema) {
|
if !ok || spec.SchemaDigest != contracts.DigestArtifactSchema(schema) {
|
||||||
t.Fatalf("registered spec = %#v, %t", spec, ok)
|
t.Fatalf("registered spec = %#v, %t", spec, ok)
|
||||||
}
|
}
|
||||||
@@ -98,7 +98,7 @@ func TestCodecStrictlyRejectsMalformedOrUnknownJSON(t *testing.T) {
|
|||||||
|
|
||||||
func TestCodecCandidatePreservesInvalidTypedValues(t *testing.T) {
|
func TestCodecCandidatePreservesInvalidTypedValues(t *testing.T) {
|
||||||
codec := New()
|
codec := New()
|
||||||
candidate := dnd.NPCList{NPCs: []dnd.NPC{{Name: "Mira Thorn", SourceRefs: []source.SourceRef{}}}}
|
candidate := dnd.NPCRegistry{NPCs: []dnd.NPC{{Name: "Mira Thorn", SourceRefs: []source.SourceRef{}}}}
|
||||||
content, err := codec.EncodeCandidate(candidate)
|
content, err := codec.EncodeCandidate(candidate)
|
||||||
if err != nil || !json.Valid(content) {
|
if err != nil || !json.Valid(content) {
|
||||||
t.Fatalf("EncodeCandidate() = %s, %v; want JSON", content, err)
|
t.Fatalf("EncodeCandidate() = %s, %v; want JSON", content, err)
|
||||||
@@ -116,11 +116,11 @@ func TestCodecRejectsEveryRequiredShapeBoundary(t *testing.T) {
|
|||||||
base := validList().NPCs[0]
|
base := validList().NPCs[0]
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
value dnd.NPCList
|
value dnd.NPCRegistry
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{name: "blank name", value: dnd.NPCList{NPCs: []dnd.NPC{{ID: base.ID, Name: " ", SourceRefs: base.SourceRefs}}}, want: "name must not be empty"},
|
{name: "blank name", value: dnd.NPCRegistry{NPCs: []dnd.NPC{{ID: base.ID, Name: " ", SourceRefs: base.SourceRefs}}}, want: "name must not be empty"},
|
||||||
{name: "empty source refs", value: dnd.NPCList{NPCs: []dnd.NPC{{ID: base.ID, Name: base.Name, SourceRefs: []source.SourceRef{}}}}, want: "source_refs must not be empty"},
|
{name: "empty source refs", value: dnd.NPCRegistry{NPCs: []dnd.NPC{{ID: base.ID, Name: base.Name, SourceRefs: []source.SourceRef{}}}}, want: "source_refs must not be empty"},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"npcs": [
|
"npcs": [
|
||||||
{
|
{
|
||||||
"id": "npc:sha256:99a16589618a04f535a7d21fdcc71a0b1c05d22f752cd492065b1086d97bc3d7",
|
"id": "npc:sha256:35ba5f679aee69e07ae3bd65c44278f29539d5dc9bb5225db1c0060555b23221",
|
||||||
"name": "Mira Thorn",
|
"name": "Mira Thorn",
|
||||||
"source_refs": [{"source_id": "session-alpha", "start_unit_id": 1, "end_unit_id": 2}]
|
"source_refs": [{"source_id": "session-alpha", "start_unit_id": 1, "end_unit_id": 2}]
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ func referenceSlots() []contracts.ReferenceSlot {
|
|||||||
Name: NPCRegistryReferenceSlot,
|
Name: NPCRegistryReferenceSlot,
|
||||||
Description: "Optional normalized NPC registry used for canonical actor grounding.",
|
Description: "Optional normalized NPC registry used for canonical actor grounding.",
|
||||||
AcceptedMediaTypes: []string{"application/json"},
|
AcceptedMediaTypes: []string{"application/json"},
|
||||||
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCListKind},
|
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCRegistryKind},
|
||||||
MaxBytes: NPCRegistryMaxBytes,
|
MaxBytes: NPCRegistryMaxBytes,
|
||||||
})
|
})
|
||||||
slots = append(slots, contracts.ReferenceSlot{
|
slots = append(slots, contracts.ReferenceSlot{
|
||||||
|
|||||||
@@ -519,7 +519,7 @@ func newExtractor(t *testing.T, client contracts.StructuredLLMClient, references
|
|||||||
|
|
||||||
func npcRegistryJSON(t *testing.T) []byte {
|
func npcRegistryJSON(t *testing.T) []byte {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
value := dnd.NPCList{NPCs: []dnd.NPC{{ID: identity.DeriveID("Mira Thorn"), Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: "other-session", StartUnitID: 1, EndUnitID: 1}}}}}
|
value := dnd.NPCRegistry{NPCs: []dnd.NPC{{ID: identity.DeriveID("Mira Thorn"), Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: "other-session", StartUnitID: 1, EndUnitID: 1}}}}}
|
||||||
content, err := npccodec.New().Encode(value)
|
content, err := npccodec.New().Encode(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("encode NPC registry: %v", err)
|
t.Fatalf("encode NPC registry: %v", err)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func referenceSlots() []contracts.ReferenceSlot {
|
|||||||
Description: "Required normalized NPC registry used only for enemy-subject grounding, never as event evidence.",
|
Description: "Required normalized NPC registry used only for enemy-subject grounding, never as event evidence.",
|
||||||
Required: true,
|
Required: true,
|
||||||
AcceptedMediaTypes: []string{npccodec.MediaType},
|
AcceptedMediaTypes: []string{npccodec.MediaType},
|
||||||
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCListKind},
|
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCRegistryKind},
|
||||||
MaxBytes: ReferenceMaxBytes,
|
MaxBytes: ReferenceMaxBytes,
|
||||||
},
|
},
|
||||||
contracts.ReferenceSlot{
|
contracts.ReferenceSlot{
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func TestReferenceSlotsDescribeRequiredTypedArtifacts(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
kind contracts.ArtifactKind
|
kind contracts.ArtifactKind
|
||||||
}{
|
}{
|
||||||
{NPCRegistryReferenceSlot, dnd.NPCListKind},
|
{NPCRegistryReferenceSlot, dnd.NPCRegistryKind},
|
||||||
{SceneDescriptionReferenceSlot, dnd.SceneDescriptionListKind},
|
{SceneDescriptionReferenceSlot, dnd.SceneDescriptionListKind},
|
||||||
{CombatTurnReferenceSlot, dnd.CombatTurnListKind},
|
{CombatTurnReferenceSlot, dnd.CombatTurnListKind},
|
||||||
{NPCInteractionReferenceSlot, dnd.NPCInteractionListKind},
|
{NPCInteractionReferenceSlot, dnd.NPCInteractionListKind},
|
||||||
@@ -195,7 +195,7 @@ func TestGroundingRejectsMissingAndInvalidReferences(t *testing.T) {
|
|||||||
|
|
||||||
func groundingReferences(t *testing.T, enemy string, sceneKind dnd.SceneKind) contracts.ReferenceSet {
|
func groundingReferences(t *testing.T, enemy string, sceneKind dnd.SceneKind) contracts.ReferenceSet {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
npcContent, err := npccodec.New().Encode(dnd.NPCList{NPCs: []dnd.NPC{{
|
npcContent, err := npccodec.New().Encode(dnd.NPCRegistry{NPCs: []dnd.NPC{{
|
||||||
ID: identity.DeriveID(enemy),
|
ID: identity.DeriveID(enemy),
|
||||||
Name: enemy,
|
Name: enemy,
|
||||||
SourceRefs: []source.SourceRef{{SourceID: "combat-session", StartUnitID: 1, EndUnitID: 1}},
|
SourceRefs: []source.SourceRef{{SourceID: "combat-session", StartUnitID: 1, EndUnitID: 1}},
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func referenceSlots() []contracts.ReferenceSlot {
|
|||||||
Description: "Required normalized NPC registry used only for interaction identity grounding, never as interaction evidence.",
|
Description: "Required normalized NPC registry used only for interaction identity grounding, never as interaction evidence.",
|
||||||
Required: true,
|
Required: true,
|
||||||
AcceptedMediaTypes: []string{"application/json"},
|
AcceptedMediaTypes: []string{"application/json"},
|
||||||
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCListKind},
|
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCRegistryKind},
|
||||||
MaxBytes: NPCRegistryMaxBytes,
|
MaxBytes: NPCRegistryMaxBytes,
|
||||||
})
|
})
|
||||||
sort.Slice(slots, func(i, j int) bool { return slots[i].Name < slots[j].Name })
|
sort.Slice(slots, func(i, j int) bool { return slots[i].Name < slots[j].Name })
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ func TestExtractResolvesGeneratedRegistryAtOperationTime(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestExtractAcceptsEmptyBoundRegistryAndEmptyResponse(t *testing.T) {
|
func TestExtractAcceptsEmptyBoundRegistryAndEmptyResponse(t *testing.T) {
|
||||||
content, err := npccodec.New().Encode(dnd.NPCList{NPCs: []dnd.NPC{}})
|
content, err := npccodec.New().Encode(dnd.NPCRegistry{NPCs: []dnd.NPC{}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -239,7 +239,7 @@ func TestModuleSpecRegistrationMetadataAndFingerprints(t *testing.T) {
|
|||||||
registrySlot = slot
|
registrySlot = slot
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !registrySlot.Required || !reflect.DeepEqual(registrySlot.AcceptedArtifactKinds, []contracts.ArtifactKind{dnd.NPCListKind}) || registrySlot.MaxBytes != NPCRegistryMaxBytes {
|
if !registrySlot.Required || !reflect.DeepEqual(registrySlot.AcceptedArtifactKinds, []contracts.ArtifactKind{dnd.NPCRegistryKind}) || registrySlot.MaxBytes != NPCRegistryMaxBytes {
|
||||||
t.Fatalf("NPC registry slot = %#v", registrySlot)
|
t.Fatalf("NPC registry slot = %#v", registrySlot)
|
||||||
}
|
}
|
||||||
got.ReferenceSlots[0].AcceptedMediaTypes[0] = "changed"
|
got.ReferenceSlots[0].AcceptedMediaTypes[0] = "changed"
|
||||||
@@ -326,7 +326,7 @@ func requiredRegistryReferences(t *testing.T, names ...string) contracts.Referen
|
|||||||
SourceRefs: []source.SourceRef{{SourceID: "other-session", StartUnitID: index + 1, EndUnitID: index + 1}},
|
SourceRefs: []source.SourceRef{{SourceID: "other-session", StartUnitID: index + 1, EndUnitID: index + 1}},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
content, err := npccodec.New().Encode(dnd.NPCList{NPCs: npcs})
|
content, err := npccodec.New().Encode(dnd.NPCRegistry{NPCs: npcs})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,9 +51,9 @@ func canonicalizeNPC(npc *npcResponse, order shared.SourceRefOrder, sourceID str
|
|||||||
return order.EarliestValid(refs)
|
return order.EarliestValid(refs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func canonicalNPCList(response extractionResponse, sourceID string) dnd.NPCList {
|
func canonicalNPCRegistry(response extractionResponse, sourceID string) dnd.NPCRegistry {
|
||||||
if response.NPCs == nil {
|
if response.NPCs == nil {
|
||||||
return dnd.NPCList{NPCs: nil}
|
return dnd.NPCRegistry{NPCs: nil}
|
||||||
}
|
}
|
||||||
npcs := make([]dnd.NPC, len(response.NPCs))
|
npcs := make([]dnd.NPC, len(response.NPCs))
|
||||||
for index, npc := range response.NPCs {
|
for index, npc := range response.NPCs {
|
||||||
@@ -63,7 +63,7 @@ func canonicalNPCList(response extractionResponse, sourceID string) dnd.NPCList
|
|||||||
SourceRefs: canonicalSourceRefs(npc.SourceRefs, sourceID),
|
SourceRefs: canonicalSourceRefs(npc.SourceRefs, sourceID),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return dnd.NPCList{NPCs: npcs}
|
return dnd.NPCRegistry{NPCs: npcs}
|
||||||
}
|
}
|
||||||
|
|
||||||
func canonicalSourceRefs(values []npcSourceRefResponse, sourceID string) []source.SourceRef {
|
func canonicalSourceRefs(values []npcSourceRefResponse, sourceID string) []source.SourceRef {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func referenceSlots() []contracts.ReferenceSlot {
|
|||||||
return shared.ReferenceSlots(referenceSlotDescriptions)
|
return shared.ReferenceSlots(referenceSlotDescriptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ contracts.Extractor[dnd.NPCList] = (*Extractor)(nil)
|
var _ contracts.Extractor[dnd.NPCRegistry] = (*Extractor)(nil)
|
||||||
var _ contracts.ManifestMetadataProvider = (*Extractor)(nil)
|
var _ contracts.ManifestMetadataProvider = (*Extractor)(nil)
|
||||||
var _ pipeline.CheckpointFingerprintProvider = (*Extractor)(nil)
|
var _ pipeline.CheckpointFingerprintProvider = (*Extractor)(nil)
|
||||||
|
|
||||||
@@ -103,16 +103,16 @@ func (e *Extractor) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRequest) (contracts.TypedExtractionResult[dnd.NPCList], error) {
|
func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRequest) (contracts.TypedExtractionResult[dnd.NPCRegistry], error) {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return contracts.TypedExtractionResult[dnd.NPCList]{}, extractorErrorf("extractor must not be nil")
|
return contracts.TypedExtractionResult[dnd.NPCRegistry]{}, extractorErrorf("extractor must not be nil")
|
||||||
}
|
}
|
||||||
if e.llm == nil {
|
if e.llm == nil {
|
||||||
return contracts.TypedExtractionResult[dnd.NPCList]{}, extractorErrorf("LLM client must not be nil")
|
return contracts.TypedExtractionResult[dnd.NPCRegistry]{}, extractorErrorf("LLM client must not be nil")
|
||||||
}
|
}
|
||||||
sourceInput, err := shared.PrepareChunkExtraction(ctx, req)
|
sourceInput, err := shared.PrepareChunkExtraction(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return contracts.TypedExtractionResult[dnd.NPCList]{}, extractorErrorf("%w", err)
|
return contracts.TypedExtractionResult[dnd.NPCRegistry]{}, extractorErrorf("%w", err)
|
||||||
}
|
}
|
||||||
order := shared.NewSourceRefOrder(req.Source)
|
order := shared.NewSourceRefOrder(req.Source)
|
||||||
|
|
||||||
@@ -125,10 +125,10 @@ func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRe
|
|||||||
SessionID: req.SessionID,
|
SessionID: req.SessionID,
|
||||||
Inputs: shared.PromptInputs(sourceInput, req.References),
|
Inputs: shared.PromptInputs(sourceInput, req.References),
|
||||||
}, &response); err != nil {
|
}, &response); err != nil {
|
||||||
return contracts.TypedExtractionResult[dnd.NPCList]{}, extractorErrorf("complete structured output: %w", err)
|
return contracts.TypedExtractionResult[dnd.NPCRegistry]{}, extractorErrorf("complete structured output: %w", err)
|
||||||
}
|
}
|
||||||
canonicalizeResponse(&response, order, req.Source.ID)
|
canonicalizeResponse(&response, order, req.Source.ID)
|
||||||
return contracts.TypedExtractionResult[dnd.NPCList]{Value: canonicalNPCList(response, req.Source.ID)}, nil
|
return contracts.TypedExtractionResult[dnd.NPCRegistry]{Value: canonicalNPCRegistry(response, req.Source.ID)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ModuleSpec() pipeline.ModuleSpec {
|
func ModuleSpec() pipeline.ModuleSpec {
|
||||||
@@ -138,13 +138,13 @@ func ModuleSpec() pipeline.ModuleSpec {
|
|||||||
ExecutionClass: contracts.ExecutionClassLLMBacked,
|
ExecutionClass: contracts.ExecutionClassLLMBacked,
|
||||||
Requires: append([]string(nil), requiredCapabilities...),
|
Requires: append([]string(nil), requiredCapabilities...),
|
||||||
Provides: append([]string(nil), providedCapabilities...),
|
Provides: append([]string(nil), providedCapabilities...),
|
||||||
ArtifactKind: dnd.NPCListKind,
|
ArtifactKind: dnd.NPCRegistryKind,
|
||||||
ReferenceSlots: referenceSlots(),
|
ReferenceSlots: referenceSlots(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Register(registry *pipeline.ExtractorRegistry) error {
|
func Register(registry *pipeline.ExtractorRegistry) error {
|
||||||
return pipeline.RegisterExtractorBuilder(registry, ModuleSpec(), validateOptions, func(request pipeline.BuildRequest) (contracts.Extractor[dnd.NPCList], error) {
|
return pipeline.RegisterExtractorBuilder(registry, ModuleSpec(), validateOptions, func(request pipeline.BuildRequest) (contracts.Extractor[dnd.NPCRegistry], error) {
|
||||||
options, err := DecodeOptions(request.Options)
|
options, err := DecodeOptions(request.Options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestExtractReturnsCanonicalNPCListFromPrivateResponse(t *testing.T) {
|
func TestExtractReturnsCanonicalNPCRegistryFromPrivateResponse(t *testing.T) {
|
||||||
client := &fakeNPCsLLMClient{response: extractionResponse{NPCs: []npcResponse{
|
client := &fakeNPCsLLMClient{response: extractionResponse{NPCs: []npcResponse{
|
||||||
{
|
{
|
||||||
Name: "Captain Vale", SourceRefs: responseSourceRefs(3, 3),
|
Name: "Captain Vale", SourceRefs: responseSourceRefs(3, 3),
|
||||||
@@ -32,7 +32,7 @@ func TestExtractReturnsCanonicalNPCListFromPrivateResponse(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Extract() error = %v, want nil", err)
|
t.Fatalf("Extract() error = %v, want nil", err)
|
||||||
}
|
}
|
||||||
want := dnd.NPCList{NPCs: []dnd.NPC{
|
want := dnd.NPCRegistry{NPCs: []dnd.NPC{
|
||||||
{ID: identity.DeriveID("Mira Thorn"), Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 1, EndUnitID: 2}, {SourceID: "session-alpha", StartUnitID: 2, EndUnitID: 2}}},
|
{ID: identity.DeriveID("Mira Thorn"), Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 1, EndUnitID: 2}, {SourceID: "session-alpha", StartUnitID: 2, EndUnitID: 2}}},
|
||||||
{ID: identity.DeriveID("Captain Vale"), Name: "Captain Vale", SourceRefs: []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 3, EndUnitID: 3}}},
|
{ID: identity.DeriveID("Captain Vale"), Name: "Captain Vale", SourceRefs: []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 3, EndUnitID: 3}}},
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func TestModuleSpecAndReferenceSlots(t *testing.T) {
|
|||||||
ExecutionClass: contracts.ExecutionClassLLMBacked,
|
ExecutionClass: contracts.ExecutionClassLLMBacked,
|
||||||
Requires: []string{"chunks", "source.transcript"},
|
Requires: []string{"chunks", "source.transcript"},
|
||||||
Provides: []string{"dnd.npcs"},
|
Provides: []string{"dnd.npcs"},
|
||||||
ArtifactKind: dnd.NPCListKind,
|
ArtifactKind: dnd.NPCRegistryKind,
|
||||||
ReferenceSlots: []contracts.ReferenceSlot{
|
ReferenceSlots: []contracts.ReferenceSlot{
|
||||||
{Name: "glossary", Description: "Optional campaign glossary reference material used only for NPC disambiguation.", AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}},
|
{Name: "glossary", Description: "Optional campaign glossary reference material used only for NPC disambiguation.", AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}},
|
||||||
{Name: "party", Description: "Optional party roster reference material used only for NPC disambiguation.", AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}},
|
{Name: "party", Description: "Optional party roster reference material used only for NPC disambiguation.", AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}},
|
||||||
@@ -77,7 +77,7 @@ func TestExtractorMetadataAndCheckpointIdentity(t *testing.T) {
|
|||||||
"prompt_id": PromptID, "prompt_version": SchemaVersion,
|
"prompt_id": PromptID, "prompt_version": SchemaVersion,
|
||||||
"response_schema_key": string(ResponseSchemaKey), "response_schema_id": ResponseSchemaID,
|
"response_schema_key": string(ResponseSchemaKey), "response_schema_id": ResponseSchemaID,
|
||||||
"response_schema_name": ResponseSchemaName, "response_schema_version": SchemaVersion,
|
"response_schema_name": ResponseSchemaName, "response_schema_version": SchemaVersion,
|
||||||
"identity_policy": "dnd.npcs.identity.v1",
|
"identity_policy": "dnd.npc_registry.identity.v1",
|
||||||
"mapping_policy": mappingPolicy,
|
"mapping_policy": mappingPolicy,
|
||||||
} {
|
} {
|
||||||
if metadata[key] != want {
|
if metadata[key] != want {
|
||||||
@@ -90,7 +90,7 @@ func TestExtractorMetadataAndCheckpointIdentity(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fingerprints := extractor.CheckpointFingerprints()
|
fingerprints := extractor.CheckpointFingerprints()
|
||||||
want := map[string]string{"prompt": metadata["prompt_sha256"].(string), "response_schema": metadata["response_schema_sha256"].(string), "identity_policy": "dnd.npcs.identity.v1", "mapping_policy": mappingPolicy}
|
want := map[string]string{"prompt": metadata["prompt_sha256"].(string), "response_schema": metadata["response_schema_sha256"].(string), "identity_policy": "dnd.npc_registry.identity.v1", "mapping_policy": mappingPolicy}
|
||||||
if len(fingerprints) != len(want) {
|
if len(fingerprints) != len(want) {
|
||||||
t.Fatalf("CheckpointFingerprints() = %#v, want %d entries", fingerprints, len(want))
|
t.Fatalf("CheckpointFingerprints() = %#v, want %d entries", fingerprints, len(want))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func referenceSlots() []contracts.ReferenceSlot {
|
|||||||
Name: NPCRegistryReferenceSlot,
|
Name: NPCRegistryReferenceSlot,
|
||||||
Description: "Optional normalized NPC registry used for canonical caster-name grounding.",
|
Description: "Optional normalized NPC registry used for canonical caster-name grounding.",
|
||||||
AcceptedMediaTypes: []string{"application/json"},
|
AcceptedMediaTypes: []string{"application/json"},
|
||||||
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCListKind},
|
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCRegistryKind},
|
||||||
MaxBytes: NPCRegistryMaxBytes,
|
MaxBytes: NPCRegistryMaxBytes,
|
||||||
})
|
})
|
||||||
sort.Slice(slots, func(i, j int) bool { return slots[i].Name < slots[j].Name })
|
sort.Slice(slots, func(i, j int) bool { return slots[i].Name < slots[j].Name })
|
||||||
|
|||||||
@@ -107,8 +107,8 @@ func TestSpellExtractorResolvesOperationNPCOverrideWithoutSingletonMetadata(t *t
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func registryFixture() dnd.NPCList {
|
func registryFixture() dnd.NPCRegistry {
|
||||||
return dnd.NPCList{NPCs: []dnd.NPC{{
|
return dnd.NPCRegistry{NPCs: []dnd.NPC{{
|
||||||
ID: identity.DeriveID("Mira Thorn"),
|
ID: identity.DeriveID("Mira Thorn"),
|
||||||
Name: "Mira Thorn",
|
Name: "Mira Thorn",
|
||||||
SourceRefs: []source.SourceRef{{SourceID: "npc-session", StartUnitID: 41, EndUnitID: 43}},
|
SourceRefs: []source.SourceRef{{SourceID: "npc-session", StartUnitID: 41, EndUnitID: 43}},
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func TestModuleSpec(t *testing.T) {
|
|||||||
Name: NPCRegistryReferenceSlot,
|
Name: NPCRegistryReferenceSlot,
|
||||||
Description: "Optional normalized NPC registry used for canonical caster-name grounding.",
|
Description: "Optional normalized NPC registry used for canonical caster-name grounding.",
|
||||||
AcceptedMediaTypes: []string{"application/json"},
|
AcceptedMediaTypes: []string{"application/json"},
|
||||||
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCListKind},
|
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCRegistryKind},
|
||||||
MaxBytes: NPCRegistryMaxBytes,
|
MaxBytes: NPCRegistryMaxBytes,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ func referenceSlots() []contracts.ReferenceSlot {
|
|||||||
Name: NPCRegistryReferenceSlot,
|
Name: NPCRegistryReferenceSlot,
|
||||||
Description: "Optional normalized NPC registry used for canonical actor grounding.",
|
Description: "Optional normalized NPC registry used for canonical actor grounding.",
|
||||||
AcceptedMediaTypes: []string{"application/json"},
|
AcceptedMediaTypes: []string{"application/json"},
|
||||||
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCListKind},
|
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCRegistryKind},
|
||||||
MaxBytes: NPCRegistryMaxBytes,
|
MaxBytes: NPCRegistryMaxBytes,
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ func testDocument() *source.SourceDocument {
|
|||||||
|
|
||||||
func npcReferences(t *testing.T) contracts.ReferenceSet {
|
func npcReferences(t *testing.T) contracts.ReferenceSet {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
npcs := dnd.NPCList{NPCs: []dnd.NPC{
|
npcs := dnd.NPCRegistry{NPCs: []dnd.NPC{
|
||||||
{ID: identity.DeriveID("Aria"), Name: "Aria", SourceRefs: []source.SourceRef{{SourceID: "npc-run", StartUnitID: 1, EndUnitID: 1}}},
|
{ID: identity.DeriveID("Aria"), Name: "Aria", SourceRefs: []source.SourceRef{{SourceID: "npc-run", StartUnitID: 1, EndUnitID: 1}}},
|
||||||
{ID: identity.DeriveID("Goblin"), Name: "Goblin", SourceRefs: []source.SourceRef{{SourceID: "npc-run", StartUnitID: 1, EndUnitID: 1}}},
|
{ID: identity.DeriveID("Goblin"), Name: "Goblin", SourceRefs: []source.SourceRef{{SourceID: "npc-run", StartUnitID: 1, EndUnitID: 1}}},
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ func referenceSlots() []contracts.ReferenceSlot {
|
|||||||
Description: "Required normalized NPC registry used only to canonicalize enemy-subject names, never as event evidence.",
|
Description: "Required normalized NPC registry used only to canonicalize enemy-subject names, never as event evidence.",
|
||||||
Required: true,
|
Required: true,
|
||||||
AcceptedMediaTypes: []string{"application/json"},
|
AcceptedMediaTypes: []string{"application/json"},
|
||||||
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCListKind},
|
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCRegistryKind},
|
||||||
MaxBytes: NPCRegistryMaxBytes,
|
MaxBytes: NPCRegistryMaxBytes,
|
||||||
}})
|
}})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ func testDocument() *source.SourceDocument {
|
|||||||
|
|
||||||
func npcReferences(t *testing.T) contracts.ReferenceSet {
|
func npcReferences(t *testing.T) contracts.ReferenceSet {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
value := dnd.NPCList{NPCs: []dnd.NPC{{
|
value := dnd.NPCRegistry{NPCs: []dnd.NPC{{
|
||||||
ID: identity.DeriveID("Ária"), Name: "Ária",
|
ID: identity.DeriveID("Ária"), Name: "Ária",
|
||||||
SourceRefs: []source.SourceRef{{SourceID: "registry", StartUnitID: 1, EndUnitID: 1}},
|
SourceRefs: []source.SourceRef{{SourceID: "registry", StartUnitID: 1, EndUnitID: 1}},
|
||||||
}}}
|
}}}
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ func referenceSlots() []contracts.ReferenceSlot {
|
|||||||
Description: "Required normalized NPC registry used only for interaction identity grounding, never as interaction evidence.",
|
Description: "Required normalized NPC registry used only for interaction identity grounding, never as interaction evidence.",
|
||||||
Required: true,
|
Required: true,
|
||||||
AcceptedMediaTypes: []string{"application/json"},
|
AcceptedMediaTypes: []string{"application/json"},
|
||||||
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCListKind},
|
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCRegistryKind},
|
||||||
MaxBytes: NPCRegistryMaxBytes,
|
MaxBytes: NPCRegistryMaxBytes,
|
||||||
})
|
})
|
||||||
sort.Slice(slots, func(left, right int) bool { return slots[left].Name < slots[right].Name })
|
sort.Slice(slots, func(left, right int) bool { return slots[left].Name < slots[right].Name })
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ func testDocument() *source.SourceDocument {
|
|||||||
|
|
||||||
func npcReferences(t *testing.T) contracts.ReferenceSet {
|
func npcReferences(t *testing.T) contracts.ReferenceSet {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
value := dnd.NPCList{NPCs: []dnd.NPC{
|
value := dnd.NPCRegistry{NPCs: []dnd.NPC{
|
||||||
{ID: identity.DeriveID("Ária"), Name: "Ária", SourceRefs: []source.SourceRef{{SourceID: "registry", StartUnitID: 1, EndUnitID: 1}}},
|
{ID: identity.DeriveID("Ária"), Name: "Ária", SourceRefs: []source.SourceRef{{SourceID: "registry", StartUnitID: 1, EndUnitID: 1}}},
|
||||||
{ID: identity.DeriveID("Borin"), Name: "Borin", SourceRefs: []source.SourceRef{{SourceID: "registry", StartUnitID: 1, EndUnitID: 1}}},
|
{ID: identity.DeriveID("Borin"), Name: "Borin", SourceRefs: []source.SourceRef{{SourceID: "registry", StartUnitID: 1, EndUnitID: 1}}},
|
||||||
}}
|
}}
|
||||||
@@ -184,7 +184,7 @@ func npcReferences(t *testing.T) contracts.ReferenceSet {
|
|||||||
t.Fatalf("encode registry: %v", err)
|
t.Fatalf("encode registry: %v", err)
|
||||||
}
|
}
|
||||||
return contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{NPCRegistryReferenceSlot: {
|
return contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{NPCRegistryReferenceSlot: {
|
||||||
Slot: contracts.ReferenceSlot{Name: NPCRegistryReferenceSlot, Required: true, AcceptedMediaTypes: []string{npccodec.MediaType}, AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCListKind}, MaxBytes: NPCRegistryMaxBytes},
|
Slot: contracts.ReferenceSlot{Name: NPCRegistryReferenceSlot, Required: true, AcceptedMediaTypes: []string{npccodec.MediaType}, AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.NPCRegistryKind}, MaxBytes: NPCRegistryMaxBytes},
|
||||||
Items: []contracts.ReferenceItem{{SlotName: NPCRegistryReferenceSlot, MediaType: npccodec.MediaType, Content: content}},
|
Items: []contracts.ReferenceItem{{SlotName: NPCRegistryReferenceSlot, MediaType: npccodec.MediaType, Content: content}},
|
||||||
}}}
|
}}}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const (
|
|||||||
var requiredCapabilities = []string{"merged"}
|
var requiredCapabilities = []string{"merged"}
|
||||||
var providedCapabilities = []string{"normalized"}
|
var providedCapabilities = []string{"normalized"}
|
||||||
|
|
||||||
var _ contracts.Normalizer[dnd.NPCList] = (*Normalizer)(nil)
|
var _ contracts.Normalizer[dnd.NPCRegistry] = (*Normalizer)(nil)
|
||||||
var _ contracts.ManifestMetadataProvider = (*Normalizer)(nil)
|
var _ contracts.ManifestMetadataProvider = (*Normalizer)(nil)
|
||||||
var _ pipeline.CheckpointFingerprintProvider = (*Normalizer)(nil)
|
var _ pipeline.CheckpointFingerprintProvider = (*Normalizer)(nil)
|
||||||
|
|
||||||
@@ -102,18 +102,18 @@ func (n *Normalizer) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalizeRequest[dnd.NPCList]) (contracts.TypedNormalizeResult[dnd.NPCList], error) {
|
func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalizeRequest[dnd.NPCRegistry]) (contracts.TypedNormalizeResult[dnd.NPCRegistry], error) {
|
||||||
if n == nil {
|
if n == nil {
|
||||||
return contracts.TypedNormalizeResult[dnd.NPCList]{}, normalizerErrorf("normalizer must not be nil")
|
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, normalizerErrorf("normalizer must not be nil")
|
||||||
}
|
}
|
||||||
if n.llm == nil {
|
if n.llm == nil {
|
||||||
return contracts.TypedNormalizeResult[dnd.NPCList]{}, normalizerErrorf("LLM client must not be nil")
|
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, normalizerErrorf("LLM client must not be nil")
|
||||||
}
|
}
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
return contracts.TypedNormalizeResult[dnd.NPCList]{}, normalizerErrorf("context must not be nil")
|
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, normalizerErrorf("context must not be nil")
|
||||||
}
|
}
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return contracts.TypedNormalizeResult[dnd.NPCList]{}, normalizerErrorf("context error before normalize: %w", err)
|
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, normalizerErrorf("context error before normalize: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
order := shared.NewSourceRefOrder(req.Source)
|
order := shared.NewSourceRefOrder(req.Source)
|
||||||
@@ -121,10 +121,10 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
|
|||||||
deterministic := recordList(records)
|
deterministic := recordList(records)
|
||||||
materials, ready, err := entityreconcile.BuildContext(req.Source, reconciliationCandidates(records), semanticContextRadius)
|
materials, ready, err := entityreconcile.BuildContext(req.Source, reconciliationCandidates(records), semanticContextRadius)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return contracts.TypedNormalizeResult[dnd.NPCList]{}, normalizerErrorf("build semantic context: %w", err)
|
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, normalizerErrorf("build semantic context: %w", err)
|
||||||
}
|
}
|
||||||
if !ready {
|
if !ready {
|
||||||
return contracts.TypedNormalizeResult[dnd.NPCList]{Value: deterministic, Warnings: limitWarnings(warnings)}, nil
|
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{Value: deterministic, Warnings: limitWarnings(warnings)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var response entityreconcile.ProposalResponse
|
var response entityreconcile.ProposalResponse
|
||||||
@@ -136,20 +136,20 @@ func (n *Normalizer) Normalize(ctx context.Context, req contracts.TypedNormalize
|
|||||||
if errors.Is(err, contracts.ErrInvalidStructuredOutput) {
|
if errors.Is(err, contracts.ErrInvalidStructuredOutput) {
|
||||||
return n.invalidStructuredResult(deterministic, warnings), nil
|
return n.invalidStructuredResult(deterministic, warnings), nil
|
||||||
}
|
}
|
||||||
return contracts.TypedNormalizeResult[dnd.NPCList]{}, normalizerErrorf("complete structured output: %w", err)
|
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{}, normalizerErrorf("complete structured output: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assessment := materials.Assess(response)
|
assessment := materials.Assess(response)
|
||||||
applied, semanticWarnings := applySafeGroups(records, reconciliationGroups(assessment, materials.CandidateKeys()), order)
|
applied, semanticWarnings := applySafeGroups(records, reconciliationGroups(assessment, materials.CandidateKeys()), order)
|
||||||
warnings = append(warnings, semanticWarnings...)
|
warnings = append(warnings, semanticWarnings...)
|
||||||
if assessment.DiscardedGroups() == 0 {
|
if assessment.DiscardedGroups() == 0 {
|
||||||
return contracts.TypedNormalizeResult[dnd.NPCList]{Value: recordList(applied), Warnings: limitWarnings(warnings)}, nil
|
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{Value: recordList(applied), Warnings: limitWarnings(warnings)}, nil
|
||||||
}
|
}
|
||||||
return retryResult(recordList(applied), warnings, assessment), nil
|
return retryResult(recordList(applied), warnings, assessment), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Normalizer) invalidStructuredResult(value dnd.NPCList, warnings []contracts.Warning) contracts.TypedNormalizeResult[dnd.NPCList] {
|
func (n *Normalizer) invalidStructuredResult(value dnd.NPCRegistry, warnings []contracts.Warning) contracts.TypedNormalizeResult[dnd.NPCRegistry] {
|
||||||
return contracts.TypedNormalizeResult[dnd.NPCList]{
|
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{
|
||||||
Value: value,
|
Value: value,
|
||||||
Warnings: limitWarningsForRetry(warnings),
|
Warnings: limitWarningsForRetry(warnings),
|
||||||
Retry: &contracts.NormalizeRetry{
|
Retry: &contracts.NormalizeRetry{
|
||||||
@@ -160,8 +160,8 @@ func (n *Normalizer) invalidStructuredResult(value dnd.NPCList, warnings []contr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func retryResult(value dnd.NPCList, warnings []contracts.Warning, assessment entityreconcile.Assessment) contracts.TypedNormalizeResult[dnd.NPCList] {
|
func retryResult(value dnd.NPCRegistry, warnings []contracts.Warning, assessment entityreconcile.Assessment) contracts.TypedNormalizeResult[dnd.NPCRegistry] {
|
||||||
return contracts.TypedNormalizeResult[dnd.NPCList]{
|
return contracts.TypedNormalizeResult[dnd.NPCRegistry]{
|
||||||
Value: value,
|
Value: value,
|
||||||
Warnings: limitWarningsForRetry(warnings),
|
Warnings: limitWarningsForRetry(warnings),
|
||||||
Retry: &contracts.NormalizeRetry{
|
Retry: &contracts.NormalizeRetry{
|
||||||
@@ -205,7 +205,7 @@ type normalizedRecord struct {
|
|||||||
earliest int
|
earliest int
|
||||||
}
|
}
|
||||||
|
|
||||||
func preprocessRecords(input dnd.NPCList, order shared.SourceRefOrder) ([]normalizedRecord, []contracts.Warning) {
|
func preprocessRecords(input dnd.NPCRegistry, order shared.SourceRefOrder) ([]normalizedRecord, []contracts.Warning) {
|
||||||
if input.NPCs == nil {
|
if input.NPCs == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -328,11 +328,11 @@ func recordValues(records []normalizedRecord) []dnd.NPC {
|
|||||||
return values
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
func recordList(records []normalizedRecord) dnd.NPCList {
|
func recordList(records []normalizedRecord) dnd.NPCRegistry {
|
||||||
if records == nil {
|
if records == nil {
|
||||||
return dnd.NPCList{}
|
return dnd.NPCRegistry{}
|
||||||
}
|
}
|
||||||
return dnd.NPCList{NPCs: recordValues(records)}
|
return dnd.NPCRegistry{NPCs: recordValues(records)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneSourceRefs(input []source.SourceRef) []source.SourceRef {
|
func cloneSourceRefs(input []source.SourceRef) []source.SourceRef {
|
||||||
@@ -362,11 +362,11 @@ func duplicateWarning(retainedIndex int, removed []int) contracts.Warning {
|
|||||||
func npcScope(index int) string { return fmt.Sprintf("npcs[%d]", index) }
|
func npcScope(index int) string { return fmt.Sprintf("npcs[%d]", index) }
|
||||||
|
|
||||||
func ModuleSpec() pipeline.ModuleSpec {
|
func ModuleSpec() pipeline.ModuleSpec {
|
||||||
return pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), ArtifactKind: dnd.NPCListKind}
|
return pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), ArtifactKind: dnd.NPCRegistryKind}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Register(registry *pipeline.NormalizerRegistry) error {
|
func Register(registry *pipeline.NormalizerRegistry) error {
|
||||||
return pipeline.RegisterNormalizerBuilder(registry, ModuleSpec(), validateOptions, func(request pipeline.BuildRequest) (contracts.Normalizer[dnd.NPCList], error) {
|
return pipeline.RegisterNormalizerBuilder(registry, ModuleSpec(), validateOptions, func(request pipeline.BuildRequest) (contracts.Normalizer[dnd.NPCRegistry], error) {
|
||||||
options, err := DecodeOptions(request.Options)
|
options, err := DecodeOptions(request.Options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func TestModuleContractAndIdentity(t *testing.T) {
|
|||||||
if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil {
|
if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil {
|
||||||
t.Fatal("DecodeOptions() accepted unknown option")
|
t.Fatal("DecodeOptions() accepted unknown option")
|
||||||
}
|
}
|
||||||
want := pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: []string{"merged"}, Provides: []string{"normalized"}, ArtifactKind: dnd.NPCListKind}
|
want := pipeline.ModuleSpec{Key: Key, Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: []string{"merged"}, Provides: []string{"normalized"}, ArtifactKind: dnd.NPCRegistryKind}
|
||||||
if got := ModuleSpec(); !reflect.DeepEqual(got, want) {
|
if got := ModuleSpec(); !reflect.DeepEqual(got, want) {
|
||||||
t.Fatalf("ModuleSpec() = %#v, want %#v", got, want)
|
t.Fatalf("ModuleSpec() = %#v, want %#v", got, want)
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ func TestModuleContractAndIdentity(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNormalizeNamesEvidenceAndIDs(t *testing.T) {
|
func TestNormalizeNamesEvidenceAndIDs(t *testing.T) {
|
||||||
input := dnd.NPCList{NPCs: []dnd.NPC{{
|
input := dnd.NPCRegistry{NPCs: []dnd.NPC{{
|
||||||
ID: "wrong", Name: " Lady\tAsh ", SourceRefs: []source.SourceRef{
|
ID: "wrong", Name: " Lady\tAsh ", SourceRefs: []source.SourceRef{
|
||||||
{SourceID: "b", StartUnitID: 2, EndUnitID: 3},
|
{SourceID: "b", StartUnitID: 2, EndUnitID: 3},
|
||||||
{SourceID: "a", StartUnitID: 4, EndUnitID: 4},
|
{SourceID: "a", StartUnitID: 4, EndUnitID: 4},
|
||||||
@@ -68,7 +68,7 @@ func TestNormalizeNamesEvidenceAndIDs(t *testing.T) {
|
|||||||
|
|
||||||
func TestNormalizeOrdersEvidenceBySourceDocumentPosition(t *testing.T) {
|
func TestNormalizeOrdersEvidenceBySourceDocumentPosition(t *testing.T) {
|
||||||
doc := &source.SourceDocument{ID: "source", Units: []source.SourceUnit{{ID: 30}, {ID: 10}}}
|
doc := &source.SourceDocument{ID: "source", Units: []source.SourceUnit{{ID: 30}, {ID: 10}}}
|
||||||
input := dnd.NPCList{NPCs: []dnd.NPC{{Name: "Lady Ash", SourceRefs: []source.SourceRef{
|
input := dnd.NPCRegistry{NPCs: []dnd.NPC{{Name: "Lady Ash", SourceRefs: []source.SourceRef{
|
||||||
{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10},
|
{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10},
|
||||||
{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30},
|
{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30},
|
||||||
{SourceID: doc.ID, StartUnitID: 999, EndUnitID: 999},
|
{SourceID: doc.ID, StartUnitID: 999, EndUnitID: 999},
|
||||||
@@ -84,7 +84,7 @@ func TestNormalizeOrdersEvidenceBySourceDocumentPosition(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNormalizeConsolidatesCanonicalNamesOnlyAndUnionsEvidence(t *testing.T) {
|
func TestNormalizeConsolidatesCanonicalNamesOnlyAndUnionsEvidence(t *testing.T) {
|
||||||
input := dnd.NPCList{NPCs: []dnd.NPC{
|
input := dnd.NPCRegistry{NPCs: []dnd.NPC{
|
||||||
{Name: " Captain Vale ", SourceRefs: []source.SourceRef{{SourceID: "a", StartUnitID: 1, EndUnitID: 1}}},
|
{Name: " Captain Vale ", SourceRefs: []source.SourceRef{{SourceID: "a", StartUnitID: 1, EndUnitID: 1}}},
|
||||||
{Name: "captain vale", SourceRefs: []source.SourceRef{{SourceID: "b", StartUnitID: 2, EndUnitID: 2}}},
|
{Name: "captain vale", SourceRefs: []source.SourceRef{{SourceID: "b", StartUnitID: 2, EndUnitID: 2}}},
|
||||||
{Name: "The Captain", SourceRefs: []source.SourceRef{{SourceID: "c", StartUnitID: 3, EndUnitID: 3}}},
|
{Name: "The Captain", SourceRefs: []source.SourceRef{{SourceID: "c", StartUnitID: 3, EndUnitID: 3}}},
|
||||||
@@ -105,8 +105,8 @@ func TestNormalizeConsolidatesCanonicalNamesOnlyAndUnionsEvidence(t *testing.T)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNormalizePreservesInvalidCandidatesAndDoesNotAliasInput(t *testing.T) {
|
func TestNormalizePreservesInvalidCandidatesAndDoesNotAliasInput(t *testing.T) {
|
||||||
input := dnd.NPCList{NPCs: []dnd.NPC{{Name: " ", SourceRefs: []source.SourceRef{{SourceID: "source", StartUnitID: 0, EndUnitID: -1}}}}}
|
input := dnd.NPCRegistry{NPCs: []dnd.NPC{{Name: " ", SourceRefs: []source.SourceRef{{SourceID: "source", StartUnitID: 0, EndUnitID: -1}}}}}
|
||||||
before := dnd.NPCList{NPCs: []dnd.NPC{{Name: " ", SourceRefs: []source.SourceRef{{SourceID: "source", StartUnitID: 0, EndUnitID: -1}}}}}
|
before := dnd.NPCRegistry{NPCs: []dnd.NPC{{Name: " ", SourceRefs: []source.SourceRef{{SourceID: "source", StartUnitID: 0, EndUnitID: -1}}}}}
|
||||||
result, err := newNormalizer(t, &recordingNPCNormalizerClient{}).Normalize(context.Background(), normalizeRequest(input))
|
result, err := newNormalizer(t, &recordingNPCNormalizerClient{}).Normalize(context.Background(), normalizeRequest(input))
|
||||||
if err != nil || !reflect.DeepEqual(input, before) {
|
if err != nil || !reflect.DeepEqual(input, before) {
|
||||||
t.Fatalf("Normalize() = %#v, %v; input mutated to %#v", result, err, input)
|
t.Fatalf("Normalize() = %#v, %v; input mutated to %#v", result, err, input)
|
||||||
@@ -122,13 +122,13 @@ func TestNormalizePreservesInvalidCandidatesAndDoesNotAliasInput(t *testing.T) {
|
|||||||
|
|
||||||
func TestNormalizeHandlesNilAndCanceledCalls(t *testing.T) {
|
func TestNormalizeHandlesNilAndCanceledCalls(t *testing.T) {
|
||||||
normalizer := newNormalizer(t, &recordingNPCNormalizerClient{})
|
normalizer := newNormalizer(t, &recordingNPCNormalizerClient{})
|
||||||
result, err := normalizer.Normalize(context.Background(), normalizeRequest(dnd.NPCList{NPCs: nil}))
|
result, err := normalizer.Normalize(context.Background(), normalizeRequest(dnd.NPCRegistry{NPCs: nil}))
|
||||||
if err != nil || result.Value.NPCs != nil {
|
if err != nil || result.Value.NPCs != nil {
|
||||||
t.Fatalf("nil list result = %#v, error = %v", result.Value, err)
|
t.Fatalf("nil list result = %#v, error = %v", result.Value, err)
|
||||||
}
|
}
|
||||||
canceled, cancel := context.WithCancel(context.Background())
|
canceled, cancel := context.WithCancel(context.Background())
|
||||||
cancel()
|
cancel()
|
||||||
if _, err := normalizer.Normalize(canceled, normalizeRequest(dnd.NPCList{})); err == nil || !strings.Contains(err.Error(), "context error") {
|
if _, err := normalizer.Normalize(canceled, normalizeRequest(dnd.NPCRegistry{})); err == nil || !strings.Contains(err.Error(), "context error") {
|
||||||
t.Fatalf("canceled Normalize() error = %v", err)
|
t.Fatalf("canceled Normalize() error = %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,11 +171,11 @@ func newNormalizer(t *testing.T, client contracts.StructuredLLMClient) *Normaliz
|
|||||||
return normalizer
|
return normalizer
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeRequest(value dnd.NPCList) contracts.TypedNormalizeRequest[dnd.NPCList] {
|
func normalizeRequest(value dnd.NPCRegistry) contracts.TypedNormalizeRequest[dnd.NPCRegistry] {
|
||||||
return contracts.TypedNormalizeRequest[dnd.NPCList]{MergeOutput: contracts.MergeArtifact[dnd.NPCList]{Value: value}}
|
return contracts.TypedNormalizeRequest[dnd.NPCRegistry]{MergeOutput: contracts.MergeArtifact[dnd.NPCRegistry]{Value: value}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeRequestWithSource(value dnd.NPCList, doc *source.SourceDocument) contracts.TypedNormalizeRequest[dnd.NPCList] {
|
func normalizeRequestWithSource(value dnd.NPCRegistry, doc *source.SourceDocument) contracts.TypedNormalizeRequest[dnd.NPCRegistry] {
|
||||||
request := normalizeRequest(value)
|
request := normalizeRequest(value)
|
||||||
request.Source = doc
|
request.Source = doc
|
||||||
return request
|
return request
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func TestNormalizeSkipsSemanticCompletionWithoutTwoEligibleCandidates(t *testing
|
|||||||
client := &recordingNPCNormalizerClient{}
|
client := &recordingNPCNormalizerClient{}
|
||||||
normalizer := newNormalizer(t, client)
|
normalizer := newNormalizer(t, client)
|
||||||
doc := semanticDocument()
|
doc := semanticDocument()
|
||||||
input := dnd.NPCList{NPCs: []dnd.NPC{{Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}}}}
|
input := dnd.NPCRegistry{NPCs: []dnd.NPC{{Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}}}}
|
||||||
result, err := normalizer.Normalize(context.Background(), normalizeRequestWithSource(input, doc))
|
result, err := normalizer.Normalize(context.Background(), normalizeRequestWithSource(input, doc))
|
||||||
if err != nil || len(client.requests) != 0 || result.Retry != nil {
|
if err != nil || len(client.requests) != 0 || result.Retry != nil {
|
||||||
t.Fatalf("Normalize() = %#v, %v; calls = %d, want deterministic no-call result", result, err, len(client.requests))
|
t.Fatalf("Normalize() = %#v, %v; calls = %d, want deterministic no-call result", result, err, len(client.requests))
|
||||||
@@ -33,12 +33,12 @@ func TestNormalizeAppliesSafeProposalAndUsesPrivateInputs(t *testing.T) {
|
|||||||
client := &recordingNPCNormalizerClient{response: `{"duplicate_groups":[{"members":["candidate-000001","candidate-000002"],"canonical":"candidate-000002"}]}`}
|
client := &recordingNPCNormalizerClient{response: `{"duplicate_groups":[{"members":["candidate-000001","candidate-000002"],"canonical":"candidate-000002"}]}`}
|
||||||
normalizer := newNormalizer(t, client)
|
normalizer := newNormalizer(t, client)
|
||||||
doc := semanticDocument()
|
doc := semanticDocument()
|
||||||
input := dnd.NPCList{NPCs: []dnd.NPC{
|
input := dnd.NPCRegistry{NPCs: []dnd.NPC{
|
||||||
{ID: "npc:sha256:short", Name: "Mira", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
|
{ID: "npc:sha256:short", Name: "Mira", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
|
||||||
{ID: "npc:sha256:long", Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}},
|
{ID: "npc:sha256:long", Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}},
|
||||||
{ID: "npc:sha256:captain", Name: "Captain Vale", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}}},
|
{ID: "npc:sha256:captain", Name: "Captain Vale", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}}},
|
||||||
}}
|
}}
|
||||||
before := cloneNPCList(input)
|
before := cloneNPCRegistry(input)
|
||||||
request := normalizeRequestWithSource(input, doc)
|
request := normalizeRequestWithSource(input, doc)
|
||||||
request.LLMProfile = "normalizer-profile"
|
request.LLMProfile = "normalizer-profile"
|
||||||
request.SessionID = "normalizer-session"
|
request.SessionID = "normalizer-session"
|
||||||
@@ -76,7 +76,7 @@ func TestNormalizeUnsafeProposalReturnsSafeRetryFallback(t *testing.T) {
|
|||||||
client := &recordingNPCNormalizerClient{response: `{"duplicate_groups":[{"members":["candidate-000001","candidate-000002"],"canonical":"candidate-000002"},{"members":["candidate-000003","unknown"],"canonical":"candidate-000003"}]}`}
|
client := &recordingNPCNormalizerClient{response: `{"duplicate_groups":[{"members":["candidate-000001","candidate-000002"],"canonical":"candidate-000002"},{"members":["candidate-000003","unknown"],"canonical":"candidate-000003"}]}`}
|
||||||
normalizer := newNormalizer(t, client)
|
normalizer := newNormalizer(t, client)
|
||||||
doc := semanticDocument()
|
doc := semanticDocument()
|
||||||
input := dnd.NPCList{NPCs: []dnd.NPC{
|
input := dnd.NPCRegistry{NPCs: []dnd.NPC{
|
||||||
{Name: "Mira", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
|
{Name: "Mira", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
|
||||||
{Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}},
|
{Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}},
|
||||||
{Name: "Captain Vale", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}}},
|
{Name: "Captain Vale", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}}},
|
||||||
@@ -98,7 +98,7 @@ func TestNormalizeUnsafeProposalReturnsSafeRetryFallback(t *testing.T) {
|
|||||||
|
|
||||||
func TestNormalizeInvalidStructuredOutputAndOperationalErrorsRemainDistinct(t *testing.T) {
|
func TestNormalizeInvalidStructuredOutputAndOperationalErrorsRemainDistinct(t *testing.T) {
|
||||||
doc := semanticDocument()
|
doc := semanticDocument()
|
||||||
input := dnd.NPCList{NPCs: []dnd.NPC{
|
input := dnd.NPCRegistry{NPCs: []dnd.NPC{
|
||||||
{Name: "Mira", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
|
{Name: "Mira", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
|
||||||
{Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}},
|
{Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}},
|
||||||
}}
|
}}
|
||||||
@@ -129,7 +129,7 @@ func TestNormalizeRedactsContextMaterialFailures(t *testing.T) {
|
|||||||
{ID: 10, Kind: "speech", Text: transcript, Metadata: map[string]any{metadataKey: math.NaN(), "value": metadataValue}},
|
{ID: 10, Kind: "speech", Text: transcript, Metadata: map[string]any{metadataKey: math.NaN(), "value": metadataValue}},
|
||||||
{ID: 20, Kind: "speech", Text: "other context"},
|
{ID: 20, Kind: "speech", Text: "other context"},
|
||||||
}}
|
}}
|
||||||
input := dnd.NPCList{NPCs: []dnd.NPC{
|
input := dnd.NPCRegistry{NPCs: []dnd.NPC{
|
||||||
{Name: firstName, SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
|
{Name: firstName, SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
|
||||||
{Name: secondName, SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}},
|
{Name: secondName, SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}},
|
||||||
}}
|
}}
|
||||||
@@ -156,7 +156,7 @@ func TestNormalizeDoesNotAccumulateSafeGroupsAcrossAttempts(t *testing.T) {
|
|||||||
}}
|
}}
|
||||||
normalizer := newNormalizer(t, client)
|
normalizer := newNormalizer(t, client)
|
||||||
doc := semanticDocument()
|
doc := semanticDocument()
|
||||||
input := dnd.NPCList{NPCs: []dnd.NPC{
|
input := dnd.NPCRegistry{NPCs: []dnd.NPC{
|
||||||
{Name: "Mira", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
|
{Name: "Mira", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
|
||||||
{Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}},
|
{Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 20, EndUnitID: 20}}},
|
||||||
{Name: "Captain Vale", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}}},
|
{Name: "Captain Vale", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}}},
|
||||||
@@ -178,7 +178,7 @@ func TestNormalizeExcludesIneligibleRecordsFromSemanticGroups(t *testing.T) {
|
|||||||
client := &recordingNPCNormalizerClient{response: `{"duplicate_groups":[{"members":["candidate-000001","candidate-000002"],"canonical":"candidate-000001"}]}`}
|
client := &recordingNPCNormalizerClient{response: `{"duplicate_groups":[{"members":["candidate-000001","candidate-000002"],"canonical":"candidate-000001"}]}`}
|
||||||
normalizer := newNormalizer(t, client)
|
normalizer := newNormalizer(t, client)
|
||||||
doc := semanticDocument()
|
doc := semanticDocument()
|
||||||
input := dnd.NPCList{NPCs: []dnd.NPC{
|
input := dnd.NPCRegistry{NPCs: []dnd.NPC{
|
||||||
{Name: "Mira", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
|
{Name: "Mira", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 10, EndUnitID: 10}}},
|
||||||
{Name: "Broken", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 999, EndUnitID: 999}}},
|
{Name: "Broken", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 999, EndUnitID: 999}}},
|
||||||
{Name: "Captain Vale", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}}},
|
{Name: "Captain Vale", SourceRefs: []source.SourceRef{{SourceID: doc.ID, StartUnitID: 30, EndUnitID: 30}}},
|
||||||
@@ -235,8 +235,8 @@ func semanticDocument() *source.SourceDocument {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneNPCList(input dnd.NPCList) dnd.NPCList {
|
func cloneNPCRegistry(input dnd.NPCRegistry) dnd.NPCRegistry {
|
||||||
output := dnd.NPCList{NPCs: make([]dnd.NPC, len(input.NPCs))}
|
output := dnd.NPCRegistry{NPCs: make([]dnd.NPC, len(input.NPCs))}
|
||||||
for index, npc := range input.NPCs {
|
for index, npc := range input.NPCs {
|
||||||
output.NPCs[index] = cloneNPC(npc)
|
output.NPCs[index] = cloneNPC(npc)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package identity
|
|||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
// Policy identifies the complete identity comparison and ID derivation
|
// Policy identifies the complete identity comparison and ID derivation
|
||||||
// policy. A future semantic change must use a new value.
|
// policy. A future semantic change must use a new value.
|
||||||
Policy = "dnd.npcs.identity.v1"
|
Policy = "dnd.npc_registry.identity.v1"
|
||||||
// IdentityPolicy is an explicit alias for callers recording policy
|
// IdentityPolicy is an explicit alias for callers recording policy
|
||||||
// fingerprints.
|
// fingerprints.
|
||||||
IdentityPolicy = Policy
|
IdentityPolicy = Policy
|
||||||
@@ -72,7 +73,11 @@ func DeriveID(name string) string {
|
|||||||
if key == "" {
|
if key == "" {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
digest := sha256.Sum256([]byte(key))
|
identity, err := json.Marshal([]string{Policy, key})
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
digest := sha256.Sum256(identity)
|
||||||
return idPrefix + hex.EncodeToString(digest[:])
|
return idPrefix + hex.EncodeToString(digest[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +103,7 @@ func ValidID(value string) bool { return IsValidID(value) }
|
|||||||
|
|
||||||
// ValidateRegistry checks all identity invariants without changing the input.
|
// ValidateRegistry checks all identity invariants without changing the input.
|
||||||
// It accepts the NPC slice used by typed pipeline artifacts. Use ValidateList
|
// It accepts the NPC slice used by typed pipeline artifacts. Use ValidateList
|
||||||
// when the enclosing NPCList is more convenient at the call site.
|
// when the enclosing NPCRegistry is more convenient at the call site.
|
||||||
func ValidateRegistry(npcs []dnd.NPC) []Issue {
|
func ValidateRegistry(npcs []dnd.NPC) []Issue {
|
||||||
issues := make([]Issue, 0)
|
issues := make([]Issue, 0)
|
||||||
canonicalOwners := make(map[string][]int)
|
canonicalOwners := make(map[string][]int)
|
||||||
@@ -136,7 +141,7 @@ func ValidateRegistry(npcs []dnd.NPC) []Issue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ValidateList validates the identity members of list.
|
// ValidateList validates the identity members of list.
|
||||||
func ValidateList(list dnd.NPCList) []Issue { return ValidateRegistry(list.NPCs) }
|
func ValidateList(list dnd.NPCRegistry) []Issue { return ValidateRegistry(list.NPCs) }
|
||||||
|
|
||||||
// Error makes an issue useful in simple callers while preserving its
|
// Error makes an issue useful in simple callers while preserving its
|
||||||
// structured fields for aggregate diagnostics.
|
// structured fields for aggregate diagnostics.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package identity
|
package identity
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -40,9 +41,16 @@ func TestNormalizeDisplayOnlyChangesWhitespace(t *testing.T) {
|
|||||||
|
|
||||||
func TestDeriveIDAndIDSyntax(t *testing.T) {
|
func TestDeriveIDAndIDSyntax(t *testing.T) {
|
||||||
got := DeriveID(" Mira\u2003Thorn ")
|
got := DeriveID(" Mira\u2003Thorn ")
|
||||||
|
const want = "npc:sha256:35ba5f679aee69e07ae3bd65c44278f29539d5dc9bb5225db1c0060555b23221"
|
||||||
|
if Policy != "dnd.npc_registry.identity.v1" || IdentityPolicy != Policy {
|
||||||
|
t.Fatalf("identity policy = %q/%q", Policy, IdentityPolicy)
|
||||||
|
}
|
||||||
if len(got) != len("npc:sha256:")+64 || !strings.HasPrefix(got, "npc:sha256:") || !IsValidID(got) {
|
if len(got) != len("npc:sha256:")+64 || !strings.HasPrefix(got, "npc:sha256:") || !IsValidID(got) {
|
||||||
t.Fatalf("DeriveID() = %q, want exact NPC ID syntax", got)
|
t.Fatalf("DeriveID() = %q, want exact NPC ID syntax", got)
|
||||||
}
|
}
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("DeriveID() = %q, want SHA-256 of compact JSON [policy, comparison_name]", got)
|
||||||
|
}
|
||||||
if got != DeriveID("Mira Thorn") || got != IDFor("Mira Thorn") {
|
if got != DeriveID("Mira Thorn") || got != IDFor("Mira Thorn") {
|
||||||
t.Fatal("DeriveID() is not deterministic across equivalent names")
|
t.Fatal("DeriveID() is not deterministic across equivalent names")
|
||||||
}
|
}
|
||||||
@@ -80,7 +88,11 @@ func TestValidateRegistryReportsIdentityCollisionCategories(t *testing.T) {
|
|||||||
{ID: validID, Name: "Mira Thorn"},
|
{ID: validID, Name: "Mira Thorn"},
|
||||||
{ID: validID, Name: "Mira Thorn"},
|
{ID: validID, Name: "Mira Thorn"},
|
||||||
}
|
}
|
||||||
|
before := append([]dnd.NPC(nil), npcs...)
|
||||||
issues := ValidateRegistry(npcs)
|
issues := ValidateRegistry(npcs)
|
||||||
|
if !reflect.DeepEqual(npcs, before) {
|
||||||
|
t.Fatal("ValidateRegistry() mutated its input")
|
||||||
|
}
|
||||||
want := map[IssueCode]bool{
|
want := map[IssueCode]bool{
|
||||||
IssueDuplicateCanonical: false,
|
IssueDuplicateCanonical: false,
|
||||||
IssueDuplicateID: false,
|
IssueDuplicateID: false,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const (
|
|||||||
// grounding. All accessors return defensive copies.
|
// grounding. All accessors return defensive copies.
|
||||||
type Registry struct {
|
type Registry struct {
|
||||||
bound bool
|
bound bool
|
||||||
list dnd.NPCList
|
list dnd.NPCRegistry
|
||||||
canonical []byte
|
canonical []byte
|
||||||
digest string
|
digest string
|
||||||
projectionDigest string
|
projectionDigest string
|
||||||
@@ -105,7 +105,7 @@ func emptyRegistry() *Registry {
|
|||||||
content := []byte(emptyPrompt)
|
content := []byte(emptyPrompt)
|
||||||
projectionDigest := semanticDigest(content)
|
projectionDigest := semanticDigest(content)
|
||||||
return &Registry{
|
return &Registry{
|
||||||
list: dnd.NPCList{NPCs: []dnd.NPC{}},
|
list: dnd.NPCRegistry{NPCs: []dnd.NPC{}},
|
||||||
canonical: append([]byte(nil), content...),
|
canonical: append([]byte(nil), content...),
|
||||||
projectionDigest: projectionDigest,
|
projectionDigest: projectionDigest,
|
||||||
promptInput: contracts.NewLLMInputMaterial(ReferenceSlot, npccodec.MediaType, content, projectionDigest, ""),
|
promptInput: contracts.NewLLMInputMaterial(ReferenceSlot, npccodec.MediaType, content, projectionDigest, ""),
|
||||||
@@ -127,7 +127,7 @@ func loadRegistry(referenceContent []byte) (*Registry, error) {
|
|||||||
return nil, fmt.Errorf("encode canonical NPC registry: approved NPC value could not be encoded")
|
return nil, fmt.Errorf("encode canonical NPC registry: approved NPC value could not be encoded")
|
||||||
}
|
}
|
||||||
|
|
||||||
list := cloneNPCList(value)
|
list := cloneNPCRegistry(value)
|
||||||
lookupByKey := make(map[string]int, len(list.NPCs))
|
lookupByKey := make(map[string]int, len(list.NPCs))
|
||||||
for index, npc := range list.NPCs {
|
for index, npc := range list.NPCs {
|
||||||
lookupByKey[identity.ComparisonKey(npc.Name)] = index
|
lookupByKey[identity.ComparisonKey(npc.Name)] = index
|
||||||
@@ -161,11 +161,11 @@ func (r *Registry) NPCs() []dnd.NPC {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// List returns a defensive copy of the validated NPC list.
|
// List returns a defensive copy of the validated NPC list.
|
||||||
func (r *Registry) List() dnd.NPCList {
|
func (r *Registry) List() dnd.NPCRegistry {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return dnd.NPCList{}
|
return dnd.NPCRegistry{}
|
||||||
}
|
}
|
||||||
return cloneNPCList(r.list)
|
return cloneNPCRegistry(r.list)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanonicalBytes returns a defensive copy of the canonical durable JSON.
|
// CanonicalBytes returns a defensive copy of the canonical durable JSON.
|
||||||
@@ -233,12 +233,12 @@ type projectedNPC struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type projectedNPCList struct {
|
type projectedNPCRegistry struct {
|
||||||
NPCs []projectedNPC `json:"npcs"`
|
NPCs []projectedNPC `json:"npcs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func nameProjection(list dnd.NPCList) ([]byte, error) {
|
func nameProjection(list dnd.NPCRegistry) ([]byte, error) {
|
||||||
projection := projectedNPCList{NPCs: make([]projectedNPC, len(list.NPCs))}
|
projection := projectedNPCRegistry{NPCs: make([]projectedNPC, len(list.NPCs))}
|
||||||
for index, npc := range list.NPCs {
|
for index, npc := range list.NPCs {
|
||||||
projection.NPCs[index] = projectedNPC{Name: npc.Name}
|
projection.NPCs[index] = projectedNPC{Name: npc.Name}
|
||||||
}
|
}
|
||||||
@@ -254,8 +254,8 @@ func formatIdentityIssues(issues []identity.Issue) string {
|
|||||||
return diagnostics.Aggregate("validate NPC registry identity", parts)
|
return diagnostics.Aggregate("validate NPC registry identity", parts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneNPCList(value dnd.NPCList) dnd.NPCList {
|
func cloneNPCRegistry(value dnd.NPCRegistry) dnd.NPCRegistry {
|
||||||
return dnd.NPCList{NPCs: cloneNPCs(value.NPCs)}
|
return dnd.NPCRegistry{NPCs: cloneNPCs(value.NPCs)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneNPCs(values []dnd.NPC) []dnd.NPC {
|
func cloneNPCs(values []dnd.NPC) []dnd.NPC {
|
||||||
|
|||||||
@@ -164,14 +164,14 @@ func TestResolverHandlesConstructionAndOperationReferences(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func registryFixture() dnd.NPCList {
|
func registryFixture() dnd.NPCRegistry {
|
||||||
return dnd.NPCList{NPCs: []dnd.NPC{
|
return dnd.NPCRegistry{NPCs: []dnd.NPC{
|
||||||
{ID: identity.DeriveID("Mira Thorn"), Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 1, EndUnitID: 2}}},
|
{ID: identity.DeriveID("Mira Thorn"), Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 1, EndUnitID: 2}}},
|
||||||
{ID: identity.DeriveID("Captain Vale"), Name: "Captain Vale", SourceRefs: []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 3, EndUnitID: 3}}},
|
{ID: identity.DeriveID("Captain Vale"), Name: "Captain Vale", SourceRefs: []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 3, EndUnitID: 3}}},
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveList(t *testing.T, list dnd.NPCList) *Registry {
|
func resolveList(t *testing.T, list dnd.NPCRegistry) *Registry {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
registry, err := Resolve(listReferenceSet(t, list))
|
registry, err := Resolve(listReferenceSet(t, list))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -180,7 +180,7 @@ func resolveList(t *testing.T, list dnd.NPCList) *Registry {
|
|||||||
return registry
|
return registry
|
||||||
}
|
}
|
||||||
|
|
||||||
func listReferenceSet(t *testing.T, list dnd.NPCList) contracts.ReferenceSet {
|
func listReferenceSet(t *testing.T, list dnd.NPCRegistry) contracts.ReferenceSet {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
content, err := npccodec.New().Encode(list)
|
content, err := npccodec.New().Encode(list)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
func registerEvidence(registry *pipeline.ArtifactEvidenceRegistry) error {
|
func registerEvidence(registry *pipeline.ArtifactEvidenceRegistry) error {
|
||||||
return runRegistrations([]registration{
|
return runRegistrations([]registration{
|
||||||
{name: "spells evidence", register: func() error { return pipeline.RegisterArtifactEvidence(registry, dnd.SpellListKind, spellEvidence) }},
|
{name: "spells evidence", register: func() error { return pipeline.RegisterArtifactEvidence(registry, dnd.SpellListKind, spellEvidence) }},
|
||||||
{name: "npcs evidence", register: func() error { return pipeline.RegisterArtifactEvidence(registry, dnd.NPCListKind, npcEvidence) }},
|
{name: "npcs evidence", register: func() error { return pipeline.RegisterArtifactEvidence(registry, dnd.NPCRegistryKind, npcEvidence) }},
|
||||||
{name: "combat turns evidence", register: func() error {
|
{name: "combat turns evidence", register: func() error {
|
||||||
return pipeline.RegisterArtifactEvidence(registry, dnd.CombatTurnListKind, combatTurnEvidence)
|
return pipeline.RegisterArtifactEvidence(registry, dnd.CombatTurnListKind, combatTurnEvidence)
|
||||||
}},
|
}},
|
||||||
@@ -42,7 +42,7 @@ func spellEvidence(value dnd.SpellList) []source.SourceRef {
|
|||||||
return append([]source.SourceRef(nil), refs...)
|
return append([]source.SourceRef(nil), refs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func npcEvidence(value dnd.NPCList) []source.SourceRef {
|
func npcEvidence(value dnd.NPCRegistry) []source.SourceRef {
|
||||||
var refs []source.SourceRef
|
var refs []source.SourceRef
|
||||||
for _, record := range value.NPCs {
|
for _, record := range value.NPCs {
|
||||||
refs = append(refs, record.SourceRefs...)
|
refs = append(refs, record.SourceRefs...)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func appendSpellLists(values []dnd.SpellList) (dnd.SpellList, error) {
|
|||||||
return combined, nil
|
return combined, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendNPCLists(values []dnd.NPCList) (dnd.NPCList, error) {
|
func appendNPCRegistries(values []dnd.NPCRegistry) (dnd.NPCRegistry, error) {
|
||||||
count := 0
|
count := 0
|
||||||
present := false
|
present := false
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
@@ -38,9 +38,9 @@ func appendNPCLists(values []dnd.NPCList) (dnd.NPCList, error) {
|
|||||||
count += len(value.NPCs)
|
count += len(value.NPCs)
|
||||||
}
|
}
|
||||||
if !present {
|
if !present {
|
||||||
return dnd.NPCList{}, nil
|
return dnd.NPCRegistry{}, nil
|
||||||
}
|
}
|
||||||
combined := dnd.NPCList{NPCs: make([]dnd.NPC, 0, count)}
|
combined := dnd.NPCRegistry{NPCs: make([]dnd.NPC, 0, count)}
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
for _, npc := range value.NPCs {
|
for _, npc := range value.NPCs {
|
||||||
combined.NPCs = append(combined.NPCs, cloneNPC(npc))
|
combined.NPCs = append(combined.NPCs, cloneNPC(npc))
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ func registerModules(registries pipeline.Registries) error {
|
|||||||
return appendorder.RegisterTyped(registries.Mergers, dnd.SpellListKind, appendSpellLists)
|
return appendorder.RegisterTyped(registries.Mergers, dnd.SpellListKind, appendSpellLists)
|
||||||
}},
|
}},
|
||||||
{name: "npc-list appendorder merger", register: func() error {
|
{name: "npc-list appendorder merger", register: func() error {
|
||||||
return appendorder.RegisterTyped(registries.Mergers, dnd.NPCListKind, appendNPCLists)
|
return appendorder.RegisterTyped(registries.Mergers, dnd.NPCRegistryKind, appendNPCRegistries)
|
||||||
}},
|
}},
|
||||||
{name: "combat-turn-list appendorder merger", register: func() error {
|
{name: "combat-turn-list appendorder merger", register: func() error {
|
||||||
return appendorder.RegisterTyped(registries.Mergers, dnd.CombatTurnListKind, appendCombatTurnLists)
|
return appendorder.RegisterTyped(registries.Mergers, dnd.CombatTurnListKind, appendCombatTurnLists)
|
||||||
@@ -103,7 +103,7 @@ func registerModules(registries pipeline.Registries) error {
|
|||||||
return noop.RegisterTyped[dnd.SpellList](registries.Normalizers, dnd.SpellListKind)
|
return noop.RegisterTyped[dnd.SpellList](registries.Normalizers, dnd.SpellListKind)
|
||||||
}},
|
}},
|
||||||
{name: "npc-list noop normalizer", register: func() error {
|
{name: "npc-list noop normalizer", register: func() error {
|
||||||
return noop.RegisterTyped[dnd.NPCList](registries.Normalizers, dnd.NPCListKind)
|
return noop.RegisterTyped[dnd.NPCRegistry](registries.Normalizers, dnd.NPCRegistryKind)
|
||||||
}},
|
}},
|
||||||
{name: "combat-turn-list noop normalizer", register: func() error {
|
{name: "combat-turn-list noop normalizer", register: func() error {
|
||||||
return noop.RegisterTyped[dnd.CombatTurnList](registries.Normalizers, dnd.CombatTurnListKind)
|
return noop.RegisterTyped[dnd.CombatTurnList](registries.Normalizers, dnd.CombatTurnListKind)
|
||||||
|
|||||||
@@ -84,11 +84,11 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
|
|||||||
assertContainsKeys(t, "chunkers", registries.Chunkers.RegisteredKeys(), []string{"dnd/scenes"})
|
assertContainsKeys(t, "chunkers", registries.Chunkers.RegisteredKeys(), []string{"dnd/scenes"})
|
||||||
assertContainsKeys(t, "extractors", registries.Extractors.RegisteredKeys(), []string{"dnd/spells", npcextract.Key, combatextract.Key, enemyeventextract.Key, itemeventextract.Key, interactionextract.Key, scenedescriptionextract.Key, locationextract.Key, locationoccurrenceextract.Key})
|
assertContainsKeys(t, "extractors", registries.Extractors.RegisteredKeys(), []string{"dnd/spells", npcextract.Key, combatextract.Key, enemyeventextract.Key, itemeventextract.Key, interactionextract.Key, scenedescriptionextract.Key, locationextract.Key, locationoccurrenceextract.Key})
|
||||||
assertContainsKeys(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{spellnormalize.Key, npcnormalize.Key, combatnormalize.Key, enemyeventnormalize.Key, itemeventnormalize.Key, interactionnormalize.Key, scenedescriptionnormalize.Key, locationnormalize.Key, locationoccurrencenormalize.Key, pipeline.DefaultNormalizeModule})
|
assertContainsKeys(t, "normalizers", registries.Normalizers.RegisteredKeys(), []string{spellnormalize.Key, npcnormalize.Key, combatnormalize.Key, enemyeventnormalize.Key, itemeventnormalize.Key, interactionnormalize.Key, scenedescriptionnormalize.Key, locationnormalize.Key, locationoccurrencenormalize.Key, pipeline.DefaultNormalizeModule})
|
||||||
assertContainsArtifactKinds(t, registries.ArtifactCodecs.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCInteractionListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind})
|
assertContainsArtifactKinds(t, registries.ArtifactCodecs.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCInteractionListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind})
|
||||||
assertContainsArtifactKinds(t, registries.ArtifactEvidence.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCInteractionListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind})
|
assertContainsArtifactKinds(t, registries.ArtifactEvidence.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCRegistryKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCInteractionListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind})
|
||||||
assertContainsArtifactKinds(t, registries.Mergers.RegisteredArtifactKinds(pipeline.DefaultMergeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCInteractionListKind, 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.NPCInteractionListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind})
|
||||||
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(pipeline.DefaultNormalizeModule), []contracts.ArtifactKind{dnd.SpellListKind, dnd.NPCListKind, dnd.CombatTurnListKind, dnd.EnemyEventListKind, dnd.ItemEventListKind, dnd.NPCInteractionListKind, 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.NPCInteractionListKind, dnd.SceneDescriptionListKind, dnd.LocationListKind, dnd.LocationOccurrenceListKind})
|
||||||
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(npcnormalize.Key), []contracts.ArtifactKind{dnd.NPCListKind})
|
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(npcnormalize.Key), []contracts.ArtifactKind{dnd.NPCRegistryKind})
|
||||||
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(combatnormalize.Key), []contracts.ArtifactKind{dnd.CombatTurnListKind})
|
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(combatnormalize.Key), []contracts.ArtifactKind{dnd.CombatTurnListKind})
|
||||||
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(enemyeventnormalize.Key), []contracts.ArtifactKind{dnd.EnemyEventListKind})
|
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(enemyeventnormalize.Key), []contracts.ArtifactKind{dnd.EnemyEventListKind})
|
||||||
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(itemeventnormalize.Key), []contracts.ArtifactKind{dnd.ItemEventListKind})
|
assertContainsArtifactKinds(t, registries.Normalizers.RegisteredArtifactKinds(itemeventnormalize.Key), []contracts.ArtifactKind{dnd.ItemEventListKind})
|
||||||
@@ -316,10 +316,10 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
|
|||||||
if spec, ok := registries.Normalizers.Spec(spellnormalize.Key); !ok || spec.ArtifactKind != dnd.SpellListKind || spec.Stage != pipeline.StageNormalize {
|
if spec, ok := registries.Normalizers.Spec(spellnormalize.Key); !ok || spec.ArtifactKind != dnd.SpellListKind || spec.Stage != pipeline.StageNormalize {
|
||||||
t.Fatalf("spell normalizer spec = %#v, present = %t; want dnd spell-list artifact", spec, ok)
|
t.Fatalf("spell normalizer spec = %#v, present = %t; want dnd spell-list artifact", spec, ok)
|
||||||
}
|
}
|
||||||
if spec, ok := registries.Extractors.Spec(npcextract.Key); !ok || spec.ArtifactKind != dnd.NPCListKind {
|
if spec, ok := registries.Extractors.Spec(npcextract.Key); !ok || spec.ArtifactKind != dnd.NPCRegistryKind {
|
||||||
t.Fatalf("NPC extractor spec = %#v, present = %t; want dnd NPC-list artifact", spec, ok)
|
t.Fatalf("NPC extractor spec = %#v, present = %t; want dnd NPC-list artifact", spec, ok)
|
||||||
}
|
}
|
||||||
if spec, ok := registries.Normalizers.Spec(npcnormalize.Key); !ok || spec.ArtifactKind != dnd.NPCListKind || spec.Stage != pipeline.StageNormalize {
|
if spec, ok := registries.Normalizers.Spec(npcnormalize.Key); !ok || spec.ArtifactKind != dnd.NPCRegistryKind || spec.Stage != pipeline.StageNormalize {
|
||||||
t.Fatalf("NPC normalizer spec = %#v, present = %t; want dnd NPC-list artifact", spec, ok)
|
t.Fatalf("NPC normalizer spec = %#v, present = %t; want dnd NPC-list artifact", spec, ok)
|
||||||
}
|
}
|
||||||
if spec, ok := registries.Extractors.Spec(combatextract.Key); !ok || spec.ArtifactKind != dnd.CombatTurnListKind {
|
if spec, ok := registries.Extractors.Spec(combatextract.Key); !ok || spec.ArtifactKind != dnd.CombatTurnListKind {
|
||||||
@@ -390,7 +390,7 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
|
|||||||
}
|
}
|
||||||
extractRegistrySlot := referenceSlot(interactionExtractSpec.ReferenceSlots, "npcs")
|
extractRegistrySlot := referenceSlot(interactionExtractSpec.ReferenceSlots, "npcs")
|
||||||
normalizeRegistrySlot := referenceSlot(interactionNormalizeSpec.ReferenceSlots, "npcs")
|
normalizeRegistrySlot := referenceSlot(interactionNormalizeSpec.ReferenceSlots, "npcs")
|
||||||
if !extractRegistrySlot.Required || !reflect.DeepEqual(extractRegistrySlot.AcceptedArtifactKinds, []contracts.ArtifactKind{dnd.NPCListKind}) || !sameReferenceSlotContract(extractRegistrySlot, normalizeRegistrySlot) {
|
if !extractRegistrySlot.Required || !reflect.DeepEqual(extractRegistrySlot.AcceptedArtifactKinds, []contracts.ArtifactKind{dnd.NPCRegistryKind}) || !sameReferenceSlotContract(extractRegistrySlot, normalizeRegistrySlot) {
|
||||||
t.Fatalf("NPC interaction registry slots disagree: %#v / %#v", interactionExtractSpec.ReferenceSlots, interactionNormalizeSpec.ReferenceSlots)
|
t.Fatalf("NPC interaction registry slots disagree: %#v / %#v", interactionExtractSpec.ReferenceSlots, interactionNormalizeSpec.ReferenceSlots)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -407,7 +407,7 @@ func TestEvidenceProjectorsPreserveDirectReferencesWithIndependentStorage(t *tes
|
|||||||
return spellEvidence(dnd.SpellList{SpellCasts: []dnd.SpellCast{{SourceRefs: []source.SourceRef{first, second}}}})
|
return spellEvidence(dnd.SpellList{SpellCasts: []dnd.SpellCast{{SourceRefs: []source.SourceRef{first, second}}}})
|
||||||
}, want: []source.SourceRef{first, second}},
|
}, want: []source.SourceRef{first, second}},
|
||||||
{name: "npcs", project: func() []source.SourceRef {
|
{name: "npcs", project: func() []source.SourceRef {
|
||||||
return npcEvidence(dnd.NPCList{NPCs: []dnd.NPC{{SourceRefs: []source.SourceRef{first, second}}}})
|
return npcEvidence(dnd.NPCRegistry{NPCs: []dnd.NPC{{SourceRefs: []source.SourceRef{first, second}}}})
|
||||||
}, want: []source.SourceRef{first, second}},
|
}, want: []source.SourceRef{first, second}},
|
||||||
{name: "combat turns", project: func() []source.SourceRef {
|
{name: "combat turns", project: func() []source.SourceRef {
|
||||||
return combatTurnEvidence(dnd.CombatTurnList{CombatTurns: []dnd.CombatTurn{{SourceRefs: []source.SourceRef{first, second}}}})
|
return combatTurnEvidence(dnd.CombatTurnList{CombatTurns: []dnd.CombatTurn{{SourceRefs: []source.SourceRef{first, second}}}})
|
||||||
@@ -459,30 +459,30 @@ func sameReferenceSlotContract(first, second contracts.ReferenceSlot) bool {
|
|||||||
return reflect.DeepEqual(first, second)
|
return reflect.DeepEqual(first, second)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAppendNPCListsPreservesOrderAndArrayPresence(t *testing.T) {
|
func TestAppendNPCRegistriesPreservesOrderAndArrayPresence(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
in []dnd.NPCList
|
in []dnd.NPCRegistry
|
||||||
want dnd.NPCList
|
want dnd.NPCRegistry
|
||||||
}{
|
}{
|
||||||
{name: "no values", in: nil, want: dnd.NPCList{}},
|
{name: "no values", in: nil, want: dnd.NPCRegistry{}},
|
||||||
{name: "nil values", in: []dnd.NPCList{{}, {}}, want: dnd.NPCList{}},
|
{name: "nil values", in: []dnd.NPCRegistry{{}, {}}, want: dnd.NPCRegistry{}},
|
||||||
{name: "present empty", in: []dnd.NPCList{{NPCs: []dnd.NPC{}}}, want: dnd.NPCList{NPCs: []dnd.NPC{}}},
|
{name: "present empty", in: []dnd.NPCRegistry{{NPCs: []dnd.NPC{}}}, want: dnd.NPCRegistry{NPCs: []dnd.NPC{}}},
|
||||||
{name: "ordered values", in: []dnd.NPCList{{NPCs: []dnd.NPC{{Name: "first"}}}, {NPCs: []dnd.NPC{{Name: "second"}}}}, want: dnd.NPCList{NPCs: []dnd.NPC{{Name: "first"}, {Name: "second"}}}},
|
{name: "ordered values", in: []dnd.NPCRegistry{{NPCs: []dnd.NPC{{Name: "first"}}}, {NPCs: []dnd.NPC{{Name: "second"}}}}, want: dnd.NPCRegistry{NPCs: []dnd.NPC{{Name: "first"}, {Name: "second"}}}},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := appendNPCLists(tt.in)
|
got, err := appendNPCRegistries(tt.in)
|
||||||
if err != nil || !reflect.DeepEqual(got, tt.want) {
|
if err != nil || !reflect.DeepEqual(got, tt.want) {
|
||||||
t.Fatalf("appendNPCLists() = %#v, error = %v, want %#v", got, err, tt.want)
|
t.Fatalf("appendNPCRegistries() = %#v, error = %v, want %#v", got, err, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
input := []dnd.NPCList{{NPCs: []dnd.NPC{{Name: "first", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 10, EndUnitID: 10}}}}}}
|
input := []dnd.NPCRegistry{{NPCs: []dnd.NPC{{Name: "first", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 10, EndUnitID: 10}}}}}}
|
||||||
merged, err := appendNPCLists(input)
|
merged, err := appendNPCRegistries(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("appendNPCLists() error = %v", err)
|
t.Fatalf("appendNPCRegistries() error = %v", err)
|
||||||
}
|
}
|
||||||
merged.NPCs[0].SourceRefs[0].StartUnitID = 999
|
merged.NPCs[0].SourceRefs[0].StartUnitID = 999
|
||||||
if input[0].NPCs[0].SourceRefs[0].StartUnitID == 999 {
|
if input[0].NPCs[0].SourceRefs[0].StartUnitID == 999 {
|
||||||
@@ -696,9 +696,9 @@ func TestAppendListsPreserveNestedSourceReferencePresence(t *testing.T) {
|
|||||||
t.Fatalf("appendSpellLists() = %#v, %v; want present-empty source refs", spells, err)
|
t.Fatalf("appendSpellLists() = %#v, %v; want present-empty source refs", spells, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
npcs, err := appendNPCLists([]dnd.NPCList{{NPCs: []dnd.NPC{{SourceRefs: []source.SourceRef{}}}}})
|
npcs, err := appendNPCRegistries([]dnd.NPCRegistry{{NPCs: []dnd.NPC{{SourceRefs: []source.SourceRef{}}}}})
|
||||||
if err != nil || npcs.NPCs[0].SourceRefs == nil {
|
if err != nil || npcs.NPCs[0].SourceRefs == nil {
|
||||||
t.Fatalf("appendNPCLists() = %#v, %v; want present-empty source refs", npcs, err)
|
t.Fatalf("appendNPCRegistries() = %#v, %v; want present-empty source refs", npcs, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
turns, err := appendCombatTurnLists([]dnd.CombatTurnList{{CombatTurns: []dnd.CombatTurn{{SourceRefs: []source.SourceRef{}}}}})
|
turns, err := appendCombatTurnLists([]dnd.CombatTurnList{{CombatTurns: []dnd.CombatTurn{{SourceRefs: []source.SourceRef{}}}}})
|
||||||
|
|||||||
@@ -94,10 +94,10 @@ func registerValidators(registries pipeline.Registries) error {
|
|||||||
return alwaysreject.RegisterTyped[dnd.SpellList](registries.Validators, dnd.SpellListKind)
|
return alwaysreject.RegisterTyped[dnd.SpellList](registries.Validators, dnd.SpellListKind)
|
||||||
}},
|
}},
|
||||||
{name: "npc-list always accept validator", register: func() error {
|
{name: "npc-list always accept validator", register: func() error {
|
||||||
return alwaysaccept.RegisterTyped[dnd.NPCList](registries.Validators, dnd.NPCListKind)
|
return alwaysaccept.RegisterTyped[dnd.NPCRegistry](registries.Validators, dnd.NPCRegistryKind)
|
||||||
}},
|
}},
|
||||||
{name: "npc-list always reject validator", register: func() error {
|
{name: "npc-list always reject validator", register: func() error {
|
||||||
return alwaysreject.RegisterTyped[dnd.NPCList](registries.Validators, dnd.NPCListKind)
|
return alwaysreject.RegisterTyped[dnd.NPCRegistry](registries.Validators, dnd.NPCRegistryKind)
|
||||||
}},
|
}},
|
||||||
{name: "combat-turn-list always accept validator", register: func() error {
|
{name: "combat-turn-list always accept validator", register: func() error {
|
||||||
return alwaysaccept.RegisterTyped[dnd.CombatTurnList](registries.Validators, dnd.CombatTurnListKind)
|
return alwaysaccept.RegisterTyped[dnd.CombatTurnList](registries.Validators, dnd.CombatTurnListKind)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
const SpellListKind contracts.ArtifactKind = "dnd/spell-list"
|
const SpellListKind contracts.ArtifactKind = "dnd/spell-list"
|
||||||
|
|
||||||
const NPCListKind contracts.ArtifactKind = "dnd/npc-list"
|
const NPCRegistryKind contracts.ArtifactKind = "dnd/npc-registry"
|
||||||
|
|
||||||
const CombatTurnListKind contracts.ArtifactKind = "dnd/combat-turn-list"
|
const CombatTurnListKind contracts.ArtifactKind = "dnd/combat-turn-list"
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ type SpellCast struct {
|
|||||||
SourceRefs []source.SourceRef `json:"source_refs"`
|
SourceRefs []source.SourceRef `json:"source_refs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NPCList struct {
|
type NPCRegistry struct {
|
||||||
NPCs []NPC `json:"npcs"`
|
NPCs []NPC `json:"npcs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ func registryReferences(t *testing.T, names ...string) contracts.ReferenceSet {
|
|||||||
for index, name := range names {
|
for index, name := range names {
|
||||||
npcs[index] = dnd.NPC{ID: identity.DeriveID(name), Name: name, SourceRefs: []source.SourceRef{{SourceID: "other", StartUnitID: index + 1, EndUnitID: index + 1}}}
|
npcs[index] = dnd.NPC{ID: identity.DeriveID(name), Name: name, SourceRefs: []source.SourceRef{{SourceID: "other", StartUnitID: index + 1, EndUnitID: index + 1}}}
|
||||||
}
|
}
|
||||||
content, err := npccodec.New().Encode(dnd.NPCList{NPCs: npcs})
|
content, err := npccodec.New().Encode(dnd.NPCRegistry{NPCs: npcs})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ func registryReferences(t *testing.T, names ...string) contracts.ReferenceSet {
|
|||||||
for index, name := range names {
|
for index, name := range names {
|
||||||
npcs[index] = dnd.NPC{ID: identity.DeriveID(name), Name: name, SourceRefs: []source.SourceRef{{SourceID: "other-session", StartUnitID: index + 1, EndUnitID: index + 1}}}
|
npcs[index] = dnd.NPC{ID: identity.DeriveID(name), Name: name, SourceRefs: []source.SourceRef{{SourceID: "other-session", StartUnitID: index + 1, EndUnitID: index + 1}}}
|
||||||
}
|
}
|
||||||
content, err := npccodec.New().Encode(dnd.NPCList{NPCs: npcs})
|
content, err := npccodec.New().Encode(dnd.NPCRegistry{NPCs: npcs})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ func registryReferences(t *testing.T, names ...string) contracts.ReferenceSet {
|
|||||||
for index, name := range names {
|
for index, name := range names {
|
||||||
npcs[index] = dnd.NPC{ID: identity.DeriveID(name), Name: name, SourceRefs: []source.SourceRef{{SourceID: "other-session", StartUnitID: index + 1, EndUnitID: index + 1}}}
|
npcs[index] = dnd.NPC{ID: identity.DeriveID(name), Name: name, SourceRefs: []source.SourceRef{{SourceID: "other-session", StartUnitID: index + 1, EndUnitID: index + 1}}}
|
||||||
}
|
}
|
||||||
content, err := npccodec.New().Encode(dnd.NPCList{NPCs: npcs})
|
content, err := npccodec.New().Encode(dnd.NPCRegistry{NPCs: npcs})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const (
|
|||||||
type Options struct{}
|
type Options struct{}
|
||||||
type Validator struct{}
|
type Validator struct{}
|
||||||
|
|
||||||
var _ contracts.TypedValidator[dnd.NPCList] = (*Validator)(nil)
|
var _ contracts.TypedValidator[dnd.NPCRegistry] = (*Validator)(nil)
|
||||||
var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil)
|
var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil)
|
||||||
|
|
||||||
func New(Options) *Validator { return &Validator{} }
|
func New(Options) *Validator { return &Validator{} }
|
||||||
@@ -36,7 +36,7 @@ func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
|
|||||||
return []pipeline.CheckpointFingerprint{{Name: "policy", Value: policy}}
|
return []pipeline.CheckpointFingerprint{{Name: "policy", Value: policy}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.NPCList]) (contracts.ValidationResult, error) {
|
func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.NPCRegistry]) (contracts.ValidationResult, error) {
|
||||||
if err := npcshape.Validate(req.Value); err != nil {
|
if err := npcshape.Validate(req.Value); err != nil {
|
||||||
return contracts.ValidationResult{Approved: true}, nil
|
return contracts.ValidationResult{Approved: true}, nil
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@ func Spec() pipeline.ValidatorSpec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Register(registry *pipeline.ValidatorRegistry) error {
|
func Register(registry *pipeline.ValidatorRegistry) error {
|
||||||
return pipeline.RegisterTypedValidatorBuilder(registry, dnd.NPCListKind, Spec(), validateOptions, func(request pipeline.BuildRequest) (contracts.TypedValidator[dnd.NPCList], error) {
|
return pipeline.RegisterTypedValidatorBuilder(registry, dnd.NPCRegistryKind, Spec(), validateOptions, func(request pipeline.BuildRequest) (contracts.TypedValidator[dnd.NPCRegistry], error) {
|
||||||
options, err := DecodeOptions(request.Options)
|
options, err := DecodeOptions(request.Options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -43,13 +43,13 @@ func TestValidatorContractAndRegistration(t *testing.T) {
|
|||||||
|
|
||||||
func TestValidatorDefersShapeAndRejectsIdentityIssues(t *testing.T) {
|
func TestValidatorDefersShapeAndRejectsIdentityIssues(t *testing.T) {
|
||||||
validator := New(Options{})
|
validator := New(Options{})
|
||||||
shapeInvalid := dnd.NPCList{NPCs: []dnd.NPC{{Name: "missing evidence"}}}
|
shapeInvalid := dnd.NPCRegistry{NPCs: []dnd.NPC{{Name: "missing evidence"}}}
|
||||||
result, err := validator.Validate(context.Background(), validationRequest(shapeInvalid))
|
result, err := validator.Validate(context.Background(), validationRequest(shapeInvalid))
|
||||||
if err != nil || !result.Approved {
|
if err != nil || !result.Approved {
|
||||||
t.Fatalf("shape-invalid result = %#v, error = %v, want deferred approval", result, err)
|
t.Fatalf("shape-invalid result = %#v, error = %v, want deferred approval", result, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
value := validNPCList(2)
|
value := validNPCRegistry(2)
|
||||||
value.NPCs[0].ID = "not-an-id"
|
value.NPCs[0].ID = "not-an-id"
|
||||||
value.NPCs[1].Name = value.NPCs[0].Name
|
value.NPCs[1].Name = value.NPCs[0].Name
|
||||||
value.NPCs[1].ID = value.NPCs[0].ID
|
value.NPCs[1].ID = value.NPCs[0].ID
|
||||||
@@ -65,7 +65,7 @@ func TestValidatorDefersShapeAndRejectsIdentityIssues(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestValidatorBoundsUnicodeDiagnostics(t *testing.T) {
|
func TestValidatorBoundsUnicodeDiagnostics(t *testing.T) {
|
||||||
value := validNPCList(30)
|
value := validNPCRegistry(30)
|
||||||
for index := range value.NPCs {
|
for index := range value.NPCs {
|
||||||
value.NPCs[index].ID = fmt.Sprintf("bad-%d", index)
|
value.NPCs[index].ID = fmt.Sprintf("bad-%d", index)
|
||||||
value.NPCs[index].Name = strings.Repeat("火", 140) + fmt.Sprintf("-%d", index)
|
value.NPCs[index].Name = strings.Repeat("火", 140) + fmt.Sprintf("-%d", index)
|
||||||
@@ -82,12 +82,12 @@ func TestValidatorBoundsUnicodeDiagnostics(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func validationRequest(value dnd.NPCList) contracts.TypedValidationRequest[dnd.NPCList] {
|
func validationRequest(value dnd.NPCRegistry) contracts.TypedValidationRequest[dnd.NPCRegistry] {
|
||||||
return contracts.TypedValidationRequest[dnd.NPCList]{Value: value}
|
return contracts.TypedValidationRequest[dnd.NPCRegistry]{Value: value}
|
||||||
}
|
}
|
||||||
|
|
||||||
func validNPCList(count int) dnd.NPCList {
|
func validNPCRegistry(count int) dnd.NPCRegistry {
|
||||||
value := dnd.NPCList{NPCs: make([]dnd.NPC, count)}
|
value := dnd.NPCRegistry{NPCs: make([]dnd.NPC, count)}
|
||||||
for index := range value.NPCs {
|
for index := range value.NPCs {
|
||||||
name := fmt.Sprintf("NPC %d", index)
|
name := fmt.Sprintf("NPC %d", index)
|
||||||
value.NPCs[index] = dnd.NPC{ID: "npc:sha256:0000000000000000000000000000000000000000000000000000000000000000", Name: name, SourceRefs: []source.SourceRef{sourceRefForTest()}}
|
value.NPCs[index] = dnd.NPC{ID: "npc:sha256:0000000000000000000000000000000000000000000000000000000000000000", Name: name, SourceRefs: []source.SourceRef{sourceRefForTest()}}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const (
|
|||||||
type Options struct{}
|
type Options struct{}
|
||||||
type Validator struct{}
|
type Validator struct{}
|
||||||
|
|
||||||
var _ contracts.TypedValidator[dnd.NPCList] = (*Validator)(nil)
|
var _ contracts.TypedValidator[dnd.NPCRegistry] = (*Validator)(nil)
|
||||||
var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil)
|
var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil)
|
||||||
|
|
||||||
func New(Options) *Validator { return &Validator{} }
|
func New(Options) *Validator { return &Validator{} }
|
||||||
@@ -32,7 +32,7 @@ func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
|
|||||||
return []pipeline.CheckpointFingerprint{{Name: "policy", Value: policy}}
|
return []pipeline.CheckpointFingerprint{{Name: "policy", Value: policy}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.NPCList]) (contracts.ValidationResult, error) {
|
func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.NPCRegistry]) (contracts.ValidationResult, error) {
|
||||||
issues := issuesFor(req.Value)
|
issues := issuesFor(req.Value)
|
||||||
if len(issues) > 0 {
|
if len(issues) > 0 {
|
||||||
return rejection(diagnostics.Aggregate("invalid NPC shape", issues)), nil
|
return rejection(diagnostics.Aggregate("invalid NPC shape", issues)), nil
|
||||||
@@ -40,7 +40,7 @@ func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationReq
|
|||||||
return contracts.ValidationResult{Approved: true}, nil
|
return contracts.ValidationResult{Approved: true}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Validate(value dnd.NPCList) error {
|
func Validate(value dnd.NPCRegistry) error {
|
||||||
issues := issuesFor(value)
|
issues := issuesFor(value)
|
||||||
if len(issues) == 0 {
|
if len(issues) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -48,7 +48,7 @@ func Validate(value dnd.NPCList) error {
|
|||||||
return fmt.Errorf("%s", diagnostics.Aggregate("invalid NPC shape", issues))
|
return fmt.Errorf("%s", diagnostics.Aggregate("invalid NPC shape", issues))
|
||||||
}
|
}
|
||||||
|
|
||||||
func issuesFor(value dnd.NPCList) []string {
|
func issuesFor(value dnd.NPCRegistry) []string {
|
||||||
issues := make([]string, 0)
|
issues := make([]string, 0)
|
||||||
if value.NPCs == nil {
|
if value.NPCs == nil {
|
||||||
return []string{"npcs must be present"}
|
return []string{"npcs must be present"}
|
||||||
@@ -73,7 +73,7 @@ func Spec() pipeline.ValidatorSpec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Register(registry *pipeline.ValidatorRegistry) error {
|
func Register(registry *pipeline.ValidatorRegistry) error {
|
||||||
return pipeline.RegisterTypedValidatorBuilder(registry, dnd.NPCListKind, Spec(), validateOptions, func(request pipeline.BuildRequest) (contracts.TypedValidator[dnd.NPCList], error) {
|
return pipeline.RegisterTypedValidatorBuilder(registry, dnd.NPCRegistryKind, Spec(), validateOptions, func(request pipeline.BuildRequest) (contracts.TypedValidator[dnd.NPCRegistry], error) {
|
||||||
options, err := DecodeOptions(request.Options)
|
options, err := DecodeOptions(request.Options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -13,21 +13,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestValidatorApprovesWellFormedNPCPayload(t *testing.T) {
|
func TestValidatorApprovesWellFormedNPCPayload(t *testing.T) {
|
||||||
result, err := New(Options{}).Validate(context.Background(), requestWithValue(validNPCList()))
|
result, err := New(Options{}).Validate(context.Background(), requestWithValue(validNPCRegistry()))
|
||||||
if err != nil || !result.Approved {
|
if err != nil || !result.Approved {
|
||||||
t.Fatalf("Validate() = %#v, %v; want approval", result, err)
|
t.Fatalf("Validate() = %#v, %v; want approval", result, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidatorRejectsRequiredShapeValues(t *testing.T) {
|
func TestValidatorRejectsRequiredShapeValues(t *testing.T) {
|
||||||
value := validNPCList()
|
value := validNPCRegistry()
|
||||||
value.NPCs[0].Name = ""
|
value.NPCs[0].Name = ""
|
||||||
result, err := New(Options{}).Validate(context.Background(), requestWithValue(value))
|
result, err := New(Options{}).Validate(context.Background(), requestWithValue(value))
|
||||||
if err != nil || result.Approved || result.ReasonCode != ReasonCode || !strings.Contains(result.Message, "name must not be empty") {
|
if err != nil || result.Approved || result.ReasonCode != ReasonCode || !strings.Contains(result.Message, "name must not be empty") {
|
||||||
t.Fatalf("Validate() = %#v, %v; want bounded shape rejection", result, err)
|
t.Fatalf("Validate() = %#v, %v; want bounded shape rejection", result, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
missing := dnd.NPCList{}
|
missing := dnd.NPCRegistry{}
|
||||||
result, err = New(Options{}).Validate(context.Background(), requestWithValue(missing))
|
result, err = New(Options{}).Validate(context.Background(), requestWithValue(missing))
|
||||||
if err != nil || result.Approved || result.ReasonCode != ReasonCode {
|
if err != nil || result.Approved || result.ReasonCode != ReasonCode {
|
||||||
t.Fatalf("missing Validate() = %#v, %v; want shape rejection", result, err)
|
t.Fatalf("missing Validate() = %#v, %v; want shape rejection", result, err)
|
||||||
@@ -35,7 +35,7 @@ func TestValidatorRejectsRequiredShapeValues(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestValidatorBoundsDiagnosticsAndQuotesUnicode(t *testing.T) {
|
func TestValidatorBoundsDiagnosticsAndQuotesUnicode(t *testing.T) {
|
||||||
value := dnd.NPCList{NPCs: make([]dnd.NPC, 24)}
|
value := dnd.NPCRegistry{NPCs: make([]dnd.NPC, 24)}
|
||||||
long := strings.Repeat("火", 220) + "\n\t"
|
long := strings.Repeat("火", 220) + "\n\t"
|
||||||
for index := range value.NPCs {
|
for index := range value.NPCs {
|
||||||
value.NPCs[index] = dnd.NPC{ID: "", Name: long, SourceRefs: []source.SourceRef{}}
|
value.NPCs[index] = dnd.NPC{ID: "", Name: long, SourceRefs: []source.SourceRef{}}
|
||||||
@@ -66,7 +66,7 @@ func TestValidatorSpecCheckpointAndRegistration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestValidatorDoesNotMutateValue(t *testing.T) {
|
func TestValidatorDoesNotMutateValue(t *testing.T) {
|
||||||
value := validNPCList()
|
value := validNPCRegistry()
|
||||||
before := value
|
before := value
|
||||||
_, err := New(Options{}).Validate(context.Background(), requestWithValue(value))
|
_, err := New(Options{}).Validate(context.Background(), requestWithValue(value))
|
||||||
if err != nil || value.NPCs[0].Name != before.NPCs[0].Name {
|
if err != nil || value.NPCs[0].Name != before.NPCs[0].Name {
|
||||||
@@ -74,12 +74,12 @@ func TestValidatorDoesNotMutateValue(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestWithValue(value dnd.NPCList) contracts.TypedValidationRequest[dnd.NPCList] {
|
func requestWithValue(value dnd.NPCRegistry) contracts.TypedValidationRequest[dnd.NPCRegistry] {
|
||||||
return contracts.TypedValidationRequest[dnd.NPCList]{Value: value}
|
return contracts.TypedValidationRequest[dnd.NPCRegistry]{Value: value}
|
||||||
}
|
}
|
||||||
|
|
||||||
func validNPCList() dnd.NPCList {
|
func validNPCRegistry() dnd.NPCRegistry {
|
||||||
return dnd.NPCList{NPCs: []dnd.NPC{{
|
return dnd.NPCRegistry{NPCs: []dnd.NPC{{
|
||||||
ID: "candidate", Name: "Mira Thorn",
|
ID: "candidate", Name: "Mira Thorn",
|
||||||
SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}},
|
SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}},
|
||||||
}}}
|
}}}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const (
|
|||||||
type Options struct{}
|
type Options struct{}
|
||||||
type Validator struct{}
|
type Validator struct{}
|
||||||
|
|
||||||
var _ contracts.TypedValidator[dnd.NPCList] = (*Validator)(nil)
|
var _ contracts.TypedValidator[dnd.NPCRegistry] = (*Validator)(nil)
|
||||||
var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil)
|
var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil)
|
||||||
|
|
||||||
func New(Options) *Validator { return &Validator{} }
|
func New(Options) *Validator { return &Validator{} }
|
||||||
@@ -33,7 +33,7 @@ func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
|
|||||||
return []pipeline.CheckpointFingerprint{{Name: "policy", Value: policy}}
|
return []pipeline.CheckpointFingerprint{{Name: "policy", Value: policy}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.NPCList]) (contracts.ValidationResult, error) {
|
func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.NPCRegistry]) (contracts.ValidationResult, error) {
|
||||||
if err := npcshape.Validate(req.Value); err != nil {
|
if err := npcshape.Validate(req.Value); err != nil {
|
||||||
return contracts.ValidationResult{Approved: true}, nil
|
return contracts.ValidationResult{Approved: true}, nil
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ func Spec() pipeline.ValidatorSpec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Register(registry *pipeline.ValidatorRegistry) error {
|
func Register(registry *pipeline.ValidatorRegistry) error {
|
||||||
return pipeline.RegisterTypedValidatorBuilder(registry, dnd.NPCListKind, Spec(), validateOptions, func(request pipeline.BuildRequest) (contracts.TypedValidator[dnd.NPCList], error) {
|
return pipeline.RegisterTypedValidatorBuilder(registry, dnd.NPCRegistryKind, Spec(), validateOptions, func(request pipeline.BuildRequest) (contracts.TypedValidator[dnd.NPCRegistry], error) {
|
||||||
options, err := DecodeOptions(request.Options)
|
options, err := DecodeOptions(request.Options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestValidatorApprovesValidSourceReferences(t *testing.T) {
|
func TestValidatorApprovesValidSourceReferences(t *testing.T) {
|
||||||
result, err := New(Options{}).Validate(context.Background(), requestWithValue(validDocument(), validNPCList()))
|
result, err := New(Options{}).Validate(context.Background(), requestWithValue(validDocument(), validNPCRegistry()))
|
||||||
if err != nil || !result.Approved || len(result.Warnings) != 0 {
|
if err != nil || !result.Approved || len(result.Warnings) != 0 {
|
||||||
t.Fatalf("Validate() = %#v, %v; want approval", result, err)
|
t.Fatalf("Validate() = %#v, %v; want approval", result, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidatorRejectsInvalidSourceReferences(t *testing.T) {
|
func TestValidatorRejectsInvalidSourceReferences(t *testing.T) {
|
||||||
value := validNPCList()
|
value := validNPCRegistry()
|
||||||
value.NPCs[0].SourceRefs = []source.SourceRef{
|
value.NPCs[0].SourceRefs = []source.SourceRef{
|
||||||
{SourceID: "foreign", StartUnitID: 1, EndUnitID: 1},
|
{SourceID: "foreign", StartUnitID: 1, EndUnitID: 1},
|
||||||
{SourceID: "session", StartUnitID: 2, EndUnitID: 1},
|
{SourceID: "session", StartUnitID: 2, EndUnitID: 1},
|
||||||
@@ -34,7 +34,7 @@ func TestValidatorRejectsInvalidSourceReferences(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestValidatorDefersMalformedShape(t *testing.T) {
|
func TestValidatorDefersMalformedShape(t *testing.T) {
|
||||||
value := dnd.NPCList{NPCs: []dnd.NPC{{Name: "Mira Thorn"}}}
|
value := dnd.NPCRegistry{NPCs: []dnd.NPC{{Name: "Mira Thorn"}}}
|
||||||
result, err := New(Options{}).Validate(context.Background(), requestWithValue(validDocument(), value))
|
result, err := New(Options{}).Validate(context.Background(), requestWithValue(validDocument(), value))
|
||||||
if err != nil || !result.Approved || result.ReasonCode != "" || result.Message != "" {
|
if err != nil || !result.Approved || result.ReasonCode != "" || result.Message != "" {
|
||||||
t.Fatalf("Validate() = %#v, %v; want shape deferral", result, err)
|
t.Fatalf("Validate() = %#v, %v; want shape deferral", result, err)
|
||||||
@@ -42,7 +42,7 @@ func TestValidatorDefersMalformedShape(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestValidatorBoundsDiagnosticsAndHandlesMissingDocument(t *testing.T) {
|
func TestValidatorBoundsDiagnosticsAndHandlesMissingDocument(t *testing.T) {
|
||||||
value := validNPCList()
|
value := validNPCRegistry()
|
||||||
value.NPCs[0].SourceRefs = make([]source.SourceRef, 24)
|
value.NPCs[0].SourceRefs = make([]source.SourceRef, 24)
|
||||||
for index := range value.NPCs[0].SourceRefs {
|
for index := range value.NPCs[0].SourceRefs {
|
||||||
value.NPCs[0].SourceRefs[index] = source.SourceRef{SourceID: strings.Repeat("火", 220) + "\n\t", StartUnitID: index + 1, EndUnitID: index + 1}
|
value.NPCs[0].SourceRefs[index] = source.SourceRef{SourceID: strings.Repeat("火", 220) + "\n\t", StartUnitID: index + 1, EndUnitID: index + 1}
|
||||||
@@ -72,8 +72,8 @@ func TestValidatorSpecCheckpointAndRegistration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestWithValue(doc *source.SourceDocument, value dnd.NPCList) contracts.TypedValidationRequest[dnd.NPCList] {
|
func requestWithValue(doc *source.SourceDocument, value dnd.NPCRegistry) contracts.TypedValidationRequest[dnd.NPCRegistry] {
|
||||||
return contracts.TypedValidationRequest[dnd.NPCList]{Source: doc, Value: value}
|
return contracts.TypedValidationRequest[dnd.NPCRegistry]{Source: doc, Value: value}
|
||||||
}
|
}
|
||||||
|
|
||||||
func validDocument() *source.SourceDocument {
|
func validDocument() *source.SourceDocument {
|
||||||
@@ -83,6 +83,6 @@ func validDocument() *source.SourceDocument {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func validNPCList() dnd.NPCList {
|
func validNPCRegistry() dnd.NPCRegistry {
|
||||||
return dnd.NPCList{NPCs: []dnd.NPC{{ID: "candidate", Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 2}}}}}
|
return dnd.NPCRegistry{NPCs: []dnd.NPC{{ID: "candidate", Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 2}}}}}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const (
|
|||||||
type Options struct{}
|
type Options struct{}
|
||||||
type Validator struct{}
|
type Validator struct{}
|
||||||
|
|
||||||
var _ contracts.TypedValidator[dnd.NPCList] = (*Validator)(nil)
|
var _ contracts.TypedValidator[dnd.NPCRegistry] = (*Validator)(nil)
|
||||||
var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil)
|
var _ pipeline.CheckpointFingerprintProvider = (*Validator)(nil)
|
||||||
|
|
||||||
func New(Options) *Validator { return &Validator{} }
|
func New(Options) *Validator { return &Validator{} }
|
||||||
@@ -33,7 +33,7 @@ func (v *Validator) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
|
|||||||
return []pipeline.CheckpointFingerprint{{Name: "policy", Value: policy}}
|
return []pipeline.CheckpointFingerprint{{Name: "policy", Value: policy}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.NPCList]) (contracts.ValidationResult, error) {
|
func (v *Validator) Validate(_ context.Context, req contracts.TypedValidationRequest[dnd.NPCRegistry]) (contracts.ValidationResult, error) {
|
||||||
if err := npcshape.Validate(req.Value); err != nil {
|
if err := npcshape.Validate(req.Value); err != nil {
|
||||||
return contracts.ValidationResult{Approved: true}, nil
|
return contracts.ValidationResult{Approved: true}, nil
|
||||||
}
|
}
|
||||||
@@ -72,7 +72,7 @@ func Spec() pipeline.ValidatorSpec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Register(registry *pipeline.ValidatorRegistry) error {
|
func Register(registry *pipeline.ValidatorRegistry) error {
|
||||||
return pipeline.RegisterTypedValidatorBuilder(registry, dnd.NPCListKind, Spec(), validateOptions, func(request pipeline.BuildRequest) (contracts.TypedValidator[dnd.NPCList], error) {
|
return pipeline.RegisterTypedValidatorBuilder(registry, dnd.NPCRegistryKind, Spec(), validateOptions, func(request pipeline.BuildRequest) (contracts.TypedValidator[dnd.NPCRegistry], error) {
|
||||||
options, err := DecodeOptions(request.Options)
|
options, err := DecodeOptions(request.Options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestValidatorMatchesCanonicalNamesWithUnicodeVariants(t *testing.T) {
|
func TestValidatorMatchesCanonicalNamesWithUnicodeVariants(t *testing.T) {
|
||||||
value := dnd.NPCList{NPCs: []dnd.NPC{
|
value := dnd.NPCRegistry{NPCs: []dnd.NPC{
|
||||||
{ID: "one", Name: "O'Rin Thorn", SourceRefs: []source.SourceRef{
|
{ID: "one", Name: "O'Rin Thorn", SourceRefs: []source.SourceRef{
|
||||||
{SourceID: "session", StartUnitID: 2, EndUnitID: 2},
|
{SourceID: "session", StartUnitID: 2, EndUnitID: 2},
|
||||||
{SourceID: "session", StartUnitID: 1, EndUnitID: 2},
|
{SourceID: "session", StartUnitID: 1, EndUnitID: 2},
|
||||||
@@ -25,17 +25,17 @@ func TestValidatorMatchesCanonicalNamesWithUnicodeVariants(t *testing.T) {
|
|||||||
{ID: 1, Kind: "message", Text: " o’rin\u2003thorn appears."},
|
{ID: 1, Kind: "message", Text: " o’rin\u2003thorn appears."},
|
||||||
{ID: 2, Kind: "message", Text: "The greencloak watches."},
|
{ID: 2, Kind: "message", Text: "The greencloak watches."},
|
||||||
}}
|
}}
|
||||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCList]{Source: doc, Value: value})
|
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCRegistry]{Source: doc, Value: value})
|
||||||
if err != nil || !result.Approved || len(result.Warnings) != 0 {
|
if err != nil || !result.Approved || len(result.Warnings) != 0 {
|
||||||
t.Fatalf("Validate() = %#v, %v; want canonical-name relatedness approval", result, err)
|
t.Fatalf("Validate() = %#v, %v; want canonical-name relatedness approval", result, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidatorWarnsAtMostOncePerNPCForUnrelatedCitations(t *testing.T) {
|
func TestValidatorWarnsAtMostOncePerNPCForUnrelatedCitations(t *testing.T) {
|
||||||
value := dnd.NPCList{NPCs: []dnd.NPC{
|
value := dnd.NPCRegistry{NPCs: []dnd.NPC{
|
||||||
{ID: "one", Name: "Missing\nName", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}, {SourceID: "session", StartUnitID: 1, EndUnitID: 1}}},
|
{ID: "one", Name: "Missing\nName", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}, {SourceID: "session", StartUnitID: 1, EndUnitID: 1}}},
|
||||||
}}
|
}}
|
||||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCList]{Source: relatednessDocument(), Value: value})
|
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCRegistry]{Source: relatednessDocument(), Value: value})
|
||||||
if err != nil || !result.Approved || len(result.Warnings) != 1 {
|
if err != nil || !result.Approved || len(result.Warnings) != 1 {
|
||||||
t.Fatalf("Validate() = %#v, %v; want one warning", result, err)
|
t.Fatalf("Validate() = %#v, %v; want one warning", result, err)
|
||||||
}
|
}
|
||||||
@@ -46,33 +46,33 @@ func TestValidatorWarnsAtMostOncePerNPCForUnrelatedCitations(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestValidatorDoesNotMatchShortNameSubstring(t *testing.T) {
|
func TestValidatorDoesNotMatchShortNameSubstring(t *testing.T) {
|
||||||
value := dnd.NPCList{NPCs: []dnd.NPC{{
|
value := dnd.NPCRegistry{NPCs: []dnd.NPC{{
|
||||||
ID: "one", Name: "Art", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}},
|
ID: "one", Name: "Art", 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: "A cart rolls past."}}}
|
doc := &source.SourceDocument{ID: "session", Kind: "transcript", Format: "application/json", Digest: "sha256:session", Units: []source.SourceUnit{{ID: 1, Kind: "message", Text: "A cart rolls past."}}}
|
||||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCList]{Source: doc, Value: value})
|
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCRegistry]{Source: doc, Value: value})
|
||||||
if err != nil || !result.Approved || len(result.Warnings) != 1 {
|
if err != nil || !result.Approved || len(result.Warnings) != 1 {
|
||||||
t.Fatalf("Validate() = %#v, %v; want short-name boundary warning", result, err)
|
t.Fatalf("Validate() = %#v, %v; want short-name boundary warning", result, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidatorDefersMalformedShapeAndInvalidRangesDoNotPanic(t *testing.T) {
|
func TestValidatorDefersMalformedShapeAndInvalidRangesDoNotPanic(t *testing.T) {
|
||||||
invalidShape := dnd.NPCList{NPCs: []dnd.NPC{{Name: "Mira Thorn"}}}
|
invalidShape := dnd.NPCRegistry{NPCs: []dnd.NPC{{Name: "Mira Thorn"}}}
|
||||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCList]{Source: relatednessDocument(), Value: invalidShape})
|
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCRegistry]{Source: relatednessDocument(), Value: invalidShape})
|
||||||
if err != nil || !result.Approved || len(result.Warnings) != 0 {
|
if err != nil || !result.Approved || len(result.Warnings) != 0 {
|
||||||
t.Fatalf("shape deferral = %#v, %v; want approval without warning", result, err)
|
t.Fatalf("shape deferral = %#v, %v; want approval without warning", result, err)
|
||||||
}
|
}
|
||||||
invalidRange := dnd.NPCList{NPCs: []dnd.NPC{{ID: "one", Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 99, EndUnitID: 99}}}}}
|
invalidRange := dnd.NPCRegistry{NPCs: []dnd.NPC{{ID: "one", Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 99, EndUnitID: 99}}}}}
|
||||||
result, err = New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCList]{Source: relatednessDocument(), Value: invalidRange})
|
result, err = New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCRegistry]{Source: relatednessDocument(), Value: invalidRange})
|
||||||
if err != nil || !result.Approved || len(result.Warnings) != 0 {
|
if err != nil || !result.Approved || len(result.Warnings) != 0 {
|
||||||
t.Fatalf("invalid-range relatedness = %#v, %v; want approval without warning", result, err)
|
t.Fatalf("invalid-range relatedness = %#v, %v; want approval without warning", result, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidatorUsesOnlyTranscriptEvidenceAndRegistersPolicy(t *testing.T) {
|
func TestValidatorUsesOnlyTranscriptEvidenceAndRegistersPolicy(t *testing.T) {
|
||||||
value := dnd.NPCList{NPCs: []dnd.NPC{{ID: "one", Name: "Opaque NPC", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}}}
|
value := dnd.NPCRegistry{NPCs: []dnd.NPC{{ID: "one", Name: "Opaque NPC", SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: 1, EndUnitID: 1}}}}}
|
||||||
references := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{"glossary": {Items: []contracts.ReferenceItem{{Content: []byte("Opaque NPC")}}}}}
|
references := contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{"glossary": {Items: []contracts.ReferenceItem{{Content: []byte("Opaque NPC")}}}}}
|
||||||
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCList]{Source: relatednessDocument(), References: references, Value: value})
|
result, err := New(Options{}).Validate(context.Background(), contracts.TypedValidationRequest[dnd.NPCRegistry]{Source: relatednessDocument(), References: references, Value: value})
|
||||||
if err != nil || len(result.Warnings) != 1 {
|
if err != nil || len(result.Warnings) != 1 {
|
||||||
t.Fatalf("reference-only relatedness = %#v, %v; want warning", result, err)
|
t.Fatalf("reference-only relatedness = %#v, %v; want warning", result, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ type combatNPCPayload struct {
|
|||||||
|
|
||||||
func combatTestNPCPayload(t *testing.T) combatNPCPayload {
|
func combatTestNPCPayload(t *testing.T) combatNPCPayload {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
value := dnd.NPCList{NPCs: []dnd.NPC{
|
value := dnd.NPCRegistry{NPCs: []dnd.NPC{
|
||||||
{ID: identity.DeriveID("Mira Thorn"), Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: "prior-npc-session", StartUnitID: 1, EndUnitID: 1}}},
|
{ID: identity.DeriveID("Mira Thorn"), Name: "Mira Thorn", SourceRefs: []source.SourceRef{{SourceID: "prior-npc-session", StartUnitID: 1, EndUnitID: 1}}},
|
||||||
{ID: identity.DeriveID("Hooded Guard"), Name: "Hooded Guard", SourceRefs: []source.SourceRef{{SourceID: "prior-npc-session", StartUnitID: 3, EndUnitID: 3}}},
|
{ID: identity.DeriveID("Hooded Guard"), Name: "Hooded Guard", SourceRefs: []source.SourceRef{{SourceID: "prior-npc-session", StartUnitID: 3, EndUnitID: 3}}},
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func TestNPCOutputGroundsSpellAndCombatConsumersThroughOneOperation(t *testing.T
|
|||||||
}
|
}
|
||||||
for name, value := range map[string]string{
|
for name, value := range map[string]string{
|
||||||
"extract:npcs:dnd/npcs:mapping_policy": "dnd.npcs.extract_mapping.v2",
|
"extract:npcs:dnd/npcs:mapping_policy": "dnd.npcs.extract_mapping.v2",
|
||||||
"normalize:npcs:dnd/npcs:identity_policy": "dnd.npcs.identity.v1",
|
"normalize:npcs:dnd/npcs:identity_policy": "dnd.npc_registry.identity.v1",
|
||||||
"normalize:npcs:dnd/npcs:normalization_policy": "dnd.npcs.normalize.v3",
|
"normalize:npcs:dnd/npcs:normalization_policy": "dnd.npcs.normalize.v3",
|
||||||
"normalize:npcs:dnd/npcs:semantic_context_policy": "dnd.entity_reconcile.context.v1:2",
|
"normalize:npcs:dnd/npcs:semantic_context_policy": "dnd.entity_reconcile.context.v1:2",
|
||||||
"extract:spells:dnd/spells:mapping_policy": "dnd.spells.extract_mapping.v2",
|
"extract:spells:dnd/spells:mapping_policy": "dnd.spells.extract_mapping.v2",
|
||||||
|
|||||||
Reference in New Issue
Block a user