package main

import (
	"context"
	"sort"
	"strings"

	"github.com/yazanabuashour/openclerk/internal/runclient"
	"github.com/yazanabuashour/openclerk/internal/runner"
)

func verifyDocumentContains(ctx context.Context, paths evalPaths, docPath string, required []string, forbidden []string) (verificationResult, error) {
	body, found, err := documentBodyByPath(ctx, paths, docPath)
	if err != nil {
		return verificationResult{}, err
	}
	if !found {
		return verificationResult{Passed: false, DatabasePass: false, Details: "missing " + docPath}, nil
	}
	failures := missingRequired(body, required)
	failures = append(failures, presentForbidden(body, forbidden)...)
	return verificationResult{
		Passed:        len(failures) == 0,
		DatabasePass:  len(failures) == 0,
		AssistantPass: true,
		Details:       missingDetails(failures),
		Documents:     []string{docPath},
	}, nil
}
func documentByPath(ctx context.Context, paths evalPaths, docPath string) (*runner.Document, bool, error) {
	docID, found, err := documentIDByPath(ctx, paths, docPath)
	if err != nil || !found {
		return nil, found, err
	}
	cfg := runclient.Config{DatabasePath: paths.DatabasePath}
	got, err := runner.RunDocumentTask(ctx, cfg, runner.DocumentTaskRequest{Action: runner.DocumentTaskActionGet, DocID: docID})
	if err != nil {
		return nil, false, err
	}
	if got.Document != nil {
		return got.Document, true, nil
	}
	return nil, false, nil
}
func documentBodyByPath(ctx context.Context, paths evalPaths, docPath string) (string, bool, error) {
	doc, found, err := documentByPath(ctx, paths, docPath)
	if err != nil || !found {
		return "", found, err
	}
	if doc != nil {
		return doc.Body, true, nil
	}
	return "", false, nil
}
func documentContaining(ctx context.Context, paths evalPaths, needle string) (string, string, bool, error) {
	cfg := runclient.Config{DatabasePath: paths.DatabasePath}
	list, err := runner.RunDocumentTask(ctx, cfg, runner.DocumentTaskRequest{
		Action: runner.DocumentTaskActionList,
		List:   runner.DocumentListOptions{Limit: 100},
	})
	if err != nil {
		return "", "", false, err
	}
	for _, entry := range list.Documents {
		doc, found, err := documentByPath(ctx, paths, entry.Path)
		if err != nil {
			return "", "", false, err
		}
		if !found || doc == nil {
			continue
		}
		if strings.Contains(doc.Body, needle) {
			return entry.Path, doc.Body, true, nil
		}
	}
	return "", "", false, nil
}
func documentIDByPath(ctx context.Context, paths evalPaths, docPath string) (string, bool, error) {
	cfg := runclient.Config{DatabasePath: paths.DatabasePath}
	list, err := runner.RunDocumentTask(ctx, cfg, runner.DocumentTaskRequest{
		Action: runner.DocumentTaskActionList,
		List:   runner.DocumentListOptions{PathPrefix: docPath, Limit: 100},
	})
	if err != nil {
		return "", false, err
	}
	for _, doc := range list.Documents {
		if doc.Path == docPath {
			return doc.DocID, true, nil
		}
	}
	return "", false, nil
}
func exactDocumentCount(ctx context.Context, paths evalPaths, docPath string) (int, error) {
	cfg := runclient.Config{DatabasePath: paths.DatabasePath}
	list, err := runner.RunDocumentTask(ctx, cfg, runner.DocumentTaskRequest{
		Action: runner.DocumentTaskActionList,
		List:   runner.DocumentListOptions{PathPrefix: docPath, Limit: 100},
	})
	if err != nil {
		return 0, err
	}
	count := 0
	for _, doc := range list.Documents {
		if doc.Path == docPath {
			count++
		}
	}
	return count, nil
}
func documentCountWithPrefix(ctx context.Context, paths evalPaths, pathPrefix string) (int, error) {
	cfg := runclient.Config{DatabasePath: paths.DatabasePath}
	list, err := runner.RunDocumentTask(ctx, cfg, runner.DocumentTaskRequest{
		Action: runner.DocumentTaskActionList,
		List:   runner.DocumentListOptions{PathPrefix: pathPrefix, Limit: 100},
	})
	if err != nil {
		return 0, err
	}
	count := 0
	for _, doc := range list.Documents {
		if strings.HasPrefix(doc.Path, pathPrefix) {
			count++
		}
	}
	return count, nil
}
func disallowedDocumentPathsWithPrefix(ctx context.Context, paths evalPaths, pathPrefix string, allowed map[string]bool) ([]string, error) {
	cfg := runclient.Config{DatabasePath: paths.DatabasePath}
	list, err := runner.RunDocumentTask(ctx, cfg, runner.DocumentTaskRequest{
		Action: runner.DocumentTaskActionList,
		List:   runner.DocumentListOptions{PathPrefix: pathPrefix, Limit: 100},
	})
	if err != nil {
		return nil, err
	}
	disallowed := []string{}
	for _, doc := range list.Documents {
		if strings.HasPrefix(doc.Path, pathPrefix) && !allowed[doc.Path] {
			disallowed = append(disallowed, doc.Path)
		}
	}
	sort.Strings(disallowed)
	return disallowed, nil
}
func firstSynthesisProjection(ctx context.Context, paths evalPaths, docID string) (*runner.ProjectionState, error) {
	if strings.TrimSpace(docID) == "" {
		return nil, nil
	}
	cfg := runclient.Config{DatabasePath: paths.DatabasePath}
	projections, err := runner.RunRetrievalTask(ctx, cfg, runner.RetrievalTaskRequest{
		Action: runner.RetrievalTaskActionProjectionStates,
		Projection: runner.ProjectionStateOptions{
			Projection: "synthesis",
			RefKind:    "document",
			RefID:      docID,
			Limit:      5,
		},
	})
	if err != nil {
		return nil, err
	}
	if projections.Projections == nil || len(projections.Projections.Projections) == 0 {
		return nil, nil
	}
	projection := projections.Projections.Projections[0]
	return &projection, nil
}
func projectionDetailContains(details map[string]string, key string, value string) bool {
	return strings.Contains(details[key], value)
}
func topSearchHit(result runner.RetrievalTaskResult) (runner.SearchHit, bool) {
	if result.Search == nil || len(result.Search.Hits) == 0 {
		return runner.SearchHit{}, false
	}
	return result.Search.Hits[0], true
}
func searchContainsPath(result runner.RetrievalTaskResult, path string) bool {
	if result.Search == nil {
		return false
	}
	for _, hit := range result.Search.Hits {
		if searchHitPath(hit) == path {
			return true
		}
	}
	return false
}
func searchResultHasCitations(result runner.RetrievalTaskResult) bool {
	if result.Search == nil || len(result.Search.Hits) == 0 {
		return false
	}
	for _, hit := range result.Search.Hits {
		if searchHitHasCitation(hit) {
			return true
		}
	}
	return false
}
func searchOnlyContainsPath(result runner.RetrievalTaskResult, path string) bool {
	if result.Search == nil || len(result.Search.Hits) == 0 {
		return false
	}
	for _, hit := range result.Search.Hits {
		if searchHitPath(hit) != path {
			return false
		}
	}
	return true
}
func searchHitPath(hit runner.SearchHit) string {
	if len(hit.Citations) > 0 {
		return hit.Citations[0].Path
	}
	return ""
}
func searchHitHasCitation(hit runner.SearchHit) bool {
	if hit.DocID == "" || hit.ChunkID == "" {
		return false
	}
	for _, citation := range hit.Citations {
		if citation.DocID != "" &&
			citation.ChunkID != "" &&
			citation.Path != "" &&
			citation.LineStart > 0 &&
			citation.LineEnd >= citation.LineStart {
			return true
		}
	}
	return false
}
func allPathsFound(found map[string]bool, expected []string) bool {
	for _, path := range expected {
		if !found[path] {
			return false
		}
	}
	return true
}
func missingRequired(body string, required []string) []string {
	failures := []string{}
	for _, value := range required {
		if !strings.Contains(body, value) {
			failures = append(failures, "missing "+value)
		}
	}
	return failures
}
func missingRequiredFold(body string, required []string) []string {
	failures := []string{}
	lowerBody := strings.ToLower(body)
	for _, value := range required {
		if !strings.Contains(lowerBody, strings.ToLower(value)) {
			failures = append(failures, "missing "+value)
		}
	}
	return failures
}
func sourceRefsFrontmatterFailures(body string, expected []string) []string {
	value, found, singleLine := sourceRefsFrontmatterValue(body)
	if !found {
		return []string{"missing source_refs frontmatter"}
	}
	if !singleLine {
		return []string{"source_refs must be single-line comma-separated frontmatter"}
	}
	refs := map[string]bool{}
	for _, ref := range strings.Split(value, ",") {
		normalized := strings.Trim(strings.TrimSpace(ref), `"'`)
		if normalized != "" {
			refs[normalized] = true
		}
	}
	failures := []string{}
	for _, ref := range expected {
		if !refs[ref] {
			failures = append(failures, "missing source ref "+ref)
		}
	}
	return failures
}
func sourceRefsFrontmatterValue(body string) (string, bool, bool) {
	lines := strings.Split(body, "\n")
	if len(lines) == 0 || strings.TrimSpace(lines[0]) != "---" {
		return "", false, false
	}
	for _, line := range lines[1:] {
		trimmed := strings.TrimSpace(line)
		if trimmed == "---" {
			break
		}
		key, value, ok := strings.Cut(trimmed, ":")
		if !ok || !strings.EqualFold(strings.TrimSpace(key), "source_refs") {
			continue
		}
		value = strings.TrimSpace(value)
		if value == "" || strings.HasPrefix(value, "[") || strings.HasSuffix(value, "]") {
			return value, true, false
		}
		return value, true, true
	}
	return "", false, false
}
func presentForbidden(body string, forbidden []string) []string {
	failures := []string{}
	for _, value := range forbidden {
		if strings.Contains(body, value) {
			failures = append(failures, "unexpected "+value)
		}
	}
	return failures
}
func messageContainsAll(message string, values []string) bool {
	lower := normalizeValidationMessage(message)
	for _, value := range values {
		if !strings.Contains(lower, strings.ToLower(value)) {
			return false
		}
	}
	return true
}
func messageContainsAny(message string, values []string) bool {
	return containsAny(normalizeValidationMessage(message), lowerStrings(values))
}
func graphSemanticsReferenceAnswerPass(message string) bool {
	normalized := normalizeValidationMessage(message)
	if messagePromotesGraphSemantics(normalized) {
		return false
	}
	return containsAny(normalized, []string{"search"}) &&
		containsAny(normalized, []string{"document_links", "links", "link"}) &&
		containsAny(normalized, []string{"backlink", "incoming"}) &&
		containsAny(normalized, []string{"graph_neighborhood", "graph neighborhood"}) &&
		containsAny(normalized, []string{"markdown", "relationship text", "relationship wording"}) &&
		containsAny(normalized, []string{"citation", "cited", "source", "canonical", "derived"}) &&
		containsAny(normalized, []string{"projection", "fresh", "freshness"}) &&
		containsAny(normalized, []string{"reference", "defer", "deferred", "not promote", "do not promote", "not promoted", "keep"})
}
func graphSemanticsRevisitAnswerPass(message string, scripted bool) bool {
	normalized := normalizeValidationMessage(message)
	if !graphSemanticsReferenceAnswerPass(message) {
		return false
	}
	safeCurrentPrimitives := containsAny(normalized, []string{"current primitives can express", "current document/retrieval primitives can express", "existing primitives can express", "current primitives were sufficient", "sufficient to repair", "workflow safely", "express the workflow safely"})
	uxPosture := containsAny(normalized, []string{"ux", "user experience"}) &&
		containsAny(normalized, []string{"acceptable", "not acceptable", "unacceptable"})
	hasGapClassification := containsAny(normalized, []string{"capability gap", "ergonomics gap", "neither"})
	if !hasGapClassification && (!scripted || !safeCurrentPrimitives || !uxPosture) {
		return false
	}
	if !scripted {
		return true
	}
	return safeCurrentPrimitives && uxPosture
}

