Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions artifacts/requirements.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5013,3 +5013,52 @@ artifacts:
created-by: ai
model: claude-opus-5-5
timestamp: 2026-10-14T20:07:55Z

- id: REQ-COMPOSEWINDOW-001
type: requirement
title: A composition never promises support past the layers it composes, unless it says why
status: approved
release: v0.43.0
description: Measured on varve#25(4) The cap shall be applied by the command the deposit already calls (varve support-horizon), given the layer's manifest; a layers repository adopts it with one argument and the realm definitions it already downloads, and no second command decides the window.
links:
- type: satisfies
target: REQ-SUPPORTUNTIL-001
- type: satisfies
target: REQ-COMPOSE-001
provenance:
created-by: ai
model: claude-opus-5-5
timestamp: 2026-10-14T19:09:24Z

- id: REQ-ENVQUOTE-001
type: requirement
title: varve env emits the store root quoted, so the line a user is told to eval cannot execute it
status: approved
release: v0.43.0
description: "varve#46: env_script_sh interpolates the store root inside sh double quotes, where $ and backticks stay live; VARVE_ROOT='/tmp/vr$(id)x' runs id when the user follows 'eval \"$(varve env)\"'. The same injection class v0.19.0 removed from shim dispatch, surviving where the user is told to eval. Clauses: (1) Every shell varve env supports shall receive the path in that shell's literal quoting (sh/bash/zsh single quotes with '\\'' escaping; fish and PowerShell by their own rules). (2) A root containing each shell's metacharacters shall round-trip as the literal path, shown by running the emitted script in that shell."
provenance:
created-by: ai
model: claude-opus-5-5
timestamp: 2026-10-14T19:09:24Z

- id: REQ-NOPINNOTOOL-001
type: requirement
title: With no pin in scope, which, run and the shims refuse as status does
status: approved
release: v0.43.0
description: "varve#198: in a directory with no varve.toml, varve status refused (no trust root) while varve which ordeal resolved an old ordeal from the legacy pre-realm store ~/.varve/core with no verification, and the shims on PATH did the same. Two code paths answering one question differently. Clauses: (1) With no pin in scope, which, run and shim dispatch shall refuse with the same reason status gives. (2) No resolution shall read the legacy pre-realm store. (3) varve shall say the legacy store exists and that nothing in it verifies, without deleting it."
provenance:
created-by: ai
model: claude-opus-5-5
timestamp: 2026-10-14T19:09:24Z

- id: REQ-MUTANTDEBT-001
type: requirement
title: Declared mutation debt is measured in full, not cut off at a time cap
status: approved
release: v0.43.0
description: "varve#181: the nightly advisory mutation run is cancelled at its 90-minute cap every night after ~32% of 2,675 mutants, so 11 of the 22 not-yet files are never reached. The instrument that makes declared debt counted rather than hidden counts half of it. Clauses: (1) Every not-yet file in mutation-scope.toml shall be tested by some scheduled run that completes. (2) A run that does not complete shall be reported as incomplete, never as a measurement."
provenance:
created-by: ai
model: claude-opus-5-5
timestamp: 2026-10-14T19:09:24Z
60 changes: 53 additions & 7 deletions crates/varve-core/src/linestatus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,46 @@ pub fn cache_baseline_from_source(
root_pk: &[u8],
store_root: &Path,
) -> Result<Option<u64>, LineStatusError> {
match current_line_status(source, layer, line, root_pk, Published::BestEffort)? {
Some((envelope, doc)) => {
StatusCache::at_root(store_root).update(line, &envelope, &doc)?;
Ok(Some(doc.counter))
}
None => Ok(None),
}
}

/// What a failed fetch of the line's published correction means.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Published {
/// Read as absence: the reader acts on the best document it can verify.
BestEffort,
/// Refuse: the reader will sign on the answer.
Required,
}

