Skip to main content

varve_core/
linestatus.rs

1//! Line-status documents (REQ-KP-001, DD-008) — signed, updatable evidence
2//! beside immutable layers.
3//!
4//! One document per release line carries what changes *after* deposit:
5//! known problems (the Ferrocene schema: workaround, detection, mitigation,
6//! affected layers), the support window, and yank markers. It is DSSE-signed
7//! with the same root as layers but under its own payload type, carries its
8//! own monotonic counter, and is cached per line so `varve status` answers
9//! offline. A yanked layer warns loudly but remains installable — the
10//! consumer owns the freeze decision.
11
12use std::collections::BTreeMap;
13use std::path::{Path, PathBuf};
14
15use serde::{Deserialize, Serialize};
16
17use crate::install::VerifyError;
18use crate::layer::{LayerId, Line};
19use crate::verify::{dsse_sign_typed, dsse_verify_typed};
20
21/// The authenticated payload type — a signed something-else cannot be
22/// replayed as a status document.
23pub const LINE_STATUS_PAYLOAD_TYPE: &str = "application/vnd.pulseengine.varve.line-status.v1+json";
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(deny_unknown_fields)]
27pub struct KnownProblem {
28    pub id: String,
29    pub title: String,
30    pub severity: String,
31    /// Layers affected, e.g. ["2026.07.0", "2026.07.1"].
32    pub affected: Vec<String>,
33    #[serde(skip_serializing_if = "Option::is_none", default)]
34    pub workaround: Option<String>,
35    #[serde(skip_serializing_if = "Option::is_none", default)]
36    pub detection: Option<String>,
37    #[serde(skip_serializing_if = "Option::is_none", default)]
38    pub mitigation: Option<String>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(deny_unknown_fields)]
43pub struct LineStatus {
44    /// The release line, e.g. "2026.07".
45    pub line: String,
46    /// Monotonic per-line document counter — a stale advisory must not
47    /// silently replace a newer one.
48    pub counter: u64,
49    /// RFC 3339 issue time of this document.
50    #[serde(rename = "issued-at")]
51    pub issued_at: String,
52    /// Stated support window end, RFC 3339 date, if committed.
53    #[serde(
54        rename = "support-until",
55        skip_serializing_if = "Option::is_none",
56        default
57    )]
58    pub support_until: Option<String>,
59    /// Yanked layers of this line, layer → reason.
60    #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
61    pub yanked: BTreeMap<String, String>,
62    #[serde(
63        rename = "known-problems",
64        skip_serializing_if = "Vec::is_empty",
65        default
66    )]
67    pub known_problems: Vec<KnownProblem>,
68}
69
70#[derive(Debug, thiserror::Error)]
71pub enum LineStatusError {
72    /// The DSSE envelope failed verification or was malformed. Carries the
73    /// verifier's reason verbatim — but under a line-status heading, because
74    /// the old `#[error(transparent)]` route surfaced these as "manifest
75    /// signature verification failed", sending the reader to the wrong
76    /// document entirely (varve#60).
77    #[error("line-status envelope rejected: {0}")]
78    Envelope(String),
79    #[error("cannot sign the line-status document: {0}")]
80    Sign(String),
81    #[error("line-status payload is not valid: {0}")]
82    Payload(String),
83    #[error("line-status covers line {got} but line {expected} was requested")]
84    LineMismatch { expected: String, got: String },
85    #[error(
86        "refusing stale line-status document for {line}: presented counter {presented}, cached {cached}"
87    )]
88    Stale {
89        line: String,
90        presented: u64,
91        cached: u64,
92    },
93    /// An advisory entry that can never fire (varve#61): `varve status`
94    /// matches `affected` ids and yank keys against installed layer ids
95    /// EXACTLY, so a typo'd id signs fine and then warns nobody. Refused on
96    /// the producing side, where the fix (re-sign) is still cheap.
97    #[error(
98        "{what} names layer '{id}', which is not a layer of line {line} ({reason}) — `varve \
99         status` matches layer ids exactly, so this entry would never fire for any installed \
100         layer; fix the id and re-sign the document"
101    )]
102    DeadReference {
103        what: String,
104        id: String,
105        line: String,
106        reason: String,
107    },
108    #[error(
109        "{layout} is not an OCI image layout (it has no index.json) — point --layout at the \
110         directory `varve deposit --out` produced"
111    )]
112    NotALayout { layout: String },
113    // The io source is NOT repeated in the message: anyhow's `{err:#}` chain
114    // already appends every source, and including it here printed the cause
115    // twice ("… No such file or directory: No such file or directory").
116    #[error("io error at {path}")]
117    Io {
118        path: String,
119        #[source]
120        source: std::io::Error,
121    },
122}
123
124impl LineStatus {
125    /// Verify an envelope against the trust root and parse the payload.
126    pub fn verify_and_parse(
127        envelope: &[u8],
128        root_public_key: &[u8],
129    ) -> Result<Self, LineStatusError> {
130        // Diagnose the not-an-envelope case BEFORE the verifier does: its
131        // wrapped parser error prints the cause twice and never names the
132        // commonest mistake — handing over the raw status JSON instead of the
133        // signed envelope (varve#60).
134        if let Ok(text) = std::str::from_utf8(envelope)
135            && wsc::dsse::DsseEnvelope::from_json(text).is_err()
136        {
137            return Err(not_an_envelope(text));
138        }
139        let payload = dsse_verify_typed(envelope, LINE_STATUS_PAYLOAD_TYPE, root_public_key)
140            .map_err(|VerifyError(msg)| {
141                // A wrong-realm signature is indistinguishable from tampering
142                // at this layer; say so, because "No valid signatures" alone
143                // sends the reader hunting for corruption.
144                let hint = if msg.contains("does not verify") {
145                    " (is the document signed by THIS realm's root? `varve pubkey <key>` \
146                     prints the public half a signature verifies against)"
147                } else {
148                    ""
149                };
150                LineStatusError::Envelope(format!("{msg}{hint}"))
151            })?;
152        serde_json::from_slice(&payload).map_err(|e| LineStatusError::Payload(e.to_string()))
153    }
154
155    /// Sign a status document (the producing side — CI, next to deposit).
156    /// Refuses a document whose yank or `affected` entries could never fire
157    /// (varve#61) — a typo'd layer id is cheapest to fix before the signature
158    /// exists.
159    pub fn sign(&self, secret_key: &[u8], key_id: &str) -> Result<String, LineStatusError> {
160        self.check_layer_refs()?;
161        let payload = serde_json::to_vec_pretty(self).expect("status serializes");
162        dsse_sign_typed(&payload, LINE_STATUS_PAYLOAD_TYPE, secret_key, key_id)
163            .map_err(|VerifyError(msg)| LineStatusError::Sign(msg))
164    }
165
166    /// Every yank key and every known problem's `affected` id must be a layer
167    /// of THIS document's line (varve#61). `report_for` matches ids exactly,
168    /// and the cache is keyed per line, so an id outside the line — or one
169    /// that is not a layer id at all — is an advisory that signs fine and
170    /// then fires for nobody. Enforced wherever a producer commits the
171    /// document: `sign` and `attach_envelope_to_layout`.
172    pub fn check_layer_refs(&self) -> Result<(), LineStatusError> {
173        let line: Line = self.line.parse().map_err(|e| {
174            LineStatusError::Payload(format!("'{}' is not a YYYY.MM line: {e}", self.line))
175        })?;
176        let check = |what: String, id: &str| -> Result<(), LineStatusError> {
177            let dead = |reason: String| LineStatusError::DeadReference {
178                what: what.clone(),
179                id: id.to_string(),
180                line: self.line.clone(),
181                reason,
182            };
183            match id.parse::<LayerId>() {
184                Ok(layer) if layer.line() == &line => Ok(()),
185                Ok(layer) => Err(dead(format!("it belongs to line {}", layer.line()))),
186                Err(e) => Err(dead(e.to_string())),
187            }
188        };
189        for id in self.yanked.keys() {
190            check("the yank entry".to_string(), id)?;
191        }
192        for kp in &self.known_problems {
193            for id in &kp.affected {
194                check(format!("known problem '{}'", kp.id), id)?;
195            }
196        }
197        Ok(())
198    }
199
200    /// What this document says about one layer.
201    pub fn report_for(&self, layer: &LayerId) -> LayerStatusReport {
202        let name = layer.to_string();
203        let problems: Vec<&KnownProblem> = self
204            .known_problems
205            .iter()
206            .filter(|kp| kp.affected.iter().any(|a| a == &name))
207            .collect();
208        LayerStatusReport {
209            yanked_reason: self.yanked.get(&name).cloned(),
210            support_until: self.support_until.clone(),
211            problems_total: problems.len(),
212            problems_with_workaround: problems.iter().filter(|kp| kp.workaround.is_some()).count(),
213        }
214    }
215}
216
217#[derive(Debug, Clone, PartialEq, Eq)]
218pub struct LayerStatusReport {
219    pub yanked_reason: Option<String>,
220    pub support_until: Option<String>,
221    pub problems_total: usize,
222    pub problems_with_workaround: usize,
223}
224
225/// Per-line cache of the newest verified document, under the varve root.
226#[derive(Debug)]
227pub struct StatusCache {
228    dir: PathBuf,
229}
230
231impl StatusCache {
232    pub fn at_root(root: &Path) -> Self {
233        StatusCache {
234            dir: root.join("state").join("line-status"),
235        }
236    }
237
238    /// Store a VERIFIED envelope for its line, refusing counter regressions.
239    pub fn update(
240        &self,
241        line: &Line,
242        envelope: &[u8],
243        parsed: &LineStatus,
244    ) -> Result<(), LineStatusError> {
245        if let Some(cached) = self.load_parsed(line)?
246            && parsed.counter < cached.counter
247        {
248            return Err(LineStatusError::Stale {
249                line: line.to_string(),
250                presented: parsed.counter,
251                cached: cached.counter,
252            });
253        }
254        let io = |path: &Path, source: std::io::Error| LineStatusError::Io {
255            path: path.display().to_string(),
256            source,
257        };
258        std::fs::create_dir_all(&self.dir).map_err(|e| io(&self.dir, e))?;
259        let path = self.envelope_path(line);
260        std::fs::write(&path, envelope).map_err(|e| io(&path, e))?;
261        Ok(())
262    }
263
264    /// The cached envelope bytes for a line, unparsed, if any.
265    ///
266    /// Used by `archive` to carry the baseline across an air gap (varve#77).
267    /// Deliberately opaque: the caller re-attaches the bytes verbatim, and the
268    /// far side re-verifies against its own trust root — archiving must not
269    /// become a place where a document is re-signed or re-shaped.
270    pub fn envelope_bytes(&self, line: &Line) -> Result<Option<Vec<u8>>, LineStatusError> {
271        let path = self.envelope_path(line);
272        match std::fs::read(&path) {
273            Ok(bytes) => Ok(Some(bytes)),
274            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
275            Err(source) => Err(LineStatusError::Io {
276                path: path.display().to_string(),
277                source,
278            }),
279        }
280    }
281
282    /// Load and re-verify the cached envelope for a line.
283    pub fn load(
284        &self,
285        line: &Line,
286        root_public_key: &[u8],
287    ) -> Result<Option<LineStatus>, LineStatusError> {
288        let path = self.envelope_path(line);
289        match std::fs::read(&path) {
290            Ok(bytes) => Ok(Some(LineStatus::verify_and_parse(&bytes, root_public_key)?)),
291            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
292            Err(source) => Err(LineStatusError::Io {
293                path: path.display().to_string(),
294                source,
295            }),
296        }
297    }
298
299    fn load_parsed(&self, line: &Line) -> Result<Option<LineStatus>, LineStatusError> {
300        let path = self.envelope_path(line);
301        match std::fs::read(&path) {
302            Ok(bytes) => {
303                // Cached envelopes were verified at update time; parse the
304                // payload without re-verifying just to read the counter.
305                let text = std::str::from_utf8(&bytes)
306                    .map_err(|_| LineStatusError::Payload("cache is not UTF-8".into()))?;
307                let env = wsc::dsse::DsseEnvelope::from_json(text)
308                    .map_err(|e| LineStatusError::Payload(e.to_string()))?;
309                let payload = env
310                    .payload_bytes()
311                    .map_err(|e| LineStatusError::Payload(e.to_string()))?;
312                Ok(Some(
313                    serde_json::from_slice(&payload)
314                        .map_err(|e| LineStatusError::Payload(e.to_string()))?,
315                ))
316            }
317            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
318            Err(source) => Err(LineStatusError::Io {
319                path: path.display().to_string(),
320                source,
321            }),
322        }
323    }
324
325    fn envelope_path(&self, line: &Line) -> PathBuf {
326        self.dir.join(format!("{line}.dsse.json"))
327    }
328}
329
330/// artifactType for a line-status envelope carried in an OCI image layout.
331pub const LINE_STATUS_ARTIFACT_TYPE: &str = LINE_STATUS_PAYLOAD_TYPE;
332/// Annotation naming the line a carried status document covers.
333pub const ANN_LINE: &str = "eu.pulseengine.varve.status-line";
334
335/// Attach a (verified-by-the-caller) status envelope to an existing OCI
336/// image layout — evidence added AFTER deposit, without touching any layer
337/// blob or digest. Replaces a previous document for the same line.
338pub fn attach_to_layout(
339    layout: &Path,
340    line: &Line,
341    envelope: &[u8],
342) -> Result<(), LineStatusError> {
343    let io = |path: &Path, source: std::io::Error| LineStatusError::Io {
344        path: path.display().to_string(),
345        source,
346    };
347    let digest = crate::store::manifest_digest(envelope);
348    let hex = digest.strip_prefix("sha256:").expect("digest shape");
349    let blob_dir = layout.join("blobs").join("sha256");
350    std::fs::create_dir_all(&blob_dir).map_err(|e| io(&blob_dir, e))?;
351    let blob_path = blob_dir.join(hex);
352    std::fs::write(&blob_path, envelope).map_err(|e| io(&blob_path, e))?;
353
354    let index_path = layout.join("index.json");
355    let mut index: serde_json::Value =
356        serde_json::from_slice(&std::fs::read(&index_path).map_err(|e| io(&index_path, e))?)
357            .map_err(|e| LineStatusError::Payload(format!("index.json: {e}")))?;
358    let entries = index["manifests"]
359        .as_array_mut()
360        .ok_or_else(|| LineStatusError::Payload("index.json has no manifests array".into()))?;
361    let line_name = line.to_string();
362    entries.retain(|e| {
363        !(e["artifactType"] == LINE_STATUS_ARTIFACT_TYPE
364            && e["annotations"][ANN_LINE] == *line_name)
365    });
366    entries.push(serde_json::json!({
367        "mediaType": "application/json",
368        "artifactType": LINE_STATUS_ARTIFACT_TYPE,
369        "digest": digest,
370        "size": envelope.len(),
371        "annotations": { ANN_LINE: line_name }
372    }));
373    std::fs::write(
374        &index_path,
375        serde_json::to_vec_pretty(&index).expect("index serializes"),
376    )
377    .map_err(|e| io(&index_path, e))?;
378    Ok(())
379}
380
381/// Fetch the baseline line-status a source carries beside a layer, verify it
382/// against the trust root, and cache it monotonically (REQ-STATUS-DIST-001).
383/// Returns `Ok(Some(counter))` when a baseline was cached, `Ok(None)` when the
384/// source carries none. Verification, cache, or transport failures are `Err`
385/// — the caller decides severity (the CLI downgrades them to a note, since a
386/// bad baseline never blocks an otherwise-verified install, but it is never
387/// silently cached). The untrusted bytes are re-verified here; the source is
388/// not trusted to have checked them.
389pub fn cache_baseline_from_source(
390    source: &dyn crate::source::LayerSource,
391    layer: &crate::source::LayerRef,
392    line: &Line,
393    root_pk: &[u8],
394    store_root: &Path,
395) -> Result<Option<u64>, LineStatusError> {
396    let envelope = match source
397        .fetch_line_status(layer)
398        .map_err(|e| LineStatusError::Payload(format!("fetching baseline line-status: {e}")))?
399    {
400        Some(bytes) => bytes,
401        None => return Ok(None),
402    };
403    let doc = LineStatus::verify_and_parse(&envelope, root_pk)?;
404    // A validly-signed status for a DIFFERENT line must not be cached under
405    // this one — mirror the `--from-file` guard so all cache paths agree.
406    if doc.line != line.to_string() {
407        return Err(LineStatusError::LineMismatch {
408            expected: line.to_string(),
409            got: doc.line,
410        });
411    }
412    let counter = doc.counter;
413    StatusCache::at_root(store_root).update(line, &envelope, &doc)?;
414    Ok(Some(counter))
415}
416
417/// Attach a signed line-status envelope to a deposit layout, deriving the
418/// line from the document itself (REQ-STATUS-DIST-001). Returns the line and
419/// counter attached. The payload is read to learn the line but not verified
420/// here — install re-verifies the bytes against the trust root, and the
421/// deposit pipeline produced this envelope moments earlier with its own key.
422pub fn attach_envelope_to_layout(
423    layout: &Path,
424    envelope: &[u8],
425) -> Result<(Line, u64), LineStatusError> {
426    // Refuse a directory that is not a layout BEFORE writing anything into
427    // it: the old path created blobs/ inside an arbitrary directory and then
428    // failed on the missing index.json with a bare io error (varve#60).
429    if !layout.join("index.json").is_file() {
430        return Err(LineStatusError::NotALayout {
431            layout: layout.display().to_string(),
432        });
433    }
434    let text = std::str::from_utf8(envelope)
435        .map_err(|e| LineStatusError::Payload(format!("envelope is not utf-8: {e}")))?;
436    let env = wsc::dsse::DsseEnvelope::from_json(text).map_err(|_| not_an_envelope(text))?;
437    let payload = env
438        .payload_bytes()
439        .map_err(|e| LineStatusError::Payload(format!("envelope payload: {e}")))?;
440    let doc: LineStatus = serde_json::from_slice(&payload)
441        .map_err(|e| LineStatusError::Payload(format!("status document: {e}")))?;
442    let line: Line = doc
443        .line
444        .parse()
445        .map_err(|e| LineStatusError::Payload(format!("status line '{}': {e}", doc.line)))?;
446    // A yank or affected id outside the layout's line would attach fine and
447    // fire for nobody (varve#61) — this command knows the line, so it is the
448    // last producer-side place the typo is cheap to fix.
449    doc.check_layer_refs()?;
450    // Monotonicity holds here too. `status --from-file` and `install` both
451    // refuse a counter regression; attaching did not, so a re-run CI step could
452    // silently downgrade a layout's baseline — shipping a pre-yank document
453    // that fresh consumers cache and are told "not yanked" about a YANKED
454    // layer. The one place the rule was missing was the one that produces the
455    // artifact.
456    if let Some(existing) = read_any_from_layout(layout)?
457        && let Ok(prev) = parse_unverified(&existing)
458        && prev.line == doc.line
459        && doc.counter < prev.counter
460    {
461        return Err(LineStatusError::Stale {
462            line: doc.line.clone(),
463            presented: doc.counter,
464            cached: prev.counter,
465        });
466    }
467    // The status must belong to THIS layout's line. Attaching a 2099.01 status
468    // to a 2026.08 layout used to succeed, leaving the consumer to discover it
469    // (REQ-PRODUCER-001).
470    if let Some(layout_line) = layout_line(layout)
471        && layout_line != line.to_string()
472    {
473        return Err(LineStatusError::LineMismatch {
474            expected: layout_line,
475            got: line.to_string(),
476        });
477    }
478    attach_to_layout(layout, &line, envelope)?;
479    Ok((line, doc.counter))
480}
481
482/// The bytes are not a DSSE envelope — with the commonest cause named: the
483/// raw status document handed over where the SIGNED envelope belongs. The
484/// verifier's own wrapping ("not a DSSE envelope: Internal error: [Failed to
485/// parse DSSE envelope: …]") said the same thing twice and the fix zero
486/// times (varve#60).
487fn not_an_envelope(text: &str) -> LineStatusError {
488    if serde_json::from_str::<LineStatus>(text).is_ok() {
489        LineStatusError::Payload(
490            "this is the UNSIGNED status document, not a signed envelope — sign it first \
491             (`varve sign-status --file <doc> --key <key> --out <envelope>`) and pass the \
492             envelope"
493                .into(),
494        )
495    } else {
496        LineStatusError::Payload(
497            "not a DSSE envelope — expected the signed output of `varve sign-status`".into(),
498        )
499    }
500}
501
502/// Parse a status document out of an envelope WITHOUT verifying it. Used only
503/// to read back what a layout already carries, so a regression can be refused;
504/// the signature is checked wherever the document is actually trusted.
505fn parse_unverified(envelope: &[u8]) -> Result<LineStatus, LineStatusError> {
506    let text = std::str::from_utf8(envelope)
507        .map_err(|e| LineStatusError::Payload(format!("envelope is not utf-8: {e}")))?;
508    let env = wsc::dsse::DsseEnvelope::from_json(text).map_err(|_| not_an_envelope(text))?;
509    let payload = env
510        .payload_bytes()
511        .map_err(|e| LineStatusError::Payload(format!("envelope payload: {e}")))?;
512    serde_json::from_slice(&payload)
513        .map_err(|e| LineStatusError::Payload(format!("status document: {e}")))
514}
515
516/// The line a deposit layout's own manifest declares, if it can be read. Best
517/// effort: a layout we cannot introspect is not blocked from being annotated,
518/// but one that plainly disagrees is.
519pub(crate) fn layout_line(layout: &Path) -> Option<String> {
520    let index: serde_json::Value =
521        serde_json::from_slice(&std::fs::read(layout.join("index.json")).ok()?).ok()?;
522    for m in index["manifests"].as_array()? {
523        let digest = m["digest"].as_str()?.replace(':', "-");
524        let blob = layout
525            .join("blobs")
526            .join("sha256")
527            .join(digest.trim_start_matches("sha256-"));
528        let Ok(bytes) = std::fs::read(&blob) else {
529            continue;
530        };
531        // The layer envelope's payload carries the line annotation. Reuse the
532        // DSSE reader already used in this module rather than hand-rolling.
533        let Ok(text) = std::str::from_utf8(&bytes) else {
534            continue;
535        };
536        let Ok(env) = wsc::dsse::DsseEnvelope::from_json(text) else {
537            continue;
538        };
539        let Ok(payload) = env.payload_bytes() else {
540            continue;
541        };
542        let Ok(doc) = serde_json::from_slice::<serde_json::Value>(&payload) else {
543            continue;
544        };
545        if let Some(line) = doc["annotations"]["eu.pulseengine.varve.line"].as_str() {
546            return Some(line.to_string());
547        }
548    }
549    None
550}
551
552/// Read the single baseline status envelope a deposit layout carries,
553/// without needing to name its line (REQ-STATUS-DIST-001). A deposit layout
554/// holds exactly one line-status; a consumer installing by digest may not
555/// know the line up front. Returns the first line-status referrer found.
556pub fn read_any_from_layout(layout: &Path) -> Result<Option<Vec<u8>>, LineStatusError> {
557    let index_path = layout.join("index.json");
558    let bytes = match std::fs::read(&index_path) {
559        Ok(bytes) => bytes,
560        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
561        Err(source) => {
562            return Err(LineStatusError::Io {
563                path: index_path.display().to_string(),
564                source,
565            });
566        }
567    };
568    let index: serde_json::Value = serde_json::from_slice(&bytes)
569        .map_err(|e| LineStatusError::Payload(format!("index.json: {e}")))?;
570    let Some(entry) = index["manifests"].as_array().and_then(|entries| {
571        entries
572            .iter()
573            .find(|e| e["artifactType"] == LINE_STATUS_ARTIFACT_TYPE)
574    }) else {
575        return Ok(None);
576    };
577    let digest = entry["digest"]
578        .as_str()
579        .ok_or_else(|| LineStatusError::Payload("status entry has no digest".into()))?;
580    let hex = digest.strip_prefix("sha256:").unwrap_or(digest);
581    let blob_path = layout.join("blobs").join("sha256").join(hex);
582    std::fs::read(&blob_path)
583        .map(Some)
584        .map_err(|source| LineStatusError::Io {
585            path: blob_path.display().to_string(),
586            source,
587        })
588}
589
590/// Read the status envelope for a line from an OCI image layout, if carried.
591pub fn read_from_layout(layout: &Path, line: &Line) -> Result<Option<Vec<u8>>, LineStatusError> {
592    let index_path = layout.join("index.json");
593    let bytes = match std::fs::read(&index_path) {
594        Ok(bytes) => bytes,
595        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
596        Err(source) => {
597            return Err(LineStatusError::Io {
598                path: index_path.display().to_string(),
599                source,
600            });
601        }
602    };
603    let index: serde_json::Value = serde_json::from_slice(&bytes)
604        .map_err(|e| LineStatusError::Payload(format!("index.json: {e}")))?;
605    let line_name = line.to_string();
606    let Some(entry) = index["manifests"].as_array().and_then(|entries| {
607        entries.iter().find(|e| {
608            e["artifactType"] == LINE_STATUS_ARTIFACT_TYPE
609                && e["annotations"][ANN_LINE] == *line_name
610        })
611    }) else {
612        return Ok(None);
613    };
614    let digest = entry["digest"]
615        .as_str()
616        .ok_or_else(|| LineStatusError::Payload("status entry has no digest".into()))?;
617    let hex = digest.strip_prefix("sha256:").unwrap_or(digest);
618    let blob_path = layout.join("blobs").join("sha256").join(hex);
619    std::fs::read(&blob_path)
620        .map(Some)
621        .map_err(|source| LineStatusError::Io {
622            path: blob_path.display().to_string(),
623            source,
624        })
625}
626
627#[cfg(test)]
628mod tests {
629    use super::*;
630    use crate::verify::generate_root_keypair;
631
632    fn status(counter: u64) -> LineStatus {
633        LineStatus {
634            line: "2026.07".into(),
635            counter,
636            issued_at: "2026-08-07T00:00:00Z".into(),
637            support_until: Some("2028-07-31".into()),
638            yanked: BTreeMap::from([(
639                "2026.07.0".to_string(),
640                "CVE-2026-0001 in synth".to_string(),
641            )]),
642            known_problems: vec![
643                KnownProblem {
644                    id: "KP-1".into(),
645                    title: "synth mla fusion regresses flat_flight".into(),
646                    severity: "medium".into(),
647                    affected: vec!["2026.07.0".into()],
648                    workaround: Some("disable mla fusion".into()),
649                    detection: None,
650                    mitigation: None,
651                },
652                KnownProblem {
653                    id: "KP-2".into(),
654                    title: "witness truth-table gap on nested variants".into(),
655                    severity: "high".into(),
656                    affected: vec!["2026.07.0".into(), "2026.07.1".into()],
657                    workaround: None,
658                    detection: Some("witness gap rows non-empty".into()),
659                    mitigation: None,
660                },
661            ],
662        }
663    }
664
665    // rivet: verifies REQ-KP-001
666    #[test]
667    fn a_signed_status_document_round_trips() {
668        let (sk, pk) = generate_root_keypair();
669        let envelope = status(1).sign(&sk, "varve-root-1").unwrap();
670        let parsed = LineStatus::verify_and_parse(envelope.as_bytes(), &pk).unwrap();
671        assert_eq!(parsed, status(1));
672    }
673
674    // rivet: verifies REQ-KP-001
675    #[test]
676    fn a_layer_manifest_envelope_cannot_pose_as_a_status_document() {
677        let (sk, pk) = generate_root_keypair();
678        // Signed with the right key but the wrong payload type.
679        let manifest = crate::manifest::fixtures::manifest(
680            "2026.07.0",
681            "qualified",
682            1,
683            "2026-08-07T00:00:00Z",
684        );
685        let envelope = crate::verify::sign_layer_manifest(&manifest, &sk, "varve-root-1").unwrap();
686        let err = LineStatus::verify_and_parse(envelope.as_bytes(), &pk).unwrap_err();
687        assert!(err.to_string().contains("payload type"), "got: {err}");
688    }
689
690    // rivet: verifies REQ-KP-001
691    #[test]
692    fn the_report_names_yank_support_window_and_problem_counts() {
693        let doc = status(1);
694        let report = doc.report_for(&"2026.07.0".parse().unwrap());
695        assert_eq!(
696            report.yanked_reason.as_deref(),
697            Some("CVE-2026-0001 in synth")
698        );
699        assert_eq!(report.support_until.as_deref(), Some("2028-07-31"));
700        assert_eq!(report.problems_total, 2);
701        assert_eq!(report.problems_with_workaround, 1);
702        let clean = doc.report_for(&"2026.07.2".parse().unwrap());
703        assert_eq!(clean.yanked_reason, None);
704        assert_eq!(clean.problems_total, 0);
705    }
706
707    // rivet: verifies REQ-KP-001
708    #[test]
709    fn attaching_status_to_a_layout_leaves_every_layer_blob_untouched() {
710        use crate::deposit::{DepositSpec, DepositTool, deposit};
711        let (sk, pk) = generate_root_keypair();
712        let tmp = tempfile::tempdir().unwrap();
713        let dest = tmp.path().join("layout");
714        let spec = DepositSpec {
715            includes: Vec::new(),
716            layer: "2026.07.0".parse().unwrap(),
717            channel: "qualified".into(),
718            counter: 1,
719            issued_at: "2026-08-07T00:00:00Z".into(),
720            tools: vec![DepositTool {
721                name: "synth".into(),
722                version: "1".into(),
723                platform: None,
724                bytes: b"t".to_vec(),
725                source: None,
726                runner: None,
727                kind: None,
728                sdk_prefix: None,
729            }],
730        };
731        let outcome = deposit(&spec, &sk, "k", &dest).unwrap();
732
733        // Snapshot the layer-relevant blobs before attaching evidence.
734        let blob_dir = dest.join("blobs/sha256");
735        let before: std::collections::BTreeMap<String, Vec<u8>> = std::fs::read_dir(&blob_dir)
736            .unwrap()
737            .map(|e| {
738                let p = e.unwrap().path();
739                (
740                    p.file_name().unwrap().to_string_lossy().into_owned(),
741                    std::fs::read(&p).unwrap(),
742                )
743            })
744            .collect();
745
746        let line: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
747        let envelope = status(1).sign(&sk, "k").unwrap();
748        attach_to_layout(&dest, &line, envelope.as_bytes()).unwrap();
749
750        // Every pre-existing blob is byte-identical; the manifest digest is
751        // unchanged — evidence was added, identity was not.
752        for (name, bytes) in &before {
753            assert_eq!(&std::fs::read(blob_dir.join(name)).unwrap(), bytes);
754        }
755        let carried = read_from_layout(&dest, &line).unwrap().unwrap();
756        let parsed = LineStatus::verify_and_parse(&carried, &pk).unwrap();
757        assert_eq!(parsed.counter, 1);
758        let hex = outcome.digest.strip_prefix("sha256:").unwrap();
759        assert!(
760            blob_dir.join(hex).is_file(),
761            "layer manifest blob still present"
762        );
763
764        // Replacing the document for the same line keeps exactly one entry.
765        let envelope2 = status(2).sign(&sk, "k").unwrap();
766        attach_to_layout(&dest, &line, envelope2.as_bytes()).unwrap();
767        let index: serde_json::Value =
768            serde_json::from_slice(&std::fs::read(dest.join("index.json")).unwrap()).unwrap();
769        let count = index["manifests"]
770            .as_array()
771            .unwrap()
772            .iter()
773            .filter(|e| e["artifactType"] == LINE_STATUS_ARTIFACT_TYPE)
774            .count();
775        assert_eq!(count, 1);
776    }
777
778    // rivet: verifies REQ-KP-001
779    #[test]
780    fn the_cache_refuses_a_counter_regression() {
781        let (sk, pk) = generate_root_keypair();
782        let tmp = tempfile::tempdir().unwrap();
783        let cache = StatusCache::at_root(tmp.path());
784        let line: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
785
786        let newer = status(2);
787        let env2 = newer.sign(&sk, "k").unwrap();
788        cache.update(&line, env2.as_bytes(), &newer).unwrap();
789
790        let older = status(1);
791        let env1 = older.sign(&sk, "k").unwrap();
792        let err = cache.update(&line, env1.as_bytes(), &older).unwrap_err();
793        assert!(matches!(
794            err,
795            LineStatusError::Stale {
796                presented: 1,
797                cached: 2,
798                ..
799            }
800        ));
801
802        // The cached newer document survives and re-verifies.
803        let loaded = cache.load(&line, &pk).unwrap().unwrap();
804        assert_eq!(loaded.counter, 2);
805    }
806
807    // rivet: verifies REQ-STATUS-DIST-001
808    #[test]
809    fn a_source_baseline_is_verified_and_cached_so_status_works_offline() {
810        use crate::source::{LayerRef, MemorySource};
811        let (sk, pk) = generate_root_keypair();
812        let tmp = tempfile::tempdir().unwrap();
813        let store_root = tmp.path();
814        let line: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
815        let doc = status(5);
816        let envelope = doc.sign(&sk, "k").unwrap();
817        let source = MemorySource::new().with_line_status(envelope.as_bytes());
818        let layer = LayerRef::Name("2026.07.0".parse().unwrap());
819
820        let cached = cache_baseline_from_source(&source, &layer, &line, &pk, store_root).unwrap();
821        assert_eq!(
822            cached,
823            Some(5),
824            "a carried baseline is cached at its counter"
825        );
826
827        // Now `status` works offline: the cache has it, verified.
828        let loaded = StatusCache::at_root(store_root)
829            .load(&line, &pk)
830            .unwrap()
831            .unwrap();
832        assert_eq!(loaded.counter, 5);
833    }
834
835    // rivet: verifies REQ-STATUS-DIST-001, REQ-VERIFY-001
836    #[test]
837    fn a_baseline_for_the_wrong_line_is_refused_not_miscached() {
838        // A root-signed status document for a DIFFERENT line must not be
839        // cached under the requested line — even validly signed. (Clean-room
840        // review finding: the --from-file path asserted this; the baseline
841        // path did not.)
842        use crate::source::{LayerRef, MemorySource};
843        let (sk, pk) = generate_root_keypair();
844        let tmp = tempfile::tempdir().unwrap();
845        let requested: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
846        // A self-consistent document for the WRONG line — internally valid,
847        // validly signed, and still not the line this consumer asked about.
848        let doc = LineStatus {
849            line: "2026.08".into(),
850            counter: 5,
851            issued_at: "2026-08-07T00:00:00Z".into(),
852            support_until: None,
853            yanked: BTreeMap::new(),
854            known_problems: Vec::new(),
855        };
856        let envelope = doc.sign(&sk, "k").unwrap();
857        let source = MemorySource::new().with_line_status(envelope.as_bytes());
858        let err = cache_baseline_from_source(
859            &source,
860            &LayerRef::Name("2026.07.0".parse().unwrap()),
861            &requested,
862            &pk,
863            tmp.path(),
864        )
865        .unwrap_err();
866        assert!(
867            matches!(err, LineStatusError::LineMismatch { .. }),
868            "a baseline for the wrong line must be refused: {err}"
869        );
870        assert!(
871            StatusCache::at_root(tmp.path())
872                .load(&requested, &pk)
873                .unwrap()
874                .is_none(),
875            "nothing is cached under the requested line"
876        );
877    }
878
879    // rivet: verifies REQ-STATUS-DIST-001
880    #[test]
881    fn a_source_with_no_baseline_caches_nothing_and_does_not_error() {
882        use crate::source::{LayerRef, MemorySource};
883        let (_sk, pk) = generate_root_keypair();
884        let tmp = tempfile::tempdir().unwrap();
885        let line: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
886        let source = MemorySource::new();
887        let cached = cache_baseline_from_source(
888            &source,
889            &LayerRef::Name("2026.07.0".parse().unwrap()),
890            &line,
891            &pk,
892            tmp.path(),
893        )
894        .unwrap();
895        assert_eq!(cached, None);
896    }
897
898    // rivet: verifies REQ-STATUS-DIST-001, REQ-VERIFY-001
899    #[test]
900    fn a_baseline_signed_by_an_impostor_is_refused_not_cached() {
901        use crate::source::{LayerRef, MemorySource};
902        let (attacker_sk, _) = generate_root_keypair();
903        let (_real_sk, real_pk) = generate_root_keypair();
904        let tmp = tempfile::tempdir().unwrap();
905        let line: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
906        let envelope = status(5).sign(&attacker_sk, "k").unwrap();
907        let source = MemorySource::new().with_line_status(envelope.as_bytes());
908        let err = cache_baseline_from_source(
909            &source,
910            &LayerRef::Name("2026.07.0".parse().unwrap()),
911            &line,
912            &real_pk,
913            tmp.path(),
914        )
915        .unwrap_err();
916        // The impostor's baseline never reaches the cache.
917        assert!(
918            StatusCache::at_root(tmp.path())
919                .load(&line, &real_pk)
920                .unwrap()
921                .is_none(),
922            "a baseline that fails verification must not be cached: {err}"
923        );
924    }
925
926    // rivet: verifies REQ-STATUS-DIST-001
927    #[test]
928    fn attaching_by_envelope_derives_the_line_from_the_document() {
929        use crate::deposit::{DepositSpec, DepositTool, deposit};
930        let (sk, pk) = generate_root_keypair();
931        let tmp = tempfile::tempdir().unwrap();
932        let dest = tmp.path().join("layout");
933        deposit(
934            &DepositSpec {
935                includes: Vec::new(),
936                layer: "2026.07.0".parse().unwrap(),
937                channel: "qualified".into(),
938                counter: 1,
939                issued_at: "2026-08-07T00:00:00Z".into(),
940                tools: vec![DepositTool {
941                    name: "synth".into(),
942                    version: "1".into(),
943                    platform: None,
944                    bytes: b"t".to_vec(),
945                    source: None,
946                    runner: None,
947                    kind: None,
948                    sdk_prefix: None,
949                }],
950            },
951            &sk,
952            "k",
953            &dest,
954        )
955        .unwrap();
956
957        let envelope = status(4).sign(&sk, "k").unwrap();
958        let (line, counter) = attach_envelope_to_layout(&dest, envelope.as_bytes()).unwrap();
959        assert_eq!(line.to_string(), "2026.07");
960        assert_eq!(counter, 4);
961        // The layout now carries it and it re-verifies.
962        let carried = read_any_from_layout(&dest).unwrap().unwrap();
963        assert_eq!(
964            LineStatus::verify_and_parse(&carried, &pk).unwrap().counter,
965            4
966        );
967    }
968
969    // rivet: verifies REQ-PRODUCE-002
970    #[test]
971    fn attaching_a_stale_document_over_a_newer_one_is_refused() {
972        // An independent review deleted the Stale block from
973        // attach_envelope_to_layout and the whole workspace stayed green: the
974        // test cited as this clause's evidence exercises StatusCache::update, a
975        // DIFFERENT function, and the attach test above attaches exactly once.
976        // Unguarded, a re-run CI step downgrades a layout's baseline — shipping
977        // a pre-yank document that fresh consumers cache and are told "not
978        // yanked" about a YANKED layer.
979        use crate::deposit::{DepositSpec, DepositTool, deposit};
980        let (sk, _pk) = generate_root_keypair();
981        let tmp = tempfile::tempdir().unwrap();
982        let dest = tmp.path().join("layout");
983        deposit(
984            &DepositSpec {
985                includes: Vec::new(),
986                layer: "2026.07.0".parse().unwrap(),
987                channel: "qualified".into(),
988                counter: 1,
989                issued_at: "2026-08-07T00:00:00Z".into(),
990                tools: vec![DepositTool {
991                    name: "synth".into(),
992                    version: "1".into(),
993                    platform: None,
994                    bytes: b"t".to_vec(),
995                    source: None,
996                    runner: None,
997                    kind: None,
998                    sdk_prefix: None,
999                }],
1000            },
1001            &sk,
1002            "k",
1003            &dest,
1004        )
1005        .unwrap();
1006
1007        // The newer document lands…
1008        attach_envelope_to_layout(&dest, status(7).sign(&sk, "k").unwrap().as_bytes()).unwrap();
1009        // …and the older one is refused, naming both counters.
1010        let err = attach_envelope_to_layout(&dest, status(3).sign(&sk, "k").unwrap().as_bytes())
1011            .unwrap_err();
1012        assert!(
1013            matches!(
1014                err,
1015                LineStatusError::Stale {
1016                    presented: 3,
1017                    cached: 7,
1018                    ..
1019                }
1020            ),
1021            "a lower counter must be refused, got {err}"
1022        );
1023        let msg = err.to_string();
1024        assert!(msg.contains('3') && msg.contains('7'), "names both: {msg}");
1025        // The layout still carries the NEWER document, not the stale one.
1026        let carried = parse_unverified(&read_any_from_layout(&dest).unwrap().unwrap()).unwrap();
1027        assert_eq!(
1028            carried.counter, 7,
1029            "the newer baseline survives the attempt"
1030        );
1031        // Re-attaching the SAME counter is not a regression and is allowed —
1032        // CI re-runs must stay idempotent.
1033        attach_envelope_to_layout(&dest, status(7).sign(&sk, "k").unwrap().as_bytes()).unwrap();
1034    }
1035
1036    // rivet: verifies REQ-PRODUCE-002
1037    #[test]
1038    fn an_advisory_that_could_never_fire_is_refused_at_sign_time() {
1039        // varve#61: `report_for` matches ids EXACTLY and the cache is keyed
1040        // per line, so a typo'd affected id — "2026.9.0" for "2026.09.0" —
1041        // signed fine and the advisory then fired for nobody. The signature
1042        // is the cheapest place to stop it.
1043        let (sk, _pk) = generate_root_keypair();
1044        let cases: &[(&str, &str)] = &[
1045            ("2026.7.0", "not a valid YYYY.MM.P id"), // typo'd month width
1046            ("2026.07", "missing its patch component"), // a line, not a layer
1047            ("2026.08.0", "belongs to another line"), // wrong line entirely
1048            ("2026.07.O", "letter O for zero"),
1049        ];
1050        for (bad, why) in cases {
1051            let mut doc = status(1);
1052            doc.known_problems[0].affected = vec![bad.to_string()];
1053            let err = doc.sign(&sk, "k").unwrap_err();
1054            assert!(
1055                matches!(err, LineStatusError::DeadReference { .. }),
1056                "{why}: affected id {bad:?} must be refused, got: {err}"
1057            );
1058            let msg = err.to_string();
1059            assert!(
1060                msg.contains(bad) && msg.contains("2026.07") && msg.contains("re-sign"),
1061                "the error must name the id, the line, and the fix: {msg}"
1062            );
1063        }
1064        // A typo'd YANK key is the same dead advisory.
1065        let mut doc = status(1);
1066        doc.yanked = BTreeMap::from([("2026.8.0".to_string(), "CVE".to_string())]);
1067        assert!(matches!(
1068            doc.sign(&sk, "k").unwrap_err(),
1069            LineStatusError::DeadReference { .. }
1070        ));
1071        // …and the untouched fixture still signs: the gate can pass, not
1072        // merely fail.
1073        status(1).sign(&sk, "k").unwrap();
1074    }
1075
1076    // rivet: verifies REQ-PRODUCE-002
1077    #[test]
1078    fn attach_refuses_a_pre_signed_advisory_that_could_never_fire() {
1079        // The envelope may come from an older varve whose sign-status did not
1080        // validate — attach is the last producer-side gate before the layout
1081        // ships. Built with the raw signer to bypass `sign`'s own check,
1082        // exactly as an old binary would have.
1083        use crate::deposit::{DepositSpec, DepositTool, deposit};
1084        let (sk, _pk) = generate_root_keypair();
1085        let tmp = tempfile::tempdir().unwrap();
1086        let dest = tmp.path().join("layout");
1087        deposit(
1088            &DepositSpec {
1089                includes: Vec::new(),
1090                layer: "2026.07.0".parse().unwrap(),
1091                channel: "qualified".into(),
1092                counter: 1,
1093                issued_at: "2026-08-07T00:00:00Z".into(),
1094                tools: vec![DepositTool {
1095                    name: "synth".into(),
1096                    version: "1".into(),
1097                    platform: None,
1098                    bytes: b"t".to_vec(),
1099                    source: None,
1100                    runner: None,
1101                    kind: None,
1102                    sdk_prefix: None,
1103                }],
1104            },
1105            &sk,
1106            "k",
1107            &dest,
1108        )
1109        .unwrap();
1110        let mut doc = status(1);
1111        doc.known_problems[0].affected = vec!["2026.7.0".to_string()];
1112        let payload = serde_json::to_vec_pretty(&doc).unwrap();
1113        let envelope = dsse_sign_typed(&payload, LINE_STATUS_PAYLOAD_TYPE, &sk, "k").unwrap();
1114        let err = attach_envelope_to_layout(&dest, envelope.as_bytes()).unwrap_err();
1115        assert!(
1116            matches!(err, LineStatusError::DeadReference { .. }),
1117            "got: {err}"
1118        );
1119        assert!(
1120            read_any_from_layout(&dest).unwrap().is_none(),
1121            "the dead advisory must not land in the layout"
1122        );
1123    }
1124
1125    // rivet: verifies REQ-PRODUCE-002
1126    #[test]
1127    fn attaching_to_a_directory_that_is_not_a_layout_is_refused_before_writing() {
1128        // The old path created blobs/sha256/ inside the directory and then
1129        // failed on index.json with "io error at …: No such file or directory
1130        // (os error 2): No such file or directory (os error 2)" — the cause
1131        // twice, the fix never (varve#60).
1132        let (sk, _pk) = generate_root_keypair();
1133        let tmp = tempfile::tempdir().unwrap();
1134        let not_a_layout = tmp.path().join("somedir");
1135        std::fs::create_dir_all(&not_a_layout).unwrap();
1136        let envelope = status(1).sign(&sk, "k").unwrap();
1137        let err = attach_envelope_to_layout(&not_a_layout, envelope.as_bytes()).unwrap_err();
1138        assert!(
1139            matches!(err, LineStatusError::NotALayout { .. }),
1140            "got: {err}"
1141        );
1142        assert!(
1143            err.to_string().contains("varve deposit"),
1144            "the error must carry its fix: {err}"
1145        );
1146        assert!(
1147            !not_a_layout.join("blobs").exists(),
1148            "nothing may be written into a directory that is not a layout"
1149        );
1150    }
1151
1152    // rivet: verifies REQ-PRODUCE-002
1153    #[test]
1154    fn the_unsigned_document_mistake_is_named_not_wrapped() {
1155        // Handing the raw status JSON where the signed envelope belongs is
1156        // the commonest producer mistake; the old error was a doubled parser
1157        // wrap that never said "sign it" (varve#60).
1158        let raw = serde_json::to_string_pretty(&status(1)).unwrap();
1159        let err = not_an_envelope(&raw);
1160        let msg = err.to_string();
1161        assert!(
1162            msg.contains("UNSIGNED") && msg.contains("varve sign-status"),
1163            "raw document must be diagnosed with its fix: {msg}"
1164        );
1165        // Garbage is still garbage, said once, with the expected shape named.
1166        let msg = not_an_envelope("garbage").to_string();
1167        assert!(
1168            msg.contains("not a DSSE envelope") && msg.contains("varve sign-status"),
1169            "got: {msg}"
1170        );
1171        // And verify_and_parse routes through the same diagnosis.
1172        let (_sk, pk) = generate_root_keypair();
1173        let err = LineStatus::verify_and_parse(raw.as_bytes(), &pk).unwrap_err();
1174        assert!(err.to_string().contains("UNSIGNED"), "got: {err}");
1175    }
1176
1177    // rivet: verifies REQ-STATUS-DIST-001
1178    #[test]
1179    fn a_deposit_layouts_baseline_is_readable_without_naming_the_line() {
1180        // A registry/layout consumer that installs by digest may not know the
1181        // line up front — the baseline must be recoverable from the layout
1182        // alone. A deposit layout carries exactly one line-status.
1183        use crate::deposit::{DepositSpec, DepositTool, deposit};
1184        let (sk, pk) = generate_root_keypair();
1185        let tmp = tempfile::tempdir().unwrap();
1186        let dest = tmp.path().join("layout");
1187        let spec = DepositSpec {
1188            includes: Vec::new(),
1189            layer: "2026.07.0".parse().unwrap(),
1190            channel: "qualified".into(),
1191            counter: 1,
1192            issued_at: "2026-08-07T00:00:00Z".into(),
1193            tools: vec![DepositTool {
1194                name: "synth".into(),
1195                version: "1".into(),
1196                platform: None,
1197                bytes: b"t".to_vec(),
1198                source: None,
1199                runner: None,
1200                kind: None,
1201                sdk_prefix: None,
1202            }],
1203        };
1204        deposit(&spec, &sk, "k", &dest).unwrap();
1205
1206        // No baseline yet -> None, not an error.
1207        assert!(read_any_from_layout(&dest).unwrap().is_none());
1208
1209        let line: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
1210        let envelope = status(3).sign(&sk, "k").unwrap();
1211        attach_to_layout(&dest, &line, envelope.as_bytes()).unwrap();
1212
1213        let carried = read_any_from_layout(&dest).unwrap().unwrap();
1214        let parsed = LineStatus::verify_and_parse(&carried, &pk).unwrap();
1215        assert_eq!(parsed.counter, 3);
1216    }
1217}