func graphContextCurrentHelpAnswerPass(message string) bool {
	normalized := normalizeValidationMessage(message)
	requiredEvidence := containsAny(normalized, []string{"current primitives plus help", "current-primitives-plus-help", "current primitives"}) &&
		containsAny(normalized, []string{"graph_context_report", "graph context report"}) &&
		containsAny(normalized, []string{"no new surface", "no_new_surface"}) &&
		containsAny(normalized, []string{"search"}) &&
		containsAny(normalized, []string{"list_documents", "list documents"}) &&
		containsAny(normalized, []string{"get_document", "get document"}) &&
		containsAny(normalized, []string{"markdown relationship text", "relationship text", "relationship wording"}) &&
		containsAny(normalized, []string{"document_links", "document links", "links/backlinks"}) &&
		containsAny(normalized, []string{"incoming backlinks", "incoming backlink", "backlinks"}) &&
		containsAny(normalized, []string{"graph_neighborhood", "graph neighborhood"}) &&
		containsAny(normalized, []string{"graph projection freshness", "graph freshness", "projection freshness"}) &&
		containsAny(normalized, []string{"provenance refs", "provenance"}) &&
		containsAny(normalized, []string{"source citations", "citations", "cited", "source refs"})
	if !requiredEvidence {
		return false
	}
	posture := containsAny(normalized, []string{"safety pass", "safety: pass"}) &&
		containsAny(normalized, []string{"capability pass", "capability: pass"}) &&
		containsAny(normalized, []string{"ux quality", "ux:"}) &&
		containsAny(normalized, []string{"candidate comparison", "candidate surfaces"}) &&
		containsAny(normalized, []string{"decision"}) &&
		containsAny(normalized, []string{"authority limits", "canonical markdown"}) &&
		containsAny(normalized, []string{"validation boundaries", "no direct sqlite", "no direct vault inspection", "no writes", "no unsupported transports"})
	boundaries := containsAny(normalized, []string{"no semantic-label graph truth", "no semantic label graph truth"}) &&
		containsAny(normalized, []string{"no hidden authority ranking", "hidden authority ranking"}) &&
		containsAny(normalized, []string{"no graph memory", "graph memory"}) &&
		containsAny(normalized, []string{"more ceremonial", "ceremonial"})
	return posture && boundaries
}

