feat(research): 0106 tasks 1-9 — document layer, content-id move, stores + referential tier
The aura-research leaf crate: ProcessDoc/CampaignDoc with strict parsing (hand-rolled StageBlock deserializer — deny_unknown_fields does not compose with internally-tagged enums; Axis declares its ScalarKind once with bare values, per the #189 user veto), canonical form + content_id_of (the SHA-256 primitive moved from aura-cli, which now delegates — the id pin set incl. the independent sweep-store recompute stayed green untouched), intrinsic validation for both document types (P1 constraints structural), and the introspection contract (schema tables single-source parse strictness AND vocabulary/describe/open-slots — the Blockly litmus made checkable). aura-registry: content-addressed processes/ + campaigns/ stores on the blueprint-store pattern (Ok(None) treat-as-empty) and the referential tier (validate_campaign_refs: process/strategy refs incl. identity-scan, axis-name + declared-kind checks against param_space). Plan deviations, verified by hand and folded back into the plan file: - Task 9's fixture could not use aura-composites as planned: every shipped composite with an open param routes through LinComb, which the zero-arg std_vocabulary roster deliberately excludes, so none round-trips through blueprint_from_json with the by-type-name resolver. The test hand-builds a minimal Bias composite instead (generic over param_space, as planned); the unused pro-forma aura-composites dev-dep from the repair pass is dropped. This is why the loop reported task 9 BLOCKED (review-loop-exhausted on the literal code block); the tree itself is complete and green. - A plan-verbatim test assertion false-matched ("deflate" as substring of "deflated-positive"); tightened to the JSON key. - OpenSlot doc comment backticked (rustdoc read strategies[0] as a link; doc gate back to 0 warnings). Verification: cargo test --workspace 898/0; clippy -D warnings clean; cargo doc --no-deps 0 warnings. Tasks 10-12 (CLI verb families, final gates) follow. refs #189
This commit is contained in:
Generated
+13
@@ -114,6 +114,7 @@ dependencies = [
|
|||||||
"aura-engine",
|
"aura-engine",
|
||||||
"aura-ingest",
|
"aura-ingest",
|
||||||
"aura-registry",
|
"aura-registry",
|
||||||
|
"aura-research",
|
||||||
"aura-std",
|
"aura-std",
|
||||||
"clap",
|
"clap",
|
||||||
"data-server",
|
"data-server",
|
||||||
@@ -174,10 +175,22 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"aura-core",
|
"aura-core",
|
||||||
"aura-engine",
|
"aura-engine",
|
||||||
|
"aura-research",
|
||||||
|
"aura-std",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aura-research"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"aura-core",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aura-std"
|
name = "aura-std"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ members = [
|
|||||||
"crates/aura-cli",
|
"crates/aura-cli",
|
||||||
"crates/aura-ingest",
|
"crates/aura-ingest",
|
||||||
"crates/aura-registry",
|
"crates/aura-registry",
|
||||||
|
"crates/aura-research",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ aura-engine = { path = "../aura-engine" }
|
|||||||
# r-sma harness wires (kept out of aura-engine so the engine stays domain-free).
|
# r-sma harness wires (kept out of aura-engine so the engine stays domain-free).
|
||||||
aura-composites = { path = "../aura-composites" }
|
aura-composites = { path = "../aura-composites" }
|
||||||
aura-registry = { path = "../aura-registry" }
|
aura-registry = { path = "../aura-registry" }
|
||||||
|
aura-research = { path = "../aura-research" }
|
||||||
aura-std = { path = "../aura-std" }
|
aura-std = { path = "../aura-std" }
|
||||||
aura-ingest = { path = "../aura-ingest" }
|
aura-ingest = { path = "../aura-ingest" }
|
||||||
# data-server: the local M1 archive `aura run --real` streams from. Mirrors the
|
# data-server: the local M1 archive `aura run --real` streams from. Mirrors the
|
||||||
|
|||||||
@@ -2585,9 +2585,7 @@ fn sma_signal(fast_len: Option<i64>, slow_len: Option<i64>) -> Composite {
|
|||||||
/// two surfaces agree by construction over the same canonical bytes. Research-side
|
/// two surfaces agree by construction over the same canonical bytes. Research-side
|
||||||
/// (aura-cli), off the frozen engine (invariant 8).
|
/// (aura-cli), off the frozen engine (invariant 8).
|
||||||
fn content_id(canonical_json: &str) -> String {
|
fn content_id(canonical_json: &str) -> String {
|
||||||
use sha2::{Digest, Sha256};
|
aura_research::content_id_of(canonical_json)
|
||||||
let digest = Sha256::digest(canonical_json.as_bytes());
|
|
||||||
digest.iter().map(|b| format!("{b:02x}")).collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// SHA256 (hex) of the canonical (#164, no-trailing-newline) serialization of a
|
/// SHA256 (hex) of the canonical (#164, no-trailing-newline) serialization of a
|
||||||
|
|||||||
@@ -10,11 +10,17 @@ publish.workspace = true
|
|||||||
# back (admitted under the amended C16 per-case policy, INDEX.md). RunReport
|
# back (admitted under the amended C16 per-case policy, INDEX.md). RunReport
|
||||||
# derives serde in aura-engine.
|
# derives serde in aura-engine.
|
||||||
aura-engine = { path = "../aura-engine" }
|
aura-engine = { path = "../aura-engine" }
|
||||||
|
# the document stores' content-id primitive (put_process/put_campaign key on
|
||||||
|
# aura_research::content_id_of, the same hash the doc types canonicalize to);
|
||||||
|
# also PrimitiveBuilder, the referential tier's resolver signature.
|
||||||
|
aura-core = { path = "../aura-core" }
|
||||||
|
aura-research = { path = "../aura-research" }
|
||||||
# the lineage record types (FamilyKind / FamilyRunRecord) derive serde; admitted
|
# the lineage record types (FamilyKind / FamilyRunRecord) derive serde; admitted
|
||||||
# under the same per-case policy as serde_json (INDEX.md).
|
# under the same per-case policy as serde_json (INDEX.md).
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
# fixtures construct RunReport/RunManifest literals, whose window needs Timestamp
|
# the referential-tier test's generic param-space fixture (a real vocabulary
|
||||||
aura-core = { path = "../aura-core" }
|
# resolver + a zero-arg node, not hand-rolled).
|
||||||
|
aura-std = { path = "../aura-std" }
|
||||||
|
|||||||
@@ -17,10 +17,13 @@ use std::fs;
|
|||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use aura_core::PrimitiveBuilder;
|
||||||
use aura_engine::{
|
use aura_engine::{
|
||||||
expected_max_of_normals, r_metrics_from_rs, resample_block, FamilySelection, MetricStats,
|
blueprint_from_json, blueprint_identity_json, expected_max_of_normals, r_metrics_from_rs,
|
||||||
RunReport, SelectionMode, SplitMix64, SweepFamily, SweepPoint,
|
resample_block, FamilySelection, MetricStats, RunReport, SelectionMode, SplitMix64,
|
||||||
|
SweepFamily, SweepPoint,
|
||||||
};
|
};
|
||||||
|
use aura_research::{CampaignDoc, DocRef};
|
||||||
|
|
||||||
mod compat;
|
mod compat;
|
||||||
|
|
||||||
@@ -131,6 +134,161 @@ impl Registry {
|
|||||||
Err(e) => Err(RegistryError::Io(e)),
|
Err(e) => Err(RegistryError::Io(e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The content-addressed document-store dir for one document kind —
|
||||||
|
/// a sibling of the runs store, like `blueprints/`.
|
||||||
|
fn doc_dir(&self, kind: &str) -> PathBuf {
|
||||||
|
self.path.with_file_name(kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The single id→path mapping every document put/get routes through
|
||||||
|
/// (the `blueprint_path` write-one/read-one lockstep discipline).
|
||||||
|
fn doc_path(&self, kind: &str, content_id: &str) -> PathBuf {
|
||||||
|
self.doc_dir(kind).join(format!("{content_id}.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Content-addressed put: computes the content id from the canonical
|
||||||
|
/// bytes (unlike `put_blueprint`, whose caller owns the hash — document
|
||||||
|
/// callers always hold canonical bytes, so self-keying is safe here)
|
||||||
|
/// and writes `<kind>/<id>.json`. Idempotent.
|
||||||
|
fn put_doc(&self, kind: &str, canonical_json: &str) -> Result<String, RegistryError> {
|
||||||
|
let id = aura_research::content_id_of(canonical_json);
|
||||||
|
fs::create_dir_all(self.doc_dir(kind))?;
|
||||||
|
fs::write(self.doc_path(kind, &id), canonical_json)?;
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a stored document by content id; `Ok(None)` if absent (the
|
||||||
|
/// `get_blueprint` treat-as-empty discipline).
|
||||||
|
fn get_doc(&self, kind: &str, content_id: &str) -> Result<Option<String>, RegistryError> {
|
||||||
|
match fs::read_to_string(self.doc_path(kind, content_id)) {
|
||||||
|
Ok(s) => Ok(Some(s)),
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||||
|
Err(e) => Err(RegistryError::Io(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store a canonical process document; returns its content id.
|
||||||
|
pub fn put_process(&self, canonical_json: &str) -> Result<String, RegistryError> {
|
||||||
|
self.put_doc("processes", canonical_json)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load a stored process document by content id (`Ok(None)` if absent).
|
||||||
|
pub fn get_process(&self, content_id: &str) -> Result<Option<String>, RegistryError> {
|
||||||
|
self.get_doc("processes", content_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store a canonical campaign document; returns its content id.
|
||||||
|
pub fn put_campaign(&self, canonical_json: &str) -> Result<String, RegistryError> {
|
||||||
|
self.put_doc("campaigns", canonical_json)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load a stored campaign document by content id (`Ok(None)` if absent).
|
||||||
|
pub fn get_campaign(&self, content_id: &str) -> Result<Option<String>, RegistryError> {
|
||||||
|
self.get_doc("campaigns", content_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Referential-validation findings for a campaign document. By-identifier
|
||||||
|
/// and Display-free (the CLI phrases them).
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum RefFault {
|
||||||
|
ProcessNotFound(String),
|
||||||
|
StrategyNotFound(String),
|
||||||
|
IdentityUnmatched(String),
|
||||||
|
StrategyUnloadable { id: String, error: String },
|
||||||
|
AxisNotInParamSpace { strategy: String, axis: String },
|
||||||
|
AxisKindMismatch { strategy: String, axis: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Registry {
|
||||||
|
/// Referential tier: resolve the campaign's references against the
|
||||||
|
/// project's stores and check each axis (name AND declared kind) against
|
||||||
|
/// the referenced strategy's param space. IO faults are Errors; semantic
|
||||||
|
/// findings are RefFaults.
|
||||||
|
pub fn validate_campaign_refs(
|
||||||
|
&self,
|
||||||
|
doc: &CampaignDoc,
|
||||||
|
resolve: &dyn Fn(&str) -> Option<PrimitiveBuilder>,
|
||||||
|
) -> Result<Vec<RefFault>, RegistryError> {
|
||||||
|
let mut faults = Vec::new();
|
||||||
|
|
||||||
|
if let DocRef::ContentId(id) = &doc.process.r#ref
|
||||||
|
&& self.get_process(id)?.is_none()
|
||||||
|
{
|
||||||
|
faults.push(RefFault::ProcessNotFound(id.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
for entry in &doc.strategies {
|
||||||
|
let (label, blueprint_json) = match &entry.r#ref {
|
||||||
|
DocRef::ContentId(id) => match self.get_blueprint(id)? {
|
||||||
|
Some(json) => (id.clone(), Some(json)),
|
||||||
|
None => {
|
||||||
|
faults.push(RefFault::StrategyNotFound(id.clone()));
|
||||||
|
(id.clone(), None)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DocRef::IdentityId(id) => match self.find_blueprint_by_identity(id, resolve)? {
|
||||||
|
Some(json) => (id.clone(), Some(json)),
|
||||||
|
None => {
|
||||||
|
faults.push(RefFault::IdentityUnmatched(id.clone()));
|
||||||
|
(id.clone(), None)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let Some(json) = blueprint_json else { continue };
|
||||||
|
let composite = match blueprint_from_json(&json, resolve) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
faults.push(RefFault::StrategyUnloadable {
|
||||||
|
id: label.clone(),
|
||||||
|
error: format!("{e:?}"),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let space = composite.param_space();
|
||||||
|
for (axis, ax) in &entry.axes {
|
||||||
|
match space.iter().find(|p| &p.name == axis) {
|
||||||
|
None => faults.push(RefFault::AxisNotInParamSpace {
|
||||||
|
strategy: label.clone(),
|
||||||
|
axis: axis.clone(),
|
||||||
|
}),
|
||||||
|
Some(spec) => {
|
||||||
|
if ax.kind != spec.kind {
|
||||||
|
faults.push(RefFault::AxisKindMismatch {
|
||||||
|
strategy: label.clone(),
|
||||||
|
axis: axis.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(faults)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scan the blueprint store for a blueprint whose identity id matches.
|
||||||
|
fn find_blueprint_by_identity(
|
||||||
|
&self,
|
||||||
|
identity_id: &str,
|
||||||
|
resolve: &dyn Fn(&str) -> Option<PrimitiveBuilder>,
|
||||||
|
) -> Result<Option<String>, RegistryError> {
|
||||||
|
let entries = match fs::read_dir(self.blueprints_dir()) {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(_) => return Ok(None), // no store yet -> nothing matches
|
||||||
|
};
|
||||||
|
for entry in entries {
|
||||||
|
let entry = entry?;
|
||||||
|
let json = fs::read_to_string(entry.path())?;
|
||||||
|
let Ok(composite) = blueprint_from_json(&json, resolve) else { continue };
|
||||||
|
let Ok(identity_json) = blueprint_identity_json(&composite) else { continue };
|
||||||
|
if aura_research::content_id_of(&identity_json) == identity_id {
|
||||||
|
return Ok(Some(json));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Which metric a best-first comparison keys on. Resolves a metric *name* (a
|
/// Which metric a best-first comparison keys on. Resolves a metric *name* (a
|
||||||
@@ -711,6 +869,21 @@ mod tests {
|
|||||||
assert_eq!(reg.get_blueprint("never-written").expect("get"), None);
|
assert_eq!(reg.get_blueprint("never-written").expect("get"), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn document_stores_round_trip_by_content_id() {
|
||||||
|
let reg = Registry::open(temp_family_dir("document_store_round_trip"));
|
||||||
|
let process = r#"{"format_version":1,"kind":"process","name":"p","pipeline":[{"block":"std::generalize","metric":"sqn"}]}"#;
|
||||||
|
let campaign = r#"{"format_version":1,"kind":"campaign","name":"c"}"#;
|
||||||
|
let pid = reg.put_process(process).expect("put process");
|
||||||
|
assert_eq!(pid, aura_research::content_id_of(process));
|
||||||
|
assert_eq!(reg.get_process(&pid).expect("get process"), Some(process.to_string()));
|
||||||
|
let cid = reg.put_campaign(campaign).expect("put campaign");
|
||||||
|
assert_eq!(reg.get_campaign(&cid).expect("get campaign"), Some(campaign.to_string()));
|
||||||
|
assert_ne!(pid, cid);
|
||||||
|
// absent id is Ok(None), not an error (treat-as-empty discipline)
|
||||||
|
assert_eq!(reg.get_process("never-written").expect("get"), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn corrupt_line_is_a_parse_error_with_line_number() {
|
fn corrupt_line_is_a_parse_error_with_line_number() {
|
||||||
let path = temp_path("corrupt");
|
let path = temp_path("corrupt");
|
||||||
@@ -1291,6 +1464,121 @@ mod tests {
|
|||||||
assert_eq!(g1, g2, "a pure fold must be deterministic (C1)");
|
assert_eq!(g1, g2, "a pure fold must be deterministic (C1)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Property: `validate_campaign_refs` resolves campaign document
|
||||||
|
/// references (content-id and identity-id) against the registry's
|
||||||
|
/// stores and cross-checks each declared tuning axis (name AND kind)
|
||||||
|
/// against the resolved strategy's real, generic param space — not a
|
||||||
|
/// hardcoded param name. Faults are reported per finding kind
|
||||||
|
/// (ProcessNotFound / StrategyNotFound / AxisNotInParamSpace /
|
||||||
|
/// AxisKindMismatch), and a fully-resolved, kind-correct document
|
||||||
|
/// yields no faults at all.
|
||||||
|
#[test]
|
||||||
|
fn referential_tier_resolves_refs_and_checks_axes() {
|
||||||
|
use aura_engine::{blueprint_to_json, Composite};
|
||||||
|
use aura_research::{Axis, DocRef};
|
||||||
|
|
||||||
|
let reg = Registry::open(temp_family_dir("referential_tier"));
|
||||||
|
let resolve = |t: &str| aura_std::std_vocabulary(t);
|
||||||
|
|
||||||
|
// A minimal fixture composite with one OPEN param, built from a
|
||||||
|
// zero-arg `aura-std` node (`Bias`) so `std_vocabulary` can actually
|
||||||
|
// resolve it on load. `aura-composites`' shipped composites (vol_stop,
|
||||||
|
// risk_executor*) all route through `LinComb`, whose builder needs a
|
||||||
|
// structural arity argument and is therefore deliberately absent from
|
||||||
|
// the zero-arg `std_vocabulary` roster (see aura-std/src/vocabulary.rs)
|
||||||
|
// — they cannot round-trip through `blueprint_from_json` with this
|
||||||
|
// resolver, so this fixture stands in as the "real, open-param,
|
||||||
|
// vocabulary-loadable composite" the test needs.
|
||||||
|
let composite = Composite::new(
|
||||||
|
"fixture",
|
||||||
|
vec![aura_std::Bias::builder().named("b").into()],
|
||||||
|
vec![],
|
||||||
|
vec![],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
let space = composite.param_space();
|
||||||
|
let real = space.first().expect("fixture composite has an open param");
|
||||||
|
let real_name = real.name.clone();
|
||||||
|
let real_kind = real.kind;
|
||||||
|
|
||||||
|
let blueprint_json = blueprint_to_json(&composite).expect("serializes");
|
||||||
|
let bp_id = aura_research::content_id_of(&blueprint_json);
|
||||||
|
reg.put_blueprint(&bp_id, &blueprint_json).expect("seed blueprint");
|
||||||
|
let process = r#"{"format_version":1,"kind":"process","name":"p","pipeline":[{"block":"std::generalize","metric":"sqn"}]}"#;
|
||||||
|
let proc_id = reg.put_process(process).expect("seed process");
|
||||||
|
|
||||||
|
// one kind-correct bare value for the real param
|
||||||
|
let bare = match real_kind {
|
||||||
|
aura_core::ScalarKind::I64 => "4",
|
||||||
|
aura_core::ScalarKind::F64 => "1.5",
|
||||||
|
aura_core::ScalarKind::Bool => "true",
|
||||||
|
aura_core::ScalarKind::Timestamp => "0",
|
||||||
|
};
|
||||||
|
let kind_tag = format!("{real_kind:?}"); // unit-variant Debug == serde string
|
||||||
|
let campaign_text = format!(
|
||||||
|
concat!(
|
||||||
|
r#"{{"format_version":1,"kind":"campaign","name":"c","#,
|
||||||
|
r#""data":{{"instruments":["GER40"],"windows":[{{"from_ms":1,"to_ms":2}}]}},"#,
|
||||||
|
r#""strategies":[{{"ref":{{"content_id":"{bp}"}},"#,
|
||||||
|
r#""axes":{{"{axis}":{{"kind":"{kind}","values":[{val}]}}}}}}],"#,
|
||||||
|
r#""process":{{"ref":{{"content_id":"{proc}"}}}},"#,
|
||||||
|
r#""seed":1,"presentation":{{"persist_taps":[],"emit":["family_table"]}}}}"#
|
||||||
|
),
|
||||||
|
bp = bp_id,
|
||||||
|
axis = real_name,
|
||||||
|
kind = kind_tag,
|
||||||
|
val = bare,
|
||||||
|
proc = proc_id,
|
||||||
|
);
|
||||||
|
let doc = aura_research::parse_campaign(&campaign_text).expect("campaign parses");
|
||||||
|
assert_eq!(reg.validate_campaign_refs(&doc, &resolve).expect("io ok"), Vec::new());
|
||||||
|
|
||||||
|
// unknown process + unknown strategy ids
|
||||||
|
let mut missing = doc.clone();
|
||||||
|
missing.process.r#ref = DocRef::ContentId("00".into());
|
||||||
|
missing.strategies[0].r#ref = DocRef::ContentId("11".into());
|
||||||
|
let faults = reg.validate_campaign_refs(&missing, &resolve).expect("io ok");
|
||||||
|
assert!(faults.contains(&RefFault::ProcessNotFound("00".into())));
|
||||||
|
assert!(faults.contains(&RefFault::StrategyNotFound("11".into())));
|
||||||
|
|
||||||
|
// unknown axis name
|
||||||
|
let mut bad_axis = doc.clone();
|
||||||
|
let ax = bad_axis.strategies[0].axes.remove(&real_name).unwrap();
|
||||||
|
bad_axis.strategies[0].axes.insert("nope".into(), ax);
|
||||||
|
assert!(reg
|
||||||
|
.validate_campaign_refs(&bad_axis, &resolve)
|
||||||
|
.expect("io ok")
|
||||||
|
.iter()
|
||||||
|
.any(|f| matches!(f, RefFault::AxisNotInParamSpace { axis, .. } if axis == "nope")));
|
||||||
|
|
||||||
|
// declared kind != the param's kind
|
||||||
|
let wrong_kind = if matches!(real_kind, aura_core::ScalarKind::I64) {
|
||||||
|
aura_core::ScalarKind::F64
|
||||||
|
} else {
|
||||||
|
aura_core::ScalarKind::I64
|
||||||
|
};
|
||||||
|
let mut mismatched = doc.clone();
|
||||||
|
// values were parsed under the right kind; re-kind the axis only —
|
||||||
|
// the check compares DECLARED kind vs the param's kind
|
||||||
|
let kept_values = mismatched.strategies[0].axes[&real_name].values.clone();
|
||||||
|
mismatched.strategies[0].axes.insert(
|
||||||
|
real_name.clone(),
|
||||||
|
Axis { kind: wrong_kind, values: kept_values },
|
||||||
|
);
|
||||||
|
assert!(reg
|
||||||
|
.validate_campaign_refs(&mismatched, &resolve)
|
||||||
|
.expect("io ok")
|
||||||
|
.iter()
|
||||||
|
.any(|f| matches!(f, RefFault::AxisKindMismatch { axis, .. } if axis == &real_name)));
|
||||||
|
|
||||||
|
// identity ref resolves via the store scan
|
||||||
|
let identity_json = blueprint_identity_json(&composite).expect("identity form");
|
||||||
|
let identity_id = aura_research::content_id_of(&identity_json);
|
||||||
|
let mut by_identity = doc.clone();
|
||||||
|
by_identity.strategies[0].r#ref = DocRef::IdentityId(identity_id);
|
||||||
|
assert_eq!(reg.validate_campaign_refs(&by_identity, &resolve).expect("io ok"), Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_r_metric_accepts_r_and_refuses_pip() {
|
fn check_r_metric_accepts_r_and_refuses_pip() {
|
||||||
assert!(check_r_metric("expectancy_r").is_ok());
|
assert!(check_r_metric("expectancy_r").is_ok());
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "aura-research"
|
||||||
|
edition.workspace = true
|
||||||
|
version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
aura-core = { path = "../aura-core" }
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
# sha2: content-addressed ids for research documents (mirrors aura-cli's
|
||||||
|
# topology_hash use, per-case C16 review, SHA256 user-settled).
|
||||||
|
sha2 = "0.10"
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1724,27 +1724,42 @@ import.)
|
|||||||
|
|
||||||
- [ ] **Step 2: Dev-deps for the fixture, if absent**
|
- [ ] **Step 2: Dev-deps for the fixture, if absent**
|
||||||
|
|
||||||
In `crates/aura-registry/Cargo.toml` `[dev-dependencies]` add whichever of
|
In `crates/aura-registry/Cargo.toml` `[dev-dependencies]` add if missing:
|
||||||
these is missing:
|
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
aura-std = { path = "../aura-std" }
|
aura-std = { path = "../aura-std" }
|
||||||
aura-composites = { path = "../aura-composites" }
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
(NOT aura-composites: its shipped composites — vol_stop, risk_executor* —
|
||||||
|
all route through `LinComb`, whose builder needs a structural arity
|
||||||
|
argument and is therefore deliberately absent from the zero-arg
|
||||||
|
`std_vocabulary` roster, so none of them can round-trip through
|
||||||
|
`blueprint_from_json` with the by-type-name resolver this test uses. The
|
||||||
|
fixture below hand-builds a minimal loadable composite instead.)
|
||||||
|
|
||||||
- [ ] **Step 3: Append the referential test (tests module). The fixture is
|
- [ ] **Step 3: Append the referential test (tests module). The fixture is
|
||||||
GENERIC over the composite's real param space — no hardcoded param names**
|
GENERIC over the composite's real param space — no hardcoded param names**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
#[test]
|
#[test]
|
||||||
fn referential_tier_resolves_refs_and_checks_axes() {
|
fn referential_tier_resolves_refs_and_checks_axes() {
|
||||||
|
use aura_engine::{blueprint_to_json, Composite};
|
||||||
use aura_research::{Axis, DocRef};
|
use aura_research::{Axis, DocRef};
|
||||||
|
|
||||||
let reg = Registry::open(temp_family_dir("referential_tier"));
|
let reg = Registry::open(temp_family_dir("referential_tier"));
|
||||||
let resolve = |t: &str| aura_std::std_vocabulary(t);
|
let resolve = |t: &str| aura_std::std_vocabulary(t);
|
||||||
|
|
||||||
// a real public composite with an open param space
|
// A minimal fixture composite with one OPEN param, built from a
|
||||||
let composite = aura_composites::vol_stop(4, 1.5);
|
// zero-arg `aura-std` node (`Bias`) so `std_vocabulary` can actually
|
||||||
|
// resolve it on load (see Step 2's note on why no aura-composites
|
||||||
|
// composite qualifies).
|
||||||
|
let composite = Composite::new(
|
||||||
|
"fixture",
|
||||||
|
vec![aura_std::Bias::builder().named("b").into()],
|
||||||
|
vec![],
|
||||||
|
vec![],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
let space = composite.param_space();
|
let space = composite.param_space();
|
||||||
let real = space.first().expect("fixture composite has an open param");
|
let real = space.first().expect("fixture composite has an open param");
|
||||||
let real_name = real.name.clone();
|
let real_name = real.name.clone();
|
||||||
@@ -1833,10 +1848,8 @@ Anchor notes: `blueprint_to_json`/`blueprint_identity_json` — import from
|
|||||||
the engine re-export the way the crate already imports engine items; if
|
the engine re-export the way the crate already imports engine items; if
|
||||||
`aura_core::ScalarKind`/`Scalar` are not yet dev-visible, they come through
|
`aura_core::ScalarKind`/`Scalar` are not yet dev-visible, they come through
|
||||||
the existing `aura-engine` dependency's re-exports or a direct
|
the existing `aura-engine` dependency's re-exports or a direct
|
||||||
`aura-core` path dep (mirror whichever the crate already uses). If
|
`aura-core` path dep (mirror whichever the crate already uses). The test
|
||||||
`vol_stop(4, 1.5).param_space()` turns out EMPTY, use
|
is generic and needs only ONE open param from the fixture composite.
|
||||||
`aura_composites::risk_executor_vol_open(1.0)` instead — the test is
|
|
||||||
generic and needs only ONE open param.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Run the test**
|
- [ ] **Step 4: Run the test**
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user