返回 DeepSeek-Reasonix
historical_archive_stage.go
根目录 / desktop / historical_archive_stage.go
1 package main
2
3 import (
4 "context"
5 "fmt"
6 "path/filepath"
7 "reasonix/desktop/internal/workspacestate"
8 "reasonix/internal/session"
9 "slices"
10 "strings"
11 )
12
13 // The caller holds source ownership through the parent archive commit.
14 func (a *App) stageHistoricalArchive(ctx context.Context, id string, source historicalSource, fingerprint string) (workspacestate.SourceMapping, []string, error) {
15 state, err := a.workspaceRegistry().Load(ctx)
16 if err != nil {
17 return workspacestate.SourceMapping{}, nil, err
18 }
19 migration, mapping, mapped, err := a.resolveHistoricalArchiveSource(ctx, state, id, source, fingerprint)
20 if err != nil {
21 return workspacestate.SourceMapping{}, nil, err
22 }
23 if !mapped {
24 state, err = a.workspaceRegistry().Load(ctx)
25 if err != nil {
26 return workspacestate.SourceMapping{}, nil, err
27 }
28 op, err := a.claimHistoricalArchiveReservation(ctx, state, source, migration.mappingKey(source.path), fingerprint)
29 if err != nil {
30 return workspacestate.SourceMapping{}, nil, err
31 }
32 if op != nil {
33 migration.operationID = op.ID
34 if op.Phase == "content_ready" {
35 ref := session.SessionRef{HostID: localDesktopHostID, SessionID: op.Mapping.SessionID}
36 if err := a.validateDesktopWorkspaceMembership(ctx, op.WorkspaceID, ref); err != nil {
37 return workspacestate.SourceMapping{}, nil, err
38 }
39 if _, err := a.desktopSessionService("").Query().Snapshot(ctx, ref); err != nil {
40 return workspacestate.SourceMapping{}, nil, err
41 }
42 return *op.Mapping, []string{op.ID}, nil
43 }
44 }
45 workspace, err := a.ensureDesktopWorkspace(ctx, source.scope, source.root)
46 if err != nil {
47 return workspacestate.SourceMapping{}, nil, err
48 }
49 if err := a.convertHistoricalSource(ctx, source, migration, workspace); err != nil {
50 return workspacestate.SourceMapping{}, nil, fmt.Errorf("stage historical archive: %w", err)
51 }
52 state, err = a.workspaceRegistry().Load(ctx)
53 if err != nil {
54 return workspacestate.SourceMapping{}, nil, err
55 }
56 mapping, mapped, err = state.ResolveSource(migration.mappingKey(source.path))
57 if err != nil {
58 return workspacestate.SourceMapping{}, nil, err
59 }
60 }
61 if mapped {
62 return mapping, []string{}, nil
63 }
64 return historicalArchiveDependency(state, migration.mappingKey(source.path), fingerprint)
65 }
66
67 func historicalArchiveDependency(state workspacestate.State, key, fingerprint string) (workspacestate.SourceMapping, []string, error) {
68 var mapping workspacestate.SourceMapping
69 dependencies := []string{}
70 {
71 for _, op := range state.PendingOperations {
72 if op.Kind != "archive-import" || op.Phase != "content_ready" || op.Mapping == nil ||
73 op.Mapping.SourceKey != key || op.Mapping.Fingerprint != fingerprint {
74 continue
75 }
76 if len(dependencies) != 0 || len(op.SessionIDs) != 1 || op.Mapping.SessionID != op.SessionIDs[0] {
77 return workspacestate.SourceMapping{}, nil, workspacestate.ErrMutationConflict
78 }
79 mapping = *op.Mapping
80 dependencies = append(dependencies, op.ID)
81 }
82 if len(dependencies) == 0 {
83 return workspacestate.SourceMapping{}, nil, workspacestate.ErrMutationConflict
84 }
85 }
86 return mapping, dependencies, nil
87 }
88
89 func (a *App) claimHistoricalArchiveReservation(ctx context.Context, state workspacestate.State, source historicalSource, key, fingerprint string) (*workspacestate.Operation, error) {
90 var selected *workspacestate.Operation
91 for _, op := range state.PendingOperations {
92 if op.Phase == "committed" || op.Mapping == nil || op.Mapping.Fingerprint != fingerprint {
93 continue
94 }
95 base := strings.TrimSuffix(key, ":review:"+fingerprint)
96 if op.Mapping.SourceKey != key && (base == key || !slices.Contains(state.SourceKeys(op.Mapping.SourceKey), base)) {
97 continue
98 }
99 if selected != nil || len(op.SessionIDs) != 1 {
100 return nil, workspacestate.ErrMutationConflict
101 }
102 if op.Mapping.SessionID != op.SessionIDs[0] || op.Mapping.WorkspaceID != op.WorkspaceID {
103 return nil, workspacestate.ErrMutationConflict
104 }
105 if !sameDesktopPath(op.Mapping.Path, source.path) || op.WorkspaceID != desktopWorkspaceOwnerID(state, source.scope, source.root) {
106 return nil, workspacestate.ErrMutationConflict
107 }
108 copy := op
109 selected = &copy
110 }
111 if selected == nil {
112 return nil, nil
113 }
114 if selected.Kind == "import" {
115 if selected.Phase == "content_ready" {
116 release, err := session.NewFilesystemPersistence(a.desktopSessions.root).AcquireMaintenance(selected.SessionIDs[0])
117 if err != nil {
118 return nil, err
119 }
120 defer release()
121 if err := a.validateHistoricalArchiveContent(ctx, source, *selected); err != nil {
122 return nil, err
123 }
124 }
125 if err := a.workspaceRegistry().StageHistoricalArchive(ctx, *selected, state.Generation, key); err != nil {
126 return nil, err
127 }
128 selected.Kind, selected.Lifecycle = "archive-import", workspacestate.Archived
129 mapping := *selected.Mapping
130 mapping.SourceKey = key
131 selected.Mapping = &mapping
132 } else if selected.Kind != "archive-import" || selected.Mapping.SourceKey != key {
133 return nil, workspacestate.ErrMutationConflict
134 }
135 return selected, nil
136 }
137
138 // Called under target maintenance ownership, including immediately before the
139 // parent commits. The journal fingerprint alone cannot prove published content.
140 func (a *App) validateHistoricalArchiveContent(ctx context.Context, source historicalSource, op workspacestate.Operation) error {
141 if op.Mapping == nil || len(op.SessionIDs) != 1 {
142 return workspacestate.ErrMutationConflict
143 }
144 ref := session.SessionRef{HostID: localDesktopHostID, SessionID: op.SessionIDs[0]}
145 if err := a.validateDesktopWorkspaceMembership(ctx, op.WorkspaceID, ref); err != nil {
146 return err
147 }
148 got, err := canonicalMigrationDigest(ctx, a.desktopSessionService("").Query(), ref)
149 if err != nil {
150 return err
151 }
152 if source.format == "canonical" {
153 old, err := session.NewService("archive-proof", session.NewFilesystemPersistence(filepath.Dir(source.path)))
154 if err != nil {
155 return err
156 }
157 defer shutdownHistoricalProofService(old)
158 want, err := canonicalMigrationDigest(ctx, old.Query(), session.SessionRef{HostID: "archive-proof", SessionID: filepath.Base(source.path)})
159 if err != nil {
160 return err
161 }
162 if want != got {
163 return workspacestate.ErrMutationConflict
164 }
165 return nil
166 }
167 key := desktopLegacyMigrationKey(source.path)
168 if source.head != "" {
169 key = desktopLegacyHeadKey(source.path, source.head)
170 }
171 if source.format == "canonical" {
172 key = desktopCanonicalMigrationKey(filepath.Dir(source.path), filepath.Base(source.path))
173 }
174 ledger, err := readDesktopMigrationLedger()
175 if err != nil {
176 return err
177 }
178 record := ledger.Records[key]
179 if strings.Contains(op.Mapping.SourceKey, ":review:") {
180 if version, ok := ledger.Records[key+":review:"+op.Mapping.Fingerprint]; ok {
181 record = version
182 }
183 }
184 if record.TargetSessionID != ref.SessionID || record.ContentDigest == "" || got != record.ContentDigest {
185 return workspacestate.ErrMutationConflict
186 }
187 return nil
188 }
189
189 lines GO