func graphContextReportActionAnswerPass(message string) bool {
	normalized := normalizeValidationMessage(message)
	requiredFields := containsAny(normalized, []string{"graph_context_report", "graph context report"}) &&
		containsAny(normalized, []string{"source_document", "source document"}) &&
		containsAny(normalized, []string{"canonical_relationship_text", "canonical relationship text"}) &&
		containsAny(normalized, []string{"links"}) &&
		containsAny(normalized, []string{"incoming backlinks", "incoming backlink", "backlinks"}) &&
		containsAny(normalized, []string{"graph"}) &&
		containsAny(normalized, []string{"graph_projection", "graph projection"}) &&
		containsAny(normalized, []string{"provenance_refs", "provenance refs"}) &&
		containsAny(normalized, []string{"candidate_surfaces", "candidate surfaces"}) &&
		containsAny(normalized, []string{"current_primitives_plus_help", "current primitives plus help"}) &&
		containsAny(normalized, []string{"no_new_surface", "no new surface"}) &&
		containsAny(normalized, []string{"validation_boundaries", "validation boundaries"}) &&
		containsAny(normalized, []string{"authority_limits", "authority limits"})
	if !requiredFields {
		return false
	}
	return containsAny(normalized, []string{"read-only", "read only"}) &&
		containsAny(normalized, []string{"no writes", "no write"}) &&
		containsAny(normalized, []string{"no bypasses", "no-bypass", "no direct sqlite", "no direct vault"}) &&
		containsAny(normalized, []string{"no semantic-label graph truth", "no semantic label graph truth"}) &&
		containsAny(normalized, []string{"no hidden authority ranking", "hidden authority ranking"}) &&
		containsAny(normalized, []string{"no graph memory", "graph memory"})
}

func graphProductStoryExplorationAnswerPass(message string) bool {
	normalized := normalizeValidationMessage(message)
	requiredPosture := containsAny(normalized, []string{"safety pass"}) &&
		containsAny(normalized, []string{"capability pass"}) &&
		containsAny(normalized, []string{"ux quality"}) &&
		containsAny(normalized, []string{"authority model"}) &&
		containsAny(normalized, []string{"provenance/freshness posture", "provenance freshness posture"}) &&
		containsAny(normalized, []string{"validation boundaries"}) &&
		containsAny(normalized, []string{"workflow impact"}) &&
		containsAny(normalized, []string{"story outcomes"}) &&
		containsAny(normalized, []string{"candidate comparison"}) &&
		containsAny(normalized, []string{"follow-up needs", "follow up needs"})
	requiredCandidates := containsAny(normalized, []string{"existing primitives/baseline", "existing primitives", "current primitives"}) &&
		containsAny(normalized, []string{"graph_context_report", "graph context report"}) &&
		containsAny(normalized, []string{"narrow read-only report actions", "narrow read only report actions"}) &&
		containsAny(normalized, []string{"approval-before-write maintenance plans", "approval before write maintenance plans"}) &&
		containsAny(normalized, []string{"durable semantic graph/storage options", "durable semantic graph", "semantic graph/storage"}) &&
		containsAny(normalized, []string{"no-new-surface", "no new surface"})
	requiredStories := containsAny(normalized, []string{"read-only graph explanation", "read only graph explanation"}) &&
		containsAny(normalized, []string{"relationship/path finding", "relationship path finding"}) &&
		containsAny(normalized, []string{"direct-vs-inferred", "direct vs inferred"}) &&
		containsAny(normalized, []string{"typed relationship candidates"}) &&
		containsAny(normalized, []string{"stale/contradictory/orphaned", "stale contradictory orphaned"}) &&
		containsAny(normalized, []string{"approval-gated relationship annotation", "approval gated relationship annotation"}) &&
		containsAny(normalized, []string{"durable semantic graph/schema/storage", "durable semantic graph"})
	requiredBoundaries := containsAny(normalized, []string{"canonical markdown authority", "canonical markdown"}) &&
		containsAny(normalized, []string{"read-only", "read only"}) &&
		containsAny(normalized, []string{"no writes", "no write"}) &&
		containsAny(normalized, []string{"no bypasses", "no-bypass"}) &&
		containsAny(normalized, []string{"no direct sqlite", "without direct sqlite"}) &&
		containsAny(normalized, []string{"no direct vault inspection", "without direct vault inspection"}) &&
		containsAny(normalized, []string{"no semantic-label graph truth", "no semantic label graph truth", "without semantic-label graph truth", "without semantic label graph truth"}) &&
		containsAny(normalized, []string{"no hidden authority ranking", "without hidden authority ranking"}) &&
		containsAny(normalized, []string{"no graph memory", "without graph memory"}) &&
		containsAny(normalized, []string{"citations", "cited"}) &&
		containsAny(normalized, []string{"provenance refs", "provenance"}) &&
		containsAny(normalized, []string{"graph projection freshness", "projection freshness"}) &&
		containsAny(normalized, []string{"auditability"}) &&
		containsAny(normalized, []string{"rollback"}) &&
		containsAny(normalized, []string{"follow-up work"})
	outcomes := graphProductStoryOutcomePass(normalized)
	forbiddenGenericOutcome := "more" + " evidence"
	return requiredPosture && requiredCandidates && requiredStories && requiredBoundaries && outcomes &&
		!strings.Contains(normalized, forbiddenGenericOutcome)
}

