返回 DeepSeek-Reasonix
todo_identity.go
根目录 / internal / evidence / todo_identity.go
1 package evidence
2
3 // Todo identity: how a citation resolves to a task-list item. A stable step id
4 // is the authority; the text predicates below are the fallback for lists that
5 // never carried one, where wording and position are all there is to go on.
6
7 import (
8 "context"
9 "strings"
10 )
11
12 // todoMatchAt is the one place a positive match is built, so every path reports
13 // the matched item's stable id alongside its position.
14 func todoMatchAt(index int, todo TodoItem) TodoStepMatch {
15 return TodoStepMatch{
16 Found: true,
17 Index: index,
18 Content: todo.Content,
19 Status: todo.Status,
20 ActiveForm: todo.ActiveForm,
21 StepID: todo.StepID,
22 }
23 }
24
25 // MatchStepID resolves a stable step id against the current list. It is the
26 // exact path complete_step should prefer: unlike a title or a 1-based number, an
27 // id survives the retitles and insertions a replan introduces.
28 func MatchStepID(stepID string, todos []TodoItem) (TodoStepMatch, bool) {
29 stepID = strings.TrimSpace(stepID)
30 if stepID == "" {
31 return TodoStepMatch{}, false
32 }
33 found := -1
34 for i, todo := range todos {
35 if todo.StepID == stepID {
36 if found >= 0 {
37 return TodoStepMatch{}, false
38 }
39 found = i
40 }
41 }
42 if found >= 0 {
43 return todoMatchAt(found+1, todos[found]), true
44 }
45 return TodoStepMatch{}, false
46 }
47
48 // TodoStepIDs lists the stable ids present in the list, for error messages that
49 // tell a model exactly which identities it may cite.
50 func TodoStepIDs(todos []TodoItem) []string {
51 out := make([]string, 0, len(todos))
52 for _, todo := range todos {
53 if todo.StepID != "" {
54 out = append(out, todo.StepID)
55 }
56 }
57 return out
58 }
59
60 // sameTodoIdentity answers by stable id whenever both items carry one: an id is
61 // identity, so a retitled step still matches and two steps that happen to share
62 // wording no longer collide. Text is the fallback for freehand lists.
63 func sameTodoIdentity(a, b TodoItem) bool {
64 if a.StepID != "" && b.StepID != "" {
65 return a.StepID == b.StepID
66 }
67 return sameStepText(a.Content, b.Content) || sameStepText(a.ActiveForm, b.ActiveForm)
68 }
69
70 func sameTodoMatch(todo TodoItem, match TodoStepMatch) bool {
71 return sameStepText(todo.Content, match.Content) || sameStepText(todo.ActiveForm, match.ActiveForm)
72 }
73
74 // todoContentRelates reports whether a todo item's preferred text has a
75 // recognisable semantic relationship (substring overlap) with the step match
76 // that was stored against a previous todo_write list. It returns true when
77 // the model has rephrased the same task, not swapped it for a different one.
78 func todoContentRelates(todo TodoItem, match TodoStepMatch) bool {
79 return textOverlaps(todo.Content, match.Content) ||
80 textOverlaps(todo.ActiveForm, match.ActiveForm)
81 }
82
83 func textOverlaps(a, b string) bool {
84 return stepTextContains(normalizeStepText(a), normalizeStepText(b))
85 }
86
87 // WithAcceptanceCriteria carries the ids of the approved plan's criteria into a
88 // tool call, so a proof citing one can be checked against the plan the user
89 // approved instead of resolving into nothing.
90 func WithAcceptanceCriteria(ctx context.Context, ids []string) context.Context {
91 if len(ids) == 0 {
92 return ctx
93 }
94 return context.WithValue(ctx, acceptanceCriteriaKey{}, append([]string(nil), ids...))
95 }
96
97 // AcceptanceCriteriaFromContext returns the approved plan's criterion ids.
98 func AcceptanceCriteriaFromContext(ctx context.Context) ([]string, bool) {
99 ids, ok := ctx.Value(acceptanceCriteriaKey{}).([]string)
100 return ids, ok && len(ids) > 0
101 }
102
103 type acceptanceCriteriaKey struct{}
104
104 lines GO