Improve D&D validation reliability

This commit is contained in:
2026-08-29 01:24:45 +00:00
parent 4da9360d74
commit 917d150279
55 changed files with 1300 additions and 565 deletions

View File

@@ -7,6 +7,7 @@ import (
"strconv"
"strings"
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
frameworkdiagnostics "gitea.maximumdirect.net/eric/notarius/internal/framework/diagnostics"
)
@@ -25,6 +26,79 @@ type Finding struct {
Message string
}
// Corrections collects domain-owned model instructions and contextual record
// descriptions without coupling them to operator-facing diagnostics. Rules are
// emitted before affected records so the bounded result remains useful even
// when a large candidate exceeds the display budget.
type Corrections struct {
groups []correctionGroup
indexes map[string]int
}
type correctionGroup struct {
rule string
records []string
recordsSeen map[string]struct{}
}
// Add records one semantic rule and, when non-empty, one contextual record to
// which it applies. key is local grouping state and is never rendered.
func (c *Corrections) Add(key, rule, record string) {
if c.indexes == nil {
c.indexes = make(map[string]int)
}
index, ok := c.indexes[key]
if !ok {
index = len(c.groups)
c.indexes[key] = index
c.groups = append(c.groups, correctionGroup{rule: rule, recordsSeen: make(map[string]struct{})})
}
if record == "" {
return
}
group := &c.groups[index]
if _, seen := group.recordsSeen[record]; seen {
return
}
group.recordsSeen[record] = struct{}{}
group.records = append(group.records, record)
}
// Guidance returns one bounded correction request. It deliberately renders
// neither grouping keys nor operator diagnostics.
func (c Corrections) Guidance(prefix string) string {
issues := make([]string, 0, len(c.groups)*2)
for _, group := range c.groups {
issues = append(issues, group.rule)
}
for _, group := range c.groups {
issues = append(issues, group.records...)
}
return Aggregate(prefix, issues)
}
// SourceRange describes cited transcript positions without exposing source
// identities or application entity IDs.
func SourceRange(refs []source.SourceRef) string {
if len(refs) == 0 {
return "without a cited source range"
}
description := SourceRefRange(refs[0])
if len(refs) > 1 {
description += fmt.Sprintf(" (first of %d cited ranges)", len(refs))
}
return description
}
// SourceRefRange describes one transcript range without exposing its source
// identity.
func SourceRefRange(ref source.SourceRef) string {
if ref.StartUnitID == ref.EndUnitID {
return "at source unit " + strconv.Itoa(ref.StartUnitID)
}
return fmt.Sprintf("at source units %d-%d", ref.StartUnitID, ref.EndUnitID)
}
// DataQualityResult converts accepted source-quality findings into bounded,
// locally grouped advisories. These findings do not indicate process
// degradation.

View File

