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    /// The lowest counter a consumer with NO history should accept on this
60    /// line (REQ-FIRSTCONTACT-001).
61    ///
62    /// Carried here because this document is already DSSE-signed by the realm
63    /// root and already distributed beside the layer — a floor is only worth
64    /// anything if it cannot be chosen by whoever serves the bytes.
65    ///
66    /// Optional, so every existing line-status keeps parsing. Absent means the
67    /// first-contact window stays open for that line, which is the behaviour
68    /// varve has always had; stating one closes it.
69    #[serde(
70        rename = "min-counter",
71        skip_serializing_if = "Option::is_none",
72        default
73    )]
74    pub min_counter: Option<u64>,
75    /// Yanked layers of this line, layer → reason.
76    #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
77    pub yanked: BTreeMap<String, String>,
78    #[serde(
79        rename = "known-problems",
80        skip_serializing_if = "Vec::is_empty",
81        default
82    )]
83    pub known_problems: Vec<KnownProblem>,
84}
85
86#[derive(Debug, thiserror::Error)]
87pub enum LineStatusError {
88    /// The DSSE envelope failed verification or was malformed. Carries the
89    /// verifier's reason verbatim — but under a line-status heading, because
90    /// the old `#[error(transparent)]` route surfaced these as "manifest
91    /// signature verification failed", sending the reader to the wrong
92    /// document entirely (varve#60).
93    #[error("line-status envelope rejected: {0}")]
94    Envelope(String),
95    #[error("cannot sign the line-status document: {0}")]
96    Sign(String),
97    #[error("line-status payload is not valid: {0}")]
98    Payload(String),
99    #[error("line-status covers line {got} but line {expected} was requested")]
100    LineMismatch { expected: String, got: String },
101    #[error(
102        "refusing stale line-status document for {line}: presented counter {presented}, cached {cached}"
103    )]
104    Stale {
105        line: String,
106        presented: u64,
107        cached: u64,
108    },
109    /// An advisory entry that can never fire (varve#61): `varve status`
110    /// matches `affected` ids and yank keys against installed layer ids
111    /// EXACTLY, so a typo'd id signs fine and then warns nobody. Refused on
112    /// the producing side, where the fix (re-sign) is still cheap.
113    #[error(
114        "{what} names layer '{id}', which is not a layer of line {line} ({reason}) — `varve \
115         status` matches layer ids exactly, so this entry would never fire for any installed \
116         layer; fix the id and re-sign the document"
117    )]
118    DeadReference {
119        what: String,
120        id: String,
121        line: String,
122        reason: String,
123    },
124    /// REQ-ADVISORY-002. The id is a well-formed layer of this line and names
125    /// no layer that EXISTS — a typo one character deep. It signs cleanly, the
126    /// producer sees success, the consumer sees nothing, and the yank silently
127    /// does not exist. Refused wherever the signer can see the line's layers;
128    /// `--force` is for the legitimate case of pre-signing an advisory for a
129    /// layer not deposited yet.
130    #[error(
131        "{what} names layer '{id}', which line {line} does not contain — it exposes: {existing}. \
132         `varve status` matches layer ids EXACTLY, so this entry would fire for nobody: you \
133         would see success, every consumer would see nothing, and the advisory would silently \
134         not exist. Fix the id, or pass --force to pre-sign an advisory for a layer that is not \
135         deposited yet."
136    )]
137    UnknownLayer {
138        what: String,
139        id: String,
140        line: String,
141        existing: String,
142    },
143    #[error(
144        "{layout} is not an OCI image layout (it has no index.json) — point --layout at the \
145         directory `varve deposit --out` produced"
146    )]
147    NotALayout { layout: String },
148    // The io source is NOT repeated in the message: anyhow's `{err:#}` chain
149    // already appends every source, and including it here printed the cause
150    // twice ("… No such file or directory: No such file or directory").
151    #[error("io error at {path}")]
152    Io {
153        path: String,
154        #[source]
155        source: std::io::Error,
156    },
157}
158
159/// What the signer could see of a line's layers when it validated an advisory
160/// (REQ-ADVISORY-002).
161///
162/// The distinction is the whole point. An `affected` id is only checkable
163/// against a LISTING of the line — the realm's signed line-index. A deposit
164/// layout holds one layer, so it is not a listing, and treating it as one
165/// would refuse advisories about layers that exist perfectly well elsewhere.
166/// Where no listing is in reach the check cannot be run, and the caller must
167/// be told WHICH check was skipped rather than handed a success that implies
168/// a complete one.
169#[derive(Debug, Clone, PartialEq, Eq)]
170pub enum KnownLayers {
171    /// The ids the signer could enumerate, and where they came from.
172    Known {
173        /// For the note: which document these came out of.
174        source: String,
175        /// The line the listing covers, when it names one — a listing for
176        /// another line is not a listing for this one.
177        line: Option<String>,
178        layers: Vec<String>,
179    },
180    /// The signer could not see the line's layers, and why not.
181    Unknown { why: String },
182}
183
184impl KnownLayers {
185    /// The realm's own statement of which layers a line has — the only
186    /// authoritative listing varve has.
187    pub fn from_index(index: &crate::lineindex::LineIndex) -> Self {
188        KnownLayers::Known {
189            source: format!(
190                "the signed line-index for {} (counter {})",
191                index.line, index.counter
192            ),
193            line: Some(index.line.clone()),
194            layers: index.layers.iter().map(|e| e.layer.clone()).collect(),
195        }
196    }
197
198    pub fn unknown(why: impl Into<String>) -> Self {
199        KnownLayers::Unknown { why: why.into() }
200    }
201}
202
203/// Which validation actually ran, so a caller reports the check it performed
204/// and not the one it did not (REQ-ADVISORY-002).
205#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct RefCheck {
207    /// True only when every id was matched against a real listing of the
208    /// line's layers.
209    pub existence_checked: bool,
210    /// One line for the operator: the check that ran, or the one that did not.
211    pub note: String,
212}
213
214/// The layers a signed line-index asserts, verified against the root that is
215/// about to sign the advisory (REQ-ADVISORY-002 clause 2).
216///
217/// Verified, not merely parsed: an unverified listing is one an attacker could
218/// choose, and one that names the typo'd layer would wave the dead advisory
219/// through. At sign time the producer holds the key, so the check is free.
220pub fn known_layers_from_index(
221    envelope: &[u8],
222    root_public_key: &[u8],
223) -> Result<KnownLayers, crate::lineindex::IndexError> {
224    let doc = crate::lineindex::LineIndex::verify_and_parse(envelope, root_public_key)?;
225    Ok(KnownLayers::from_index(&doc))
226}
227
228/// The line's layers as the PRODUCER can see them, from their own layouts —
229/// no network, and no published index required (REQ-ADVISORY-002 clause 5,
230/// DD-023).
231///
232/// `signed-index` is false by default, so an index-only existence check would
233/// rarely have anything to check against: opt-in safety, which is how a typo'd
234/// `affected` id came to sign cleanly and fire for nobody. The producer already
235/// HOLDS the layers on disk. That listing is more trustworthy than a registry's
236/// — a compromised registry can hide a layer and thereby block the yank of the
237/// very layer it hides (the reason DD-023 keeps the network out of the signing
238/// command) — and it works for a realm that never publishes an index at all.
239///
240/// Each path is either a layout directory or a directory OF layout
241/// directories, because a producer's output tree is usually the latter.
242pub fn known_layers_in_layout_dirs(dirs: &[std::path::PathBuf], line: &str) -> KnownLayers {
243    let mut layers: Vec<String> = Vec::new();
244    let mut scanned = 0usize;
245    let visit = |dir: &Path, layers: &mut Vec<String>| {
246        // A real `varve deposit --out` writes an OCI layout: index.json plus
247        // blobs/sha256/. The bare `manifests/`+`blobs/` shape is the other
248        // source form varve accepts. Both are read, because a fixture that
249        // only spoke one of them is how this function first shipped passing a
250        // test against bytes the tool never produces.
251        let mut candidates: Vec<Vec<u8>> = Vec::new();
252        if let Ok(index) = std::fs::read(dir.join("index.json"))
253            && let Ok(idx) = serde_json::from_slice::<serde_json::Value>(&index)
254        {
255            for m in idx
256                .get("manifests")
257                .and_then(|m| m.as_array())
258                .into_iter()
259                .flatten()
260            {
261                if let Some(d) = m.get("digest").and_then(|d| d.as_str())
262                    && let Some((_, hex)) = d.split_once(':')
263                    && let Ok(b) = std::fs::read(dir.join("blobs").join("sha256").join(hex))
264                {
265                    candidates.push(b);
266                }
267            }
268        }
269        if let Ok(entries) = std::fs::read_dir(dir.join("manifests")) {
270            for e in entries.filter_map(|e| e.ok()) {
271                if let Ok(b) = std::fs::read(e.path()) {
272                    candidates.push(b);
273                }
274            }
275        }
276        if candidates.is_empty() {
277            return false;
278        }
279        for bytes in candidates {
280            // A layout stores the SIGNED envelope; the layer id lives in its
281            // payload. Unverified is correct here: this is the producer's own
282            // output tree, and the id is being used to catch a typo, not to
283            // decide trust.
284            let payload = std::str::from_utf8(&bytes)
285                .ok()
286                .and_then(|t| wsc::dsse::DsseEnvelope::from_json(t).ok())
287                .and_then(|env| env.payload_bytes().ok())
288                .unwrap_or_else(|| bytes.clone());
289            if let Ok(m) = crate::manifest::LayerManifest::parse(&payload) {
290                let id = m.layer.to_string();
291                if id.starts_with(&format!("{line}.")) && !layers.contains(&id) {
292                    layers.push(id);
293                }
294            }
295        }
296        true
297    };
298    for dir in dirs {
299        if visit(dir, &mut layers) {
300            scanned += 1;
301            continue;
302        }
303        // Not a layout itself — try its children.
304        if let Ok(children) = std::fs::read_dir(dir) {
305            for c in children.filter_map(|e| e.ok()) {
306                if c.path().is_dir() && visit(&c.path(), &mut layers) {
307                    scanned += 1;
308                }
309            }
310        }
311    }
312    if scanned == 0 {
313        return KnownLayers::unknown(format!(
314            "no oci-layout was found under {} — pass the directory `varve deposit --out` \
315             wrote, or one holding several of them",
316            dirs.iter()
317                .map(|d| d.display().to_string())
318                .collect::<Vec<_>>()
319                .join(", ")
320        ));
321    }
322    layers.sort();
323    KnownLayers::Known {
324        source: format!("{scanned} local layout(s) the producer holds"),
325        line: Some(line.to_string()),
326        layers,
327    }
328}
329
330/// What a deposit layout can tell a producer about the line's layers.
331///
332/// A layout carries the realm's signed index only once `attach-index` has run,
333/// and the documented CI order attaches the status FIRST — so the ordinary
334/// answer here is `Unknown`, stated plainly rather than passed off as a clean
335/// bill of health.
336pub fn known_layers_in_layout(layout: &Path, line: &str) -> KnownLayers {
337    match crate::lineindex::read_from_layout(layout, line) {
338        Ok(Some(envelope)) => match crate::lineindex::parse_unverified(&envelope) {
339            Ok(doc) if doc.line == line => KnownLayers::from_index(&doc),
340            Ok(doc) => KnownLayers::unknown(format!(
341                "the line-index this layout carries is for line {}, not {line}",
342                doc.line
343            )),
344            Err(e) => KnownLayers::unknown(format!(
345                "the line-index this layout carries could not be read ({e})"
346            )),
347        },
348        Ok(None) => KnownLayers::unknown(format!(
349            "this layout carries no signed line-index for {line}, and a layout holds ONE layer \
350             — it is not a listing of the line. Attach the index first (`varve attach-index`), \
351             or sign against it (`varve sign-status --index <envelope>`)"
352        )),
353        Err(e) => KnownLayers::unknown(format!("the layout's index.json could not be read ({e})")),
354    }
355}
356
357impl LineStatus {
358    /// Verify an envelope against the trust root and parse the payload.
359    pub fn verify_and_parse(
360        envelope: &[u8],
361        root_public_key: &[u8],
362    ) -> Result<Self, LineStatusError> {
363        // Diagnose the not-an-envelope case BEFORE the verifier does: its
364        // wrapped parser error prints the cause twice and never names the
365        // commonest mistake — handing over the raw status JSON instead of the
366        // signed envelope (varve#60).
367        if let Ok(text) = std::str::from_utf8(envelope)
368            && wsc::dsse::DsseEnvelope::from_json(text).is_err()
369        {
370            return Err(not_an_envelope(text));
371        }
372        let payload = dsse_verify_typed(envelope, LINE_STATUS_PAYLOAD_TYPE, root_public_key)
373            .map_err(|VerifyError(msg)| {
374                // A wrong-realm signature is indistinguishable from tampering
375                // at this layer; say so, because "No valid signatures" alone
376                // sends the reader hunting for corruption.
377                let hint = if msg.contains("does not verify") {
378                    " (is the document signed by THIS realm's root? `varve pubkey <key>` \
379                     prints the public half a signature verifies against)"
380                } else {
381                    ""
382                };
383                LineStatusError::Envelope(format!("{msg}{hint}"))
384            })?;
385        serde_json::from_slice(&payload).map_err(|e| LineStatusError::Payload(e.to_string()))
386    }
387
388    /// Sign a status document (the producing side — CI, next to deposit).
389    /// Refuses a document whose yank or `affected` entries could never fire
390    /// (varve#61) — a typo'd layer id is cheapest to fix before the signature
391    /// exists.
392    pub fn sign(&self, secret_key: &[u8], key_id: &str) -> Result<String, LineStatusError> {
393        self.check_layer_refs()?;
394        let payload = serde_json::to_vec_pretty(self).expect("status serializes");
395        dsse_sign_typed(&payload, LINE_STATUS_PAYLOAD_TYPE, secret_key, key_id)
396            .map_err(|VerifyError(msg)| LineStatusError::Sign(msg))
397    }
398
399    /// Sign, having checked every advisory reference against what the signer
400    /// can actually see of the line (REQ-ADVISORY-002). Returns the envelope
401    /// and a statement of which check ran — a caller that prints only
402    /// "signed" implies a completeness it may not have.
403    pub fn sign_against(
404        &self,
405        known: &KnownLayers,
406        force: bool,
407        secret_key: &[u8],
408        key_id: &str,
409    ) -> Result<(String, RefCheck), LineStatusError> {
410        let check = self.check_layer_refs_against(known, force)?;
411        let payload = serde_json::to_vec_pretty(self).expect("status serializes");
412        let envelope = dsse_sign_typed(&payload, LINE_STATUS_PAYLOAD_TYPE, secret_key, key_id)
413            .map_err(|VerifyError(msg)| LineStatusError::Sign(msg))?;
414        Ok((envelope, check))
415    }
416
417    /// Every yank key and every `affected` id, checked as far as this signer
418    /// can see (REQ-ADVISORY-002).
419    ///
420    /// Two checks, deliberately separated:
421    ///
422    ///  * SHAPE and line membership — always run, never overridable. An id
423    ///    that is not a well-formed layer identifier of this line cannot
424    ///    become correct later, so `--force` has nothing to allow.
425    ///  * EXISTENCE — run only where a listing of the line is in reach.
426    ///    `--force` allows it through, because pre-signing an advisory for a
427    ///    layer about to be deposited is a legitimate thing to do. Silence is
428    ///    not: where the check does not run, the returned `RefCheck` says so.
429    pub fn check_layer_refs_against(
430        &self,
431        known: &KnownLayers,
432        force: bool,
433    ) -> Result<RefCheck, LineStatusError> {
434        self.check_layer_refs()?;
435        // How many layer ids this document actually asserts anything about.
436        // A BASELINE status — no yanks, no known problems — refers to nothing,
437        // so there is nothing an existence check could have caught. Warning
438        // there is cry-wolf: it fires on the correct setup `docs own-realm`
439        // tells every new operator to perform, and REQ-SHADOW-001's own lesson
440        // is that a check which fires on correct setups is one people switch
441        // off. Found by clean-room review, which noted it also printed TWICE
442        // (sign-status and attach-status) on the documented happy path.
443        let referenced = self.yanked.len()
444            + self
445                .known_problems
446                .iter()
447                .map(|p| p.affected.len())
448                .sum::<usize>();
449        let (source, layers) = match known {
450            KnownLayers::Unknown { .. } if referenced == 0 => {
451                return Ok(RefCheck {
452                    existence_checked: true,
453                    note: "this document names no layer — nothing to check against the line"
454                        .to_string(),
455                });
456            }
457            KnownLayers::Unknown { why } => {
458                return Ok(RefCheck {
459                    existence_checked: false,
460                    note: format!(
461                        "advisory references were checked for SHAPE only — NOT against the \
462                         layers line {} actually has: {why}. An id naming a layer that does not \
463                         exist still signs cleanly here and fires for nobody.",
464                        self.line
465                    ),
466                });
467            }
468            KnownLayers::Known {
469                source,
470                line,
471                layers,
472            } => {
473                // A listing for another line answers a different question. It
474                // would either wave everything through or refuse everything,
475                // and both verdicts would be reported as if they meant
476                // something.
477                if let Some(listing_line) = line
478                    && listing_line != &self.line
479                {
480                    return Err(LineStatusError::LineMismatch {
481                        expected: self.line.clone(),
482                        got: listing_line.clone(),
483                    });
484                }
485                (source, layers)
486            }
487        };
488        if force {
489            return Ok(RefCheck {
490                existence_checked: false,
491                note: format!(
492                    "--force: advisory references were NOT checked against the layers line {} \
493                     has. An entry naming a layer that has not been deposited yet fires only \
494                     once it is.",
495                    self.line
496                ),
497            });
498        }
499        let existing = if layers.is_empty() {
500            "no layers at all — this line has none yet".to_string()
501        } else {
502            layers.join(", ")
503        };
504        let mut refs = 0usize;
505        let mut check = |what: String, id: &str| -> Result<(), LineStatusError> {
506            refs += 1;
507            if layers.iter().any(|l| l == id) {
508                return Ok(());
509            }
510            Err(LineStatusError::UnknownLayer {
511                what,
512                id: id.to_string(),
513                line: self.line.clone(),
514                existing: existing.clone(),
515            })
516        };
517        for id in self.yanked.keys() {
518            check("the yank entry".to_string(), id)?;
519        }
520        for kp in &self.known_problems {
521            for id in &kp.affected {
522                check(format!("known problem '{}'", kp.id), id)?;
523            }
524        }
525        Ok(RefCheck {
526            existence_checked: true,
527            note: format!(
528                "{refs} advisory reference{} checked against the {} layer{} {source} lists for \
529                 line {}",
530                if refs == 1 { "" } else { "s" },
531                layers.len(),
532                if layers.len() == 1 { "" } else { "s" },
533                self.line
534            ),
535        })
536    }
537
538    /// Every yank key and every known problem's `affected` id must be a layer
539    /// of THIS document's line (varve#61). `report_for` matches ids exactly,
540    /// and the cache is keyed per line, so an id outside the line — or one
541    /// that is not a layer id at all — is an advisory that signs fine and
542    /// then fires for nobody. Enforced wherever a producer commits the
543    /// document: `sign` and `attach_envelope_to_layout`.
544    pub fn check_layer_refs(&self) -> Result<(), LineStatusError> {
545        let line: Line = self.line.parse().map_err(|e| {
546            LineStatusError::Payload(format!("'{}' is not a YYYY.MM line: {e}", self.line))
547        })?;
548        let check = |what: String, id: &str| -> Result<(), LineStatusError> {
549            let dead = |reason: String| LineStatusError::DeadReference {
550                what: what.clone(),
551                id: id.to_string(),
552                line: self.line.clone(),
553                reason,
554            };
555            match id.parse::<LayerId>() {
556                Ok(layer) if layer.line() == &line => Ok(()),
557                Ok(layer) => Err(dead(format!("it belongs to line {}", layer.line()))),
558                Err(e) => Err(dead(e.to_string())),
559            }
560        };
561        for id in self.yanked.keys() {
562            check("the yank entry".to_string(), id)?;
563        }
564        for kp in &self.known_problems {
565            for id in &kp.affected {
566                check(format!("known problem '{}'", kp.id), id)?;
567            }
568        }
569        Ok(())
570    }
571
572    /// What this document says about one layer.
573    pub fn report_for(&self, layer: &LayerId) -> LayerStatusReport {
574        let name = layer.to_string();
575        let problems: Vec<&KnownProblem> = self
576            .known_problems
577            .iter()
578            .filter(|kp| kp.affected.iter().any(|a| a == &name))
579            .collect();
580        LayerStatusReport {
581            yanked_reason: self.yanked.get(&name).cloned(),
582            support_until: self.support_until.clone(),
583            problems_total: problems.len(),
584            problems_with_workaround: problems.iter().filter(|kp| kp.workaround.is_some()).count(),
585        }
586    }
587}
588
589#[derive(Debug, Clone, PartialEq, Eq)]
590pub struct LayerStatusReport {
591    pub yanked_reason: Option<String>,
592    pub support_until: Option<String>,
593    pub problems_total: usize,
594    pub problems_with_workaround: usize,
595}
596
597/// Per-line cache of the newest verified document, under the varve root.
598#[derive(Debug)]
599pub struct StatusCache {
600    dir: PathBuf,
601}
602
603impl StatusCache {
604    pub fn at_root(root: &Path) -> Self {
605        StatusCache {
606            dir: root.join("state").join("line-status"),
607        }
608    }
609
610    /// Store a VERIFIED envelope for its line, refusing counter regressions.
611    pub fn update(
612        &self,
613        line: &Line,
614        envelope: &[u8],
615        parsed: &LineStatus,
616    ) -> Result<(), LineStatusError> {
617        if let Some(cached) = self.load_parsed(line)?
618            && parsed.counter < cached.counter
619        {
620            return Err(LineStatusError::Stale {
621                line: line.to_string(),
622                presented: parsed.counter,
623                cached: cached.counter,
624            });
625        }
626        let io = |path: &Path, source: std::io::Error| LineStatusError::Io {
627            path: path.display().to_string(),
628            source,
629        };
630        std::fs::create_dir_all(&self.dir).map_err(|e| io(&self.dir, e))?;
631        let path = self.envelope_path(line);
632        std::fs::write(&path, envelope).map_err(|e| io(&path, e))?;
633        Ok(())
634    }
635
636    /// The cached envelope bytes for a line, unparsed, if any.
637    ///
638    /// Used by `archive` to carry the baseline across an air gap (varve#77).
639    /// Deliberately opaque: the caller re-attaches the bytes verbatim, and the
640    /// far side re-verifies against its own trust root — archiving must not
641    /// become a place where a document is re-signed or re-shaped.
642    pub fn envelope_bytes(&self, line: &Line) -> Result<Option<Vec<u8>>, LineStatusError> {
643        let path = self.envelope_path(line);
644        match std::fs::read(&path) {
645            Ok(bytes) => Ok(Some(bytes)),
646            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
647            Err(source) => Err(LineStatusError::Io {
648                path: path.display().to_string(),
649                source,
650            }),
651        }
652    }
653
654    /// Load and re-verify the cached envelope for a line.
655    pub fn load(
656        &self,
657        line: &Line,
658        root_public_key: &[u8],
659    ) -> Result<Option<LineStatus>, LineStatusError> {
660        let path = self.envelope_path(line);
661        match std::fs::read(&path) {
662            Ok(bytes) => Ok(Some(LineStatus::verify_and_parse(&bytes, root_public_key)?)),
663            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
664            Err(source) => Err(LineStatusError::Io {
665                path: path.display().to_string(),
666                source,
667            }),
668        }
669    }
670
671    fn load_parsed(&self, line: &Line) -> Result<Option<LineStatus>, LineStatusError> {
672        let path = self.envelope_path(line);
673        match std::fs::read(&path) {
674            Ok(bytes) => {
675                // Cached envelopes were verified at update time; parse the
676                // payload without re-verifying just to read the counter.
677                let text = std::str::from_utf8(&bytes)
678                    .map_err(|_| LineStatusError::Payload("cache is not UTF-8".into()))?;
679                let env = wsc::dsse::DsseEnvelope::from_json(text)
680                    .map_err(|e| LineStatusError::Payload(e.to_string()))?;
681                let payload = env
682                    .payload_bytes()
683                    .map_err(|e| LineStatusError::Payload(e.to_string()))?;
684                Ok(Some(
685                    serde_json::from_slice(&payload)
686                        .map_err(|e| LineStatusError::Payload(e.to_string()))?,
687                ))
688            }
689            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
690            Err(source) => Err(LineStatusError::Io {
691                path: path.display().to_string(),
692                source,
693            }),
694        }
695    }
696
697    fn envelope_path(&self, line: &Line) -> PathBuf {
698        self.dir.join(format!("{line}.dsse.json"))
699    }
700}
701
702/// artifactType for a line-status envelope carried in an OCI image layout.
703pub const LINE_STATUS_ARTIFACT_TYPE: &str = LINE_STATUS_PAYLOAD_TYPE;
704/// Annotation naming the line a carried status document covers.
705pub const ANN_LINE: &str = "eu.pulseengine.varve.status-line";
706
707/// Attach a (verified-by-the-caller) status envelope to an existing OCI
708/// image layout — evidence added AFTER deposit, without touching any layer
709/// blob or digest. Replaces a previous document for the same line.
710pub fn attach_to_layout(
711    layout: &Path,
712    line: &Line,
713    envelope: &[u8],
714) -> Result<(), LineStatusError> {
715    let io = |path: &Path, source: std::io::Error| LineStatusError::Io {
716        path: path.display().to_string(),
717        source,
718    };
719    let digest = crate::store::manifest_digest(envelope);
720    let hex = digest.strip_prefix("sha256:").expect("digest shape");
721    let blob_dir = layout.join("blobs").join("sha256");
722    std::fs::create_dir_all(&blob_dir).map_err(|e| io(&blob_dir, e))?;
723    let blob_path = blob_dir.join(hex);
724    std::fs::write(&blob_path, envelope).map_err(|e| io(&blob_path, e))?;
725
726    let index_path = layout.join("index.json");
727    let mut index: serde_json::Value =
728        serde_json::from_slice(&std::fs::read(&index_path).map_err(|e| io(&index_path, e))?)
729            .map_err(|e| LineStatusError::Payload(format!("index.json: {e}")))?;
730    let entries = index["manifests"]
731        .as_array_mut()
732        .ok_or_else(|| LineStatusError::Payload("index.json has no manifests array".into()))?;
733    let line_name = line.to_string();
734    entries.retain(|e| {
735        !(e["artifactType"] == LINE_STATUS_ARTIFACT_TYPE
736            && e["annotations"][ANN_LINE] == *line_name)
737    });
738    entries.push(serde_json::json!({
739        "mediaType": "application/json",
740        "artifactType": LINE_STATUS_ARTIFACT_TYPE,
741        "digest": digest,
742        "size": envelope.len(),
743        "annotations": { ANN_LINE: line_name }
744    }));
745    std::fs::write(
746        &index_path,
747        serde_json::to_vec_pretty(&index).expect("index serializes"),
748    )
749    .map_err(|e| io(&index_path, e))?;
750    Ok(())
751}
752
753/// Fetch the baseline line-status a source carries beside a layer, verify it
754/// against the trust root, and cache it monotonically (REQ-STATUS-DIST-001).
755/// Returns `Ok(Some(counter))` when a baseline was cached, `Ok(None)` when the
756/// source carries none. Verification, cache, or transport failures are `Err`
757/// — the caller decides severity (the CLI downgrades them to a note, since a
758/// bad baseline never blocks an otherwise-verified install, but it is never
759/// silently cached). The untrusted bytes are re-verified here; the source is
760/// not trusted to have checked them.
761pub fn cache_baseline_from_source(
762    source: &dyn crate::source::LayerSource,
763    layer: &crate::source::LayerRef,
764    line: &Line,
765    root_pk: &[u8],
766    store_root: &Path,
767) -> Result<Option<u64>, LineStatusError> {
768    let envelope = match source
769        .fetch_line_status(layer)
770        .map_err(|e| LineStatusError::Payload(format!("fetching baseline line-status: {e}")))?
771    {
772        Some(bytes) => bytes,
773        None => return Ok(None),
774    };
775    let doc = LineStatus::verify_and_parse(&envelope, root_pk)?;
776    // A validly-signed status for a DIFFERENT line must not be cached under
777    // this one — mirror the `--from-file` guard so all cache paths agree.
778    if doc.line != line.to_string() {
779        return Err(LineStatusError::LineMismatch {
780            expected: line.to_string(),
781            got: doc.line,
782        });
783    }
784    let counter = doc.counter;
785    StatusCache::at_root(store_root).update(line, &envelope, &doc)?;
786    Ok(Some(counter))
787}
788
789/// Attach a signed line-status envelope to a deposit layout, deriving the
790/// line from the document itself (REQ-STATUS-DIST-001). Returns the line and
791/// counter attached. The payload is read to learn the line but not verified
792/// here — install re-verifies the bytes against the trust root, and the
793/// deposit pipeline produced this envelope moments earlier with its own key.
794pub fn attach_envelope_to_layout(
795    layout: &Path,
796    envelope: &[u8],
797) -> Result<(Line, u64), LineStatusError> {
798    let (line, counter, _) = attach_envelope_to_layout_checked(layout, envelope, false)?;
799    Ok((line, counter))
800}
801
802/// `attach_envelope_to_layout`, reporting which advisory check it was able to
803/// run and allowing the deliberate override (REQ-ADVISORY-002).
804pub fn attach_envelope_to_layout_checked(
805    layout: &Path,
806    envelope: &[u8],
807    force: bool,
808) -> Result<(Line, u64, RefCheck), LineStatusError> {
809    // Refuse a directory that is not a layout BEFORE writing anything into
810    // it: the old path created blobs/ inside an arbitrary directory and then
811    // failed on the missing index.json with a bare io error (varve#60).
812    if !layout.join("index.json").is_file() {
813        return Err(LineStatusError::NotALayout {
814            layout: layout.display().to_string(),
815        });
816    }
817    let text = std::str::from_utf8(envelope)
818        .map_err(|e| LineStatusError::Payload(format!("envelope is not utf-8: {e}")))?;
819    let env = wsc::dsse::DsseEnvelope::from_json(text).map_err(|_| not_an_envelope(text))?;
820    let payload = env
821        .payload_bytes()
822        .map_err(|e| LineStatusError::Payload(format!("envelope payload: {e}")))?;
823    let doc: LineStatus = serde_json::from_slice(&payload)
824        .map_err(|e| LineStatusError::Payload(format!("status document: {e}")))?;
825    let line: Line = doc
826        .line
827        .parse()
828        .map_err(|e| LineStatusError::Payload(format!("status line '{}': {e}", doc.line)))?;
829    // A yank or affected id outside the layout's line would attach fine and
830    // fire for nobody (varve#61) — this command knows the line, so it is the
831    // last producer-side place the typo is cheap to fix. And where the layout
832    // carries the realm's signed index, the ids can be checked against the
833    // layers that actually EXIST, not merely against the shape of a layer id
834    // (REQ-ADVISORY-002).
835    let check = doc.check_layer_refs_against(&known_layers_in_layout(layout, &doc.line), force)?;
836    // Monotonicity holds here too. `status --from-file` and `install` both
837    // refuse a counter regression; attaching did not, so a re-run CI step could
838    // silently downgrade a layout's baseline — shipping a pre-yank document
839    // that fresh consumers cache and are told "not yanked" about a YANKED
840    // layer. The one place the rule was missing was the one that produces the
841    // artifact.
842    if let Some(existing) = read_any_from_layout(layout)?
843        && let Ok(prev) = parse_unverified(&existing)
844        && prev.line == doc.line
845        && doc.counter < prev.counter
846    {
847        return Err(LineStatusError::Stale {
848            line: doc.line.clone(),
849            presented: doc.counter,
850            cached: prev.counter,
851        });
852    }
853    // The status must belong to THIS layout's line. Attaching a 2099.01 status
854    // to a 2026.08 layout used to succeed, leaving the consumer to discover it
855    // (REQ-PRODUCER-001).
856    if let Some(layout_line) = layout_line(layout)
857        && layout_line != line.to_string()
858    {
859        return Err(LineStatusError::LineMismatch {
860            expected: layout_line,
861            got: line.to_string(),
862        });
863    }
864    attach_to_layout(layout, &line, envelope)?;
865    Ok((line, doc.counter, check))
866}
867
868/// The bytes are not a DSSE envelope — with the commonest cause named: the
869/// raw status document handed over where the SIGNED envelope belongs. The
870/// verifier's own wrapping ("not a DSSE envelope: Internal error: [Failed to
871/// parse DSSE envelope: …]") said the same thing twice and the fix zero
872/// times (varve#60).
873fn not_an_envelope(text: &str) -> LineStatusError {
874    if serde_json::from_str::<LineStatus>(text).is_ok() {
875        LineStatusError::Payload(
876            "this is the UNSIGNED status document, not a signed envelope — sign it first \
877             (`varve sign-status --file <doc> --key <key> --out <envelope>`) and pass the \
878             envelope"
879                .into(),
880        )
881    } else {
882        LineStatusError::Payload(
883            "not a DSSE envelope — expected the signed output of `varve sign-status`".into(),
884        )
885    }
886}
887
888/// Parse a status document out of an envelope WITHOUT verifying it. Used only
889/// to read back what a layout already carries, so a regression can be refused;
890/// the signature is checked wherever the document is actually trusted.
891fn parse_unverified(envelope: &[u8]) -> Result<LineStatus, LineStatusError> {
892    let text = std::str::from_utf8(envelope)
893        .map_err(|e| LineStatusError::Payload(format!("envelope is not utf-8: {e}")))?;
894    let env = wsc::dsse::DsseEnvelope::from_json(text).map_err(|_| not_an_envelope(text))?;
895    let payload = env
896        .payload_bytes()
897        .map_err(|e| LineStatusError::Payload(format!("envelope payload: {e}")))?;
898    serde_json::from_slice(&payload)
899        .map_err(|e| LineStatusError::Payload(format!("status document: {e}")))
900}
901
902/// The line a deposit layout's own manifest declares, if it can be read. Best
903/// effort: a layout we cannot introspect is not blocked from being annotated,
904/// but one that plainly disagrees is.
905pub(crate) fn layout_line(layout: &Path) -> Option<String> {
906    let index: serde_json::Value =
907        serde_json::from_slice(&std::fs::read(layout.join("index.json")).ok()?).ok()?;
908    for m in index["manifests"].as_array()? {
909        let digest = m["digest"].as_str()?.replace(':', "-");
910        let blob = layout
911            .join("blobs")
912            .join("sha256")
913            .join(digest.trim_start_matches("sha256-"));
914        let Ok(bytes) = std::fs::read(&blob) else {
915            continue;
916        };
917        // The layer envelope's payload carries the line annotation. Reuse the
918        // DSSE reader already used in this module rather than hand-rolling.
919        let Ok(text) = std::str::from_utf8(&bytes) else {
920            continue;
921        };
922        let Ok(env) = wsc::dsse::DsseEnvelope::from_json(text) else {
923            continue;
924        };
925        let Ok(payload) = env.payload_bytes() else {
926            continue;
927        };
928        let Ok(doc) = serde_json::from_slice::<serde_json::Value>(&payload) else {
929            continue;
930        };
931        if let Some(line) = doc["annotations"]["eu.pulseengine.varve.line"].as_str() {
932            return Some(line.to_string());
933        }
934    }
935    None
936}
937
938/// Read the single baseline status envelope a deposit layout carries,
939/// without needing to name its line (REQ-STATUS-DIST-001). A deposit layout
940/// holds exactly one line-status; a consumer installing by digest may not
941/// know the line up front. Returns the first line-status referrer found.
942pub fn read_any_from_layout(layout: &Path) -> Result<Option<Vec<u8>>, LineStatusError> {
943    let index_path = layout.join("index.json");
944    let bytes = match std::fs::read(&index_path) {
945        Ok(bytes) => bytes,
946        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
947        Err(source) => {
948            return Err(LineStatusError::Io {
949                path: index_path.display().to_string(),
950                source,
951            });
952        }
953    };
954    let index: serde_json::Value = serde_json::from_slice(&bytes)
955        .map_err(|e| LineStatusError::Payload(format!("index.json: {e}")))?;
956    let Some(entry) = index["manifests"].as_array().and_then(|entries| {
957        entries
958            .iter()
959            .find(|e| e["artifactType"] == LINE_STATUS_ARTIFACT_TYPE)
960    }) else {
961        return Ok(None);
962    };
963    let digest = entry["digest"]
964        .as_str()
965        .ok_or_else(|| LineStatusError::Payload("status entry has no digest".into()))?;
966    let hex = digest.strip_prefix("sha256:").unwrap_or(digest);
967    let blob_path = layout.join("blobs").join("sha256").join(hex);
968    std::fs::read(&blob_path)
969        .map(Some)
970        .map_err(|source| LineStatusError::Io {
971            path: blob_path.display().to_string(),
972            source,
973        })
974}
975
976/// Read the status envelope for a line from an OCI image layout, if carried.
977pub fn read_from_layout(layout: &Path, line: &Line) -> Result<Option<Vec<u8>>, LineStatusError> {
978    let index_path = layout.join("index.json");
979    let bytes = match std::fs::read(&index_path) {
980        Ok(bytes) => bytes,
981        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
982        Err(source) => {
983            return Err(LineStatusError::Io {
984                path: index_path.display().to_string(),
985                source,
986            });
987        }
988    };
989    let index: serde_json::Value = serde_json::from_slice(&bytes)
990        .map_err(|e| LineStatusError::Payload(format!("index.json: {e}")))?;
991    let line_name = line.to_string();
992    let Some(entry) = index["manifests"].as_array().and_then(|entries| {
993        entries.iter().find(|e| {
994            e["artifactType"] == LINE_STATUS_ARTIFACT_TYPE
995                && e["annotations"][ANN_LINE] == *line_name
996        })
997    }) else {
998        return Ok(None);
999    };
1000    let digest = entry["digest"]
1001        .as_str()
1002        .ok_or_else(|| LineStatusError::Payload("status entry has no digest".into()))?;
1003    let hex = digest.strip_prefix("sha256:").unwrap_or(digest);
1004    let blob_path = layout.join("blobs").join("sha256").join(hex);
1005    std::fs::read(&blob_path)
1006        .map(Some)
1007        .map_err(|source| LineStatusError::Io {
1008            path: blob_path.display().to_string(),
1009            source,
1010        })
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015    use super::*;
1016    use crate::verify::generate_root_keypair;
1017
1018    fn status(counter: u64) -> LineStatus {
1019        LineStatus {
1020            min_counter: None,
1021            line: "2026.07".into(),
1022            counter,
1023            issued_at: "2026-08-07T00:00:00Z".into(),
1024            support_until: Some("2028-07-31".into()),
1025            yanked: BTreeMap::from([(
1026                "2026.07.0".to_string(),
1027                "CVE-2026-0001 in synth".to_string(),
1028            )]),
1029            known_problems: vec![
1030                KnownProblem {
1031                    id: "KP-1".into(),
1032                    title: "synth mla fusion regresses flat_flight".into(),
1033                    severity: "medium".into(),
1034                    affected: vec!["2026.07.0".into()],
1035                    workaround: Some("disable mla fusion".into()),
1036                    detection: None,
1037                    mitigation: None,
1038                },
1039                KnownProblem {
1040                    id: "KP-2".into(),
1041                    title: "witness truth-table gap on nested variants".into(),
1042                    severity: "high".into(),
1043                    affected: vec!["2026.07.0".into(), "2026.07.1".into()],
1044                    workaround: None,
1045                    detection: Some("witness gap rows non-empty".into()),
1046                    mitigation: None,
1047                },
1048            ],
1049        }
1050    }
1051
1052    // rivet: verifies REQ-KP-001
1053    #[test]
1054    fn a_signed_status_document_round_trips() {
1055        let (sk, pk) = generate_root_keypair();
1056        let envelope = status(1).sign(&sk, "varve-root-1").unwrap();
1057        let parsed = LineStatus::verify_and_parse(envelope.as_bytes(), &pk).unwrap();
1058        assert_eq!(parsed, status(1));
1059    }
1060
1061    // rivet: verifies REQ-KP-001
1062    #[test]
1063    fn a_layer_manifest_envelope_cannot_pose_as_a_status_document() {
1064        let (sk, pk) = generate_root_keypair();
1065        // Signed with the right key but the wrong payload type.
1066        let manifest = crate::manifest::fixtures::manifest(
1067            "2026.07.0",
1068            "qualified",
1069            1,
1070            "2026-08-07T00:00:00Z",
1071        );
1072        let envelope = crate::verify::sign_layer_manifest(&manifest, &sk, "varve-root-1").unwrap();
1073        let err = LineStatus::verify_and_parse(envelope.as_bytes(), &pk).unwrap_err();
1074        assert!(err.to_string().contains("payload type"), "got: {err}");
1075    }
1076
1077    // rivet: verifies REQ-KP-001
1078    #[test]
1079    fn the_report_names_yank_support_window_and_problem_counts() {
1080        let doc = status(1);
1081        let report = doc.report_for(&"2026.07.0".parse().unwrap());
1082        assert_eq!(
1083            report.yanked_reason.as_deref(),
1084            Some("CVE-2026-0001 in synth")
1085        );
1086        assert_eq!(report.support_until.as_deref(), Some("2028-07-31"));
1087        assert_eq!(report.problems_total, 2);
1088        assert_eq!(report.problems_with_workaround, 1);
1089        let clean = doc.report_for(&"2026.07.2".parse().unwrap());
1090        assert_eq!(clean.yanked_reason, None);
1091        assert_eq!(clean.problems_total, 0);
1092    }
1093
1094    // rivet: verifies REQ-KP-001
1095    #[test]
1096    fn attaching_status_to_a_layout_leaves_every_layer_blob_untouched() {
1097        use crate::deposit::{DepositSpec, DepositTool, deposit};
1098        let (sk, pk) = generate_root_keypair();
1099        let tmp = tempfile::tempdir().unwrap();
1100        let dest = tmp.path().join("layout");
1101        let spec = DepositSpec {
1102            includes: Vec::new(),
1103            layer: "2026.07.0".parse().unwrap(),
1104            channel: "qualified".into(),
1105            counter: 1,
1106            issued_at: "2026-08-07T00:00:00Z".into(),
1107            tools: vec![DepositTool {
1108                name: "synth".into(),
1109                version: "1".into(),
1110                platform: None,
1111                bytes: b"t".to_vec(),
1112                source: None,
1113                runner: None,
1114                kind: None,
1115                sdk_prefix: None,
1116            }],
1117        };
1118        let outcome = deposit(&spec, &sk, "k", &dest).unwrap();
1119
1120        // Snapshot the layer-relevant blobs before attaching evidence.
1121        let blob_dir = dest.join("blobs/sha256");
1122        let before: std::collections::BTreeMap<String, Vec<u8>> = std::fs::read_dir(&blob_dir)
1123            .unwrap()
1124            .map(|e| {
1125                let p = e.unwrap().path();
1126                (
1127                    p.file_name().unwrap().to_string_lossy().into_owned(),
1128                    std::fs::read(&p).unwrap(),
1129                )
1130            })
1131            .collect();
1132
1133        let line: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
1134        let envelope = status(1).sign(&sk, "k").unwrap();
1135        attach_to_layout(&dest, &line, envelope.as_bytes()).unwrap();
1136
1137        // Every pre-existing blob is byte-identical; the manifest digest is
1138        // unchanged — evidence was added, identity was not.
1139        for (name, bytes) in &before {
1140            assert_eq!(&std::fs::read(blob_dir.join(name)).unwrap(), bytes);
1141        }
1142        let carried = read_from_layout(&dest, &line).unwrap().unwrap();
1143        let parsed = LineStatus::verify_and_parse(&carried, &pk).unwrap();
1144        assert_eq!(parsed.counter, 1);
1145        let hex = outcome.digest.strip_prefix("sha256:").unwrap();
1146        assert!(
1147            blob_dir.join(hex).is_file(),
1148            "layer manifest blob still present"
1149        );
1150
1151        // Replacing the document for the same line keeps exactly one entry.
1152        let envelope2 = status(2).sign(&sk, "k").unwrap();
1153        attach_to_layout(&dest, &line, envelope2.as_bytes()).unwrap();
1154        let index: serde_json::Value =
1155            serde_json::from_slice(&std::fs::read(dest.join("index.json")).unwrap()).unwrap();
1156        let count = index["manifests"]
1157            .as_array()
1158            .unwrap()
1159            .iter()
1160            .filter(|e| e["artifactType"] == LINE_STATUS_ARTIFACT_TYPE)
1161            .count();
1162        assert_eq!(count, 1);
1163    }
1164
1165    // rivet: verifies REQ-KP-001
1166    #[test]
1167    fn the_cache_refuses_a_counter_regression() {
1168        let (sk, pk) = generate_root_keypair();
1169        let tmp = tempfile::tempdir().unwrap();
1170        let cache = StatusCache::at_root(tmp.path());
1171        let line: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
1172
1173        let newer = status(2);
1174        let env2 = newer.sign(&sk, "k").unwrap();
1175        cache.update(&line, env2.as_bytes(), &newer).unwrap();
1176
1177        let older = status(1);
1178        let env1 = older.sign(&sk, "k").unwrap();
1179        let err = cache.update(&line, env1.as_bytes(), &older).unwrap_err();
1180        assert!(matches!(
1181            err,
1182            LineStatusError::Stale {
1183                presented: 1,
1184                cached: 2,
1185                ..
1186            }
1187        ));
1188
1189        // The cached newer document survives and re-verifies.
1190        let loaded = cache.load(&line, &pk).unwrap().unwrap();
1191        assert_eq!(loaded.counter, 2);
1192    }
1193
1194    // rivet: verifies REQ-STATUS-DIST-001
1195    #[test]
1196    fn a_source_baseline_is_verified_and_cached_so_status_works_offline() {
1197        use crate::source::{LayerRef, MemorySource};
1198        let (sk, pk) = generate_root_keypair();
1199        let tmp = tempfile::tempdir().unwrap();
1200        let store_root = tmp.path();
1201        let line: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
1202        let doc = status(5);
1203        let envelope = doc.sign(&sk, "k").unwrap();
1204        let source = MemorySource::new().with_line_status(envelope.as_bytes());
1205        let layer = LayerRef::Name("2026.07.0".parse().unwrap());
1206
1207        let cached = cache_baseline_from_source(&source, &layer, &line, &pk, store_root).unwrap();
1208        assert_eq!(
1209            cached,
1210            Some(5),
1211            "a carried baseline is cached at its counter"
1212        );
1213
1214        // Now `status` works offline: the cache has it, verified.
1215        let loaded = StatusCache::at_root(store_root)
1216            .load(&line, &pk)
1217            .unwrap()
1218            .unwrap();
1219        assert_eq!(loaded.counter, 5);
1220    }
1221
1222    // rivet: verifies REQ-STATUS-DIST-001, REQ-VERIFY-001
1223    #[test]
1224    fn a_baseline_for_the_wrong_line_is_refused_not_miscached() {
1225        // A root-signed status document for a DIFFERENT line must not be
1226        // cached under the requested line — even validly signed. (Clean-room
1227        // review finding: the --from-file path asserted this; the baseline
1228        // path did not.)
1229        use crate::source::{LayerRef, MemorySource};
1230        let (sk, pk) = generate_root_keypair();
1231        let tmp = tempfile::tempdir().unwrap();
1232        let requested: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
1233        // A self-consistent document for the WRONG line — internally valid,
1234        // validly signed, and still not the line this consumer asked about.
1235        let doc = LineStatus {
1236            min_counter: None,
1237            line: "2026.08".into(),
1238            counter: 5,
1239            issued_at: "2026-08-07T00:00:00Z".into(),
1240            support_until: None,
1241            yanked: BTreeMap::new(),
1242            known_problems: Vec::new(),
1243        };
1244        let envelope = doc.sign(&sk, "k").unwrap();
1245        let source = MemorySource::new().with_line_status(envelope.as_bytes());
1246        let err = cache_baseline_from_source(
1247            &source,
1248            &LayerRef::Name("2026.07.0".parse().unwrap()),
1249            &requested,
1250            &pk,
1251            tmp.path(),
1252        )
1253        .unwrap_err();
1254        assert!(
1255            matches!(err, LineStatusError::LineMismatch { .. }),
1256            "a baseline for the wrong line must be refused: {err}"
1257        );
1258        assert!(
1259            StatusCache::at_root(tmp.path())
1260                .load(&requested, &pk)
1261                .unwrap()
1262                .is_none(),
1263            "nothing is cached under the requested line"
1264        );
1265    }
1266
1267    // rivet: verifies REQ-STATUS-DIST-001
1268    #[test]
1269    fn a_source_with_no_baseline_caches_nothing_and_does_not_error() {
1270        use crate::source::{LayerRef, MemorySource};
1271        let (_sk, pk) = generate_root_keypair();
1272        let tmp = tempfile::tempdir().unwrap();
1273        let line: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
1274        let source = MemorySource::new();
1275        let cached = cache_baseline_from_source(
1276            &source,
1277            &LayerRef::Name("2026.07.0".parse().unwrap()),
1278            &line,
1279            &pk,
1280            tmp.path(),
1281        )
1282        .unwrap();
1283        assert_eq!(cached, None);
1284    }
1285
1286    // rivet: verifies REQ-STATUS-DIST-001, REQ-VERIFY-001
1287    #[test]
1288    fn a_baseline_signed_by_an_impostor_is_refused_not_cached() {
1289        use crate::source::{LayerRef, MemorySource};
1290        let (attacker_sk, _) = generate_root_keypair();
1291        let (_real_sk, real_pk) = generate_root_keypair();
1292        let tmp = tempfile::tempdir().unwrap();
1293        let line: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
1294        let envelope = status(5).sign(&attacker_sk, "k").unwrap();
1295        let source = MemorySource::new().with_line_status(envelope.as_bytes());
1296        let err = cache_baseline_from_source(
1297            &source,
1298            &LayerRef::Name("2026.07.0".parse().unwrap()),
1299            &line,
1300            &real_pk,
1301            tmp.path(),
1302        )
1303        .unwrap_err();
1304        // The impostor's baseline never reaches the cache.
1305        assert!(
1306            StatusCache::at_root(tmp.path())
1307                .load(&line, &real_pk)
1308                .unwrap()
1309                .is_none(),
1310            "a baseline that fails verification must not be cached: {err}"
1311        );
1312    }
1313
1314    // rivet: verifies REQ-STATUS-DIST-001
1315    #[test]
1316    fn attaching_by_envelope_derives_the_line_from_the_document() {
1317        use crate::deposit::{DepositSpec, DepositTool, deposit};
1318        let (sk, pk) = generate_root_keypair();
1319        let tmp = tempfile::tempdir().unwrap();
1320        let dest = tmp.path().join("layout");
1321        deposit(
1322            &DepositSpec {
1323                includes: Vec::new(),
1324                layer: "2026.07.0".parse().unwrap(),
1325                channel: "qualified".into(),
1326                counter: 1,
1327                issued_at: "2026-08-07T00:00:00Z".into(),
1328                tools: vec![DepositTool {
1329                    name: "synth".into(),
1330                    version: "1".into(),
1331                    platform: None,
1332                    bytes: b"t".to_vec(),
1333                    source: None,
1334                    runner: None,
1335                    kind: None,
1336                    sdk_prefix: None,
1337                }],
1338            },
1339            &sk,
1340            "k",
1341            &dest,
1342        )
1343        .unwrap();
1344
1345        let envelope = status(4).sign(&sk, "k").unwrap();
1346        let (line, counter) = attach_envelope_to_layout(&dest, envelope.as_bytes()).unwrap();
1347        assert_eq!(line.to_string(), "2026.07");
1348        assert_eq!(counter, 4);
1349        // The layout now carries it and it re-verifies.
1350        let carried = read_any_from_layout(&dest).unwrap().unwrap();
1351        assert_eq!(
1352            LineStatus::verify_and_parse(&carried, &pk).unwrap().counter,
1353            4
1354        );
1355    }
1356
1357    // rivet: verifies REQ-PRODUCE-002
1358    #[test]
1359    fn attaching_a_stale_document_over_a_newer_one_is_refused() {
1360        // An independent review deleted the Stale block from
1361        // attach_envelope_to_layout and the whole workspace stayed green: the
1362        // test cited as this clause's evidence exercises StatusCache::update, a
1363        // DIFFERENT function, and the attach test above attaches exactly once.
1364        // Unguarded, a re-run CI step downgrades a layout's baseline — shipping
1365        // a pre-yank document that fresh consumers cache and are told "not
1366        // yanked" about a YANKED layer.
1367        use crate::deposit::{DepositSpec, DepositTool, deposit};
1368        let (sk, _pk) = generate_root_keypair();
1369        let tmp = tempfile::tempdir().unwrap();
1370        let dest = tmp.path().join("layout");
1371        deposit(
1372            &DepositSpec {
1373                includes: Vec::new(),
1374                layer: "2026.07.0".parse().unwrap(),
1375                channel: "qualified".into(),
1376                counter: 1,
1377                issued_at: "2026-08-07T00:00:00Z".into(),
1378                tools: vec![DepositTool {
1379                    name: "synth".into(),
1380                    version: "1".into(),
1381                    platform: None,
1382                    bytes: b"t".to_vec(),
1383                    source: None,
1384                    runner: None,
1385                    kind: None,
1386                    sdk_prefix: None,
1387                }],
1388            },
1389            &sk,
1390            "k",
1391            &dest,
1392        )
1393        .unwrap();
1394
1395        // The newer document lands…
1396        attach_envelope_to_layout(&dest, status(7).sign(&sk, "k").unwrap().as_bytes()).unwrap();
1397        // …and the older one is refused, naming both counters.
1398        let err = attach_envelope_to_layout(&dest, status(3).sign(&sk, "k").unwrap().as_bytes())
1399            .unwrap_err();
1400        assert!(
1401            matches!(
1402                err,
1403                LineStatusError::Stale {
1404                    presented: 3,
1405                    cached: 7,
1406                    ..
1407                }
1408            ),
1409            "a lower counter must be refused, got {err}"
1410        );
1411        let msg = err.to_string();
1412        assert!(msg.contains('3') && msg.contains('7'), "names both: {msg}");
1413        // The layout still carries the NEWER document, not the stale one.
1414        let carried = parse_unverified(&read_any_from_layout(&dest).unwrap().unwrap()).unwrap();
1415        assert_eq!(
1416            carried.counter, 7,
1417            "the newer baseline survives the attempt"
1418        );
1419        // Re-attaching the SAME counter is not a regression and is allowed —
1420        // CI re-runs must stay idempotent.
1421        attach_envelope_to_layout(&dest, status(7).sign(&sk, "k").unwrap().as_bytes()).unwrap();
1422    }
1423
1424    // rivet: verifies REQ-PRODUCE-002
1425    #[test]
1426    fn an_advisory_that_could_never_fire_is_refused_at_sign_time() {
1427        // varve#61: `report_for` matches ids EXACTLY and the cache is keyed
1428        // per line, so a typo'd affected id — "2026.9.0" for "2026.09.0" —
1429        // signed fine and the advisory then fired for nobody. The signature
1430        // is the cheapest place to stop it.
1431        let (sk, _pk) = generate_root_keypair();
1432        let cases: &[(&str, &str)] = &[
1433            ("2026.7.0", "not a valid YYYY.MM.P id"), // typo'd month width
1434            ("2026.07", "missing its patch component"), // a line, not a layer
1435            ("2026.08.0", "belongs to another line"), // wrong line entirely
1436            ("2026.07.O", "letter O for zero"),
1437        ];
1438        for (bad, why) in cases {
1439            let mut doc = status(1);
1440            doc.known_problems[0].affected = vec![bad.to_string()];
1441            let err = doc.sign(&sk, "k").unwrap_err();
1442            assert!(
1443                matches!(err, LineStatusError::DeadReference { .. }),
1444                "{why}: affected id {bad:?} must be refused, got: {err}"
1445            );
1446            let msg = err.to_string();
1447            assert!(
1448                msg.contains(bad) && msg.contains("2026.07") && msg.contains("re-sign"),
1449                "the error must name the id, the line, and the fix: {msg}"
1450            );
1451        }
1452        // A typo'd YANK key is the same dead advisory.
1453        let mut doc = status(1);
1454        doc.yanked = BTreeMap::from([("2026.8.0".to_string(), "CVE".to_string())]);
1455        assert!(matches!(
1456            doc.sign(&sk, "k").unwrap_err(),
1457            LineStatusError::DeadReference { .. }
1458        ));
1459        // …and the untouched fixture still signs: the gate can pass, not
1460        // merely fail.
1461        status(1).sign(&sk, "k").unwrap();
1462    }
1463
1464    // rivet: verifies REQ-PRODUCE-002
1465    #[test]
1466    fn attach_refuses_a_pre_signed_advisory_that_could_never_fire() {
1467        // The envelope may come from an older varve whose sign-status did not
1468        // validate — attach is the last producer-side gate before the layout
1469        // ships. Built with the raw signer to bypass `sign`'s own check,
1470        // exactly as an old binary would have.
1471        use crate::deposit::{DepositSpec, DepositTool, deposit};
1472        let (sk, _pk) = generate_root_keypair();
1473        let tmp = tempfile::tempdir().unwrap();
1474        let dest = tmp.path().join("layout");
1475        deposit(
1476            &DepositSpec {
1477                includes: Vec::new(),
1478                layer: "2026.07.0".parse().unwrap(),
1479                channel: "qualified".into(),
1480                counter: 1,
1481                issued_at: "2026-08-07T00:00:00Z".into(),
1482                tools: vec![DepositTool {
1483                    name: "synth".into(),
1484                    version: "1".into(),
1485                    platform: None,
1486                    bytes: b"t".to_vec(),
1487                    source: None,
1488                    runner: None,
1489                    kind: None,
1490                    sdk_prefix: None,
1491                }],
1492            },
1493            &sk,
1494            "k",
1495            &dest,
1496        )
1497        .unwrap();
1498        let mut doc = status(1);
1499        doc.known_problems[0].affected = vec!["2026.7.0".to_string()];
1500        let payload = serde_json::to_vec_pretty(&doc).unwrap();
1501        let envelope = dsse_sign_typed(&payload, LINE_STATUS_PAYLOAD_TYPE, &sk, "k").unwrap();
1502        let err = attach_envelope_to_layout(&dest, envelope.as_bytes()).unwrap_err();
1503        assert!(
1504            matches!(err, LineStatusError::DeadReference { .. }),
1505            "got: {err}"
1506        );
1507        assert!(
1508            read_any_from_layout(&dest).unwrap().is_none(),
1509            "the dead advisory must not land in the layout"
1510        );
1511    }
1512
1513    // rivet: verifies REQ-PRODUCE-002
1514    #[test]
1515    fn attaching_to_a_directory_that_is_not_a_layout_is_refused_before_writing() {
1516        // The old path created blobs/sha256/ inside the directory and then
1517        // failed on index.json with "io error at …: No such file or directory
1518        // (os error 2): No such file or directory (os error 2)" — the cause
1519        // twice, the fix never (varve#60).
1520        let (sk, _pk) = generate_root_keypair();
1521        let tmp = tempfile::tempdir().unwrap();
1522        let not_a_layout = tmp.path().join("somedir");
1523        std::fs::create_dir_all(&not_a_layout).unwrap();
1524        let envelope = status(1).sign(&sk, "k").unwrap();
1525        let err = attach_envelope_to_layout(&not_a_layout, envelope.as_bytes()).unwrap_err();
1526        assert!(
1527            matches!(err, LineStatusError::NotALayout { .. }),
1528            "got: {err}"
1529        );
1530        assert!(
1531            err.to_string().contains("varve deposit"),
1532            "the error must carry its fix: {err}"
1533        );
1534        assert!(
1535            !not_a_layout.join("blobs").exists(),
1536            "nothing may be written into a directory that is not a layout"
1537        );
1538    }
1539
1540    // rivet: verifies REQ-PRODUCE-002
1541    #[test]
1542    fn the_unsigned_document_mistake_is_named_not_wrapped() {
1543        // Handing the raw status JSON where the signed envelope belongs is
1544        // the commonest producer mistake; the old error was a doubled parser
1545        // wrap that never said "sign it" (varve#60).
1546        let raw = serde_json::to_string_pretty(&status(1)).unwrap();
1547        let err = not_an_envelope(&raw);
1548        let msg = err.to_string();
1549        assert!(
1550            msg.contains("UNSIGNED") && msg.contains("varve sign-status"),
1551            "raw document must be diagnosed with its fix: {msg}"
1552        );
1553        // Garbage is still garbage, said once, with the expected shape named.
1554        let msg = not_an_envelope("garbage").to_string();
1555        assert!(
1556            msg.contains("not a DSSE envelope") && msg.contains("varve sign-status"),
1557            "got: {msg}"
1558        );
1559        // And verify_and_parse routes through the same diagnosis.
1560        let (_sk, pk) = generate_root_keypair();
1561        let err = LineStatus::verify_and_parse(raw.as_bytes(), &pk).unwrap_err();
1562        assert!(err.to_string().contains("UNSIGNED"), "got: {err}");
1563    }
1564
1565    // rivet: verifies REQ-STATUS-DIST-001
1566    #[test]
1567    fn a_deposit_layouts_baseline_is_readable_without_naming_the_line() {
1568        // A registry/layout consumer that installs by digest may not know the
1569        // line up front — the baseline must be recoverable from the layout
1570        // alone. A deposit layout carries exactly one line-status.
1571        use crate::deposit::{DepositSpec, DepositTool, deposit};
1572        let (sk, pk) = generate_root_keypair();
1573        let tmp = tempfile::tempdir().unwrap();
1574        let dest = tmp.path().join("layout");
1575        let spec = DepositSpec {
1576            includes: Vec::new(),
1577            layer: "2026.07.0".parse().unwrap(),
1578            channel: "qualified".into(),
1579            counter: 1,
1580            issued_at: "2026-08-07T00:00:00Z".into(),
1581            tools: vec![DepositTool {
1582                name: "synth".into(),
1583                version: "1".into(),
1584                platform: None,
1585                bytes: b"t".to_vec(),
1586                source: None,
1587                runner: None,
1588                kind: None,
1589                sdk_prefix: None,
1590            }],
1591        };
1592        deposit(&spec, &sk, "k", &dest).unwrap();
1593
1594        // No baseline yet -> None, not an error.
1595        assert!(read_any_from_layout(&dest).unwrap().is_none());
1596
1597        let line: Line = "2026.07.0".parse::<LayerId>().unwrap().line().clone();
1598        let envelope = status(3).sign(&sk, "k").unwrap();
1599        attach_to_layout(&dest, &line, envelope.as_bytes()).unwrap();
1600
1601        let carried = read_any_from_layout(&dest).unwrap().unwrap();
1602        let parsed = LineStatus::verify_and_parse(&carried, &pk).unwrap();
1603        assert_eq!(parsed.counter, 3);
1604    }
1605
1606    // ───────────────────────── REQ-ADVISORY-002 ─────────────────────────
1607
1608    /// A listing naming exactly the layers of the July line the fixture
1609    /// document talks about.
1610    fn listing(layers: &[&str]) -> KnownLayers {
1611        KnownLayers::Known {
1612            source: "the signed line-index for 2026.07 (counter 1)".into(),
1613            line: Some("2026.07".into()),
1614            layers: layers.iter().map(|s| s.to_string()).collect(),
1615        }
1616    }
1617
1618    // rivet: verifies REQ-ADVISORY-002
1619    #[test]
1620    fn an_affected_id_naming_no_existing_layer_is_refused_and_the_verdict_lists_what_does_exist() {
1621        // The defect: one wrong character. `2026.07.10` is a well-formed layer
1622        // id of the right line, so every shape check passes; it names no layer
1623        // that exists, so `varve status` — which matches ids EXACTLY — never
1624        // fires it. The producer sees success, the consumer sees nothing.
1625        let mut doc = status(1);
1626        doc.yanked.clear();
1627        doc.known_problems = vec![KnownProblem {
1628            id: "KP-1".into(),
1629            title: "t".into(),
1630            severity: "high".into(),
1631            affected: vec!["2026.07.10".into()],
1632            workaround: None,
1633            detection: None,
1634            mitigation: None,
1635        }];
1636        let err = doc
1637            .check_layer_refs_against(&listing(&["2026.07.0", "2026.07.1"]), false)
1638            .expect_err("an advisory that can never fire must be refused");
1639        assert!(
1640            matches!(&err, LineStatusError::UnknownLayer { id, .. } if id == "2026.07.10"),
1641            "got: {err}"
1642        );
1643        let msg = err.to_string();
1644        assert!(msg.contains("KP-1"), "name the entry at fault: {msg}");
1645        // The ids that DO exist — the shape varve already uses for tools
1646        // ("it exposes: …"). A refusal that does not show the alternatives
1647        // sends the operator back to the registry to guess.
1648        assert!(
1649            msg.contains("2026.07.0") && msg.contains("2026.07.1"),
1650            "the refusal must list the ids that exist: {msg}"
1651        );
1652        assert!(msg.contains("--force"), "{msg}");
1653    }
1654
1655    // rivet: verifies REQ-ADVISORY-002
1656    #[test]
1657    fn a_yank_key_is_checked_against_existing_layers_too_not_only_affected() {
1658        // A yank is the entry with the most consequence and the least
1659        // redundancy: nothing else in the document repeats it, so a typo'd
1660        // yank key is a withdrawal that silently never happened.
1661        let mut doc = status(1);
1662        doc.known_problems.clear();
1663        doc.yanked = BTreeMap::from([("2026.07.9".to_string(), "CVE".to_string())]);
1664        let err = doc
1665            .check_layer_refs_against(&listing(&["2026.07.0"]), false)
1666            .expect_err("a yank naming no layer must be refused");
1667        assert!(
1668            matches!(&err, LineStatusError::UnknownLayer { what, id, .. }
1669                     if what.contains("yank") && id == "2026.07.9"),
1670            "got: {err}"
1671        );
1672    }
1673
1674    // rivet: verifies REQ-ADVISORY-002
1675    #[test]
1676    fn a_document_whose_ids_all_exist_passes_and_says_what_was_checked() {
1677        // The other half of the rule: the check must be capable of PASSING, or
1678        // it is not a check, it is a ban on advisories.
1679        let check = status(1)
1680            .check_layer_refs_against(&listing(&["2026.07.0", "2026.07.1"]), false)
1681            .expect("every id in the fixture exists on the line");
1682        assert!(check.existence_checked);
1683        assert!(
1684            check.note.contains("checked against") && check.note.contains("2 layers"),
1685            "the note must state the check that RAN: {}",
1686            check.note
1687        );
1688    }
1689
1690    // rivet: verifies REQ-ADVISORY-002
1691    #[test]
1692    fn where_the_line_is_not_visible_the_answer_says_which_check_was_not_run() {
1693        // "Silence must not." Where no listing is in reach the existence check
1694        // cannot run, and a bare "signed" would imply a completeness that was
1695        // never established — the exact shape of the defect, moved one level
1696        // up into the tool's own reporting.
1697        let check = status(1)
1698            .check_layer_refs_against(&KnownLayers::unknown("no line-index was supplied"), false)
1699            .unwrap();
1700        assert!(
1701            !check.existence_checked,
1702            "an unchecked document must not report itself as checked"
1703        );
1704        assert!(
1705            check.note.contains("NOT") && check.note.contains("no line-index was supplied"),
1706            "the note must name the check that did NOT run, and why: {}",
1707            check.note
1708        );
1709    }
1710
1711    // rivet: verifies REQ-ADVISORY-002
1712    #[test]
1713    fn force_allows_a_layer_not_deposited_yet_but_never_a_malformed_id() {
1714        // `--force` exists for one legitimate case: pre-signing an advisory
1715        // for a layer about to be deposited. It must not become a way past the
1716        // SHAPE check — an id that is not a layer identifier of this line
1717        // cannot become correct later, so there is nothing for force to allow.
1718        let mut doc = status(1);
1719        doc.yanked.clear();
1720        doc.known_problems = vec![KnownProblem {
1721            id: "KP-1".into(),
1722            title: "t".into(),
1723            severity: "high".into(),
1724            affected: vec!["2026.07.9".into()],
1725            workaround: None,
1726            detection: None,
1727            mitigation: None,
1728        }];
1729        let check = doc
1730            .check_layer_refs_against(&listing(&["2026.07.0"]), true)
1731            .expect("--force pre-signs for a layer not deposited yet");
1732        assert!(
1733            !check.existence_checked,
1734            "forcing must not report the check as having run"
1735        );
1736        assert!(check.note.contains("--force"), "{}", check.note);
1737
1738        // …and the same document with a typo that is not a layer id at all is
1739        // still refused, force or no force.
1740        for id in ["2026.07", "twenty-twenty-six", "2026.08.0"] {
1741            doc.known_problems[0].affected = vec![id.to_string()];
1742            match doc.check_layer_refs_against(&listing(&["2026.07.0"]), true) {
1743                Err(LineStatusError::DeadReference { .. }) => {}
1744                other => panic!("'{id}' must be refused even under --force, got: {other:?}"),
1745            }
1746        }
1747    }
1748
1749    // rivet: verifies REQ-ADVISORY-002
1750    #[test]
1751    fn a_listing_for_another_line_is_refused_rather_than_used() {
1752        // A listing for a different line answers a different question. Used
1753        // anyway it would either wave everything through or refuse everything,
1754        // and both verdicts would be reported as if they meant something.
1755        let wrong = KnownLayers::Known {
1756            source: "the signed line-index for 2026.08".into(),
1757            line: Some("2026.08".into()),
1758            layers: vec!["2026.08.0".into()],
1759        };
1760        let err = status(1)
1761            .check_layer_refs_against(&wrong, false)
1762            .expect_err("a listing for another line must not be used as this line's");
1763        assert!(
1764            matches!(&err, LineStatusError::LineMismatch { expected, got }
1765                     if expected == "2026.07" && got == "2026.08"),
1766            "got: {err}"
1767        );
1768    }
1769
1770    // rivet: verifies REQ-ADVISORY-002
1771    #[test]
1772    fn a_layout_becomes_a_listing_only_once_the_signed_index_is_attached() {
1773        // Where the signer CAN see the line's layers, and where it cannot. A
1774        // deposit layout holds ONE layer — it is not a listing of the line, and
1775        // treating it as one would refuse advisories about layers that exist
1776        // perfectly well elsewhere. The realm's signed index IS a listing.
1777        use crate::deposit::{DepositSpec, DepositTool, deposit};
1778        let (sk, _pk) = generate_root_keypair();
1779        let tmp = tempfile::tempdir().unwrap();
1780        let dest = tmp.path().join("layout");
1781        deposit(
1782            &DepositSpec {
1783                includes: Vec::new(),
1784                layer: "2026.07.0".parse().unwrap(),
1785                channel: "qualified".into(),
1786                counter: 1,
1787                issued_at: "2026-08-07T00:00:00Z".into(),
1788                tools: vec![DepositTool {
1789                    name: "synth".into(),
1790                    version: "1".into(),
1791                    platform: None,
1792                    bytes: b"t".to_vec(),
1793                    source: None,
1794                    runner: None,
1795                    kind: None,
1796                    sdk_prefix: None,
1797                }],
1798            },
1799            &sk,
1800            "k",
1801            &dest,
1802        )
1803        .unwrap();
1804
1805        // No index yet: NOT a listing, and it says why rather than pretending.
1806        let known = known_layers_in_layout(&dest, "2026.07");
1807        assert!(
1808            matches!(&known, KnownLayers::Unknown { why } if why.contains("not a listing")),
1809            "got: {known:?}"
1810        );
1811
1812        let index = crate::lineindex::LineIndex {
1813            line: "2026.07".into(),
1814            counter: 1,
1815            issued_at: "2026-08-07T00:00:00Z".into(),
1816            layers: vec![crate::lineindex::IndexedLayer {
1817                layer: "2026.07.0".into(),
1818                digest: "sha256:aa".into(),
1819                channel: "qualified".into(),
1820                counter: 1,
1821            }],
1822        };
1823        crate::lineindex::attach_to_layout(
1824            &dest,
1825            "2026.07",
1826            index.sign(&sk, "k").unwrap().as_bytes(),
1827        )
1828        .unwrap();
1829
1830        let known = known_layers_in_layout(&dest, "2026.07");
1831        assert_eq!(
1832            known,
1833            KnownLayers::Known {
1834                source: "the signed line-index for 2026.07 (counter 1)".into(),
1835                line: Some("2026.07".into()),
1836                layers: vec!["2026.07.0".into()],
1837            }
1838        );
1839
1840        // …and the attach seam now refuses the advisory that could never fire,
1841        // while the same document naming the real layer attaches and reports
1842        // the check it ran.
1843        let mut doc = status(2);
1844        doc.yanked.clear();
1845        doc.known_problems = vec![KnownProblem {
1846            id: "KP-1".into(),
1847            title: "t".into(),
1848            severity: "high".into(),
1849            affected: vec!["2026.07.1".into()],
1850            workaround: None,
1851            detection: None,
1852            mitigation: None,
1853        }];
1854        let err = attach_envelope_to_layout(&dest, doc.sign(&sk, "k").unwrap().as_bytes())
1855            .expect_err("2026.07.1 is not on this line's index");
1856        assert!(
1857            matches!(&err, LineStatusError::UnknownLayer { .. }),
1858            "got: {err}"
1859        );
1860
1861        doc.known_problems[0].affected = vec!["2026.07.0".into()];
1862        let (_line, counter, check) =
1863            attach_envelope_to_layout_checked(&dest, doc.sign(&sk, "k").unwrap().as_bytes(), false)
1864                .unwrap();
1865        assert_eq!(counter, 2);
1866        assert!(check.existence_checked, "{}", check.note);
1867    }
1868
1869    // rivet: verifies REQ-ADVISORY-002
1870    #[test]
1871    fn signing_reports_the_check_it_ran_alongside_the_envelope() {
1872        // The producing seam. `sign_against` hands back the envelope AND what
1873        // was verified about it, so the CLI can print the check rather than a
1874        // bare "signed" that implies a complete one.
1875        let (sk, pk) = generate_root_keypair();
1876        let (envelope, check) = status(1)
1877            .sign_against(&listing(&["2026.07.0", "2026.07.1"]), false, &sk, "k")
1878            .unwrap();
1879        assert!(check.existence_checked);
1880        assert_eq!(
1881            LineStatus::verify_and_parse(envelope.as_bytes(), &pk).unwrap(),
1882            status(1),
1883            "the checked path must sign the same document the plain path does"
1884        );
1885
1886        // And a document that could never fire is not signed at all — the
1887        // point is that the signature must not exist.
1888        let mut doc = status(1);
1889        doc.yanked.clear();
1890        doc.known_problems[0].affected = vec!["2026.07.7".into()];
1891        doc.known_problems[1].affected = vec!["2026.07.0".into()];
1892        assert!(
1893            doc.sign_against(&listing(&["2026.07.0"]), false, &sk, "k")
1894                .is_err()
1895        );
1896    }
1897
1898    // rivet: verifies REQ-ADVISORY-002
1899    #[test]
1900    fn a_producer_can_list_their_own_line_without_a_network_or_an_index() {
1901        // DD-023 clause 5. `signed-index` is false by default, so an
1902        // index-only existence check would usually have nothing to check
1903        // against — and opt-in safety is how a typo'd `affected` id came to
1904        // sign cleanly and fire for nobody. The producer holds the layers.
1905        let tmp = tempfile::tempdir().unwrap();
1906        let (sk, _pk) = crate::generate_root_keypair();
1907        // REAL layouts, written by `deposit` itself. The first version of this
1908        // test built them with `DirSource::put`, which writes the bare
1909        // manifests/+blobs/ SOURCE shape — not what `varve deposit --out`
1910        // produces. It passed, and the CLI then found nothing at all. A
1911        // fixture speaking a shape the tool never emits is the defect this
1912        // release is named for, so this one uses the real writer.
1913        for (id, counter, dir) in [
1914            ("2026.08.0", 1u64, "out-a"),
1915            ("2026.08.1", 2, "out-b"),
1916            // A layer of a DIFFERENT line must not be counted as this line's.
1917            ("2026.09.0", 1, "out-other"),
1918        ] {
1919            let spec = crate::deposit::DepositSpec {
1920                layer: id.parse().unwrap(),
1921                channel: "rolling".into(),
1922                counter,
1923                issued_at: "2026-08-07T00:00:00Z".into(),
1924                tools: vec![crate::DepositTool {
1925                    name: "t".into(),
1926                    version: "1.0".into(),
1927                    platform: None,
1928                    bytes: b"x".to_vec(),
1929                    source: None,
1930                    runner: None,
1931                    kind: None,
1932                    sdk_prefix: None,
1933                }],
1934                includes: Vec::new(),
1935            };
1936            crate::deposit(&spec, &sk, "k", &tmp.path().join(dir)).unwrap();
1937        }
1938
1939        // Pointed at the PARENT of several layouts, which is the usual shape.
1940        let known = known_layers_in_layout_dirs(&[tmp.path().to_path_buf()], "2026.08");
1941        match &known {
1942            KnownLayers::Known { layers, line, .. } => {
1943                assert_eq!(layers, &["2026.08.0", "2026.08.1"], "got {layers:?}");
1944                assert_eq!(line.as_deref(), Some("2026.08"));
1945            }
1946            KnownLayers::Unknown { why } => panic!("expected a listing, got: {why}"),
1947        }
1948
1949        // …and it actually catches the typo it exists for.
1950        let mut doc = status(2);
1951        doc.line = "2026.08".into();
1952        doc.yanked = BTreeMap::from([(
1953            "2026.08.10".to_string(),
1954            "typo — never deposited".to_string(),
1955        )]);
1956        doc.known_problems.clear();
1957        let err = doc
1958            .check_layer_refs_against(&known, false)
1959            .expect_err("a yank naming a layer this line does not have must be refused");
1960        let msg = err.to_string();
1961        assert!(
1962            msg.contains("2026.08.10") && msg.contains("2026.08.0"),
1963            "the refusal must name the bad id AND the ids that exist: {msg}"
1964        );
1965
1966        // A directory holding no layout says so rather than passing clean.
1967        let empty = tempfile::tempdir().unwrap();
1968        assert!(matches!(
1969            known_layers_in_layout_dirs(&[empty.path().to_path_buf()], "2026.08"),
1970            KnownLayers::Unknown { .. }
1971        ));
1972    }
1973}