@@ -132,19 +132,24 @@ type Exclusions struct {
132132// Manifest is the on-disk MANIFEST.json structure. Field tags match the
133133// spec in docs/design/2026_04_29_proposed_snapshot_logical_decoder.md.
134134type Manifest struct {
135- FormatVersion uint32 `json:"format_version"`
136- Phase string `json:"phase"`
137- ElastickvVersion string `json:"elastickv_version,omitempty"`
138- ClusterID string `json:"cluster_id,omitempty"`
139- SnapshotIndex uint64 `json:"snapshot_index,omitempty"`
140- LastCommitTS uint64 `json:"last_commit_ts,omitempty"`
141- WallTimeISO string `json:"wall_time_iso"`
142- Source * Source `json:"source,omitempty"`
143- Live * Live `json:"live,omitempty"`
144- Adapters Adapters `json:"adapters"`
145- Exclusions Exclusions `json:"exclusions"`
146- ChecksumAlgorithm string `json:"checksum_algorithm"`
147- ChecksumFormat string `json:"checksum_format"`
135+ FormatVersion uint32 `json:"format_version"`
136+ Phase string `json:"phase"`
137+ ElastickvVersion string `json:"elastickv_version,omitempty"`
138+ ClusterID string `json:"cluster_id,omitempty"`
139+ SnapshotIndex uint64 `json:"snapshot_index,omitempty"`
140+ LastCommitTS uint64 `json:"last_commit_ts,omitempty"`
141+ WallTimeISO string `json:"wall_time_iso"`
142+ Source * Source `json:"source,omitempty"`
143+ Live * Live `json:"live,omitempty"`
144+ // Adapters and Exclusions are pointer types so ReadManifest can
145+ // distinguish "section omitted entirely" (a corrupted or
146+ // truncated dump that should fail validation) from "section
147+ // present but populated with default values" (legitimate
148+ // scope-everything-excluded). Codex P2 #146 (round 3).
149+ Adapters * Adapters `json:"adapters"`
150+ Exclusions * Exclusions `json:"exclusions"`
151+ ChecksumAlgorithm string `json:"checksum_algorithm"`
152+ ChecksumFormat string `json:"checksum_format"`
148153
149154 EncodedFilenameCharset string `json:"encoded_filename_charset"`
150155 KeySegmentMaxBytes uint32 `json:"key_segment_max_bytes"`
@@ -163,12 +168,17 @@ var ErrInvalidManifest = errors.New("backup: manifest invalid")
163168
164169// NewPhase0SnapshotManifest seeds a manifest with the Phase 0a defaults.
165170// Callers fill in scope (Adapters), Source/wall time and exclusions before
166- // passing it to WriteManifest.
171+ // passing it to WriteManifest. Adapters and Exclusions are seeded to
172+ // non-nil zero values so the resulting manifest passes the
173+ // "section-present" validation; callers populating individual scopes
174+ // reach in via the now-non-nil pointer.
167175func NewPhase0SnapshotManifest (now time.Time ) Manifest {
168176 return Manifest {
169177 FormatVersion : CurrentFormatVersion ,
170178 Phase : PhasePhase0SnapshotDecode ,
171179 WallTimeISO : now .UTC ().Format (time .RFC3339Nano ),
180+ Adapters : & Adapters {},
181+ Exclusions : & Exclusions {},
172182 ChecksumAlgorithm : ChecksumAlgorithmSHA256 ,
173183 ChecksumFormat : ChecksumFormatSha256sum ,
174184 EncodedFilenameCharset : EncodedFilenameCharsetRFC3986 ,
@@ -266,6 +276,15 @@ func (m Manifest) validateRequiredFields() error {
266276 if _ , err := time .Parse (time .RFC3339Nano , m .WallTimeISO ); err != nil {
267277 return errors .Wrapf (ErrInvalidManifest , "wall_time_iso unparseable: %v" , err )
268278 }
279+ // Adapters and Exclusions are required structural sections.
280+ // A manifest that omits either is treated as truncated/corrupted
281+ // (Codex P2 #146 round 3).
282+ if m .Adapters == nil {
283+ return errors .Wrap (ErrInvalidManifest , "adapters section missing" )
284+ }
285+ if m .Exclusions == nil {
286+ return errors .Wrap (ErrInvalidManifest , "exclusions section missing" )
287+ }
269288 return nil
270289}
271290
0 commit comments