can1357/oh-my-pi · error

modified change requires peer root

Error message

modified change requires peer root

What it means

The pi-iso differ builds a unified diff for a Modified entry by reading both the changed file (primary side) and its unchanged counterpart in the peer root (lower layer). plain_change documents that `op == Modified` requires `peer_root = Some(lower)`; when a caller classifies an entry as Modified but passes `peer_root: None`, this expect panics. It is an internal invariant violation: the diff of a modification cannot be computed without the original bytes.

Source

Thrown at crates/pi-iso/src/diff.rs:366

/// `op == Modified` requires `peer_root = Some(lower)` so we can read the
/// counterpart; `Added`/`Removed` only need the side we already know about.
fn plain_change(
	side: &Path,
	rel: &Path,
	op: ChangeKind,
	peer_root: Option<&Path>,
) -> IsoResult<FileChange> {
	let full = side.join(rel);
	let primary = std::fs::read(&full)
		.map_err(|err| IsoError::other(format!("read {}: {err}", full.display())))?;
	if looks_binary(&primary) {
		return Ok(FileChange { path: rel.to_path_buf(), op, diff: None });
	}
	let (old_bytes, new_bytes) = match op {
		ChangeKind::Added => (Vec::new(), primary),
		ChangeKind::Removed => (primary, Vec::new()),
		ChangeKind::Modified => {
			let peer = peer_root.expect("modified change requires peer root");
			let peer_full = peer.join(rel);
			let peer_bytes = std::fs::read(&peer_full)
				.map_err(|err| IsoError::other(format!("read {}: {err}", peer_full.display())))?;
			if looks_binary(&peer_bytes) {
				return Ok(FileChange { path: rel.to_path_buf(), op, diff: None });
			}
			(peer_bytes, primary)
		},
	};
	let (Ok(old_text), Ok(new_text)) =
		(std::str::from_utf8(&old_bytes), std::str::from_utf8(&new_bytes))
	else {
		return Ok(FileChange { path: rel.to_path_buf(), op, diff: None });
	};
	Ok(FileChange {
		path: rel.to_path_buf(),
		op,
		diff: Some(render_unified(rel, op, old_text, new_text)),

View on GitHub (pinned to 9690622007)

Solutions

  1. Ensure the diff is always invoked with both roots configured — pass the lower/base directory as peer_root whenever a Modified change is possible.
  2. Verify the peer root exists and is readable before diffing (index_tree skips missing roots silently, so a missing peer surfaces later as this panic).
  3. If diffing a standalone tree is intended, treat every entry as Added rather than Modified so no peer bytes are needed.
  4. Check the pi-iso version/upgrade notes: if walk_diff_blocking's signature changed, update call sites to supply the peer root parameter.

Example fix

// before: diffing only the upper layer
walk_diff_blocking(&upper, None /* peer_root */, |change| { ... });
// panic: modified change requires peer root

// after: supply the base layer
walk_diff_blocking(&upper, Some(&lower), |change| { ... });
Defensive patterns

Strategy: validation

Validate before calling

import * as fs from 'node:fs';
function assertPeerRoot(lowerPath) {
  if (!lowerPath || !fs.existsSync(lowerPath)) {
    throw new Error(`peer (lower) root must exist before diffing Modified entries: ${lowerPath}`);
  }
}

Type guard

function isConfiguredPeerRoot(peerRoot) {
  return typeof peerRoot === 'string' && peerRoot.length > 0;
}

Try / catch

try {
  await iso.diff({ upper, lower });
} catch (err) {
  if (err instanceof Error && err.message.includes('modified change requires peer root')) {
    console.error('Diff was run without a base layer; configure the lower root');
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: Calling walk_diff_blocking / the diff API such that an entry is classified ChangeKind::Modified while the peer (lower) root is None — typically diffing against a non-existent or unset lower layer, or a custom caller invoking plain_change directly with Modified and peer_root=None.

Common situations: Diffing an overlay/upper directory without configuring the lower (base) directory it was layered on; the peer root was deleted or moved between snapshot and diff; a version change where the diff API started accepting a single-root mode but Modified classification still assumes a peer.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/e95586f5549dc5da. Report an issue: GitHub.