Retire annotated D&D scene plans
This commit is contained in:
@@ -177,17 +177,16 @@ window settings used by `Plan`.
|
|||||||
|
|
||||||
The scene chunker prepares a structured Scriptorium request from the full
|
The scene chunker prepares a structured Scriptorium request from the full
|
||||||
transcript, session, and optional D&D reference inputs. It validates the model's
|
transcript, session, and optional D&D reference inputs. It validates the model's
|
||||||
scene boundaries against source-unit IDs and converts them into deterministic
|
inclusive source-unit endpoints against document position and converts them
|
||||||
plan ranges with optional scene annotations. Preparation injects the shared
|
into deterministic plan ranges. Preparation injects the shared structured LLM
|
||||||
structured LLM client into the chunker; `Plan`
|
client into the chunker; `Plan` supplies only the run-specific profile, session,
|
||||||
supplies only the run-specific profile, session, source, references, and
|
source, references, and metadata.
|
||||||
metadata.
|
|
||||||
|
|
||||||
Scene validation requires sequential, contiguous, non-overlapping coverage from
|
Scene validation requires sequential, contiguous, non-overlapping coverage from
|
||||||
the first source unit through the last. Scene descriptions, boundaries,
|
the first source unit through the last. Its private response contains only the
|
||||||
confidence, and participants are module-owned annotations. Boundary caveats
|
boundary endpoints; the accepted plan has no D&D-specific annotations and
|
||||||
become warnings. Malformed
|
produces no boundary warnings. Malformed structured output is returned as an
|
||||||
structured output is returned as an error; there is no fallback chunker.
|
error; there is no fallback chunker.
|
||||||
|
|
||||||
The package embeds its prompt and response schema and reports their non-secret
|
The package embeds its prompt and response schema and reports their non-secret
|
||||||
identity and hashes through singleton module metadata. Shared D&D assets supply
|
identity and hashes through singleton module metadata. Shared D&D assets supply
|
||||||
|
|||||||
@@ -16,12 +16,8 @@ not as committed release dates.
|
|||||||
validator, and normalizer development. Treat model-quality review as an
|
validator, and normalizer development. Treat model-quality review as an
|
||||||
iterative human evaluation aid, not a deterministic correctness gate.
|
iterative human evaluation aid, not a deterministic correctness gate.
|
||||||
|
|
||||||
### Minimize And Use D&D Scene Chunking
|
### Use D&D Scene Chunking
|
||||||
|
|
||||||
- Reduce the D&D scene chunker toward its narrow responsibility: identifying
|
|
||||||
coherent scene boundaries. Retain boundary confidence or caveats only when a
|
|
||||||
demonstrated validator or operator workflow consumes them; move title,
|
|
||||||
summary, scene kind, and participant duties to dedicated artifacts.
|
|
||||||
- Allow the combat extractor to no-op for chunks classified as non-combat only
|
- Allow the combat extractor to no-op for chunks classified as non-combat only
|
||||||
after the scene-description artifact can be supplied through an explicit
|
after the scene-description artifact can be supplied through an explicit
|
||||||
ordered dependency. Do not make generic chunk materialization depend on a D&D
|
ordered dependency. Do not make generic chunk materialization depend on a D&D
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Minimal D&D Scene Chunking
|
# Minimal D&D Scene Chunking
|
||||||
|
|
||||||
Status: Proposed
|
Status: Implemented
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
|
|||||||
@@ -458,7 +458,7 @@ func TestProductionNormalizeValidatorOverrideRemainsAuthoritative(t *testing.T)
|
|||||||
t.Fatalf("resolved validator chains = %#v, want normalize chain for %q", effective.ResolvedPipeline.ValidatorChains, spellnormalize.Key)
|
t.Fatalf("resolved validator chains = %#v, want normalize chain for %q", effective.ResolvedPipeline.ValidatorChains, spellnormalize.Key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProductionSceneRunRecordsChunkerWarningsAndProvenance(t *testing.T) {
|
func TestProductionSceneRunRecordsAnnotationFreeChunkPlanAndProvenance(t *testing.T) {
|
||||||
outputRoot := filepath.Join(t.TempDir(), "output")
|
outputRoot := filepath.Join(t.TempDir(), "output")
|
||||||
configPath := writeProductionContractConfig(t, productionRunConfig(outputRoot, "dnd/scenes"))
|
configPath := writeProductionContractConfig(t, productionRunConfig(outputRoot, "dnd/scenes"))
|
||||||
fake := &productionFakeLLMClient{}
|
fake := &productionFakeLLMClient{}
|
||||||
@@ -505,24 +505,17 @@ func TestProductionSceneRunRecordsChunkerWarningsAndProvenance(t *testing.T) {
|
|||||||
if len(chunkMap.Chunks) != 1 || chunkMap.Chunks[0].ID != "chunk-000001" || chunkMap.Chunks[0].Index != 0 || chunkMap.Chunks[0].SourceRef.SourceID != "session-alpha" || chunkMap.Chunks[0].SourceRef.StartUnitID != 1 || chunkMap.Chunks[0].SourceRef.EndUnitID != 2 || chunkMap.Chunks[0].UnitCount != 2 {
|
if len(chunkMap.Chunks) != 1 || chunkMap.Chunks[0].ID != "chunk-000001" || chunkMap.Chunks[0].Index != 0 || chunkMap.Chunks[0].SourceRef.SourceID != "session-alpha" || chunkMap.Chunks[0].SourceRef.StartUnitID != 1 || chunkMap.Chunks[0].SourceRef.EndUnitID != 2 || chunkMap.Chunks[0].UnitCount != 2 {
|
||||||
t.Fatalf("chunk map chunks = %#v, want one stable accepted scene range", chunkMap.Chunks)
|
t.Fatalf("chunk map chunks = %#v, want one stable accepted scene range", chunkMap.Chunks)
|
||||||
}
|
}
|
||||||
var planAnnotation struct {
|
if len(chunkMap.PlanAnnotations) != 0 {
|
||||||
BoundaryCaveats []string `json:"boundary_caveats"`
|
t.Fatalf("chunk map plan annotations = %#v, want none", chunkMap.PlanAnnotations)
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(chunkMap.PlanAnnotations["dnd/scenes"], &planAnnotation); err != nil || len(planAnnotation.BoundaryCaveats) != 1 {
|
if len(chunkMap.Chunks[0].Annotations) != 0 {
|
||||||
t.Fatalf("chunk map plan annotation = %s, %v; want scene caveat", chunkMap.PlanAnnotations["dnd/scenes"], err)
|
t.Fatalf("chunk map range annotations = %#v, want none", chunkMap.Chunks[0].Annotations)
|
||||||
}
|
|
||||||
var rangeAnnotation struct {
|
|
||||||
ShortTitle string `json:"short_title"`
|
|
||||||
PrimaryMode string `json:"primary_mode"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(chunkMap.Chunks[0].Annotations["dnd/scenes"], &rangeAnnotation); err != nil || rangeAnnotation.ShortTitle != "Opening scene" || rangeAnnotation.PrimaryMode != "Narrative" {
|
|
||||||
t.Fatalf("chunk map range annotation = %s, %v; want surviving scene annotation", chunkMap.Chunks[0].Annotations["dnd/scenes"], err)
|
|
||||||
}
|
}
|
||||||
warnings := readProductionJSON[struct {
|
warnings := readProductionJSON[struct {
|
||||||
Warnings []contracts.Warning `json:"warnings"`
|
Warnings []contracts.Warning `json:"warnings"`
|
||||||
}](t, filepath.Join(outputRoot, productionRunID, "warnings.json"))
|
}](t, filepath.Join(outputRoot, productionRunID, "warnings.json"))
|
||||||
if len(warnings.Warnings) != 1 || warnings.Warnings[0].ReasonCode != "scene_boundary_caveat" {
|
if len(warnings.Warnings) != 0 {
|
||||||
t.Fatalf("warnings = %#v, want one scene boundary warning", warnings.Warnings)
|
t.Fatalf("warnings = %#v, want none", warnings.Warnings)
|
||||||
}
|
}
|
||||||
if len(fake.requestsFor(scenes.PromptID)) != 1 || len(fake.requestsFor(spells.PromptID)) != 1 {
|
if len(fake.requestsFor(scenes.PromptID)) != 1 || len(fake.requestsFor(spells.PromptID)) != 1 {
|
||||||
t.Fatalf("fake prompt requests = %#v, want one scene and one spell request", fake.requestPrompts())
|
t.Fatalf("fake prompt requests = %#v, want one scene and one spell request", fake.requestPrompts())
|
||||||
@@ -725,7 +718,7 @@ func (client *productionFakeLLMClient) CompleteStructured(ctx context.Context, r
|
|||||||
var content []byte
|
var content []byte
|
||||||
switch req.PromptID {
|
switch req.PromptID {
|
||||||
case scenes.PromptID:
|
case scenes.PromptID:
|
||||||
content = []byte(`{"scenes":[{"start_unit_id":1,"end_unit_id":2,"short_title":"Opening scene","primary_mode":"Narrative","main_participants":["Aria"],"summary":"The session opens.","boundary_note":"The opening covers the available transcript.","boundary_confidence":"High"}],"boundary_caveats":["The opening boundary is inferred from the short transcript."]}`)
|
content = []byte(`{"scenes":[{"start_unit_id":1,"end_unit_id":2}]}`)
|
||||||
case spells.PromptID:
|
case spells.PromptID:
|
||||||
if client.spellResponse != "" {
|
if client.spellResponse != "" {
|
||||||
content = []byte(client.spellResponse)
|
content = []byte(client.spellResponse)
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ func TestCodecRoundTripsValidFixture(t *testing.T) {
|
|||||||
func TestBuildCanonicalizesAnnotationFormatting(t *testing.T) {
|
func TestBuildCanonicalizesAnnotationFormatting(t *testing.T) {
|
||||||
first := acceptedBuildRequest(t)
|
first := acceptedBuildRequest(t)
|
||||||
second := acceptedBuildRequest(t)
|
second := acceptedBuildRequest(t)
|
||||||
second.Plan.Annotations["dnd/scenes"] = json.RawMessage(" { \n \t\"title\" : \"Gate\" \n } ")
|
second.Plan.Annotations["test/chunker"] = json.RawMessage(" { \n \t\"label\" : \"fixture\" \n } ")
|
||||||
canonical, err := source.CanonicalizeChunkPlan(second.Plan)
|
canonical, err := source.CanonicalizeChunkPlan(second.Plan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("CanonicalizeChunkPlan() error = %v", err)
|
t.Fatalf("CanonicalizeChunkPlan() error = %v", err)
|
||||||
@@ -108,7 +108,7 @@ func TestCodecRejectsInvalidDurableBoundaries(t *testing.T) {
|
|||||||
{name: "invalid range", mutate: func(value *ChunkMap) { value.Chunks[0].SourceRef.StartUnitID = 0 }},
|
{name: "invalid range", mutate: func(value *ChunkMap) { value.Chunks[0].SourceRef.StartUnitID = 0 }},
|
||||||
{name: "invalid count", mutate: func(value *ChunkMap) { value.Chunks[0].UnitCount = 0 }},
|
{name: "invalid count", mutate: func(value *ChunkMap) { value.Chunks[0].UnitCount = 0 }},
|
||||||
{name: "invalid namespace", mutate: func(value *ChunkMap) { value.PlanAnnotations[" "] = json.RawMessage(`null`) }},
|
{name: "invalid namespace", mutate: func(value *ChunkMap) { value.PlanAnnotations[" "] = json.RawMessage(`null`) }},
|
||||||
{name: "invalid annotation", mutate: func(value *ChunkMap) { value.Chunks[0].Annotations["dnd/scenes"] = json.RawMessage(`{`) }},
|
{name: "invalid annotation", mutate: func(value *ChunkMap) { value.Chunks[0].Annotations["test/chunker"] = json.RawMessage(`{`) }},
|
||||||
{name: "plan digest mismatch", mutate: func(value *ChunkMap) { value.PlanDigest = "sha256:" + strings.Repeat("a", 64) }},
|
{name: "plan digest mismatch", mutate: func(value *ChunkMap) { value.PlanDigest = "sha256:" + strings.Repeat("a", 64) }},
|
||||||
} {
|
} {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
@@ -131,13 +131,13 @@ func TestCodecRejectsInvalidDurableBoundaries(t *testing.T) {
|
|||||||
t.Fatalf("Decode(%s) error = nil, want strict JSON rejection", raw)
|
t.Fatalf("Decode(%s) error = nil, want strict JSON rejection", raw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
formatted := bytes.Replace(content, []byte(`{"title":"Gate"}`), []byte("{\n \"title\": \"Gate\"\n}"), 1)
|
formatted := bytes.Replace(content, []byte(`{"label":"fixture"}`), []byte("{\n \"label\": \"fixture\"\n}"), 1)
|
||||||
decoded, err := New().Decode(formatted)
|
decoded, err := New().Decode(formatted)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Decode(formatted annotations) error = %v", err)
|
t.Fatalf("Decode(formatted annotations) error = %v", err)
|
||||||
}
|
}
|
||||||
if string(decoded.PlanAnnotations["dnd/scenes"]) != `{"title":"Gate"}` {
|
if string(decoded.PlanAnnotations["test/chunker"]) != `{"label":"fixture"}` {
|
||||||
t.Fatalf("decoded annotation = %s, want canonical JSON", decoded.PlanAnnotations["dnd/scenes"])
|
t.Fatalf("decoded annotation = %s, want canonical JSON", decoded.PlanAnnotations["test/chunker"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +187,7 @@ func TestEncodeDoesNotMutateValue(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
value.Chunks[0].Annotations["dnd/scenes"] = json.RawMessage(" { \n \"kind\" : \"narrative\" \n } ")
|
value.Chunks[0].Annotations["test/chunker"] = json.RawMessage(" { \n \"category\" : \"sample\" \n } ")
|
||||||
before := clone(value)
|
before := clone(value)
|
||||||
if _, err := New().Encode(value); err != nil {
|
if _, err := New().Encode(value); err != nil {
|
||||||
t.Fatalf("Encode() error = %v", err)
|
t.Fatalf("Encode() error = %v", err)
|
||||||
@@ -203,14 +203,14 @@ func TestChunkMapOwnershipIsIndependent(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
request.Plan.Annotations["dnd/scenes"][0] = '['
|
request.Plan.Annotations["test/chunker"][0] = '['
|
||||||
first.PlanAnnotations["dnd/scenes"][0] = '['
|
first.PlanAnnotations["test/chunker"][0] = '['
|
||||||
second, err := Build(acceptedBuildRequest(t))
|
second, err := Build(acceptedBuildRequest(t))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if string(second.PlanAnnotations["dnd/scenes"]) != `{"title":"Gate"}` {
|
if string(second.PlanAnnotations["test/chunker"]) != `{"label":"fixture"}` {
|
||||||
t.Fatalf("Build() shared mutable annotations: %s", second.PlanAnnotations["dnd/scenes"])
|
t.Fatalf("Build() shared mutable annotations: %s", second.PlanAnnotations["test/chunker"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,11 +232,11 @@ func chunkDocument(value map[string]any, index int) map[string]any {
|
|||||||
func acceptedBuildRequest(t *testing.T) BuildRequest {
|
func acceptedBuildRequest(t *testing.T) BuildRequest {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
document := &source.SourceDocument{
|
document := &source.SourceDocument{
|
||||||
ID: "session-7", Kind: "transcript", Format: "application/json",
|
ID: "source-test", Kind: "transcript", Format: "application/json",
|
||||||
Units: []source.SourceUnit{
|
Units: []source.SourceUnit{
|
||||||
{ID: 10, Kind: "segment", Text: "At the gate.", Ref: source.SourceRef{SourceID: "session-7", StartUnitID: 10, EndUnitID: 10}},
|
{ID: 10, Kind: "segment", Text: "First unit.", Ref: source.SourceRef{SourceID: "source-test", StartUnitID: 10, EndUnitID: 10}},
|
||||||
{ID: 3, Kind: "segment", Text: "The guard speaks.", Ref: source.SourceRef{SourceID: "session-7", StartUnitID: 3, EndUnitID: 3}},
|
{ID: 3, Kind: "segment", Text: "Second unit.", Ref: source.SourceRef{SourceID: "source-test", StartUnitID: 3, EndUnitID: 3}},
|
||||||
{ID: 20, Kind: "segment", Text: "The party enters.", Ref: source.SourceRef{SourceID: "session-7", StartUnitID: 20, EndUnitID: 20}},
|
{ID: 20, Kind: "segment", Text: "Third unit.", Ref: source.SourceRef{SourceID: "source-test", StartUnitID: 20, EndUnitID: 20}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
digest, err := source.DigestDocument(document)
|
digest, err := source.DigestDocument(document)
|
||||||
@@ -246,9 +246,9 @@ func acceptedBuildRequest(t *testing.T) BuildRequest {
|
|||||||
document.Digest = digest
|
document.Digest = digest
|
||||||
plan := source.ChunkPlan{
|
plan := source.ChunkPlan{
|
||||||
SourceDigest: digest,
|
SourceDigest: digest,
|
||||||
Annotations: source.ChunkAnnotations{"dnd/scenes": json.RawMessage(`{"title":"Gate"}`)},
|
Annotations: source.ChunkAnnotations{"test/chunker": json.RawMessage(`{"label":"fixture"}`)},
|
||||||
Ranges: []source.ChunkRange{
|
Ranges: []source.ChunkRange{
|
||||||
{StartUnitID: 10, EndUnitID: 3, Annotations: source.ChunkAnnotations{"dnd/scenes": json.RawMessage(`{"kind":"narrative"}`)}},
|
{StartUnitID: 10, EndUnitID: 3, Annotations: source.ChunkAnnotations{"test/chunker": json.RawMessage(`{"category":"sample"}`)}},
|
||||||
{StartUnitID: 20, EndUnitID: 20},
|
{StartUnitID: 20, EndUnitID: 20},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -257,7 +257,7 @@ func acceptedBuildRequest(t *testing.T) BuildRequest {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
return BuildRequest{
|
return BuildRequest{
|
||||||
Source: document, Plan: plan, Chunks: chunks, RequestedChunker: "dnd/scenes",
|
Source: document, Plan: plan, Chunks: chunks, RequestedChunker: "chunk/requested",
|
||||||
Producer: Producer{InputModule: "seriatim", ChunkModule: "dnd/scenes", LLMProfile: "dnd-scenes"},
|
Producer: Producer{InputModule: "input/producer", ChunkModule: "chunk/producer", LLMProfile: "profile/test"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"source_id":"session-7","source_digest":"sha256:87d04d40537217e2adcbdec841c5873dbc1034c426d83c4578a0431a5ef855f6","plan_digest":"sha256:a6bfc33d52f1c0f4eb3287dd4a00e8672b3d3d9579f9c0366f18a5ff0dba7d14","requested_chunker":"dnd/scenes","producer":{"input_module":"seriatim","chunk_module":"dnd/scenes","llm_profile":"dnd-scenes"},"plan_annotations":{"dnd/scenes":{"title":"Gate"}},"chunks":[{"id":"chunk-000001","index":0,"source_ref":{"source_id":"session-7","start_unit_id":10,"end_unit_id":3},"unit_count":2,"annotations":{"dnd/scenes":{"kind":"narrative"}}},{"id":"chunk-000002","index":1,"source_ref":{"source_id":"session-7","start_unit_id":20,"end_unit_id":20},"unit_count":1,"annotations":{}}]}
|
{"source_id":"source-test","source_digest":"sha256:186b2d30029e7fda40545f88e75ade38f22bff3e5543af4dfeb86587536a01af","plan_digest":"sha256:e50cc7da9070ac8a4339d79b2c206be842846c5c22af7b0ad58f7e9a34ceaf97","requested_chunker":"chunk/requested","producer":{"input_module":"input/producer","chunk_module":"chunk/producer","llm_profile":"profile/test"},"plan_annotations":{"test/chunker":{"label":"fixture"}},"chunks":[{"id":"chunk-000001","index":0,"source_ref":{"source_id":"source-test","start_unit_id":10,"end_unit_id":3},"unit_count":2,"annotations":{"test/chunker":{"category":"sample"}}},{"id":"chunk-000002","index":1,"source_ref":{"source_id":"source-test","start_unit_id":20,"end_unit_id":20},"unit_count":1,"annotations":{}}]}
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ func TestFilesystemStoreReportsInvalidRecordsAsRecoverable(t *testing.T) {
|
|||||||
return bytes.Replace(data, []byte(`{"schema_version"`), []byte(`{"SENTINEL_UNKNOWN_FIELD":true,"schema_version"`), 1)
|
return bytes.Replace(data, []byte(`{"schema_version"`), []byte(`{"SENTINEL_UNKNOWN_FIELD":true,"schema_version"`), 1)
|
||||||
}},
|
}},
|
||||||
{name: "truncated JSON", mutate: func(data []byte) []byte { return data[:len(data)/2] }},
|
{name: "truncated JSON", mutate: func(data []byte) []byte { return data[:len(data)/2] }},
|
||||||
{name: "schema mismatch", mutate: replaceJSON(`notarius.chunk-plan.v1`, `SENTINEL_SCHEMA_VALUE`)},
|
{name: "legacy v1 record", mutate: replaceJSON(`notarius.chunk-plan.v2`, `notarius.chunk-plan.v1`)},
|
||||||
{name: "source mismatch", mutate: replaceJSON(testSourceDigest, "sha256:"+strings.Repeat("b", 64))},
|
{name: "source mismatch", mutate: replaceJSON(testSourceDigest, "sha256:"+strings.Repeat("b", 64))},
|
||||||
{name: "plan digest mismatch", mutate: func(data []byte) []byte {
|
{name: "plan digest mismatch", mutate: func(data []byte) []byte {
|
||||||
prefix := []byte(`"plan_digest":"sha256:`)
|
prefix := []byte(`"plan_digest":"sha256:`)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ChunkPlanSchemaVersion = "notarius.chunk-plan.v1"
|
const ChunkPlanSchemaVersion = "notarius.chunk-plan.v2"
|
||||||
|
|
||||||
type ChunkPlanProducer struct {
|
type ChunkPlanProducer struct {
|
||||||
InputModule string `json:"input_module"`
|
InputModule string `json:"input_module"`
|
||||||
|
|||||||
Reference in New Issue
Block a user