/// The line-status that currently speaks for `layer`: the baseline deposited
/// beside it or the document later published under the line's own tag,
/// whichever VERIFIES and carries the higher counter. `Ok(None)` when the
/// source carries neither; `Err` when something was present but bad.
///
/// `published` says what a FAILED fetch of the correction means. Install
/// reads it as absence ([`Published::BestEffort`]) and documents the residual.
/// A reader about to SIGN something on the strength of the answer cannot:
/// a correction only ever narrows what the baseline said, so "could not ask"
/// read as "nothing newer" fails open ([`Published::Required`]).
///
/// One rule for every reader. The install cache and a composition reading its
/// includes' windows (REQ-COMPOSEWINDOW-001) must agree about which document
/// is current, or a correction that shortened a window would bind one and
/// not the other.
pub fn current_line_status(
source: &dyn crate::source::LayerSource,
layer: &crate::source::LayerRef,
line: &Line,
root_pk: &[u8],
published: Published,
) -> Result<Option<(Vec<u8>, LineStatus)>, LineStatusError> {
let baseline = source
.fetch_line_status(layer)
.map_err(|e| LineStatusError::Payload(format!("fetching baseline line-status: {e}")))?;
Expand All @@ -793,9 +833,18 @@ pub fn cache_baseline_from_source(
// A source that cannot serve one must not be able to make that look like a
// failure, so a transport error here is treated as absence — the baseline
// path decides whether the consumer ends up with nothing.
let published = source
.fetch_published_line_status(&line.to_string())
.unwrap_or(None);
let published = match (
source.fetch_published_line_status(&line.to_string()),
published,
) {
(Ok(doc), _) => doc,
(Err(_), Published::BestEffort) => None,
(Err(e), Published::Required) => {
return Err(LineStatusError::Payload(format!(
"cannot tell whether a correction was published for line {line}: {e}"
)));
}
};

// VERIFY FIRST, RANK SECOND. Both documents are untrusted bytes from the
// party this evidence exists to constrain. Choosing by counter before
Expand Down Expand Up @@ -830,10 +879,7 @@ pub fn cache_baseline_from_source(
}

match best {
Some((counter, envelope, doc)) => {
StatusCache::at_root(store_root).update(line, &envelope, &doc)?;
Ok(Some(counter))
}
Some((_, envelope, doc)) => Ok(Some((envelope, doc))),
// Nothing usable. If something was PRESENT but bad, that is a finding
// and is raised; if both were simply absent, the source carries none.
None => match first_error {
Expand Down
73 changes: 43 additions & 30 deletions crates/varve-core/src/sdkexport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,40 @@ fn find_sub(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
(from..=haystack.len() - needle.len()).find(|&i| &haystack[i..i + needle.len()] == needle)
}

/// Every occurrence of `from` in `hay` replaced by `to`, and how many there
/// were. ONE copy, used by the text path, the per-string binary path and the
/// SYSDIRS table.
///
/// Written without index arithmetic on purpose. The three loops this replaced
/// advanced with `i = hit + from.len()`, and cargo-mutants' `+` → `*` turned
/// that into a loop that never advances when the hit is at 0 — while
/// appending on every pass. It did not just hang: it grew a buffer until the
/// CI runner ran out of memory and was shut down, twice, taking the whole
/// mutation shard with it. Slicing with `get` leaves no operator for that
/// mutant, and the loop below cannot run without consuming input.
fn replace_all(hay: &[u8], from: &[u8], to: &[u8]) -> (Vec<u8>, usize) {
let mut out = Vec::with_capacity(hay.len());
let mut rest = hay;
let mut count = 0;
// Every pass must CONSUME input or stop. Advancing only past a match
// that lies wholly inside `rest` makes termination independent of what
// `find_sub` answers: a mutant `find_sub` that always says `Some(0)`
// once spun this loop forever on an empty `rest`, appending each time,
// until the CI runner ran out of memory — the failure it was written to
// end, reached by a different mutant (2026-10-16).
while let Some(at) = find_sub(rest, from, 0) {
let Some(tail) = rest.get(at..).and_then(|t| t.get(from.len()..)) else {
break;
};
out.extend_from_slice(&rest[..at]);
out.extend_from_slice(to);
rest = tail;
count += 1;
}
out.extend_from_slice(rest);
(out, count)
}

/// What relocating one member's bytes produced.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Relocation {
Expand Down Expand Up @@ -253,16 +287,7 @@ pub fn relocate_bytes(

if !is_binary(bytes) {
// The `sed -i` half: no field to overflow, so the text may change length.
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
let mut substitutions = 0;
while let Some(hit) = find_sub(bytes, built, i) {
out.extend_from_slice(&bytes[i..hit]);
out.extend_from_slice(dest);
i = hit + built.len();
substitutions += 1;
}
out.extend_from_slice(&bytes[i..]);
let (out, substitutions) = replace_all(bytes, built, dest);
return Ok(Relocation {
bytes: out,
fields: 0,
Expand All @@ -278,6 +303,12 @@ pub fn relocate_bytes(
let mut fields = sysdirs.as_ref().map_or(0, |(_, n)| *n);
let mut cursor = 0;
while let Some(hit) = find_sub(&out, built, cursor) {
// A hit behind the cursor is a search that did not advance; stop
// rather than re-patch the same field forever (a mutant `find_sub`
// returning a fixed position hung here until the test timed out).
if hit < cursor {
break;
}
if let Some((range, _)) = &sysdirs
&& range.contains(&hit)
{
Expand Down Expand Up @@ -317,15 +348,7 @@ pub fn relocate_bytes(
let capacity = pad_end - start;

// Replace every occurrence of the prefix WITHIN this one string.
let old = out[start..end].to_vec();
let mut new = Vec::with_capacity(old.len());
let mut i = 0;
while let Some(h) = find_sub(&old, built, i) {
new.extend_from_slice(&old[i..h]);
new.extend_from_slice(dest);
i = h + built.len();
}
new.extend_from_slice(&old[i..]);
let (new, _) = replace_all(&out[start..end], built, dest);

// `if len(new_dl_path) >= p_filesz: ERROR` — transcribed, not
// paraphrased. The destination check has already made this
Expand Down Expand Up @@ -447,17 +470,7 @@ fn relocate_sysdirs(
let paths: Vec<Vec<u8>> = table
.split(|b| *b == 0)
.take_while(|p| !p.is_empty())
.map(|p| {
let mut new = Vec::with_capacity(p.len());
let mut i = 0;
while let Some(h) = find_sub(p, built, i) {
new.extend_from_slice(&p[i..h]);
new.extend_from_slice(dest);
i = h + built.len();
}
new.extend_from_slice(&p[i..]);
new
})
.map(|p| replace_all(p, built, dest).0)
.collect();
let width = if class64 { 8 } else { 4 };
let slots = len_size / width;
Expand Down
Loading
Loading