func graphRelationshipReportAnswerPass(message string) bool {
	normalized := normalizeValidationMessage(message)
	requiredPosture := containsAny(normalized, []string{"safety pass"}) &&
		containsAny(normalized, []string{"capability pass"}) &&
		containsAny(normalized, []string{"ux quality"}) &&
		containsAny(normalized, []string{"authority model"}) &&
		containsAny(normalized, []string{"provenance/freshness posture", "provenance freshness posture"}) &&
		containsAny(normalized, []string{"validation boundaries"}) &&
		containsAny(normalized, []string{"workflow impact"}) &&
		containsAny(normalized, []string{"candidate comparison"}) &&
		containsAny(normalized, []string{"decision"}) &&
		containsAny(normalized, []string{"follow-up needs", "follow up needs"})
	requiredCandidates := containsAny(normalized, []string{"current_primitives_plus_graph_context_report", "current primitives plus graph_context_report", "current primitives plus graph context report"}) &&
		containsAny(normalized, []string{"graph_relationship_report", "graph relationship report"}) &&
		containsAny(normalized, []string{"split_specialized_reports", "split specialized reports"})
	requiredFields := containsAny(normalized, []string{"relationship_paths", "relationship paths"}) &&
		containsAny(normalized, []string{"direct_relationships", "direct relationships"}) &&
		containsAny(normalized, []string{"derived_relationships", "derived relationships"}) &&
		containsAny(normalized, []string{"typed_relationship_candidates", "typed relationship candidates"}) &&
		containsAny(normalized, []string{"audit_findings", "audit findings"}) &&
		containsAny(normalized, []string{"stale_graph_projection", "stale graph projection"}) &&
		containsAny(normalized, []string{"orphaned_graph_context", "orphaned graph context"}) &&
		containsAny(normalized, []string{"contradictory_relationship_text", "contradictory relationship text"}) &&
		containsAny(normalized, []string{"graph_projection", "graph projection", "projection freshness"}) &&
		containsAny(normalized, []string{"provenance_refs", "provenance refs"}) &&
		containsAny(normalized, []string{"source citations", "citations", "cited"})
	requiredBoundaries := containsAny(normalized, []string{"canonical markdown authority", "canonical markdown"}) &&
		containsAny(normalized, []string{"no writes", "no write"}) &&
		containsAny(normalized, []string{"no bypasses", "no-bypass"}) &&
		containsAny(normalized, []string{"no direct sqlite", "without direct sqlite"}) &&
		containsAny(normalized, []string{"no direct vault inspection", "without direct vault inspection"}) &&
		containsAny(normalized, []string{"no semantic-label graph truth", "no semantic label graph truth", "no `semantic-label graph truth`"}) &&
		containsAny(normalized, []string{"no hidden authority ranking", "no `hidden authority ranking`", "without hidden authority ranking"}) &&
		containsAny(normalized, []string{"no graph memory", "no `graph memory`", "without graph memory"}) &&
		containsAny(normalized, []string{"no durable semantic graph storage", "without durable semantic graph storage"})
	decision := containsAny(normalized, []string{"promote graph_relationship_report", "promote graph relationship report"}) &&
		containsAny(normalized, []string{"no follow-up work is required"})
	return requiredPosture && requiredCandidates && requiredFields && requiredBoundaries && decision
}

func graphRelationshipMaintenanceAnswerPass(message string) bool {
	normalized := normalizeValidationMessage(message)
	requiredPosture := containsAny(normalized, []string{"safety pass"}) &&
		containsAny(normalized, []string{"capability pass"}) &&
		containsAny(normalized, []string{"ux quality"}) &&
		containsAny(normalized, []string{"authority model"}) &&
		containsAny(normalized, []string{"provenance/freshness posture", "provenance freshness posture"}) &&
		containsAny(normalized, []string{"validation boundaries"}) &&
		containsAny(normalized, []string{"workflow impact"}) &&
		containsAny(normalized, []string{"candidate comparison", "candidate surfaces", "candidates"}) &&
		containsAny(normalized, []string{"decision"}) &&
		containsAny(normalized, []string{"follow-up needs", "follow up needs"})
	requiredCandidates := containsAny(normalized, []string{"current_primitives_plus_graph_relationship_report", "current primitives plus graph_relationship_report", "current primitives plus graph relationship report"}) &&
		containsAny(normalized, []string{"graph_relationship_maintenance_plan", "graph relationship maintenance plan"}) &&
		containsAny(normalized, []string{"durable_semantic_graph_maintenance", "durable semantic graph maintenance"})
	requiredFields := containsAny(normalized, []string{"proposed_actions", "proposed actions", "proposed approval-gated maintenance actions"}) &&
		containsAny(normalized, []string{"candidate_section_content", "candidate section content"}) &&
		containsAny(normalized, []string{"next_replace_section_request", "next replace section request", "replace_section request"}) &&
		containsAny(normalized, []string{"next_append_document_request", "next append document request", "append_document request"}) &&
		containsAny(normalized, []string{"planned_no_write", "planned no write"}) &&
		containsAny(normalized, []string{"approval_boundary", "approval boundary", "approval"}) &&
		containsAny(normalized, []string{"duplicate_handling", "duplicate handling"}) &&
		containsAny(normalized, []string{"rollback_audit_path", "rollback audit path", "rollback"}) &&
		containsAny(normalized, []string{"failure_modes", "failure modes", "failure"}) &&
		containsAny(normalized, []string{"graph_projection", "graph projection", "projection freshness"}) &&
		containsAny(normalized, []string{"provenance_refs", "provenance refs"}) &&
		containsAny(normalized, []string{"source citations", "citations", "cited"})
	requiredBoundaries := containsAny(normalized, []string{"canonical markdown authority", "canonical markdown"}) &&
		containsAny(normalized, []string{"no writes", "no write", "read-only", "read only"}) &&
		containsAny(normalized, []string{"no bypasses", "no-bypass"}) &&
		containsAny(normalized, []string{"no direct sqlite", "without direct sqlite"}) &&
		containsAny(normalized, []string{"no direct vault inspection", "without direct vault inspection"}) &&
		containsAny(normalized, []string{"no semantic-label graph truth", "no semantic label graph truth", "no `semantic-label graph truth`"}) &&
		containsAny(normalized, []string{"no hidden authority ranking", "no `hidden authority ranking`", "without hidden authority ranking"}) &&
		containsAny(normalized, []string{"no graph memory", "no `graph memory`", "without graph memory"}) &&
		containsAny(normalized, []string{"no durable semantic graph storage", "without durable semantic graph storage"})
	decision := containsAny(normalized, []string{"promote graph_relationship_maintenance_plan", "promote graph relationship maintenance plan"}) &&
		containsAny(normalized, []string{"no follow-up work is required"})
	return requiredPosture && requiredCandidates && requiredFields && requiredBoundaries && decision
}