@@ -6,9 +6,40 @@ import (
"testing"
"unicode/utf8"
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
)
func TestCorrectionsGroupRulesBeforeContextAndHideKeys(t *testing.T) {
var corrections Corrections
corrections.Add("internal-kind", "Use a supported kind.", "Affected goblin at source unit 4.")
corrections.Add("internal-kind", "Use a supported kind.", "Affected ogre at source units 8-9.")
corrections.Add("internal-kind", "Use a supported kind.", "Affected goblin at source unit 4.")
corrections.Add("name", "Provide a contextual name.", "Affected unnamed record at source unit 12.")
guidance := corrections.Guidance("Correct every record")
if strings.Contains(guidance, "internal-kind") {
t.Fatalf("Guidance() exposed grouping key: %q", guidance)
}
if strings.Count(guidance, "Use a supported kind.") != 1 || strings.Count(guidance, "Affected goblin") != 1 {
t.Fatalf("Guidance() did not de-duplicate rules and records: %q", guidance)
}
if strings.Index(guidance, "Use a supported kind.") > strings.Index(guidance, "Affected goblin") {
t.Fatalf("Guidance() = %q, want rules before records", guidance)
}
}
func TestSourceRangeUsesOnlyTranscriptPositions(t *testing.T) {
refs := []source.SourceRef{
{SourceID: "opaque-source", StartUnitID: 8, EndUnitID: 10},
{SourceID: "opaque-source", StartUnitID: 12, EndUnitID: 12},
}
got := SourceRange(refs)
if strings.Contains(got, "opaque-source") || !strings.Contains(got, "8-10") || !strings.Contains(got, "first of 2") {
t.Fatalf("SourceRange() = %q", got)
}
}
func TestAggregateEnforcesByteBudgetAndReportsOmissions(t *testing.T) {
issues := make([]string, MaxIssues)
for index := range issues {

View File

@@ -0,0 +1,48 @@
package shared
import "gitea.maximumdirect.net/eric/notarius/internal/core/source"
// ChunkCoverage is an immutable snapshot of the source units materialized in
// one extraction chunk.
type ChunkCoverage struct {
sourceID string
unitIDs map[int]struct{}
}
// NewChunkCoverage snapshots chunk without retaining or mutating it.
func NewChunkCoverage(chunk *source.Chunk) ChunkCoverage {
if chunk == nil {
return ChunkCoverage{}
}
coverage := ChunkCoverage{
sourceID: chunk.SourceID,
unitIDs: make(map[int]struct{}, len(chunk.Units)),
}
for _, unit := range chunk.Units {
coverage.unitIDs[unit.ID] = struct{}{}
}
return coverage
}
// Contains reports whether ref identifies a valid inclusive span in index and
// every unit in that document-ordered span is present in the chunk snapshot.
func (c ChunkCoverage) Contains(index source.DocumentIndex, doc *source.SourceDocument, ref source.SourceRef) bool {
documentID, ok := index.DocumentID()
if !ok || doc == nil || doc.ID != documentID || c.sourceID == "" || ref.SourceID != c.sourceID || ref.SourceID != documentID {
return false
}
start, startOK := index.Position(ref.StartUnitID)
end, endOK := index.Position(ref.EndUnitID)
if !startOK || !endOK || start > end {
return false
}
for position := start; position <= end; position++ {
if position >= len(doc.Units) {
return false
}
if _, found := c.unitIDs[doc.Units[position].ID]; !found {
return false
}
}
return true
}

View File

@@ -0,0 +1,69 @@
package shared
import (
"testing"
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
)
func TestChunkCoverageRequiresCompleteDocumentOrderedSpan(t *testing.T) {
doc := coverageDocument(30, 10, 20, 40)
index := source.NewDocumentIndex(doc)
for _, test := range []struct {
name string
chunk *source.Chunk
ref source.SourceRef
want bool
}{
{name: "complete non-monotonic span", chunk: coverageChunk(doc.ID, 30, 10, 20), ref: coverageRef(doc.ID, 30, 20), want: true},
{name: "single unit", chunk: coverageChunk(doc.ID, 10), ref: coverageRef(doc.ID, 10, 10), want: true},
{name: "missing middle unit", chunk: coverageChunk(doc.ID, 30, 20), ref: coverageRef(doc.ID, 30, 20)},
{name: "only endpoints", chunk: coverageChunk(doc.ID, 30, 40), ref: coverageRef(doc.ID, 30, 40)},
{name: "reversed", chunk: coverageChunk(doc.ID, 30, 10), ref: coverageRef(doc.ID, 10, 30)},
{name: "wrong source", chunk: coverageChunk(doc.ID, 30), ref: coverageRef("other", 30, 30)},
{name: "missing endpoint", chunk: coverageChunk(doc.ID, 30), ref: coverageRef(doc.ID, 30, 999)},
{name: "nil chunk", ref: coverageRef(doc.ID, 30, 30)},
} {
t.Run(test.name, func(t *testing.T) {
coverage := NewChunkCoverage(test.chunk)
if got := coverage.Contains(index, doc, test.ref); got != test.want {
t.Fatalf("Contains() = %t, want %t", got, test.want)
}
})
}
if NewChunkCoverage(coverageChunk(doc.ID, 30)).Contains(source.DocumentIndex{}, nil, coverageRef(doc.ID, 30, 30)) {
t.Fatal("Contains() with nil document and zero index = true, want false")
}
}
func TestChunkCoverageDoesNotRetainMutableChunkState(t *testing.T) {
doc := coverageDocument(1, 2)
chunk := coverageChunk(doc.ID, 1, 2)
coverage := NewChunkCoverage(chunk)
chunk.SourceID = "changed"
chunk.Units[0].ID = 99
if !coverage.Contains(source.NewDocumentIndex(doc), doc, coverageRef(doc.ID, 1, 2)) {
t.Fatal("Contains() changed after mutating source chunk")
}
}
func coverageDocument(ids ...int) *source.SourceDocument {
doc := &source.SourceDocument{ID: "session", Units: make([]source.SourceUnit, len(ids))}
for index, id := range ids {
doc.Units[index] = source.SourceUnit{ID: id}
}
return doc
}
func coverageChunk(sourceID string, ids ...int) *source.Chunk {
chunk := &source.Chunk{SourceID: sourceID, Units: make([]source.SourceUnit, len(ids))}
for index, id := range ids {
chunk.Units[index] = source.SourceUnit{ID: id}
}
return chunk
}
func coverageRef(sourceID string, start, end int) source.SourceRef {
return source.SourceRef{SourceID: sourceID, StartUnitID: start, EndUnitID: end}
}