feat(project): the project-as-crate load boundary (cycle 0102)
A research project is now a loadable external cdylib crate. Inside a directory whose ancestry holds an Aura.toml, aura discovers the project root cargo-style, locates the compiled dylib via cargo metadata (debug default, --release opt-in), loads it load-and-hold, and refuses mismatches before trusting anything: the AURA_PROJECT descriptor (aura-core::project, #[repr(C)]) carries a C-ABI stamp prefix (rustc + aura-core version, baked per consuming build by the new aura-core build.rs) validated before any Rust-ABI field is read. The vocabulary charter gates the merged resolution: project type ids are ::-namespaced (std stays bare), duplicates refuse, and the enumerable type-id list must agree with the resolver, so introspection can never silently omit a project type. All blueprint verbs resolve through the merged project + std vocabulary via a per-invocation Env threaded through the dispatch chains; registry, trace-store, and data paths anchor at the project runs root (Aura.toml [paths], paths-only by design — instrument geometry stays the recorded sidecar, C15). RunManifest gains the Tier-1 project provenance field (namespace + dylib sha256 + best-effort commit), stamped beside topology_hash on the blueprint-run paths; pre-0102 registry lines load unchanged. Default node names strip the namespace, so :: never reaches the param-path address space. Proven by the demo-project fixture (built by the e2e via cargo, path-dep on this workspace): run twice bit-identical, provenance recorded, introspection lists demo::* beside std, registry anchors at the discovered root from a subdirectory; the badcharter fixture proves the charter refusal through the real libloading path; a never-built project refuses with a cargo-build hint. Outside a project every path collapses to the previous literals — goldens and manifest pins byte-identical. Verification: cargo build --workspace clean; cargo test --workspace 862 passed / 0 failed (incl. 7 project_load e2e); clippy -D warnings clean (one precedent-matching allow(too_many_arguments) on run_oos_blueprint, whose arity the Env threading raised to 8); doc build unchanged. Docs/ledger aligned: Aura.toml field lists are paths-only in project-layout.md, glossary, C16/C17; new C13 realization note records the per-invocation-reload reading and the load-and-hold one-shot scope boundary. New deps, per-case review (aura-cli leaf binary only, never the frozen artifact): libloading, toml. refs #180
This commit is contained in:
@@ -1003,6 +1003,7 @@ mod tests {
|
||||
selection: None,
|
||||
instrument: None,
|
||||
topology_hash: None,
|
||||
project: None,
|
||||
},
|
||||
metrics: summarize(&equity, &exposure),
|
||||
}
|
||||
|
||||
@@ -71,7 +71,8 @@ pub use harness::{
|
||||
pub use report::{
|
||||
derive_position_events, expected_max_of_normals, f64_field, inv_norm_cdf, join_on_ts,
|
||||
r_metrics_from_rs, summarize, summarize_r, ColumnarTrace, FamilySelection, JoinedRow,
|
||||
PositionAction, PositionEvent, RMetrics, RunManifest, RunMetrics, RunReport, SelectionMode,
|
||||
PositionAction, PositionEvent, ProjectProvenance, RMetrics, RunManifest, RunMetrics,
|
||||
RunReport, SelectionMode,
|
||||
};
|
||||
pub use sweep::{
|
||||
sweep, GridSpace, ParamRange, RandomSpace, Space, SweepError, SweepFamily, SweepPoint,
|
||||
|
||||
@@ -245,6 +245,7 @@ mod tests {
|
||||
selection: None,
|
||||
instrument: None,
|
||||
topology_hash: None,
|
||||
project: None,
|
||||
},
|
||||
metrics: summarize(&equity, &exposure),
|
||||
}
|
||||
@@ -271,6 +272,7 @@ mod tests {
|
||||
selection: None,
|
||||
instrument: None,
|
||||
topology_hash: None,
|
||||
project: None,
|
||||
},
|
||||
metrics: RunMetrics {
|
||||
total_pips: v,
|
||||
|
||||
@@ -57,6 +57,23 @@ pub struct RunManifest {
|
||||
/// serde widening (Tier-1, #156), identical idiom to `selection` / `instrument`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub topology_hash: Option<String>,
|
||||
/// Identity of the loaded project dylib, when the run used one (cycle
|
||||
/// 0102). Pre-0102 records read as `None`; `None` serializes as an absent
|
||||
/// key (the same one-directional widening as `instrument`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub project: Option<ProjectProvenance>,
|
||||
}
|
||||
|
||||
/// Provenance of the project cdylib a run was resolved through (C16/C18).
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ProjectProvenance {
|
||||
/// The project's vocabulary namespace (from the descriptor).
|
||||
pub namespace: String,
|
||||
/// SHA-256 (hex) of the loaded dylib file's bytes.
|
||||
pub dylib_sha256: String,
|
||||
/// The project repo's HEAD (+ `-dirty`), when derivable.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub commit: Option<String>,
|
||||
}
|
||||
|
||||
/// A run's full structured result: the descriptor plus the metrics it
|
||||
@@ -280,6 +297,7 @@ mod tests {
|
||||
commit: "c".into(), params: vec![], window: (Timestamp(0), Timestamp(0)),
|
||||
seed: 0, broker: "b".into(), selection: None, instrument: None,
|
||||
topology_hash: None,
|
||||
project: None,
|
||||
};
|
||||
assert!(!serde_json::to_string(&m).unwrap().contains("selection"));
|
||||
}
|
||||
@@ -298,6 +316,7 @@ mod tests {
|
||||
commit: "c".into(), params: vec![], window: (Timestamp(0), Timestamp(0)),
|
||||
seed: 0, broker: "b".into(), selection: None, instrument: Some("GER40".into()),
|
||||
topology_hash: None,
|
||||
project: None,
|
||||
};
|
||||
let json = serde_json::to_string(&with).unwrap();
|
||||
assert!(json.contains("\"instrument\":\"GER40\""), "json: {json}");
|
||||
@@ -308,11 +327,33 @@ mod tests {
|
||||
commit: "c".into(), params: vec![], window: (Timestamp(0), Timestamp(0)),
|
||||
seed: 0, broker: "b".into(), selection: None, instrument: None,
|
||||
topology_hash: None,
|
||||
project: None,
|
||||
};
|
||||
// skip_serializing_if omits the key when None -> existing manifest bytes unchanged (C23).
|
||||
assert!(!serde_json::to_string(&without).unwrap().contains("instrument"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runmanifest_project_field_round_trips_and_omits_when_none() {
|
||||
// A pre-0102 line (no "project" key) must load with project: None.
|
||||
let legacy = r#"{"commit":"c","params":[],"window":[0,1],"seed":7,"broker":"b"}"#;
|
||||
let m: RunManifest = serde_json::from_str(legacy).expect("legacy line loads");
|
||||
assert_eq!(m.project, None);
|
||||
// None must serialize with the key absent (byte-stability of old paths).
|
||||
let out = serde_json::to_string(&m).expect("serializes");
|
||||
assert!(!out.contains("\"project\""));
|
||||
// Some(..) must round-trip.
|
||||
let p = ProjectProvenance {
|
||||
namespace: "demo".into(),
|
||||
dylib_sha256: "ab".repeat(32),
|
||||
commit: None,
|
||||
};
|
||||
let m2 = RunManifest { project: Some(p.clone()), ..m };
|
||||
let s = serde_json::to_string(&m2).expect("serializes");
|
||||
let back: RunManifest = serde_json::from_str(&s).expect("round-trips");
|
||||
assert_eq!(back.project, Some(p));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn family_selection_round_trips_on_the_manifest() {
|
||||
let sel = FamilySelection {
|
||||
@@ -326,6 +367,7 @@ mod tests {
|
||||
commit: "c".into(), params: vec![], window: (Timestamp(0), Timestamp(0)),
|
||||
seed: 0, broker: "b".into(), selection: Some(sel.clone()), instrument: None,
|
||||
topology_hash: None,
|
||||
project: None,
|
||||
};
|
||||
let back: RunManifest = serde_json::from_str(&serde_json::to_string(&m).unwrap()).unwrap();
|
||||
assert_eq!(back.selection, Some(sel));
|
||||
@@ -466,6 +508,7 @@ mod tests {
|
||||
selection: None,
|
||||
instrument: None,
|
||||
topology_hash: None,
|
||||
project: None,
|
||||
},
|
||||
metrics,
|
||||
}
|
||||
@@ -571,6 +614,7 @@ mod tests {
|
||||
selection: None,
|
||||
instrument: None,
|
||||
topology_hash: None,
|
||||
project: None,
|
||||
},
|
||||
metrics: RunMetrics {
|
||||
total_pips: 12.0,
|
||||
@@ -602,6 +646,7 @@ mod tests {
|
||||
selection: None,
|
||||
instrument: None,
|
||||
topology_hash: None,
|
||||
project: None,
|
||||
},
|
||||
metrics: RunMetrics { total_pips: 12.0, max_drawdown: 1.0, bias_sign_flips: 1, r: None },
|
||||
};
|
||||
@@ -625,6 +670,7 @@ mod tests {
|
||||
selection: None,
|
||||
instrument: None,
|
||||
topology_hash: None,
|
||||
project: None,
|
||||
},
|
||||
metrics: RunMetrics { total_pips: 12.0, max_drawdown: 1.0, bias_sign_flips: 1, r: None },
|
||||
};
|
||||
|
||||
@@ -670,6 +670,7 @@ mod tests {
|
||||
selection: None,
|
||||
instrument: None,
|
||||
topology_hash: None,
|
||||
project: None,
|
||||
},
|
||||
metrics: summarize(&equity, &exposure),
|
||||
}
|
||||
|
||||
@@ -294,6 +294,7 @@ mod tests {
|
||||
selection: None,
|
||||
instrument: None,
|
||||
topology_hash: None,
|
||||
project: None,
|
||||
},
|
||||
metrics: summarize(&[], &[]),
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@ fn run_point(point: &[Cell]) -> RunReport {
|
||||
selection: None,
|
||||
instrument: None,
|
||||
topology_hash: None,
|
||||
project: None,
|
||||
},
|
||||
metrics: summarize(&equity, &exposure),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user