A Transparent and Verifiable Way to Sync the AT Protocol's PLC Directory
1package bundle
2
3import (
4 "fmt"
5 "path/filepath"
6 "time"
7
8 "tangled.org/atscan.net/plcbundle/plc"
9)
10
11const (
12 // BUNDLE_SIZE is the standard number of operations per bundle
13 BUNDLE_SIZE = 10000
14)
15
16// Bundle represents a PLC bundle
17type Bundle struct {
18 BundleNumber int `json:"bundle_number"`
19 StartTime time.Time `json:"start_time"`
20 EndTime time.Time `json:"end_time"`
21 Operations []plc.PLCOperation `json:"-"`
22 DIDCount int `json:"did_count"`
23
24 Hash string `json:"hash"` // Chain hash (primary)
25 ContentHash string `json:"content_hash"` // Content hash
26 Parent string `json:"parent"`
27
28 CompressedHash string `json:"compressed_hash"`
29 CompressedSize int64 `json:"compressed_size"`
30 UncompressedSize int64 `json:"uncompressed_size"`
31 Cursor string `json:"cursor"`
32 BoundaryCIDs []string `json:"boundary_cids,omitempty"`
33 Compressed bool `json:"compressed"`
34 CreatedAt time.Time `json:"created_at"`
35}
36
37// GetFilePath returns the file path for this bundle
38func (b *Bundle) GetFilePath(bundleDir string) string {
39 return filepath.Join(bundleDir, fmt.Sprintf("%06d.jsonl.zst", b.BundleNumber))
40}
41
42// GetUncompressedFilePath returns the uncompressed file path
43func (b *Bundle) GetUncompressedFilePath(bundleDir string) string {
44 return filepath.Join(bundleDir, fmt.Sprintf("%06d.jsonl", b.BundleNumber))
45}
46
47// OperationCount returns the number of operations (always BUNDLE_SIZE for complete bundles)
48func (b *Bundle) OperationCount() int {
49 if len(b.Operations) > 0 {
50 return len(b.Operations)
51 }
52 return BUNDLE_SIZE
53}
54
55// CompressionRatio returns the compression ratio
56func (b *Bundle) CompressionRatio() float64 {
57 if b.CompressedSize == 0 {
58 return 0
59 }
60 return float64(b.UncompressedSize) / float64(b.CompressedSize)
61}
62
63// ValidateForSave performs validation before saving (no hash required)
64func (b *Bundle) ValidateForSave() error {
65 if b.BundleNumber < 1 {
66 return fmt.Errorf("invalid bundle number: %d", b.BundleNumber)
67 }
68 if len(b.Operations) != BUNDLE_SIZE {
69 return fmt.Errorf("invalid operation count: expected %d, got %d", BUNDLE_SIZE, len(b.Operations))
70 }
71 if b.StartTime.After(b.EndTime) {
72 return fmt.Errorf("start_time is after end_time")
73 }
74 return nil
75}
76
77// Validate performs full validation (including hashes)
78func (b *Bundle) Validate() error {
79 if err := b.ValidateForSave(); err != nil {
80 return err
81 }
82 if b.Hash == "" {
83 return fmt.Errorf("missing hash")
84 }
85 if b.CompressedHash == "" {
86 return fmt.Errorf("missing compressed hash")
87 }
88 return nil
89}
90
91// BundleMetadata represents metadata about a bundle
92type BundleMetadata struct {
93 BundleNumber int `json:"bundle_number"`
94 StartTime time.Time `json:"start_time"`
95 EndTime time.Time `json:"end_time"`
96 OperationCount int `json:"operation_count"`
97 DIDCount int `json:"did_count"`
98
99 // Primary hash - cumulative chain hash (includes all history)
100 Hash string `json:"hash"`
101
102 // Content hash - SHA256 of bundle operations only
103 ContentHash string `json:"content_hash"`
104
105 // Parent chain hash - links to previous bundle
106 Parent string `json:"parent,omitempty"`
107
108 // File hashes and sizes
109 CompressedHash string `json:"compressed_hash"`
110 CompressedSize int64 `json:"compressed_size"`
111 UncompressedSize int64 `json:"uncompressed_size"`
112 Cursor string `json:"cursor"`
113 CreatedAt time.Time `json:"created_at"`
114}
115
116func (b *Bundle) ToMetadata() *BundleMetadata {
117 return &BundleMetadata{
118 BundleNumber: b.BundleNumber,
119 StartTime: b.StartTime,
120 EndTime: b.EndTime,
121 OperationCount: b.OperationCount(),
122 DIDCount: b.DIDCount,
123 Hash: b.Hash, // Chain hash
124 ContentHash: b.ContentHash, // Content hash
125 Parent: b.Parent,
126 CompressedHash: b.CompressedHash,
127 CompressedSize: b.CompressedSize,
128 UncompressedSize: b.UncompressedSize,
129 Cursor: b.Cursor,
130 CreatedAt: b.CreatedAt,
131 }
132}
133
134// VerificationResult contains the result of bundle verification
135type VerificationResult struct {
136 BundleNumber int
137 Valid bool
138 HashMatch bool
139 FileExists bool
140 Error error
141 LocalHash string
142 ExpectedHash string
143}
144
145// ChainVerificationResult contains the result of chain verification
146type ChainVerificationResult struct {
147 Valid bool
148 ChainLength int
149 BrokenAt int
150 Error string
151 VerifiedBundles []int
152}
153
154// DirectoryScanResult contains results from scanning a directory
155type DirectoryScanResult struct {
156 BundleDir string
157 BundleCount int
158 FirstBundle int
159 LastBundle int
160 MissingGaps []int
161 TotalSize int64 // Compressed size
162 TotalUncompressed int64 // Uncompressed size (NEW)
163 IndexUpdated bool
164}
165
166// Logger interface for bundle operations
167type Logger interface {
168 Printf(format string, v ...interface{})
169 Println(v ...interface{})
170}
171
172// Config holds configuration for bundle operations
173type Config struct {
174 BundleDir string
175 VerifyOnLoad bool
176 AutoRebuild bool
177 RebuildWorkers int // Number of workers for parallel rebuild (0 = auto-detect)
178 RebuildProgress func(current, total int) // Progress callback for rebuild
179 Logger Logger
180}
181
182// DefaultConfig returns default configuration
183func DefaultConfig(bundleDir string) *Config {
184 return &Config{
185 BundleDir: bundleDir,
186 VerifyOnLoad: true,
187 AutoRebuild: true,
188 RebuildWorkers: 0, // 0 means auto-detect CPU count
189 RebuildProgress: nil, // No progress callback by default
190 Logger: nil,
191 }
192}
193
194// CloneOptions configures cloning behavior
195type CloneOptions struct {
196 RemoteURL string
197 Workers int
198 SkipExisting bool
199 ProgressFunc func(downloaded, total int, bytesDownloaded, bytesTotal int64)
200 SaveInterval time.Duration
201 Verbose bool
202 Logger Logger
203}
204
205// CloneResult contains cloning results
206type CloneResult struct {
207 RemoteBundles int
208 Downloaded int
209 Failed int
210 Skipped int
211 TotalBytes int64
212 Duration time.Duration
213 Interrupted bool
214 FailedBundles []int
215}