返回 DeepSeek-Reasonix
evidence_targets.go
根目录 / internal / tool / builtin / evidence_targets.go
1 package builtin
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "os"
8 "strings"
9
10 udiff "github.com/aymanbagabas/go-udiff"
11 "reasonix/internal/diff"
12 "reasonix/internal/readcoord"
13 "reasonix/internal/tool"
14 )
15
16 // Evidence is resolved by the same preview implementation that validates the
17 // writer's matches and builds its final edit. Multiple edits are compared to
18 // the original source, so text created by an earlier step needs no prior read.
19 func previewEvidence(change diff.Change, err error) (tool.EvidenceTargetInfo, error) {
20 if err != nil {
21 return tool.EvidenceTargetInfo{}, err
22 }
23 info := tool.EvidenceTargetInfo{Path: change.Path, SourceTextDigest: digestText(change.OldText)}
24 if change.Kind == diff.Create || change.OldText == change.NewText {
25 return info, nil
26 }
27 if change.Binary {
28 info.WholeFile = true
29 return info, nil
30 }
31 lines := strings.Split(strings.TrimSuffix(strings.ReplaceAll(change.OldText, "\r\n", "\n"), "\n"), "\n")
32 if change.OldText == "" {
33 return info, nil
34 }
35 var ranges []tool.ReadRange
36 for _, edit := range udiff.Lines(change.OldText, change.NewText) {
37 start := strings.Count(change.OldText[:edit.Start], "\n")
38 end := strings.Count(change.OldText[:edit.End], "\n")
39 if edit.End > edit.Start && change.OldText[edit.End-1] != '\n' {
40 end++
41 }
42 if start == end {
43 start = max(0, start-1)
44 end++
45 }
46 ranges = append(ranges, tool.ReadRange{Start: min(start, len(lines)-1), End: min(end, len(lines))})
47 }
48 info.Ranges = readcoord.Normalize(ranges)
49 for _, r := range info.Ranges {
50 for _, line := range lines[r.Start:r.End] {
51 info.Hashes = append(info.Hashes, digestText(line))
52 }
53 }
54 return info, nil
55 }
56
57 func (e editFile) DeclareEvidenceTarget(ctx context.Context, args json.RawMessage) (tool.EvidenceTargetInfo, error) {
58 change, err := e.Preview(ctx, args)
59 return versionedPreviewEvidence(ctx, e.overlay, change, err)
60 }
61 func (m multiEdit) DeclareEvidenceTarget(ctx context.Context, args json.RawMessage) (tool.EvidenceTargetInfo, error) {
62 change, err := m.Preview(ctx, args)
63 return versionedPreviewEvidence(ctx, m.overlay, change, err)
64 }
65 func (d deleteSymbol) DeclareEvidenceTarget(ctx context.Context, args json.RawMessage) (tool.EvidenceTargetInfo, error) {
66 change, err := d.Preview(ctx, args)
67 return versionedPreviewEvidence(ctx, d.overlay, change, err)
68 }
69 func (n notebookEdit) DeclareEvidenceTarget(ctx context.Context, args json.RawMessage) (tool.EvidenceTargetInfo, error) {
70 change, err := n.Preview(ctx, args)
71 return versionedPreviewEvidence(ctx, n.overlay, change, err)
72 }
73
74 // Bind the preview to the same current source route as Execute. A change
75 // between preview and capture fails closed instead of attaching a new version
76 // to old hashes; Execute rechecks this identity once more before writing.
77 func versionedPreviewEvidence(ctx context.Context, overlay FileOverlay, change diff.Change, previewErr error) (tool.EvidenceTargetInfo, error) {
78 info, err := previewEvidence(change, previewErr)
79 if err != nil || change.Kind == diff.Create {
80 return info, err
81 }
82 src, err := readEditSource(ctx, overlay, change.Path)
83 if err != nil {
84 return tool.EvidenceTargetInfo{}, err
85 }
86 if src.content != change.OldText {
87 return tool.EvidenceTargetInfo{}, &tool.OperationError{Diagnostic: tool.OperationDiagnostic{Code: tool.WriteEvidenceStale, Path: change.Path, Recovery: "source changed during evidence preview; re-read then retry"}, Cause: ErrFileChanged}
88 }
89 if err := src.assertUnchanged(ctx, overlay, change.Path); err != nil {
90 return tool.EvidenceTargetInfo{}, err
91 }
92 info.Snapshot = src.readSnapshot(change.Path)
93 return info, nil
94 }
95 func (m moveFile) DeclareEvidenceTarget(ctx context.Context, args json.RawMessage) (tool.EvidenceTargetInfo, error) {
96 var p struct {
97 Source string `json:"source_path"`
98 Destination string `json:"destination_path"`
99 }
100 if err := json.Unmarshal(args, &p); err != nil {
101 return tool.EvidenceTargetInfo{}, err
102 }
103 if p.Source == "" || p.Destination == "" {
104 return tool.EvidenceTargetInfo{}, fmt.Errorf("source_path and destination_path are required")
105 }
106 source, dest := resolveIn(m.workDir, p.Source), resolveIn(m.workDir, p.Destination)
107 if source == dest {
108 return tool.EvidenceTargetInfo{}, nil
109 }
110 for _, path := range []string{source, dest} {
111 if err := confinePreview(effectiveWriteRoots(ctx, m.rootSet, m.roots), m.guard, m.managed, path); err != nil {
112 return tool.EvidenceTargetInfo{}, err
113 }
114 }
115 id, err := diskIdentity(source)
116 if err != nil {
117 return tool.EvidenceTargetInfo{}, err
118 }
119 if !id.existed {
120 return tool.EvidenceTargetInfo{}, &tool.OperationError{Diagnostic: tool.OperationDiagnostic{Code: tool.WriteTargetAbsent, Path: source, Recovery: "read the source at its current path"}, Cause: os.ErrNotExist}
121 }
122 return tool.EvidenceTargetInfo{Path: source, Snapshot: tool.SourceSnapshot(tool.ReadSourceDisk, source, fmt.Sprintf("raw-sha256:%x", id.sum)), PreservesContent: true}, nil
123 }
124
124 lines GO