Merge pull request 'Add bounded evidence to work checkpoint candidates' (#31) from feature/fast-slow-u2-u3 into main
governance / governance (push) Successful in 16s
governance / governance (push) Successful in 16s
This commit was merged in pull request #31.
This commit is contained in:
@@ -20,6 +20,7 @@ type ActivityCard struct {
|
||||
Title string `json:"title"`
|
||||
Status CardStatus `json:"status"`
|
||||
Tone string `json:"tone,omitempty"` // ok | warn | danger | info
|
||||
Task *TaskSnapshot `json:"task,omitempty"`
|
||||
Body []UINode `json:"body,omitempty"`
|
||||
Actions []UIAction `json:"actions,omitempty"` // 走命令总线;未绑定 command 降级为发消息
|
||||
Pinned bool `json:"pinned,omitempty"`
|
||||
|
||||
@@ -13,6 +13,7 @@ type ParsedDocument struct {
|
||||
}
|
||||
|
||||
type DocMeta struct {
|
||||
Origin *DocumentOrigin `json:"origin,omitempty"`
|
||||
Filename string `json:"filename"`
|
||||
MIME string `json:"mime,omitempty"`
|
||||
Parser string `json:"parser"` // builtin:md / builtin:docx / mineru / agent
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DocumentOrigin struct {
|
||||
ArtifactID ID `json:"artifactId"`
|
||||
ContentHash string `json:"contentHash"`
|
||||
ParserRevision string `json:"parserRevision"`
|
||||
}
|
||||
type TaskPhase struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
ObservedAt time.Time `json:"observedAt,omitempty"`
|
||||
}
|
||||
type TaskSourceRef struct {
|
||||
ID string `json:"id"`
|
||||
ArtifactID string `json:"artifactId"`
|
||||
ContentHash string `json:"contentHash"`
|
||||
ParserRevision string `json:"parserRevision"`
|
||||
BlockIndex int `json:"blockIndex"`
|
||||
Label string `json:"label"`
|
||||
Quote string `json:"quote,omitempty"`
|
||||
Availability string `json:"availability,omitempty"`
|
||||
}
|
||||
type TaskResultRef struct {
|
||||
ArtifactID ID `json:"artifactId,omitempty"`
|
||||
SurfaceID ID `json:"surfaceId,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
ContentHash string `json:"contentHash,omitempty"`
|
||||
Availability string `json:"availability"`
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
type TaskFinding struct {
|
||||
ID string `json:"id"`
|
||||
Text string `json:"text"`
|
||||
Impact string `json:"impact,omitempty"`
|
||||
Kind string `json:"kind"`
|
||||
Status string `json:"status"`
|
||||
Revision int64 `json:"revision"`
|
||||
Sources []TaskSourceRef `json:"sources"`
|
||||
Assumptions string `json:"assumptions,omitempty"`
|
||||
Corrects string `json:"corrects,omitempty"`
|
||||
}
|
||||
type TaskSnapshot struct {
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
TaskID ID `json:"taskId"`
|
||||
AttemptID ID `json:"attemptId"`
|
||||
CardID ID `json:"cardId"`
|
||||
ProjectID ID `json:"projectId"`
|
||||
ConversationID ID `json:"conversationId"`
|
||||
SourceMessageID ID `json:"sourceMessageId,omitempty"`
|
||||
ParentRunID ID `json:"parentRunId,omitempty"`
|
||||
RunID ID `json:"runId,omitempty"`
|
||||
OwnerInstanceID string `json:"ownerInstanceId"`
|
||||
ExecutionEpoch int64 `json:"executionEpoch,omitempty"`
|
||||
Revision int64 `json:"revision"`
|
||||
SourceSeq int64 `json:"sourceSeq"`
|
||||
StreamScope string `json:"streamScope"`
|
||||
InputRevision string `json:"inputRevision,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Goal string `json:"goal,omitempty"`
|
||||
ExecutionState string `json:"executionState"`
|
||||
OutcomeCompleteness string `json:"outcomeCompleteness"`
|
||||
DeliveryState string `json:"deliveryState"`
|
||||
Phase TaskPhase `json:"phase"`
|
||||
Focus string `json:"focus,omitempty"`
|
||||
Coverage string `json:"coverage,omitempty"`
|
||||
Remaining string `json:"remaining,omitempty"`
|
||||
NextStep string `json:"nextStep,omitempty"`
|
||||
Conclusion string `json:"conclusion,omitempty"`
|
||||
Findings []TaskFinding `json:"findings,omitempty"`
|
||||
Limitations []string `json:"limitations,omitempty"`
|
||||
Inputs []TaskSourceRef `json:"inputs,omitempty"`
|
||||
Actions []UIAction `json:"actions,omitempty"`
|
||||
ResultRefs []TaskResultRef `json:"resultRefs,omitempty"`
|
||||
LastActivityAt time.Time `json:"lastActivityAt,omitempty"`
|
||||
LastMeaningfulUpdateAt time.Time `json:"lastMeaningfulUpdateAt,omitempty"`
|
||||
CheckpointKeys map[string]string `json:"checkpointKeys,omitempty"`
|
||||
CheckpointCount int `json:"checkpointCount,omitempty"`
|
||||
CheckpointSeq int64 `json:"checkpointSeq,omitempty"`
|
||||
}
|
||||
type TaskSourceQuote struct {
|
||||
SourceID string `json:"sourceId"`
|
||||
BlockIndex int `json:"blockIndex"`
|
||||
Quote string `json:"quote"`
|
||||
}
|
||||
type WorkCheckpointCandidate struct {
|
||||
CheckpointID string `json:"checkpointId"`
|
||||
PhaseID string `json:"phaseId"`
|
||||
Focus string `json:"focus,omitempty"`
|
||||
Remaining string `json:"remaining,omitempty"`
|
||||
NextStep string `json:"nextStep,omitempty"`
|
||||
Findings []WorkFindingCandidate `json:"findings"`
|
||||
}
|
||||
type WorkFindingCandidate struct {
|
||||
FindingKey string `json:"findingKey"`
|
||||
Text string `json:"text"`
|
||||
Impact string `json:"impact,omitempty"`
|
||||
Kind string `json:"kind"`
|
||||
Assumptions string `json:"assumptions,omitempty"`
|
||||
Corrects string `json:"corrects,omitempty"`
|
||||
Sources []TaskSourceQuote `json:"sources"`
|
||||
Evidence *WorkFindingEvidence `json:"evidence,omitempty"`
|
||||
}
|
||||
|
||||
// WorkFindingEvidence is a model-supplied candidate, not a persisted proof.
|
||||
// Backend checks the quoted source text and computes any resulting statement.
|
||||
type WorkEvidencePremise struct {
|
||||
Role string `json:"role"`
|
||||
SourceIndex int `json:"sourceIndex"`
|
||||
Value *int `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
type WorkFindingEvidence struct {
|
||||
Mode string `json:"mode"`
|
||||
Premises []WorkEvidencePremise `json:"premises"`
|
||||
Result *int `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
func validateWorkFindingEvidence(e *WorkFindingEvidence, sourceCount int) error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
var required []string
|
||||
switch e.Mode {
|
||||
case "direct_relation":
|
||||
required = []string{"direct_relation"}
|
||||
if e.Result != nil {
|
||||
return fmt.Errorf("checkpoint.evidence: direct relation cannot have result")
|
||||
}
|
||||
case "difference":
|
||||
required = []string{"total", "phase"}
|
||||
if e.Result == nil || *e.Result < 0 || *e.Result > 1000000 {
|
||||
return fmt.Errorf("checkpoint.evidence: difference requires bounded result")
|
||||
}
|
||||
case "unresolved_relation":
|
||||
required = []string{"total", "phase", "dependent"}
|
||||
if e.Result != nil {
|
||||
return fmt.Errorf("checkpoint.evidence: unresolved relation cannot have result")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("checkpoint.evidence: unknown mode")
|
||||
}
|
||||
if len(e.Premises) != len(required) {
|
||||
return fmt.Errorf("checkpoint.evidence: wrong premise count")
|
||||
}
|
||||
roles := make(map[string]bool, len(required))
|
||||
for _, premise := range e.Premises {
|
||||
if roles[premise.Role] || premise.SourceIndex < 0 || premise.SourceIndex >= sourceCount {
|
||||
return fmt.Errorf("checkpoint.evidence: duplicate role or invalid source index")
|
||||
}
|
||||
roles[premise.Role] = true
|
||||
if e.Mode == "direct_relation" {
|
||||
if premise.Value != nil {
|
||||
return fmt.Errorf("checkpoint.evidence: direct relation cannot have value")
|
||||
}
|
||||
} else if premise.Value == nil || *premise.Value < 0 || *premise.Value > 1000000 {
|
||||
return fmt.Errorf("checkpoint.evidence: count premise requires bounded value")
|
||||
}
|
||||
}
|
||||
for _, role := range required {
|
||||
if !roles[role] {
|
||||
return fmt.Errorf("checkpoint.evidence: missing premise role")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate enforces wire limits; owner/source authorization belongs to Backend.
|
||||
func (s TaskSnapshot) Validate() error {
|
||||
if s.SchemaVersion != 1 {
|
||||
return fmt.Errorf("task.schema: unsupported")
|
||||
}
|
||||
if s.TaskID == "" || s.AttemptID == "" || s.CardID == "" || s.ProjectID == "" || s.ConversationID == "" || s.OwnerInstanceID == "" {
|
||||
return fmt.Errorf("task.scope: missing identity")
|
||||
}
|
||||
if s.StreamScope != "conversation:"+s.ConversationID {
|
||||
return fmt.Errorf("task.scope: inconsistent stream")
|
||||
}
|
||||
if s.Revision < 1 || s.Revision > 9007199254740991 || s.SourceSeq < 0 || s.SourceSeq > 9007199254740991 || s.ExecutionEpoch < 0 {
|
||||
return fmt.Errorf("task.revision: unsafe cursor")
|
||||
}
|
||||
switch s.ExecutionState {
|
||||
case "queued", "running", "awaiting_user", "completed", "failed", "interrupted":
|
||||
default:
|
||||
return fmt.Errorf("task.state: unknown")
|
||||
}
|
||||
if (s.ExecutionState == "running" || s.ExecutionState == "awaiting_user") && s.RunID == "" {
|
||||
return fmt.Errorf("task.run: active task requires run")
|
||||
}
|
||||
switch s.OutcomeCompleteness {
|
||||
case "none", "partial", "complete":
|
||||
default:
|
||||
return fmt.Errorf("task.outcome: unknown")
|
||||
}
|
||||
switch s.DeliveryState {
|
||||
case "pending", "available", "reported":
|
||||
default:
|
||||
return fmt.Errorf("task.delivery: unknown")
|
||||
}
|
||||
active := 0
|
||||
for _, f := range s.Findings {
|
||||
if f.ID == "" || f.Revision < 1 {
|
||||
return fmt.Errorf("task.finding: missing identity")
|
||||
}
|
||||
switch f.Kind {
|
||||
case "material_record", "analysis", "provisional", "needs_user", "verified":
|
||||
default:
|
||||
return fmt.Errorf("task.finding.kind: unknown")
|
||||
}
|
||||
switch f.Status {
|
||||
case "active":
|
||||
active++
|
||||
case "corrected", "withdrawn", "superseded":
|
||||
default:
|
||||
return fmt.Errorf("task.finding.status: unknown")
|
||||
}
|
||||
}
|
||||
if active > 12 {
|
||||
return fmt.Errorf("task.findings: too many active")
|
||||
}
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(b) > 64*1024 {
|
||||
return fmt.Errorf("task.size: exceeds 64KiB")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (s TaskSnapshot) Terminal() bool {
|
||||
return s.ExecutionState == "completed" || s.ExecutionState == "failed" || s.ExecutionState == "interrupted"
|
||||
}
|
||||
func ValidateWorkCheckpoint(c WorkCheckpointCandidate) error {
|
||||
if strings.TrimSpace(c.CheckpointID) == "" || len(c.CheckpointID) > 128 {
|
||||
return fmt.Errorf("checkpoint.id: required or too long")
|
||||
}
|
||||
switch c.PhaseID {
|
||||
case "reading", "crosscheck", "summarizing", "draft-ready":
|
||||
default:
|
||||
return fmt.Errorf("checkpoint.phase: unknown")
|
||||
}
|
||||
if len(c.Findings) == 0 || len(c.Findings) > 6 {
|
||||
return fmt.Errorf("checkpoint.findings: require 1..6")
|
||||
}
|
||||
keys := map[string]bool{}
|
||||
for _, f := range c.Findings {
|
||||
switch f.Kind {
|
||||
case "material_record", "analysis", "provisional", "needs_user":
|
||||
default:
|
||||
return fmt.Errorf("checkpoint.kind: model cannot grant verified status")
|
||||
}
|
||||
if f.FindingKey == "" || keys[f.FindingKey] || strings.TrimSpace(f.Text) == "" {
|
||||
return fmt.Errorf("checkpoint.finding: empty or duplicate key")
|
||||
}
|
||||
keys[f.FindingKey] = true
|
||||
if len(f.Sources) == 0 {
|
||||
return fmt.Errorf("checkpoint.sources: evidence required")
|
||||
}
|
||||
for _, src := range f.Sources {
|
||||
if src.SourceID == "" || src.BlockIndex < 0 || strings.TrimSpace(src.Quote) == "" {
|
||||
return fmt.Errorf("checkpoint.source: invalid")
|
||||
}
|
||||
}
|
||||
if err := validateWorkFindingEvidence(f.Evidence, len(f.Sources)); err != nil {
|
||||
return err
|
||||
}
|
||||
if (f.Kind == "analysis" || f.Kind == "provisional") && strings.TrimSpace(f.Assumptions) == "" {
|
||||
return fmt.Errorf("checkpoint.assumptions: required")
|
||||
}
|
||||
}
|
||||
b, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(b) > 16*1024 {
|
||||
return fmt.Errorf("checkpoint.size: exceeds 16KiB")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (s TaskSnapshot) SummaryBody() []UINode {
|
||||
labels := map[string]string{"queued": "排队中", "running": "进行中", "awaiting_user": "需要你处理", "completed": "已完成", "failed": "执行失败", "interrupted": "已中断"}
|
||||
var lines []string
|
||||
for _, f := range s.Findings {
|
||||
if f.Status == "active" {
|
||||
line := f.Text
|
||||
if f.Impact != "" {
|
||||
line += ";" + f.Impact
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
note := strings.Join(lines, "\n")
|
||||
if note == "" {
|
||||
note = TaskSummaryNoFindingsYet
|
||||
if s.ExecutionState == "queued" {
|
||||
note = "尚未开始处理,等待执行资源"
|
||||
}
|
||||
if s.Terminal() {
|
||||
note = s.Conclusion
|
||||
if note == "" {
|
||||
note = "没有可用阶段成果"
|
||||
}
|
||||
}
|
||||
}
|
||||
v := TaskSummaryView{Focus: s.Focus, FindingsNote: note, Remaining: s.Remaining, NextStep: s.NextStep, OutcomeLine: s.Conclusion}
|
||||
var body []UINode
|
||||
if s.Terminal() {
|
||||
body = v.FinishedBody(labels[s.ExecutionState], nil)
|
||||
} else {
|
||||
body = v.RunningBody(labels[s.ExecutionState], nil)
|
||||
}
|
||||
if s.Coverage != "" {
|
||||
body = append(body, UINode{Type: UIKV, KV: [][2]string{{"提供范围", s.Coverage}}})
|
||||
}
|
||||
if len(s.Limitations) > 0 {
|
||||
body = append(body, UINode{Type: UIList, Title: "范围与限制", Items: s.Limitations})
|
||||
}
|
||||
for _, r := range s.ResultRefs {
|
||||
if r.ArtifactID != "" && r.Availability == "available" {
|
||||
body = append(body, UINode{Type: UIDocRef, DocID: r.ArtifactID, Title: r.Name})
|
||||
}
|
||||
}
|
||||
return body
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Loss of the optional task object makes refresh discard stage findings.
|
||||
func TestTaskSnapshotCardRoundTrip(t *testing.T) {
|
||||
raw := []byte(`{"id":"card1","projectId":"p1","conversationId":"c1","kind":"progress","status":"active","task":{"schemaVersion":1,"taskId":"t1","attemptId":"a1","cardId":"card1","revision":4,"executionState":"running","focus":"核对材料","findings":[{"id":"f1","text":"日期不同","kind":"material_record","status":"active","revision":1}]}}`)
|
||||
var card ActivityCard
|
||||
if err := json.Unmarshal(raw, &card); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
encoded, err := json.Marshal(card)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var restored map[string]json.RawMessage
|
||||
if err := json.Unmarshal(encoded, &restored); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := restored["task"]; !ok {
|
||||
t.Fatal("task findings were lost in card persistence")
|
||||
}
|
||||
var task map[string]json.RawMessage
|
||||
if err := json.Unmarshal(restored["task"], &task); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(task["revision"]) != "4" || string(task["focus"]) != `"核对材料"` {
|
||||
t.Fatal("revision/focus did not survive")
|
||||
}
|
||||
}
|
||||
|
||||
func validTaskSnapshot() TaskSnapshot {
|
||||
return TaskSnapshot{SchemaVersion: 1, TaskID: "t1", AttemptID: "a1", CardID: "card1", ProjectID: "p1", ConversationID: "c1", OwnerInstanceID: "i1", Revision: 1, ExecutionState: "queued", OutcomeCompleteness: "none", DeliveryState: "pending", StreamScope: "conversation:c1", Title: "整理材料"}
|
||||
}
|
||||
func TestTaskSnapshotRejectsUnsafeAndUnboundState(t *testing.T) {
|
||||
for _, mutate := range []func(*TaskSnapshot){
|
||||
func(s *TaskSnapshot) { s.TaskID = "" }, func(s *TaskSnapshot) { s.AttemptID = "" },
|
||||
func(s *TaskSnapshot) { s.Revision = 9007199254740992 }, func(s *TaskSnapshot) { s.SourceSeq = -1 },
|
||||
func(s *TaskSnapshot) { s.ExecutionState = "imagined" }, func(s *TaskSnapshot) { s.StreamScope = "conversation:other" },
|
||||
func(s *TaskSnapshot) { s.ExecutionState = "running" },
|
||||
} {
|
||||
s := validTaskSnapshot()
|
||||
mutate(&s)
|
||||
if s.Validate() == nil {
|
||||
t.Errorf("invalid snapshot accepted: %+v", s)
|
||||
}
|
||||
}
|
||||
if err := validTaskSnapshot().Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
func TestWorkCheckpointRejectsVerifiedAndEmptyEvidence(t *testing.T) {
|
||||
c := WorkCheckpointCandidate{CheckpointID: "cp1", PhaseID: "crosscheck", Findings: []WorkFindingCandidate{{FindingKey: "date", Text: "日期不同", Kind: "verified", Sources: []TaskSourceQuote{{SourceID: "src1", Quote: "六月"}}}}}
|
||||
if err := ValidateWorkCheckpoint(c); err == nil || !strings.Contains(err.Error(), "kind") {
|
||||
t.Fatal("model granted verified status", err)
|
||||
}
|
||||
c.Findings[0].Kind = "material_record"
|
||||
c.Findings[0].Sources = nil
|
||||
if ValidateWorkCheckpoint(c) == nil {
|
||||
t.Fatal("source-free fact accepted")
|
||||
}
|
||||
c.Findings[0].Sources = []TaskSourceQuote{{SourceID: "src1", Quote: "六月"}}
|
||||
if err := ValidateWorkCheckpoint(c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.Findings[0].Text = strings.Repeat("x", 17000)
|
||||
if ValidateWorkCheckpoint(c) == nil {
|
||||
t.Fatal("unbounded candidate accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkCheckpointEvidenceModesAndBounds(t *testing.T) {
|
||||
base := func() WorkCheckpointCandidate {
|
||||
return WorkCheckpointCandidate{CheckpointID: "cp-evidence", PhaseID: "crosscheck", Findings: []WorkFindingCandidate{{
|
||||
FindingKey: "phase", Text: "待核对", Kind: "needs_user",
|
||||
Sources: []TaskSourceQuote{{SourceID: "total", Quote: "总12项"}, {SourceID: "phase", Quote: "一期8项"}, {SourceID: "dependent", Quote: "其中3项依赖接口"}},
|
||||
}}}
|
||||
}
|
||||
n := func(v int) *int { return &v }
|
||||
total := WorkEvidencePremise{Role: "total", SourceIndex: 0, Value: n(12)}
|
||||
phase := WorkEvidencePremise{Role: "phase", SourceIndex: 1, Value: n(8)}
|
||||
dependent := WorkEvidencePremise{Role: "dependent", SourceIndex: 2, Value: n(3)}
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
evidence WorkFindingEvidence
|
||||
valid bool
|
||||
}{
|
||||
{"direct", WorkFindingEvidence{Mode: "direct_relation", Premises: []WorkEvidencePremise{{Role: "direct_relation", SourceIndex: 0}}}, true},
|
||||
{"difference", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{total, phase}, Result: n(4)}, true},
|
||||
{"unresolved", WorkFindingEvidence{Mode: "unresolved_relation", Premises: []WorkEvidencePremise{total, phase, dependent}}, true},
|
||||
{"unknown mode", WorkFindingEvidence{Mode: "prediction", Premises: []WorkEvidencePremise{total, phase}}, false},
|
||||
{"missing result", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{total, phase}}, false},
|
||||
{"extra result", WorkFindingEvidence{Mode: "unresolved_relation", Premises: []WorkEvidencePremise{total, phase, dependent}, Result: n(3)}, false},
|
||||
{"duplicate role", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{total, total}, Result: n(4)}, false},
|
||||
{"missing premise", WorkFindingEvidence{Mode: "unresolved_relation", Premises: []WorkEvidencePremise{total, phase}}, false},
|
||||
{"negative index", WorkFindingEvidence{Mode: "direct_relation", Premises: []WorkEvidencePremise{{Role: "direct_relation", SourceIndex: -1}}}, false},
|
||||
{"foreign index", WorkFindingEvidence{Mode: "direct_relation", Premises: []WorkEvidencePremise{{Role: "direct_relation", SourceIndex: 3}}}, false},
|
||||
{"negative value", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{{Role: "total", SourceIndex: 0, Value: n(-1)}, phase}, Result: n(4)}, false},
|
||||
{"large value", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{{Role: "total", SourceIndex: 0, Value: n(1000001)}, phase}, Result: n(4)}, false},
|
||||
{"direct value", WorkFindingEvidence{Mode: "direct_relation", Premises: []WorkEvidencePremise{{Role: "direct_relation", SourceIndex: 0, Value: n(1)}}}, false},
|
||||
{"missing value", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{{Role: "total", SourceIndex: 0}, phase}, Result: n(4)}, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := base()
|
||||
c.Findings[0].Evidence = &tc.evidence
|
||||
err := ValidateWorkCheckpoint(c)
|
||||
if (err == nil) != tc.valid {
|
||||
t.Fatalf("valid=%v err=%v", tc.valid, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
c := base()
|
||||
c.Findings[0].Evidence = &WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{total, phase}, Result: n(4)}
|
||||
c.Findings[0].Text = strings.Repeat("字", 5500)
|
||||
if err := ValidateWorkCheckpoint(c); err == nil || !strings.Contains(err.Error(), "16KiB") {
|
||||
t.Fatal("evidence bypassed checkpoint size cap", err)
|
||||
}
|
||||
}
|
||||
func TestTaskSnapshotBodyPreservesLimitsAndHonestStates(t *testing.T) {
|
||||
s := validTaskSnapshot()
|
||||
s.Remaining = "图片内容未纳入"
|
||||
s.ExecutionState = "failed"
|
||||
s.Conclusion = "处理失败,已有发现可查看"
|
||||
s.Findings = []TaskFinding{{ID: "f1", Text: "金额缺失", Kind: "material_record", Status: "active", Revision: 1}}
|
||||
raw, _ := json.Marshal(s.SummaryBody())
|
||||
for _, want := range []string{"执行失败", "图片内容未纳入", "金额缺失", "处理失败"} {
|
||||
if !strings.Contains(string(raw), want) {
|
||||
t.Errorf("lost critical summary %s", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskSnapshotUnstartedAndFinishedDoNotClaimReading(t *testing.T) {
|
||||
for _, state := range []string{"queued", "completed", "interrupted"} {
|
||||
s := validTaskSnapshot()
|
||||
s.ExecutionState = state
|
||||
s.Conclusion = "处理已结束"
|
||||
b, _ := json.Marshal(s.SummaryBody())
|
||||
if strings.Contains(string(b), "正在读取") {
|
||||
t.Errorf("%s falsely claims active reading", state)
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
-1
@@ -149,6 +149,7 @@ export interface ActivityCard {
|
||||
id: ID; projectId: ID; conversationId?: ID;
|
||||
sourceRunId?: ID; anchorMessageId?: ID;
|
||||
kind: CardKind; title: string; status: CardStatus; tone?: string;
|
||||
task?: TaskSnapshot;
|
||||
body?: UINode[]; actions?: { id: string; label: string; style?: 'primary' | 'default' | 'danger'; command?: string }[];
|
||||
pinned?: boolean; createdAt: string; updatedAt: string; work?: WorkCard;
|
||||
}
|
||||
@@ -350,7 +351,7 @@ export interface PluginPageDecl {
|
||||
|
||||
export interface ParsedDocument {
|
||||
meta: {
|
||||
filename: string; mime?: string; parser: string; confidence?: number;
|
||||
filename: string; mime?: string; parser: string; confidence?: number; origin?: DocumentOrigin;
|
||||
crossCheck?: { with: string; diffBlocks: number; verdict: 'agree' | 'differ' };
|
||||
parsedAt: string;
|
||||
};
|
||||
@@ -413,3 +414,28 @@ export interface SurfaceMeta {
|
||||
conversationId?: ID;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// Persistent work projection; host-owned identities and revisions.
|
||||
export interface DocumentOrigin { artifactId: ID; contentHash: string; parserRevision: string }
|
||||
export interface TaskPhase { id: string; label: string; observedAt?: string }
|
||||
export interface TaskSourceRef { id: string; artifactId: ID; contentHash: string; parserRevision: string; blockIndex: number; label: string; quote?: string; availability?: string }
|
||||
export interface TaskResultRef { artifactId?: ID; surfaceId?: ID; version?: string; contentHash?: string; availability: string; name?: string }
|
||||
export interface TaskFinding { id: string; text: string; impact?: string; kind: string; status: string; revision: number; sources: TaskSourceRef[]; assumptions?: string; corrects?: string }
|
||||
export interface TaskSnapshot {
|
||||
inputs?: TaskSourceRef[]
|
||||
limitations?: string[]
|
||||
schemaVersion: number; taskId: ID; attemptId: ID; cardId: ID; projectId: ID; conversationId: ID;
|
||||
sourceMessageId?: ID; parentRunId?: ID; runId?: ID; ownerInstanceId: string; executionEpoch?: number;
|
||||
revision: number; sourceSeq: number; streamScope: string; inputRevision?: string; title: string; goal?: string;
|
||||
executionState: 'queued'|'running'|'awaiting_user'|'completed'|'failed'|'interrupted'; outcomeCompleteness: 'none'|'partial'|'complete'; deliveryState: 'pending'|'available'|'reported'; phase: TaskPhase;
|
||||
focus?: string; coverage?: string; remaining?: string; nextStep?: string; conclusion?: string;
|
||||
findings?: TaskFinding[]; actions?: UIAction[]; resultRefs?: TaskResultRef[]; lastActivityAt?: string; lastMeaningfulUpdateAt?: string;
|
||||
checkpointKeys?: Record<string,string>; checkpointCount?: number; checkpointSeq?: number;
|
||||
}
|
||||
export interface TaskSourceQuote { sourceId: string; blockIndex: number; quote: string }
|
||||
interface WorkEvidencePremise { role: "total" | "phase" | "dependent" | "direct_relation"; sourceIndex: number; value?: number }
|
||||
interface WorkFindingEvidence { mode: "direct_relation" | "difference" | "unresolved_relation"; premises: WorkEvidencePremise[]; result?: number }
|
||||
export interface WorkFindingCandidate { findingKey: string; text: string; impact?: string; kind: string; assumptions?: string; corrects?: string; sources: TaskSourceQuote[]; evidence?: WorkFindingEvidence }
|
||||
export interface WorkCheckpointCandidate { checkpointId: string; phaseId: string; focus?: string; remaining?: string; nextStep?: string; findings: WorkFindingCandidate[] }
|
||||
|
||||
export interface UIAction { id: string; label: string; style?: string; command?: string }
|
||||
|
||||
Reference in New Issue
Block a user