Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
76 lines
2.3 KiB
Go
76 lines
2.3 KiB
Go
// Package chunker - validator.go inspects a tier's output and decides whether
|
|
// it is good enough to ship or whether the strategy chain should fall through
|
|
// to the next tier. The validator is intentionally permissive: a single
|
|
// "obviously broken" output is rejected, but plausible-looking variation is
|
|
// accepted so we don't oscillate between tiers.
|
|
package chunker
|
|
|
|
import "math"
|
|
|
|
// ValidationResult captures the verdict and reason for a chunk-set.
|
|
type ValidationResult struct {
|
|
OK bool
|
|
Reason string
|
|
}
|
|
|
|
// ValidateChunks checks whether the given chunks form a usable result for a
|
|
// document of totalChars characters with a target chunkSize. Returns OK=true
|
|
// when no broken-output indicator triggers.
|
|
func ValidateChunks(chunks []Chunk, totalChars, chunkSize int) ValidationResult {
|
|
if len(chunks) == 0 {
|
|
return ValidationResult{Reason: "no chunks produced"}
|
|
}
|
|
|
|
// A single chunk for a document much larger than chunkSize means the
|
|
// strategy did not actually split — fail so the next tier runs.
|
|
if len(chunks) == 1 && totalChars > 2*chunkSize {
|
|
return ValidationResult{Reason: "single chunk for large document"}
|
|
}
|
|
|
|
// Compute size statistics.
|
|
var sum, sumSq float64
|
|
maxLen, minLen := 0, math.MaxInt32
|
|
for _, c := range chunks {
|
|
l := len([]rune(c.Content))
|
|
sum += float64(l)
|
|
sumSq += float64(l * l)
|
|
if l > maxLen {
|
|
maxLen = l
|
|
}
|
|
if l < minLen {
|
|
minLen = l
|
|
}
|
|
}
|
|
avg := sum / float64(len(chunks))
|
|
|
|
// All but the last chunk should carry meaningful content. We allow the
|
|
// last chunk to be tiny because tail residue is normal.
|
|
tinyCount := 0
|
|
for i, c := range chunks {
|
|
if i == len(chunks)-1 {
|
|
continue
|
|
}
|
|
if len([]rune(c.Content)) < 50 {
|
|
tinyCount++
|
|
}
|
|
}
|
|
if tinyCount > len(chunks)/4 && tinyCount > 2 {
|
|
return ValidationResult{Reason: "too many tiny chunks"}
|
|
}
|
|
|
|
// Reject when no chunk reached at least 25% of the target — the splitter
|
|
// is fragmenting too aggressively to be useful.
|
|
if maxLen < chunkSize/4 && totalChars > chunkSize {
|
|
return ValidationResult{Reason: "all chunks far below target size"}
|
|
}
|
|
|
|
// Sanity check on absolute upper bound. Anything past 2x chunkSize is a
|
|
// red flag — the splitter ignored its size budget.
|
|
if maxLen > 2*chunkSize && chunkSize > 0 {
|
|
return ValidationResult{Reason: "chunk exceeds 2x target size"}
|
|
}
|
|
|
|
_ = avg
|
|
return ValidationResult{OK: true}
|
|
}
|