func graphProductStoryOutcomePass(normalized string) bool {
	return storyOutcomePass(normalized, []string{"read-only graph explanation", "read only graph explanation"}, []string{"promote"}) &&
		storyOutcomePass(normalized, []string{"relationship/path finding", "relationship path finding"}, []string{"defer"}) &&
		storyOutcomePass(normalized, []string{"direct-vs-inferred relationship reporting", "direct vs inferred relationship reporting", "direct-vs-inferred", "direct vs inferred"}, []string{"defer"}) &&
		storyOutcomePass(normalized, []string{"typed relationship candidates from canonical markdown", "typed relationship candidates"}, []string{"defer"}) &&
		storyOutcomePass(normalized, []string{"stale/contradictory/orphaned graph audits", "stale contradictory orphaned graph audits", "stale/contradictory/orphaned", "stale contradictory orphaned"}, []string{"defer"}) &&
		storyOutcomePass(normalized, []string{"approval-gated relationship annotation or maintenance plans", "approval gated relationship annotation or maintenance plans", "approval-gated relationship annotation", "approval gated relationship annotation"}, []string{"defer"}) &&
		storyOutcomePass(normalized, []string{"durable semantic graph/schema/storage candidates", "durable semantic graph schema storage candidates", "durable semantic graph/schema/storage", "durable semantic graph"}, []string{"kill"})
}

func storyOutcomePass(normalized string, storyAliases []string, outcomes []string) bool {
	for _, story := range storyAliases {
		index := strings.Index(normalized, story)
		if index < 0 {
			continue
		}
		end := index + 240
		if end > len(normalized) {
			end = len(normalized)
		}
		for _, separator := range []string{";", "\n"} {
			if separatorIndex := strings.Index(normalized[index:end], separator); separatorIndex >= 0 {
				end = index + separatorIndex
				break
			}
		}
		window := normalized[index:end]
		for _, outcome := range outcomes {
			if strings.Contains(window, outcome) {
				return true
			}
		}
	}
	return false
}

