{"record":{"id":"e95586f5549dc5da","repo":"can1357/oh-my-pi","slug":"modified-change-requires-peer-root","errorCode":null,"errorMessage":"modified change requires peer root","messagePattern":"modified change requires peer root","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/pi-iso/src/diff.rs","lineNumber":366,"sourceCode":"/// `op == Modified` requires `peer_root = Some(lower)` so we can read the\n/// counterpart; `Added`/`Removed` only need the side we already know about.\nfn plain_change(\n\tside: &Path,\n\trel: &Path,\n\top: ChangeKind,\n\tpeer_root: Option<&Path>,\n) -> IsoResult<FileChange> {\n\tlet full = side.join(rel);\n\tlet primary = std::fs::read(&full)\n\t\t.map_err(|err| IsoError::other(format!(\"read {}: {err}\", full.display())))?;\n\tif looks_binary(&primary) {\n\t\treturn Ok(FileChange { path: rel.to_path_buf(), op, diff: None });\n\t}\n\tlet (old_bytes, new_bytes) = match op {\n\t\tChangeKind::Added => (Vec::new(), primary),\n\t\tChangeKind::Removed => (primary, Vec::new()),\n\t\tChangeKind::Modified => {\n\t\t\tlet peer = peer_root.expect(\"modified change requires peer root\");\n\t\t\tlet peer_full = peer.join(rel);\n\t\t\tlet peer_bytes = std::fs::read(&peer_full)\n\t\t\t\t.map_err(|err| IsoError::other(format!(\"read {}: {err}\", peer_full.display())))?;\n\t\t\tif looks_binary(&peer_bytes) {\n\t\t\t\treturn Ok(FileChange { path: rel.to_path_buf(), op, diff: None });\n\t\t\t}\n\t\t\t(peer_bytes, primary)\n\t\t},\n\t};\n\tlet (Ok(old_text), Ok(new_text)) =\n\t\t(std::str::from_utf8(&old_bytes), std::str::from_utf8(&new_bytes))\n\telse {\n\t\treturn Ok(FileChange { path: rel.to_path_buf(), op, diff: None });\n\t};\n\tOk(FileChange {\n\t\tpath: rel.to_path_buf(),\n\t\top,\n\t\tdiff: Some(render_unified(rel, op, old_text, new_text)),","sourceCodeStart":348,"sourceCodeEnd":384,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/crates/pi-iso/src/diff.rs#L348-L384","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure the diff is always invoked with both roots configured — pass the lower/base directory as peer_root whenever a Modified change is possible.","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).","If diffing a standalone tree is intended, treat every entry as Added rather than Modified so no peer bytes are needed.","Check the pi-iso version/upgrade notes: if walk_diff_blocking's signature changed, update call sites to supply the peer root parameter."],"exampleFix":"// before: diffing only the upper layer\nwalk_diff_blocking(&upper, None /* peer_root */, |change| { ... });\n// panic: modified change requires peer root\n\n// after: supply the base layer\nwalk_diff_blocking(&upper, Some(&lower), |change| { ... });","handlingStrategy":"validation","validationCode":"import * as fs from 'node:fs';\nfunction assertPeerRoot(lowerPath) {\n  if (!lowerPath || !fs.existsSync(lowerPath)) {\n    throw new Error(`peer (lower) root must exist before diffing Modified entries: ${lowerPath}`);\n  }\n}","typeGuard":"function isConfiguredPeerRoot(peerRoot) {\n  return typeof peerRoot === 'string' && peerRoot.length > 0;\n}","tryCatchPattern":"try {\n  await iso.diff({ upper, lower });\n} catch (err) {\n  if (err instanceof Error && err.message.includes('modified change requires peer root')) {\n    console.error('Diff was run without a base layer; configure the lower root');\n  } else {\n    throw err;\n  }\n}","preventionTips":["Always pass both upper and lower roots when calling the iso diff API; never diff an overlay without its base.","Validate that the lower root exists and is readable before diffing.","Keep the base layer stable for the lifetime of the overlay — don't delete or move it between writes and diffs."],"tags":["diff","invariant-violation","panic","filesystem"],"backgroundTag":"missing-peer-root","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}