feat(aura-runner, aura-cli): reproduce returns its report; mismatch guard replaces silent DIVERGED

reproduce_family returns Result<ReproduceReport, RunnerError> — divergence
is a result the embedding caller reads (all_identical()), never an error;
the CLI renders the per-member lines and summary byte-identically and owns
the exit (1 on divergence, no stderr line). The library's print block and
the empty-message refusal are gone; exit_on_runner_error always prints
(no empty producer remains).

reproduce_family_in gains the identity/pip guard, firing before any store
fetch: an instrument mismatch (incl. synthetic-for-real) and a broker/pip
mismatch — forward-built label compare against the stored broker string
(RunManifest carries no pip field; the mint's label constructors became
shared pub(crate) helpers, so guard and stamp cannot drift). The class is
context-borne: the explicit-source seam refuses 2 (caller-named source),
the derived simple path refuses 1 (data drift), same prose. A window
guard is deliberately absent — no reproduce path takes a caller window.
In-process tests pin the promise: divergence returns Ok, drift refuses
class 1, the host survives. Fork minutes: issues/299#issuecomment-4875,
-4877, -4879.

closes #299
This commit is contained in:
2026-07-26 19:23:44 +02:00
parent fd303de94d
commit 58d5345a75
4 changed files with 389 additions and 41 deletions
+161 -12
View File
@@ -1069,12 +1069,11 @@ pub(crate) fn exit_on_campaign_result(r: Result<usize, String>) {
}
}
/// The dispatch_reproduce pattern, shared: print the runner refusal and
/// exit with its class (#297). Empty message = no stderr line.
/// The shared runner-refusal arm: print the message and exit with its
/// class (#297). Every producer carries prose since #299 retired the
/// empty-message divergence refusal (divergence is report data now).
fn exit_on_runner_error(e: aura_runner::RunnerError) -> ! {
if !e.message.is_empty() {
eprintln!("aura: {}", e.message);
}
eprintln!("aura: {}", e.message);
std::process::exit(e.exit_code);
}
@@ -1467,13 +1466,24 @@ fn dispatch_runs(a: RunsCmd, env: &aura_runner::project::Env) {
}
fn dispatch_reproduce(a: ReproduceCmd, env: &aura_runner::project::Env) {
// `reproduce_family` (#295) returns a `RunnerError` instead of exiting
// the process itself; the shared helper prints its message (empty for
// the "not every member reproduced" refusal, since no stderr line
// accompanies it — only the stdout summary `reproduce_family` already
// emitted) and exits with its code.
if let Err(e) = aura_runner::reproduce::reproduce_family(&a.id, env) {
exit_on_runner_error(e);
// `reproduce_family` (#299) returns the report as a value — the library
// never prints and never exits. This arm renders it (byte-identical to
// the pre-#299 shell) and owns the exit: `exit(1)` when not every member
// reproduced, with NO accompanying stderr line (unchanged observable).
match aura_runner::reproduce::reproduce_family(&a.id, env) {
Ok(report) => {
let total = report.outcomes.len();
let ok = report.outcomes.iter().filter(|(_, b)| *b).count();
for (label, identical) in &report.outcomes {
let verdict = if *identical { "bit-identical" } else { "DIVERGED" };
println!("{} member {label} reproduced: {verdict}", a.id);
}
println!("reproduced {ok}/{total} members bit-identically");
if ok != total {
std::process::exit(1); // no stderr line — unchanged observable
}
}
Err(e) => exit_on_runner_error(e),
}
}
@@ -1933,6 +1943,24 @@ mod tests {
.expect("loads")
}
/// An `Env` whose registry resolves to `dir` (#299): the
/// project-facing surface `reproduce_family` reads via `env.registry()`,
/// built directly (no `Aura.toml` on disk) by pointing `paths.runs` at the
/// same absolute directory a test's own `Registry::open` uses — so seeding
/// through the raw `Registry` handle and reading back through
/// `reproduce_family`'s derived path see the identical `runs.jsonl`.
fn env_over(dir: &std::path::Path) -> aura_runner::project::Env {
aura_runner::project::Env::with_project(aura_runner::project::ProjectEnv {
root: std::path::PathBuf::from("."),
toml: aura_runner::project::AuraToml {
paths: aura_runner::project::AuraPaths { data: None, runs: Some(dir.to_path_buf()) },
nodes: Default::default(),
},
commit: None,
native: None,
})
}
fn cmp_member(key: &str, ts: &[i64], vals: &[f64]) -> FamilyMember {
cmp_member_win(key, ts, vals, (0, 0))
}
@@ -2302,6 +2330,127 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
/// #299: `reproduce_family` (the derived-source seam behind
/// plain `aura reproduce <id>`) survives a family whose STORED metrics were
/// hand-falsified after minting — it returns `Ok(report)` with
/// `all_identical() == false`, never a process exit or panic. The process
/// obviously survives: this test returns normally and inspects the value
/// `reproduce_family` handed back, not a captured exit code.
#[test]
fn reproduce_family_survives_a_falsified_stored_metric() {
let dir = std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/tmp"))
.join("aura-repro-falsified");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("temp dir");
let reg = Registry::open(dir.join("runs.jsonl"));
let env = aura_runner::project::Env::std();
let closed = load_closed_r_sma();
let doc = blueprint_to_json(&closed).expect("serializes");
let reload = || blueprint_from_json(&doc, &|t| std_vocabulary(t)).expect("loads");
let topo = test_topology_hash(&reload());
let space = blueprint_axis_probe(&doc, &env).param_space();
let data = DataSource::Synthetic;
let pip = data.pip_size();
let window = data.full_window(&env).expect("synthetic full_window never refuses");
let binding = aura_runner::binding::resolve_binding("falsified", reload().input_roles(), &BTreeMap::new())
.expect("the price role resolves");
let mut report = run_blueprint_member(
reload(),
&[],
&space,
data.run_sources(&env, &binding.columns()).expect("synthetic run_sources never refuses"),
window,
0,
pip,
&topo,
&env,
StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K },
&binding,
&[],
"",
)
.expect("no cost model: nothing to refuse");
// Hand-falsify the STORED metric after an honest mint: the record on
// disk now diverges from what a re-run actually produces — exactly the
// property `reproduce_family` must surface as data, not a process exit.
report.metrics.total_pips += 1.0;
let canonical = blueprint_to_json(&reload()).unwrap();
reg.put_blueprint(&topo, &canonical).expect("store blueprint");
let id = reg.append_family("falsified", FamilyKind::Sweep, &[report]).expect("append");
let rep = aura_runner::reproduce::reproduce_family(&id, &env_over(&dir))
.expect("a divergent member is a reported outcome, not a refusal");
assert!(!rep.all_identical(), "the falsified member must report DIVERGED: {:?}", rep.outcomes);
let _ = std::fs::remove_dir_all(&dir);
}
/// #299: the derived-source seam's identity/pip guard
/// (Fork 6) refuses class 1, NOT class 2, on `reproduce_family` — a
/// drifted stored broker label is data drift within the family (this
/// path's `data` is reconstructed FROM the manifest, never caller-given),
/// distinct from `reproduce_family_in`'s explicit-source class 2 (pinned
/// in `aura_runner::reproduce`'s own test module).
#[test]
fn reproduce_family_reports_the_derived_path_mismatch_as_class_one() {
let dir = std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/tmp"))
.join("aura-repro-derived-class");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("temp dir");
let reg = Registry::open(dir.join("runs.jsonl"));
let env = aura_runner::project::Env::std();
let closed = load_closed_r_sma();
let doc = blueprint_to_json(&closed).expect("serializes");
let reload = || blueprint_from_json(&doc, &|t| std_vocabulary(t)).expect("loads");
let topo = test_topology_hash(&reload());
let space = blueprint_axis_probe(&doc, &env).param_space();
let data = DataSource::Synthetic;
let pip = data.pip_size();
let window = data.full_window(&env).expect("synthetic full_window never refuses");
let binding = aura_runner::binding::resolve_binding("driftclass", reload().input_roles(), &BTreeMap::new())
.expect("the price role resolves");
let mut report = run_blueprint_member(
reload(),
&[],
&space,
data.run_sources(&env, &binding.columns()).expect("synthetic run_sources never refuses"),
window,
0,
pip,
&topo,
&env,
StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K },
&binding,
&[],
"",
)
.expect("no cost model: nothing to refuse");
// Drift the STORED broker label away from what this pip re-derives
// (mirrors `aura_runner::reproduce`'s own pip-mismatch guard fixture,
// but read back through the derived `reproduce_family` seam here).
report.manifest.broker = "sim-optimal(pip_size=0.0002)".to_string();
let canonical = blueprint_to_json(&reload()).unwrap();
reg.put_blueprint(&topo, &canonical).expect("store blueprint");
let id = reg.append_family("driftclass", FamilyKind::Sweep, &[report]).expect("append");
let err = aura_runner::reproduce::reproduce_family(&id, &env_over(&dir))
.expect_err("a drifted stored broker label must refuse, not silently reproduce");
assert_eq!(err.exit_code, 1, "derived-path mismatch is class 1, not class 2: {}", err.message);
assert!(
err.message.contains("reproduce broker/pip mismatch"),
"the refusal names the mismatch: {}",
err.message
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn reproduce_re_derives_the_member_stop_regime_not_the_default() {
// Property: reproduce re-runs each member under the SAME stop regime the member
+11 -3
View File
@@ -77,7 +77,7 @@ pub fn sim_optimal_manifest(
defaults: Vec::new(),
window,
seed,
broker: format!("sim-optimal(pip_size={pip_size})"),
broker: sim_optimal_broker_label(pip_size),
selection: None,
instrument: None,
topology_hash: None,
@@ -101,10 +101,18 @@ pub fn raw_bound_defaults(signal: &Composite) -> Vec<(String, Scalar)> {
signal.bound_param_space().into_iter().map(|b| (b.name, b.value)).collect()
}
/// The plain sim-optimal broker label (no RiskExecutor branch): shared by
/// `sim_optimal_manifest` and (#299) `reproduce_family_in`'s forward-built
/// guard comparison, so the two cannot drift.
pub(crate) fn sim_optimal_broker_label(pip_size: f64) -> String {
format!("sim-optimal(pip_size={pip_size})")
}
/// The honest broker label for the dual-tap r-sma harness: it runs a RiskExecutor
/// branch alongside the SimBroker, so the plain "sim-optimal" label would under-report
/// it (#132). Shared by the single run and the sweep so the two cannot drift.
fn r_sma_broker_label(pip_size: f64) -> String {
/// it (#132). Shared by the single run and the sweep (#299: and `reproduce_family_in`'s
/// forward-built guard comparison) so they cannot drift.
pub(crate) fn r_sma_broker_label(pip_size: f64) -> String {
format!("sim-optimal+risk-executor(pip_size={pip_size})")
}
+211 -25
View File
@@ -1,10 +1,13 @@
//! Bit-identical reproduction (`aura reproduce`) — #295.
//!
//! A library function here reports a refusal as a returned
//! [`crate::RunnerError`] rather than ending the process. The shell dispatch
//! arm (`aura-cli`'s `dispatch_reproduce`) is the single place that prints
//! the error's message to stderr and calls `std::process::exit` on its code,
//! keeping the observable stderr/exit bytes unchanged (C18).
//! Both outcomes here are values, not process control flow: success returns a
//! [`ReproduceReport`] the caller inspects (`ReproduceReport::all_identical`);
//! a refusal returns a [`crate::RunnerError`] — neither ends the process. The
//! shell dispatch arm (`aura-cli`'s `dispatch_reproduce`) is the single place
//! that renders the report and prints a refusal's message to stderr, calling
//! `std::process::exit` on its code, keeping the observable stderr/exit bytes
//! unchanged (C18); a library embedder (a World program) reads the returned
//! report directly instead.
use std::collections::BTreeMap;
use std::sync::mpsc;
@@ -23,10 +26,18 @@ use crate::RunnerError;
/// The outcome of reproducing one persisted family: per member, whether its re-run
/// metrics are bit-identical to the stored metrics (C1).
#[derive(Debug)]
pub struct ReproduceReport {
pub outcomes: Vec<(String, bool)>,
}
impl ReproduceReport {
/// True iff every member reproduced bit-identically (C1).
pub fn all_identical(&self) -> bool {
self.outcomes.iter().all(|(_, identical)| *identical)
}
}
/// Look up a persisted family by id, or refuse (exit code 1: unknown id / registry
/// load failure) — the single place `reproduce_family` and `reproduce_family_in`
/// resolve a family, so the two exit-1 error phrasings can't drift out of sync
@@ -68,11 +79,35 @@ pub fn load_family(reg: &Registry, id: &str) -> Result<Family, RunnerError> {
/// Re-derive every member of a persisted sweep family from the content-addressed store
/// and compare to the stored result, against an explicit registry (testable seam).
///
/// Refusal contract: an identity/pip mismatch (below) refuses class 2 — this is the
/// explicit-source seam, so a caller-given `data` that contradicts the member
/// manifests is argv-named content (C14). `reproduce_family`, the simple/derived
/// path, hits the same guard at class 1 instead (Fork 6, #299: on that path `data`
/// is derived FROM the family's own manifest, so a mismatch there is data drift, not
/// a caller error). A WINDOW guard is deliberately absent: no reproduce path takes a
/// caller window (WF and real-sweep re-runs load stored.manifest.window; MonteCarlo
/// derives its walk from manifest.seed; synthetic uses the full window).
pub fn reproduce_family_in(
reg: &Registry,
id: &str,
data: &DataSource,
env: &Env,
) -> Result<ReproduceReport, RunnerError> {
reproduce_family_with_class(reg, id, data, env, 2)
}
/// The shared reproduce loop (#299 Fork 6): `mismatch_class` is the exit code the
/// identity/pip guard refuses with — context-borne, not a property of the guard
/// itself. `reproduce_family_in` (explicit-source seam) passes 2; `reproduce_family`
/// (derived-source seam) passes 1. Guard messages are identical either way; only the
/// class varies.
fn reproduce_family_with_class(
reg: &Registry,
id: &str,
data: &DataSource,
env: &Env,
mismatch_class: i32,
) -> Result<ReproduceReport, RunnerError> {
let family = load_family(reg, id)?;
let pip = data.pip_size();
@@ -83,6 +118,52 @@ pub fn reproduce_family_in(
exit_code: 1,
message: "family member has no topology_hash; not a generated run".to_string(),
})?;
// Identity/geometry guard (#299): a source whose identity or pip
// contradicts the member manifests makes divergence mechanical. The
// class is context-borne (Fork 6, see this fn's doc): caller error on
// the explicit-source seam, data drift on the derived seam.
if let Some(recorded) = stored.manifest.instrument.as_deref() {
match data {
DataSource::Synthetic => {
return Err(RunnerError {
exit_code: mismatch_class,
message: format!(
"reproduce source mismatch: family member was recorded over instrument '{recorded}', the source is synthetic"
),
});
}
DataSource::Real { symbol, .. } if symbol != recorded => {
return Err(RunnerError {
exit_code: mismatch_class,
message: format!(
"reproduce source mismatch: family member was recorded over instrument '{recorded}', the source names '{symbol}'"
),
});
}
_ => {}
}
}
// pip has no manifest field of its own: forward-build the label the
// FAMILY MINT stamps for this pip (the r_sma/risk-executor variant —
// `run_blueprint_member` unconditionally stamps that label) and
// compare against the recorded one. The plain-arm branch below
// tolerates a stored record shaped like a single-run stamp (legacy or
// otherwise), not a variant any live re-run of THIS family would
// itself produce.
let expected = if stored.manifest.broker.contains("+risk-executor") {
crate::member::r_sma_broker_label(pip)
} else {
crate::member::sim_optimal_broker_label(pip)
};
if expected != stored.manifest.broker {
return Err(RunnerError {
exit_code: mismatch_class,
message: format!(
"reproduce broker/pip mismatch: manifest recorded '{}', the source's pip renders '{expected}'",
stored.manifest.broker
),
});
}
let doc = reg
.get_blueprint(&hash)
.map_err(|e| RunnerError { exit_code: 1, message: format!("{e}") })?
@@ -253,13 +334,17 @@ pub fn reproduce_family_in(
/// re-derive a real-data family over the wrong stream; see `reproduce_family_in`'s
/// per-member window loader for how the reconstructed source is actually used.
///
/// Prints its own per-member + summary lines to stdout (unchanged from the
/// pre-#295 shell function — this is the report's normal output, not an error
/// path); only the final "not every member reproduced" outcome is a refusal,
/// returned with an EMPTY message (there is no accompanying stderr line today,
/// so the shell dispatch arm must not synthesize one either — see
/// `dispatch_reproduce`).
pub fn reproduce_family(id: &str, env: &Env) -> Result<(), RunnerError> {
/// Returns the report as a value (#299): divergence is data the caller inspects
/// (`ReproduceReport::all_identical`), not a process exit — this library never
/// prints and never exits. The CLI's `dispatch_reproduce` renders the report and
/// owns the exit; a library embedder (a World program) reads `report.outcomes`
/// directly.
///
/// This is the derived-source seam (Fork 6, #299): `data` is reconstructed FROM
/// the family's own manifest, not caller-given, so the identity/pip guard inside
/// the shared loop refuses class 1 here (data drift within the stored family),
/// not class 2 (`reproduce_family_in`'s explicit-source class — see its doc).
pub fn reproduce_family(id: &str, env: &Env) -> Result<ReproduceReport, RunnerError> {
let reg = env.registry();
let family = load_family(&reg, id)?;
// Reconstruct the DataSource the family was minted over: `None` instrument
@@ -272,18 +357,7 @@ pub fn reproduce_family(id: &str, env: &Env) -> Result<(), RunnerError> {
DataSource::from_choice(DataChoice::Real { symbol, from_ms: None, to_ms: None }, env)?
}
};
let rep = reproduce_family_in(&reg, id, &data, env)?;
let total = rep.outcomes.len();
let ok = rep.outcomes.iter().filter(|(_, b)| *b).count();
for (label, identical) in &rep.outcomes {
let verdict = if *identical { "bit-identical" } else { "DIVERGED" };
println!("{id} member {label} reproduced: {verdict}");
}
println!("reproduced {ok}/{total} members bit-identically");
if ok != total {
return Err(RunnerError { exit_code: 1, message: String::new() });
}
Ok(())
reproduce_family_with_class(&reg, id, &data, env, 1)
}
#[cfg(test)]
@@ -293,7 +367,9 @@ mod tests {
use aura_engine::RunManifest;
use aura_registry::{FamilyKind, Registry};
use super::load_family;
use super::{load_family, reproduce_family_in, ReproduceReport};
use crate::family::DataSource;
use crate::project::Env;
/// A registry over a fresh per-test directory (the #258 tag-keyed pattern:
/// fixed name under the build-tree tmp anchor, pre-create wipe).
@@ -324,6 +400,116 @@ mod tests {
}
}
/// A persisted member carrying a `topology_hash` (#299 guard tests): the
/// identity/pip guards fire BEFORE the blueprint fetch, so a seeded record
/// needs a `topology_hash` to clear the earlier "not a generated run"
/// check, but the hash itself never resolves against the store.
fn stamped_report(instrument: Option<&str>, broker: &str) -> RunReport {
RunReport {
manifest: RunManifest {
topology_hash: Some("deadbeef".to_string()),
instrument: instrument.map(str::to_string),
broker: broker.to_string(),
..minimal_report().manifest
},
..minimal_report()
}
}
/// The instrument guard (#299): a family member recorded over a real
/// instrument refuses class 2 — before any blueprint fetch — when the
/// caller-given source is `Synthetic`, naming the recorded instrument in
/// the message.
#[test]
fn reproduce_family_in_refuses_synthetic_source_for_a_real_instrument_member() {
let reg = temp_registry("guard-synthetic-for-real");
reg.append_family(
"f",
FamilyKind::Sweep,
&[stamped_report(Some("GER40"), "sim-optimal(pip_size=0.0001)")],
)
.expect("append family");
let err = reproduce_family_in(&reg, "f", &DataSource::Synthetic, &Env::std())
.expect_err("a synthetic source must not silently reproduce a real-instrument member");
assert_eq!(err.exit_code, 2);
assert!(err.message.contains("GER40"), "message: {}", err.message);
assert!(err.message.contains("synthetic"), "message: {}", err.message);
}
/// The instrument guard's Real-vs-Real twin: a caller-given `Real` source
/// naming a DIFFERENT symbol than the member's recorded instrument refuses
/// class 2, naming both sides.
#[test]
fn reproduce_family_in_refuses_a_real_source_naming_the_wrong_symbol() {
let reg = temp_registry("guard-wrong-symbol");
reg.append_family(
"f",
FamilyKind::Sweep,
&[stamped_report(Some("GER40"), "sim-optimal(pip_size=0.0001)")],
)
.expect("append family");
// A hand-built `DataSource::Real` fixture: `DataServer::new` merely
// scans its base path (a no-op on a missing directory), so the guard
// — which never touches the server — is exercised without any real
// archive on disk.
let server = std::sync::Arc::new(data_server::DataServer::new("/nonexistent-aura-test-path"));
let data = DataSource::Real {
server,
symbol: "US500".to_string(),
from_ms: None,
to_ms: None,
pip: 1.0,
};
let err = reproduce_family_in(&reg, "f", &data, &Env::std())
.expect_err("a real source naming the wrong symbol must not silently reproduce");
assert_eq!(err.exit_code, 2);
assert!(err.message.contains("GER40"), "message: {}", err.message);
assert!(err.message.contains("US500"), "message: {}", err.message);
}
/// The broker/pip guard (#299): the recorded manifest carries no pip
/// field of its own — the pip survives only inside the broker label — so
/// the guard forward-builds the label the family mint stamps for this pip
/// (tolerating a plain-shape stored record generically, not asserting a
/// live re-run would itself produce it — #299) and refuses
/// class 2 on a label mismatch, naming both labels.
#[test]
fn reproduce_family_in_refuses_a_pip_mismatch_via_the_broker_label() {
let reg = temp_registry("guard-pip-mismatch");
// Recorded under a pip the actual synthetic run (0.0001, `SYNTHETIC_PIP_SIZE`)
// does not match: the label the re-run would forward-build diverges from
// the stored one, so the guard must fire before any re-run.
reg.append_family(
"f",
FamilyKind::Sweep,
&[stamped_report(None, "sim-optimal(pip_size=0.0002)")],
)
.expect("append family");
let err = reproduce_family_in(&reg, "f", &DataSource::Synthetic, &Env::std())
.expect_err("a pip mismatch forward-built via the broker label must refuse");
assert_eq!(err.exit_code, 2);
assert!(err.message.contains("sim-optimal(pip_size=0.0002)"), "message: {}", err.message);
assert!(err.message.contains("sim-optimal(pip_size=0.0001)"), "message: {}", err.message);
}
/// `all_identical` is true iff every outcome reproduced bit-identically.
#[test]
fn all_identical_reflects_every_outcome() {
let all_ok = ReproduceReport {
outcomes: vec![("a".to_string(), true), ("b".to_string(), true)],
};
assert!(all_ok.all_identical());
let one_diverged = ReproduceReport {
outcomes: vec![("a".to_string(), true), ("b".to_string(), false)],
};
assert!(!one_diverged.all_identical());
}
/// One id vocabulary across enumeration and reproduction (C18, #298): the
/// family-identity string a consumer lifts off the registry's own member
/// enumeration (`FamilyRunRecord.family`) resolves through the reproduce
+6 -1
View File
@@ -857,7 +857,12 @@ error; only its environment refusals (a tap-trace store write failure)
exit 1. The no-data family (no local data, no data in the requested window,
no recorded geometry) belongs to the real-data verbs — `aura run`,
`reproduce`, the campaign legs — where it likewise exits 1 (#297; the full
partition lives in [C14](design/contracts/c14-headless-two-faces.md)).
partition lives in [C14](design/contracts/c14-headless-two-faces.md)). `aura
reproduce`'s identity/pip guard (a stored member's recorded instrument or
broker/pip label contradicting the source it re-derives against) is
likewise class 1 on the plain verb — its source is derived from the
family's own manifest, so a mismatch there is data drift, not a caller
error (Fork 6, #299).
The class boundary is FORM vs VALUE, not "environment/data/IO vs everything
else": an `--override` value that is well-formed but out of the node's own