Files
nucleic/Sources/NucleicCore/Carbon/CarbonManifest.swift
T

145 lines
6.4 KiB
Swift

import Foundation
import NucleicProtocol
/// One snapshot event in a Carbon stream chain (docs/CARBON_SHARDING.md §6.3): what was
/// captured, which generation/seq fences it, which manifest precedes it, and the ordered
/// shards whose decrypted concatenation is the payload.
///
/// Encoding is **canonical CBOR v1** — RFC 8949 §4.2 core deterministic (definite lengths,
/// map keys sorted by encoded bytes), which `CBOREncoder` produces by construction. Field
/// names are the frozen wire strings (`CarbonManifestTests` pins the bytes); `nil` optionals
/// are omitted, so two encoders can never disagree on a null representation. The manifest ID
/// is the SHA-256 of the *encrypted* manifest (`sealed(with:)`), mirroring shard IDs.
public struct CarbonManifest: Sendable, Codable, Equatable {
/// Project identity (spec: UUID / root commit / normalized remote — the transfer triple)
/// plus what a restorer needs to re-create the project when absent (§9.1 step 1).
public struct ProjectRef: Sendable, Codable, Equatable {
public var uuid: UUID
public var rootCommitSHA: String?
public var normalizedRemote: String?
public var cloneURL: String?
public var localOnly: Bool
public init(
uuid: UUID, rootCommitSHA: String? = nil, normalizedRemote: String? = nil,
cloneURL: String? = nil, localOnly: Bool = false
) {
self.uuid = uuid
self.rootCommitSHA = rootCommitSHA
self.normalizedRemote = normalizedRemote
self.cloneURL = cloneURL
self.localOnly = localOnly
}
}
/// An ordered reference to one payload shard. Plaintext concatenation order is the array
/// order; `byteCount` is the **ciphertext** size (what disk/wire accounting sums).
public struct ShardRef: Sendable, Codable, Equatable {
public var id: String
public var byteCount: UInt64
public init(id: String, byteCount: UInt64) {
self.id = id
self.byteCount = byteCount
}
}
public var v: Int
public var project: ProjectRef
public var stream: CarbonStreamKind
/// nil for per-project chains (the base `history` chain; the lock ledger).
public var sessionID: String?
/// Session streams: the session branch. Lock ledger: the domain ref (spec §3.4).
public var branch: String?
/// Session streams: the fork anchor, preserved verbatim.
public var baseSHA: String?
public var ownerDeviceID: String
/// The resuscitation-fence stamp (spec §10); the lock ledger rides its
/// `authorityGeneration` here (§3.4).
public var ownerGeneration: UInt64
/// Monotonic per (stream, sessionID, ownerGeneration), starting at 1.
public var snapshotSeq: UInt64
/// The previous manifest in this chain, or nil at a chain start/restart.
public var parentManifest: String?
/// worktreeSnapshot: tip at capture. history: the new tip the pack reaches.
public var branchTipSHA: String?
public var wtCommitSHA: String?
public var worktreeTreeSHA: String?
public var idxCommitSHA: String?
public var indexTreeSHA: String?
/// The index had unmerged entries at capture (spec §6.1 step 2); restore leaves
/// everything unstaged and the UI says so honestly.
public var indexUnavailable: Bool
public var payloadKind: CarbonPayloadKind
/// jsonlSegment: byte offset of this segment in the native file (0 restarts the file).
public var segmentOffset: UInt64?
public var backendSessionID: String?
/// Ordered; decrypted concatenation = the payload.
public var shards: [ShardRef]
/// Producer wall clock, informational only — **never** used for ordering (spec §10).
public var createdAtMs: UInt64
public init(
v: Int = 1, project: ProjectRef, stream: CarbonStreamKind, sessionID: String? = nil,
branch: String? = nil, baseSHA: String? = nil, ownerDeviceID: String,
ownerGeneration: UInt64, snapshotSeq: UInt64, parentManifest: String? = nil,
branchTipSHA: String? = nil, wtCommitSHA: String? = nil, worktreeTreeSHA: String? = nil,
idxCommitSHA: String? = nil, indexTreeSHA: String? = nil, indexUnavailable: Bool = false,
payloadKind: CarbonPayloadKind, segmentOffset: UInt64? = nil,
backendSessionID: String? = nil, shards: [ShardRef], createdAtMs: UInt64
) {
self.v = v
self.project = project
self.stream = stream
self.sessionID = sessionID
self.branch = branch
self.baseSHA = baseSHA
self.ownerDeviceID = ownerDeviceID
self.ownerGeneration = ownerGeneration
self.snapshotSeq = snapshotSeq
self.parentManifest = parentManifest
self.branchTipSHA = branchTipSHA
self.wtCommitSHA = wtCommitSHA
self.worktreeTreeSHA = worktreeTreeSHA
self.idxCommitSHA = idxCommitSHA
self.indexTreeSHA = indexTreeSHA
self.indexUnavailable = indexUnavailable
self.payloadKind = payloadKind
self.segmentOffset = segmentOffset
self.backendSessionID = backendSessionID
self.shards = shards
self.createdAtMs = createdAtMs
}
// MARK: - Canonical codec
/// The frozen plaintext encoding (deterministic CBOR). Byte-stable across platforms and
/// releases — `CarbonManifestTests.canonicalBytesAreFrozen` pins it.
public func canonicalCBOR() throws -> Data {
try CBOREncoder().encode(self)
}
public static func decode(canonical: Data) throws -> CarbonManifest {
try CBORDecoder().decode(CarbonManifest.self, from: canonical)
}
/// Encrypt with the project's crypto; the sealed blob's `id` is the **manifest ID**.
public func sealed(with crypto: CarbonCrypto) throws -> CarbonCrypto.Sealed {
try crypto.seal(try canonicalCBOR())
}
/// Decrypt + decode a sealed manifest blob.
public static func open(_ blob: Data, with crypto: CarbonCrypto) throws -> CarbonManifest {
try decode(canonical: try crypto.open(blob))
}
/// The head this manifest advances its chain to (once its shards are held — the honest-
/// head rule, spec §8.2 step 3).
public func head(manifestID: String) -> CarbonHead {
CarbonHead(
key: CarbonStreamKey(
projectUUID: project.uuid, stream: stream, sessionID: sessionID),
manifestID: manifestID, ownerGeneration: ownerGeneration, snapshotSeq: snapshotSeq)
}
}