438 lines
31 KiB
Go
438 lines
31 KiB
Go
package runmgr
|
||
|
||
import (
|
||
core "agent-system-core"
|
||
"encoding/json"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
func evidenceTestSources() materialSourceRegistry {
|
||
docs := []core.ParsedDocument{
|
||
{Meta: core.DocMeta{Filename: "需求.txt", Origin: &core.DocumentOrigin{ArtifactID: "req", ContentHash: "hash-req", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "上线六月。需求规定总预算为50万元,交付包含12项需求,其中3项依赖外部接口。"}}},
|
||
{Meta: core.DocMeta{Filename: "会议.txt", Origin: &core.DocumentOrigin{ArtifactID: "meeting", ContentHash: "hash-meeting", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "会议决定分两期交付,第一期先完成8项核心需求。总预算暂按50万元评估,但审批尚未完成。"}}},
|
||
{Meta: core.DocMeta{Filename: "评审.txt", Origin: &core.DocumentOrigin{ArtifactID: "review", ContentHash: "hash-review", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "评审意见要求上线七月以留出接口联调时间;不等于已批准增加经费。"}}},
|
||
}
|
||
return materialSources(docs, 48000)
|
||
}
|
||
|
||
func evidenceQuote(t *testing.T, sources materialSourceRegistry, artifact, quote string) core.TaskSourceQuote {
|
||
t.Helper()
|
||
for id, source := range sources {
|
||
if source.Ref.ArtifactID == artifact {
|
||
return core.TaskSourceQuote{SourceID: id, BlockIndex: source.Ref.BlockIndex, Quote: quote}
|
||
}
|
||
}
|
||
t.Fatalf("missing artifact %s", artifact)
|
||
return core.TaskSourceQuote{}
|
||
}
|
||
|
||
func evidenceCandidate(t *testing.T, sources materialSourceRegistry, mode string) core.WorkCheckpointCandidate {
|
||
t.Helper()
|
||
n := func(v int) *int { return &v }
|
||
return core.WorkCheckpointCandidate{CheckpointID: "cp-evidence", PhaseID: "crosscheck", Findings: []core.WorkFindingCandidate{{
|
||
FindingKey: "phase-list-gap", Text: "剩余4项包含全部3项接口依赖需求", Kind: "analysis", Assumptions: "把总量中的依赖项视作第二期清单",
|
||
Sources: []core.TaskSourceQuote{
|
||
evidenceQuote(t, sources, "req", "交付包含12项需求,其中3项依赖外部接口"),
|
||
evidenceQuote(t, sources, "meeting", "第一期先完成8项核心需求"),
|
||
},
|
||
Evidence: &core.WorkFindingEvidence{Mode: mode, Premises: []core.WorkEvidencePremise{
|
||
{Role: "total", SourceIndex: 0, Value: n(12)},
|
||
{Role: "phase", SourceIndex: 1, Value: n(8)},
|
||
{Role: "dependent", SourceIndex: 0, Value: n(3)},
|
||
}},
|
||
}}}
|
||
}
|
||
|
||
func TestMaterialEvidenceUnsupportedMembershipBecomesPending(t *testing.T) {
|
||
sources := evidenceTestSources()
|
||
candidate := evidenceCandidate(t, sources, "unresolved_relation")
|
||
normalized, notes, err := normalizeMaterialEvidence(candidate, sources)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
f := normalized.Findings[0]
|
||
if f.Kind != "needs_user" || strings.Contains(f.Text+f.Impact+f.Assumptions, "包含全部3项") || !strings.Contains(f.Text, "0–3") {
|
||
t.Fatalf("unsupported membership escaped: %+v", f)
|
||
}
|
||
if len(notes) == 0 || !strings.Contains(strings.Join(notes, " "), "待确认") {
|
||
t.Fatal("model did not receive downgrade receipt", notes)
|
||
}
|
||
}
|
||
|
||
func TestMaterialEvidenceDifferenceUsesOnlyVerifiedCounts(t *testing.T) {
|
||
sources := evidenceTestSources()
|
||
candidate := evidenceCandidate(t, sources, "difference")
|
||
candidate.Findings[0].Evidence.Premises = candidate.Findings[0].Evidence.Premises[:2]
|
||
result := 4
|
||
candidate.Findings[0].Evidence.Result = &result
|
||
normalized, _, err := normalizeMaterialEvidence(candidate, sources)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
f := normalized.Findings[0]
|
||
if f.Kind != "analysis" || !strings.Contains(f.Text, "12−8=4") || !strings.Contains(f.Text, "推算") || strings.Contains(f.Text+f.Impact+f.Assumptions, "包含全部3项") {
|
||
t.Fatalf("difference was not safely rewritten: %+v", f)
|
||
}
|
||
result = 5
|
||
candidate.Findings[0].Evidence.Result = &result
|
||
normalized, _, err = normalizeMaterialEvidence(candidate, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "needs_user" {
|
||
t.Fatalf("wrong model result became a fact: %+v %v", normalized, err)
|
||
}
|
||
}
|
||
|
||
func TestMaterialEvidenceDirectRelationRequiresLiteralQuote(t *testing.T) {
|
||
docs := []core.ParsedDocument{{Meta: core.DocMeta{Filename: "决定.txt", Origin: &core.DocumentOrigin{ArtifactID: "direct", ContentHash: "hash-direct", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "第二期包含3项依赖外部接口的需求。接口负责人仍待确认。"}}}}
|
||
sources := materialSources(docs, 48000)
|
||
quote := evidenceQuote(t, sources, "direct", "第二期包含3项依赖外部接口的需求")
|
||
candidate := core.WorkCheckpointCandidate{CheckpointID: "direct", PhaseID: "crosscheck", Findings: []core.WorkFindingCandidate{{FindingKey: "relation", Text: "模型概括", Kind: "material_record", Sources: []core.TaskSourceQuote{quote}, Evidence: &core.WorkFindingEvidence{Mode: "direct_relation", Premises: []core.WorkEvidencePremise{{Role: "direct_relation", SourceIndex: 0}}}}}}
|
||
normalized, _, err := normalizeMaterialEvidence(candidate, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "material_record" || normalized.Findings[0].Text != quote.Quote {
|
||
t.Fatalf("explicit relation was lost: %+v %v", normalized, err)
|
||
}
|
||
candidate.Findings[0].Sources[0].Quote = quote.Quote + "。"
|
||
normalized, _, err = normalizeMaterialEvidence(candidate, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "material_record" {
|
||
t.Fatalf("complete sentence with punctuation was lost: %+v %v", normalized, err)
|
||
}
|
||
candidate.Findings[0].Sources[0].Quote = "第二期"
|
||
normalized, _, err = normalizeMaterialEvidence(candidate, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "needs_user" {
|
||
t.Fatalf("incomplete relation was promoted: %+v %v", normalized, err)
|
||
}
|
||
}
|
||
|
||
func TestMaterialEvidenceDirectRelationCannotCarryUncheckedExtraQuote(t *testing.T) {
|
||
docs := []core.ParsedDocument{
|
||
{Meta: core.DocMeta{Filename: "决定.txt", Origin: &core.DocumentOrigin{ArtifactID: "direct", ContentHash: "direct-hash", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "第二期包含3项依赖外部接口的需求。"}}},
|
||
{Meta: core.DocMeta{Filename: "评审.txt", Origin: &core.DocumentOrigin{ArtifactID: "review", ContentHash: "review-hash", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "评审认为不等于已批准增加经费。"}}},
|
||
}
|
||
sources := materialSources(docs, 48000)
|
||
candidate := core.WorkCheckpointCandidate{CheckpointID: "extra-quote", PhaseID: "crosscheck", Findings: []core.WorkFindingCandidate{{FindingKey: "relation", Text: "已批准", Kind: "material_record", Sources: []core.TaskSourceQuote{evidenceQuote(t, sources, "direct", "第二期包含3项依赖外部接口的需求"), evidenceQuote(t, sources, "review", "已批准")}, Evidence: &core.WorkFindingEvidence{Mode: "direct_relation", Premises: []core.WorkEvidencePremise{{Role: "direct_relation", SourceIndex: 0}}}}}}
|
||
normalized, _, err := normalizeMaterialEvidence(candidate, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "needs_user" {
|
||
t.Fatalf("unchecked second quote escaped direct relation: %+v %v", normalized, err)
|
||
}
|
||
}
|
||
|
||
func TestMaterialEvidenceRejectsUnknownNumericRevisionUnit(t *testing.T) {
|
||
docs := []core.ParsedDocument{
|
||
{Meta: core.DocMeta{Filename: "旧范围.txt", Origin: &core.DocumentOrigin{ArtifactID: "total", ContentHash: "total-hash", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "总12项需求,现为13个。"}}},
|
||
{Meta: core.DocMeta{Filename: "阶段.txt", Origin: &core.DocumentOrigin{ArtifactID: "phase", ContentHash: "phase-hash", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "第一期先完成8项核心需求。"}}},
|
||
}
|
||
sources := materialSources(docs, 48000)
|
||
n := func(v int) *int { return &v }
|
||
candidate := core.WorkCheckpointCandidate{CheckpointID: "revised-unit", PhaseID: "crosscheck", Findings: []core.WorkFindingCandidate{{FindingKey: "gap", Text: "差额4项", Kind: "analysis", Assumptions: "按旧值推算", Sources: []core.TaskSourceQuote{evidenceQuote(t, sources, "total", "总12项需求"), evidenceQuote(t, sources, "phase", "第一期先完成8项核心需求")}, Evidence: &core.WorkFindingEvidence{Mode: "difference", Premises: []core.WorkEvidencePremise{{Role: "total", SourceIndex: 0, Value: n(12)}, {Role: "phase", SourceIndex: 1, Value: n(8)}}, Result: n(4)}}}}
|
||
normalized, _, err := normalizeMaterialEvidence(candidate, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "needs_user" {
|
||
t.Fatalf("unknown revised unit was ignored: %+v %v", normalized, err)
|
||
}
|
||
}
|
||
|
||
func TestMaterialEvidenceRejectsMixedUnitsAndAmbiguousPhaseCounts(t *testing.T) {
|
||
sources := evidenceTestSources()
|
||
candidate := evidenceCandidate(t, sources, "difference")
|
||
candidate.Findings[0].Evidence.Premises = candidate.Findings[0].Evidence.Premises[:2]
|
||
result := 4
|
||
candidate.Findings[0].Evidence.Result = &result
|
||
candidate.Findings[0].Sources[1].Quote = "总预算暂按50万元评估"
|
||
normalized, _, err := normalizeMaterialEvidence(candidate, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "needs_user" {
|
||
t.Fatalf("budget amount used as requirement count: %+v %v", normalized, err)
|
||
}
|
||
docs := []core.ParsedDocument{{Meta: core.DocMeta{Filename: "阶段.txt", Origin: &core.DocumentOrigin{ArtifactID: "phase", ContentHash: "h", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "第一期8项、第二期4项"}}}}
|
||
phaseSources := materialSources(docs, 48000)
|
||
candidate = evidenceCandidate(t, sources, "difference")
|
||
candidate.Findings[0].Evidence.Premises = candidate.Findings[0].Evidence.Premises[:2]
|
||
candidate.Findings[0].Evidence.Premises[1].Value = &result
|
||
candidate.Findings[0].Evidence.Result = &result
|
||
candidate.Findings[0].Sources[1] = evidenceQuote(t, phaseSources, "phase", "第一期8项、第二期4项")
|
||
for k, v := range phaseSources {
|
||
sources[k] = v
|
||
}
|
||
normalized, _, err = normalizeMaterialEvidence(candidate, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "needs_user" {
|
||
t.Fatalf("second-phase number used as first-phase count: %+v %v", normalized, err)
|
||
}
|
||
}
|
||
|
||
func TestMaterialEvidenceDoesNotCountBudgetItemsAsRequirements(t *testing.T) {
|
||
sources := evidenceTestSources()
|
||
candidate := evidenceCandidate(t, sources, "difference")
|
||
candidate.Findings[0].Evidence.Premises = candidate.Findings[0].Evidence.Premises[:2]
|
||
result := 4
|
||
candidate.Findings[0].Evidence.Result = &result
|
||
for _, tc := range []struct {
|
||
artifact string
|
||
text string
|
||
index int
|
||
}{
|
||
{"budget-total", "项目共有12项预算科目", 0},
|
||
{"budget-phase", "第一期先完成8项预算条目", 1},
|
||
} {
|
||
doc := core.ParsedDocument{Meta: core.DocMeta{Filename: tc.artifact + ".txt", Origin: &core.DocumentOrigin{ArtifactID: tc.artifact, ContentHash: tc.artifact, ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: tc.text}}}
|
||
for id, src := range materialSources([]core.ParsedDocument{doc}, 48000) {
|
||
sources[id] = src
|
||
}
|
||
cp := candidate
|
||
cp.Findings = append([]core.WorkFindingCandidate(nil), candidate.Findings...)
|
||
cp.Findings[0].Sources = append([]core.TaskSourceQuote(nil), candidate.Findings[0].Sources...)
|
||
cp.Findings[0].Sources[tc.index] = evidenceQuote(t, sources, tc.artifact, tc.text)
|
||
normalized, _, err := normalizeMaterialEvidence(cp, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "needs_user" {
|
||
t.Fatalf("%s was treated as requirement count: %+v %v", tc.artifact, normalized, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestMaterialEvidenceHostTextDoesNotConsumeCandidateWireLimit(t *testing.T) {
|
||
sources := evidenceTestSources()
|
||
candidate := evidenceCandidate(t, sources, "difference")
|
||
candidate.Findings[0].Evidence.Premises = candidate.Findings[0].Evidence.Premises[:2]
|
||
result := 4
|
||
candidate.Findings[0].Evidence.Result = &result
|
||
candidate.Findings[0].Text = "4"
|
||
candidate.Findings[0].Assumptions = "x"
|
||
raw, err := json.Marshal(candidate)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
candidate.Focus = strings.Repeat("x", 16*1024-len(raw)-25)
|
||
if err := core.ValidateWorkCheckpoint(candidate); err != nil {
|
||
t.Fatal("wire candidate must fit", err)
|
||
}
|
||
if _, _, err := normalizeMaterialEvidence(candidate, sources); err != nil {
|
||
t.Fatal("host-generated text should not be treated as a second wire candidate", err)
|
||
}
|
||
}
|
||
|
||
func TestMaterialEvidenceRejectsUnprovidedOrForeignSource(t *testing.T) {
|
||
sources := evidenceTestSources()
|
||
candidate := evidenceCandidate(t, sources, "unresolved_relation")
|
||
candidate.Findings[0].Sources[0].SourceID = "foreign"
|
||
if _, _, err := normalizeMaterialEvidence(candidate, sources); err == nil {
|
||
t.Fatal("foreign source was accepted")
|
||
}
|
||
withoutOrigin := core.ParsedDocument{Meta: core.DocMeta{Filename: "未知.txt"}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "总12项"}}}
|
||
if len(materialSources([]core.ParsedDocument{withoutOrigin}, 48000)) != 0 {
|
||
t.Fatal("missing Origin produced a source")
|
||
}
|
||
doc := core.ParsedDocument{Meta: core.DocMeta{Filename: "截断.txt", Origin: &core.DocumentOrigin{ArtifactID: "short", ContentHash: "h", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "已提供前缀"}, {Type: core.BlockPara, Text: "隐藏的第二期包含3项"}}}
|
||
short := materialSources([]core.ParsedDocument{doc}, 5)
|
||
for _, src := range short {
|
||
if src.Ref.BlockIndex != 0 || strings.Contains(src.Provided, "第二期") {
|
||
t.Fatal("hidden block was registered", src)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestMaterialEvidenceDoesNotDropNegationOutsideShortQuote(t *testing.T) {
|
||
docs := []core.ParsedDocument{
|
||
{Meta: core.DocMeta{Filename: "评审.txt", Origin: &core.DocumentOrigin{ArtifactID: "review", ContentHash: "review-hash", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "评审认为不等于已批准增加经费。"}}},
|
||
{Meta: core.DocMeta{Filename: "需求.txt", Origin: &core.DocumentOrigin{ArtifactID: "req", ContentHash: "req-hash", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "并非总12项需求。"}}},
|
||
{Meta: core.DocMeta{Filename: "会议.txt", Origin: &core.DocumentOrigin{ArtifactID: "meeting", ContentHash: "meeting-hash", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "第一期先完成8项核心需求。"}}},
|
||
}
|
||
sources := materialSources(docs, 48000)
|
||
approval := core.WorkCheckpointCandidate{CheckpointID: "approval-short", PhaseID: "crosscheck", Findings: []core.WorkFindingCandidate{{FindingKey: "approval", Text: "已批准", Kind: "material_record", Sources: []core.TaskSourceQuote{evidenceQuote(t, sources, "review", "已批准")}}}}
|
||
normalized, _, err := normalizeMaterialEvidence(approval, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "material_record" || !strings.Contains(normalized.Findings[0].Sources[0].Quote, "不等于已批准") {
|
||
t.Fatalf("negation was removed from a source record: %+v %v", normalized, err)
|
||
}
|
||
approval.Findings[0].Evidence = &core.WorkFindingEvidence{Mode: "direct_relation", Premises: []core.WorkEvidencePremise{{Role: "direct_relation", SourceIndex: 0}}}
|
||
normalized, _, err = normalizeMaterialEvidence(approval, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "needs_user" {
|
||
t.Fatalf("negated approval was promoted as a direct relation: %+v %v", normalized, err)
|
||
}
|
||
n := func(v int) *int { return &v }
|
||
counts := core.WorkCheckpointCandidate{CheckpointID: "count-short", PhaseID: "crosscheck", Findings: []core.WorkFindingCandidate{{FindingKey: "count", Text: "差额4项", Kind: "analysis", Assumptions: "据材料推算", Sources: []core.TaskSourceQuote{evidenceQuote(t, sources, "req", "总12项需求"), evidenceQuote(t, sources, "meeting", "第一期先完成8项核心需求")}, Evidence: &core.WorkFindingEvidence{Mode: "difference", Premises: []core.WorkEvidencePremise{{Role: "total", SourceIndex: 0, Value: n(12)}, {Role: "phase", SourceIndex: 1, Value: n(8)}}, Result: n(4)}}}}
|
||
normalized, _, err = normalizeMaterialEvidence(counts, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "needs_user" {
|
||
t.Fatalf("negated total was used in arithmetic: %+v %v", normalized, err)
|
||
}
|
||
}
|
||
|
||
func TestMaterialEvidenceRestoresNegationForUnlistedDecisionWord(t *testing.T) {
|
||
docs := []core.ParsedDocument{{Meta: core.DocMeta{Filename: "方案.txt", Origin: &core.DocumentOrigin{ArtifactID: "decision", ContentHash: "decision-hash", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "方案尚未通过。"}}}}
|
||
sources := materialSources(docs, 48000)
|
||
candidate := core.WorkCheckpointCandidate{CheckpointID: "decision-short", PhaseID: "crosscheck", Findings: []core.WorkFindingCandidate{{FindingKey: "decision", Text: "通过", Kind: "material_record", Sources: []core.TaskSourceQuote{evidenceQuote(t, sources, "decision", "通过")}}}}
|
||
normalized, _, err := normalizeMaterialEvidence(candidate, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "material_record" || normalized.Findings[0].Sources[0].Quote != "方案尚未通过" {
|
||
t.Fatalf("short decision quote lost negation: %+v %v", normalized, err)
|
||
}
|
||
}
|
||
|
||
func TestMaterialEvidenceRejectsProjectPrefixScopesAndCancelledCounts(t *testing.T) {
|
||
n := func(v int) *int { return &v }
|
||
for _, tc := range []struct {
|
||
name, total, phase, dependent string
|
||
}{
|
||
{"two scopes in one source", "项目A总12项需求,项目B第一期8项需求,其中12项依赖接口", "项目A总12项需求,项目B第一期8项需求,其中12项依赖接口", "项目A总12项需求,项目B第一期8项需求,其中12项依赖接口"},
|
||
{"cancelled total", "已取消总12项需求", "第一期先完成8项核心需求", "其中3项依赖外部接口"},
|
||
} {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
docs := []core.ParsedDocument{
|
||
{Meta: core.DocMeta{Filename: "范围.txt", Origin: &core.DocumentOrigin{ArtifactID: "total", ContentHash: "h-total", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: tc.total}}},
|
||
{Meta: core.DocMeta{Filename: "阶段.txt", Origin: &core.DocumentOrigin{ArtifactID: "phase", ContentHash: "h-phase", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: tc.phase}}},
|
||
{Meta: core.DocMeta{Filename: "依赖.txt", Origin: &core.DocumentOrigin{ArtifactID: "dependent", ContentHash: "h-dependent", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: tc.dependent}}},
|
||
}
|
||
sources := materialSources(docs, 48000)
|
||
candidate := core.WorkCheckpointCandidate{CheckpointID: "unsafe-scope", PhaseID: "crosscheck", Findings: []core.WorkFindingCandidate{{FindingKey: "scope", Text: "差额与归属确定", Kind: "analysis", Assumptions: "模型推断", Sources: []core.TaskSourceQuote{evidenceQuote(t, sources, "total", tc.total), evidenceQuote(t, sources, "phase", tc.phase), evidenceQuote(t, sources, "dependent", tc.dependent)}, Evidence: &core.WorkFindingEvidence{Mode: "unresolved_relation", Premises: []core.WorkEvidencePremise{{Role: "total", SourceIndex: 0, Value: n(12)}, {Role: "phase", SourceIndex: 1, Value: n(8)}, {Role: "dependent", SourceIndex: 2, Value: n(12)}}}}}}
|
||
if tc.name == "two scopes in one source" {
|
||
candidate.Findings[0].Sources = candidate.Findings[0].Sources[:1]
|
||
candidate.Findings[0].Evidence.Premises[1].SourceIndex = 0
|
||
candidate.Findings[0].Evidence.Premises[2].SourceIndex = 0
|
||
} else {
|
||
candidate.Findings[0].Evidence.Mode = "difference"
|
||
candidate.Findings[0].Evidence.Premises = candidate.Findings[0].Evidence.Premises[:2]
|
||
candidate.Findings[0].Evidence.Result = n(4)
|
||
}
|
||
normalized, _, err := normalizeMaterialEvidence(candidate, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "needs_user" {
|
||
t.Fatalf("unsafe scope/count was treated as proven: %+v %v", normalized, err)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestMaterialEvidenceDoesNotPromoteTruncatedVisibleSentence(t *testing.T) {
|
||
visible := "预算已批准"
|
||
doc := core.ParsedDocument{Meta: core.DocMeta{Filename: "预算.txt", Origin: &core.DocumentOrigin{ArtifactID: "budget", ContentHash: "budget-hash", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "预算已批准只是传闻,尚未核实。"}}}
|
||
sources := materialSources([]core.ParsedDocument{doc}, len([]rune(visible)))
|
||
candidate := core.WorkCheckpointCandidate{CheckpointID: "truncated", PhaseID: "reading", Findings: []core.WorkFindingCandidate{{FindingKey: "approval", Text: "预算已批准", Kind: "material_record", Sources: []core.TaskSourceQuote{evidenceQuote(t, sources, "budget", "已批准")}}}}
|
||
normalized, _, err := normalizeMaterialEvidence(candidate, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "needs_user" {
|
||
t.Fatalf("unfinished sentence became a positive source record: %+v %v", normalized, err)
|
||
}
|
||
}
|
||
|
||
func TestMaterialEvidenceRejectsRevisedNumberInSameSentence(t *testing.T) {
|
||
docs := []core.ParsedDocument{
|
||
{Meta: core.DocMeta{Filename: "旧范围.txt", Origin: &core.DocumentOrigin{ArtifactID: "total", ContentHash: "total-hash", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "总12项需求,已调整为13项需求。"}}},
|
||
{Meta: core.DocMeta{Filename: "阶段.txt", Origin: &core.DocumentOrigin{ArtifactID: "phase", ContentHash: "phase-hash", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "第一期先完成8项核心需求。"}}},
|
||
}
|
||
sources := materialSources(docs, 48000)
|
||
n := func(v int) *int { return &v }
|
||
candidate := core.WorkCheckpointCandidate{CheckpointID: "revised-count", PhaseID: "crosscheck", Findings: []core.WorkFindingCandidate{{FindingKey: "gap", Text: "差额4项", Kind: "analysis", Assumptions: "按旧值推算", Sources: []core.TaskSourceQuote{evidenceQuote(t, sources, "total", "总12项需求"), evidenceQuote(t, sources, "phase", "第一期先完成8项核心需求")}, Evidence: &core.WorkFindingEvidence{Mode: "difference", Premises: []core.WorkEvidencePremise{{Role: "total", SourceIndex: 0, Value: n(12)}, {Role: "phase", SourceIndex: 1, Value: n(8)}}, Result: n(4)}}}}
|
||
normalized, _, err := normalizeMaterialEvidence(candidate, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "needs_user" {
|
||
t.Fatalf("revised number was used as current count: %+v %v", normalized, err)
|
||
}
|
||
}
|
||
|
||
func TestMaterialEvidenceSingleSourceDoesNotProveUniqueMembership(t *testing.T) {
|
||
doc := core.ParsedDocument{Meta: core.DocMeta{Filename: "三项数字.txt", Origin: &core.DocumentOrigin{ArtifactID: "counts", ContentHash: "counts-hash", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "总12项需求。第一期8项需求。其中12项依赖接口。"}}}
|
||
sources := materialSources([]core.ParsedDocument{doc}, 48000)
|
||
n := func(v int) *int { return &v }
|
||
quote := evidenceQuote(t, sources, "counts", "总12项需求。第一期8项需求。其中12项依赖接口")
|
||
candidate := core.WorkCheckpointCandidate{CheckpointID: "same-source", PhaseID: "crosscheck", Findings: []core.WorkFindingCandidate{{FindingKey: "membership", Text: "接口依赖项都在第一期之外", Kind: "analysis", Assumptions: "同一来源", Sources: []core.TaskSourceQuote{quote}, Evidence: &core.WorkFindingEvidence{Mode: "unresolved_relation", Premises: []core.WorkEvidencePremise{{Role: "total", SourceIndex: 0, Value: n(12)}, {Role: "phase", SourceIndex: 0, Value: n(8)}, {Role: "dependent", SourceIndex: 0, Value: n(12)}}}}}}
|
||
normalized, _, err := normalizeMaterialEvidence(candidate, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "needs_user" {
|
||
t.Fatalf("source position was treated as a proof of common scope: %+v %v", normalized, err)
|
||
}
|
||
}
|
||
|
||
func TestMaterialEvidenceSameExplicitScopeCanProveUniqueMembership(t *testing.T) {
|
||
doc := core.ParsedDocument{Meta: core.DocMeta{Filename: "A项目.txt", Origin: &core.DocumentOrigin{ArtifactID: "project-a", ContentHash: "project-a-hash", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "项目A总12项需求,项目A第一期8项需求,项目A其中12项依赖接口。"}}}
|
||
sources := materialSources([]core.ParsedDocument{doc}, 48000)
|
||
n := func(v int) *int { return &v }
|
||
quote := evidenceQuote(t, sources, "project-a", "项目A总12项需求,项目A第一期8项需求,项目A其中12项依赖接口")
|
||
candidate := core.WorkCheckpointCandidate{CheckpointID: "same-named-scope", PhaseID: "crosscheck", Findings: []core.WorkFindingCandidate{{FindingKey: "membership", Text: "第一期之外4项依赖接口", Kind: "analysis", Assumptions: "同名范围", Sources: []core.TaskSourceQuote{quote}, Evidence: &core.WorkFindingEvidence{Mode: "unresolved_relation", Premises: []core.WorkEvidencePremise{{Role: "total", SourceIndex: 0, Value: n(12)}, {Role: "phase", SourceIndex: 0, Value: n(8)}, {Role: "dependent", SourceIndex: 0, Value: n(12)}}}}}}
|
||
normalized, _, err := normalizeMaterialEvidence(candidate, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "analysis" || !strings.Contains(normalized.Findings[0].Text, "4项") {
|
||
t.Fatalf("explicit same scope lost its calculable relation: %+v %v", normalized, err)
|
||
}
|
||
}
|
||
|
||
func TestMaterialEvidenceDifferentExplicitScopesBecomePending(t *testing.T) {
|
||
docs := []core.ParsedDocument{
|
||
{Meta: core.DocMeta{Filename: "A.txt", Origin: &core.DocumentOrigin{ArtifactID: "req", ContentHash: "req-hash", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "A项目总12项需求。"}}},
|
||
{Meta: core.DocMeta{Filename: "B.txt", Origin: &core.DocumentOrigin{ArtifactID: "meeting", ContentHash: "meeting-hash", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "B项目第一期8项需求。"}}},
|
||
}
|
||
sources := materialSources(docs, 48000)
|
||
n := func(v int) *int { return &v }
|
||
candidate := core.WorkCheckpointCandidate{CheckpointID: "different-projects", PhaseID: "crosscheck", Findings: []core.WorkFindingCandidate{{FindingKey: "gap", Text: "差额4项", Kind: "analysis", Assumptions: "模型推断", Sources: []core.TaskSourceQuote{evidenceQuote(t, sources, "req", "总12项需求"), evidenceQuote(t, sources, "meeting", "第一期8项需求")}, Evidence: &core.WorkFindingEvidence{Mode: "difference", Premises: []core.WorkEvidencePremise{{Role: "total", SourceIndex: 0, Value: n(12)}, {Role: "phase", SourceIndex: 1, Value: n(8)}}, Result: n(4)}}}}
|
||
normalized, _, err := normalizeMaterialEvidence(candidate, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "needs_user" {
|
||
t.Fatalf("different project counts were subtracted: %+v %v", normalized, err)
|
||
}
|
||
}
|
||
|
||
func TestMaterialEvidenceUnstructuredBatchPlacementBecomesPending(t *testing.T) {
|
||
sources := evidenceTestSources()
|
||
candidate := evidenceCandidate(t, sources, "difference")
|
||
candidate.Findings[0].Evidence = nil
|
||
candidate.Findings[0].Text = "接口依赖工作均安排在后续批次"
|
||
candidate.Findings[0].Impact = "应直接进入下批联调"
|
||
candidate.Findings[0].Assumptions = "模型推断"
|
||
normalized, _, err := normalizeMaterialEvidence(candidate, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "needs_user" || strings.Contains(normalized.Findings[0].Text, "均安排") {
|
||
t.Fatalf("unstructured placement escaped: %+v %v", normalized, err)
|
||
}
|
||
}
|
||
|
||
func TestMaterialEvidenceOnlyKeepsCautiousOrdinaryAnalysisWithoutProof(t *testing.T) {
|
||
sources := evidenceTestSources()
|
||
candidate := evidenceCandidate(t, sources, "difference")
|
||
candidate.Findings[0].Evidence = nil
|
||
candidate.Findings[0].Assumptions = "基于所引材料"
|
||
candidate.Findings[0].Text = "两份材料存在差异,需核对"
|
||
candidate.Findings[0].Impact = ""
|
||
normalized, _, err := normalizeMaterialEvidence(candidate, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "analysis" {
|
||
t.Fatalf("cautious ordinary analysis was lost: %+v %v", normalized, err)
|
||
}
|
||
candidate.Findings[0].Text = "外部依赖工作全放到下一轮"
|
||
normalized, _, err = normalizeMaterialEvidence(candidate, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "needs_user" {
|
||
t.Fatalf("unproven paraphrased placement escaped: %+v %v", normalized, err)
|
||
}
|
||
}
|
||
|
||
func TestMaterialEvidenceDoesNotCopyApprovalBehindCautionWord(t *testing.T) {
|
||
sources := evidenceTestSources()
|
||
for _, assertion := range []string{"方案已获准,建议执行", "方案已同意,需核对", "建议直接执行已获准方案"} {
|
||
candidate := evidenceCandidate(t, sources, "difference")
|
||
candidate.Findings[0].Evidence = nil
|
||
candidate.Findings[0].Text = assertion
|
||
candidate.Findings[0].Impact = ""
|
||
candidate.Findings[0].Assumptions = "根据所引材料"
|
||
normalized, _, err := normalizeMaterialEvidence(candidate, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "needs_user" {
|
||
t.Fatalf("affirmative decision hid behind caution: %+v %v", normalized, err)
|
||
}
|
||
}
|
||
candidate := evidenceCandidate(t, sources, "difference")
|
||
candidate.Findings[0].Evidence = nil
|
||
candidate.Findings[0].Text = "两份材料存在差异,需核对"
|
||
candidate.Findings[0].Impact = "建议执行已获准方案"
|
||
candidate.Findings[0].Assumptions = "根据所引材料"
|
||
normalized, _, err := normalizeMaterialEvidence(candidate, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "analysis" || normalized.Findings[0].Impact != "" || strings.Contains(normalized.Findings[0].Assumptions, "已获准") {
|
||
t.Fatalf("ordinary cautious analysis retained unsafe ancillary prose: %+v %v", normalized, err)
|
||
}
|
||
}
|
||
|
||
func TestMaterialEvidenceAnalysisDirectRelationBecomesNeedsUser(t *testing.T) {
|
||
docs := []core.ParsedDocument{{Meta: core.DocMeta{Filename: "决定.txt", Origin: &core.DocumentOrigin{ArtifactID: "direct", ContentHash: "hash-direct", ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: "第二期包含3项依赖外部接口的需求。接口负责人仍待确认。"}}}}
|
||
sources := materialSources(docs, 48000)
|
||
quote := evidenceQuote(t, sources, "direct", "第二期包含3项依赖外部接口的需求")
|
||
analysis := core.WorkCheckpointCandidate{CheckpointID: "analysis-direct", PhaseID: "crosscheck", Findings: []core.WorkFindingCandidate{{
|
||
FindingKey: "relation", Text: "第二期含接口依赖", Kind: "analysis", Assumptions: "按原句理解归属",
|
||
Sources: []core.TaskSourceQuote{quote},
|
||
Evidence: &core.WorkFindingEvidence{Mode: "direct_relation", Premises: []core.WorkEvidencePremise{{Role: "direct_relation", SourceIndex: 0}}},
|
||
}}}
|
||
normalized, _, err := normalizeMaterialEvidence(analysis, sources)
|
||
if err != nil || normalized.Findings[0].Kind != "needs_user" {
|
||
t.Fatalf("analysis+direct_relation was not downgraded: %+v %v", normalized, err)
|
||
}
|
||
record := core.WorkCheckpointCandidate{CheckpointID: "record-plain", PhaseID: "reading", Findings: []core.WorkFindingCandidate{{
|
||
FindingKey: "date", Text: "上线六月", Kind: "material_record",
|
||
Sources: []core.TaskSourceQuote{evidenceQuote(t, evidenceTestSources(), "req", "上线六月")},
|
||
}}}
|
||
normalized, _, err = normalizeMaterialEvidence(record, evidenceTestSources())
|
||
if err != nil || normalized.Findings[0].Kind != "material_record" {
|
||
t.Fatalf("material_record without evidence lost kind: %+v %v", normalized, err)
|
||
}
|
||
}
|