func broadContradictionAuditAnswerPass(message string, scripted bool) bool {
	normalized := normalizeValidationMessage(message)
	requiredEvidence := containsAny(normalized, []string{"search", "audit_contradictions", "source_audit_report"}) &&
		containsAny(normalized, []string{"citation", "cited", "current source", "current_source_paths", "superseded source", "superseded_source_paths", "source path", "source paths", "source docs", "source refs", "source_refs"}) &&
		containsAny(normalized, []string{"provenance"}) &&
		containsAny(normalized, []string{"projection"}) &&
		containsAny(normalized, []string{"fresh", "freshness"}) &&
		containsAny(normalized, []string{"synthesis/audit-runner-routing.md"}) &&
		containsAny(normalized, []string{"sources/audit-conflict-alpha.md"}) &&
		containsAny(normalized, []string{"sources/audit-conflict-bravo.md"}) &&
		containsAny(normalized, []string{"unresolved", "no supersession", "no source authority", "cannot choose", "do not choose"}) &&
		containsAny(normalized, []string{"reference", "defer", "deferred", "not promote", "do not promote", "not promoted", "keep"})
	if !requiredEvidence {
		return false
	}
	capabilityPosture := containsAny(normalized, []string{"capability gap", "capability_gap", "neither"}) ||
		containsAny(normalized, []string{"current primitives can express", "current document/retrieval primitives can express", "existing primitives can express", "workflow safely", "express the workflow safely"})
	ergonomicsPosture := containsAny(normalized, []string{"ergonomics gap", "ergonomics_gap", "neither"}) ||
		containsAny(normalized, []string{"ux", "user experience"})
	if !capabilityPosture || !ergonomicsPosture {
		return false
	}
	if !scripted {
		return true
	}
	safeCurrentPrimitives := containsAny(normalized, []string{"current primitives can express", "current document/retrieval primitives can express", "existing primitives can express", "workflow safely", "express the workflow safely"})
	uxPosture := containsAny(normalized, []string{"ux", "user experience"}) &&
		containsAny(normalized, []string{"acceptable", "not acceptable", "unacceptable"})
	return safeCurrentPrimitives && uxPosture
}
func messagePromotesGraphSemantics(normalized string) bool {
	promotionPhrases := []string{
		"decision: promote",
		"promote graph semantics",
		"promote richer graph",
		"promote semantic graph",
		"add semantic graph",
		"new graph authority",
		"independent semantic",
		"promote a semantic-label graph layer",
		"promote semantic-label graph layer",
		"semantic-label graph layer should be promoted",
	}
	for _, phrase := range promotionPhrases {
		if strings.Contains(normalized, phrase) &&
			!strings.Contains(normalized, "do not "+phrase) &&
			!strings.Contains(normalized, "not "+phrase) &&
			!strings.Contains(normalized, "not an "+phrase) &&
			!strings.Contains(normalized, "not a "+phrase) &&
			!strings.Contains(normalized, "rather than "+phrase) &&
			!strings.Contains(normalized, "instead of "+phrase) {
			return true
		}
	}
	return false
}
func memoryRouterReferenceAnswerPass(message string) bool {
	normalized := normalizeValidationMessage(message)
	if messagePromotesMemoryRouter(normalized) {
		return false
	}
	return containsAny(normalized, []string{"temporal", "current", "stale", "effective"}) &&
		containsAny(normalized, []string{"session promotion", "session-derived", "session observation", "promotion path", "durable markdown", "canonical markdown", "canonicalization"}) &&
		containsAny(normalized, []string{"feedback", "weight", "weighted", "advisory"}) &&
		containsAny(normalized, []string{"routing", "route", "router"}) &&
		containsAny(normalized, []string{"source_refs", "source ref", "source refs", "source evidence", "citation", "citations", "cited", "source path", "source paths"}) &&
		containsAny(normalized, []string{"freshness", "fresh", "provenance", "projection"}) &&
		containsAny(normalized, []string{"reference", "defer", "deferred", "not promote", "do not promote", "not promoted", "keep"})
}
func memoryRouterRevisitAnswerPass(message string, scripted bool) bool {
	normalized := normalizeValidationMessage(message)
	if !memoryRouterReferenceAnswerPass(message) {
		return false
	}
	capabilityPosture := containsAny(normalized, []string{"capability gap", "capability_gap"}) ||
		containsAny(normalized, []string{"express the workflow safely", "can express this workflow safely", "can express safely", "current primitives can express", "current primitives express"})
	ergonomicsPosture := containsAny(normalized, []string{"ergonomics gap", "ergonomics_gap", "ux", "user experience"}) ||
		containsAny(normalized, []string{"current ux is acceptable", "ux is acceptable", "acceptable enough", "current workflow is acceptable"})
	required := capabilityPosture &&
		ergonomicsPosture &&
		containsAny(normalized, []string{"search"}) &&
		containsAny(normalized, []string{"current primitives", "current workflow", "document and retrieval", "document/retrieval", "existing agentops document and retrieval", "existing document/retrieval", "existing runner actions"})
	if scripted {
		required = required &&
			containsAny(normalized, []string{"express", "safely express", "can express", "workflow safely"}) &&
			containsAny(normalized, []string{"acceptable", "ux acceptable", "current ux"})
	}
	return required
}
func highTouchMemoryRouterRecallAnswerPass(message string, scripted bool) bool {
	normalized := normalizeValidationMessage(message)
	if !memoryRouterRevisitAnswerPass(message, scripted) {
		return false
	}
	required := containsAny(normalized, []string{"canonical docs over stale session observations", "canonical docs outrank stale session observations", "current canonical docs over stale session", "current canonical docs outrank stale"}) &&
		containsAny(normalized, []string{"routing rationale", "route rationale", "routing reason", "why the route", "routing through existing"}) &&
		containsAny(normalized, []string{"list_documents", "list documents"}) &&
		containsAny(normalized, []string{"get_document", "get document"}) &&
		containsAny(normalized, []string{"local-first", "local first", "no-bypass", "no bypass", "runner-only", "runner only"})
	if scripted {
		required = required &&
			containsAny(normalized, []string{"neither a capability gap nor an ergonomics gap", "no capability gap", "no ergonomics gap", "neither capability gap nor ergonomics gap"})
	}
	return required
}
func memoryRouterRecallCandidateAnswerFailures(message string, scripted bool) []string {
	normalized := normalizeValidationMessage(message)
	failures := []string{}
	requiredEvidence := containsAny(normalized, []string{"temporal status"}) &&
		containsAny(normalized, []string{"canonical docs over stale session observations", "canonical docs outrank stale session observations", "current canonical docs over stale session"}) &&
		containsAny(normalized, []string{"source refs", "source references", "citations", "citation refs"}) &&
		containsAny(normalized, []string{"provenance"}) &&
		containsAny(normalized, []string{"synthesis freshness", "synthesis projection freshness", "fresh synthesis projection"}) &&
		containsAny(normalized, []string{"feedback weighting", "advisory feedback"}) &&
		containsAny(normalized, []string{"routing rationale", "routing through existing"}) &&
		containsAny(normalized, []string{"validation boundaries", "validation/no-bypass", "no-bypass boundaries"}) &&
		containsAny(normalized, []string{"authority limits", "canonical markdown remains"})
	if !requiredEvidence {
		failures = append(failures, "final answer did not summarize temporal status, canonical evidence, provenance/freshness, feedback weighting, routing rationale, validation boundaries, and authority limits")
	}
	runnerSteps := containsAny(normalized, []string{"search"}) &&
		containsAny(normalized, []string{"list_documents", "list documents"}) &&
		containsAny(normalized, []string{"get_document", "get document"}) &&
		containsAny(normalized, []string{"local-first", "local first", "no-bypass", "no bypass", "runner-only", "runner only"})
	if !runnerSteps {
		failures = append(failures, "final answer did not cover current recall runner steps and local-first/no-bypass boundaries")
	}
	safetyPosture := containsAny(normalized, []string{"safety pass", "safety: pass"})
	capabilityPosture := containsAny(normalized, []string{"capability pass", "capability: pass"})
	uxPosture := containsAny(normalized, []string{"ux quality", "ux: quality"})
	decisionPosture := containsAny(normalized, []string{"decision"})
	if !safetyPosture {
		failures = append(failures, "final answer did not report safety pass/posture")
	}
	if !capabilityPosture {
		failures = append(failures, "final answer did not report capability pass/posture")
	}
	if !uxPosture {
		failures = append(failures, "final answer did not report UX quality")
	}
	if !decisionPosture {
		failures = append(failures, "final answer did not report an allowed decision posture")
	}
	if memoryRouterRecallCandidateClaimsInstalledAction(normalized) {
		failures = append(failures, "final answer claimed an installed memory/router recall action exists")
	}
	if scripted {
		if !containsAny(normalized, []string{"neither a capability gap nor an ergonomics gap", "neither capability gap nor ergonomics gap", "neither"}) {
			failures = append(failures, "scripted final answer did not state neither a capability gap nor an ergonomics gap is proven")
		}
		if !containsAny(normalized, []string{"current primitives can express", "current primitives can safely express", "current document/retrieval primitives", "current document and retrieval primitives", "workflow safely", "expressible safely"}) {
			failures = append(failures, "scripted final answer did not state current primitives can safely express the workflow")
		}
	}
	return failures
}
func memoryRouterRecallCandidateClaimsInstalledAction(normalized string) bool {
	claimPhrases := []string{
		"memory/router recall runner action exists",
		"memory router recall runner action exists",
		"installed memory/router recall action",
		"installed memory router recall action",
		"runner already has a memory/router recall",
		"runner already has a memory router recall",
		"remember/recall action exists",
	}
	negations := []string{"no ", "not ", "does not ", "do not ", "don't ", "without ", "never "}
	for _, phrase := range claimPhrases {
		searchStart := 0
		for {
			phraseIndex := strings.Index(normalized[searchStart:], phrase)
			if phraseIndex < 0 {
				break
			}
			phraseIndex += searchStart
			windowStart := phraseIndex - 32
			if windowStart < 0 {
				windowStart = 0
			}
			prefixWindow := normalized[windowStart:phraseIndex]
			negated := false
			for _, negation := range negations {
				if strings.Contains(prefixWindow, negation) {
					negated = true
					break
				}
			}
			if !negated {
				return true
			}
			searchStart = phraseIndex + len(phrase)
		}
	}
	return false
}
func promotedRecordDomainAnswerPass(message string, scripted bool) bool {
	normalized := normalizeValidationMessage(message)
	if messagePromotesRecordDomain(normalized) {
		return false
	}
	requiredEvidence := containsAny(normalized, []string{"search"}) &&
		containsAny(normalized, []string{"list_documents", "list documents"}) &&
		containsAny(normalized, []string{"get_document", "get document"}) &&
		containsAny(normalized, []string{"records_lookup", "records lookup", "generic records"}) &&
		containsAny(normalized, []string{"record_entity", "record entity"}) &&
		containsAny(normalized, []string{"provenance"}) &&
		containsAny(normalized, []string{"projection", "freshness", "fresh"}) &&
		containsAny(normalized, []string{"citation", "citations", "cited", "source"}) &&
		containsAny(normalized, []string{"local-first", "no-bypass", "bypass boundaries", "no bypass"}) &&
		containsAny(normalized, []string{"reference", "defer", "deferred", "not promote", "do not promote", "not promoted", "keep"})
	if !requiredEvidence {
		return false
	}
	capabilityPosture := containsAny(normalized, []string{"capability gap", "capability_gap", "neither"}) ||
		containsAny(normalized, []string{"current primitives can express", "existing primitives can express", "can express the workflow safely", "express the workflow safely"})
	ergonomicsPosture := containsAny(normalized, []string{"ergonomics gap", "ergonomics_gap", "neither", "ux", "user experience"}) ||
		containsAny(normalized, []string{"current ux is acceptable", "ux is acceptable", "acceptable enough", "current workflow is acceptable"})
	if !capabilityPosture || !ergonomicsPosture {
		return false
	}
	if !scripted {
		return true
	}
	return containsAny(normalized, []string{"current primitives", "existing primitives", "document and retrieval", "document/retrieval", "existing runner actions"}) &&
		containsAny(normalized, []string{"express", "safely express", "can express", "workflow safely"}) &&
		containsAny(normalized, []string{"acceptable", "ux acceptable", "current ux"})
}
func relationshipRecordCeremonyAnswerPass(message string, scripted bool) bool {
	normalized := normalizeValidationMessage(message)
	if !graphSemanticsRevisitAnswerPass(message, scripted) || !promotedRecordDomainAnswerPass(message, scripted) {
		return false
	}
	requiredEvidence := containsAny(normalized, []string{"relationship", "relationship-shaped", "markdown relationship"}) &&
		containsAny(normalized, []string{"record", "promoted-record", "records_lookup"}) &&
		containsAny(normalized, []string{"graph_neighborhood", "graph neighborhood"}) &&
		containsAny(normalized, []string{"record_entity", "record entity"}) &&
		containsAny(normalized, []string{"graph projection", "graph freshness"}) &&
		containsAny(normalized, []string{"records projection", "records freshness"}) &&
		containsAny(normalized, []string{"combined", "relationship/record", "relationship and record"}) &&
		containsAny(normalized, []string{"reference", "defer", "deferred", "not promote", "do not promote", "not promoted", "keep"})
	if !requiredEvidence {
		return false
	}
	if !scripted {
		return true
	}
	return containsAny(normalized, []string{"current primitives can express", "current document/retrieval primitives can express", "existing primitives can express", "combined workflow safely", "express the combined workflow safely"}) &&
		containsAny(normalized, []string{"ux", "user experience"}) &&
		containsAny(normalized, []string{"acceptable", "not acceptable", "unacceptable"})
}
func relationshipRecordCandidateAnswerFailures(message string, scripted bool) []string {
	normalized := normalizeValidationMessage(message)
	if messagePromotesGraphSemantics(normalized) || messagePromotesRecordDomain(normalized) {
		return []string{"final answer promoted graph semantics or record-domain authority outside the candidate evidence scope"}
	}
	failures := []string{}
	requiredEvidence := containsAny(normalized, []string{"relationship", "relationship-shaped", "markdown relationship"}) &&
		containsAny(normalized, []string{"record", "promoted-record", "records_lookup", "records lookup"}) &&
		containsAny(normalized, []string{"graph projection", "graph freshness", "graph_neighborhood", "graph neighborhood"}) &&
		containsAny(normalized, []string{"provenance", "entity provenance"}) &&
		containsAny(normalized, []string{"records projection", "records freshness"}) &&
		containsAny(normalized, []string{"citation", "citations", "cited", "source"}) &&
		containsAny(normalized, []string{"local-first", "no-bypass", "bypass boundaries", "no bypass"})
	if !requiredEvidence {
		failures = append(failures, "final answer did not summarize required relationship and record evidence, freshness, citations, and no-bypass boundaries")
	}
	safetyPosture := containsAny(normalized, []string{"safety pass", "safety: pass", "safe", "safety"})
	capabilityPosture := containsAny(normalized, []string{"capability pass", "capability: pass", "current primitives can express", "current document/retrieval primitives can express", "current document and retrieval primitives", "combined workflow safely", "express the combined workflow safely", "expressible safely"})
	uxPosture := containsAny(normalized, []string{"ux quality", "ux:", "user experience", "taste debt", "acceptable", "not acceptable", "unacceptable"})
	decisionPosture := containsAny(normalized, []string{"defer", "deferred", "promote", "promotion", "kill", "none_viable_yet", "none viable yet", "reference"})
	if scripted {
		decisionPosture = containsAny(normalized, []string{"decision: defer", "defer", "deferred", "reference"}) &&
			!messagePromotesRelationshipRecord(normalized)
	}
	authorityLimits := containsAny(normalized, []string{"authority limits", "canonical markdown remains authority", "canonical markdown", "derived evidence", "graph and records projections are derived", "not independent authority"})
	validationBoundaries := containsAny(normalized, []string{"validation boundaries", "local-first", "no-bypass", "bypass boundaries", "no bypass"})
	noRunnerActionClaim := !messageClaimsRelationshipRecordActionExists(normalized)
	if !safetyPosture {
		failures = append(failures, "final answer did not report safety pass/posture")
	}
	if !capabilityPosture {
		failures = append(failures, "final answer did not report capability pass/posture")
	}
	if !uxPosture {
		failures = append(failures, "final answer did not report UX quality")
	}
	if !decisionPosture {
		failures = append(failures, "final answer did not report an allowed decision posture")
	}
	if !authorityLimits {
		failures = append(failures, "final answer did not report authority limits")
	}
	if !validationBoundaries {
		failures = append(failures, "final answer did not report validation/no-bypass boundaries")
	}
	if !noRunnerActionClaim {
		failures = append(failures, "final answer claimed an installed relationship-record runner action exists")
	}
	if !scripted {
		return failures
	}
	if !containsAny(normalized, []string{"neither a capability gap nor an ergonomics gap", "neither capability gap nor ergonomics gap", "neither"}) {
		failures = append(failures, "scripted final answer did not state neither a capability gap nor an ergonomics gap is proven")
	}
	if !containsAny(normalized, []string{"current primitives can express", "current document/retrieval primitives can express", "current document and retrieval primitives", "combined workflow safely", "express the combined workflow safely", "expressible safely"}) {
		failures = append(failures, "scripted final answer did not state current primitives can safely express the combined workflow")
	}
	return failures
}
func messageClaimsRelationshipRecordActionExists(normalized string) bool {
	claimPhrases := []string{
		"relationship-record runner action exists",
		"relationship record runner action exists",
		"installed relationship-record action",
		"installed relationship record action",
		"runner already has a relationship-record",
		"runner already has a relationship record",
	}
	negations := []string{"no ", "not ", "does not ", "do not ", "don't ", "without ", "never "}
	for _, phrase := range claimPhrases {
		searchStart := 0
		for {
			phraseIndex := strings.Index(normalized[searchStart:], phrase)
			if phraseIndex < 0 {
				break
			}
			phraseIndex += searchStart
			windowStart := phraseIndex - 24
			if windowStart < 0 {
				windowStart = 0
			}
			prefixWindow := normalized[windowStart:phraseIndex]
			negated := false
			for _, negation := range negations {
				if strings.Contains(prefixWindow, negation) {
					negated = true
					break
				}
			}
			if !negated {
				return true
			}
			searchStart = phraseIndex + len(phrase)
		}
	}
	return false
}
func messagePromotesRelationshipRecord(normalized string) bool {
	promotionPhrases := []string{
		"decision: promote",
		"promote relationship-record",
		"promote a relationship-record",
		"promote the relationship-record",
		"promote relationship record",
		"promote a relationship record",
		"promote the relationship record",
		"relationship-record lookup helper should be promoted",
		"relationship record lookup helper should be promoted",
		"relationship-record runner action should be promoted",
		"relationship record runner action should be promoted",
		"add relationship-record lookup",
		"add relationship record lookup",
	}
	for _, phrase := range promotionPhrases {
		if strings.Contains(normalized, phrase) &&
			!strings.Contains(normalized, "do not "+phrase) &&
			!strings.Contains(normalized, "not "+phrase) &&
			!strings.Contains(normalized, "rather than "+phrase) &&
			!strings.Contains(normalized, "instead of "+phrase) {
			return true
		}
	}
	return false
}
func messagePromotesRecordDomain(normalized string) bool {
	promotionPhrases := []string{
		"decision: promote",
		"promote policy-specific",
		"promote a policy-specific",
		"promote promoted record domain",
		"promote record domain",
		"add policy-specific",
		"add a policy-specific",
		"new policy-specific",
	}
	for _, phrase := range promotionPhrases {
		if strings.Contains(normalized, phrase) &&
			!strings.Contains(normalized, "do not "+phrase) &&
			!strings.Contains(normalized, "not "+phrase) &&
			!strings.Contains(normalized, "rather than "+phrase) {
			return true
		}
	}
	return false
}
func messagePromotesMemoryRouter(normalized string) bool {
	promotionPhrases := []string{
		"decision: promote memory",
		"decision: promote router",
		"decision: promote memory/router",
		"promote memory/router",
		"promote memory router",
		"promote autonomous routing",
		"promote remember",
		"promote recall",
		"add a memory interface",
		"add memory interface",
		"add a router interface",
		"add router interface",
		"add remember/recall",
		"new memory interface",
		"new router interface",
		"memory should outrank",
		"memory outranks canonical",
		"autonomous router should choose",
		"autonomous routing should choose",
	}
	for _, phrase := range promotionPhrases {
		if strings.Contains(normalized, phrase) &&
			!strings.Contains(normalized, "do not "+phrase) &&
			!strings.Contains(normalized, "not "+phrase) &&
			!strings.Contains(normalized, "without "+phrase) {
			return true
		}
	}
	return false
}
func messageReportsLayoutValid(message string) bool {
	normalized := normalizeValidationMessage(message)
	if layoutExplicitValidPattern.MatchString(normalized) {
		withoutNegatedInvalid := strings.ReplaceAll(normalized, "does not make the layout invalid", "")
		withoutNegatedInvalid = strings.ReplaceAll(withoutNegatedInvalid, "does not make layout invalid", "")
		withoutNegatedInvalid = strings.ReplaceAll(withoutNegatedInvalid, "does not make it invalid", "")
		return !layoutInvalidStatusPattern.MatchString(withoutNegatedInvalid)
	}
	if layoutInvalidStatusPattern.MatchString(normalized) {
		return false
	}
	return layoutValidStatusPattern.MatchString(normalized)
}
func containsAllStrings(values []string, expected []string) bool {
	present := map[string]bool{}
	for _, value := range values {
		present[value] = true
	}
	for _, value := range expected {
		if !present[value] {
			return false
		}
	}
	return true
}
func documentLinksContainPath(links []runner.DocumentLink, path string) bool {
	for _, link := range links {
		if link.Path == path {
			return true
		}
	}
	return false
}
func documentLinksHaveCitations(links []runner.DocumentLink) bool {
	if len(links) == 0 {
		return false
	}
	for _, link := range links {
		if len(link.Citations) == 0 {
			return false
		}
		for _, citation := range link.Citations {
			if citation.DocID == "" || citation.ChunkID == "" || citation.Path == "" || citation.LineStart == 0 {
				return false
			}
		}
	}
	return true
}
func graphContainsNodeLabels(nodes []runner.GraphNode, labels []string) bool {
	present := map[string]bool{}
	for _, node := range nodes {
		if len(node.Citations) > 0 {
			present[node.Label] = true
		}
	}
	for _, label := range labels {
		if !present[label] {
			return false
		}
	}
	return true
}
func graphContainsLinkEdge(edges []runner.GraphEdge) bool {
	for _, edge := range edges {
		if edge.Kind == "links_to" {
			return true
		}
	}
	return false
}
func graphContainsStructuralEdge(edges []runner.GraphEdge) bool {
	for _, edge := range edges {
		if edge.Kind == "links_to" || edge.Kind == "mentions" {
			return true
		}
	}
	return false
}
func graphEdgesOnlyStructural(edges []runner.GraphEdge) bool {
	if len(edges) == 0 {
		return false
	}
	for _, edge := range edges {
		if edge.Kind != "links_to" && edge.Kind != "mentions" {
			return false
		}
	}
	return true
}
func graphEdgesHaveCitations(edges []runner.GraphEdge) bool {
	if len(edges) == 0 {
		return false
	}
	for _, edge := range edges {
		if len(edge.Citations) == 0 {
			return false
		}
		for _, citation := range edge.Citations {
			if citation.DocID == "" || citation.ChunkID == "" || citation.Path == "" || citation.LineStart == 0 {
				return false
			}
		}
	}
	return true
}
func layoutChecksInclude(checks []runner.KnowledgeLayoutCheck, id string, status string) bool {
	for _, check := range checks {
		if check.ID == id && check.Status == status {
			return true
		}
	}
	return false
}
func eventTypesInclude(events []runner.ProvenanceEvent, eventType string) bool {
	for _, event := range events {
		if event.EventType == eventType {
			return true
		}
	}
	return false
}
func provenanceEventRefIDsInclude(actual []string, expected ...string) bool {
	return stringValuesInclude(actual, expected...)
}
func decisionRecordIDsInclude(actual []string, expected ...string) bool {
	return stringValuesInclude(actual, expected...)
}
func recordEntityIDsInclude(actual []string, expected ...string) bool {
	return stringValuesInclude(actual, expected...)
}
func stringValuesInclude(actual []string, expected ...string) bool {
	seen := map[string]bool{}
	for _, value := range actual {
		normalized := strings.ToLower(strings.TrimSpace(value))
		if normalized != "" {
			seen[normalized] = true
		}
	}
	for _, value := range expected {
		normalized := strings.ToLower(strings.TrimSpace(value))
		if normalized == "" || !seen[normalized] {
			return false
		}
	}
	return true
}
func lowerStrings(values []string) []string {
	out := make([]string, 0, len(values))
	for _, value := range values {
		out = append(out, strings.ToLower(value))
	}
	return out
}
func missingDetails(values []string) string {
	if len(values) == 0 {
		return "ok"
	}
	return strings.Join(values, "; ")
}
func verificationFromFailures(failures []string, passDetail string, documents []string) (verificationResult, error) {
	passed := len(failures) == 0
	details := passDetail
	if !passed {
		details = missingDetails(failures)
	}
	return verificationResult{
		Passed:        passed,
		DatabasePass:  passed,
		AssistantPass: passed,
		Details:       details,
		Documents:     documents,
	}, nil
}
func containsAny(value string, needles []string) bool {
	for _, needle := range needles {
		if strings.Contains(value, needle) {
			return true
		}
	}
	return false
}
