feat(daemon,ops): daemon surface + ontology-content implementation
Substantive changes across the crate tree (cargo check --workspace: clean): - ontoref-daemon: UI handlers/pages, MCP surface, api, domain query delegation - ontoref-ops: runtime - ontoref-ontology / ontoref-ontology-content / oplog / triples / commit: supporting - domains/rustelo: schema surface sync
This commit is contained in:
parent
a4edc9e95a
commit
234432856c
106 changed files with 1448 additions and 1078 deletions
|
|
@ -15,17 +15,16 @@ use crate::store::BlobId;
|
||||||
///
|
///
|
||||||
/// Contract any backend MUST honour:
|
/// Contract any backend MUST honour:
|
||||||
///
|
///
|
||||||
/// 1. `put(bytes)` returns the address `BlobId::from_bytes(bytes)` —
|
/// 1. `put(bytes)` returns the address `BlobId::from_bytes(bytes)` — addressing
|
||||||
/// addressing is by blake3, identical across backends.
|
/// is by blake3, identical across backends.
|
||||||
/// 2. `get(id)` returns the exact bytes that produced `id`, or an
|
/// 2. `get(id)` returns the exact bytes that produced `id`, or an error.
|
||||||
/// error. Corrupted bytes MUST be rejected with
|
/// Corrupted bytes MUST be rejected with [`crate::Error::Corrupt`].
|
||||||
/// [`crate::Error::Corrupt`].
|
/// 3. `exists(id)` is consistent with `get(id)`: it returns true iff a
|
||||||
/// 3. `exists(id)` is consistent with `get(id)`: it returns true iff
|
/// subsequent `get(id)` would not return `NotFound`. It MUST NOT perform an
|
||||||
/// a subsequent `get(id)` would not return `NotFound`. It MUST NOT
|
/// integrity check (use `get` for that).
|
||||||
/// perform an integrity check (use `get` for that).
|
/// 4. `list_prefix(prefix_hex)` returns every id whose hex representation
|
||||||
/// 4. `list_prefix(prefix_hex)` returns every id whose hex
|
/// starts with `prefix_hex`. An empty `prefix_hex` returns every id known to
|
||||||
/// representation starts with `prefix_hex`. An empty `prefix_hex`
|
/// the backend.
|
||||||
/// returns every id known to the backend.
|
|
||||||
pub trait BlobBackend {
|
pub trait BlobBackend {
|
||||||
/// Store `bytes` and return its content address.
|
/// Store `bytes` and return its content address.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ pub enum Error {
|
||||||
/// Canonical encoding/decoding failure.
|
/// Canonical encoding/decoding failure.
|
||||||
#[error("canonical: {0}")]
|
#[error("canonical: {0}")]
|
||||||
Canonical(#[from] ontoref_types::Error),
|
Canonical(#[from] ontoref_types::Error),
|
||||||
/// Requested cell does not exist in the commit layer; no witness can be built.
|
/// Requested cell does not exist in the commit layer; no witness can be
|
||||||
|
/// built.
|
||||||
#[error("cell not found in commit layer")]
|
#[error("cell not found in commit layer")]
|
||||||
CellNotFound,
|
CellNotFound,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,10 +57,7 @@ impl MergePolicy {
|
||||||
|
|
||||||
/// Merge kind for `attr` — `Lww` when undeclared.
|
/// Merge kind for `attr` — `Lww` when undeclared.
|
||||||
pub fn kind_for(&self, attr: &AttrId) -> CellMergeKind {
|
pub fn kind_for(&self, attr: &AttrId) -> CellMergeKind {
|
||||||
self.by_attr
|
self.by_attr.get(attr.as_str()).copied().unwrap_or_default()
|
||||||
.get(attr.as_str())
|
|
||||||
.copied()
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,8 @@ pub mod tree;
|
||||||
|
|
||||||
pub use error::{Error, Result};
|
pub use error::{Error, Result};
|
||||||
pub use layer::{BinaryMerkle, CellMergeKind, MergePolicy};
|
pub use layer::{BinaryMerkle, CellMergeKind, MergePolicy};
|
||||||
pub use tree::{cell_key, empty_root, verify_witness, CellKey, PathStep, StateRoot, Witness};
|
|
||||||
|
|
||||||
use ontoref_types::{AttrId, EntityId, Operation};
|
use ontoref_types::{AttrId, EntityId, Operation};
|
||||||
|
pub use tree::{cell_key, empty_root, verify_witness, CellKey, PathStep, StateRoot, Witness};
|
||||||
|
|
||||||
/// Pluggable commitment-backend trait (ADR-026 / D15).
|
/// Pluggable commitment-backend trait (ADR-026 / D15).
|
||||||
///
|
///
|
||||||
|
|
@ -39,15 +38,14 @@ use ontoref_types::{AttrId, EntityId, Operation};
|
||||||
/// Contract any backend MUST honour:
|
/// Contract any backend MUST honour:
|
||||||
///
|
///
|
||||||
/// 1. `apply(op)` is the only mutation channel; calling it with the same
|
/// 1. `apply(op)` is the only mutation channel; calling it with the same
|
||||||
/// sequence of operations from any initial state must yield the same
|
/// sequence of operations from any initial state must yield the same state
|
||||||
/// state root.
|
/// root.
|
||||||
/// 2. `root()` is deterministic — no clock, no randomness, no
|
/// 2. `root()` is deterministic — no clock, no randomness, no instance-specific
|
||||||
/// instance-specific state may influence the bytes.
|
/// state may influence the bytes.
|
||||||
/// 3. A witness emitted by `witness(entity, attr)` MUST verify against
|
/// 3. A witness emitted by `witness(entity, attr)` MUST verify against the root
|
||||||
/// the root returned by the same instance immediately after the call.
|
/// returned by the same instance immediately after the call.
|
||||||
/// 4. `verify_witness(w, r)` is a pure function — backends MUST NOT
|
/// 4. `verify_witness(w, r)` is a pure function — backends MUST NOT consult any
|
||||||
/// consult any external state and MUST return the same bool for the
|
/// external state and MUST return the same bool for the same inputs.
|
||||||
/// same inputs.
|
|
||||||
pub trait CommitmentBackend {
|
pub trait CommitmentBackend {
|
||||||
/// Apply one operation. Successful application updates internal state
|
/// Apply one operation. Successful application updates internal state
|
||||||
/// such that `root()` reflects the operation's effects.
|
/// such that `root()` reflects the operation's effects.
|
||||||
|
|
|
||||||
|
|
@ -103,10 +103,7 @@ pub(crate) fn merkle_root(entries: &[(CellKey, &[u8])]) -> StateRoot {
|
||||||
/// `entries`. Returns `None` if the key is absent.
|
/// `entries`. Returns `None` if the key is absent.
|
||||||
///
|
///
|
||||||
/// `entries` must be sorted by key (same constraint as `merkle_root`).
|
/// `entries` must be sorted by key (same constraint as `merkle_root`).
|
||||||
pub(crate) fn build_witness(
|
pub(crate) fn build_witness(entries: &[(CellKey, &[u8])], target_key: &CellKey) -> Option<Witness> {
|
||||||
entries: &[(CellKey, &[u8])],
|
|
||||||
target_key: &CellKey,
|
|
||||||
) -> Option<Witness> {
|
|
||||||
let index = entries.iter().position(|(k, _)| k == target_key)?;
|
let index = entries.iter().position(|(k, _)| k == target_key)?;
|
||||||
let value = entries[index].1.to_vec();
|
let value = entries[index].1.to_vec();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
use ed25519_dalek::SigningKey;
|
use ed25519_dalek::SigningKey;
|
||||||
|
use ontoref_commit::{verify_witness, CommitLayer};
|
||||||
use ontoref_types::{
|
use ontoref_types::{
|
||||||
operation::{OpBody, OpPayload, Operation},
|
operation::{OpBody, OpPayload, Operation},
|
||||||
AttrId, EntityId, Hlc, PublicKey, Value,
|
AttrId, EntityId, Hlc, PublicKey, Value,
|
||||||
};
|
};
|
||||||
|
|
||||||
use ontoref_commit::{verify_witness, CommitLayer};
|
|
||||||
|
|
||||||
fn signing_key() -> SigningKey {
|
fn signing_key() -> SigningKey {
|
||||||
SigningKey::from_bytes(&[11u8; 32])
|
SigningKey::from_bytes(&[11u8; 32])
|
||||||
}
|
}
|
||||||
|
|
@ -40,17 +39,10 @@ fn fixture_ops() -> Vec<Operation> {
|
||||||
),
|
),
|
||||||
assert_op(
|
assert_op(
|
||||||
"bob",
|
"bob",
|
||||||
vec![
|
vec![("name", Value::Str("Bob".into())), ("age", Value::Int(25))],
|
||||||
("name", Value::Str("Bob".into())),
|
|
||||||
("age", Value::Int(25)),
|
|
||||||
],
|
|
||||||
Hlc::new(2, 0),
|
Hlc::new(2, 0),
|
||||||
),
|
),
|
||||||
assert_op(
|
assert_op("alice", vec![("age", Value::Int(31))], Hlc::new(3, 0)),
|
||||||
"alice",
|
|
||||||
vec![("age", Value::Int(31))],
|
|
||||||
Hlc::new(3, 0),
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,7 +79,10 @@ fn determinism_root_changes_with_value_overwrite() {
|
||||||
Hlc::new(2, 0),
|
Hlc::new(2, 0),
|
||||||
));
|
));
|
||||||
let r2 = layer.root();
|
let r2 = layer.root();
|
||||||
assert_ne!(r1, r2, "root must change when an existing cell's value changes");
|
assert_ne!(
|
||||||
|
r1, r2,
|
||||||
|
"root must change when an existing cell's value changes"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -224,8 +219,16 @@ fn union_cell_accumulates_and_is_order_independent() {
|
||||||
|
|
||||||
// Two actors assert different tag elements on the same cell. Applied in
|
// Two actors assert different tag elements on the same cell. Applied in
|
||||||
// either order, the cell must hold BOTH and hash to the same root.
|
// either order, the cell must hold BOTH and hash to the same root.
|
||||||
let a = assert_op("doc", vec![("tags", Value::Str("red".into()))], Hlc::new(1, 0));
|
let a = assert_op(
|
||||||
let b = assert_op("doc", vec![("tags", Value::Str("blue".into()))], Hlc::new(2, 0));
|
"doc",
|
||||||
|
vec![("tags", Value::Str("red".into()))],
|
||||||
|
Hlc::new(1, 0),
|
||||||
|
);
|
||||||
|
let b = assert_op(
|
||||||
|
"doc",
|
||||||
|
vec![("tags", Value::Str("blue".into()))],
|
||||||
|
Hlc::new(2, 0),
|
||||||
|
);
|
||||||
|
|
||||||
let mut forward = CommitLayer::new().with_policy(policy.clone());
|
let mut forward = CommitLayer::new().with_policy(policy.clone());
|
||||||
forward.apply(&a);
|
forward.apply(&a);
|
||||||
|
|
@ -266,5 +269,9 @@ fn lww_is_unchanged_without_a_policy() {
|
||||||
// Equals a layer that only ever saw the winning (b) assertion.
|
// Equals a layer that only ever saw the winning (b) assertion.
|
||||||
let mut only_b = CommitLayer::new();
|
let mut only_b = CommitLayer::new();
|
||||||
only_b.apply(&b);
|
only_b.apply(&b);
|
||||||
assert_eq!(layer.root(), only_b.root(), "default policy stays last-writer-wins");
|
assert_eq!(
|
||||||
|
layer.root(),
|
||||||
|
only_b.root(),
|
||||||
|
"default policy stays last-writer-wins"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
//! Composition layer integrating L1 through L5 of the ontoref verifiable substrate.
|
//! Composition layer integrating L1 through L5 of the ontoref verifiable
|
||||||
|
//! substrate.
|
||||||
//!
|
//!
|
||||||
//! `Substrate::open(path)` materialises a project on disk: blob store, op log,
|
//! `Substrate::open(path)` materialises a project on disk: blob store, op log,
|
||||||
//! triple store; the in-memory commit layer is rebuilt by replaying the op log
|
//! triple store; the in-memory commit layer is rebuilt by replaying the op log
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
use ed25519_dalek::SigningKey;
|
use ed25519_dalek::SigningKey;
|
||||||
use ontoref_commit::{empty_root, verify_witness};
|
use ontoref_commit::{empty_root, verify_witness};
|
||||||
|
use ontoref_core::Substrate;
|
||||||
use ontoref_types::{
|
use ontoref_types::{
|
||||||
operation::{OpBody, OpPayload, Operation},
|
operation::{OpBody, OpPayload, Operation},
|
||||||
AttrId, EntityId, Hlc, PublicKey, Value,
|
AttrId, EntityId, Hlc, PublicKey, Value,
|
||||||
};
|
};
|
||||||
|
|
||||||
use ontoref_core::Substrate;
|
|
||||||
|
|
||||||
fn signing_key() -> SigningKey {
|
fn signing_key() -> SigningKey {
|
||||||
SigningKey::from_bytes(&[17u8; 32])
|
SigningKey::from_bytes(&[17u8; 32])
|
||||||
}
|
}
|
||||||
|
|
@ -109,11 +108,7 @@ fn apply_pipeline_materialises_triples_visible_to_query() {
|
||||||
let attrs = substrate
|
let attrs = substrate
|
||||||
.query()
|
.query()
|
||||||
.triples()
|
.triples()
|
||||||
.entity_attrs(
|
.entity_attrs(&EntityId::new("alice"), Hlc::new(10, 0), Hlc::new(10, 0))
|
||||||
&EntityId::new("alice"),
|
|
||||||
Hlc::new(10, 0),
|
|
||||||
Hlc::new(10, 0),
|
|
||||||
)
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(attrs.len(), 1);
|
assert_eq!(attrs.len(), 1);
|
||||||
assert_eq!(attrs[0].0, AttrId::new("name"));
|
assert_eq!(attrs[0].0, AttrId::new("name"));
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@
|
||||||
//!
|
//!
|
||||||
//! 1. The op log holds at least every op the child reported as completed.
|
//! 1. The op log holds at least every op the child reported as completed.
|
||||||
//! 2. At most one extra op is present (the in-flight one at kill time).
|
//! 2. At most one extra op is present (the in-flight one at kill time).
|
||||||
//! 3. Reopening the substrate twice produces the same state root —
|
//! 3. Reopening the substrate twice produces the same state root — i.e.
|
||||||
//! i.e. recovery from the durable layer is deterministic.
|
//! recovery from the durable layer is deterministic.
|
||||||
//!
|
//!
|
||||||
//! Marked `#[ignore]` because it kills a real process; run with
|
//! Marked `#[ignore]` because it kills a real process; run with
|
||||||
//! `cargo test -p ontoref-core --test crash_recovery -- --ignored`.
|
//! `cargo test -p ontoref-core --test crash_recovery -- --ignored`.
|
||||||
|
|
@ -26,10 +26,10 @@ const PROGRESS_PATH_ENV: &str = "ONTOREF_CRASH_RECOVERY_PROGRESS";
|
||||||
const TEST_NAME: &str = "crash_recovery_kill9_during_apply";
|
const TEST_NAME: &str = "crash_recovery_kill9_during_apply";
|
||||||
|
|
||||||
fn run_as_child() -> ! {
|
fn run_as_child() -> ! {
|
||||||
let sub_path = std::env::var(SUBSTRATE_PATH_ENV)
|
let sub_path =
|
||||||
.expect("child requires SUBSTRATE_PATH env var");
|
std::env::var(SUBSTRATE_PATH_ENV).expect("child requires SUBSTRATE_PATH env var");
|
||||||
let progress_path = std::env::var(PROGRESS_PATH_ENV)
|
let progress_path =
|
||||||
.expect("child requires PROGRESS_PATH env var");
|
std::env::var(PROGRESS_PATH_ENV).expect("child requires PROGRESS_PATH env var");
|
||||||
|
|
||||||
let mut substrate = Substrate::open(&sub_path).expect("child failed to open substrate");
|
let mut substrate = Substrate::open(&sub_path).expect("child failed to open substrate");
|
||||||
let key = SigningKey::from_bytes(&[42u8; 32]);
|
let key = SigningKey::from_bytes(&[42u8; 32]);
|
||||||
|
|
@ -48,8 +48,7 @@ fn run_as_child() -> ! {
|
||||||
};
|
};
|
||||||
let op = Operation::sign(body, &key);
|
let op = Operation::sign(body, &key);
|
||||||
substrate.apply(&op).expect("child apply failed");
|
substrate.apply(&op).expect("child apply failed");
|
||||||
std::fs::write(&progress_path, format!("{counter}"))
|
std::fs::write(&progress_path, format!("{counter}")).expect("child progress write failed");
|
||||||
.expect("child progress write failed");
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||||
counter += 1;
|
counter += 1;
|
||||||
}
|
}
|
||||||
|
|
@ -96,11 +95,13 @@ fn crash_recovery_kill9_during_apply() {
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
oplog_len > last_completed,
|
oplog_len > last_completed,
|
||||||
"oplog must hold every reported op (last_completed={last_completed}, oplog_len={oplog_len})"
|
"oplog must hold every reported op (last_completed={last_completed}, \
|
||||||
|
oplog_len={oplog_len})"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
oplog_len < last_completed + 3,
|
oplog_len < last_completed + 3,
|
||||||
"oplog should not be more than one op past the last reported (last_completed={last_completed}, oplog_len={oplog_len})"
|
"oplog should not be more than one op past the last reported \
|
||||||
|
(last_completed={last_completed}, oplog_len={oplog_len})"
|
||||||
);
|
);
|
||||||
|
|
||||||
let root_first = substrate.state_root();
|
let root_first = substrate.state_root();
|
||||||
|
|
@ -110,6 +111,7 @@ fn crash_recovery_kill9_during_apply() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
substrate_again.state_root(),
|
substrate_again.state_root(),
|
||||||
root_first,
|
root_first,
|
||||||
"state root must be deterministic across reopens — recovery is a pure function of the durable oplog"
|
"state root must be deterministic across reopens — recovery is a pure function of the \
|
||||||
|
durable oplog"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,13 +80,21 @@ fn two_actors_converge_on_the_same_cell() {
|
||||||
bob.apply(&bob_op).expect("bob apply");
|
bob.apply(&bob_op).expect("bob apply");
|
||||||
|
|
||||||
// Diverged before sync.
|
// Diverged before sync.
|
||||||
assert_ne!(alice.state_root(), bob.state_root(), "actors diverge pre-sync");
|
assert_ne!(
|
||||||
|
alice.state_root(),
|
||||||
|
bob.state_root(),
|
||||||
|
"actors diverge pre-sync"
|
||||||
|
);
|
||||||
|
|
||||||
// Each shares its op, then pulls + ingests the peer's.
|
// Each shares its op, then pulls + ingests the peer's.
|
||||||
share(&alice_sync, &alice_op);
|
share(&alice_sync, &alice_op);
|
||||||
alice_sync.announce(&hex(&alice_op.id().0)).expect("alice announce");
|
alice_sync
|
||||||
|
.announce(&hex(&alice_op.id().0))
|
||||||
|
.expect("alice announce");
|
||||||
share(&bob_sync, &bob_op);
|
share(&bob_sync, &bob_op);
|
||||||
bob_sync.announce(&hex(&bob_op.id().0)).expect("bob announce");
|
bob_sync
|
||||||
|
.announce(&hex(&bob_op.id().0))
|
||||||
|
.expect("bob announce");
|
||||||
|
|
||||||
let into_bob = pull_and_ingest(&bob_sync, "alice", &mut bob);
|
let into_bob = pull_and_ingest(&bob_sync, "alice", &mut bob);
|
||||||
let into_alice = pull_and_ingest(&alice_sync, "bob", &mut alice);
|
let into_alice = pull_and_ingest(&alice_sync, "bob", &mut alice);
|
||||||
|
|
@ -105,10 +113,15 @@ fn two_actors_converge_on_the_same_cell() {
|
||||||
let root_before = alice.state_root();
|
let root_before = alice.state_root();
|
||||||
let again = pull_and_ingest(&alice_sync, "bob", &mut alice);
|
let again = pull_and_ingest(&alice_sync, "bob", &mut alice);
|
||||||
assert_eq!(again, 0, "re-ingest is idempotent");
|
assert_eq!(again, 0, "re-ingest is idempotent");
|
||||||
assert_eq!(alice.state_root(), root_before, "idempotent ingest leaves the root");
|
assert_eq!(
|
||||||
|
alice.state_root(),
|
||||||
|
root_before,
|
||||||
|
"idempotent ingest leaves the root"
|
||||||
|
);
|
||||||
|
|
||||||
for op in alice.oplog().all_ops_in_hlc_order().expect("ops") {
|
for op in alice.oplog().all_ops_in_hlc_order().expect("ops") {
|
||||||
op.verify_signature().expect("every converged witness verifies");
|
op.verify_signature()
|
||||||
|
.expect("every converged witness verifies");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -159,7 +172,9 @@ fn two_actors_converge_with_a_removal() {
|
||||||
// The convergent state has the cell removed: it equals a substrate that
|
// The convergent state has the cell removed: it equals a substrate that
|
||||||
// only ever saw the (higher-HLC) retract.
|
// only ever saw the (higher-HLC) retract.
|
||||||
let mut reference = Substrate::open(dir.path().join("ref")).expect("ref");
|
let mut reference = Substrate::open(dir.path().join("ref")).expect("ref");
|
||||||
reference.apply(&retract_op(&b_key, "shared", &["v"], Hlc::new(20, 0))).expect("ref");
|
reference
|
||||||
|
.apply(&retract_op(&b_key, "shared", &["v"], Hlc::new(20, 0)))
|
||||||
|
.expect("ref");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
alice.state_root(),
|
alice.state_root(),
|
||||||
reference.state_root(),
|
reference.state_root(),
|
||||||
|
|
@ -195,16 +210,29 @@ fn union_domain_converges_across_actors_via_substrate() {
|
||||||
let a_key = SigningKey::from_bytes(&[1u8; 32]);
|
let a_key = SigningKey::from_bytes(&[1u8; 32]);
|
||||||
let b_key = SigningKey::from_bytes(&[2u8; 32]);
|
let b_key = SigningKey::from_bytes(&[2u8; 32]);
|
||||||
|
|
||||||
let mut alice =
|
let mut alice = Substrate::open_with_policy(dir.path().join("a"), policy.clone()).expect("a");
|
||||||
Substrate::open_with_policy(dir.path().join("a"), policy.clone()).expect("a");
|
|
||||||
let mut bob = Substrate::open_with_policy(dir.path().join("b"), policy.clone()).expect("b");
|
let mut bob = Substrate::open_with_policy(dir.path().join("b"), policy.clone()).expect("b");
|
||||||
|
|
||||||
let oa = assert_attr(&a_key, "doc", "tags", Value::Str("red".into()), Hlc::new(10, 0));
|
let oa = assert_attr(
|
||||||
let ob = assert_attr(&b_key, "doc", "tags", Value::Str("blue".into()), Hlc::new(20, 0));
|
&a_key,
|
||||||
|
"doc",
|
||||||
|
"tags",
|
||||||
|
Value::Str("red".into()),
|
||||||
|
Hlc::new(10, 0),
|
||||||
|
);
|
||||||
|
let ob = assert_attr(
|
||||||
|
&b_key,
|
||||||
|
"doc",
|
||||||
|
"tags",
|
||||||
|
Value::Str("blue".into()),
|
||||||
|
Hlc::new(20, 0),
|
||||||
|
);
|
||||||
alice.apply(&oa).expect("alice");
|
alice.apply(&oa).expect("alice");
|
||||||
bob.apply(&ob).expect("bob");
|
bob.apply(&ob).expect("bob");
|
||||||
|
|
||||||
alice.ingest(std::slice::from_ref(&ob)).expect("alice ingest");
|
alice
|
||||||
|
.ingest(std::slice::from_ref(&ob))
|
||||||
|
.expect("alice ingest");
|
||||||
bob.ingest(std::slice::from_ref(&oa)).expect("bob ingest");
|
bob.ingest(std::slice::from_ref(&oa)).expect("bob ingest");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -215,10 +243,15 @@ fn union_domain_converges_across_actors_via_substrate() {
|
||||||
|
|
||||||
// The converged root retains BOTH tags: it differs from a substrate that
|
// The converged root retains BOTH tags: it differs from a substrate that
|
||||||
// only ever saw one element (proving union, not last-writer-wins).
|
// only ever saw one element (proving union, not last-writer-wins).
|
||||||
let mut only_one =
|
let mut only_one = Substrate::open_with_policy(dir.path().join("c"), policy).expect("c");
|
||||||
Substrate::open_with_policy(dir.path().join("c"), policy).expect("c");
|
|
||||||
only_one
|
only_one
|
||||||
.apply(&assert_attr(&a_key, "doc", "tags", Value::Str("red".into()), Hlc::new(10, 0)))
|
.apply(&assert_attr(
|
||||||
|
&a_key,
|
||||||
|
"doc",
|
||||||
|
"tags",
|
||||||
|
Value::Str("red".into()),
|
||||||
|
Hlc::new(10, 0),
|
||||||
|
))
|
||||||
.expect("c apply");
|
.expect("c apply");
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
alice.state_root(),
|
alice.state_root(),
|
||||||
|
|
@ -242,13 +275,11 @@ fn semantic_validator_quarantines_a_peer_op_post_merge() {
|
||||||
|
|
||||||
// Domain rule: reject assertions to the "forbidden" entity.
|
// Domain rule: reject assertions to the "forbidden" entity.
|
||||||
let report = local
|
let report = local
|
||||||
.ingest_with(&[good.clone(), bad.clone()], |op| {
|
.ingest_with(&[good.clone(), bad.clone()], |op| match &op.body.payload {
|
||||||
match &op.body.payload {
|
OpPayload::EntityAssert { entity, .. } if entity.0 == "forbidden" => {
|
||||||
OpPayload::EntityAssert { entity, .. } if entity.0 == "forbidden" => {
|
Err("entity 'forbidden' is not admissible".to_owned())
|
||||||
Err("entity 'forbidden' is not admissible".to_owned())
|
|
||||||
}
|
|
||||||
_ => Ok(()),
|
|
||||||
}
|
}
|
||||||
|
_ => Ok(()),
|
||||||
})
|
})
|
||||||
.expect("ingest_with");
|
.expect("ingest_with");
|
||||||
|
|
||||||
|
|
@ -265,5 +296,8 @@ fn semantic_validator_quarantines_a_peer_op_post_merge() {
|
||||||
.map(|o| o.id())
|
.map(|o| o.id())
|
||||||
.collect();
|
.collect();
|
||||||
assert!(ids.contains(&good.id()));
|
assert!(ids.contains(&good.id()));
|
||||||
assert!(!ids.contains(&bad.id()), "quarantined op never reaches the oplog");
|
assert!(
|
||||||
|
!ids.contains(&bad.id()),
|
||||||
|
"quarantined op never reaches the oplog"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,10 @@ fn snapshot_enables_incremental_recovery() {
|
||||||
snap.state_root, root_at_snapshot,
|
snap.state_root, root_at_snapshot,
|
||||||
"snapshot records the state root for direct offline verification"
|
"snapshot records the state root for direct offline verification"
|
||||||
);
|
);
|
||||||
assert!(snap.boundary.is_some(), "boundary pins the replay resume point");
|
assert!(
|
||||||
|
snap.boundary.is_some(),
|
||||||
|
"boundary pins the replay resume point"
|
||||||
|
);
|
||||||
|
|
||||||
// Reopen via the snapshot reproduces the root with nothing new to replay.
|
// Reopen via the snapshot reproduces the root with nothing new to replay.
|
||||||
let root_final = {
|
let root_final = {
|
||||||
|
|
|
||||||
|
|
@ -438,12 +438,12 @@ fn process_alive(_pid: u32) -> bool {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[test]
|
#[test]
|
||||||
fn keypair_lifecycle() {
|
fn keypair_lifecycle() {
|
||||||
use crate::keypair::{
|
|
||||||
gen_for_actor, load_for_actor, public_key_hex, register_actor_in_ncl,
|
|
||||||
sign_witness_payload,
|
|
||||||
};
|
|
||||||
use ontoref_types::signature::{PublicKey, Sig};
|
use ontoref_types::signature::{PublicKey, Sig};
|
||||||
|
|
||||||
|
use crate::keypair::{
|
||||||
|
gen_for_actor, load_for_actor, public_key_hex, register_actor_in_ncl, sign_witness_payload,
|
||||||
|
};
|
||||||
|
|
||||||
let tmp = tempfile::tempdir().expect("tempdir");
|
let tmp = tempfile::tempdir().expect("tempdir");
|
||||||
let key_dir = tmp.path().join("keys");
|
let key_dir = tmp.path().join("keys");
|
||||||
let actors_ncl = tmp.path().join(".ontoref").join("actors.ncl");
|
let actors_ncl = tmp.path().join(".ontoref").join("actors.ncl");
|
||||||
|
|
@ -466,14 +466,23 @@ fn keypair_lifecycle() {
|
||||||
register_actor_in_ncl(&actors_ncl, actor, &public_key, "2026-05-26T00:00:00Z")
|
register_actor_in_ncl(&actors_ncl, actor, &public_key, "2026-05-26T00:00:00Z")
|
||||||
.expect("register ok");
|
.expect("register ok");
|
||||||
let ncl = std::fs::read_to_string(&actors_ncl).expect("read actors.ncl");
|
let ncl = std::fs::read_to_string(&actors_ncl).expect("read actors.ncl");
|
||||||
assert!(ncl.contains("public_key"), "actors.ncl must contain public_key");
|
assert!(
|
||||||
assert!(ncl.contains(&public_key_hex(&public_key)), "actors.ncl must carry exact pubkey hex");
|
ncl.contains("public_key"),
|
||||||
|
"actors.ncl must contain public_key"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
ncl.contains(&public_key_hex(&public_key)),
|
||||||
|
"actors.ncl must carry exact pubkey hex"
|
||||||
|
);
|
||||||
assert!(ncl.contains(actor), "actors.ncl must carry the actor id");
|
assert!(ncl.contains(actor), "actors.ncl must carry the actor id");
|
||||||
|
|
||||||
// 3. load — round-trips the private key from disk
|
// 3. load — round-trips the private key from disk
|
||||||
let signing_key = load_for_actor(actor, &key_dir).expect("load ok");
|
let signing_key = load_for_actor(actor, &key_dir).expect("load ok");
|
||||||
let derived = PublicKey::from_signing_key(&signing_key);
|
let derived = PublicKey::from_signing_key(&signing_key);
|
||||||
assert_eq!(derived.0, public_key.0, "loaded key must derive same public key");
|
assert_eq!(
|
||||||
|
derived.0, public_key.0,
|
||||||
|
"loaded key must derive same public key"
|
||||||
|
);
|
||||||
|
|
||||||
// 4. sign — produces a valid signature over the canonical witness payload
|
// 4. sign — produces a valid signature over the canonical witness payload
|
||||||
let op_id = b"op:move_fsm_state:00000001";
|
let op_id = b"op:move_fsm_state:00000001";
|
||||||
|
|
|
||||||
|
|
@ -226,7 +226,8 @@ pub struct AppState {
|
||||||
///
|
///
|
||||||
/// Exposes its JWKS at `GET /.well-known/ontoref-keys.json` and is the
|
/// Exposes its JWKS at `GET /.well-known/ontoref-keys.json` and is the
|
||||||
/// signing key for tokens accepted by SSO-mode panels (federation contract
|
/// signing key for tokens accepted by SSO-mode panels (federation contract
|
||||||
/// with sibling crates that consume `ontoref-auth`'s `SignedTokenProvider`).
|
/// with sibling crates that consume `ontoref-auth`'s
|
||||||
|
/// `SignedTokenProvider`).
|
||||||
pub token_issuer: Arc<auth::TokenIssuer>,
|
pub token_issuer: Arc<auth::TokenIssuer>,
|
||||||
/// Verifies daemon-minted JWTs against the issuer's JWKS. Refreshed in
|
/// Verifies daemon-minted JWTs against the issuer's JWKS. Refreshed in
|
||||||
/// lockstep with `token_issuer` on key rotation.
|
/// lockstep with `token_issuer` on key rotation.
|
||||||
|
|
@ -536,8 +537,8 @@ pub fn router(state: AppState) -> axum::Router {
|
||||||
// SSO surface: JWKS publication + token introspection (RFC 7662-shaped).
|
// SSO surface: JWKS publication + token introspection (RFC 7662-shaped).
|
||||||
// No auth on either endpoint:
|
// No auth on either endpoint:
|
||||||
// - JWKS is public by definition (anyone verifying a token needs it).
|
// - JWKS is public by definition (anyone verifying a token needs it).
|
||||||
// - Introspection takes the token in the request body and reveals only
|
// - Introspection takes the token in the request body and reveals only what was
|
||||||
// what was already cryptographically vouched by the issuer.
|
// already cryptographically vouched by the issuer.
|
||||||
let app = app
|
let app = app
|
||||||
.route("/.well-known/ontoref-keys.json", get(jwks_handler))
|
.route("/.well-known/ontoref-keys.json", get(jwks_handler))
|
||||||
.route("/.session/introspect", post(introspect_handler));
|
.route("/.session/introspect", post(introspect_handler));
|
||||||
|
|
@ -684,7 +685,10 @@ async fn api_catalog_handler() -> impl IntoResponse {
|
||||||
async fn ops_dispatch_handler(
|
async fn ops_dispatch_handler(
|
||||||
axum::extract::Path(op_id): axum::extract::Path<String>,
|
axum::extract::Path(op_id): axum::extract::Path<String>,
|
||||||
Json(request): Json<crate::ops_surface::DispatchRequest>,
|
Json(request): Json<crate::ops_surface::DispatchRequest>,
|
||||||
) -> Result<Json<crate::ops_surface::DispatchResponse>, (axum::http::StatusCode, Json<serde_json::Value>)> {
|
) -> Result<
|
||||||
|
Json<crate::ops_surface::DispatchResponse>,
|
||||||
|
(axum::http::StatusCode, Json<serde_json::Value>),
|
||||||
|
> {
|
||||||
match crate::ops_surface::dispatch_op_request(&op_id, request) {
|
match crate::ops_surface::dispatch_op_request(&op_id, request) {
|
||||||
Ok(response) => Ok(Json(response)),
|
Ok(response) => Ok(Json(response)),
|
||||||
Err(e) => Err((
|
Err(e) => Err((
|
||||||
|
|
@ -702,7 +706,8 @@ async fn ops_dispatch_handler(
|
||||||
path = "/ops",
|
path = "/ops",
|
||||||
auth = "none",
|
auth = "none",
|
||||||
actors = "agent, developer, ci, admin",
|
actors = "agent, developer, ci, admin",
|
||||||
description = "List every registered domain operation with its declarative shape (MCP-aligned projection)",
|
description = "List every registered domain operation with its declarative shape (MCP-aligned \
|
||||||
|
projection)",
|
||||||
tags = "ops, catalog"
|
tags = "ops, catalog"
|
||||||
)]
|
)]
|
||||||
async fn ops_list_handler() -> Json<serde_json::Value> {
|
async fn ops_list_handler() -> Json<serde_json::Value> {
|
||||||
|
|
@ -727,8 +732,7 @@ struct ChronicleQuery {
|
||||||
auth = "none",
|
auth = "none",
|
||||||
actors = "agent, developer, ci, admin",
|
actors = "agent, developer, ci, admin",
|
||||||
params = "since:string:optional:ISO date lower bound (inclusive); \
|
params = "since:string:optional:ISO date lower bound (inclusive); \
|
||||||
kind:string:optional:adr|migration; \
|
kind:string:optional:adr|migration; domain:string:optional:Domain filter; \
|
||||||
domain:string:optional:Domain filter; \
|
|
||||||
slug:string:optional:Project slug",
|
slug:string:optional:Project slug",
|
||||||
tags = "memory, chronicle"
|
tags = "memory, chronicle"
|
||||||
)]
|
)]
|
||||||
|
|
@ -764,8 +768,7 @@ struct RoadmapQuery {
|
||||||
auth = "none",
|
auth = "none",
|
||||||
actors = "agent, developer, ci, admin",
|
actors = "agent, developer, ci, admin",
|
||||||
params = "status:string:optional:Open|InProgress|Done|Cancelled; \
|
params = "status:string:optional:Open|InProgress|Done|Cancelled; \
|
||||||
horizon:string:optional:now|next|later; \
|
horizon:string:optional:now|next|later; slug:string:optional:Project slug",
|
||||||
slug:string:optional:Project slug",
|
|
||||||
tags = "memory, roadmap"
|
tags = "memory, roadmap"
|
||||||
)]
|
)]
|
||||||
async fn roadmap_handler(
|
async fn roadmap_handler(
|
||||||
|
|
@ -807,13 +810,8 @@ async fn criteria_handler(
|
||||||
) -> Json<serde_json::Value> {
|
) -> Json<serde_json::Value> {
|
||||||
state.touch_activity();
|
state.touch_activity();
|
||||||
let (root, cache, ip) = project_ctx_tuple(&state, q.slug.as_deref());
|
let (root, cache, ip) = project_ctx_tuple(&state, q.slug.as_deref());
|
||||||
let criteria = crate::memory::criteria_list(
|
let criteria =
|
||||||
&root,
|
crate::memory::criteria_list(&root, &cache, ip.as_deref(), q.kind.as_deref()).await;
|
||||||
&cache,
|
|
||||||
ip.as_deref(),
|
|
||||||
q.kind.as_deref(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
Json(serde_json::json!({ "count": criteria.len(), "criteria": criteria }))
|
Json(serde_json::json!({ "count": criteria.len(), "criteria": criteria }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -829,8 +827,8 @@ struct PanelsQuery {
|
||||||
path = "/api/panels",
|
path = "/api/panels",
|
||||||
auth = "none",
|
auth = "none",
|
||||||
actors = "agent, developer, ci, admin",
|
actors = "agent, developer, ci, admin",
|
||||||
params = "kind:string:optional:Timeline|Gantt|Graph|Table|Board; \
|
params = "kind:string:optional:Timeline|Gantt|Graph|Table|Board; slug:string:optional:Project \
|
||||||
slug:string:optional:Project slug",
|
slug",
|
||||||
tags = "memory, panels"
|
tags = "memory, panels"
|
||||||
)]
|
)]
|
||||||
async fn panels_handler(
|
async fn panels_handler(
|
||||||
|
|
@ -839,13 +837,7 @@ async fn panels_handler(
|
||||||
) -> Json<serde_json::Value> {
|
) -> Json<serde_json::Value> {
|
||||||
state.touch_activity();
|
state.touch_activity();
|
||||||
let (root, cache, ip) = project_ctx_tuple(&state, q.slug.as_deref());
|
let (root, cache, ip) = project_ctx_tuple(&state, q.slug.as_deref());
|
||||||
let panels = crate::memory::panels_list(
|
let panels = crate::memory::panels_list(&root, &cache, ip.as_deref(), q.kind.as_deref()).await;
|
||||||
&root,
|
|
||||||
&cache,
|
|
||||||
ip.as_deref(),
|
|
||||||
q.kind.as_deref(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
Json(serde_json::json!({ "count": panels.len(), "panels": panels }))
|
Json(serde_json::json!({ "count": panels.len(), "panels": panels }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1104,9 +1096,8 @@ struct RegisterResponse {
|
||||||
auth = "none",
|
auth = "none",
|
||||||
actors = "agent, developer, ci",
|
actors = "agent, developer, ci",
|
||||||
params = "actor_type:string:required:Actor type (agent|developer|ci|admin); \
|
params = "actor_type:string:required:Actor type (agent|developer|ci|admin); \
|
||||||
hostname:string:required:Caller hostname (audit trail); \
|
hostname:string:required:Caller hostname (audit trail); pid:number:required:Caller \
|
||||||
pid:number:required:Caller process id (audit trail); \
|
process id (audit trail); project:string:required:Project slug to associate with; \
|
||||||
project:string:required:Project slug to associate with; \
|
|
||||||
role:string:optional:Actor role; preferences:object:optional:Free-form actor \
|
role:string:optional:Actor role; preferences:object:optional:Free-form actor \
|
||||||
preferences",
|
preferences",
|
||||||
tags = "actors, auth"
|
tags = "actors, auth"
|
||||||
|
|
@ -1439,11 +1430,7 @@ async fn notifications_emit(
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
(
|
(StatusCode::ACCEPTED, Json(EmitResponse { id, project })).into_response()
|
||||||
StatusCode::ACCEPTED,
|
|
||||||
Json(EmitResponse { id, project }),
|
|
||||||
)
|
|
||||||
.into_response()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── SSE Notification Stream ───────────────────────────────────────────────
|
// ── SSE Notification Stream ───────────────────────────────────────────────
|
||||||
|
|
@ -3902,8 +3889,8 @@ struct IntrospectRequest {
|
||||||
path = "/.session/introspect",
|
path = "/.session/introspect",
|
||||||
auth = "none",
|
auth = "none",
|
||||||
actors = "agent, developer, ci, admin",
|
actors = "agent, developer, ci, admin",
|
||||||
params = "token:string:required:JWT to introspect; \
|
params = "token:string:required:JWT to introspect; audience:string:optional:Expected aud \
|
||||||
audience:string:optional:Expected aud (Exact match)",
|
(Exact match)",
|
||||||
tags = "meta, sso"
|
tags = "meta, sso"
|
||||||
)]
|
)]
|
||||||
async fn introspect_handler(
|
async fn introspect_handler(
|
||||||
|
|
@ -3959,7 +3946,8 @@ fn persist_keys_overlay(config_dir: &std::path::Path, registry: &crate::registry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Domain graph endpoints ────────────────────────────────────────────────────
|
// ── Domain graph endpoints
|
||||||
|
// ────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct DomainQuery {
|
struct DomainQuery {
|
||||||
|
|
@ -3981,11 +3969,16 @@ async fn domain_list(
|
||||||
Query(q): Query<DomainQuery>,
|
Query(q): Query<DomainQuery>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
state.touch_activity();
|
state.touch_activity();
|
||||||
let ctx = q.slug.as_deref()
|
let ctx = q
|
||||||
|
.slug
|
||||||
|
.as_deref()
|
||||||
.and_then(|s| state.registry.get(s))
|
.and_then(|s| state.registry.get(s))
|
||||||
.unwrap_or_else(|| state.registry.primary());
|
.unwrap_or_else(|| state.registry.primary());
|
||||||
let domains = crate::domain::project_domains(&ctx.root);
|
let domains = crate::domain::project_domains(&ctx.root);
|
||||||
(StatusCode::OK, Json(serde_json::json!({ "slug": ctx.slug, "domains": domains })))
|
(
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(serde_json::json!({ "slug": ctx.slug, "domains": domains })),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// All registered projects implementing a given domain id
|
/// All registered projects implementing a given domain id
|
||||||
|
|
@ -4004,10 +3997,18 @@ async fn domain_consumers(
|
||||||
state.touch_activity();
|
state.touch_activity();
|
||||||
let id = match q.id {
|
let id = match q.id {
|
||||||
Some(ref s) if !s.is_empty() => s.as_str(),
|
Some(ref s) if !s.is_empty() => s.as_str(),
|
||||||
_ => return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "id required" }))),
|
_ => {
|
||||||
|
return (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(serde_json::json!({ "error": "id required" })),
|
||||||
|
)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let consumers = crate::domain::consumers_of(&state.registry, id);
|
let consumers = crate::domain::consumers_of(&state.registry, id);
|
||||||
(StatusCode::OK, Json(serde_json::json!({ "domain_id": id, "consumers": consumers })))
|
(
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(serde_json::json!({ "domain_id": id, "consumers": consumers })),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Domain metadata from the ontoref domain registry
|
/// Domain metadata from the ontoref domain registry
|
||||||
|
|
@ -4026,7 +4027,12 @@ async fn domain_show(
|
||||||
state.touch_activity();
|
state.touch_activity();
|
||||||
let id = match q.id {
|
let id = match q.id {
|
||||||
Some(ref s) if !s.is_empty() => s.as_str(),
|
Some(ref s) if !s.is_empty() => s.as_str(),
|
||||||
_ => return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "id required" }))),
|
_ => {
|
||||||
|
return (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(serde_json::json!({ "error": "id required" })),
|
||||||
|
)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let root = state.registry.primary().root.clone();
|
let root = state.registry.primary().root.clone();
|
||||||
// Walk up to find ontoref root (domains/ dir).
|
// Walk up to find ontoref root (domains/ dir).
|
||||||
|
|
@ -4160,9 +4166,10 @@ async fn tier_next(
|
||||||
|
|
||||||
(
|
(
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Json(serde_json::to_value(projection).unwrap_or_else(|e| {
|
Json(
|
||||||
serde_json::json!({ "error": e.to_string() })
|
serde_json::to_value(projection)
|
||||||
})),
|
.unwrap_or_else(|e| serde_json::json!({ "error": e.to_string() })),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ use std::path::PathBuf;
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
use ontoref_daemon::config::{OpsConfig, Phase, Tier};
|
use ontoref_daemon::config::{OpsConfig, Phase, Tier};
|
||||||
use ontoref_daemon::tier_transition::{execute, TransitionContext, TransitionError};
|
use ontoref_daemon::tier_transition::{execute, TransitionContext, TransitionError};
|
||||||
|
|
||||||
|
|
@ -181,7 +180,8 @@ fn run() -> Result<(), String> {
|
||||||
Ok(outcome) => {
|
Ok(outcome) => {
|
||||||
if json {
|
if json {
|
||||||
println!(
|
println!(
|
||||||
"{{\"ok\":true,\"from\":\"{:?}\",\"to\":\"{:?}\",\"phase\":\"{:?}\",\"rewritten\":{}}}",
|
"{{\"ok\":true,\"from\":\"{:?}\",\"to\":\"{:?}\",\"phase\":\"{:?}\",\"\
|
||||||
|
rewritten\":{}}}",
|
||||||
outcome.from_tier,
|
outcome.from_tier,
|
||||||
outcome.to_tier,
|
outcome.to_tier,
|
||||||
outcome.phase,
|
outcome.phase,
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,10 @@ fn extract_config_flags(content: &str) -> Vec<String> {
|
||||||
let eq_pos = rest.find('=')?;
|
let eq_pos = rest.find('=')?;
|
||||||
let id = rest[..eq_pos].trim();
|
let id = rest[..eq_pos].trim();
|
||||||
let val = rest[eq_pos + 1..].trim();
|
let val = rest[eq_pos + 1..].trim();
|
||||||
if val.starts_with("true") && id.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') && !id.is_empty() {
|
if val.starts_with("true")
|
||||||
|
&& id.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
|
||||||
|
&& !id.is_empty()
|
||||||
|
{
|
||||||
Some(id.to_string())
|
Some(id.to_string())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
@ -74,7 +77,10 @@ fn extract_domain_origin(content: &str) -> Option<(String, String)> {
|
||||||
'{' => depth += 1,
|
'{' => depth += 1,
|
||||||
'}' => {
|
'}' => {
|
||||||
depth -= 1;
|
depth -= 1;
|
||||||
if depth == 0 { close = open + i; break; }
|
if depth == 0 {
|
||||||
|
close = open + i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
@ -84,16 +90,23 @@ fn extract_domain_origin(content: &str) -> Option<(String, String)> {
|
||||||
let id = inner.lines().find_map(|l| {
|
let id = inner.lines().find_map(|l| {
|
||||||
let t = l.trim();
|
let t = l.trim();
|
||||||
let rest = t.strip_prefix("id")?.trim_start_matches([' ', '=']).trim();
|
let rest = t.strip_prefix("id")?.trim_start_matches([' ', '=']).trim();
|
||||||
rest.strip_prefix('"')?.split('"').next().map(|s| {
|
rest.strip_prefix('"')?
|
||||||
s.replace("-framework", "").replace("-domain", "")
|
.split('"')
|
||||||
})
|
.next()
|
||||||
|
.map(|s| s.replace("-framework", "").replace("-domain", ""))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let path = inner.lines().find_map(|l| {
|
let path = inner
|
||||||
let t = l.trim();
|
.lines()
|
||||||
let rest = t.strip_prefix("path")?.trim_start_matches([' ', '=']).trim();
|
.find_map(|l| {
|
||||||
rest.strip_prefix('"')?.split('"').next().map(String::from)
|
let t = l.trim();
|
||||||
}).unwrap_or_default();
|
let rest = t
|
||||||
|
.strip_prefix("path")?
|
||||||
|
.trim_start_matches([' ', '='])
|
||||||
|
.trim();
|
||||||
|
rest.strip_prefix('"')?.split('"').next().map(String::from)
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
Some((id, path))
|
Some((id, path))
|
||||||
}
|
}
|
||||||
|
|
@ -107,7 +120,11 @@ pub fn project_domains(root: &Path) -> Vec<DomainDecl> {
|
||||||
if let Ok(content) = std::fs::read_to_string(&config_path) {
|
if let Ok(content) = std::fs::read_to_string(&config_path) {
|
||||||
for id in extract_config_flags(&content) {
|
for id in extract_config_flags(&content) {
|
||||||
if !domains.iter().any(|d| d.id == id) {
|
if !domains.iter().any(|d| d.id == id) {
|
||||||
domains.push(DomainDecl { id, kind: DomainDeclKind::ConfigFlag, path: String::new() });
|
domains.push(DomainDecl {
|
||||||
|
id,
|
||||||
|
kind: DomainDeclKind::ConfigFlag,
|
||||||
|
path: String::new(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -117,7 +134,11 @@ pub fn project_domains(root: &Path) -> Vec<DomainDecl> {
|
||||||
if let Ok(content) = std::fs::read_to_string(&manifest_path) {
|
if let Ok(content) = std::fs::read_to_string(&manifest_path) {
|
||||||
if let Some((id, path)) = extract_domain_origin(&content) {
|
if let Some((id, path)) = extract_domain_origin(&content) {
|
||||||
if !domains.iter().any(|d| d.id == id) {
|
if !domains.iter().any(|d| d.id == id) {
|
||||||
domains.push(DomainDecl { id, kind: DomainDeclKind::DomainOrigin, path });
|
domains.push(DomainDecl {
|
||||||
|
id,
|
||||||
|
kind: DomainDeclKind::DomainOrigin,
|
||||||
|
path,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -136,7 +157,11 @@ pub fn project_domains(root: &Path) -> Vec<DomainDecl> {
|
||||||
.trim_end_matches(".ontoref")
|
.trim_end_matches(".ontoref")
|
||||||
.to_string();
|
.to_string();
|
||||||
if !domains.iter().any(|d| d.id == id) {
|
if !domains.iter().any(|d| d.id == id) {
|
||||||
domains.push(DomainDecl { id, kind: DomainDeclKind::ImplDir, path: String::new() });
|
domains.push(DomainDecl {
|
||||||
|
id,
|
||||||
|
kind: DomainDeclKind::ImplDir,
|
||||||
|
path: String::new(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -152,16 +177,20 @@ pub fn consumers_of(registry: &ProjectRegistry, domain_id: &str) -> Vec<DomainCo
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|ctx| {
|
.filter_map(|ctx| {
|
||||||
let domains = project_domains(&ctx.root);
|
let domains = project_domains(&ctx.root);
|
||||||
domains.into_iter().find(|d| d.id == domain_id).map(|d| DomainConsumer {
|
domains
|
||||||
slug: ctx.slug.clone(),
|
.into_iter()
|
||||||
root: ctx.root.to_string_lossy().into_owned(),
|
.find(|d| d.id == domain_id)
|
||||||
kind: d.kind,
|
.map(|d| DomainConsumer {
|
||||||
})
|
slug: ctx.slug.clone(),
|
||||||
|
root: ctx.root.to_string_lossy().into_owned(),
|
||||||
|
kind: d.kind,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registered domain ids (directories under $ONTOREF_ROOT/domains/ with domain.ncl).
|
/// Registered domain ids (directories under $ONTOREF_ROOT/domains/ with
|
||||||
|
/// domain.ncl).
|
||||||
pub fn registered_domain_ids(ontoref_root: &Path) -> Vec<String> {
|
pub fn registered_domain_ids(ontoref_root: &Path) -> Vec<String> {
|
||||||
let domains_dir = ontoref_root.join("domains");
|
let domains_dir = ontoref_root.join("domains");
|
||||||
std::fs::read_dir(&domains_dir)
|
std::fs::read_dir(&domains_dir)
|
||||||
|
|
@ -183,15 +212,21 @@ pub struct DomainMeta {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_ncl_string_field(content: &str, field: &str) -> String {
|
fn extract_ncl_string_field(content: &str, field: &str) -> String {
|
||||||
content.lines().find_map(|l| {
|
content
|
||||||
let t = l.trim();
|
.lines()
|
||||||
let rest = t.strip_prefix(field)?.trim_start_matches([' ', '=']).trim();
|
.find_map(|l| {
|
||||||
rest.strip_prefix('"')?.split('"').next().map(String::from)
|
let t = l.trim();
|
||||||
}).unwrap_or_default()
|
let rest = t.strip_prefix(field)?.trim_start_matches([' ', '=']).trim();
|
||||||
|
rest.strip_prefix('"')?.split('"').next().map(String::from)
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn domain_meta(ontoref_root: &Path, domain_id: &str) -> Option<DomainMeta> {
|
pub fn domain_meta(ontoref_root: &Path, domain_id: &str) -> Option<DomainMeta> {
|
||||||
let ncl = ontoref_root.join("domains").join(domain_id).join("domain.ncl");
|
let ncl = ontoref_root
|
||||||
|
.join("domains")
|
||||||
|
.join(domain_id)
|
||||||
|
.join("domain.ncl");
|
||||||
let content = std::fs::read_to_string(&ncl).ok()?;
|
let content = std::fs::read_to_string(&ncl).ok()?;
|
||||||
Some(DomainMeta {
|
Some(DomainMeta {
|
||||||
id: domain_id.to_string(),
|
id: domain_id.to_string(),
|
||||||
|
|
@ -207,7 +242,8 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn extract_config_flags_finds_domain_declarations() {
|
fn extract_config_flags_finds_domain_declarations() {
|
||||||
let content = "ops = { tier = 'Tier0 },\n domain_provisioning = true,\n domain_rustelo = true,\n other = false,";
|
let content = "ops = { tier = 'Tier0 },\n domain_provisioning = true,\n domain_rustelo \
|
||||||
|
= true,\n other = false,";
|
||||||
let flags = extract_config_flags(content);
|
let flags = extract_config_flags(content);
|
||||||
assert!(flags.contains(&"provisioning".to_string()));
|
assert!(flags.contains(&"provisioning".to_string()));
|
||||||
assert!(flags.contains(&"rustelo".to_string()));
|
assert!(flags.contains(&"rustelo".to_string()));
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,8 @@ impl FederatedQuery {
|
||||||
if ctx.push_only {
|
if ctx.push_only {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let conn_path = ontoref_ontology::layout::resolve_section(&ctx.root, "ontology/connections.ncl");
|
let conn_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&ctx.root, "ontology/connections.ncl");
|
||||||
if !conn_path.exists() {
|
if !conn_path.exists() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -343,7 +344,8 @@ pub async fn validate_connections(registry: &Arc<ProjectRegistry>, slug: &str) -
|
||||||
let Some(ctx) = registry.get(slug) else {
|
let Some(ctx) = registry.get(slug) else {
|
||||||
return vec![format!("slug '{slug}' not found in registry")];
|
return vec![format!("slug '{slug}' not found in registry")];
|
||||||
};
|
};
|
||||||
let conn_path = ontoref_ontology::layout::resolve_section(&ctx.root, "ontology/connections.ncl");
|
let conn_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&ctx.root, "ontology/connections.ncl");
|
||||||
if !conn_path.exists() {
|
if !conn_path.exists() {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,20 @@
|
||||||
//! Constellation-memory GraphQL output types with single-source cross-surface equivalence.
|
//! Constellation-memory GraphQL output types with single-source cross-surface
|
||||||
|
//! equivalence.
|
||||||
//!
|
//!
|
||||||
//! Re-exports from [`crate::memory_types`] — the canonical single definition that uses
|
//! Re-exports from [`crate::memory_types`] — the canonical single definition
|
||||||
//! `#[cfg_attr(feature = "graphql", derive(SimpleObject))]` so the types compile on every
|
//! that uses `#[cfg_attr(feature = "graphql", derive(SimpleObject))]` so the
|
||||||
//! surface (HTTP, MCP, GraphQL) without duplication. G-4 coherence tests live here where
|
//! types compile on every surface (HTTP, MCP, GraphQL) without duplication. G-4
|
||||||
//! they can reference the `async_graphql` SDL generation logic.
|
//! coherence tests live here where they can reference the `async_graphql` SDL
|
||||||
|
//! generation logic.
|
||||||
|
|
||||||
pub use crate::memory_types::{ChronicleEntry, Criterion, PanelSpec, RoadmapItem};
|
pub use crate::memory_types::{ChronicleEntry, Criterion, PanelSpec, RoadmapItem};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
|
||||||
use async_graphql::{EmptyMutation, EmptySubscription, Object, Schema};
|
use async_graphql::{EmptyMutation, EmptySubscription, Object, Schema};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
fn serde_keys<T: serde::Serialize>(value: &T) -> Vec<String> {
|
fn serde_keys<T: serde::Serialize>(value: &T) -> Vec<String> {
|
||||||
let v = serde_json::to_value(value).expect("entity must serialize");
|
let v = serde_json::to_value(value).expect("entity must serialize");
|
||||||
let mut keys: Vec<String> = v
|
let mut keys: Vec<String> = v
|
||||||
|
|
@ -91,7 +94,14 @@ mod tests {
|
||||||
surfaces: vec!["Ui".into()],
|
surfaces: vec!["Ui".into()],
|
||||||
actors: vec!["Developer".into()],
|
actors: vec!["Developer".into()],
|
||||||
}),
|
}),
|
||||||
sorted(&["id", "title", "kind", "data_source_op", "surfaces", "actors"]),
|
sorted(&[
|
||||||
|
"id",
|
||||||
|
"title",
|
||||||
|
"kind",
|
||||||
|
"data_source_op",
|
||||||
|
"surfaces",
|
||||||
|
"actors"
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -888,15 +888,19 @@ impl QueryRoot {
|
||||||
slug: Option<String>,
|
slug: Option<String>,
|
||||||
) -> async_graphql::Result<Vec<DomainDeclGql>> {
|
) -> async_graphql::Result<Vec<DomainDeclGql>> {
|
||||||
let state = ctx.data::<AppState>()?;
|
let state = ctx.data::<AppState>()?;
|
||||||
let pctx = slug.as_deref()
|
let pctx = slug
|
||||||
|
.as_deref()
|
||||||
.and_then(|s| state.registry.get(s))
|
.and_then(|s| state.registry.get(s))
|
||||||
.unwrap_or_else(|| state.registry.primary());
|
.unwrap_or_else(|| state.registry.primary());
|
||||||
let decls = crate::domain::project_domains(&pctx.root);
|
let decls = crate::domain::project_domains(&pctx.root);
|
||||||
Ok(decls.into_iter().map(|d| DomainDeclGql {
|
Ok(decls
|
||||||
id: d.id,
|
.into_iter()
|
||||||
kind: format!("{:?}", d.kind),
|
.map(|d| DomainDeclGql {
|
||||||
path: d.path,
|
id: d.id,
|
||||||
}).collect())
|
kind: format!("{:?}", d.kind),
|
||||||
|
path: d.path,
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// All registered projects implementing a given domain.
|
/// All registered projects implementing a given domain.
|
||||||
|
|
@ -907,11 +911,14 @@ impl QueryRoot {
|
||||||
) -> async_graphql::Result<Vec<DomainConsumerGql>> {
|
) -> async_graphql::Result<Vec<DomainConsumerGql>> {
|
||||||
let state = ctx.data::<AppState>()?;
|
let state = ctx.data::<AppState>()?;
|
||||||
let consumers = crate::domain::consumers_of(&state.registry, &domain_id);
|
let consumers = crate::domain::consumers_of(&state.registry, &domain_id);
|
||||||
Ok(consumers.into_iter().map(|c| DomainConsumerGql {
|
Ok(consumers
|
||||||
slug: c.slug,
|
.into_iter()
|
||||||
root: c.root,
|
.map(|c| DomainConsumerGql {
|
||||||
kind: format!("{:?}", c.kind),
|
slug: c.slug,
|
||||||
}).collect())
|
root: c.root,
|
||||||
|
kind: format!("{:?}", c.kind),
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Domain metadata + all its consumers.
|
/// Domain metadata + all its consumers.
|
||||||
|
|
@ -933,11 +940,14 @@ impl QueryRoot {
|
||||||
name: meta.name,
|
name: meta.name,
|
||||||
description: meta.description,
|
description: meta.description,
|
||||||
schema_cmd: meta.schema_cmd,
|
schema_cmd: meta.schema_cmd,
|
||||||
consumers: consumers.into_iter().map(|c| DomainConsumerGql {
|
consumers: consumers
|
||||||
slug: c.slug,
|
.into_iter()
|
||||||
root: c.root,
|
.map(|c| DomainConsumerGql {
|
||||||
kind: format!("{:?}", c.kind),
|
slug: c.slug,
|
||||||
}).collect(),
|
root: c.root,
|
||||||
|
kind: format!("{:?}", c.kind),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -948,7 +958,9 @@ impl QueryRoot {
|
||||||
slug: Option<String>,
|
slug: Option<String>,
|
||||||
) -> async_graphql::Result<TierStatusGql> {
|
) -> async_graphql::Result<TierStatusGql> {
|
||||||
let state = ctx.data::<AppState>()?;
|
let state = ctx.data::<AppState>()?;
|
||||||
let effective = slug.as_deref().unwrap_or_else(|| state.registry.primary_slug());
|
let effective = slug
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or_else(|| state.registry.primary_slug());
|
||||||
let pctx = state
|
let pctx = state
|
||||||
.registry
|
.registry
|
||||||
.get(effective)
|
.get(effective)
|
||||||
|
|
@ -979,7 +991,9 @@ impl QueryRoot {
|
||||||
slug: Option<String>,
|
slug: Option<String>,
|
||||||
) -> async_graphql::Result<TierProjectionGql> {
|
) -> async_graphql::Result<TierProjectionGql> {
|
||||||
let state = ctx.data::<AppState>()?;
|
let state = ctx.data::<AppState>()?;
|
||||||
let effective = slug.as_deref().unwrap_or_else(|| state.registry.primary_slug());
|
let effective = slug
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or_else(|| state.registry.primary_slug());
|
||||||
let pctx = state
|
let pctx = state
|
||||||
.registry
|
.registry
|
||||||
.get(effective)
|
.get(effective)
|
||||||
|
|
@ -1002,7 +1016,8 @@ impl QueryRoot {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Chronicle timeline entries woven from Accepted ADRs and protocol migrations.
|
/// Chronicle timeline entries woven from Accepted ADRs and protocol
|
||||||
|
/// migrations.
|
||||||
async fn chronicle(
|
async fn chronicle(
|
||||||
&self,
|
&self,
|
||||||
ctx: &Context<'_>,
|
ctx: &Context<'_>,
|
||||||
|
|
@ -1053,13 +1068,10 @@ impl QueryRoot {
|
||||||
) -> async_graphql::Result<Vec<Criterion>> {
|
) -> async_graphql::Result<Vec<Criterion>> {
|
||||||
let state = ctx.data::<AppState>()?;
|
let state = ctx.data::<AppState>()?;
|
||||||
let (root, cache, import_path) = project_ctx(state, slug.as_deref());
|
let (root, cache, import_path) = project_ctx(state, slug.as_deref());
|
||||||
Ok(crate::memory::criteria_list(
|
Ok(
|
||||||
&root,
|
crate::memory::criteria_list(&root, &cache, import_path.as_deref(), kind.as_deref())
|
||||||
&cache,
|
.await,
|
||||||
import_path.as_deref(),
|
|
||||||
kind.as_deref(),
|
|
||||||
)
|
)
|
||||||
.await)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolved panel specs from the presentation directory (ADR-053).
|
/// Resolved panel specs from the presentation directory (ADR-053).
|
||||||
|
|
@ -1071,13 +1083,10 @@ impl QueryRoot {
|
||||||
) -> async_graphql::Result<Vec<PanelSpec>> {
|
) -> async_graphql::Result<Vec<PanelSpec>> {
|
||||||
let state = ctx.data::<AppState>()?;
|
let state = ctx.data::<AppState>()?;
|
||||||
let (root, cache, import_path) = project_ctx(state, slug.as_deref());
|
let (root, cache, import_path) = project_ctx(state, slug.as_deref());
|
||||||
Ok(crate::memory::panels_list(
|
Ok(
|
||||||
&root,
|
crate::memory::panels_list(&root, &cache, import_path.as_deref(), kind.as_deref())
|
||||||
&cache,
|
.await,
|
||||||
import_path.as_deref(),
|
|
||||||
kind.as_deref(),
|
|
||||||
)
|
)
|
||||||
.await)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Combined config.ncl + card.ncl for a project.
|
/// Combined config.ncl + card.ncl for a project.
|
||||||
|
|
|
||||||
|
|
@ -175,10 +175,10 @@ pub fn sign_payload_for_actor(
|
||||||
|
|
||||||
/// Verify a payload signature given a hex-encoded public key (32 bytes) and a
|
/// Verify a payload signature given a hex-encoded public key (32 bytes) and a
|
||||||
/// hex-encoded Ed25519 signature (64 bytes). Returns `false` for any malformed
|
/// hex-encoded Ed25519 signature (64 bytes). Returns `false` for any malformed
|
||||||
/// input rather than erroring — the caller treats verification as a boolean gate.
|
/// input rather than erroring — the caller treats verification as a boolean
|
||||||
|
/// gate.
|
||||||
pub fn verify_payload_hex(public_key_hex: &str, payload: &[u8], signature_hex: &str) -> bool {
|
pub fn verify_payload_hex(public_key_hex: &str, payload: &[u8], signature_hex: &str) -> bool {
|
||||||
let (Some(pk_bytes), Some(sig_bytes)) =
|
let (Some(pk_bytes), Some(sig_bytes)) = (hex_decode(public_key_hex), hex_decode(signature_hex))
|
||||||
(hex_decode(public_key_hex), hex_decode(signature_hex))
|
|
||||||
else {
|
else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
@ -212,7 +212,8 @@ pub fn register_actor_in_ncl(
|
||||||
validate_actor_id(actor_id)?;
|
validate_actor_id(actor_id)?;
|
||||||
let pk_hex = public_key_hex(public_key);
|
let pk_hex = public_key_hex(public_key);
|
||||||
let entry = format!(
|
let entry = format!(
|
||||||
" {{ id = \"{actor_id}\", public_key = \"{pk_hex}\", created_at = \"{created_at}\" }},\n"
|
" {{ id = \"{actor_id}\", public_key = \"{pk_hex}\", created_at = \"{created_at}\" \
|
||||||
|
}},\n"
|
||||||
);
|
);
|
||||||
|
|
||||||
if !actors_ncl.exists() {
|
if !actors_ncl.exists() {
|
||||||
|
|
|
||||||
|
|
@ -19,30 +19,30 @@ pub mod config_coherence;
|
||||||
pub mod domain;
|
pub mod domain;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod federation;
|
pub mod federation;
|
||||||
pub mod keypair;
|
|
||||||
#[cfg(feature = "nats")]
|
|
||||||
pub mod nats_sync;
|
|
||||||
pub mod ops_surface;
|
|
||||||
#[cfg(feature = "substrate")]
|
|
||||||
pub mod rotation;
|
|
||||||
#[cfg(feature = "graphql")]
|
#[cfg(feature = "graphql")]
|
||||||
pub mod graphql;
|
pub mod graphql;
|
||||||
|
pub mod keypair;
|
||||||
#[cfg(feature = "mcp")]
|
#[cfg(feature = "mcp")]
|
||||||
pub mod mcp;
|
pub mod mcp;
|
||||||
#[cfg(feature = "nats")]
|
|
||||||
pub mod nats;
|
|
||||||
pub mod notifications;
|
|
||||||
pub mod positioning;
|
|
||||||
pub mod registry;
|
|
||||||
pub mod memory;
|
pub mod memory;
|
||||||
pub mod memory_types;
|
pub mod memory_types;
|
||||||
pub mod views;
|
#[cfg(feature = "nats")]
|
||||||
|
pub mod nats;
|
||||||
|
#[cfg(feature = "nats")]
|
||||||
|
pub mod nats_sync;
|
||||||
|
pub mod notifications;
|
||||||
|
pub mod ops_surface;
|
||||||
|
pub mod positioning;
|
||||||
|
pub mod registry;
|
||||||
|
#[cfg(feature = "substrate")]
|
||||||
|
pub mod rotation;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
#[cfg(feature = "db")]
|
#[cfg(feature = "db")]
|
||||||
pub mod seed;
|
pub mod seed;
|
||||||
#[cfg(feature = "ui")]
|
#[cfg(feature = "ui")]
|
||||||
pub mod session;
|
pub mod session;
|
||||||
pub mod sync;
|
pub mod sync;
|
||||||
|
pub mod views;
|
||||||
// ADR-033 tier/transition/reload primitives — moved to ontoref-ops so the
|
// ADR-033 tier/transition/reload primitives — moved to ontoref-ops so the
|
||||||
// catalogued op handler can reuse them. Re-exported here for backward
|
// catalogued op handler can reuse them. Re-exported here for backward
|
||||||
// compatibility with consumers that imported via ontoref_daemon::*.
|
// compatibility with consumers that imported via ontoref_daemon::*.
|
||||||
|
|
|
||||||
|
|
@ -320,14 +320,14 @@ struct Cli {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
actor: Option<String>,
|
actor: Option<String>,
|
||||||
|
|
||||||
/// Sign an arbitrary payload (e.g. a substrate-view adjustment digest, ADR-046
|
/// Sign an arbitrary payload (e.g. a substrate-view adjustment digest,
|
||||||
/// C2) with the actor's Ed25519 key, print `{actor, payload, signature,
|
/// ADR-046 C2) with the actor's Ed25519 key, print `{actor, payload,
|
||||||
/// public_key}` as JSON, then exit. Requires --actor.
|
/// signature, public_key}` as JSON, then exit. Requires --actor.
|
||||||
#[arg(long, value_name = "PAYLOAD", requires = "actor")]
|
#[arg(long, value_name = "PAYLOAD", requires = "actor")]
|
||||||
sign_payload: Option<String>,
|
sign_payload: Option<String>,
|
||||||
|
|
||||||
/// Verify a payload signature. Requires --actor-pubkey and --signature. Prints
|
/// Verify a payload signature. Requires --actor-pubkey and --signature.
|
||||||
/// `{valid}` and exits 0 (valid) or 4 (invalid).
|
/// Prints `{valid}` and exits 0 (valid) or 4 (invalid).
|
||||||
#[arg(long, value_name = "PAYLOAD", requires_all = ["actor_pubkey", "signature"])]
|
#[arg(long, value_name = "PAYLOAD", requires_all = ["actor_pubkey", "signature"])]
|
||||||
verify_payload: Option<String>,
|
verify_payload: Option<String>,
|
||||||
|
|
||||||
|
|
@ -516,9 +516,7 @@ fn apply_keys_overlay(
|
||||||
fn init_tera(
|
fn init_tera(
|
||||||
templates_dir: Option<&std::path::Path>,
|
templates_dir: Option<&std::path::Path>,
|
||||||
) -> Option<Arc<tokio::sync::RwLock<ui::TeraEnv>>> {
|
) -> Option<Arc<tokio::sync::RwLock<ui::TeraEnv>>> {
|
||||||
use ui::{
|
use ui::{AssetUrls, Brand, BrandKind, SessionOptions, TeraOptions, ThemeOptions, UiFeatures};
|
||||||
AssetUrls, Brand, BrandKind, SessionOptions, TeraOptions, ThemeOptions, UiFeatures,
|
|
||||||
};
|
|
||||||
|
|
||||||
let tdir = templates_dir?;
|
let tdir = templates_dir?;
|
||||||
let cache_buster = std::time::SystemTime::now()
|
let cache_buster = std::time::SystemTime::now()
|
||||||
|
|
@ -681,7 +679,9 @@ async fn main() {
|
||||||
|
|
||||||
let Some(classification) = ontoref_ontology_content::OntologyType::parse(type_tag) else {
|
let Some(classification) = ontoref_ontology_content::OntologyType::parse(type_tag) else {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"extract-ontology: unknown --extract-type-tag '{type_tag}'; expected Type1Protocol | Type2aFederable | Type2bMandatoryDecentralized | Type3ProjectSpecific | Type4Private"
|
"extract-ontology: unknown --extract-type-tag '{type_tag}'; expected \
|
||||||
|
Type1Protocol | Type2aFederable | Type2bMandatoryDecentralized | \
|
||||||
|
Type3ProjectSpecific | Type4Private"
|
||||||
);
|
);
|
||||||
std::process::exit(2);
|
std::process::exit(2);
|
||||||
};
|
};
|
||||||
|
|
@ -756,8 +756,11 @@ async fn main() {
|
||||||
std::process::exit(2);
|
std::process::exit(2);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match ontoref_daemon::keypair::sign_payload_for_actor(actor_id, &key_dir, payload.as_bytes())
|
match ontoref_daemon::keypair::sign_payload_for_actor(
|
||||||
{
|
actor_id,
|
||||||
|
&key_dir,
|
||||||
|
payload.as_bytes(),
|
||||||
|
) {
|
||||||
Ok((signature, public_key)) => {
|
Ok((signature, public_key)) => {
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
|
|
@ -791,8 +794,8 @@ async fn main() {
|
||||||
// FIPS-204 plug-in slot: recognized scheme, backend not built into
|
// FIPS-204 plug-in slot: recognized scheme, backend not built into
|
||||||
// this binary (would require the `pqc` feature + a validated module).
|
// this binary (would require the `pqc` feature + a validated module).
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"verify-payload: scheme 'ml-dsa-65' recognized but not built — \
|
"verify-payload: scheme 'ml-dsa-65' recognized but not built — rebuild with \
|
||||||
rebuild with the pqc feature"
|
the pqc feature"
|
||||||
);
|
);
|
||||||
std::process::exit(5);
|
std::process::exit(5);
|
||||||
}
|
}
|
||||||
|
|
@ -1229,8 +1232,8 @@ async fn main() {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!(
|
warn!(
|
||||||
"ONTOREF_SIGNING_KEY_FILE not set — generating ephemeral Ed25519 key; \
|
"ONTOREF_SIGNING_KEY_FILE not set — generating ephemeral Ed25519 key; JWKS will \
|
||||||
JWKS will change on every restart"
|
change on every restart"
|
||||||
);
|
);
|
||||||
Arc::new(auth::TokenIssuer::generate(issuer_id))
|
Arc::new(auth::TokenIssuer::generate(issuer_id))
|
||||||
}
|
}
|
||||||
|
|
@ -1380,11 +1383,7 @@ async fn main() {
|
||||||
// the guard via `ProjectContext::apply_ops_reload`; rejected reloads
|
// the guard via `ProjectContext::apply_ops_reload`; rejected reloads
|
||||||
// log a Block warning and the in-memory ops value is retained.
|
// log a Block warning and the in-memory ops value is retained.
|
||||||
let _ops_watcher = {
|
let _ops_watcher = {
|
||||||
let ops_config_path = registry
|
let ops_config_path = registry.primary().root.join(".ontoref").join("config.ncl");
|
||||||
.primary()
|
|
||||||
.root
|
|
||||||
.join(".ontoref")
|
|
||||||
.join("config.ncl");
|
|
||||||
if ops_config_path.exists() {
|
if ops_config_path.exists() {
|
||||||
let primary_ref = registry.primary();
|
let primary_ref = registry.primary();
|
||||||
match ontoref_daemon::watcher::OpsWatcher::start(
|
match ontoref_daemon::watcher::OpsWatcher::start(
|
||||||
|
|
@ -1461,8 +1460,8 @@ async fn main() {
|
||||||
.layer(axum::middleware::from_fn(
|
.layer(axum::middleware::from_fn(
|
||||||
|req: axum::extract::Request, next: axum::middleware::Next| async move {
|
|req: axum::extract::Request, next: axum::middleware::Next| async move {
|
||||||
let method = req.method().clone();
|
let method = req.method().clone();
|
||||||
let uri = req.uri().clone();
|
let uri = req.uri().clone();
|
||||||
let resp = next.run(req).await;
|
let resp = next.run(req).await;
|
||||||
let status = resp.status().as_u16();
|
let status = resp.status().as_u16();
|
||||||
if status >= 500 {
|
if status >= 500 {
|
||||||
tracing::error!(method = %method, uri = %uri, status = status, "5xx");
|
tracing::error!(method = %method, uri = %uri, status = status, "5xx");
|
||||||
|
|
@ -1954,8 +1953,8 @@ fn extract_ontology_now(
|
||||||
String::from_utf8_lossy(&output.stderr).trim()
|
String::from_utf8_lossy(&output.stderr).trim()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let json: serde_json::Value = serde_json::from_slice(&output.stdout)
|
let json: serde_json::Value =
|
||||||
.map_err(|e| format!("parsing nickel JSON: {e}"))?;
|
serde_json::from_slice(&output.stdout).map_err(|e| format!("parsing nickel JSON: {e}"))?;
|
||||||
|
|
||||||
// Deterministic signing key derived from the ontology name — keeps
|
// Deterministic signing key derived from the ontology name — keeps
|
||||||
// re-runs idempotent without requiring per-extraction keypair setup.
|
// re-runs idempotent without requiring per-extraction keypair setup.
|
||||||
|
|
@ -1973,9 +1972,8 @@ fn extract_ontology_now(
|
||||||
.map_err(|e| format!("creating {}: {e}", parent.display()))?;
|
.map_err(|e| format!("creating {}: {e}", parent.display()))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let stats =
|
let stats = ontoref_ontology_content::extract_core_to_oplog(&json, &oplog_path, &signing_key)
|
||||||
ontoref_ontology_content::extract_core_to_oplog(&json, &oplog_path, &signing_key)
|
.map_err(|e| format!("extraction: {e}"))?;
|
||||||
.map_err(|e| format!("extraction: {e}"))?;
|
|
||||||
|
|
||||||
let refs_path = ontoref_ontology::layout::resolve_section(project_root, "ontology/_refs.ncl");
|
let refs_path = ontoref_ontology::layout::resolve_section(project_root, "ontology/_refs.ncl");
|
||||||
ontoref_ontology_content::write_ontology_refs(
|
ontoref_ontology_content::write_ontology_refs(
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,8 @@ struct GetItemInput {
|
||||||
|
|
||||||
#[derive(Deserialize, JsonSchema, Default)]
|
#[derive(Deserialize, JsonSchema, Default)]
|
||||||
struct DisciplineInput {
|
struct DisciplineInput {
|
||||||
/// Discipline (work area) to mount — e.g. `coding`, `positioning`, `difusion`.
|
/// Discipline (work area) to mount — e.g. `coding`, `positioning`,
|
||||||
|
/// `difusion`.
|
||||||
discipline: String,
|
discipline: String,
|
||||||
/// Project slug. Omit to use the default project.
|
/// Project slug. Omit to use the default project.
|
||||||
project: Option<String>,
|
project: Option<String>,
|
||||||
|
|
@ -602,11 +603,12 @@ impl AsyncTool<OntoreServer> for ViewListTool {
|
||||||
|
|
||||||
#[ontoref_derive::onto_mcp_tool(
|
#[ontoref_derive::onto_mcp_tool(
|
||||||
name = "ontoref_view_mount",
|
name = "ontoref_view_mount",
|
||||||
description = "Mount a discipline's substrate view: the composed recipe (declared prior \
|
description = "Mount a discipline's substrate view: the composed recipe (declared prior plus \
|
||||||
plus learned tuning) of which levels to load, ordered by salience, with \
|
learned tuning) of which levels to load, ordered by salience, with out-of-repo \
|
||||||
out-of-repo levels resolved by witness. The agent-facing provisioning surface.",
|
levels resolved by witness. The agent-facing provisioning surface.",
|
||||||
category = "discovery",
|
category = "discovery",
|
||||||
params = "discipline:string:required:Discipline/work area (e.g. coding, positioning); project:string:optional:Project slug"
|
params = "discipline:string:required:Discipline/work area (e.g. coding, positioning); \
|
||||||
|
project:string:optional:Project slug"
|
||||||
)]
|
)]
|
||||||
struct ViewMountTool;
|
struct ViewMountTool;
|
||||||
|
|
||||||
|
|
@ -620,7 +622,11 @@ impl ToolBase for ViewMountTool {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn description() -> Option<Cow<'static, str>> {
|
fn description() -> Option<Cow<'static, str>> {
|
||||||
Some("Mount a discipline's substrate view — composed recipe, prior plus learned tuning (ADR-046).".into())
|
Some(
|
||||||
|
"Mount a discipline's substrate view — composed recipe, prior plus learned tuning \
|
||||||
|
(ADR-046)."
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn output_schema() -> Option<Arc<JsonObject>> {
|
fn output_schema() -> Option<Arc<JsonObject>> {
|
||||||
|
|
@ -691,7 +697,8 @@ impl AsyncTool<OntoreServer> for DescribeProjectTool {
|
||||||
) -> Result<serde_json::Value, ToolError> {
|
) -> Result<serde_json::Value, ToolError> {
|
||||||
debug!(tool = "describe_project", project = ?param.project);
|
debug!(tool = "describe_project", project = ?param.project);
|
||||||
let ctx = service.project_ctx(param.project.as_deref());
|
let ctx = service.project_ctx(param.project.as_deref());
|
||||||
let manifest_path = ontoref_ontology::layout::resolve_section(&ctx.root, "ontology/manifest.ncl");
|
let manifest_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&ctx.root, "ontology/manifest.ncl");
|
||||||
|
|
||||||
let manifest = if manifest_path.exists() {
|
let manifest = if manifest_path.exists() {
|
||||||
match ctx
|
match ctx
|
||||||
|
|
@ -1001,7 +1008,8 @@ impl AsyncTool<OntoreServer> for GetOntologyExtensionTool {
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = ontoref_ontology::layout::resolve_section(&ctx.root, &format!("ontology/{file}"));
|
let path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&ctx.root, &format!("ontology/{file}"));
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
return Err(ToolError(format!(
|
return Err(ToolError(format!(
|
||||||
"ontology extension '{}' not found",
|
"ontology extension '{}' not found",
|
||||||
|
|
@ -1203,7 +1211,8 @@ impl AsyncTool<OntoreServer> for GetBacklogTool {
|
||||||
) -> Result<serde_json::Value, ToolError> {
|
) -> Result<serde_json::Value, ToolError> {
|
||||||
debug!(tool = "get_backlog", project = ?param.project, status = ?param.status);
|
debug!(tool = "get_backlog", project = ?param.project, status = ?param.status);
|
||||||
let ctx = service.project_ctx(param.project.as_deref());
|
let ctx = service.project_ctx(param.project.as_deref());
|
||||||
let backlog_path = ontoref_ontology::layout::resolve_section(&ctx.root, "reflection/backlog.ncl");
|
let backlog_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&ctx.root, "reflection/backlog.ncl");
|
||||||
|
|
||||||
if !backlog_path.exists() {
|
if !backlog_path.exists() {
|
||||||
return Ok(serde_json::json!({ "items": [] }));
|
return Ok(serde_json::json!({ "items": [] }));
|
||||||
|
|
@ -1555,7 +1564,8 @@ impl AsyncTool<OntoreServer> for ProjectStatusTool {
|
||||||
let recent_notifications = service.state.notifications.all_recent().len();
|
let recent_notifications = service.state.notifications.all_recent().len();
|
||||||
|
|
||||||
// Backlog stats
|
// Backlog stats
|
||||||
let backlog_path = ontoref_ontology::layout::resolve_section(&ctx.root, "reflection/backlog.ncl");
|
let backlog_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&ctx.root, "reflection/backlog.ncl");
|
||||||
let backlog_stats = if backlog_path.exists() {
|
let backlog_stats = if backlog_path.exists() {
|
||||||
match ctx
|
match ctx
|
||||||
.cache
|
.cache
|
||||||
|
|
@ -1585,25 +1595,30 @@ impl AsyncTool<OntoreServer> for ProjectStatusTool {
|
||||||
};
|
};
|
||||||
|
|
||||||
// ADR count
|
// ADR count
|
||||||
let adr_count = std::fs::read_dir(ontoref_ontology::layout::resolve_section(&ctx.root, "adrs"))
|
let adr_count =
|
||||||
.map(|rd| {
|
std::fs::read_dir(ontoref_ontology::layout::resolve_section(&ctx.root, "adrs"))
|
||||||
rd.flatten()
|
.map(|rd| {
|
||||||
.filter(|e| e.path().extension().and_then(|x| x.to_str()) == Some("ncl"))
|
rd.flatten()
|
||||||
.count()
|
.filter(|e| e.path().extension().and_then(|x| x.to_str()) == Some("ncl"))
|
||||||
})
|
.count()
|
||||||
.unwrap_or(0);
|
})
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
// Mode count
|
// Mode count
|
||||||
let mode_count = std::fs::read_dir(ontoref_ontology::layout::resolve_section(&ctx.root, "reflection/modes"))
|
let mode_count = std::fs::read_dir(ontoref_ontology::layout::resolve_section(
|
||||||
.map(|rd| {
|
&ctx.root,
|
||||||
rd.flatten()
|
"reflection/modes",
|
||||||
.filter(|e| e.path().extension().and_then(|x| x.to_str()) == Some("ncl"))
|
))
|
||||||
.count()
|
.map(|rd| {
|
||||||
})
|
rd.flatten()
|
||||||
.unwrap_or(0);
|
.filter(|e| e.path().extension().and_then(|x| x.to_str()) == Some("ncl"))
|
||||||
|
.count()
|
||||||
|
})
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
// Manifest
|
// Manifest
|
||||||
let manifest_path = ontoref_ontology::layout::resolve_section(&ctx.root, "ontology/manifest.ncl");
|
let manifest_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&ctx.root, "ontology/manifest.ncl");
|
||||||
let manifest = if manifest_path.exists() {
|
let manifest = if manifest_path.exists() {
|
||||||
match ctx
|
match ctx
|
||||||
.cache
|
.cache
|
||||||
|
|
@ -1732,7 +1747,8 @@ impl AsyncTool<OntoreServer> for BacklogOpTool {
|
||||||
) -> Result<serde_json::Value, ToolError> {
|
) -> Result<serde_json::Value, ToolError> {
|
||||||
debug!(tool = "backlog", operation = %param.operation, project = ?param.project);
|
debug!(tool = "backlog", operation = %param.operation, project = ?param.project);
|
||||||
let ctx = service.project_ctx(param.project.as_deref());
|
let ctx = service.project_ctx(param.project.as_deref());
|
||||||
let backlog_path = ontoref_ontology::layout::resolve_section(&ctx.root, "reflection/backlog.ncl");
|
let backlog_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&ctx.root, "reflection/backlog.ncl");
|
||||||
|
|
||||||
if !backlog_path.exists() {
|
if !backlog_path.exists() {
|
||||||
return Err(ToolError(format!(
|
return Err(ToolError(format!(
|
||||||
|
|
@ -2816,7 +2832,8 @@ impl AsyncTool<OntoreServer> for BookmarkListTool {
|
||||||
) -> Result<serde_json::Value, ToolError> {
|
) -> Result<serde_json::Value, ToolError> {
|
||||||
debug!(tool = "bookmark_list", project = ?param.project);
|
debug!(tool = "bookmark_list", project = ?param.project);
|
||||||
let ctx = service.project_ctx(param.project.as_deref());
|
let ctx = service.project_ctx(param.project.as_deref());
|
||||||
let bm_path = ontoref_ontology::layout::resolve_section(&ctx.root, "reflection/search_bookmarks.ncl");
|
let bm_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&ctx.root, "reflection/search_bookmarks.ncl");
|
||||||
|
|
||||||
if !bm_path.exists() {
|
if !bm_path.exists() {
|
||||||
return Ok(serde_json::json!({ "entries": [], "count": 0 }));
|
return Ok(serde_json::json!({ "entries": [], "count": 0 }));
|
||||||
|
|
@ -2902,7 +2919,8 @@ impl AsyncTool<OntoreServer> for BookmarkAddTool {
|
||||||
) -> Result<serde_json::Value, ToolError> {
|
) -> Result<serde_json::Value, ToolError> {
|
||||||
debug!(tool = "bookmark_add", project = ?param.project, node_id = %param.node_id);
|
debug!(tool = "bookmark_add", project = ?param.project, node_id = %param.node_id);
|
||||||
let ctx = service.project_ctx(param.project.as_deref());
|
let ctx = service.project_ctx(param.project.as_deref());
|
||||||
let bm_path = ontoref_ontology::layout::resolve_section(&ctx.root, "reflection/search_bookmarks.ncl");
|
let bm_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&ctx.root, "reflection/search_bookmarks.ncl");
|
||||||
|
|
||||||
if !bm_path.exists() {
|
if !bm_path.exists() {
|
||||||
return Err(ToolError(format!(
|
return Err(ToolError(format!(
|
||||||
|
|
@ -3537,12 +3555,7 @@ impl AsyncTool<OntoreServer> for PositioningGetTool {
|
||||||
let live_value_props = p
|
let live_value_props = p
|
||||||
.value_props()
|
.value_props()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|v| {
|
.filter(|v| matches!(v.status, ontoref_ontology::types::PositioningStatus::Live))
|
||||||
matches!(
|
|
||||||
v.status,
|
|
||||||
ontoref_ontology::types::PositioningStatus::Live
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.count();
|
.count();
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
"audiences": p.audiences(),
|
"audiences": p.audiences(),
|
||||||
|
|
@ -3582,8 +3595,8 @@ impl ToolBase for PositioningGetValuePropTool {
|
||||||
|
|
||||||
fn description() -> Option<Cow<'static, str>> {
|
fn description() -> Option<Cow<'static, str>> {
|
||||||
Some(
|
Some(
|
||||||
"Single value-prop by id with audiences expanded inline. Saves a second tool call: \
|
"Single value-prop by id with audiences expanded inline. Saves a second tool call: an \
|
||||||
an agent reasoning about an audience for a claim gets the resolved Audience records \
|
agent reasoning about an audience for a claim gets the resolved Audience records \
|
||||||
alongside the value-prop in one response."
|
alongside the value-prop in one response."
|
||||||
.into(),
|
.into(),
|
||||||
)
|
)
|
||||||
|
|
@ -3643,9 +3656,10 @@ impl ToolBase for PositioningAuditTool {
|
||||||
|
|
||||||
fn description() -> Option<Cow<'static, str>> {
|
fn description() -> Option<Cow<'static, str>> {
|
||||||
Some(
|
Some(
|
||||||
"Cross-reference evidence_adrs against ADR status across all value-props. Drift kinds: \
|
"Cross-reference evidence_adrs against ADR status across all value-props. Drift \
|
||||||
Missing (cited file absent), Superseded (with optional by= replacement), Deprecated, \
|
kinds: Missing (cited file absent), Superseded (with optional by= replacement), \
|
||||||
Proposed (claim cites uncommitted decision). Returns { checked_value_props, drift }."
|
Deprecated, Proposed (claim cites uncommitted decision). Returns { \
|
||||||
|
checked_value_props, drift }."
|
||||||
.into(),
|
.into(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -3686,25 +3700,34 @@ impl ToolBase for DomainListTool {
|
||||||
type Output = serde_json::Value;
|
type Output = serde_json::Value;
|
||||||
type Error = ToolError;
|
type Error = ToolError;
|
||||||
|
|
||||||
fn name() -> Cow<'static, str> { "ontoref_domain_list".into() }
|
fn name() -> Cow<'static, str> {
|
||||||
|
"ontoref_domain_list".into()
|
||||||
|
}
|
||||||
|
|
||||||
fn description() -> Option<Cow<'static, str>> {
|
fn description() -> Option<Cow<'static, str>> {
|
||||||
Some(
|
Some(
|
||||||
"Return all domains the project implements as { id, kind, path }[]. \
|
"Return all domains the project implements as { id, kind, path }[]. kind: config_flag \
|
||||||
kind: config_flag (domain_X=true in config.ncl), domain_origin (manifest.ncl), \
|
(domain_X=true in config.ncl), domain_origin (manifest.ncl), or impl_dir \
|
||||||
or impl_dir (.{domain}.ontoref/ directory). \
|
(.{domain}.ontoref/ directory). Use to navigate from a consumer project to its \
|
||||||
Use to navigate from a consumer project to its domain source."
|
domain source."
|
||||||
.into(),
|
.into(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn output_schema() -> Option<Arc<JsonObject>> { None }
|
fn output_schema() -> Option<Arc<JsonObject>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsyncTool<OntoreServer> for DomainListTool {
|
impl AsyncTool<OntoreServer> for DomainListTool {
|
||||||
async fn invoke(service: &OntoreServer, param: ProjectParam) -> Result<serde_json::Value, ToolError> {
|
async fn invoke(
|
||||||
|
service: &OntoreServer,
|
||||||
|
param: ProjectParam,
|
||||||
|
) -> Result<serde_json::Value, ToolError> {
|
||||||
debug!(tool = "domain_list", project = ?param.project);
|
debug!(tool = "domain_list", project = ?param.project);
|
||||||
let pctx = param.project.as_deref()
|
let pctx = param
|
||||||
|
.project
|
||||||
|
.as_deref()
|
||||||
.and_then(|s| service.state.registry.get(s))
|
.and_then(|s| service.state.registry.get(s))
|
||||||
.unwrap_or_else(|| service.state.registry.primary());
|
.unwrap_or_else(|| service.state.registry.primary());
|
||||||
let decls = crate::domain::project_domains(&pctx.root);
|
let decls = crate::domain::project_domains(&pctx.root);
|
||||||
|
|
@ -3722,7 +3745,8 @@ struct DomainIdInput {
|
||||||
|
|
||||||
#[ontoref_derive::onto_mcp_tool(
|
#[ontoref_derive::onto_mcp_tool(
|
||||||
name = "ontoref_domain_consumers",
|
name = "ontoref_domain_consumers",
|
||||||
description = "List all registered projects that implement a given domain (domain → consumers direction).",
|
description = "List all registered projects that implement a given domain (domain → consumers \
|
||||||
|
direction).",
|
||||||
category = "domain",
|
category = "domain",
|
||||||
params = "domain_id:string:required:Domain id (e.g. rustelo, provisioning)"
|
params = "domain_id:string:required:Domain id (e.g. rustelo, provisioning)"
|
||||||
)]
|
)]
|
||||||
|
|
@ -3733,22 +3757,29 @@ impl ToolBase for DomainConsumersTool {
|
||||||
type Output = serde_json::Value;
|
type Output = serde_json::Value;
|
||||||
type Error = ToolError;
|
type Error = ToolError;
|
||||||
|
|
||||||
fn name() -> Cow<'static, str> { "ontoref_domain_consumers".into() }
|
fn name() -> Cow<'static, str> {
|
||||||
|
"ontoref_domain_consumers".into()
|
||||||
|
}
|
||||||
|
|
||||||
fn description() -> Option<Cow<'static, str>> {
|
fn description() -> Option<Cow<'static, str>> {
|
||||||
Some(
|
Some(
|
||||||
"Return all registered projects implementing the given domain id. \
|
"Return all registered projects implementing the given domain id. Result: { \
|
||||||
Result: { domain_id, consumers: [{ slug, root, kind }] }. \
|
domain_id, consumers: [{ slug, root, kind }] }. Use to navigate from a domain \
|
||||||
Use to navigate from a domain project to all its consumer implementations."
|
project to all its consumer implementations."
|
||||||
.into(),
|
.into(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn output_schema() -> Option<Arc<JsonObject>> { None }
|
fn output_schema() -> Option<Arc<JsonObject>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsyncTool<OntoreServer> for DomainConsumersTool {
|
impl AsyncTool<OntoreServer> for DomainConsumersTool {
|
||||||
async fn invoke(service: &OntoreServer, param: DomainIdInput) -> Result<serde_json::Value, ToolError> {
|
async fn invoke(
|
||||||
|
service: &OntoreServer,
|
||||||
|
param: DomainIdInput,
|
||||||
|
) -> Result<serde_json::Value, ToolError> {
|
||||||
debug!(tool = "domain_consumers", id = %param.domain_id);
|
debug!(tool = "domain_consumers", id = %param.domain_id);
|
||||||
let consumers = crate::domain::consumers_of(&service.state.registry, ¶m.domain_id);
|
let consumers = crate::domain::consumers_of(&service.state.registry, ¶m.domain_id);
|
||||||
Ok(serde_json::json!({ "domain_id": param.domain_id, "consumers": consumers }))
|
Ok(serde_json::json!({ "domain_id": param.domain_id, "consumers": consumers }))
|
||||||
|
|
@ -3770,21 +3801,28 @@ impl ToolBase for DomainShowTool {
|
||||||
type Output = serde_json::Value;
|
type Output = serde_json::Value;
|
||||||
type Error = ToolError;
|
type Error = ToolError;
|
||||||
|
|
||||||
fn name() -> Cow<'static, str> { "ontoref_domain_show".into() }
|
fn name() -> Cow<'static, str> {
|
||||||
|
"ontoref_domain_show".into()
|
||||||
|
}
|
||||||
|
|
||||||
fn description() -> Option<Cow<'static, str>> {
|
fn description() -> Option<Cow<'static, str>> {
|
||||||
Some(
|
Some(
|
||||||
"Return domain metadata (name, description, schema_cmd) and all registered \
|
"Return domain metadata (name, description, schema_cmd) and all registered consumer \
|
||||||
consumer projects. Combines the domain definition with the live consumer graph."
|
projects. Combines the domain definition with the live consumer graph."
|
||||||
.into(),
|
.into(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn output_schema() -> Option<Arc<JsonObject>> { None }
|
fn output_schema() -> Option<Arc<JsonObject>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsyncTool<OntoreServer> for DomainShowTool {
|
impl AsyncTool<OntoreServer> for DomainShowTool {
|
||||||
async fn invoke(service: &OntoreServer, param: DomainIdInput) -> Result<serde_json::Value, ToolError> {
|
async fn invoke(
|
||||||
|
service: &OntoreServer,
|
||||||
|
param: DomainIdInput,
|
||||||
|
) -> Result<serde_json::Value, ToolError> {
|
||||||
debug!(tool = "domain_show", id = %param.domain_id);
|
debug!(tool = "domain_show", id = %param.domain_id);
|
||||||
let ontoref_root = std::env::var("ONTOREF_ROOT")
|
let ontoref_root = std::env::var("ONTOREF_ROOT")
|
||||||
.map(std::path::PathBuf::from)
|
.map(std::path::PathBuf::from)
|
||||||
|
|
@ -3971,9 +4009,8 @@ impl AsyncTool<OntoreServer> for TierNextMcpTool {
|
||||||
name = "ontoref_chronicle",
|
name = "ontoref_chronicle",
|
||||||
description = "Chronicle timeline: Accepted ADRs and protocol migrations, newest first.",
|
description = "Chronicle timeline: Accepted ADRs and protocol migrations, newest first.",
|
||||||
category = "memory",
|
category = "memory",
|
||||||
params = "project:string:optional:Project slug; \
|
params = "project:string:optional:Project slug; since:string:optional:ISO date lower bound \
|
||||||
since:string:optional:ISO date lower bound (e.g. 2026-01-01); \
|
(e.g. 2026-01-01); kind:string:optional:adr|migration; \
|
||||||
kind:string:optional:adr|migration; \
|
|
||||||
domain:string:optional:Domain filter"
|
domain:string:optional:Domain filter"
|
||||||
)]
|
)]
|
||||||
struct ChronicleListTool;
|
struct ChronicleListTool;
|
||||||
|
|
@ -4069,8 +4106,7 @@ impl AsyncTool<OntoreServer> for RoadmapListTool {
|
||||||
name = "ontoref_criteria",
|
name = "ontoref_criteria",
|
||||||
description = "Criteria promoted to catalog validators (ADR-049/050).",
|
description = "Criteria promoted to catalog validators (ADR-049/050).",
|
||||||
category = "memory",
|
category = "memory",
|
||||||
params = "project:string:optional:Project slug; \
|
params = "project:string:optional:Project slug; kind:string:optional:normative"
|
||||||
kind:string:optional:normative"
|
|
||||||
)]
|
)]
|
||||||
struct CriteriaListTool;
|
struct CriteriaListTool;
|
||||||
|
|
||||||
|
|
@ -4106,10 +4142,8 @@ impl AsyncTool<OntoreServer> for CriteriaListTool {
|
||||||
param.kind.as_deref(),
|
param.kind.as_deref(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
serde_json::to_value(
|
serde_json::to_value(serde_json::json!({ "count": criteria.len(), "criteria": criteria }))
|
||||||
serde_json::json!({ "count": criteria.len(), "criteria": criteria }),
|
.map_err(ToolError::from)
|
||||||
)
|
|
||||||
.map_err(ToolError::from)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
//! Read-query functions for the constellation-memory surface (component J,
|
//! Read-query functions for the constellation-memory surface (component J,
|
||||||
//! ADR-051 / ADR-053). Each function reads NCL files via the cache and returns
|
//! ADR-051 / ADR-053). Each function reads NCL files via the cache and returns
|
||||||
//! typed vectors — the same underlying data regardless of which surface calls it
|
//! typed vectors — the same underlying data regardless of which surface calls
|
||||||
//! (HTTP GET, GraphQL resolver, MCP tool). This is the G-4 equivalence contract
|
//! it (HTTP GET, GraphQL resolver, MCP tool). This is the G-4 equivalence
|
||||||
//! made concrete: one data path, four display surfaces.
|
//! contract made concrete: one data path, four display surfaces.
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -16,9 +16,10 @@ fn str_val<'a>(v: &'a serde_json::Value, key: &str) -> &'a str {
|
||||||
|
|
||||||
/// Chronicle entries woven from ADRs and protocol migrations.
|
/// Chronicle entries woven from ADRs and protocol migrations.
|
||||||
///
|
///
|
||||||
/// Sources: `.ontoref/adrs/*.ncl` (Accepted status) + `.ontoref/reflection/migrations/*.ncl`.
|
/// Sources: `.ontoref/adrs/*.ncl` (Accepted status) +
|
||||||
/// Optional filters: `since` (ISO date prefix), `kind` ("adr" | "migration"), `domain`.
|
/// `.ontoref/reflection/migrations/*.ncl`. Optional filters: `since` (ISO date
|
||||||
/// Result is sorted descending by date.
|
/// prefix), `kind` ("adr" | "migration"), `domain`. Result is sorted descending
|
||||||
|
/// by date.
|
||||||
pub async fn chronicle_entries(
|
pub async fn chronicle_entries(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
cache: &Arc<NclCache>,
|
cache: &Arc<NclCache>,
|
||||||
|
|
@ -47,13 +48,20 @@ pub async fn chronicle_entries(
|
||||||
let id = str_val(&json, "id").to_owned();
|
let id = str_val(&json, "id").to_owned();
|
||||||
let date = str_val(&json, "date").to_owned();
|
let date = str_val(&json, "date").to_owned();
|
||||||
let title_raw = str_val(&json, "title");
|
let title_raw = str_val(&json, "title");
|
||||||
let title = if title_raw.is_empty() { id.clone() } else { title_raw.to_owned() };
|
let title = if title_raw.is_empty() {
|
||||||
|
id.clone()
|
||||||
|
} else {
|
||||||
|
title_raw.to_owned()
|
||||||
|
};
|
||||||
entries.push(ChronicleEntry {
|
entries.push(ChronicleEntry {
|
||||||
id: format!("adr:{id}"),
|
id: format!("adr:{id}"),
|
||||||
date,
|
date,
|
||||||
kind: "adr".to_owned(),
|
kind: "adr".to_owned(),
|
||||||
title,
|
title,
|
||||||
summary: json.get("context").and_then(|v| v.as_str()).map(str::to_owned),
|
summary: json
|
||||||
|
.get("context")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(str::to_owned),
|
||||||
domain: None,
|
domain: None,
|
||||||
refs: if id.is_empty() { vec![] } else { vec![id] },
|
refs: if id.is_empty() { vec![] } else { vec![id] },
|
||||||
});
|
});
|
||||||
|
|
@ -121,7 +129,8 @@ pub async fn chronicle_entries(
|
||||||
|
|
||||||
/// Roadmap items derived from backlog entries.
|
/// Roadmap items derived from backlog entries.
|
||||||
///
|
///
|
||||||
/// Source: `.ontoref/reflection/backlog.ncl`. Optional filters: `status`, `horizon`.
|
/// Source: `.ontoref/reflection/backlog.ncl`. Optional filters: `status`,
|
||||||
|
/// `horizon`.
|
||||||
pub async fn roadmap_items(
|
pub async fn roadmap_items(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
cache: &Arc<NclCache>,
|
cache: &Arc<NclCache>,
|
||||||
|
|
@ -185,8 +194,9 @@ pub async fn roadmap_items(
|
||||||
|
|
||||||
/// Criteria derived from catalog validator declarations.
|
/// Criteria derived from catalog validator declarations.
|
||||||
///
|
///
|
||||||
/// Source: `.ontoref/catalog/validators/*.ncl`. Optional filter: `kind` ("normative").
|
/// Source: `.ontoref/catalog/validators/*.ncl`. Optional filter: `kind`
|
||||||
/// All catalog-declared validators are normative (explicitly promoted per ADR-050).
|
/// ("normative"). All catalog-declared validators are normative (explicitly
|
||||||
|
/// promoted per ADR-050).
|
||||||
pub async fn criteria_list(
|
pub async fn criteria_list(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
cache: &Arc<NclCache>,
|
cache: &Arc<NclCache>,
|
||||||
|
|
@ -232,11 +242,19 @@ pub async fn criteria_list(
|
||||||
provenance: None,
|
provenance: None,
|
||||||
validator_ref: {
|
validator_ref: {
|
||||||
let r = str_val(&json, "predicate_ref");
|
let r = str_val(&json, "predicate_ref");
|
||||||
if r.is_empty() { None } else { Some(r.to_owned()) }
|
if r.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(r.to_owned())
|
||||||
|
}
|
||||||
},
|
},
|
||||||
related_adr: {
|
related_adr: {
|
||||||
let r = str_val(&json, "criterion_ref");
|
let r = str_val(&json, "criterion_ref");
|
||||||
if r.is_empty() { None } else { Some(r.to_owned()) }
|
if r.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(r.to_owned())
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -245,9 +263,11 @@ pub async fn criteria_list(
|
||||||
criteria
|
criteria
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Panel specs resolved from dedicated panel-*.ncl files in the presentation directory.
|
/// Panel specs resolved from dedicated panel-*.ncl files in the presentation
|
||||||
|
/// directory.
|
||||||
///
|
///
|
||||||
/// Source: `.ontoref/presentation/panel-*.ncl`. Optional filter: `kind` (Timeline/Gantt/…).
|
/// Source: `.ontoref/presentation/panel-*.ncl`. Optional filter: `kind`
|
||||||
|
/// (Timeline/Gantt/…).
|
||||||
pub async fn panels_list(
|
pub async fn panels_list(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
cache: &Arc<NclCache>,
|
cache: &Arc<NclCache>,
|
||||||
|
|
@ -266,12 +286,8 @@ pub async fn panels_list(
|
||||||
let mut panels = Vec::new();
|
let mut panels = Vec::new();
|
||||||
for entry in rd.flatten() {
|
for entry in rd.flatten() {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
let stem = path
|
let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
|
||||||
.file_stem()
|
if path.extension().and_then(|e| e.to_str()) != Some("ncl") || !stem.starts_with("panel-") {
|
||||||
.and_then(|s| s.to_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
if path.extension().and_then(|e| e.to_str()) != Some("ncl") || !stem.starts_with("panel-")
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Ok((json, _)) = cache.export(&path, import_path).await else {
|
let Ok((json, _)) = cache.export(&path, import_path).await else {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// One woven point on the retrospective chronicle timeline (ADR-051, component B).
|
/// One woven point on the retrospective chronicle timeline (ADR-051, component
|
||||||
|
/// B).
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "graphql", derive(async_graphql::SimpleObject))]
|
#[cfg_attr(feature = "graphql", derive(async_graphql::SimpleObject))]
|
||||||
pub struct ChronicleEntry {
|
pub struct ChronicleEntry {
|
||||||
|
|
@ -21,7 +22,8 @@ pub struct ChronicleEntry {
|
||||||
pub refs: Vec<String>,
|
pub refs: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A roadmap item composed from the backlog roadmap and positioning roadmap (component B/G).
|
/// A roadmap item composed from the backlog roadmap and positioning roadmap
|
||||||
|
/// (component B/G).
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "graphql", derive(async_graphql::SimpleObject))]
|
#[cfg_attr(feature = "graphql", derive(async_graphql::SimpleObject))]
|
||||||
pub struct RoadmapItem {
|
pub struct RoadmapItem {
|
||||||
|
|
@ -46,7 +48,8 @@ pub struct Criterion {
|
||||||
pub related_adr: Option<String>,
|
pub related_adr: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A resolved panel descriptor — identifying and dispatch fields of a Panel (ADR-053).
|
/// A resolved panel descriptor — identifying and dispatch fields of a Panel
|
||||||
|
/// (ADR-053).
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "graphql", derive(async_graphql::SimpleObject))]
|
#[cfg_attr(feature = "graphql", derive(async_graphql::SimpleObject))]
|
||||||
pub struct PanelSpec {
|
pub struct PanelSpec {
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,11 @@ use std::collections::HashMap;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use tokio::runtime::Runtime;
|
|
||||||
|
|
||||||
use ontoref_sync::{SyncBackend, SyncError};
|
use ontoref_sync::{SyncBackend, SyncError};
|
||||||
use ontoref_types::{canonical, Operation};
|
use ontoref_types::{canonical, Operation};
|
||||||
use platform_nats::topology::{AckPolicy, ConsumerDef, Retention, Storage, StreamDef};
|
use platform_nats::topology::{AckPolicy, ConsumerDef, Retention, Storage, StreamDef};
|
||||||
use platform_nats::{EventStream, NatsConnectionConfig, TopologyConfig};
|
use platform_nats::{EventStream, NatsConnectionConfig, TopologyConfig};
|
||||||
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
/// NATS JetStream sync backend bound to one project stream and one durable
|
/// NATS JetStream sync backend bound to one project stream and one durable
|
||||||
/// consumer (per local peer). Holds its own runtime — see module docs.
|
/// consumer (per local peer). Holds its own runtime — see module docs.
|
||||||
|
|
@ -239,13 +238,14 @@ fn anyhow_io(context: &str, e: &anyhow::Error) -> SyncError {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
|
||||||
use ed25519_dalek::SigningKey;
|
use ed25519_dalek::SigningKey;
|
||||||
use ontoref_types::{
|
use ontoref_types::{
|
||||||
operation::{OpBody, OpPayload},
|
operation::{OpBody, OpPayload},
|
||||||
AttrId, EntityId, Hlc, PublicKey, Value,
|
AttrId, EntityId, Hlc, PublicKey, Value,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
/// End-to-end round-trip against a real broker. Env-gated on
|
/// End-to-end round-trip against a real broker. Env-gated on
|
||||||
/// `ONTOREF_NATS_URL` (e.g. `nats://0.0.0.0:4222`); skips when unset so
|
/// `ONTOREF_NATS_URL` (e.g. `nats://0.0.0.0:4222`); skips when unset so
|
||||||
/// CI without a broker stays green. Run with the broker:
|
/// CI without a broker stays green. Run with the broker:
|
||||||
|
|
|
||||||
|
|
@ -8,22 +8,20 @@
|
||||||
//! produce equivalent witnesses for the same `(op_id, inputs)`; this
|
//! produce equivalent witnesses for the same `(op_id, inputs)`; this
|
||||||
//! function is the contract.
|
//! function is the contract.
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use ed25519_dalek::SigningKey;
|
|
||||||
use ontoref_ontology::types::StateConfig;
|
|
||||||
use ontoref_ops::{dispatch_op, OpContext, OpError, PipelineResult};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[cfg(feature = "substrate")]
|
#[cfg(feature = "substrate")]
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
#[cfg(feature = "substrate")]
|
#[cfg(feature = "substrate")]
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
#[cfg(feature = "substrate")]
|
#[cfg(feature = "substrate")]
|
||||||
use std::sync::{Arc, Mutex, OnceLock};
|
use std::sync::{Arc, Mutex, OnceLock};
|
||||||
|
|
||||||
|
use ed25519_dalek::SigningKey;
|
||||||
|
use ontoref_ontology::types::StateConfig;
|
||||||
#[cfg(feature = "substrate")]
|
#[cfg(feature = "substrate")]
|
||||||
use ontoref_oplog::OpLog;
|
use ontoref_oplog::OpLog;
|
||||||
|
use ontoref_ops::{dispatch_op, OpContext, OpError, PipelineResult};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// JSON envelope every surface decodes into. `inputs` is the op-specific
|
/// JSON envelope every surface decodes into. `inputs` is the op-specific
|
||||||
/// payload; the remaining fields configure the [`OpContext`].
|
/// payload; the remaining fields configure the [`OpContext`].
|
||||||
|
|
@ -102,10 +100,11 @@ fn parse_seed_hex(hex: &str) -> Result<[u8; 32], DispatchSurfaceError> {
|
||||||
}
|
}
|
||||||
let mut out = [0u8; 32];
|
let mut out = [0u8; 32];
|
||||||
for i in 0..32 {
|
for i in 0..32 {
|
||||||
out[i] = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16)
|
out[i] = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).map_err(|_| {
|
||||||
.map_err(|_| DispatchSurfaceError::InvalidSeed {
|
DispatchSurfaceError::InvalidSeed {
|
||||||
reason: format!("non-hex byte at position {}", i * 2),
|
reason: format!("non-hex byte at position {}", i * 2),
|
||||||
})?;
|
}
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
@ -186,11 +185,12 @@ fn oplog_for(project_root: &Path) -> Result<Option<Arc<OpLog>>, DispatchSurfaceE
|
||||||
if let Some(existing) = cache.get(&oplog_path) {
|
if let Some(existing) = cache.get(&oplog_path) {
|
||||||
return Ok(Some(Arc::clone(existing)));
|
return Ok(Some(Arc::clone(existing)));
|
||||||
}
|
}
|
||||||
let opened = Arc::new(OpLog::open(&oplog_path).map_err(|e| {
|
let opened =
|
||||||
DispatchSurfaceError::OplogUnavailable {
|
Arc::new(
|
||||||
reason: format!("{}: {e}", oplog_path.display()),
|
OpLog::open(&oplog_path).map_err(|e| DispatchSurfaceError::OplogUnavailable {
|
||||||
}
|
reason: format!("{}: {e}", oplog_path.display()),
|
||||||
})?);
|
})?,
|
||||||
|
);
|
||||||
cache.insert(oplog_path, Arc::clone(&opened));
|
cache.insert(oplog_path, Arc::clone(&opened));
|
||||||
Ok(Some(opened))
|
Ok(Some(opened))
|
||||||
}
|
}
|
||||||
|
|
@ -352,7 +352,11 @@ mod substrate_tests {
|
||||||
"no oplog file → tier-0 → in-memory, no persistence"
|
"no oplog file → tier-0 → in-memory, no persistence"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!root.join(".ontoref").join("substrate").join("oplog.redb").exists(),
|
!root
|
||||||
|
.join(".ontoref")
|
||||||
|
.join("substrate")
|
||||||
|
.join("oplog.redb")
|
||||||
|
.exists(),
|
||||||
"in-memory dispatch must not create an oplog"
|
"in-memory dispatch must not create an oplog"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
//! - `GET /api/positioning` — full layer
|
//! - `GET /api/positioning` — full layer
|
||||||
//! - `GET /api/positioning/audience/:id` — single audience
|
//! - `GET /api/positioning/audience/:id` — single audience
|
||||||
//! - `GET /api/positioning/value-prop/:id` — single value-prop
|
//! - `GET /api/positioning/value-prop/:id` — single value-prop
|
||||||
//! - `POST /api/positioning/audit` — drift report (evidence_adrs → ADR status)
|
//! - `POST /api/positioning/audit` — drift report (evidence_adrs → ADR
|
||||||
|
//! status)
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
|
@ -92,9 +93,7 @@ pub async fn positioning_get(
|
||||||
let realized_viability_paths = p
|
let realized_viability_paths = p
|
||||||
.viability_paths()
|
.viability_paths()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|v| {
|
.filter(|v| v.status != PositioningStatus::Draft && v.realized_outcome.is_some())
|
||||||
v.status != PositioningStatus::Draft && v.realized_outcome.is_some()
|
|
||||||
})
|
|
||||||
.count();
|
.count();
|
||||||
let counts = PositioningCounts {
|
let counts = PositioningCounts {
|
||||||
audiences: p.audiences().len(),
|
audiences: p.audiences().len(),
|
||||||
|
|
@ -194,7 +193,8 @@ pub enum AdrIssue {
|
||||||
/// - `Missing` — cited ADR file does not exist
|
/// - `Missing` — cited ADR file does not exist
|
||||||
/// - `Superseded` — ADR status is `'Superseded`
|
/// - `Superseded` — ADR status is `'Superseded`
|
||||||
/// - `Deprecated` — ADR status is `'Deprecated`
|
/// - `Deprecated` — ADR status is `'Deprecated`
|
||||||
/// - `Proposed` — ADR is still `'Proposed` (claim cites uncommitted decision)
|
/// - `Proposed` — ADR is still `'Proposed` (claim cites uncommitted
|
||||||
|
/// decision)
|
||||||
///
|
///
|
||||||
/// Only Accepted ADRs are clean backing for a Live value-prop. The audit
|
/// Only Accepted ADRs are clean backing for a Live value-prop. The audit
|
||||||
/// reports drift across ALL value-props regardless of status — `Draft` ones
|
/// reports drift across ALL value-props regardless of status — `Draft` ones
|
||||||
|
|
@ -218,10 +218,7 @@ pub async fn positioning_audit(
|
||||||
///
|
///
|
||||||
/// Extracted from [`positioning_audit`] so the MCP surface and any other
|
/// Extracted from [`positioning_audit`] so the MCP surface and any other
|
||||||
/// caller can reuse it without going through axum extractors.
|
/// caller can reuse it without going through axum extractors.
|
||||||
pub(crate) fn audit_positioning(
|
pub(crate) fn audit_positioning(p: &Positioning, adrs_dir: &Path) -> AuditReport {
|
||||||
p: &Positioning,
|
|
||||||
adrs_dir: &Path,
|
|
||||||
) -> AuditReport {
|
|
||||||
let mut drift: Vec<DriftEntry> = Vec::new();
|
let mut drift: Vec<DriftEntry> = Vec::new();
|
||||||
for vp in p.value_props() {
|
for vp in p.value_props() {
|
||||||
for adr_id in &vp.evidence_adrs {
|
for adr_id in &vp.evidence_adrs {
|
||||||
|
|
@ -317,10 +314,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn classify_proposed_returns_proposed() {
|
fn classify_proposed_returns_proposed() {
|
||||||
let txt = "status = 'Proposed,";
|
let txt = "status = 'Proposed,";
|
||||||
assert!(matches!(
|
assert!(matches!(classify_adr_status(txt), Some(AdrIssue::Proposed)));
|
||||||
classify_adr_status(txt),
|
|
||||||
Some(AdrIssue::Proposed)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -532,16 +532,16 @@ impl ProjectRegistry {
|
||||||
/// In addition to the project's declared paths, two sets of extras are always
|
/// In addition to the project's declared paths, two sets of extras are always
|
||||||
/// appended:
|
/// appended:
|
||||||
///
|
///
|
||||||
/// 1. **Project-local schema dirs** — derived from [`LayoutConfig::detect`]
|
/// 1. **Project-local schema dirs** — derived from [`LayoutConfig::detect`] via
|
||||||
/// via [`project_schema_dirs`]. Covers `.ontoref/ontology/`,
|
/// [`project_schema_dirs`]. Covers `.ontoref/ontology/`,
|
||||||
/// `.ontoref/ontology/schemas/`, `.ontoref/adrs/`, etc. so imports like
|
/// `.ontoref/ontology/schemas/`, `.ontoref/adrs/`, etc. so imports like
|
||||||
/// `import "manifest"` or `import "schemas/connections"` resolve from
|
/// `import "manifest"` or `import "schemas/connections"` resolve from
|
||||||
/// anywhere inside the project. Both canonical and legacy layouts are
|
/// anywhere inside the project. Both canonical and legacy layouts are
|
||||||
/// included when present on disk.
|
/// included when present on disk.
|
||||||
///
|
///
|
||||||
/// 2. **User-global schemas dir** — `~/.config/ontoref/schemas/` so
|
/// 2. **User-global schemas dir** — `~/.config/ontoref/schemas/` so `import
|
||||||
/// `import "ontoref-project.ncl"` (the canonical project schema)
|
/// "ontoref-project.ncl"` (the canonical project schema) resolves even when
|
||||||
/// resolves even when the project does not declare it.
|
/// the project does not declare it.
|
||||||
fn user_global_schemas_path() -> Option<String> {
|
fn user_global_schemas_path() -> Option<String> {
|
||||||
let home = std::env::var_os("HOME")?;
|
let home = std::env::var_os("HOME")?;
|
||||||
let p = Path::new(&home)
|
let p = Path::new(&home)
|
||||||
|
|
@ -646,12 +646,13 @@ pub struct ContextSpec {
|
||||||
/// - the manifest has no `config_surface` field
|
/// - the manifest has no `config_surface` field
|
||||||
/// - nickel export or deserialisation fails (logged at warn level)
|
/// - nickel export or deserialisation fails (logged at warn level)
|
||||||
pub fn load_config_surface(root: &Path, import_path: Option<&str>) -> Option<ConfigSurface> {
|
pub fn load_config_surface(root: &Path, import_path: Option<&str>) -> Option<ConfigSurface> {
|
||||||
// This used to hardcode the pre-0023 manifest location with no fallback, so on any
|
// This used to hardcode the pre-0023 manifest location with no fallback, so on
|
||||||
// project migrated to the consolidated layout the file was never found: the function
|
// any project migrated to the consolidated layout the file was never found:
|
||||||
// returned None and the config surface silently vanished. The resolver prefers the
|
// the function returned None and the config surface silently vanished. The
|
||||||
// canonical path and falls back to legacy, which is exactly what this call site needs.
|
// resolver prefers the canonical path and falls back to legacy, which is
|
||||||
// (Spelled in prose deliberately: the adr-032 invariant is a grep, and it cannot tell
|
// exactly what this call site needs. (Spelled in prose deliberately: the
|
||||||
// a comment quoting the drift from the drift itself.)
|
// adr-032 invariant is a grep, and it cannot tell a comment quoting the
|
||||||
|
// drift from the drift itself.)
|
||||||
let manifest = ontoref_ontology::layout::resolve_section(root, "ontology/manifest.ncl");
|
let manifest = ontoref_ontology::layout::resolve_section(root, "ontology/manifest.ncl");
|
||||||
if !manifest.exists() {
|
if !manifest.exists() {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -930,8 +931,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn absolute_path_returned_as_is() {
|
fn absolute_path_returned_as_is() {
|
||||||
let result =
|
let result = resolve_import_path_inner(&["/abs/path".to_string()], Path::new("/root"), &[]);
|
||||||
resolve_import_path_inner(&["/abs/path".to_string()], Path::new("/root"), &[]);
|
|
||||||
assert_eq!(result, Some("/abs/path".to_string()));
|
assert_eq!(result, Some("/abs/path".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -989,10 +989,11 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Materialise a PRE-0023 section dir. Named, rather than spelled inline, because the
|
/// Materialise a PRE-0023 section dir. Named, rather than spelled inline,
|
||||||
/// adr-032 invariant is enforced by grepping for hardcoded layout joins: a test that
|
/// because the adr-032 invariant is enforced by grepping for hardcoded
|
||||||
/// deliberately builds a legacy tree is correct code, and it must not read to the
|
/// layout joins: a test that deliberately builds a legacy tree is
|
||||||
/// checker as the very drift the check exists to catch.
|
/// correct code, and it must not read to the checker as the very drift
|
||||||
|
/// the check exists to catch.
|
||||||
fn legacy_section(root: &Path, relative: &str) -> PathBuf {
|
fn legacy_section(root: &Path, relative: &str) -> PathBuf {
|
||||||
root.join(relative)
|
root.join(relative)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,9 +82,10 @@ pub fn rotate_actor_key(
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
|
||||||
use ontoref_types::{operation::OpPayload, valid_keys_for_actor, PublicKey};
|
use ontoref_types::{operation::OpPayload, valid_keys_for_actor, PublicKey};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rotation_records_succession_and_preserves_old_key_validity() {
|
fn rotation_records_succession_and_preserves_old_key_validity() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
|
@ -117,11 +118,17 @@ mod tests {
|
||||||
let ops = oplog.all_ops_in_hlc_order().unwrap();
|
let ops = oplog.all_ops_in_hlc_order().unwrap();
|
||||||
assert_eq!(ops.len(), 1);
|
assert_eq!(ops.len(), 1);
|
||||||
ops[0].verify_signature().expect("succession verifies");
|
ops[0].verify_signature().expect("succession verifies");
|
||||||
assert!(matches!(ops[0].body.payload, OpPayload::KeySuccession { .. }));
|
assert!(matches!(
|
||||||
|
ops[0].body.payload,
|
||||||
|
OpPayload::KeySuccession { .. }
|
||||||
|
));
|
||||||
assert_eq!(ops[0].body.actor, old_pub, "signed by the retiring key");
|
assert_eq!(ops[0].body.actor, old_pub, "signed by the retiring key");
|
||||||
|
|
||||||
let valid = valid_keys_for_actor(&ops, "agent", new_pub);
|
let valid = valid_keys_for_actor(&ops, "agent", new_pub);
|
||||||
assert!(valid.contains(&old_pub), "witnesses under the old key still verify");
|
assert!(
|
||||||
|
valid.contains(&old_pub),
|
||||||
|
"witnesses under the old key still verify"
|
||||||
|
);
|
||||||
assert!(valid.contains(&new_pub));
|
assert!(valid.contains(&new_pub));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,12 @@
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub use auth::session::SessionView;
|
||||||
use auth::session::{
|
use auth::session::{
|
||||||
self as auth_session, RevokeAuthorisation, RevokeResult as AuthRevokeResult, Session,
|
self as auth_session, RevokeAuthorisation, RevokeResult as AuthRevokeResult, Session,
|
||||||
SessionStore as AuthSessionStore,
|
SessionStore as AuthSessionStore,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use auth::session::SessionView;
|
|
||||||
|
|
||||||
use crate::registry::Role;
|
use crate::registry::Role;
|
||||||
|
|
||||||
/// Cookie name used by the daemon UI for browser sessions.
|
/// Cookie name used by the daemon UI for browser sessions.
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,8 @@ use axum::extract::{Path, State};
|
||||||
use axum::http::{header, HeaderValue, StatusCode};
|
use axum::http::{header, HeaderValue, StatusCode};
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
|
|
||||||
use crate::api::AppState;
|
|
||||||
|
|
||||||
use super::common::UiError;
|
use super::common::UiError;
|
||||||
|
use crate::api::AppState;
|
||||||
|
|
||||||
/// Serve a file from `{project_root}/assets/` by slug + relative path.
|
/// Serve a file from `{project_root}/assets/` by slug + relative path.
|
||||||
/// Validates that the resolved path stays under the assets directory.
|
/// Validates that the resolved path stays under the assets directory.
|
||||||
|
|
@ -88,10 +87,7 @@ pub(crate) fn asset_content_type(path: &std::path::Path) -> &'static str {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generic file server for an arbitrary base directory.
|
/// Generic file server for an arbitrary base directory.
|
||||||
pub(crate) async fn serve_dir_from(
|
pub(crate) async fn serve_dir_from(base: &std::path::Path, rel: &str) -> Result<Response, UiError> {
|
||||||
base: &std::path::Path,
|
|
||||||
rel: &str,
|
|
||||||
) -> Result<Response, UiError> {
|
|
||||||
let Ok(canonical_base) = base.canonicalize() else {
|
let Ok(canonical_base) = base.canonicalize() else {
|
||||||
return Ok(StatusCode::NOT_FOUND.into_response());
|
return Ok(StatusCode::NOT_FOUND.into_response());
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,8 @@ pub(crate) fn backlog_stats(items: &[serde_json::Value]) -> BacklogStats {
|
||||||
|
|
||||||
pub async fn backlog_page(State(state): State<AppState>) -> Result<Html<String>, UiError> {
|
pub async fn backlog_page(State(state): State<AppState>) -> Result<Html<String>, UiError> {
|
||||||
let tera = tera_ref(&state)?;
|
let tera = tera_ref(&state)?;
|
||||||
let backlog_path = ontoref_ontology::layout::resolve_section(&state.project_root, "reflection/backlog.ncl");
|
let backlog_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&state.project_root, "reflection/backlog.ncl");
|
||||||
let items = if backlog_path.exists() {
|
let items = if backlog_path.exists() {
|
||||||
load_backlog_items(
|
load_backlog_items(
|
||||||
&state.cache,
|
&state.cache,
|
||||||
|
|
@ -136,7 +137,8 @@ pub async fn backlog_page_mp(
|
||||||
) -> Result<Html<String>, UiError> {
|
) -> Result<Html<String>, UiError> {
|
||||||
let tera = tera_ref(&state)?;
|
let tera = tera_ref(&state)?;
|
||||||
let proj = state.registry.get(&slug).ok_or(UiError::NotConfigured)?;
|
let proj = state.registry.get(&slug).ok_or(UiError::NotConfigured)?;
|
||||||
let backlog_path = ontoref_ontology::layout::resolve_section(&proj.root, "reflection/backlog.ncl");
|
let backlog_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&proj.root, "reflection/backlog.ncl");
|
||||||
let items = if backlog_path.exists() {
|
let items = if backlog_path.exists() {
|
||||||
load_backlog_items(&proj.cache, &backlog_path, proj.import_path.as_deref()).await
|
load_backlog_items(&proj.cache, &backlog_path, proj.import_path.as_deref()).await
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -170,13 +172,14 @@ pub async fn backlog_update_status(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Form(form): Form<BacklogStatusForm>,
|
Form(form): Form<BacklogStatusForm>,
|
||||||
) -> Result<Response, UiError> {
|
) -> Result<Response, UiError> {
|
||||||
let backlog_path = ontoref_ontology::layout::resolve_section(&state.project_root, "reflection/backlog.ncl");
|
let backlog_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&state.project_root, "reflection/backlog.ncl");
|
||||||
let _guard = state.ncl_write_lock.acquire(&backlog_path).await;
|
let _guard = state.ncl_write_lock.acquire(&backlog_path).await;
|
||||||
super::super::backlog_ncl::update_status(&backlog_path, &form.id, &form.status, &today_iso())
|
super::super::backlog_ncl::update_status(&backlog_path, &form.id, &form.status, &today_iso())
|
||||||
.map_err(|e| UiError::NclExport {
|
.map_err(|e| UiError::NclExport {
|
||||||
path: "backlog.ncl".into(),
|
path: "backlog.ncl".into(),
|
||||||
reason: e.to_string(),
|
reason: e.to_string(),
|
||||||
})?;
|
})?;
|
||||||
state
|
state
|
||||||
.cache
|
.cache
|
||||||
.invalidate_file(&backlog_path.canonicalize().unwrap_or(backlog_path));
|
.invalidate_file(&backlog_path.canonicalize().unwrap_or(backlog_path));
|
||||||
|
|
@ -196,13 +199,14 @@ pub async fn backlog_update_status_mp(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let proj = state.registry.get(&slug).ok_or(UiError::NotConfigured)?;
|
let proj = state.registry.get(&slug).ok_or(UiError::NotConfigured)?;
|
||||||
let backlog_path = ontoref_ontology::layout::resolve_section(&proj.root, "reflection/backlog.ncl");
|
let backlog_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&proj.root, "reflection/backlog.ncl");
|
||||||
let _guard = state.ncl_write_lock.acquire(&backlog_path).await;
|
let _guard = state.ncl_write_lock.acquire(&backlog_path).await;
|
||||||
super::super::backlog_ncl::update_status(&backlog_path, &form.id, &form.status, &today_iso())
|
super::super::backlog_ncl::update_status(&backlog_path, &form.id, &form.status, &today_iso())
|
||||||
.map_err(|e| UiError::NclExport {
|
.map_err(|e| UiError::NclExport {
|
||||||
path: "backlog.ncl".into(),
|
path: "backlog.ncl".into(),
|
||||||
reason: e.to_string(),
|
reason: e.to_string(),
|
||||||
})?;
|
})?;
|
||||||
proj.cache
|
proj.cache
|
||||||
.invalidate_file(&backlog_path.canonicalize().unwrap_or(backlog_path.clone()));
|
.invalidate_file(&backlog_path.canonicalize().unwrap_or(backlog_path.clone()));
|
||||||
|
|
||||||
|
|
@ -224,7 +228,8 @@ pub async fn backlog_add(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Form(form): Form<BacklogAddForm>,
|
Form(form): Form<BacklogAddForm>,
|
||||||
) -> Result<Response, UiError> {
|
) -> Result<Response, UiError> {
|
||||||
let backlog_path = ontoref_ontology::layout::resolve_section(&state.project_root, "reflection/backlog.ncl");
|
let backlog_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&state.project_root, "reflection/backlog.ncl");
|
||||||
let _guard = state.ncl_write_lock.acquire(&backlog_path).await;
|
let _guard = state.ncl_write_lock.acquire(&backlog_path).await;
|
||||||
super::super::backlog_ncl::add_item(
|
super::super::backlog_ncl::add_item(
|
||||||
&backlog_path,
|
&backlog_path,
|
||||||
|
|
@ -257,7 +262,8 @@ pub async fn backlog_add_mp(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let proj = state.registry.get(&slug).ok_or(UiError::NotConfigured)?;
|
let proj = state.registry.get(&slug).ok_or(UiError::NotConfigured)?;
|
||||||
let backlog_path = ontoref_ontology::layout::resolve_section(&proj.root, "reflection/backlog.ncl");
|
let backlog_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&proj.root, "reflection/backlog.ncl");
|
||||||
let _guard = state.ncl_write_lock.acquire(&backlog_path).await;
|
let _guard = state.ncl_write_lock.acquire(&backlog_path).await;
|
||||||
super::super::backlog_ncl::add_item(
|
super::super::backlog_ncl::add_item(
|
||||||
&backlog_path,
|
&backlog_path,
|
||||||
|
|
@ -301,7 +307,8 @@ pub async fn backlog_edit_mp(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let proj = state.registry.get(&slug).ok_or(UiError::NotConfigured)?;
|
let proj = state.registry.get(&slug).ok_or(UiError::NotConfigured)?;
|
||||||
let backlog_path = ontoref_ontology::layout::resolve_section(&proj.root, "reflection/backlog.ncl");
|
let backlog_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&proj.root, "reflection/backlog.ncl");
|
||||||
let _guard = state.ncl_write_lock.acquire(&backlog_path).await;
|
let _guard = state.ncl_write_lock.acquire(&backlog_path).await;
|
||||||
super::super::backlog_ncl::update_item(
|
super::super::backlog_ncl::update_item(
|
||||||
&backlog_path,
|
&backlog_path,
|
||||||
|
|
@ -352,7 +359,8 @@ pub async fn backlog_delete_mp(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let proj = state.registry.get(&slug).ok_or(UiError::NotConfigured)?;
|
let proj = state.registry.get(&slug).ok_or(UiError::NotConfigured)?;
|
||||||
let backlog_path = ontoref_ontology::layout::resolve_section(&proj.root, "reflection/backlog.ncl");
|
let backlog_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&proj.root, "reflection/backlog.ncl");
|
||||||
let _guard = state.ncl_write_lock.acquire(&backlog_path).await;
|
let _guard = state.ncl_write_lock.acquire(&backlog_path).await;
|
||||||
super::super::backlog_ncl::delete_item(&backlog_path, &form.id).map_err(|e| {
|
super::super::backlog_ncl::delete_item(&backlog_path, &form.id).map_err(|e| {
|
||||||
UiError::NclExport {
|
UiError::NclExport {
|
||||||
|
|
@ -386,7 +394,8 @@ pub async fn backlog_propose_status_mp(
|
||||||
) -> Result<Response, UiError> {
|
) -> Result<Response, UiError> {
|
||||||
let proj = state.registry.get(&slug).ok_or(UiError::NotConfigured)?;
|
let proj = state.registry.get(&slug).ok_or(UiError::NotConfigured)?;
|
||||||
|
|
||||||
let backlog_path = ontoref_ontology::layout::resolve_section(&proj.root, "reflection/backlog.ncl");
|
let backlog_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&proj.root, "reflection/backlog.ncl");
|
||||||
let items = load_backlog_items(&proj.cache, &backlog_path, proj.import_path.as_deref()).await;
|
let items = load_backlog_items(&proj.cache, &backlog_path, proj.import_path.as_deref()).await;
|
||||||
let item_title = items
|
let item_title = items
|
||||||
.iter()
|
.iter()
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use axum::response::{Html, IntoResponse, Response};
|
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
|
use axum::response::{Html, IntoResponse, Response};
|
||||||
use tera::Context;
|
use tera::Context;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
|
@ -479,7 +479,8 @@ pub(crate) async fn resolve_domain_ctx(
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_string();
|
.to_string();
|
||||||
for proj_ctx in registry.all() {
|
for proj_ctx in registry.all() {
|
||||||
let mpath = ontoref_ontology::layout::resolve_section(&proj_ctx.root, "ontology/manifest.ncl");
|
let mpath =
|
||||||
|
ontoref_ontology::layout::resolve_section(&proj_ctx.root, "ontology/manifest.ncl");
|
||||||
if !mpath.exists() {
|
if !mpath.exists() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,12 @@ use super::common::{
|
||||||
ncl_export, now_hms, readme_description, render, tera_ref, UiError,
|
ncl_export, now_hms, readme_description, render, tera_ref, UiError,
|
||||||
};
|
};
|
||||||
use super::graph_helpers::{
|
use super::graph_helpers::{
|
||||||
api_catalog_graph_nodes, manifest_graph_nodes, read_project_api_catalogs,
|
api_catalog_graph_nodes, manifest_graph_nodes, read_project_api_catalogs, state_graph_nodes,
|
||||||
state_graph_nodes, workspace_crate_nodes,
|
workspace_crate_nodes,
|
||||||
|
};
|
||||||
|
use super::picker::{
|
||||||
|
count_ncl_files, detect_generated, detect_showcase, git_remotes, load_pending_migrations,
|
||||||
};
|
};
|
||||||
use super::picker::{count_ncl_files, detect_generated, detect_showcase, load_pending_migrations, git_remotes};
|
|
||||||
use crate::api::AppState;
|
use crate::api::AppState;
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
|
|
@ -148,7 +150,8 @@ pub async fn dashboard_mp(
|
||||||
};
|
};
|
||||||
|
|
||||||
let base_url = format!("/ui/{slug}");
|
let base_url = format!("/ui/{slug}");
|
||||||
let backlog_path = ontoref_ontology::layout::resolve_section(&ctx_ref.root, "reflection/backlog.ncl");
|
let backlog_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&ctx_ref.root, "reflection/backlog.ncl");
|
||||||
let backlog_items = if backlog_path.exists() {
|
let backlog_items = if backlog_path.exists() {
|
||||||
load_backlog_items(
|
load_backlog_items(
|
||||||
&ctx_ref.cache,
|
&ctx_ref.cache,
|
||||||
|
|
@ -160,8 +163,14 @@ pub async fn dashboard_mp(
|
||||||
vec![]
|
vec![]
|
||||||
};
|
};
|
||||||
let backlog = backlog_stats(&backlog_items);
|
let backlog = backlog_stats(&backlog_items);
|
||||||
let adr_count = count_ncl_files(&ontoref_ontology::layout::resolve_section(&ctx_ref.root, "adrs"));
|
let adr_count = count_ncl_files(&ontoref_ontology::layout::resolve_section(
|
||||||
let mode_count = count_ncl_files(&ontoref_ontology::layout::resolve_section(&ctx_ref.root, "reflection/modes"));
|
&ctx_ref.root,
|
||||||
|
"adrs",
|
||||||
|
));
|
||||||
|
let mode_count = count_ncl_files(&ontoref_ontology::layout::resolve_section(
|
||||||
|
&ctx_ref.root,
|
||||||
|
"reflection/modes",
|
||||||
|
));
|
||||||
|
|
||||||
// ── Project identity (same data as project_picker card) ──────────────────
|
// ── Project identity (same data as project_picker card) ──────────────────
|
||||||
let config_json = load_config_json(
|
let config_json = load_config_json(
|
||||||
|
|
@ -184,7 +193,8 @@ pub async fn dashboard_mp(
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
let manifest_path = ontoref_ontology::layout::resolve_section(&ctx_ref.root, "ontology/manifest.ncl");
|
let manifest_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&ctx_ref.root, "ontology/manifest.ncl");
|
||||||
let (layers, op_modes, default_mode, repo_kind) = if manifest_path.exists() {
|
let (layers, op_modes, default_mode, repo_kind) = if manifest_path.exists() {
|
||||||
match ctx_ref
|
match ctx_ref
|
||||||
.cache
|
.cache
|
||||||
|
|
@ -388,7 +398,8 @@ pub async fn graph_mp(
|
||||||
let (ws_nodes, ws_edges) = workspace_crate_nodes(&ctx_ref.root);
|
let (ws_nodes, ws_edges) = workspace_crate_nodes(&ctx_ref.root);
|
||||||
|
|
||||||
// Merge manifest config sections + requirements.
|
// Merge manifest config sections + requirements.
|
||||||
let manifest_path = ontoref_ontology::layout::resolve_section(&ctx_ref.root, "ontology/manifest.ncl");
|
let manifest_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&ctx_ref.root, "ontology/manifest.ncl");
|
||||||
let (manifest_nodes, manifest_edges) = if manifest_path.exists() {
|
let (manifest_nodes, manifest_edges) = if manifest_path.exists() {
|
||||||
match ctx_ref
|
match ctx_ref
|
||||||
.cache
|
.cache
|
||||||
|
|
|
||||||
|
|
@ -168,8 +168,9 @@ async fn load_registry_entry(
|
||||||
.export(&project_ncl, state.nickel_import_path.as_deref())
|
.export(&project_ncl, state.nickel_import_path.as_deref())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok((json, _)) => serde_json::from_value(json)
|
Ok((json, _)) => {
|
||||||
.unwrap_or_else(|_| make_registry_entry_from_root(root)),
|
serde_json::from_value(json).unwrap_or_else(|_| make_registry_entry_from_root(root))
|
||||||
|
}
|
||||||
Err(_) => make_registry_entry_from_root(root),
|
Err(_) => make_registry_entry_from_root(root),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -177,7 +178,8 @@ async fn load_registry_entry(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Guarded variant: requires `AdminGuard` (any admin session or no auth configured).
|
/// Guarded variant: requires `AdminGuard` (any admin session or no auth
|
||||||
|
/// configured).
|
||||||
pub async fn manage_page_guarded(
|
pub async fn manage_page_guarded(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_guard: super::super::auth::AdminGuard,
|
_guard: super::super::auth::AdminGuard,
|
||||||
|
|
|
||||||
|
|
@ -12,58 +12,42 @@ pub mod session;
|
||||||
|
|
||||||
// ── Re-exports — public surface identical to the old handlers.rs ─────────────
|
// ── Re-exports — public surface identical to the old handlers.rs ─────────────
|
||||||
|
|
||||||
pub(crate) use common::{
|
// Assets + public file serving
|
||||||
insert_brand_ctx, insert_mcp_ctx, readme_description, render, UiError,
|
pub use assets::{serve_asset_mp, serve_asset_single, serve_public_mp, serve_public_single};
|
||||||
|
// Backlog
|
||||||
|
pub use backlog::{
|
||||||
|
backlog_add, backlog_add_mp, backlog_delete_mp, backlog_edit_mp, backlog_page, backlog_page_mp,
|
||||||
|
backlog_propose_status_mp, backlog_update_status, backlog_update_status_mp, BacklogAddForm,
|
||||||
|
BacklogDeleteForm, BacklogEditForm, BacklogProposeForm, BacklogStatusForm,
|
||||||
};
|
};
|
||||||
|
pub(crate) use common::{insert_brand_ctx, insert_mcp_ctx, readme_description, render, UiError};
|
||||||
|
// Compose
|
||||||
|
pub use compose::{compose_form_schema_mp, compose_page_mp, compose_send_mp, ComposeSendBody};
|
||||||
// Dashboard + graph
|
// Dashboard + graph
|
||||||
pub use dashboard::{
|
pub use dashboard::{
|
||||||
api_catalog_page_mp, dashboard, dashboard_mp, dashboard_stats_mp, graph, graph_mp,
|
api_catalog_page_mp, dashboard, dashboard_mp, dashboard_stats_mp, graph, graph_mp,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sessions + notifications
|
|
||||||
pub use session::{
|
|
||||||
emit_notification_mp, notification_action_mp, notifications_mp, notifications_page,
|
|
||||||
sessions, sessions_mp, EmitNotifForm, NotifActionForm,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Project picker
|
|
||||||
pub(crate) use picker::{pending_migrations_fragment_mp, project_picker};
|
|
||||||
|
|
||||||
// Manage
|
// Manage
|
||||||
pub use manage::{
|
pub use manage::{
|
||||||
manage_add, manage_add_guarded, manage_logout, manage_page, manage_page_guarded,
|
manage_add, manage_add_guarded, manage_logout, manage_page, manage_page_guarded, manage_remove,
|
||||||
manage_remove, manage_remove_guarded, service_toggle_ui, AddProjectForm, RemoveProjectForm,
|
manage_remove_guarded, service_toggle_ui, AddProjectForm, RemoveProjectForm,
|
||||||
};
|
};
|
||||||
|
// Pages — modes, actions, qa, config, adrs, domain, tier, timeline, roadmap, panel
|
||||||
// Backlog
|
pub use pages::{
|
||||||
pub use backlog::{
|
actions_page, actions_page_mp, actions_run, actions_run_mp, adrs_page_mp, career_page_mp,
|
||||||
backlog_add, backlog_add_mp, backlog_delete_mp, backlog_edit_mp, backlog_page,
|
config_page_mp, modes, modes_mp, panel_page_mp, personal_page_mp, provisioning_page_mp, qa_add,
|
||||||
backlog_page_mp, backlog_propose_status_mp, backlog_update_status, backlog_update_status_mp,
|
qa_delete, qa_page, qa_page_mp, qa_update, roadmap_page_mp, tier_page_mp, timeline_page_mp,
|
||||||
BacklogAddForm, BacklogDeleteForm, BacklogEditForm, BacklogProposeForm, BacklogStatusForm,
|
views_page, views_page_mp, ActionRunForm, QaAddRequest, QaDeleteRequest, QaUpdateRequest,
|
||||||
};
|
};
|
||||||
|
// Project picker
|
||||||
|
pub(crate) use picker::{pending_migrations_fragment_mp, project_picker};
|
||||||
// Search + bookmarks
|
// Search + bookmarks
|
||||||
pub use search::{
|
pub use search::{
|
||||||
search_bookmark_add, search_bookmark_delete, search_page, search_page_mp, BookmarkAddRequest,
|
search_bookmark_add, search_bookmark_delete, search_page, search_page_mp, BookmarkAddRequest,
|
||||||
BookmarkDeleteRequest,
|
BookmarkDeleteRequest,
|
||||||
};
|
};
|
||||||
|
// Sessions + notifications
|
||||||
// Pages — modes, actions, qa, config, adrs, domain, tier, timeline, roadmap, panel
|
pub use session::{
|
||||||
pub use pages::{
|
emit_notification_mp, notification_action_mp, notifications_mp, notifications_page, sessions,
|
||||||
actions_page, actions_page_mp, actions_run, actions_run_mp, adrs_page_mp, career_page_mp,
|
sessions_mp, EmitNotifForm, NotifActionForm,
|
||||||
config_page_mp, modes, modes_mp, panel_page_mp, personal_page_mp, provisioning_page_mp,
|
|
||||||
qa_add, qa_delete, qa_page, qa_page_mp, qa_update, roadmap_page_mp, tier_page_mp,
|
|
||||||
timeline_page_mp, views_page, views_page_mp, ActionRunForm, QaAddRequest, QaDeleteRequest,
|
|
||||||
QaUpdateRequest,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Compose
|
|
||||||
pub use compose::{
|
|
||||||
compose_form_schema_mp, compose_page_mp, compose_send_mp, ComposeSendBody,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Assets + public file serving
|
|
||||||
pub use assets::{
|
|
||||||
serve_asset_mp, serve_asset_single, serve_public_mp, serve_public_single,
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,7 @@ use tera::Context;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use super::super::auth::AuthUser;
|
use super::super::auth::AuthUser;
|
||||||
use super::common::{
|
use super::common::{auth_role_str, insert_brand_ctx, now_iso, render, tera_ref, UiError};
|
||||||
auth_role_str, insert_brand_ctx, now_iso, render, tera_ref, UiError,
|
|
||||||
};
|
|
||||||
use super::picker::{detect_generated, detect_showcase};
|
use super::picker::{detect_generated, detect_showcase};
|
||||||
use crate::api::AppState;
|
use crate::api::AppState;
|
||||||
use crate::memory_types::ChronicleEntry;
|
use crate::memory_types::ChronicleEntry;
|
||||||
|
|
@ -21,7 +19,8 @@ use crate::memory_types::ChronicleEntry;
|
||||||
pub async fn modes(State(state): State<AppState>) -> Result<Html<String>, UiError> {
|
pub async fn modes(State(state): State<AppState>) -> Result<Html<String>, UiError> {
|
||||||
let tera = tera_ref(&state)?;
|
let tera = tera_ref(&state)?;
|
||||||
|
|
||||||
let modes_dir = ontoref_ontology::layout::resolve_section(&state.project_root, "reflection/modes");
|
let modes_dir =
|
||||||
|
ontoref_ontology::layout::resolve_section(&state.project_root, "reflection/modes");
|
||||||
let mut mode_entries: Vec<serde_json::Value> = Vec::new();
|
let mut mode_entries: Vec<serde_json::Value> = Vec::new();
|
||||||
|
|
||||||
if let Ok(entries) = std::fs::read_dir(&modes_dir) {
|
if let Ok(entries) = std::fs::read_dir(&modes_dir) {
|
||||||
|
|
@ -362,7 +361,8 @@ fn resolve_qa_ctx(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Substrate views panel (ADR-046). Read-only — the page fetches /api/views and
|
/// Substrate views panel (ADR-046). Read-only — the page fetches /api/views and
|
||||||
/// /api/views/{discipline}/mount client-side, so the handler only renders the shell.
|
/// /api/views/{discipline}/mount client-side, so the handler only renders the
|
||||||
|
/// shell.
|
||||||
pub async fn views_page(State(state): State<AppState>) -> Result<Html<String>, UiError> {
|
pub async fn views_page(State(state): State<AppState>) -> Result<Html<String>, UiError> {
|
||||||
let tera = tera_ref(&state)?;
|
let tera = tera_ref(&state)?;
|
||||||
let mut ctx = Context::new();
|
let mut ctx = Context::new();
|
||||||
|
|
@ -506,7 +506,15 @@ pub async fn qa_add(
|
||||||
let related = body.related.as_deref().unwrap_or(&[]);
|
let related = body.related.as_deref().unwrap_or(&[]);
|
||||||
let now = now_iso();
|
let now = now_iso();
|
||||||
|
|
||||||
match super::super::qa_ncl::add_entry(&qa_path, &body.question, answer, actor, &now, tags, related) {
|
match super::super::qa_ncl::add_entry(
|
||||||
|
&qa_path,
|
||||||
|
&body.question,
|
||||||
|
answer,
|
||||||
|
actor,
|
||||||
|
&now,
|
||||||
|
tags,
|
||||||
|
related,
|
||||||
|
) {
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
cache.invalidate_file(&qa_path);
|
cache.invalidate_file(&qa_path);
|
||||||
(
|
(
|
||||||
|
|
@ -767,7 +775,8 @@ pub async fn adrs_page_mp(
|
||||||
render(tera, "pages/adrs.html", &ctx).await
|
render(tera, "pages/adrs.html", &ctx).await
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Domain extension pages ────────────────────────────────────────────────────
|
// ── Domain extension pages
|
||||||
|
// ────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Personal ontology page — contents pipeline + opportunities.
|
/// Personal ontology page — contents pipeline + opportunities.
|
||||||
pub async fn personal_page_mp(
|
pub async fn personal_page_mp(
|
||||||
|
|
@ -869,9 +878,8 @@ pub async fn career_page_mp(
|
||||||
.unwrap_or_else(|_| "[]".to_string());
|
.unwrap_or_else(|_| "[]".to_string());
|
||||||
let talks = serde_json::to_string(v.get("talks").unwrap_or(&empty))
|
let talks = serde_json::to_string(v.get("talks").unwrap_or(&empty))
|
||||||
.unwrap_or_else(|_| "[]".to_string());
|
.unwrap_or_else(|_| "[]".to_string());
|
||||||
let positioning =
|
let positioning = serde_json::to_string(v.get("positioning").unwrap_or(&empty))
|
||||||
serde_json::to_string(v.get("positioning").unwrap_or(&empty))
|
.unwrap_or_else(|_| "[]".to_string());
|
||||||
.unwrap_or_else(|_| "[]".to_string());
|
|
||||||
let publications =
|
let publications =
|
||||||
serde_json::to_string(v.get("publications").unwrap_or(&empty))
|
serde_json::to_string(v.get("publications").unwrap_or(&empty))
|
||||||
.unwrap_or_else(|_| "[]".to_string());
|
.unwrap_or_else(|_| "[]".to_string());
|
||||||
|
|
@ -934,7 +942,8 @@ pub async fn provisioning_page_mp(
|
||||||
let cache = ctx_ref.cache.clone();
|
let cache = ctx_ref.cache.clone();
|
||||||
let root = ctx_ref.root.clone();
|
let root = ctx_ref.root.clone();
|
||||||
|
|
||||||
let connections_path = ontoref_ontology::layout::resolve_section(&root, "ontology/connections.ncl");
|
let connections_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&root, "ontology/connections.ncl");
|
||||||
let connections_json = if connections_path.exists() {
|
let connections_json = if connections_path.exists() {
|
||||||
match cache
|
match cache
|
||||||
.export(&connections_path, import_path.as_deref())
|
.export(&connections_path, import_path.as_deref())
|
||||||
|
|
@ -987,7 +996,8 @@ pub async fn provisioning_page_mp(
|
||||||
render(tera, "pages/provisioning.html", &ctx).await
|
render(tera, "pages/provisioning.html", &ctx).await
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Tier page ─────────────────────────────────────────────────────────────────
|
// ── Tier page
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub async fn tier_page_mp(
|
pub async fn tier_page_mp(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
|
@ -1083,7 +1093,8 @@ pub async fn tier_page_mp(
|
||||||
render(tera, "pages/tier.html", &ctx).await
|
render(tera, "pages/tier.html", &ctx).await
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Chronicle timeline ────────────────────────────────────────────────────────
|
// ── Chronicle timeline
|
||||||
|
// ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// `GET /{slug}/timeline` — render the chronicle timeline panel.
|
/// `GET /{slug}/timeline` — render the chronicle timeline panel.
|
||||||
/// Reads all `entries.jsonl` files under `{root}/.coder/` and serves them as
|
/// Reads all `entries.jsonl` files under `{root}/.coder/` and serves them as
|
||||||
|
|
@ -1099,8 +1110,7 @@ pub async fn timeline_page_mp(
|
||||||
|
|
||||||
let coder_dir = ctx_ref.root.join(".coder");
|
let coder_dir = ctx_ref.root.join(".coder");
|
||||||
let entries = collect_chronicle_entries(&coder_dir);
|
let entries = collect_chronicle_entries(&coder_dir);
|
||||||
let timeline_json =
|
let timeline_json = serde_json::to_string(&entries).unwrap_or_else(|_| "[]".to_string());
|
||||||
serde_json::to_string(&entries).unwrap_or_else(|_| "[]".to_string());
|
|
||||||
|
|
||||||
let mut ctx = Context::new();
|
let mut ctx = Context::new();
|
||||||
ctx.insert("slug", &slug);
|
ctx.insert("slug", &slug);
|
||||||
|
|
@ -1142,31 +1152,60 @@ fn parse_chronicle_line(line: &str, idx: usize) -> Option<ChronicleEntry> {
|
||||||
return Some(e);
|
return Some(e);
|
||||||
}
|
}
|
||||||
let v = serde_json::from_str::<serde_json::Value>(line).ok()?;
|
let v = serde_json::from_str::<serde_json::Value>(line).ok()?;
|
||||||
let id = v.get("id")
|
let id = v
|
||||||
|
.get("id")
|
||||||
.and_then(|x| x.as_str())
|
.and_then(|x| x.as_str())
|
||||||
.map(String::from)
|
.map(String::from)
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
let date = v.get("date").and_then(|x| x.as_str()).unwrap_or("0000");
|
let date = v.get("date").and_then(|x| x.as_str()).unwrap_or("0000");
|
||||||
format!("{date}-{idx}")
|
format!("{date}-{idx}")
|
||||||
});
|
});
|
||||||
let title = v.get("title").and_then(|x| x.as_str()).unwrap_or("(untitled)").to_string();
|
let title = v
|
||||||
let date = v.get("date").and_then(|x| x.as_str()).unwrap_or("1970-01-01").to_string();
|
.get("title")
|
||||||
let kind = v.get("kind").and_then(|x| x.as_str()).unwrap_or("info").to_string();
|
.and_then(|x| x.as_str())
|
||||||
let refs: Vec<String> = v.get("relates_to")
|
.unwrap_or("(untitled)")
|
||||||
|
.to_string();
|
||||||
|
let date = v
|
||||||
|
.get("date")
|
||||||
|
.and_then(|x| x.as_str())
|
||||||
|
.unwrap_or("1970-01-01")
|
||||||
|
.to_string();
|
||||||
|
let kind = v
|
||||||
|
.get("kind")
|
||||||
|
.and_then(|x| x.as_str())
|
||||||
|
.unwrap_or("info")
|
||||||
|
.to_string();
|
||||||
|
let refs: Vec<String> = v
|
||||||
|
.get("relates_to")
|
||||||
.or_else(|| v.get("refs"))
|
.or_else(|| v.get("refs"))
|
||||||
.and_then(|x| x.as_array())
|
.and_then(|x| x.as_array())
|
||||||
.map(|a| a.iter().filter_map(|r| r.as_str().map(String::from)).collect())
|
.map(|a| {
|
||||||
|
a.iter()
|
||||||
|
.filter_map(|r| r.as_str().map(String::from))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let summary = v.get("content")
|
let summary = v
|
||||||
|
.get("content")
|
||||||
.or_else(|| v.get("summary"))
|
.or_else(|| v.get("summary"))
|
||||||
.and_then(|x| x.as_str())
|
.and_then(|x| x.as_str())
|
||||||
.map(|s| s.chars().take(200).collect());
|
.map(|s| s.chars().take(200).collect());
|
||||||
let domain = v.get("domain").and_then(|x| x.as_str()).map(String::from);
|
let domain = v.get("domain").and_then(|x| x.as_str()).map(String::from);
|
||||||
Some(ChronicleEntry { id, title, date, kind, summary, domain, refs })
|
Some(ChronicleEntry {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
date,
|
||||||
|
kind,
|
||||||
|
summary,
|
||||||
|
domain,
|
||||||
|
refs,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_entries_recursive(dir: &std::path::Path, out: &mut Vec<ChronicleEntry>) {
|
fn collect_entries_recursive(dir: &std::path::Path, out: &mut Vec<ChronicleEntry>) {
|
||||||
let Ok(rd) = std::fs::read_dir(dir) else { return };
|
let Ok(rd) = std::fs::read_dir(dir) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
for entry in rd.flatten() {
|
for entry in rd.flatten() {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
|
|
@ -1174,14 +1213,18 @@ fn collect_entries_recursive(dir: &std::path::Path, out: &mut Vec<ChronicleEntry
|
||||||
} else if path.file_name().and_then(|n| n.to_str()) == Some("entries.jsonl") {
|
} else if path.file_name().and_then(|n| n.to_str()) == Some("entries.jsonl") {
|
||||||
if let Ok(content) = std::fs::read_to_string(&path) {
|
if let Ok(content) = std::fs::read_to_string(&path) {
|
||||||
out.extend(
|
out.extend(
|
||||||
content.lines().enumerate().filter_map(|(idx, l)| parse_chronicle_line(l.trim(), idx))
|
content
|
||||||
|
.lines()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(idx, l)| parse_chronicle_line(l.trim(), idx)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Positioning roadmap ───────────────────────────────────────────────────────
|
// ── Positioning roadmap
|
||||||
|
// ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// `GET /{slug}/roadmap` — render the positioning roadmap panel.
|
/// `GET /{slug}/roadmap` — render the positioning roadmap panel.
|
||||||
/// Loads positioning data via `Positioning::load` and serves mechanisms +
|
/// Loads positioning data via `Positioning::load` and serves mechanisms +
|
||||||
|
|
@ -1196,52 +1239,58 @@ pub async fn roadmap_page_mp(
|
||||||
let base_url = format!("/ui/{slug}");
|
let base_url = format!("/ui/{slug}");
|
||||||
|
|
||||||
let pos_dir = ctx_ref.root.join(".ontoref").join("positioning");
|
let pos_dir = ctx_ref.root.join(".ontoref").join("positioning");
|
||||||
let (mechanisms, milestones, orphan_milestones) =
|
let (mechanisms, milestones, orphan_milestones) = if pos_dir.exists() {
|
||||||
if pos_dir.exists() {
|
match ontoref_ontology::ontology::Positioning::load(&pos_dir) {
|
||||||
match ontoref_ontology::ontology::Positioning::load(&pos_dir) {
|
Ok(pos) => {
|
||||||
Ok(pos) => {
|
let mechs: Vec<serde_json::Value> = pos
|
||||||
let mechs: Vec<serde_json::Value> = pos
|
|
||||||
.mechanisms()
|
.mechanisms()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|m| serde_json::json!({ "id": m.id, "label": m.name, "status": format!("{:?}", m.status) }))
|
.map(|m| serde_json::json!({ "id": m.id, "label": m.name, "status": format!("{:?}", m.status) }))
|
||||||
.collect();
|
.collect();
|
||||||
// milestones come from the chronicle entries in .coder/
|
// milestones come from the chronicle entries in .coder/
|
||||||
let coder_dir = ctx_ref.root.join(".coder");
|
let coder_dir = ctx_ref.root.join(".coder");
|
||||||
let all_entries = collect_chronicle_entries(&coder_dir);
|
let all_entries = collect_chronicle_entries(&coder_dir);
|
||||||
let ms: Vec<serde_json::Value> = all_entries
|
let ms: Vec<serde_json::Value> = all_entries
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|e| e.kind == "milestone")
|
.filter(|e| e.kind == "milestone")
|
||||||
.map(|e| serde_json::json!({
|
.map(|e| {
|
||||||
|
serde_json::json!({
|
||||||
"id": e.id, "title": e.title, "date": e.date,
|
"id": e.id, "title": e.title, "date": e.date,
|
||||||
"refs": e.refs, "mechanism": e.refs.first().cloned(),
|
"refs": e.refs, "mechanism": e.refs.first().cloned(),
|
||||||
}))
|
|
||||||
.collect();
|
|
||||||
let diff_adrs: std::collections::HashSet<String> = pos
|
|
||||||
.differentiators()
|
|
||||||
.iter()
|
|
||||||
.flat_map(|d| d.evidence_adrs.clone())
|
|
||||||
.collect();
|
|
||||||
let orphans: Vec<serde_json::Value> = ms
|
|
||||||
.iter()
|
|
||||||
.filter(|m| {
|
|
||||||
let refs = m.get("refs")
|
|
||||||
.and_then(|r| r.as_array())
|
|
||||||
.map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect::<Vec<_>>())
|
|
||||||
.unwrap_or_default();
|
|
||||||
!refs.iter().any(|r| diff_adrs.contains(r))
|
|
||||||
})
|
})
|
||||||
.cloned()
|
})
|
||||||
.collect();
|
.collect();
|
||||||
(mechs, ms, orphans)
|
let diff_adrs: std::collections::HashSet<String> = pos
|
||||||
}
|
.differentiators()
|
||||||
Err(e) => {
|
.iter()
|
||||||
warn!(slug = %slug, error = %e, "roadmap: positioning load failed");
|
.flat_map(|d| d.evidence_adrs.clone())
|
||||||
(vec![], vec![], vec![])
|
.collect();
|
||||||
}
|
let orphans: Vec<serde_json::Value> = ms
|
||||||
|
.iter()
|
||||||
|
.filter(|m| {
|
||||||
|
let refs = m
|
||||||
|
.get("refs")
|
||||||
|
.and_then(|r| r.as_array())
|
||||||
|
.map(|a| {
|
||||||
|
a.iter()
|
||||||
|
.filter_map(|v| v.as_str().map(String::from))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
!refs.iter().any(|r| diff_adrs.contains(r))
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
(mechs, ms, orphans)
|
||||||
}
|
}
|
||||||
} else {
|
Err(e) => {
|
||||||
(vec![], vec![], vec![])
|
warn!(slug = %slug, error = %e, "roadmap: positioning load failed");
|
||||||
};
|
(vec![], vec![], vec![])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(vec![], vec![], vec![])
|
||||||
|
};
|
||||||
|
|
||||||
let roadmap_json = serde_json::to_string(&serde_json::json!({
|
let roadmap_json = serde_json::to_string(&serde_json::json!({
|
||||||
"mechanisms": mechanisms,
|
"mechanisms": mechanisms,
|
||||||
|
|
@ -1271,7 +1320,8 @@ pub async fn roadmap_page_mp(
|
||||||
render(tera, "pages/roadmap.html", &ctx).await
|
render(tera, "pages/roadmap.html", &ctx).await
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Generic panel ─────────────────────────────────────────────────────────────
|
// ── Generic panel
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// `GET /{slug}/panel/{id}` — render a presentation-spec panel by id.
|
/// `GET /{slug}/panel/{id}` — render a presentation-spec panel by id.
|
||||||
/// Loads the panel NCL from `.ontoref/presentation/panel-{id}.ncl` and
|
/// Loads the panel NCL from `.ontoref/presentation/panel-{id}.ncl` and
|
||||||
|
|
@ -1286,7 +1336,8 @@ pub async fn panel_page_mp(
|
||||||
let base_url = format!("/ui/{slug}");
|
let base_url = format!("/ui/{slug}");
|
||||||
let _ = auth;
|
let _ = auth;
|
||||||
|
|
||||||
let panel_path = ctx_ref.root
|
let panel_path = ctx_ref
|
||||||
|
.root
|
||||||
.join(".ontoref")
|
.join(".ontoref")
|
||||||
.join("presentation")
|
.join("presentation")
|
||||||
.join(format!("panel-{panel_id}.ncl"));
|
.join(format!("panel-{panel_id}.ncl"));
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ use tera::Context;
|
||||||
|
|
||||||
use super::super::auth::AuthUser;
|
use super::super::auth::AuthUser;
|
||||||
use super::common::{
|
use super::common::{
|
||||||
epoch_secs, extract_card_from_config, extract_registry_ctx, extract_vault_ctx,
|
epoch_secs, extract_card_from_config, extract_registry_ctx, extract_vault_ctx, insert_mcp_ctx,
|
||||||
insert_mcp_ctx, load_config_json, readme_description, render, tera_ref, UiError,
|
load_config_json, readme_description, render, tera_ref, UiError,
|
||||||
};
|
};
|
||||||
use crate::api::AppState;
|
use crate::api::AppState;
|
||||||
|
|
||||||
|
|
@ -20,7 +20,8 @@ pub(crate) fn count_ncl_files(dir: &std::path::Path) -> usize {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read git remote names+URLs from `{root}/.git/config` by parsing the ini file
|
/// Read git remote names+URLs from `{root}/.git/config` by parsing the ini file
|
||||||
/// directly. Returns `[{ "name": "origin", "url": "git@github.com:..." }, ...]`.
|
/// directly. Returns `[{ "name": "origin", "url": "git@github.com:..." },
|
||||||
|
/// ...]`.
|
||||||
pub(crate) fn git_remotes(root: &std::path::Path) -> Vec<serde_json::Value> {
|
pub(crate) fn git_remotes(root: &std::path::Path) -> Vec<serde_json::Value> {
|
||||||
let Ok(content) = std::fs::read_to_string(root.join(".git").join("config")) else {
|
let Ok(content) = std::fs::read_to_string(root.join(".git").join("config")) else {
|
||||||
return vec![];
|
return vec![];
|
||||||
|
|
@ -197,7 +198,8 @@ pub async fn project_picker(State(state): State<AppState>) -> Result<Html<String
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Backlog — optional NCL export
|
// Backlog — optional NCL export
|
||||||
let backlog_path = ontoref_ontology::layout::resolve_section(&proj.root, "reflection/backlog.ncl");
|
let backlog_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&proj.root, "reflection/backlog.ncl");
|
||||||
let (backlog_items, backlog_open) = if backlog_path.exists() {
|
let (backlog_items, backlog_open) = if backlog_path.exists() {
|
||||||
match proj
|
match proj
|
||||||
.cache
|
.cache
|
||||||
|
|
@ -233,7 +235,8 @@ pub async fn project_picker(State(state): State<AppState>) -> Result<Html<String
|
||||||
};
|
};
|
||||||
|
|
||||||
// Manifest — layers, operational_modes, default_mode, repo_kind, registry
|
// Manifest — layers, operational_modes, default_mode, repo_kind, registry
|
||||||
let manifest_path = ontoref_ontology::layout::resolve_section(&proj.root, "ontology/manifest.ncl");
|
let manifest_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&proj.root, "ontology/manifest.ncl");
|
||||||
let manifest_json: Option<serde_json::Value> = if manifest_path.exists() {
|
let manifest_json: Option<serde_json::Value> = if manifest_path.exists() {
|
||||||
proj.cache
|
proj.cache
|
||||||
.export(&manifest_path, proj.import_path.as_deref())
|
.export(&manifest_path, proj.import_path.as_deref())
|
||||||
|
|
@ -319,7 +322,8 @@ pub async fn project_picker(State(state): State<AppState>) -> Result<Html<String
|
||||||
"project card loaded"
|
"project card loaded"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Description — first meaningful text line from README.md (fallback when no card)
|
// Description — first meaningful text line from README.md (fallback when no
|
||||||
|
// card)
|
||||||
let description = if card
|
let description = if card
|
||||||
.get("tagline")
|
.get("tagline")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@ pub(crate) async fn load_bookmark_entries(
|
||||||
root: &std::path::Path,
|
root: &std::path::Path,
|
||||||
import_path: Option<&str>,
|
import_path: Option<&str>,
|
||||||
) -> Vec<serde_json::Value> {
|
) -> Vec<serde_json::Value> {
|
||||||
let bm_path = ontoref_ontology::layout::resolve_section(root, "reflection/search_bookmarks.ncl");
|
let bm_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(root, "reflection/search_bookmarks.ncl");
|
||||||
if !bm_path.exists() {
|
if !bm_path.exists() {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
|
|
@ -122,7 +123,8 @@ pub async fn search_bookmark_add(
|
||||||
Json(body): Json<BookmarkAddRequest>,
|
Json(body): Json<BookmarkAddRequest>,
|
||||||
) -> impl axum::response::IntoResponse {
|
) -> impl axum::response::IntoResponse {
|
||||||
let (root, cache) = resolve_bookmark_ctx(&state, body.slug.as_deref());
|
let (root, cache) = resolve_bookmark_ctx(&state, body.slug.as_deref());
|
||||||
let bm_path = ontoref_ontology::layout::resolve_section(&root, "reflection/search_bookmarks.ncl");
|
let bm_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&root, "reflection/search_bookmarks.ncl");
|
||||||
let _guard = state.ncl_write_lock.acquire(&bm_path).await;
|
let _guard = state.ncl_write_lock.acquire(&bm_path).await;
|
||||||
if !bm_path.exists() {
|
if !bm_path.exists() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -180,7 +182,8 @@ pub async fn search_bookmark_delete(
|
||||||
Json(body): Json<BookmarkDeleteRequest>,
|
Json(body): Json<BookmarkDeleteRequest>,
|
||||||
) -> impl axum::response::IntoResponse {
|
) -> impl axum::response::IntoResponse {
|
||||||
let (root, cache) = resolve_bookmark_ctx(&state, body.slug.as_deref());
|
let (root, cache) = resolve_bookmark_ctx(&state, body.slug.as_deref());
|
||||||
let bm_path = ontoref_ontology::layout::resolve_section(&root, "reflection/search_bookmarks.ncl");
|
let bm_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&root, "reflection/search_bookmarks.ncl");
|
||||||
let _guard = state.ncl_write_lock.acquire(&bm_path).await;
|
let _guard = state.ncl_write_lock.acquire(&bm_path).await;
|
||||||
if !bm_path.exists() {
|
if !bm_path.exists() {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use tera::Context;
|
use tera::Context;
|
||||||
|
|
||||||
use super::super::auth::AuthUser;
|
use super::super::auth::AuthUser;
|
||||||
use super::common::{
|
use super::common::{auth_role_str, epoch_secs, insert_brand_ctx, render, tera_ref, UiError};
|
||||||
auth_role_str, epoch_secs, insert_brand_ctx, render, tera_ref, UiError,
|
|
||||||
};
|
|
||||||
use crate::api::AppState;
|
use crate::api::AppState;
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
|
|
@ -320,7 +318,8 @@ pub async fn notification_action_mp(
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
UiError::BadRequest("backlog_approve: missing proposed_status".into())
|
UiError::BadRequest("backlog_approve: missing proposed_status".into())
|
||||||
})?;
|
})?;
|
||||||
let backlog_path = ontoref_ontology::layout::resolve_section(&ctx_ref.root, "reflection/backlog.ncl");
|
let backlog_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(&ctx_ref.root, "reflection/backlog.ncl");
|
||||||
let _guard = state.ncl_write_lock.acquire(&backlog_path).await;
|
let _guard = state.ncl_write_lock.acquire(&backlog_path).await;
|
||||||
super::super::backlog_ncl::update_status(
|
super::super::backlog_ncl::update_status(
|
||||||
&backlog_path,
|
&backlog_path,
|
||||||
|
|
|
||||||
|
|
@ -55,10 +55,7 @@ impl TemplateWatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn reload_loop(
|
async fn reload_loop(mut rx: mpsc::Receiver<PathBuf>, tera: Arc<RwLock<ui::TeraEnv>>) {
|
||||||
mut rx: mpsc::Receiver<PathBuf>,
|
|
||||||
tera: Arc<RwLock<ui::TeraEnv>>,
|
|
||||||
) {
|
|
||||||
let debounce = Duration::from_millis(150);
|
let debounce = Duration::from_millis(150);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@
|
||||||
//! read path, shared by the HTTP API and the MCP surface so an interface IS a
|
//! read path, shared by the HTTP API and the MCP surface so an interface IS a
|
||||||
//! mounted view (not bespoke code). Mirrors the deterministic fold of the
|
//! mounted view (not bespoke code). Mirrors the deterministic fold of the
|
||||||
//! Nushell resolver (`reflection/modules/view.nu`): the declared prior ⊕ the
|
//! Nushell resolver (`reflection/modules/view.nu`): the declared prior ⊕ the
|
||||||
//! active adjustment chain. The Nushell path remains the source of truth and the
|
//! active adjustment chain. The Nushell path remains the source of truth and
|
||||||
//! offline fallback (ADR-029). View files and the adjustment log are read as JSON
|
//! the offline fallback (ADR-029). View files and the adjustment log are read
|
||||||
//! `Value` so the schema is not re-declared in Rust — only the fold is, pinned by
|
//! as JSON `Value` so the schema is not re-declared in Rust — only the fold is,
|
||||||
//! a unit test against the Nushell semantics.
|
//! pinned by a unit test against the Nushell semantics.
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
@ -26,8 +26,8 @@ fn adjustments_path(root: &Path) -> PathBuf {
|
||||||
root.join(".coder").join("view-adjustments.jsonl")
|
root.join(".coder").join("view-adjustments.jsonl")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Export one view NCL file to a JSON `Value` (relative imports resolve from the
|
/// Export one view NCL file to a JSON `Value` (relative imports resolve from
|
||||||
/// file's directory; the import path is supplied for any bare imports).
|
/// the file's directory; the import path is supplied for any bare imports).
|
||||||
fn export_ncl(path: &Path, import_path: Option<&str>) -> Option<Value> {
|
fn export_ncl(path: &Path, import_path: Option<&str>) -> Option<Value> {
|
||||||
let mut cmd = Command::new("nickel");
|
let mut cmd = Command::new("nickel");
|
||||||
cmd.args(["export", "--format", "json"]).arg(path);
|
cmd.args(["export", "--format", "json"]).arg(path);
|
||||||
|
|
@ -104,7 +104,10 @@ fn compose(view: &Value, adjustments: &[Value]) -> Value {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|l| {
|
.map(|l| {
|
||||||
let lref = l.get("ref").and_then(Value::as_str).unwrap_or("");
|
let lref = l.get("ref").and_then(Value::as_str).unwrap_or("");
|
||||||
let elig = l.get("eligibility").and_then(Value::as_str).unwrap_or("Always");
|
let elig = l
|
||||||
|
.get("eligibility")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.unwrap_or("Always");
|
||||||
let base = l.get("salience").and_then(Value::as_i64).unwrap_or(0);
|
let base = l.get("salience").and_then(Value::as_i64).unwrap_or(0);
|
||||||
let d = if elig == "Learned" {
|
let d = if elig == "Learned" {
|
||||||
delta_for(&active, lref)
|
delta_for(&active, lref)
|
||||||
|
|
@ -143,14 +146,16 @@ fn compose(view: &Value, adjustments: &[Value]) -> Value {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Why a discipline could not be mounted — mapped to HTTP status / MCP error by callers.
|
/// Why a discipline could not be mounted — mapped to HTTP status / MCP error by
|
||||||
|
/// callers.
|
||||||
pub enum MountError {
|
pub enum MountError {
|
||||||
NotFound(String),
|
NotFound(String),
|
||||||
Multiple(Vec<String>),
|
Multiple(Vec<String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Declared substrate views (id, discipline, context, learns_from) as JSON.
|
/// Declared substrate views (id, discipline, context, learns_from) as JSON.
|
||||||
/// Shared by the HTTP `views_list` handler and the MCP `ontoref_view_list` tool.
|
/// Shared by the HTTP `views_list` handler and the MCP `ontoref_view_list`
|
||||||
|
/// tool.
|
||||||
pub fn list_views_json(root: &Path, import_path: Option<&str>) -> Value {
|
pub fn list_views_json(root: &Path, import_path: Option<&str>) -> Value {
|
||||||
let rows: Vec<Value> = load_views(root, import_path)
|
let rows: Vec<Value> = load_views(root, import_path)
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -213,10 +218,7 @@ fn resolve_root_for_slug<'a>(
|
||||||
std::borrow::Cow::Borrowed(state.project_root.as_path())
|
std::borrow::Cow::Borrowed(state.project_root.as_path())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_import_path_for_slug(
|
fn resolve_import_path_for_slug(state: &AppState, slug: Option<&str>) -> Option<String> {
|
||||||
state: &AppState,
|
|
||||||
slug: Option<&str>,
|
|
||||||
) -> Option<String> {
|
|
||||||
if let Some(s) = slug {
|
if let Some(s) = slug {
|
||||||
if let Some(ctx) = state.registry.get(s) {
|
if let Some(ctx) = state.registry.get(s) {
|
||||||
return ctx.import_path.clone();
|
return ctx.import_path.clone();
|
||||||
|
|
@ -225,7 +227,8 @@ fn resolve_import_path_for_slug(
|
||||||
state.nickel_import_path.clone()
|
state.nickel_import_path.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Declared substrate views. Accepts optional `?slug=` to query a registry project's views.
|
/// Declared substrate views. Accepts optional `?slug=` to query a registry
|
||||||
|
/// project's views.
|
||||||
#[ontoref_derive::onto_api(
|
#[ontoref_derive::onto_api(
|
||||||
method = "GET",
|
method = "GET",
|
||||||
path = "/api/views",
|
path = "/api/views",
|
||||||
|
|
@ -233,10 +236,7 @@ fn resolve_import_path_for_slug(
|
||||||
actors = "agent, developer, ci, admin",
|
actors = "agent, developer, ci, admin",
|
||||||
tags = "views, substrate"
|
tags = "views, substrate"
|
||||||
)]
|
)]
|
||||||
pub async fn views_list(
|
pub async fn views_list(State(state): State<AppState>, Query(q): Query<SlugQuery>) -> Json<Value> {
|
||||||
State(state): State<AppState>,
|
|
||||||
Query(q): Query<SlugQuery>,
|
|
||||||
) -> Json<Value> {
|
|
||||||
let root = resolve_root_for_slug(&state, q.slug.as_deref());
|
let root = resolve_root_for_slug(&state, q.slug.as_deref());
|
||||||
let import_path = resolve_import_path_for_slug(&state, q.slug.as_deref());
|
let import_path = resolve_import_path_for_slug(&state, q.slug.as_deref());
|
||||||
Json(list_views_json(&root, import_path.as_deref()))
|
Json(list_views_json(&root, import_path.as_deref()))
|
||||||
|
|
@ -266,7 +266,9 @@ pub async fn views_mount(
|
||||||
)),
|
)),
|
||||||
Err(MountError::Multiple(contexts)) => Err((
|
Err(MountError::Multiple(contexts)) => Err((
|
||||||
StatusCode::CONFLICT,
|
StatusCode::CONFLICT,
|
||||||
Json(json!({ "error": "multiple views for discipline; specify context", "contexts": contexts })),
|
Json(
|
||||||
|
json!({ "error": "multiple views for discipline; specify context", "contexts": contexts }),
|
||||||
|
),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -516,10 +516,7 @@ fn discover_pending_migrations(project_root: &Path) -> Vec<String> {
|
||||||
/// Run `nickel export --format json` against `config_path` with the given
|
/// Run `nickel export --format json` against `config_path` with the given
|
||||||
/// import path. Returns the parsed JSON or `None` on any failure (with a
|
/// import path. Returns the parsed JSON or `None` on any failure (with a
|
||||||
/// `warn!` log emitted by the caller's diagnostic context).
|
/// `warn!` log emitted by the caller's diagnostic context).
|
||||||
fn export_config_json(
|
fn export_config_json(config_path: &Path, import_path: Option<&str>) -> Option<serde_json::Value> {
|
||||||
config_path: &Path,
|
|
||||||
import_path: Option<&str>,
|
|
||||||
) -> Option<serde_json::Value> {
|
|
||||||
let mut cmd = std::process::Command::new("nickel");
|
let mut cmd = std::process::Command::new("nickel");
|
||||||
cmd.args(["export", "--format", "json"]).arg(config_path);
|
cmd.args(["export", "--format", "json"]).arg(config_path);
|
||||||
if let Some(ip) = import_path {
|
if let Some(ip) = import_path {
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@
|
||||||
//! the resulting witnesses match on every cross-surface invariant
|
//! the resulting witnesses match on every cross-surface invariant
|
||||||
//! (op_id, witness_shape, state_root, signature, effects).
|
//! (op_id, witness_shape, state_root, signature, effects).
|
||||||
//!
|
//!
|
||||||
//! - HTTP: in-process axum via `tower::ServiceExt::oneshot` — no
|
//! - HTTP: in-process axum via `tower::ServiceExt::oneshot` — no live socket
|
||||||
//! live socket required.
|
//! required.
|
||||||
//! - CLI: spawns the compiled daemon binary via
|
//! - CLI: spawns the compiled daemon binary via
|
||||||
//! `env!("CARGO_BIN_EXE_ontoref-daemon")`.
|
//! `env!("CARGO_BIN_EXE_ontoref-daemon")`.
|
||||||
//! - MCP: calls the pure-function projection that the MCP `tools/list`
|
//! - MCP: calls the pure-function projection that the MCP `tools/list` response
|
||||||
//! response is built from.
|
//! is built from.
|
||||||
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
|
@ -107,11 +107,13 @@ async fn http_surface_dispatches_propose_adr() {
|
||||||
let body_bytes = axum::body::to_bytes(response.into_body(), 16 * 1024)
|
let body_bytes = axum::body::to_bytes(response.into_body(), 16 * 1024)
|
||||||
.await
|
.await
|
||||||
.expect("collect body");
|
.expect("collect body");
|
||||||
let response: DispatchResponse =
|
let response: DispatchResponse = serde_json::from_slice(&body_bytes).expect("response parses");
|
||||||
serde_json::from_slice(&body_bytes).expect("response parses");
|
|
||||||
assert_eq!(response.op_id, "propose_adr");
|
assert_eq!(response.op_id, "propose_adr");
|
||||||
assert_eq!(response.witness_shape, "adr_proposed");
|
assert_eq!(response.witness_shape, "adr_proposed");
|
||||||
assert!(tmp.path().join(".ontoref/adrs/adr-502-surface-equivalence.ncl").exists());
|
assert!(tmp
|
||||||
|
.path()
|
||||||
|
.join(".ontoref/adrs/adr-502-surface-equivalence.ncl")
|
||||||
|
.exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -155,7 +157,10 @@ fn cli_surface_dispatches_propose_adr() {
|
||||||
let response: DispatchResponse =
|
let response: DispatchResponse =
|
||||||
serde_json::from_str(&stdout).expect("stdout parses as DispatchResponse");
|
serde_json::from_str(&stdout).expect("stdout parses as DispatchResponse");
|
||||||
assert_eq!(response.op_id, "propose_adr");
|
assert_eq!(response.op_id, "propose_adr");
|
||||||
assert!(tmp.path().join(".ontoref/adrs/adr-503-cli-test.ncl").exists());
|
assert!(tmp
|
||||||
|
.path()
|
||||||
|
.join(".ontoref/adrs/adr-503-cli-test.ncl")
|
||||||
|
.exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -224,7 +229,10 @@ fn function_cli_and_http_produce_equivalent_witnesses() {
|
||||||
assert_eq!(fn_response.state_root_hex, cli_response.state_root_hex);
|
assert_eq!(fn_response.state_root_hex, cli_response.state_root_hex);
|
||||||
assert_eq!(fn_response.signature_hex, cli_response.signature_hex);
|
assert_eq!(fn_response.signature_hex, cli_response.signature_hex);
|
||||||
assert_eq!(fn_response.effects_hash_hex, cli_response.effects_hash_hex);
|
assert_eq!(fn_response.effects_hash_hex, cli_response.effects_hash_hex);
|
||||||
assert_eq!(fn_response.actor_public_key_hex, cli_response.actor_public_key_hex);
|
assert_eq!(
|
||||||
|
fn_response.actor_public_key_hex,
|
||||||
|
cli_response.actor_public_key_hex
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inline copies of the daemon's HTTP handlers — strip the AppState
|
/// Inline copies of the daemon's HTTP handlers — strip the AppState
|
||||||
|
|
|
||||||
|
|
@ -98,10 +98,7 @@ fn apply_diffs_dispatches_via_runtime() {
|
||||||
let result = &results[0];
|
let result = &results[0];
|
||||||
assert_eq!(result.witness.op_id, "move_fsm_state");
|
assert_eq!(result.witness.op_id, "move_fsm_state");
|
||||||
result.witness.verify().expect("witness verifies");
|
result.witness.verify().expect("witness verifies");
|
||||||
assert_eq!(
|
assert_eq!(ctx.state.as_ref().unwrap().dimensions[0].current_state, "b");
|
||||||
ctx.state.as_ref().unwrap().dimensions[0].current_state,
|
|
||||||
"b"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -132,17 +129,17 @@ fn apply_diffs_rolls_back_on_uncatalogued_edit() {
|
||||||
let err = apply_diffs(&diffs, &mut ctx).expect_err("uncatalogued edit must reject");
|
let err = apply_diffs(&diffs, &mut ctx).expect_err("uncatalogued edit must reject");
|
||||||
match err {
|
match err {
|
||||||
ReconcileError::UncatalogedDiff(msg) => {
|
ReconcileError::UncatalogedDiff(msg) => {
|
||||||
assert!(msg.contains("new-dim"), "diff summary must reference the new dim: {msg}");
|
assert!(
|
||||||
|
msg.contains("new-dim"),
|
||||||
|
"diff summary must reference the new dim: {msg}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
other => panic!("expected UncatalogedDiff, got {other:?}"),
|
other => panic!("expected UncatalogedDiff, got {other:?}"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// State left untouched — first-pass refusal preserves the
|
// State left untouched — first-pass refusal preserves the
|
||||||
// pre-edit substrate.
|
// pre-edit substrate.
|
||||||
assert_eq!(
|
assert_eq!(ctx.state.as_ref().unwrap().dimensions[0].current_state, "a");
|
||||||
ctx.state.as_ref().unwrap().dimensions[0].current_state,
|
|
||||||
"a"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -1094,7 +1094,8 @@ fn expand_onto_operation(
|
||||||
return Err(syn::Error::new_spanned(
|
return Err(syn::Error::new_spanned(
|
||||||
&kv.value,
|
&kv.value,
|
||||||
format!(
|
format!(
|
||||||
"unknown validation_sla '{other}'; expected Synchronous | EventualWithin | Background"
|
"unknown validation_sla '{other}'; expected Synchronous | \
|
||||||
|
EventualWithin | Background"
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
@ -1124,7 +1125,8 @@ fn expand_onto_operation(
|
||||||
let validation_sla = validation_sla.ok_or_else(|| {
|
let validation_sla = validation_sla.ok_or_else(|| {
|
||||||
syn::Error::new(
|
syn::Error::new(
|
||||||
Span::call_site(),
|
Span::call_site(),
|
||||||
"#[onto_operation] requires validation_sla = \"Synchronous\" | \"EventualWithin\" | \"Background\"",
|
"#[onto_operation] requires validation_sla = \"Synchronous\" | \"EventualWithin\" | \
|
||||||
|
\"Background\"",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let desc = description.or(doc_desc).ok_or_else(|| {
|
let desc = description.or(doc_desc).ok_or_else(|| {
|
||||||
|
|
@ -1152,7 +1154,8 @@ fn expand_onto_operation(
|
||||||
let fn_name = fn_ident_name.ok_or_else(|| {
|
let fn_name = fn_ident_name.ok_or_else(|| {
|
||||||
syn::Error::new(
|
syn::Error::new(
|
||||||
Span::call_site(),
|
Span::call_site(),
|
||||||
"#[onto_operation] must be applied to a function — the function pointer is used as the op's execute field",
|
"#[onto_operation] must be applied to a function — the function pointer is used as \
|
||||||
|
the op's execute field",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
Ok(emit_onto_operation(attr, &fn_name, input))
|
Ok(emit_onto_operation(attr, &fn_name, input))
|
||||||
|
|
@ -1299,7 +1302,9 @@ fn expand_onto_validator(
|
||||||
let doc_desc: Option<String> = parsed_fn
|
let doc_desc: Option<String> = parsed_fn
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|fn_item| fn_item.attrs.iter().cloned().find_map(doc_attr_text));
|
.and_then(|fn_item| fn_item.attrs.iter().cloned().find_map(doc_attr_text));
|
||||||
let fn_name: Option<String> = parsed_fn.as_ref().map(|fn_item| fn_item.sig.ident.to_string());
|
let fn_name: Option<String> = parsed_fn
|
||||||
|
.as_ref()
|
||||||
|
.map(|fn_item| fn_item.sig.ident.to_string());
|
||||||
|
|
||||||
let kv_args = syn::parse::Parser::parse2(
|
let kv_args = syn::parse::Parser::parse2(
|
||||||
Punctuated::<MetaNameValue, Token![,]>::parse_terminated,
|
Punctuated::<MetaNameValue, Token![,]>::parse_terminated,
|
||||||
|
|
@ -1330,9 +1335,7 @@ fn expand_onto_validator(
|
||||||
other => {
|
other => {
|
||||||
return Err(syn::Error::new_spanned(
|
return Err(syn::Error::new_spanned(
|
||||||
&kv.value,
|
&kv.value,
|
||||||
format!(
|
format!("unknown category '{other}'; expected Structural | Contextual"),
|
||||||
"unknown category '{other}'; expected Structural | Contextual"
|
|
||||||
),
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -1366,7 +1369,8 @@ fn expand_onto_validator(
|
||||||
let predicate_ref = predicate_ref.or(fn_name).ok_or_else(|| {
|
let predicate_ref = predicate_ref.or(fn_name).ok_or_else(|| {
|
||||||
syn::Error::new(
|
syn::Error::new(
|
||||||
Span::call_site(),
|
Span::call_site(),
|
||||||
"#[onto_validator] could not infer predicate_ref — supply it explicitly when applied to a non-fn item",
|
"#[onto_validator] could not infer predicate_ref — supply it explicitly when applied \
|
||||||
|
to a non-fn item",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
@ -1427,9 +1431,10 @@ fn emit_onto_validator(
|
||||||
mod tests {
|
mod tests {
|
||||||
mod onto_operation {
|
mod onto_operation {
|
||||||
mod expand {
|
mod expand {
|
||||||
use crate::expand_onto_operation;
|
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
|
||||||
|
use crate::expand_onto_operation;
|
||||||
|
|
||||||
fn must_expand(
|
fn must_expand(
|
||||||
args: proc_macro2::TokenStream,
|
args: proc_macro2::TokenStream,
|
||||||
input: proc_macro2::TokenStream,
|
input: proc_macro2::TokenStream,
|
||||||
|
|
@ -1461,10 +1466,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert!(s.contains("\"test_op\""), "must embed the op id");
|
assert!(s.contains("\"test_op\""), "must embed the op id");
|
||||||
assert!(s.contains("\"minimal test\""), "must embed description");
|
assert!(s.contains("\"minimal test\""), "must embed description");
|
||||||
assert!(
|
assert!(s.contains("\"Synchronous\""), "must embed validation_sla");
|
||||||
s.contains("\"Synchronous\""),
|
|
||||||
"must embed validation_sla"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -1542,9 +1544,10 @@ mod tests {
|
||||||
|
|
||||||
mod onto_validator {
|
mod onto_validator {
|
||||||
mod expand {
|
mod expand {
|
||||||
use crate::expand_onto_validator;
|
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
|
||||||
|
use crate::expand_onto_validator;
|
||||||
|
|
||||||
fn must_expand(
|
fn must_expand(
|
||||||
args: proc_macro2::TokenStream,
|
args: proc_macro2::TokenStream,
|
||||||
input: proc_macro2::TokenStream,
|
input: proc_macro2::TokenStream,
|
||||||
|
|
@ -1622,7 +1625,8 @@ mod tests {
|
||||||
description = "y",
|
description = "y",
|
||||||
category = "Probabilistic"
|
category = "Probabilistic"
|
||||||
};
|
};
|
||||||
let input = quote! { fn h(_: &Slice, _: &ValidationCtx) -> Verdict { Verdict::Accept } };
|
let input =
|
||||||
|
quote! { fn h(_: &Slice, _: &ValidationCtx) -> Verdict { Verdict::Accept } };
|
||||||
let err = expand_onto_validator(args, input).expect_err("bad category rejected");
|
let err = expand_onto_validator(args, input).expect_err("bad category rejected");
|
||||||
assert!(err.to_string().contains("unknown category"));
|
assert!(err.to_string().contains("unknown category"));
|
||||||
}
|
}
|
||||||
|
|
@ -1635,7 +1639,8 @@ mod tests {
|
||||||
category = "Structural",
|
category = "Structural",
|
||||||
bogus = "z"
|
bogus = "z"
|
||||||
};
|
};
|
||||||
let input = quote! { fn h(_: &Slice, _: &ValidationCtx) -> Verdict { Verdict::Accept } };
|
let input =
|
||||||
|
quote! { fn h(_: &Slice, _: &ValidationCtx) -> Verdict { Verdict::Accept } };
|
||||||
let err = expand_onto_validator(args, input).expect_err("unknown key rejected");
|
let err = expand_onto_validator(args, input).expect_err("unknown key rejected");
|
||||||
assert!(err.to_string().contains("unknown onto_validator key"));
|
assert!(err.to_string().contains("unknown onto_validator key"));
|
||||||
}
|
}
|
||||||
|
|
@ -1643,7 +1648,8 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn requires_id_and_category() {
|
fn requires_id_and_category() {
|
||||||
let args = quote! { description = "y" };
|
let args = quote! { description = "y" };
|
||||||
let input = quote! { fn h(_: &Slice, _: &ValidationCtx) -> Verdict { Verdict::Accept } };
|
let input =
|
||||||
|
quote! { fn h(_: &Slice, _: &ValidationCtx) -> Verdict { Verdict::Accept } };
|
||||||
let err = expand_onto_validator(args, input).expect_err("missing id");
|
let err = expand_onto_validator(args, input).expect_err("missing id");
|
||||||
assert!(err.to_string().contains("requires id"));
|
assert!(err.to_string().contains("requires id"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,18 @@
|
||||||
//! Four types govern how an ontology is shared and verified across
|
//! Four types govern how an ontology is shared and verified across
|
||||||
//! actors. Each type carries different governance and sync expectations:
|
//! actors. Each type carries different governance and sync expectations:
|
||||||
//!
|
//!
|
||||||
//! - **Type 1 (Protocol)** — mandatorily decentralized; ontoref-core is
|
//! - **Type 1 (Protocol)** — mandatorily decentralized; ontoref-core is the
|
||||||
//! the first extracted. Every actor MUST be able to operate without
|
//! first extracted. Every actor MUST be able to operate without relying on a
|
||||||
//! relying on a central authority for the ontology's content.
|
//! central authority for the ontology's content.
|
||||||
//! - **Type 2a (Shared-Domain Federable)** — shared across projects but
|
//! - **Type 2a (Shared-Domain Federable)** — shared across projects but
|
||||||
//! federable: a maintainer publishes the canonical version while
|
//! federable: a maintainer publishes the canonical version while forks may
|
||||||
//! forks may diverge with their own ids.
|
//! diverge with their own ids.
|
||||||
//! - **Type 2b (Shared-Domain Mandatorily Decentralized)** — shared
|
//! - **Type 2b (Shared-Domain Mandatorily Decentralized)** — shared across
|
||||||
//! across projects and mandatorily decentralized: no canonical
|
//! projects and mandatorily decentralized: no canonical maintainer; the
|
||||||
//! maintainer; the address itself is the authority.
|
//! address itself is the authority.
|
||||||
//! - **Type 3 (Project-Specific)** — opt-in per project; each project's
|
//! - **Type 3 (Project-Specific)** — opt-in per project; each project's
|
||||||
//! self-description lives here.
|
//! self-description lives here.
|
||||||
//! - **Type 4 (Private)** — tier-0; never leaves the actor's local
|
//! - **Type 4 (Private)** — tier-0; never leaves the actor's local environment.
|
||||||
//! environment.
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
@ -61,7 +60,10 @@ impl OntologyType {
|
||||||
|
|
||||||
/// Whether this type forbids any centralised authority.
|
/// Whether this type forbids any centralised authority.
|
||||||
pub fn must_be_decentralized(&self) -> bool {
|
pub fn must_be_decentralized(&self) -> bool {
|
||||||
matches!(self, Self::Type1Protocol | Self::Type2bMandatoryDecentralized)
|
matches!(
|
||||||
|
self,
|
||||||
|
Self::Type1Protocol | Self::Type2bMandatoryDecentralized
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether instances of this type are allowed to leave the actor.
|
/// Whether instances of this type are allowed to leave the actor.
|
||||||
|
|
|
||||||
|
|
@ -64,21 +64,12 @@ pub fn extract_core_to_oplog(
|
||||||
|
|
||||||
if let Some(nodes) = core_json.get("nodes").and_then(|v| v.as_array()) {
|
if let Some(nodes) = core_json.get("nodes").and_then(|v| v.as_array()) {
|
||||||
for node in nodes {
|
for node in nodes {
|
||||||
let entity_id = node
|
let entity_id = node.get("id").and_then(|v| v.as_str()).unwrap_or_default();
|
||||||
.get("id")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or_default();
|
|
||||||
if entity_id.is_empty() {
|
if entity_id.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let attrs = build_attrs_from_object(node);
|
let attrs = build_attrs_from_object(node);
|
||||||
let op = build_signed_op(
|
let op = build_signed_op(signing_key, &pubkey, entity_id, attrs, Hlc::new(next_ts, 0));
|
||||||
signing_key,
|
|
||||||
&pubkey,
|
|
||||||
entity_id,
|
|
||||||
attrs,
|
|
||||||
Hlc::new(next_ts, 0),
|
|
||||||
);
|
|
||||||
log.append(&op).map_err(Error::Append)?;
|
log.append(&op).map_err(Error::Append)?;
|
||||||
next_ts += 1;
|
next_ts += 1;
|
||||||
nodes_extracted += 1;
|
nodes_extracted += 1;
|
||||||
|
|
@ -137,14 +128,16 @@ pub fn write_ontology_refs(
|
||||||
}
|
}
|
||||||
|
|
||||||
let entry = format!(
|
let entry = format!(
|
||||||
" {{\n name = \"{name}\",\n ontology_id = \"{}\",\n oplog_path = \"{}\",\n classification = '{},\n }},\n",
|
" {{\n name = \"{name}\",\n ontology_id = \"{}\",\n oplog_path = \
|
||||||
|
\"{}\",\n classification = '{},\n }},\n",
|
||||||
ontology_id.to_hex(),
|
ontology_id.to_hex(),
|
||||||
oplog_path.display(),
|
oplog_path.display(),
|
||||||
classification.tag(),
|
classification.tag(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let content = format!(
|
let content = format!(
|
||||||
"# Auto-generated by `ontoref-daemon --extract-ontology`.\n# Re-run extraction to update; do not edit by hand.\n\n{{\n ontology_refs = [\n{entry} ],\n}}\n"
|
"# Auto-generated by `ontoref-daemon --extract-ontology`.\n# Re-run extraction to update; \
|
||||||
|
do not edit by hand.\n\n{{\n ontology_refs = [\n{entry} ],\n}}\n"
|
||||||
);
|
);
|
||||||
|
|
||||||
std::fs::write(refs_path, content).map_err(|e| Error::Io {
|
std::fs::write(refs_path, content).map_err(|e| Error::Io {
|
||||||
|
|
@ -212,7 +205,9 @@ fn build_signed_op(
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{fetch_cell_witness, verify_cell, CellQuery, OntologyRegistry, OntologyType, RegistryEntry};
|
use crate::{
|
||||||
|
fetch_cell_witness, verify_cell, CellQuery, OntologyRegistry, OntologyType, RegistryEntry,
|
||||||
|
};
|
||||||
|
|
||||||
fn signing_key() -> SigningKey {
|
fn signing_key() -> SigningKey {
|
||||||
SigningKey::from_bytes(&[7u8; 32])
|
SigningKey::from_bytes(&[7u8; 32])
|
||||||
|
|
@ -233,15 +228,18 @@ mod tests {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
let stats = extract_core_to_oplog(&core_json, &oplog_path, &signing_key())
|
let stats =
|
||||||
.expect("extract ok");
|
extract_core_to_oplog(&core_json, &oplog_path, &signing_key()).expect("extract ok");
|
||||||
assert_eq!(stats.nodes_extracted, 2);
|
assert_eq!(stats.nodes_extracted, 2);
|
||||||
assert_eq!(stats.edges_extracted, 1);
|
assert_eq!(stats.edges_extracted, 1);
|
||||||
|
|
||||||
// Idempotent: second extraction adds nothing.
|
// Idempotent: second extraction adds nothing.
|
||||||
let stats2 = extract_core_to_oplog(&core_json, &oplog_path, &signing_key())
|
let stats2 = extract_core_to_oplog(&core_json, &oplog_path, &signing_key())
|
||||||
.expect("second extract ok");
|
.expect("second extract ok");
|
||||||
assert_eq!(stats2.ontology_id, stats.ontology_id, "idempotent extraction must produce the same id");
|
assert_eq!(
|
||||||
|
stats2.ontology_id, stats.ontology_id,
|
||||||
|
"idempotent extraction must produce the same id"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -256,8 +254,8 @@ mod tests {
|
||||||
"edges": [],
|
"edges": [],
|
||||||
});
|
});
|
||||||
|
|
||||||
let stats = extract_core_to_oplog(&core_json, &oplog_path, &signing_key())
|
let stats =
|
||||||
.expect("extract ok");
|
extract_core_to_oplog(&core_json, &oplog_path, &signing_key()).expect("extract ok");
|
||||||
|
|
||||||
let mut registry = OntologyRegistry::empty();
|
let mut registry = OntologyRegistry::empty();
|
||||||
registry.register(RegistryEntry {
|
registry.register(RegistryEntry {
|
||||||
|
|
@ -269,8 +267,7 @@ mod tests {
|
||||||
|
|
||||||
// Project A can fetch the cell witness for protocol-not-runtime/name.
|
// Project A can fetch the cell witness for protocol-not-runtime/name.
|
||||||
let query = CellQuery::new("protocol-not-runtime", "name");
|
let query = CellQuery::new("protocol-not-runtime", "name");
|
||||||
let response =
|
let response = fetch_cell_witness(®istry, &stats.ontology_id, &query).expect("fetch ok");
|
||||||
fetch_cell_witness(®istry, &stats.ontology_id, &query).expect("fetch ok");
|
|
||||||
assert!(verify_cell(&response.witness, &response.state_root));
|
assert!(verify_cell(&response.witness, &response.state_root));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -281,8 +278,14 @@ mod tests {
|
||||||
let oplog_path = tmp.path().join("ontoref-core.oplog");
|
let oplog_path = tmp.path().join("ontoref-core.oplog");
|
||||||
let id = OntologyId([0xAB; 32]);
|
let id = OntologyId([0xAB; 32]);
|
||||||
|
|
||||||
write_ontology_refs(&refs, "ontoref-core", &id, &oplog_path, OntologyType::Type1Protocol)
|
write_ontology_refs(
|
||||||
.expect("write ok");
|
&refs,
|
||||||
|
"ontoref-core",
|
||||||
|
&id,
|
||||||
|
&oplog_path,
|
||||||
|
OntologyType::Type1Protocol,
|
||||||
|
)
|
||||||
|
.expect("write ok");
|
||||||
|
|
||||||
let content = std::fs::read_to_string(&refs).expect("read ok");
|
let content = std::fs::read_to_string(&refs).expect("read ok");
|
||||||
assert!(content.contains("ontoref-core"));
|
assert!(content.contains("ontoref-core"));
|
||||||
|
|
|
||||||
|
|
@ -82,12 +82,9 @@ pub struct OntologyHandle {
|
||||||
///
|
///
|
||||||
/// - [`Error::UnknownOntology`] when `id` is not registered locally.
|
/// - [`Error::UnknownOntology`] when `id` is not registered locally.
|
||||||
/// - [`Error::OpenOplog`] when the oplog file cannot be opened.
|
/// - [`Error::OpenOplog`] when the oplog file cannot be opened.
|
||||||
/// - [`Error::Append`] when the oplog returns an internal error while
|
/// - [`Error::Append`] when the oplog returns an internal error while replaying
|
||||||
/// replaying operations.
|
/// operations.
|
||||||
pub fn fetch_ontology(
|
pub fn fetch_ontology(registry: &OntologyRegistry, id: &OntologyId) -> Result<OntologyHandle> {
|
||||||
registry: &OntologyRegistry,
|
|
||||||
id: &OntologyId,
|
|
||||||
) -> Result<OntologyHandle> {
|
|
||||||
let entry = resolve(registry, id)?;
|
let entry = resolve(registry, id)?;
|
||||||
let state_root = compute_state_root(&entry.oplog_path)?;
|
let state_root = compute_state_root(&entry.oplog_path)?;
|
||||||
Ok(OntologyHandle {
|
Ok(OntologyHandle {
|
||||||
|
|
@ -110,10 +107,10 @@ pub fn fetch_ontology(
|
||||||
///
|
///
|
||||||
/// - [`Error::UnknownOntology`] when `id` is not registered locally.
|
/// - [`Error::UnknownOntology`] when `id` is not registered locally.
|
||||||
/// - [`Error::OpenOplog`] when the oplog file cannot be opened.
|
/// - [`Error::OpenOplog`] when the oplog file cannot be opened.
|
||||||
/// - [`Error::CellMissing`] when the cell `(entity, attr)` has never
|
/// - [`Error::CellMissing`] when the cell `(entity, attr)` has never been
|
||||||
/// been asserted under the ontology's commit layer.
|
/// asserted under the ontology's commit layer.
|
||||||
/// - [`Error::Append`] when the oplog returns an internal error while
|
/// - [`Error::Append`] when the oplog returns an internal error while replaying
|
||||||
/// replaying operations.
|
/// operations.
|
||||||
pub fn fetch_cell_witness(
|
pub fn fetch_cell_witness(
|
||||||
registry: &OntologyRegistry,
|
registry: &OntologyRegistry,
|
||||||
id: &OntologyId,
|
id: &OntologyId,
|
||||||
|
|
@ -135,10 +132,7 @@ pub fn fetch_cell_witness(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve<'r>(
|
fn resolve<'r>(registry: &'r OntologyRegistry, id: &OntologyId) -> Result<&'r RegistryEntry> {
|
||||||
registry: &'r OntologyRegistry,
|
|
||||||
id: &OntologyId,
|
|
||||||
) -> Result<&'r RegistryEntry> {
|
|
||||||
registry
|
registry
|
||||||
.lookup(id)
|
.lookup(id)
|
||||||
.ok_or_else(|| Error::UnknownOntology(id.to_hex()))
|
.ok_or_else(|| Error::UnknownOntology(id.to_hex()))
|
||||||
|
|
@ -146,9 +140,7 @@ fn resolve<'r>(
|
||||||
|
|
||||||
fn rebuild_commit_layer(oplog_path: &PathBuf) -> Result<BinaryMerkle> {
|
fn rebuild_commit_layer(oplog_path: &PathBuf) -> Result<BinaryMerkle> {
|
||||||
let log = open_oplog(oplog_path)?;
|
let log = open_oplog(oplog_path)?;
|
||||||
let ops: Vec<Operation> = log
|
let ops: Vec<Operation> = log.all_ops_in_hlc_order().map_err(Error::Append)?;
|
||||||
.all_ops_in_hlc_order()
|
|
||||||
.map_err(Error::Append)?;
|
|
||||||
let mut commit = BinaryMerkle::new();
|
let mut commit = BinaryMerkle::new();
|
||||||
for op in &ops {
|
for op in &ops {
|
||||||
commit.apply(op);
|
commit.apply(op);
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,9 @@ pub mod verify;
|
||||||
|
|
||||||
pub use classification::OntologyType;
|
pub use classification::OntologyType;
|
||||||
pub use extract::{extract_core_to_oplog, write_ontology_refs, ExtractStats};
|
pub use extract::{extract_core_to_oplog, write_ontology_refs, ExtractStats};
|
||||||
pub use fetch::{fetch_cell_witness, fetch_ontology, CellQuery, CellWitnessResponse, OntologyHandle};
|
pub use fetch::{
|
||||||
|
fetch_cell_witness, fetch_ontology, CellQuery, CellWitnessResponse, OntologyHandle,
|
||||||
|
};
|
||||||
pub use ontology_id::{compute_ontology_id, OntologyId};
|
pub use ontology_id::{compute_ontology_id, OntologyId};
|
||||||
pub use publish::publish_op;
|
pub use publish::publish_op;
|
||||||
pub use registry::{OntologyRegistry, RegistryEntry};
|
pub use registry::{OntologyRegistry, RegistryEntry};
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ use crate::{Error, Result};
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// - [`Error::OpenOplog`] when the oplog file cannot be opened.
|
/// - [`Error::OpenOplog`] when the oplog file cannot be opened.
|
||||||
/// - [`Error::Append`] when the oplog rejects the operation (bad
|
/// - [`Error::Append`] when the oplog rejects the operation (bad signature,
|
||||||
/// signature, unknown parent, non-monotonic timestamp, IO).
|
/// unknown parent, non-monotonic timestamp, IO).
|
||||||
pub fn publish_op(oplog_path: &Path, op: &Operation) -> Result<OpId> {
|
pub fn publish_op(oplog_path: &Path, op: &Operation) -> Result<OpId> {
|
||||||
let log = OpLog::open(oplog_path).map_err(|e| Error::OpenOplog {
|
let log = OpLog::open(oplog_path).map_err(|e| Error::OpenOplog {
|
||||||
path: oplog_path.display().to_string(),
|
path: oplog_path.display().to_string(),
|
||||||
|
|
|
||||||
|
|
@ -50,17 +50,16 @@ impl OntologyRegistry {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// - [`Error::Io`] when the file exists but cannot be read.
|
/// - [`Error::Io`] when the file exists but cannot be read.
|
||||||
/// - [`Error::RegistryParse`] when the file exists but does not
|
/// - [`Error::RegistryParse`] when the file exists but does not parse into
|
||||||
/// parse into the registry schema.
|
/// the registry schema.
|
||||||
pub fn load(path: &Path) -> Result<Self> {
|
pub fn load(path: &Path) -> Result<Self> {
|
||||||
match std::fs::read(path) {
|
match std::fs::read(path) {
|
||||||
Ok(bytes) => {
|
Ok(bytes) => {
|
||||||
let reg = serde_json::from_slice::<Self>(&bytes).map_err(|e| {
|
let reg =
|
||||||
Error::RegistryParse {
|
serde_json::from_slice::<Self>(&bytes).map_err(|e| Error::RegistryParse {
|
||||||
path: path.display().to_string(),
|
path: path.display().to_string(),
|
||||||
message: e.to_string(),
|
message: e.to_string(),
|
||||||
}
|
})?;
|
||||||
})?;
|
|
||||||
Ok(reg)
|
Ok(reg)
|
||||||
}
|
}
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
|
||||||
|
|
@ -102,7 +101,11 @@ impl OntologyRegistry {
|
||||||
/// Register a new entry, replacing any existing one with the same
|
/// Register a new entry, replacing any existing one with the same
|
||||||
/// ontology id.
|
/// ontology id.
|
||||||
pub fn register(&mut self, entry: RegistryEntry) {
|
pub fn register(&mut self, entry: RegistryEntry) {
|
||||||
if let Some(existing) = self.entries.iter_mut().find(|e| e.ontology_id == entry.ontology_id) {
|
if let Some(existing) = self
|
||||||
|
.entries
|
||||||
|
.iter_mut()
|
||||||
|
.find(|e| e.ontology_id == entry.ontology_id)
|
||||||
|
{
|
||||||
*existing = entry;
|
*existing = entry;
|
||||||
} else {
|
} else {
|
||||||
self.entries.push(entry);
|
self.entries.push(entry);
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,16 @@
|
||||||
|
|
||||||
use ed25519_dalek::SigningKey;
|
use ed25519_dalek::SigningKey;
|
||||||
use ontoref_commit::BinaryMerkle;
|
use ontoref_commit::BinaryMerkle;
|
||||||
|
use ontoref_ontology_content::{
|
||||||
|
compute_ontology_id, fetch_cell_witness, verify_cell, CellQuery, Error, OntologyRegistry,
|
||||||
|
OntologyType, RegistryEntry,
|
||||||
|
};
|
||||||
use ontoref_oplog::OpLog;
|
use ontoref_oplog::OpLog;
|
||||||
use ontoref_types::{
|
use ontoref_types::{
|
||||||
operation::{OpBody, OpPayload, Operation},
|
operation::{OpBody, OpPayload, Operation},
|
||||||
AttrId, EntityId, Hlc, PublicKey, Value,
|
AttrId, EntityId, Hlc, PublicKey, Value,
|
||||||
};
|
};
|
||||||
|
|
||||||
use ontoref_ontology_content::{
|
|
||||||
compute_ontology_id, fetch_cell_witness, verify_cell, CellQuery, Error, OntologyRegistry,
|
|
||||||
OntologyType, RegistryEntry,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn signing_key(seed: u8) -> SigningKey {
|
fn signing_key(seed: u8) -> SigningKey {
|
||||||
SigningKey::from_bytes(&[seed; 32])
|
SigningKey::from_bytes(&[seed; 32])
|
||||||
}
|
}
|
||||||
|
|
@ -45,7 +44,10 @@ fn assert_op(entity: &str, attr: &str, value: Value, ts_logical: u64, seed: u8)
|
||||||
|
|
||||||
/// Append `ops` to a fresh oplog at `path` and return the resulting
|
/// Append `ops` to a fresh oplog at `path` and return the resulting
|
||||||
/// [`OntologyId`] computed from the oplog's heads.
|
/// [`OntologyId`] computed from the oplog's heads.
|
||||||
fn build_ontology(path: &std::path::Path, ops: &[Operation]) -> ontoref_ontology_content::OntologyId {
|
fn build_ontology(
|
||||||
|
path: &std::path::Path,
|
||||||
|
ops: &[Operation],
|
||||||
|
) -> ontoref_ontology_content::OntologyId {
|
||||||
let log = OpLog::open(path).expect("open oplog");
|
let log = OpLog::open(path).expect("open oplog");
|
||||||
for op in ops {
|
for op in ops {
|
||||||
log.append(op).expect("append op");
|
log.append(op).expect("append op");
|
||||||
|
|
@ -61,20 +63,8 @@ fn fetch_cell_witness_returns_only_witness_and_root() {
|
||||||
// ── Foreign ontology X — carries an "x-entity" cell. ────────────────────
|
// ── Foreign ontology X — carries an "x-entity" cell. ────────────────────
|
||||||
let x_oplog = tmp.path().join("x.oplog");
|
let x_oplog = tmp.path().join("x.oplog");
|
||||||
let x_ops = vec![
|
let x_ops = vec![
|
||||||
assert_op(
|
assert_op("x-entity", "x-attr", Value::Str("x-value".into()), 1, 0xAA),
|
||||||
"x-entity",
|
assert_op("x-entity-other", "x-attr-other", Value::Int(42), 2, 0xAA),
|
||||||
"x-attr",
|
|
||||||
Value::Str("x-value".into()),
|
|
||||||
1,
|
|
||||||
0xAA,
|
|
||||||
),
|
|
||||||
assert_op(
|
|
||||||
"x-entity-other",
|
|
||||||
"x-attr-other",
|
|
||||||
Value::Int(42),
|
|
||||||
2,
|
|
||||||
0xAA,
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
let x_id = build_ontology(&x_oplog, &x_ops);
|
let x_id = build_ontology(&x_oplog, &x_ops);
|
||||||
|
|
||||||
|
|
@ -116,8 +106,8 @@ fn fetch_cell_witness_returns_only_witness_and_root() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// The witness contains the canonical encoding of "x-value".
|
// The witness contains the canonical encoding of "x-value".
|
||||||
let expected = ontoref_types::canonical::encode(&Value::Str("x-value".into()))
|
let expected =
|
||||||
.expect("encode value");
|
ontoref_types::canonical::encode(&Value::Str("x-value".into())).expect("encode value");
|
||||||
assert_eq!(response.witness.value, expected);
|
assert_eq!(response.witness.value, expected);
|
||||||
|
|
||||||
// ── Negative: the witness must NOT verify under Y's state root. ────────
|
// ── Negative: the witness must NOT verify under Y's state root. ────────
|
||||||
|
|
@ -130,7 +120,10 @@ fn fetch_cell_witness_returns_only_witness_and_root() {
|
||||||
y_commit.apply(op);
|
y_commit.apply(op);
|
||||||
}
|
}
|
||||||
let y_root = y_commit.root();
|
let y_root = y_commit.root();
|
||||||
assert_ne!(response.state_root, y_root, "X and Y must have different roots");
|
assert_ne!(
|
||||||
|
response.state_root, y_root,
|
||||||
|
"X and Y must have different roots"
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!verify_cell(&response.witness, &y_root),
|
!verify_cell(&response.witness, &y_root),
|
||||||
"witness from X must not verify under Y's state root"
|
"witness from X must not verify under Y's state root"
|
||||||
|
|
@ -144,7 +137,8 @@ fn fetch_cell_witness_returns_only_witness_and_root() {
|
||||||
let full_oplog_size_bound = x_ops.len() * 1024; // pessimistic per-op size
|
let full_oplog_size_bound = x_ops.len() * 1024; // pessimistic per-op size
|
||||||
assert!(
|
assert!(
|
||||||
response_size < full_oplog_size_bound,
|
response_size < full_oplog_size_bound,
|
||||||
"response must be bounded — full oplog leakage would exceed {full_oplog_size_bound} bytes; got {response_size}"
|
"response must be bounded — full oplog leakage would exceed {full_oplog_size_bound} \
|
||||||
|
bytes; got {response_size}"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fetching a cell that does not exist in X returns CellMissing,
|
// Fetching a cell that does not exist in X returns CellMissing,
|
||||||
|
|
@ -159,10 +153,7 @@ fn fetch_cell_witness_returns_only_witness_and_root() {
|
||||||
fn fetch_unknown_ontology_id_returns_error() {
|
fn fetch_unknown_ontology_id_returns_error() {
|
||||||
let tmp = tempfile::tempdir().expect("tempdir");
|
let tmp = tempfile::tempdir().expect("tempdir");
|
||||||
let oplog = tmp.path().join("x.oplog");
|
let oplog = tmp.path().join("x.oplog");
|
||||||
let _ = build_ontology(
|
let _ = build_ontology(&oplog, &[assert_op("e", "a", Value::Int(1), 1, 0xCC)]);
|
||||||
&oplog,
|
|
||||||
&[assert_op("e", "a", Value::Int(1), 1, 0xCC)],
|
|
||||||
);
|
|
||||||
|
|
||||||
let registry = OntologyRegistry::empty();
|
let registry = OntologyRegistry::empty();
|
||||||
let unknown_id = ontoref_ontology_content::OntologyId([0xFFu8; 32]);
|
let unknown_id = ontoref_ontology_content::OntologyId([0xFFu8; 32]);
|
||||||
|
|
|
||||||
|
|
@ -291,10 +291,12 @@ pub fn is_legacy_layout(project_root: &Path) -> bool {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_prefers_canonical_when_both_exist() {
|
fn resolve_prefers_canonical_when_both_exist() {
|
||||||
let dir = TempDir::new().expect("tempdir");
|
let dir = TempDir::new().expect("tempdir");
|
||||||
|
|
@ -370,7 +372,10 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn relative_in_section_matches_both_prefixes() {
|
fn relative_in_section_matches_both_prefixes() {
|
||||||
assert!(relative_in_section(".ontoref/ontology/core.ncl", "ontology"));
|
assert!(relative_in_section(
|
||||||
|
".ontoref/ontology/core.ncl",
|
||||||
|
"ontology"
|
||||||
|
));
|
||||||
assert!(relative_in_section(".ontology/core.ncl", "ontology"));
|
assert!(relative_in_section(".ontology/core.ncl", "ontology"));
|
||||||
assert!(relative_in_section(".ontoref/adrs/adr-001.ncl", "adrs"));
|
assert!(relative_in_section(".ontoref/adrs/adr-001.ncl", "adrs"));
|
||||||
assert!(relative_in_section("adrs/adr-001.ncl", "adrs"));
|
assert!(relative_in_section("adrs/adr-001.ncl", "adrs"));
|
||||||
|
|
|
||||||
|
|
@ -395,7 +395,8 @@ impl Gate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Positioning (ADR-035) ─────────────────────────────────────────────────────
|
// ── Positioning (ADR-035)
|
||||||
|
// ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Runtime view of a project's positioning layer.
|
/// Runtime view of a project's positioning layer.
|
||||||
///
|
///
|
||||||
|
|
@ -611,19 +612,15 @@ impl Positioning {
|
||||||
cfg.value_props =
|
cfg.value_props =
|
||||||
load_dir::<ValueProp>(&positioning_dir.join("value-props"), "value-prop")?;
|
load_dir::<ValueProp>(&positioning_dir.join("value-props"), "value-prop")?;
|
||||||
cfg.campaigns = load_dir::<Campaign>(&positioning_dir.join("campaigns"), "campaign")?;
|
cfg.campaigns = load_dir::<Campaign>(&positioning_dir.join("campaigns"), "campaign")?;
|
||||||
cfg.differentiators = load_dir::<Differentiator>(
|
cfg.differentiators =
|
||||||
&positioning_dir.join("differentiators"),
|
load_dir::<Differentiator>(&positioning_dir.join("differentiators"), "differentiator")?;
|
||||||
"differentiator",
|
|
||||||
)?;
|
|
||||||
cfg.competitors =
|
cfg.competitors =
|
||||||
load_dir::<Competitor>(&positioning_dir.join("competitors"), "competitor")?;
|
load_dir::<Competitor>(&positioning_dir.join("competitors"), "competitor")?;
|
||||||
cfg.mechanisms =
|
cfg.mechanisms =
|
||||||
load_dir::<DifusionMechanism>(&positioning_dir.join("difusion"), "mechanism")?;
|
load_dir::<DifusionMechanism>(&positioning_dir.join("difusion"), "mechanism")?;
|
||||||
cfg.proofs = load_dir::<Proof>(&positioning_dir.join("proofs"), "proof")?;
|
cfg.proofs = load_dir::<Proof>(&positioning_dir.join("proofs"), "proof")?;
|
||||||
cfg.viability_paths = load_dir::<ViabilityPath>(
|
cfg.viability_paths =
|
||||||
&positioning_dir.join("viability"),
|
load_dir::<ViabilityPath>(&positioning_dir.join("viability"), "viability-path")?;
|
||||||
"viability-path",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Self::from_config(cfg)
|
Self::from_config(cfg)
|
||||||
}
|
}
|
||||||
|
|
@ -673,9 +670,7 @@ impl Positioning {
|
||||||
.map(|&i| &self.viability_paths[i])
|
.map(|&i| &self.viability_paths[i])
|
||||||
}
|
}
|
||||||
pub fn value_prop_by_id(&self, id: &str) -> Option<&ValueProp> {
|
pub fn value_prop_by_id(&self, id: &str) -> Option<&ValueProp> {
|
||||||
self.value_prop_by_id
|
self.value_prop_by_id.get(id).map(|&i| &self.value_props[i])
|
||||||
.get(id)
|
|
||||||
.map(|&i| &self.value_props[i])
|
|
||||||
}
|
}
|
||||||
pub fn campaign_by_id(&self, id: &str) -> Option<&Campaign> {
|
pub fn campaign_by_id(&self, id: &str) -> Option<&Campaign> {
|
||||||
self.campaign_by_id.get(id).map(|&i| &self.campaigns[i])
|
self.campaign_by_id.get(id).map(|&i| &self.campaigns[i])
|
||||||
|
|
@ -698,10 +693,7 @@ impl Positioning {
|
||||||
/// e.g. `_template.ncl`, `_smoke-test.ncl`). A missing `dir` yields an
|
/// e.g. `_template.ncl`, `_smoke-test.ncl`). A missing `dir` yields an
|
||||||
/// empty Vec — sub-directories are independently optional within the
|
/// empty Vec — sub-directories are independently optional within the
|
||||||
/// positioning layer (a project may have audiences but no campaigns yet).
|
/// positioning layer (a project may have audiences but no campaigns yet).
|
||||||
fn load_dir<T: serde::de::DeserializeOwned>(
|
fn load_dir<T: serde::de::DeserializeOwned>(dir: &Path, section: &'static str) -> Result<Vec<T>> {
|
||||||
dir: &Path,
|
|
||||||
section: &'static str,
|
|
||||||
) -> Result<Vec<T>> {
|
|
||||||
if !dir.exists() {
|
if !dir.exists() {
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
|
|
@ -722,10 +714,8 @@ fn load_dir<T: serde::de::DeserializeOwned>(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let raw = nickel_export(&path, section)?;
|
let raw = nickel_export(&path, section)?;
|
||||||
let item: T = serde_json::from_slice(&raw).map_err(|e| OntologyError::Parse {
|
let item: T = serde_json::from_slice(&raw)
|
||||||
section,
|
.map_err(|e| OntologyError::Parse { section, source: e })?;
|
||||||
source: e,
|
|
||||||
})?;
|
|
||||||
items.push(item);
|
items.push(item);
|
||||||
}
|
}
|
||||||
Ok(items)
|
Ok(items)
|
||||||
|
|
@ -1005,11 +995,7 @@ mod tests {
|
||||||
let cfg = PositioningConfig {
|
let cfg = PositioningConfig {
|
||||||
audiences: vec![mk_audience("aud-1")],
|
audiences: vec![mk_audience("aud-1")],
|
||||||
value_props: vec![mk_value_prop("vp-1", vec!["aud-1"])],
|
value_props: vec![mk_value_prop("vp-1", vec!["aud-1"])],
|
||||||
campaigns: vec![mk_campaign(
|
campaigns: vec![mk_campaign("camp-1", vec!["aud-missing"], vec!["vp-1"])],
|
||||||
"camp-1",
|
|
||||||
vec!["aud-missing"],
|
|
||||||
vec!["vp-1"],
|
|
||||||
)],
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let err = Positioning::from_config(cfg).expect_err("must reject");
|
let err = Positioning::from_config(cfg).expect_err("must reject");
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
//! Byte-stable renderers from authoritative Rust state to NCL files (ADR-025 / D9).
|
//! Byte-stable renderers from authoritative Rust state to NCL files (ADR-025 /
|
||||||
|
//! D9).
|
||||||
//!
|
//!
|
||||||
//! Phase 1 covers `state.ncl` fully — the file consumed by the
|
//! Phase 1 covers `state.ncl` fully — the file consumed by the
|
||||||
//! `move_fsm_state` operation (O3). Other renderable paths
|
//! `move_fsm_state` operation (O3). Other renderable paths
|
||||||
|
|
|
||||||
|
|
@ -45,12 +45,11 @@ pub enum PipelineError {
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// - [`PipelineError::Render`] — propagated from
|
/// - [`PipelineError::Render`] — propagated from [`crate::render::render`] for
|
||||||
/// [`crate::render::render`] for unsupported paths or render failure.
|
/// unsupported paths or render failure.
|
||||||
/// - [`PipelineError::TypecheckFailed`] — when `nickel typecheck`
|
/// - [`PipelineError::TypecheckFailed`] — when `nickel typecheck` reports a
|
||||||
/// reports a contract or syntax error on the staged file.
|
/// contract or syntax error on the staged file.
|
||||||
/// - [`PipelineError::NickelMissing`] — the `nickel` binary cannot be
|
/// - [`PipelineError::NickelMissing`] — the `nickel` binary cannot be spawned.
|
||||||
/// spawned.
|
|
||||||
/// - [`PipelineError::Io`] — directory creation, write, or rename failed.
|
/// - [`PipelineError::Io`] — directory creation, write, or rename failed.
|
||||||
pub fn render_and_write(state: &StateConfig, path: &Path) -> Result<(), PipelineError> {
|
pub fn render_and_write(state: &StateConfig, path: &Path) -> Result<(), PipelineError> {
|
||||||
let bytes = render(state, path)?;
|
let bytes = render(state, path)?;
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,10 @@ pub enum AbstractionLevel {
|
||||||
Moment,
|
Moment,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Edge type between nodes. Reconciled 2026-07-09 (G-2) to the vocabulary actually
|
/// Edge type between nodes. Reconciled 2026-07-09 (G-2) to the vocabulary
|
||||||
/// used in core.ncl, mirroring `code/ontology/schemas/core.ncl::edge_type`; the
|
/// actually used in core.ncl, mirroring
|
||||||
/// prior enum listed four unused kinds and omitted four in active use.
|
/// `code/ontology/schemas/core.ncl::edge_type`; the prior enum listed four
|
||||||
|
/// unused kinds and omitted four in active use.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum EdgeType {
|
pub enum EdgeType {
|
||||||
Complements,
|
Complements,
|
||||||
|
|
@ -275,7 +276,8 @@ pub enum ProofKind {
|
||||||
Benchmark,
|
Benchmark,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Audience validation state (ADR-043) — Hypothesis until a Proof references it.
|
/// Audience validation state (ADR-043) — Hypothesis until a Proof references
|
||||||
|
/// it.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||||
pub enum AudienceStatus {
|
pub enum AudienceStatus {
|
||||||
#[default]
|
#[default]
|
||||||
|
|
@ -517,10 +519,11 @@ pub struct ViabilityConverges {
|
||||||
|
|
||||||
/// ViabilityPath — the sustainability seam (ADR-055), a second costura parallel
|
/// ViabilityPath — the sustainability seam (ADR-055), a second costura parallel
|
||||||
/// to [`Proof`]. Where a Proof answers "is the claim real?", a ViabilityPath
|
/// to [`Proof`]. Where a Proof answers "is the claim real?", a ViabilityPath
|
||||||
/// answers "does the claim sustain its maker?". It converges PARA QUIÉN × CÓMO ×
|
/// answers "does the claim sustain its maker?". It converges PARA QUIÉN × CÓMO
|
||||||
/// (QUÉ×QUIÉN); nothing references it. `realized_outcome` is present only once a
|
/// × (QUÉ×QUIÉN); nothing references it. `realized_outcome` is present only
|
||||||
/// real measured compensation promotes the path past `Draft` (the one-directional
|
/// once a real measured compensation promotes the path past `Draft` (the
|
||||||
/// realized⇒outcome gate, enforced by the Nickel `make_viability_path` contract).
|
/// one-directional realized⇒outcome gate, enforced by the Nickel
|
||||||
|
/// `make_viability_path` contract).
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ViabilityPath {
|
pub struct ViabilityPath {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|
@ -599,7 +602,11 @@ mod positioning_tests {
|
||||||
other => panic!("expected FileExists, got {other:?}"),
|
other => panic!("expected FileExists, got {other:?}"),
|
||||||
}
|
}
|
||||||
match &vp.launch_criteria[1] {
|
match &vp.launch_criteria[1] {
|
||||||
LaunchCheck::Grep { pattern, paths, must_be_empty } => {
|
LaunchCheck::Grep {
|
||||||
|
pattern,
|
||||||
|
paths,
|
||||||
|
must_be_empty,
|
||||||
|
} => {
|
||||||
assert_eq!(pattern, "p");
|
assert_eq!(pattern, "p");
|
||||||
assert_eq!(paths, &vec!["a".to_string()]);
|
assert_eq!(paths, &vec!["a".to_string()]);
|
||||||
assert!(!*must_be_empty);
|
assert!(!*must_be_empty);
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,11 @@ use ontoref_ontology::types::StateConfig;
|
||||||
|
|
||||||
/// Project root that holds the `.ontoref/` spine.
|
/// Project root that holds the `.ontoref/` spine.
|
||||||
///
|
///
|
||||||
/// Walks up from this crate until it finds the directory containing `.ontoref/`.
|
/// Walks up from this crate until it finds the directory containing
|
||||||
/// Post-ADR-048 (constellation) the spine sits at the constellation parent,
|
/// `.ontoref/`. Post-ADR-048 (constellation) the spine sits at the
|
||||||
/// outside `code/`; pre-ADR-048 it was inside the workspace. Searching ancestors
|
/// constellation parent, outside `code/`; pre-ADR-048 it was inside the
|
||||||
/// resolves it under either layout instead of hard-coding a hop count.
|
/// workspace. Searching ancestors resolves it under either layout instead of
|
||||||
|
/// hard-coding a hop count.
|
||||||
fn workspace_root() -> PathBuf {
|
fn workspace_root() -> PathBuf {
|
||||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
.ancestors()
|
.ancestors()
|
||||||
|
|
@ -46,6 +47,12 @@ fn repeated_render_produces_identical_bytes() {
|
||||||
let second = render_state_ncl(&state);
|
let second = render_state_ncl(&state);
|
||||||
let third = render_state_ncl(&state);
|
let third = render_state_ncl(&state);
|
||||||
|
|
||||||
assert_eq!(first, second, "render is not deterministic across two calls");
|
assert_eq!(
|
||||||
assert_eq!(second, third, "render is not deterministic across three calls");
|
first, second,
|
||||||
|
"render is not deterministic across two calls"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
second, third,
|
||||||
|
"render is not deterministic across three calls"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,11 @@ use ontoref_ontology::types::StateConfig;
|
||||||
|
|
||||||
/// Project root that holds the `.ontoref/` spine.
|
/// Project root that holds the `.ontoref/` spine.
|
||||||
///
|
///
|
||||||
/// Walks up from this crate until it finds the directory containing `.ontoref/`.
|
/// Walks up from this crate until it finds the directory containing
|
||||||
/// Post-ADR-048 (constellation) the spine sits at the constellation parent,
|
/// `.ontoref/`. Post-ADR-048 (constellation) the spine sits at the
|
||||||
/// outside `code/`; pre-ADR-048 it was inside the workspace. Searching ancestors
|
/// constellation parent, outside `code/`; pre-ADR-048 it was inside the
|
||||||
/// resolves it under either layout instead of hard-coding a hop count.
|
/// workspace. Searching ancestors resolves it under either layout instead of
|
||||||
|
/// hard-coding a hop count.
|
||||||
fn workspace_root() -> PathBuf {
|
fn workspace_root() -> PathBuf {
|
||||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
.ancestors()
|
.ancestors()
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
//! Typecheck pipeline for the `state.ncl` renderer (O1.5 / D29).
|
//! Typecheck pipeline for the `state.ncl` renderer (O1.5 / D29).
|
||||||
//!
|
//!
|
||||||
//! Verifies two contracts:
|
//! Verifies two contracts:
|
||||||
//! 1. Rendered bytes from any well-formed `StateConfig` pass
|
//! 1. Rendered bytes from any well-formed `StateConfig` pass `nickel
|
||||||
//! `nickel typecheck`.
|
//! typecheck`.
|
||||||
//! 2. The transactional pipeline aborts and removes its staging file
|
//! 2. The transactional pipeline aborts and removes its staging file when
|
||||||
//! when typecheck fails — proving the invariant "every NCL file on
|
//! typecheck fails — proving the invariant "every NCL file on disk parses
|
||||||
//! disk parses against its schema".
|
//! against its schema".
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
@ -16,10 +16,11 @@ use ontoref_ontology::types::StateConfig;
|
||||||
|
|
||||||
/// Project root that holds the `.ontoref/` spine.
|
/// Project root that holds the `.ontoref/` spine.
|
||||||
///
|
///
|
||||||
/// Walks up from this crate until it finds the directory containing `.ontoref/`.
|
/// Walks up from this crate until it finds the directory containing
|
||||||
/// Post-ADR-048 (constellation) the spine sits at the constellation parent,
|
/// `.ontoref/`. Post-ADR-048 (constellation) the spine sits at the
|
||||||
/// outside `code/`; pre-ADR-048 it was inside the workspace. Searching ancestors
|
/// constellation parent, outside `code/`; pre-ADR-048 it was inside the
|
||||||
/// resolves it under either layout instead of hard-coding a hop count.
|
/// workspace. Searching ancestors resolves it under either layout instead of
|
||||||
|
/// hard-coding a hop count.
|
||||||
fn workspace_root() -> PathBuf {
|
fn workspace_root() -> PathBuf {
|
||||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
.ancestors()
|
.ancestors()
|
||||||
|
|
@ -89,5 +90,8 @@ fn pipeline_writes_atomically_when_typecheck_passes() {
|
||||||
let staging_present = entries
|
let staging_present = entries
|
||||||
.iter()
|
.iter()
|
||||||
.any(|n| n.to_string_lossy().ends_with(".staged"));
|
.any(|n| n.to_string_lossy().ends_with(".staged"));
|
||||||
assert!(!staging_present, "staging file must be cleaned up on success");
|
assert!(
|
||||||
|
!staging_present,
|
||||||
|
"staging file must be cleaned up on success"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ pub enum Error {
|
||||||
/// An operation references a parent that is not in the log.
|
/// An operation references a parent that is not in the log.
|
||||||
#[error("unknown parent {0:?}")]
|
#[error("unknown parent {0:?}")]
|
||||||
UnknownParent(OpId),
|
UnknownParent(OpId),
|
||||||
/// An operation declares a timestamp not strictly greater than every parent's.
|
/// An operation declares a timestamp not strictly greater than every
|
||||||
|
/// parent's.
|
||||||
#[error("non-monotonic timestamp on operation {0:?}")]
|
#[error("non-monotonic timestamp on operation {0:?}")]
|
||||||
NonMonotonicTimestamp(OpId),
|
NonMonotonicTimestamp(OpId),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ pub struct OpLog {
|
||||||
|
|
||||||
impl OpLog {
|
impl OpLog {
|
||||||
/// Open or create the log at `path`. The redb file is created on first use;
|
/// Open or create the log at `path`. The redb file is created on first use;
|
||||||
/// tables are initialised inside a transaction so subsequent reads find them.
|
/// tables are initialised inside a transaction so subsequent reads find
|
||||||
|
/// them.
|
||||||
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
|
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
|
||||||
let db = Database::create(path).map_err(Error::from)?;
|
let db = Database::create(path).map_err(Error::from)?;
|
||||||
// Touch both tables in a write transaction so they exist for reads.
|
// Touch both tables in a write transaction so they exist for reads.
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use ed25519_dalek::SigningKey;
|
use ed25519_dalek::SigningKey;
|
||||||
|
use ontoref_oplog::{Error, OpLog};
|
||||||
use ontoref_types::{
|
use ontoref_types::{
|
||||||
operation::{OpBody, OpPayload, Operation},
|
operation::{OpBody, OpPayload, Operation},
|
||||||
Hlc, OpId, PublicKey,
|
Hlc, OpId, PublicKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
use ontoref_oplog::{Error, OpLog};
|
|
||||||
|
|
||||||
fn signing_key() -> SigningKey {
|
fn signing_key() -> SigningKey {
|
||||||
SigningKey::from_bytes(&[42u8; 32])
|
SigningKey::from_bytes(&[42u8; 32])
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +46,11 @@ fn replay_yields_same_heads_for_linear_chain() {
|
||||||
// Drop the OpLog (closes the redb handle), reopen the file, re-read heads.
|
// Drop the OpLog (closes the redb handle), reopen the file, re-read heads.
|
||||||
let reopened_heads = OpLog::open(&path).unwrap().heads().unwrap();
|
let reopened_heads = OpLog::open(&path).unwrap().heads().unwrap();
|
||||||
assert_eq!(original_heads, reopened_heads);
|
assert_eq!(original_heads, reopened_heads);
|
||||||
assert_eq!(original_heads.len(), 1, "a linear chain has exactly one head");
|
assert_eq!(
|
||||||
|
original_heads.len(),
|
||||||
|
1,
|
||||||
|
"a linear chain has exactly one head"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@ static CAPABILITIES: &[TierCapability] = &[
|
||||||
id: "describe",
|
id: "describe",
|
||||||
label: "Describe / self-knowledge",
|
label: "Describe / self-knowledge",
|
||||||
min_tier: Tier::Tier0,
|
min_tier: Tier::Tier0,
|
||||||
description: "describe project, capabilities, config, connections, state — full self-knowledge surface",
|
description: "describe project, capabilities, config, connections, state — full \
|
||||||
|
self-knowledge surface",
|
||||||
},
|
},
|
||||||
TierCapability {
|
TierCapability {
|
||||||
id: "adrs",
|
id: "adrs",
|
||||||
|
|
@ -58,7 +59,8 @@ static CAPABILITIES: &[TierCapability] = &[
|
||||||
id: "substrate",
|
id: "substrate",
|
||||||
label: "Substrate (commit layer + state root)",
|
label: "Substrate (commit layer + state root)",
|
||||||
min_tier: Tier::Tier1,
|
min_tier: Tier::Tier1,
|
||||||
description: "Content-addressed ontology commits with state root — enables signed attestations and audit trails (ADR-023)",
|
description: "Content-addressed ontology commits with state root — enables signed \
|
||||||
|
attestations and audit trails (ADR-023)",
|
||||||
},
|
},
|
||||||
TierCapability {
|
TierCapability {
|
||||||
id: "signed-artifacts",
|
id: "signed-artifacts",
|
||||||
|
|
@ -70,25 +72,29 @@ static CAPABILITIES: &[TierCapability] = &[
|
||||||
id: "ops-dispatch",
|
id: "ops-dispatch",
|
||||||
label: "Ops dispatch (typed mutations)",
|
label: "Ops dispatch (typed mutations)",
|
||||||
min_tier: Tier::Tier2,
|
min_tier: Tier::Tier2,
|
||||||
description: "All state mutations route through declared domain operations — the agent action boundary (ADR-024)",
|
description: "All state mutations route through declared domain operations — the agent \
|
||||||
|
action boundary (ADR-024)",
|
||||||
},
|
},
|
||||||
TierCapability {
|
TierCapability {
|
||||||
id: "catalogued-ops",
|
id: "catalogued-ops",
|
||||||
label: "Catalogued operations (ADR-026)",
|
label: "Catalogued operations (ADR-026)",
|
||||||
min_tier: Tier::Tier2,
|
min_tier: Tier::Tier2,
|
||||||
description: "Ops registered in the artifact catalog with validators, preconditions, and SLA declarations",
|
description: "Ops registered in the artifact catalog with validators, preconditions, and \
|
||||||
|
SLA declarations",
|
||||||
},
|
},
|
||||||
TierCapability {
|
TierCapability {
|
||||||
id: "runtime-mutation",
|
id: "runtime-mutation",
|
||||||
label: "Runtime-only state mutation",
|
label: "Runtime-only state mutation",
|
||||||
min_tier: Tier::Tier2,
|
min_tier: Tier::Tier2,
|
||||||
description: "NCL files become render targets; all writes go through the ops runtime — human and AI paths unified",
|
description: "NCL files become render targets; all writes go through the ops runtime — \
|
||||||
|
human and AI paths unified",
|
||||||
},
|
},
|
||||||
TierCapability {
|
TierCapability {
|
||||||
id: "witness-verification",
|
id: "witness-verification",
|
||||||
label: "Witness verification at boundary",
|
label: "Witness verification at boundary",
|
||||||
min_tier: Tier::Tier2,
|
min_tier: Tier::Tier2,
|
||||||
description: "Every op produces a verifiable witness; validators reject ill-typed state without a valid witness chain",
|
description: "Every op produces a verifiable witness; validators reject ill-typed state \
|
||||||
|
without a valid witness chain",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -121,9 +127,8 @@ impl From<&'static TierCapability> for CapabilityRef {
|
||||||
|
|
||||||
/// Returns the capability split for the given tier.
|
/// Returns the capability split for the given tier.
|
||||||
pub fn capability_view(current: Tier) -> CapabilityView {
|
pub fn capability_view(current: Tier) -> CapabilityView {
|
||||||
let (available, locked): (Vec<_>, Vec<_>) = CAPABILITIES
|
let (available, locked): (Vec<_>, Vec<_>) =
|
||||||
.iter()
|
CAPABILITIES.iter().partition(|c| c.min_tier <= current);
|
||||||
.partition(|c| c.min_tier <= current);
|
|
||||||
CapabilityView {
|
CapabilityView {
|
||||||
available: available.into_iter().map(CapabilityRef::from).collect(),
|
available: available.into_iter().map(CapabilityRef::from).collect(),
|
||||||
locked: locked.into_iter().map(CapabilityRef::from).collect(),
|
locked: locked.into_iter().map(CapabilityRef::from).collect(),
|
||||||
|
|
@ -160,9 +165,9 @@ pub fn tier_projection(current: Tier) -> TierProjection {
|
||||||
.filter(|c| c.min_tier == Tier::Tier1)
|
.filter(|c| c.min_tier == Tier::Tier1)
|
||||||
.map(CapabilityRef::from)
|
.map(CapabilityRef::from)
|
||||||
.collect(),
|
.collect(),
|
||||||
"Tier-1 adopts the substrate layer (ADR-023): content-addressed ontology commits, \
|
"Tier-1 adopts the substrate layer (ADR-023): content-addressed ontology commits, a \
|
||||||
a state root, and signed artifacts. Every mutation becomes auditable and \
|
state root, and signed artifacts. Every mutation becomes auditable and reproducible. \
|
||||||
reproducible. Prerequisite for ops-dispatch at Tier-2.",
|
Prerequisite for ops-dispatch at Tier-2.",
|
||||||
),
|
),
|
||||||
Some(Tier::Tier2) => (
|
Some(Tier::Tier2) => (
|
||||||
CAPABILITIES
|
CAPABILITIES
|
||||||
|
|
@ -171,14 +176,22 @@ pub fn tier_projection(current: Tier) -> TierProjection {
|
||||||
.map(CapabilityRef::from)
|
.map(CapabilityRef::from)
|
||||||
.collect(),
|
.collect(),
|
||||||
"Tier-2 adopts the operations layer (ADR-024): all state mutations route through \
|
"Tier-2 adopts the operations layer (ADR-024): all state mutations route through \
|
||||||
declared domain operations. NCL files become render targets. The runtime is the \
|
declared domain operations. NCL files become render targets. The runtime is the only \
|
||||||
only mutation path; witnesses are cryptographically verifiable; validators reject \
|
mutation path; witnesses are cryptographically verifiable; validators reject \
|
||||||
ill-typed state.",
|
ill-typed state.",
|
||||||
),
|
),
|
||||||
None => (vec![], "Already at the highest tier. No further evolution is defined."),
|
None => (
|
||||||
|
vec![],
|
||||||
|
"Already at the highest tier. No further evolution is defined.",
|
||||||
|
),
|
||||||
Some(_) => unreachable!(),
|
Some(_) => unreachable!(),
|
||||||
};
|
};
|
||||||
TierProjection { from: current, to, adds, narrative }
|
TierProjection {
|
||||||
|
from: current,
|
||||||
|
to,
|
||||||
|
adds,
|
||||||
|
narrative,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,10 @@
|
||||||
//! consults the guard, and the function returns one of two outcomes:
|
//! consults the guard, and the function returns one of two outcomes:
|
||||||
//!
|
//!
|
||||||
//! - `ReloadAction::Apply(new)` — caller swaps registry/state in.
|
//! - `ReloadAction::Apply(new)` — caller swaps registry/state in.
|
||||||
//! - `ReloadAction::Block { .. }` — caller logs `Block`-severity
|
//! - `ReloadAction::Block { .. }` — caller logs `Block`-severity notification
|
||||||
//! notification and retains the previous config; the file on disk is
|
//! and retains the previous config; the file on disk is NOT mutated (per
|
||||||
//! NOT mutated (per ADR-033: the daemon only refuses to honour the
|
//! ADR-033: the daemon only refuses to honour the edit, never overwrites the
|
||||||
//! edit, never overwrites the user's NCL).
|
//! user's NCL).
|
||||||
|
|
||||||
use crate::tier::OpsConfig;
|
use crate::tier::OpsConfig;
|
||||||
use crate::tier_guard::{enforce_tier_transition_guard, TierGuardError};
|
use crate::tier_guard::{enforce_tier_transition_guard, TierGuardError};
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
extern crate self as ontoref_ops;
|
extern crate self as ontoref_ops;
|
||||||
|
|
||||||
pub mod capabilities;
|
pub mod capabilities;
|
||||||
|
pub mod config_reload;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
pub mod dispatch;
|
pub mod dispatch;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|
@ -31,7 +32,6 @@ pub mod runtime;
|
||||||
pub mod tier;
|
pub mod tier;
|
||||||
pub mod tier_guard;
|
pub mod tier_guard;
|
||||||
pub mod tier_transition;
|
pub mod tier_transition;
|
||||||
pub mod config_reload;
|
|
||||||
pub mod validation;
|
pub mod validation;
|
||||||
pub mod witness;
|
pub mod witness;
|
||||||
|
|
||||||
|
|
@ -39,6 +39,6 @@ pub use context::OpContext;
|
||||||
pub use dispatch::{dispatch_op, registered_op_ids};
|
pub use dispatch::{dispatch_op, registered_op_ids};
|
||||||
pub use error::OpError;
|
pub use error::OpError;
|
||||||
pub use output::{EffectRecord, OpOutput};
|
pub use output::{EffectRecord, OpOutput};
|
||||||
pub use registry::{OperationEntry, OpFn};
|
pub use registry::{OpFn, OperationEntry};
|
||||||
pub use runtime::{execute_op, PipelineMetrics, PipelineResult};
|
pub use runtime::{execute_op, PipelineMetrics, PipelineResult};
|
||||||
pub use witness::{sign_witness, PipelineWitness};
|
pub use witness::{sign_witness, PipelineWitness};
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,7 @@ use crate::output::{EffectRecord, OpOutput};
|
||||||
witness_shape = "dev_session_close",
|
witness_shape = "dev_session_close",
|
||||||
actor_policy = "developer,admin"
|
actor_policy = "developer,admin"
|
||||||
)]
|
)]
|
||||||
fn close_dev_session(
|
fn close_dev_session(inputs: &serde_json::Value, ctx: &mut OpContext) -> Result<OpOutput, OpError> {
|
||||||
inputs: &serde_json::Value,
|
|
||||||
ctx: &mut OpContext,
|
|
||||||
) -> Result<OpOutput, OpError> {
|
|
||||||
let session_id = inputs
|
let session_id = inputs
|
||||||
.get("session_id")
|
.get("session_id")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
|
|
@ -65,8 +62,8 @@ fn close_dev_session(
|
||||||
);
|
);
|
||||||
ctx.commit_layer.apply(&op);
|
ctx.commit_layer.apply(&op);
|
||||||
|
|
||||||
let witness_payload = canonical::encode(&op)
|
let witness_payload =
|
||||||
.map_err(|e| OpError::body("close_dev_session", e.to_string()))?;
|
canonical::encode(&op).map_err(|e| OpError::body("close_dev_session", e.to_string()))?;
|
||||||
Ok(OpOutput {
|
Ok(OpOutput {
|
||||||
effects: vec![EffectRecord {
|
effects: vec![EffectRecord {
|
||||||
kind: "Update".to_owned(),
|
kind: "Update".to_owned(),
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,12 @@ use crate::context::OpContext;
|
||||||
use crate::error::OpError;
|
use crate::error::OpError;
|
||||||
use crate::output::{EffectRecord, OpOutput};
|
use crate::output::{EffectRecord, OpOutput};
|
||||||
|
|
||||||
/// Inputs: `{ topic: String, engaged_tensions: [String], synthesis_state: String, motion_direction: String }`.
|
/// Inputs: `{ topic: String, engaged_tensions: [String], synthesis_state:
|
||||||
|
/// String, motion_direction: String }`.
|
||||||
#[onto_operation(
|
#[onto_operation(
|
||||||
id = "evaluate_ondaod",
|
id = "evaluate_ondaod",
|
||||||
description = "Record an ondaod (Dao Discipline) evaluation as a meta-decision entity. No state mutation, no render.",
|
description = "Record an ondaod (Dao Discipline) evaluation as a meta-decision entity. No \
|
||||||
|
state mutation, no render.",
|
||||||
validation_sla = "Synchronous",
|
validation_sla = "Synchronous",
|
||||||
precondition = "ondaod",
|
precondition = "ondaod",
|
||||||
pre_validators = "",
|
pre_validators = "",
|
||||||
|
|
@ -27,10 +29,7 @@ use crate::output::{EffectRecord, OpOutput};
|
||||||
witness_shape = "ondaod_evaluation",
|
witness_shape = "ondaod_evaluation",
|
||||||
actor_policy = "developer,admin,agent"
|
actor_policy = "developer,admin,agent"
|
||||||
)]
|
)]
|
||||||
fn evaluate_ondaod(
|
fn evaluate_ondaod(inputs: &serde_json::Value, ctx: &mut OpContext) -> Result<OpOutput, OpError> {
|
||||||
inputs: &serde_json::Value,
|
|
||||||
ctx: &mut OpContext,
|
|
||||||
) -> Result<OpOutput, OpError> {
|
|
||||||
let topic = inputs
|
let topic = inputs
|
||||||
.get("topic")
|
.get("topic")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@ pub const WITNESS_SHAPE: &str = "backlog_item";
|
||||||
/// - `status` (String, optional): `"Open" | "InProgress" | "Closed"`.
|
/// - `status` (String, optional): `"Open" | "InProgress" | "Closed"`.
|
||||||
#[onto_operation(
|
#[onto_operation(
|
||||||
id = "manage_backlog_item",
|
id = "manage_backlog_item",
|
||||||
description = "Add or update one backlog entry in reflection/backlog.ncl. Add asserts a fresh entry; update mutates an existing one in place.",
|
description = "Add or update one backlog entry in reflection/backlog.ncl. Add asserts a fresh \
|
||||||
|
entry; update mutates an existing one in place.",
|
||||||
validation_sla = "Synchronous",
|
validation_sla = "Synchronous",
|
||||||
precondition = "backlog_open_items",
|
precondition = "backlog_open_items",
|
||||||
pre_validators = "backlog_item_well_formed",
|
pre_validators = "backlog_item_well_formed",
|
||||||
|
|
@ -62,10 +63,7 @@ fn manage_backlog_item(
|
||||||
.get_mut("items")
|
.get_mut("items")
|
||||||
.and_then(|v| v.as_array_mut())
|
.and_then(|v| v.as_array_mut())
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
OpError::body(
|
OpError::body("manage_backlog_item", "ctx.backlog.items is not an array")
|
||||||
"manage_backlog_item",
|
|
||||||
"ctx.backlog.items is not an array",
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let id = inputs
|
let id = inputs
|
||||||
|
|
@ -152,7 +150,8 @@ fn manage_backlog_item(
|
||||||
ctx.commit_layer.apply(&op);
|
ctx.commit_layer.apply(&op);
|
||||||
|
|
||||||
if let Some(root) = ctx.project_root.as_ref() {
|
if let Some(root) = ctx.project_root.as_ref() {
|
||||||
let backlog_path = ontoref_ontology::layout::resolve_section(root, "reflection/backlog.ncl");
|
let backlog_path =
|
||||||
|
ontoref_ontology::layout::resolve_section(root, "reflection/backlog.ncl");
|
||||||
if let Some(parent) = backlog_path.parent() {
|
if let Some(parent) = backlog_path.parent() {
|
||||||
std::fs::create_dir_all(parent).map_err(|e| OpError::Render {
|
std::fs::create_dir_all(parent).map_err(|e| OpError::Render {
|
||||||
path: parent.display().to_string(),
|
path: parent.display().to_string(),
|
||||||
|
|
@ -170,8 +169,8 @@ fn manage_backlog_item(
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let witness_payload = canonical::encode(&op)
|
let witness_payload =
|
||||||
.map_err(|e| OpError::body("manage_backlog_item", e.to_string()))?;
|
canonical::encode(&op).map_err(|e| OpError::body("manage_backlog_item", e.to_string()))?;
|
||||||
|
|
||||||
Ok(OpOutput {
|
Ok(OpOutput {
|
||||||
effects: vec![EffectRecord {
|
effects: vec![EffectRecord {
|
||||||
|
|
@ -195,7 +194,8 @@ fn render_backlog_stub(backlog: &serde_json::Value) -> String {
|
||||||
/// Reject malformed backlog requests at the validator boundary.
|
/// Reject malformed backlog requests at the validator boundary.
|
||||||
#[onto_validator(
|
#[onto_validator(
|
||||||
id = "backlog_item_well_formed",
|
id = "backlog_item_well_formed",
|
||||||
description = "Reject manage_backlog_item when action is missing/invalid or required fields are absent.",
|
description = "Reject manage_backlog_item when action is missing/invalid or required fields \
|
||||||
|
are absent.",
|
||||||
category = "Contextual",
|
category = "Contextual",
|
||||||
slice_query = "backlog_open_items"
|
slice_query = "backlog_open_items"
|
||||||
)]
|
)]
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,8 @@ pub const WITNESS_SHAPE: &str = "state_transition";
|
||||||
/// can produce the corresponding `state.ncl` bytes.
|
/// can produce the corresponding `state.ncl` bytes.
|
||||||
#[onto_operation(
|
#[onto_operation(
|
||||||
id = "move_fsm_state",
|
id = "move_fsm_state",
|
||||||
description = "Transition an FSM dimension's current_state. Pre-validates the transition exists and post-validates FSM invariants.",
|
description = "Transition an FSM dimension's current_state. Pre-validates the transition \
|
||||||
|
exists and post-validates FSM invariants.",
|
||||||
validation_sla = "Synchronous",
|
validation_sla = "Synchronous",
|
||||||
precondition = "fsm_dimension_by_id",
|
precondition = "fsm_dimension_by_id",
|
||||||
pre_validators = "fsm_transition_allowed",
|
pre_validators = "fsm_transition_allowed",
|
||||||
|
|
@ -46,10 +47,7 @@ pub const WITNESS_SHAPE: &str = "state_transition";
|
||||||
witness_shape = "state_transition",
|
witness_shape = "state_transition",
|
||||||
actor_policy = "developer,admin,ci"
|
actor_policy = "developer,admin,ci"
|
||||||
)]
|
)]
|
||||||
fn move_fsm_state(
|
fn move_fsm_state(inputs: &serde_json::Value, ctx: &mut OpContext) -> Result<OpOutput, OpError> {
|
||||||
inputs: &serde_json::Value,
|
|
||||||
ctx: &mut OpContext,
|
|
||||||
) -> Result<OpOutput, OpError> {
|
|
||||||
let dim_id = inputs
|
let dim_id = inputs
|
||||||
.get("dimension")
|
.get("dimension")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
|
|
@ -126,7 +124,8 @@ use crate::validation::verdict::Verdict;
|
||||||
/// inputs: <op-inputs> }` (see `runtime::load_fsm_dimension_slice`).
|
/// inputs: <op-inputs> }` (see `runtime::load_fsm_dimension_slice`).
|
||||||
#[onto_validator(
|
#[onto_validator(
|
||||||
id = "fsm_transition_allowed",
|
id = "fsm_transition_allowed",
|
||||||
description = "Reject move_fsm_state when no transition with the requested `to` state exists in the dimension.",
|
description = "Reject move_fsm_state when no transition with the requested `to` state exists \
|
||||||
|
in the dimension.",
|
||||||
category = "Contextual",
|
category = "Contextual",
|
||||||
slice_query = "fsm_dimension_by_id"
|
slice_query = "fsm_dimension_by_id"
|
||||||
)]
|
)]
|
||||||
|
|
@ -135,10 +134,7 @@ fn fsm_transition_allowed(slice: &Slice, _ctx: &ValidationCtx) -> Verdict {
|
||||||
return Verdict::Unknown;
|
return Verdict::Unknown;
|
||||||
};
|
};
|
||||||
let Some(new_state) = inputs.get("new_state").and_then(|v| v.as_str()) else {
|
let Some(new_state) = inputs.get("new_state").and_then(|v| v.as_str()) else {
|
||||||
return Verdict::reject(
|
return Verdict::reject("missing 'new_state' input", "fsm_transition_allowed");
|
||||||
"missing 'new_state' input",
|
|
||||||
"fsm_transition_allowed",
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
let Some(dimension) = slice.get("dimension") else {
|
let Some(dimension) = slice.get("dimension") else {
|
||||||
return Verdict::Unknown;
|
return Verdict::Unknown;
|
||||||
|
|
@ -171,7 +167,8 @@ fn fsm_transition_allowed(slice: &Slice, _ctx: &ValidationCtx) -> Verdict {
|
||||||
/// checks once those land.)
|
/// checks once those land.)
|
||||||
#[onto_validator(
|
#[onto_validator(
|
||||||
id = "fsm_invariants_preserved",
|
id = "fsm_invariants_preserved",
|
||||||
description = "Reject if the dimension's current_state does not appear in any transition after move_fsm_state has run.",
|
description = "Reject if the dimension's current_state does not appear in any transition \
|
||||||
|
after move_fsm_state has run.",
|
||||||
category = "Contextual",
|
category = "Contextual",
|
||||||
slice_query = "fsm_dimension_by_id"
|
slice_query = "fsm_dimension_by_id"
|
||||||
)]
|
)]
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,12 @@ pub const WITNESS_SHAPE: &str = "adr_proposed";
|
||||||
/// - `slug` (String): kebab-case slug for the file name.
|
/// - `slug` (String): kebab-case slug for the file name.
|
||||||
/// - `title` (String).
|
/// - `title` (String).
|
||||||
/// - `decision` (String): the decision text.
|
/// - `decision` (String): the decision text.
|
||||||
/// - `ondaod_evaluation` (Object): `{ engaged_tensions: [...], synthesis_state, motion_direction }`.
|
/// - `ondaod_evaluation` (Object): `{ engaged_tensions: [...], synthesis_state,
|
||||||
|
/// motion_direction }`.
|
||||||
#[onto_operation(
|
#[onto_operation(
|
||||||
id = "propose_adr",
|
id = "propose_adr",
|
||||||
description = "Declare a new ADR in Proposed status. Requires ondaod evaluation as precondition (ADR-024 / dao-discipline).",
|
description = "Declare a new ADR in Proposed status. Requires ondaod evaluation as \
|
||||||
|
precondition (ADR-024 / dao-discipline).",
|
||||||
validation_sla = "Synchronous",
|
validation_sla = "Synchronous",
|
||||||
precondition = "ondaod",
|
precondition = "ondaod",
|
||||||
pre_validators = "ondaod_engagement_declared",
|
pre_validators = "ondaod_engagement_declared",
|
||||||
|
|
@ -146,7 +148,9 @@ fn render_adr_stub(id: &str, title: &str, decision: &str) -> String {
|
||||||
let title_escaped = title.replace('\\', "\\\\").replace('"', "\\\"");
|
let title_escaped = title.replace('\\', "\\\\").replace('"', "\\\"");
|
||||||
let decision_escaped = decision.replace('\\', "\\\\").replace('"', "\\\"");
|
let decision_escaped = decision.replace('\\', "\\\\").replace('"', "\\\"");
|
||||||
format!(
|
format!(
|
||||||
"# Generated by `propose_adr` op (O4). Edit via `transition_adr` once accepted.\n\n{{\n id = \"{id}\",\n title = \"{title_escaped}\",\n status = 'Proposed,\n decision = \"{decision_escaped}\",\n}}\n"
|
"# Generated by `propose_adr` op (O4). Edit via `transition_adr` once accepted.\n\n{{\n \
|
||||||
|
id = \"{id}\",\n title = \"{title_escaped}\",\n status = 'Proposed,\n decision = \
|
||||||
|
\"{decision_escaped}\",\n}}\n"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,7 +159,8 @@ fn render_adr_stub(id: &str, title: &str, decision: &str) -> String {
|
||||||
/// other. Slice shape: see `runtime::load_ondaod_slice`.
|
/// other. Slice shape: see `runtime::load_ondaod_slice`.
|
||||||
#[onto_validator(
|
#[onto_validator(
|
||||||
id = "ondaod_engagement_declared",
|
id = "ondaod_engagement_declared",
|
||||||
description = "Reject propose_adr when the ondaod evaluation declares neither engaged tensions nor an empty-set rationale.",
|
description = "Reject propose_adr when the ondaod evaluation declares neither engaged \
|
||||||
|
tensions nor an empty-set rationale.",
|
||||||
category = "Contextual",
|
category = "Contextual",
|
||||||
slice_query = "ondaod"
|
slice_query = "ondaod"
|
||||||
)]
|
)]
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@ use crate::output::{EffectRecord, OpOutput};
|
||||||
|
|
||||||
#[onto_operation(
|
#[onto_operation(
|
||||||
id = "start_dev_session",
|
id = "start_dev_session",
|
||||||
description = "Open a code dev session with declared scope and target paths. Records session id, scope, and applicable ADRs.",
|
description = "Open a code dev session with declared scope and target paths. Records session \
|
||||||
|
id, scope, and applicable ADRs.",
|
||||||
validation_sla = "Synchronous",
|
validation_sla = "Synchronous",
|
||||||
precondition = "",
|
precondition = "",
|
||||||
pre_validators = "",
|
pre_validators = "",
|
||||||
|
|
@ -25,10 +26,7 @@ use crate::output::{EffectRecord, OpOutput};
|
||||||
witness_shape = "dev_session_start",
|
witness_shape = "dev_session_start",
|
||||||
actor_policy = "developer,admin"
|
actor_policy = "developer,admin"
|
||||||
)]
|
)]
|
||||||
fn start_dev_session(
|
fn start_dev_session(inputs: &serde_json::Value, ctx: &mut OpContext) -> Result<OpOutput, OpError> {
|
||||||
inputs: &serde_json::Value,
|
|
||||||
ctx: &mut OpContext,
|
|
||||||
) -> Result<OpOutput, OpError> {
|
|
||||||
let scope = inputs
|
let scope = inputs
|
||||||
.get("scope")
|
.get("scope")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
|
|
@ -66,8 +64,8 @@ fn start_dev_session(
|
||||||
);
|
);
|
||||||
ctx.commit_layer.apply(&op);
|
ctx.commit_layer.apply(&op);
|
||||||
|
|
||||||
let witness_payload = canonical::encode(&op)
|
let witness_payload =
|
||||||
.map_err(|e| OpError::body("start_dev_session", e.to_string()))?;
|
canonical::encode(&op).map_err(|e| OpError::body("start_dev_session", e.to_string()))?;
|
||||||
Ok(OpOutput {
|
Ok(OpOutput {
|
||||||
effects: vec![EffectRecord {
|
effects: vec![EffectRecord {
|
||||||
kind: "Insert".to_owned(),
|
kind: "Insert".to_owned(),
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ use crate::output::{EffectRecord, OpOutput};
|
||||||
|
|
||||||
#[onto_operation(
|
#[onto_operation(
|
||||||
id = "sync_apply",
|
id = "sync_apply",
|
||||||
description = "Apply a batch of reconciled diffs as a single sync event. Phase 1 records the batch attempt; recursive dispatch arrives later.",
|
description = "Apply a batch of reconciled diffs as a single sync event. Phase 1 records the \
|
||||||
|
batch attempt; recursive dispatch arrives later.",
|
||||||
validation_sla = "Synchronous",
|
validation_sla = "Synchronous",
|
||||||
precondition = "",
|
precondition = "",
|
||||||
pre_validators = "",
|
pre_validators = "",
|
||||||
|
|
@ -38,10 +39,7 @@ fn sync_apply(inputs: &serde_json::Value, ctx: &mut OpContext) -> Result<OpOutpu
|
||||||
.map(|a| a.len())
|
.map(|a| a.len())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
let entity_id = format!(
|
let entity_id = format!("sync_batch:{}", blake3::hash(summary.as_bytes()).to_hex());
|
||||||
"sync_batch:{}",
|
|
||||||
blake3::hash(summary.as_bytes()).to_hex()
|
|
||||||
);
|
|
||||||
let signing_key = ctx.signing_key.clone();
|
let signing_key = ctx.signing_key.clone();
|
||||||
let actor = ontoref_types::PublicKey::from_signing_key(&signing_key);
|
let actor = ontoref_types::PublicKey::from_signing_key(&signing_key);
|
||||||
let timestamp = ctx.next_hlc();
|
let timestamp = ctx.next_hlc();
|
||||||
|
|
@ -51,14 +49,8 @@ fn sync_apply(inputs: &serde_json::Value, ctx: &mut OpContext) -> Result<OpOutpu
|
||||||
parents: vec![],
|
parents: vec![],
|
||||||
payload: OpPayload::EntityAssert {
|
payload: OpPayload::EntityAssert {
|
||||||
attrs: vec![
|
attrs: vec![
|
||||||
(
|
(AttrId::new("diff_count"), Value::Int(diff_count as i64)),
|
||||||
AttrId::new("diff_count"),
|
(AttrId::new("summary"), Value::Str(SmolStr::new(&summary))),
|
||||||
Value::Int(diff_count as i64),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
AttrId::new("summary"),
|
|
||||||
Value::Str(SmolStr::new(&summary)),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
entity: EntityId::new(&entity_id),
|
entity: EntityId::new(&entity_id),
|
||||||
valid_from: None,
|
valid_from: None,
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ use crate::validation::verdict::Verdict;
|
||||||
|
|
||||||
#[onto_operation(
|
#[onto_operation(
|
||||||
id = "transition_adr",
|
id = "transition_adr",
|
||||||
description = "Move an ADR from Proposed to Accepted/Deprecated, or from Accepted to Superseded. Accept requires ondaod.",
|
description = "Move an ADR from Proposed to Accepted/Deprecated, or from Accepted to \
|
||||||
|
Superseded. Accept requires ondaod.",
|
||||||
validation_sla = "Synchronous",
|
validation_sla = "Synchronous",
|
||||||
precondition = "ondaod",
|
precondition = "ondaod",
|
||||||
pre_validators = "adr_transition_allowed",
|
pre_validators = "adr_transition_allowed",
|
||||||
|
|
@ -79,7 +80,8 @@ fn transition_adr(inputs: &serde_json::Value, ctx: &mut OpContext) -> Result<OpO
|
||||||
/// Accept transitions without ondaod engagement.
|
/// Accept transitions without ondaod engagement.
|
||||||
#[onto_validator(
|
#[onto_validator(
|
||||||
id = "adr_transition_allowed",
|
id = "adr_transition_allowed",
|
||||||
description = "Reject unsupported ADR transitions; require ondaod when transitioning to Accepted.",
|
description = "Reject unsupported ADR transitions; require ondaod when transitioning to \
|
||||||
|
Accepted.",
|
||||||
category = "Contextual",
|
category = "Contextual",
|
||||||
slice_query = "ondaod"
|
slice_query = "ondaod"
|
||||||
)]
|
)]
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,9 @@ fn current_ops_from_disk(project_root: &Path) -> OpsConfig {
|
||||||
|
|
||||||
#[onto_operation(
|
#[onto_operation(
|
||||||
id = "transition_tier",
|
id = "transition_tier",
|
||||||
description = "Mutate the project's `ops.tier` declaration. Enforces ADR-029 / ADR-033 guards: refuses when migrations are pending, atomically rewrites .ontoref/config.ncl, emits a tier-transition witness covering the change.",
|
description = "Mutate the project's `ops.tier` declaration. Enforces ADR-029 / ADR-033 \
|
||||||
|
guards: refuses when migrations are pending, atomically rewrites \
|
||||||
|
.ontoref/config.ncl, emits a tier-transition witness covering the change.",
|
||||||
validation_sla = "Synchronous",
|
validation_sla = "Synchronous",
|
||||||
precondition = "",
|
precondition = "",
|
||||||
pre_validators = "",
|
pre_validators = "",
|
||||||
|
|
@ -76,10 +78,7 @@ fn current_ops_from_disk(project_root: &Path) -> OpsConfig {
|
||||||
witness_shape = "tier_transition",
|
witness_shape = "tier_transition",
|
||||||
actor_policy = "developer,admin"
|
actor_policy = "developer,admin"
|
||||||
)]
|
)]
|
||||||
fn transition_tier(
|
fn transition_tier(inputs: &serde_json::Value, ctx: &mut OpContext) -> Result<OpOutput, OpError> {
|
||||||
inputs: &serde_json::Value,
|
|
||||||
ctx: &mut OpContext,
|
|
||||||
) -> Result<OpOutput, OpError> {
|
|
||||||
let target_str = inputs
|
let target_str = inputs
|
||||||
.get("target_tier")
|
.get("target_tier")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
|
|
@ -106,9 +105,8 @@ fn transition_tier(
|
||||||
descent_signing_key: Some(ctx.signing_key.clone()),
|
descent_signing_key: Some(ctx.signing_key.clone()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let outcome = execute(target, transition_ctx).map_err(|e| {
|
let outcome = execute(target, transition_ctx)
|
||||||
OpError::body("transition_tier", format!("tier transition failed: {e}"))
|
.map_err(|e| OpError::body("transition_tier", format!("tier transition failed: {e}")))?;
|
||||||
})?;
|
|
||||||
|
|
||||||
let entity_id = "project_ops:state".to_owned();
|
let entity_id = "project_ops:state".to_owned();
|
||||||
let signing_key = ctx.signing_key.clone();
|
let signing_key = ctx.signing_key.clone();
|
||||||
|
|
@ -142,8 +140,8 @@ fn transition_tier(
|
||||||
);
|
);
|
||||||
ctx.commit_layer.apply(&op);
|
ctx.commit_layer.apply(&op);
|
||||||
|
|
||||||
let witness_payload = canonical::encode(&op)
|
let witness_payload =
|
||||||
.map_err(|e| OpError::body("transition_tier", e.to_string()))?;
|
canonical::encode(&op).map_err(|e| OpError::body("transition_tier", e.to_string()))?;
|
||||||
|
|
||||||
Ok(OpOutput {
|
Ok(OpOutput {
|
||||||
effects: vec![EffectRecord {
|
effects: vec![EffectRecord {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ use crate::validation::verdict::Verdict;
|
||||||
|
|
||||||
#[onto_operation(
|
#[onto_operation(
|
||||||
id = "update_ontology_edge",
|
id = "update_ontology_edge",
|
||||||
description = "Insert one edge into core.ncl edges array. Pre-validator rejects direct self-loops and 1-step cycles.",
|
description = "Insert one edge into core.ncl edges array. Pre-validator rejects direct \
|
||||||
|
self-loops and 1-step cycles.",
|
||||||
validation_sla = "Synchronous",
|
validation_sla = "Synchronous",
|
||||||
precondition = "ontology_edge_dag_check",
|
precondition = "ontology_edge_dag_check",
|
||||||
pre_validators = "ontology_edge_no_cycle",
|
pre_validators = "ontology_edge_no_cycle",
|
||||||
|
|
@ -105,10 +106,7 @@ fn ontology_edge_no_cycle(slice: &Slice, _ctx: &ValidationCtx) -> Verdict {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if from == to {
|
if from == to {
|
||||||
return Verdict::reject(
|
return Verdict::reject("self-loop rejected (from == to)", "ontology_edge_no_cycle");
|
||||||
"self-loop rejected (from == to)",
|
|
||||||
"ontology_edge_no_cycle",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
let reverse_exists = slice
|
let reverse_exists = slice
|
||||||
.get("existing_reverse")
|
.get("existing_reverse")
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,8 @@ pub const WITNESS_SHAPE: &str = "node_upsert";
|
||||||
/// - `invariant` (Bool, optional).
|
/// - `invariant` (Bool, optional).
|
||||||
#[onto_operation(
|
#[onto_operation(
|
||||||
id = "update_ontology_node",
|
id = "update_ontology_node",
|
||||||
description = "Insert or update one node in .ontology/core.ncl. Mutates ctx.core in place and writes the file back via Phase 1 inline render.",
|
description = "Insert or update one node in .ontology/core.ncl. Mutates ctx.core in place and \
|
||||||
|
writes the file back via Phase 1 inline render.",
|
||||||
validation_sla = "Synchronous",
|
validation_sla = "Synchronous",
|
||||||
precondition = "ontology_node_by_id",
|
precondition = "ontology_node_by_id",
|
||||||
pre_validators = "ontology_node_well_formed",
|
pre_validators = "ontology_node_well_formed",
|
||||||
|
|
@ -150,10 +151,14 @@ fn update_ontology_node(
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let witness_payload = canonical::encode(&op)
|
let witness_payload =
|
||||||
.map_err(|e| OpError::body("update_ontology_node", e.to_string()))?;
|
canonical::encode(&op).map_err(|e| OpError::body("update_ontology_node", e.to_string()))?;
|
||||||
|
|
||||||
let effect_kind = if existing_idx.is_some() { "Update" } else { "Insert" };
|
let effect_kind = if existing_idx.is_some() {
|
||||||
|
"Update"
|
||||||
|
} else {
|
||||||
|
"Insert"
|
||||||
|
};
|
||||||
Ok(OpOutput {
|
Ok(OpOutput {
|
||||||
effects: vec![EffectRecord {
|
effects: vec![EffectRecord {
|
||||||
kind: effect_kind.to_owned(),
|
kind: effect_kind.to_owned(),
|
||||||
|
|
@ -180,7 +185,8 @@ fn render_core_stub(core: &serde_json::Value) -> String {
|
||||||
/// Reject when the proposed node is missing required identity fields.
|
/// Reject when the proposed node is missing required identity fields.
|
||||||
#[onto_validator(
|
#[onto_validator(
|
||||||
id = "ontology_node_well_formed",
|
id = "ontology_node_well_formed",
|
||||||
description = "Reject update_ontology_node when the proposed node lacks id, or when both name and description are missing on an insert.",
|
description = "Reject update_ontology_node when the proposed node lacks id, or when both name \
|
||||||
|
and description are missing on an insert.",
|
||||||
category = "Contextual",
|
category = "Contextual",
|
||||||
slice_query = "ontology_node_by_id"
|
slice_query = "ontology_node_by_id"
|
||||||
)]
|
)]
|
||||||
|
|
@ -190,10 +196,7 @@ fn ontology_node_well_formed(slice: &Slice, _ctx: &ValidationCtx) -> Verdict {
|
||||||
};
|
};
|
||||||
let node_id = inputs.get("node_id").and_then(|v| v.as_str()).unwrap_or("");
|
let node_id = inputs.get("node_id").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
if node_id.is_empty() {
|
if node_id.is_empty() {
|
||||||
return Verdict::reject(
|
return Verdict::reject("missing 'node_id' input", "ontology_node_well_formed");
|
||||||
"missing 'node_id' input",
|
|
||||||
"ontology_node_well_formed",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
let is_insert = slice
|
let is_insert = slice
|
||||||
.get("existing_node")
|
.get("existing_node")
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,12 @@
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
// NOTE: this loader intentionally does NOT use `ontoref_ontology::Core::from_value`
|
// NOTE: this loader intentionally does NOT use
|
||||||
// because that constructor parses the `edges` array, and the existing
|
// `ontoref_ontology::Core::from_value` because that constructor parses the
|
||||||
// `Edge.weight: f64` Rust type does not match the real `.ontology/core.ncl`
|
// `edges` array, and the existing `Edge.weight: f64` Rust type does not match
|
||||||
// edges (which carry `weight = 'High'` enum tags). Fixing the Edge weight type
|
// the real `.ontology/core.ncl` edges (which carry `weight = 'High'` enum
|
||||||
// is tracked separately; O0 reads only the `nodes` array which is unaffected.
|
// tags). Fixing the Edge weight type is tracked separately; O0 reads only the
|
||||||
|
// `nodes` array which is unaffected.
|
||||||
|
|
||||||
/// Canonical Q&A entry id for the protocol baseline ondaod procedure.
|
/// Canonical Q&A entry id for the protocol baseline ondaod procedure.
|
||||||
pub const ONDAOD_QA_ID: &str = "ontoref-dao-discipline";
|
pub const ONDAOD_QA_ID: &str = "ontoref-dao-discipline";
|
||||||
|
|
@ -157,8 +158,8 @@ pub fn load(
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let qa: QaStore = serde_json::from_value(qa_json.clone())
|
let qa: QaStore =
|
||||||
.map_err(|e| OndaodError::Qa(anyhow::anyhow!(e)))?;
|
serde_json::from_value(qa_json.clone()).map_err(|e| OndaodError::Qa(anyhow::anyhow!(e)))?;
|
||||||
|
|
||||||
let entry = qa
|
let entry = qa
|
||||||
.entries
|
.entries
|
||||||
|
|
@ -182,10 +183,10 @@ pub fn load(
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// - [`OndaodError::UnknownTension`] when any `tension_id` in `engaged` is
|
/// - [`OndaodError::UnknownTension`] when any `tension_id` in `engaged` is not
|
||||||
/// not present in `context.tensions`. The evaluation rejects entirely;
|
/// present in `context.tensions`. The evaluation rejects entirely; partial
|
||||||
/// partial application is not supported because the discipline requires
|
/// application is not supported because the discipline requires the
|
||||||
/// the engagement set to be coherent with the loaded tensions snapshot.
|
/// engagement set to be coherent with the loaded tensions snapshot.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
|
|
@ -263,10 +264,11 @@ struct NodeRow {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
fn workspace_root() -> PathBuf {
|
fn workspace_root() -> PathBuf {
|
||||||
// CARGO_MANIFEST_DIR = crates/ontoref-ops; parent twice = workspace root.
|
// CARGO_MANIFEST_DIR = crates/ontoref-ops; parent twice = workspace root.
|
||||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
|
@ -296,8 +298,14 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn load_discipline_and_evaluate() {
|
fn load_discipline_and_evaluate() {
|
||||||
let root = workspace_root();
|
let root = workspace_root();
|
||||||
let core_json = nickel_export(&ontoref_ontology::layout::resolve_section(&root, "ontology/core.ncl"));
|
let core_json = nickel_export(&ontoref_ontology::layout::resolve_section(
|
||||||
let qa_json = nickel_export(&ontoref_ontology::layout::resolve_section(&root, "reflection/qa.ncl"));
|
&root,
|
||||||
|
"ontology/core.ncl",
|
||||||
|
));
|
||||||
|
let qa_json = nickel_export(&ontoref_ontology::layout::resolve_section(
|
||||||
|
&root,
|
||||||
|
"reflection/qa.ncl",
|
||||||
|
));
|
||||||
|
|
||||||
let context = load(&core_json, &qa_json).expect("load ondaod context");
|
let context = load(&core_json, &qa_json).expect("load ondaod context");
|
||||||
|
|
||||||
|
|
@ -339,8 +347,14 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_engagement_is_explicit() {
|
fn empty_engagement_is_explicit() {
|
||||||
let root = workspace_root();
|
let root = workspace_root();
|
||||||
let core_json = nickel_export(&ontoref_ontology::layout::resolve_section(&root, "ontology/core.ncl"));
|
let core_json = nickel_export(&ontoref_ontology::layout::resolve_section(
|
||||||
let qa_json = nickel_export(&ontoref_ontology::layout::resolve_section(&root, "reflection/qa.ncl"));
|
&root,
|
||||||
|
"ontology/core.ncl",
|
||||||
|
));
|
||||||
|
let qa_json = nickel_export(&ontoref_ontology::layout::resolve_section(
|
||||||
|
&root,
|
||||||
|
"reflection/qa.ncl",
|
||||||
|
));
|
||||||
|
|
||||||
let context = load(&core_json, &qa_json).expect("load ondaod context");
|
let context = load(&core_json, &qa_json).expect("load ondaod context");
|
||||||
let evaluation = evaluate(context, vec![], MotionDirection::Static)
|
let evaluation = evaluate(context, vec![], MotionDirection::Static)
|
||||||
|
|
@ -353,8 +367,14 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn unknown_tension_rejected() {
|
fn unknown_tension_rejected() {
|
||||||
let root = workspace_root();
|
let root = workspace_root();
|
||||||
let core_json = nickel_export(&ontoref_ontology::layout::resolve_section(&root, "ontology/core.ncl"));
|
let core_json = nickel_export(&ontoref_ontology::layout::resolve_section(
|
||||||
let qa_json = nickel_export(&ontoref_ontology::layout::resolve_section(&root, "reflection/qa.ncl"));
|
&root,
|
||||||
|
"ontology/core.ncl",
|
||||||
|
));
|
||||||
|
let qa_json = nickel_export(&ontoref_ontology::layout::resolve_section(
|
||||||
|
&root,
|
||||||
|
"reflection/qa.ncl",
|
||||||
|
));
|
||||||
|
|
||||||
let context = load(&core_json, &qa_json).expect("load ondaod context");
|
let context = load(&core_json, &qa_json).expect("load ondaod context");
|
||||||
let err = evaluate(
|
let err = evaluate(
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,10 @@ pub fn diff_state(old: &StateConfig, new: &StateConfig) -> Vec<StateDiff> {
|
||||||
diffs.push(StateDiff {
|
diffs.push(StateDiff {
|
||||||
op_id: String::new(),
|
op_id: String::new(),
|
||||||
inputs: serde_json::json!({ "dimension_added": new_dim.id }),
|
inputs: serde_json::json!({ "dimension_added": new_dim.id }),
|
||||||
summary: format!("dimension '{}' added — no catalogued op covers this", new_dim.id),
|
summary: format!(
|
||||||
|
"dimension '{}' added — no catalogued op covers this",
|
||||||
|
new_dim.id
|
||||||
|
),
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
@ -88,7 +91,8 @@ pub fn diff_state(old: &StateConfig, new: &StateConfig) -> Vec<StateDiff> {
|
||||||
"new_desired": new_dim.desired_state,
|
"new_desired": new_dim.desired_state,
|
||||||
}),
|
}),
|
||||||
summary: format!(
|
summary: format!(
|
||||||
"dimension '{}' desired_state: '{}' → '{}' — no catalogued op covers this in Phase 1",
|
"dimension '{}' desired_state: '{}' → '{}' — no catalogued op covers this in \
|
||||||
|
Phase 1",
|
||||||
new_dim.id, old_dim.desired_state, new_dim.desired_state
|
new_dim.id, old_dim.desired_state, new_dim.desired_state
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
@ -117,10 +121,10 @@ pub fn diff_state(old: &StateConfig, new: &StateConfig) -> Vec<StateDiff> {
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// - [`ReconcileError::UncatalogedDiff`] when any diff carries an empty
|
/// - [`ReconcileError::UncatalogedDiff`] when any diff carries an empty `op_id`
|
||||||
/// `op_id` (no registered op covers the edit).
|
/// (no registered op covers the edit).
|
||||||
/// - [`ReconcileError::Dispatch`] for runtime errors propagated from
|
/// - [`ReconcileError::Dispatch`] for runtime errors propagated from the
|
||||||
/// the underlying [`dispatch_op`].
|
/// underlying [`dispatch_op`].
|
||||||
pub fn apply_diffs(
|
pub fn apply_diffs(
|
||||||
diffs: &[StateDiff],
|
diffs: &[StateDiff],
|
||||||
ctx: &mut OpContext,
|
ctx: &mut OpContext,
|
||||||
|
|
@ -156,11 +160,12 @@ pub fn apply_diffs(
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
|
||||||
use ontoref_ontology::types::{
|
use ontoref_ontology::types::{
|
||||||
Dimension, DimensionState, Horizon, StateConfig, TensionLevel, Transition,
|
Dimension, DimensionState, Horizon, StateConfig, TensionLevel, Transition,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
fn dim(id: &str, current: &str) -> Dimension {
|
fn dim(id: &str, current: &str) -> Dimension {
|
||||||
Dimension {
|
Dimension {
|
||||||
id: id.to_owned(),
|
id: id.to_owned(),
|
||||||
|
|
@ -247,8 +252,12 @@ mod tests {
|
||||||
a.desired_state = "z".to_owned();
|
a.desired_state = "z".to_owned();
|
||||||
let mut b = dim("d1", "a");
|
let mut b = dim("d1", "a");
|
||||||
b.desired_state = "q".to_owned();
|
b.desired_state = "q".to_owned();
|
||||||
let s1 = StateConfig { dimensions: vec![a] };
|
let s1 = StateConfig {
|
||||||
let s2 = StateConfig { dimensions: vec![b] };
|
dimensions: vec![a],
|
||||||
|
};
|
||||||
|
let s2 = StateConfig {
|
||||||
|
dimensions: vec![b],
|
||||||
|
};
|
||||||
let diffs = diff_state(&s1, &s2);
|
let diffs = diff_state(&s1, &s2);
|
||||||
assert_eq!(diffs.len(), 1);
|
assert_eq!(diffs.len(), 1);
|
||||||
assert!(diffs[0].op_id.is_empty());
|
assert!(diffs[0].op_id.is_empty());
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
//! Pipeline executor — `load → pre_validate → exec → apply → render → post_validate → emit_witness`.
|
//! Pipeline executor — `load → pre_validate → exec → apply → render →
|
||||||
|
//! post_validate → emit_witness`.
|
||||||
//!
|
//!
|
||||||
//! Phase 1 wires the skeleton: every step is instrumented with
|
//! Phase 1 wires the skeleton: every step is instrumented with
|
||||||
//! microsecond-resolution metrics (ADR-026 SLA observability). Concrete
|
//! microsecond-resolution metrics (ADR-026 SLA observability). Concrete
|
||||||
|
|
@ -74,9 +75,9 @@ pub fn execute_op(
|
||||||
// reference it without an extra parameter.
|
// reference it without an extra parameter.
|
||||||
ctx.op_id = entry.id.to_owned();
|
ctx.op_id = entry.id.to_owned();
|
||||||
|
|
||||||
// 1. Precondition load — dispatched by entry.precondition tag.
|
// 1. Precondition load — dispatched by entry.precondition tag. Bundles the
|
||||||
// Bundles the loaded slice with the op's inputs so validators
|
// loaded slice with the op's inputs so validators see both without an extra
|
||||||
// see both without an extra parameter.
|
// parameter.
|
||||||
let t0 = Instant::now();
|
let t0 = Instant::now();
|
||||||
let slice = load_precondition(entry, inputs, ctx)?;
|
let slice = load_precondition(entry, inputs, ctx)?;
|
||||||
metrics.precondition_load_us = elapsed_us(t0);
|
metrics.precondition_load_us = elapsed_us(t0);
|
||||||
|
|
@ -98,8 +99,8 @@ pub fn execute_op(
|
||||||
apply_effects(entry, &output, ctx);
|
apply_effects(entry, &output, ctx);
|
||||||
metrics.apply_us = elapsed_us(t0);
|
metrics.apply_us = elapsed_us(t0);
|
||||||
|
|
||||||
// 5. Render NCL paths — Phase 1 supports state.ncl when
|
// 5. Render NCL paths — Phase 1 supports state.ncl when ctx.state +
|
||||||
// ctx.state + ctx.project_root are set.
|
// ctx.project_root are set.
|
||||||
let t0 = Instant::now();
|
let t0 = Instant::now();
|
||||||
render_paths(entry, ctx)?;
|
render_paths(entry, ctx)?;
|
||||||
metrics.render_us = elapsed_us(t0);
|
metrics.render_us = elapsed_us(t0);
|
||||||
|
|
@ -107,7 +108,12 @@ pub fn execute_op(
|
||||||
// 6. Post-validators — re-load the slice (state has mutated) and run.
|
// 6. Post-validators — re-load the slice (state has mutated) and run.
|
||||||
let t0 = Instant::now();
|
let t0 = Instant::now();
|
||||||
let post_slice = load_precondition(entry, inputs, ctx)?;
|
let post_slice = load_precondition(entry, inputs, ctx)?;
|
||||||
run_validators(entry.id, entry.post_validators, &post_slice, &validation_ctx)?;
|
run_validators(
|
||||||
|
entry.id,
|
||||||
|
entry.post_validators,
|
||||||
|
&post_slice,
|
||||||
|
&validation_ctx,
|
||||||
|
)?;
|
||||||
metrics.post_validate_us = elapsed_us(t0);
|
metrics.post_validate_us = elapsed_us(t0);
|
||||||
|
|
||||||
// 7. Emit witness.
|
// 7. Emit witness.
|
||||||
|
|
@ -122,12 +128,11 @@ pub fn execute_op(
|
||||||
);
|
);
|
||||||
metrics.witness_us = elapsed_us(t0);
|
metrics.witness_us = elapsed_us(t0);
|
||||||
|
|
||||||
// 8. Persist the witness to the oplog as the final stage (substrate
|
// 8. Persist the witness to the oplog as the final stage (substrate feature).
|
||||||
// feature). Append-first semantics: the oplog is the source of
|
// Append-first semantics: the oplog is the source of truth, the rendered NCL
|
||||||
// truth, the rendered NCL is a projection. Done here, after the
|
// is a projection. Done here, after the in-memory render, the only
|
||||||
// in-memory render, the only divergence window is render-ok /
|
// divergence window is render-ok / append-fail, surfaced as
|
||||||
// append-fail, surfaced as OpError::WitnessPersist for the caller
|
// OpError::WitnessPersist for the caller to reconcile.
|
||||||
// to reconcile.
|
|
||||||
#[cfg(feature = "substrate")]
|
#[cfg(feature = "substrate")]
|
||||||
let oplog_op_id = persist_witness(&witness, ctx)?;
|
let oplog_op_id = persist_witness(&witness, ctx)?;
|
||||||
|
|
||||||
|
|
@ -256,9 +261,10 @@ fn load_ondaod_slice(
|
||||||
// crate::precondition::ondaod::load arrives when the runtime takes
|
// crate::precondition::ondaod::load arrives when the runtime takes
|
||||||
// ownership of the qa.ncl + core.ncl JSON snapshots.
|
// ownership of the qa.ncl + core.ncl JSON snapshots.
|
||||||
let mut slice = serde_json::Map::new();
|
let mut slice = serde_json::Map::new();
|
||||||
let ondaod = inputs.get("ondaod_evaluation").cloned().unwrap_or_else(|| {
|
let ondaod = inputs
|
||||||
serde_json::json!({ "engaged_tensions": [], "synthesis_state": "" })
|
.get("ondaod_evaluation")
|
||||||
});
|
.cloned()
|
||||||
|
.unwrap_or_else(|| serde_json::json!({ "engaged_tensions": [], "synthesis_state": "" }));
|
||||||
slice.insert("ondaod".to_owned(), ondaod);
|
slice.insert("ondaod".to_owned(), ondaod);
|
||||||
slice.insert("inputs".to_owned(), inputs.clone());
|
slice.insert("inputs".to_owned(), inputs.clone());
|
||||||
slice.insert(
|
slice.insert(
|
||||||
|
|
@ -318,20 +324,22 @@ fn load_ontology_edge_dag_check_slice(
|
||||||
inputs: &serde_json::Value,
|
inputs: &serde_json::Value,
|
||||||
ctx: &OpContext,
|
ctx: &OpContext,
|
||||||
) -> Result<serde_json::Value, OpError> {
|
) -> Result<serde_json::Value, OpError> {
|
||||||
let from = inputs
|
let from =
|
||||||
.get("from")
|
inputs
|
||||||
.and_then(|v| v.as_str())
|
.get("from")
|
||||||
.ok_or_else(|| OpError::PreconditionLoad {
|
.and_then(|v| v.as_str())
|
||||||
op_id: entry.id.to_owned(),
|
.ok_or_else(|| OpError::PreconditionLoad {
|
||||||
reason: "missing 'from' input".to_owned(),
|
op_id: entry.id.to_owned(),
|
||||||
})?;
|
reason: "missing 'from' input".to_owned(),
|
||||||
let to = inputs
|
})?;
|
||||||
.get("to")
|
let to =
|
||||||
.and_then(|v| v.as_str())
|
inputs
|
||||||
.ok_or_else(|| OpError::PreconditionLoad {
|
.get("to")
|
||||||
op_id: entry.id.to_owned(),
|
.and_then(|v| v.as_str())
|
||||||
reason: "missing 'to' input".to_owned(),
|
.ok_or_else(|| OpError::PreconditionLoad {
|
||||||
})?;
|
op_id: entry.id.to_owned(),
|
||||||
|
reason: "missing 'to' input".to_owned(),
|
||||||
|
})?;
|
||||||
// The reverse edge (to → from) is what would close a 1-step cycle.
|
// The reverse edge (to → from) is what would close a 1-step cycle.
|
||||||
let existing_reverse = ctx
|
let existing_reverse = ctx
|
||||||
.core
|
.core
|
||||||
|
|
@ -370,7 +378,8 @@ fn load_fsm_dimension_slice(
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| OpError::PreconditionLoad {
|
.ok_or_else(|| OpError::PreconditionLoad {
|
||||||
op_id: entry.id.to_owned(),
|
op_id: entry.id.to_owned(),
|
||||||
reason: "ctx.state is None — fsm_dimension_by_id requires a loaded StateConfig".to_owned(),
|
reason: "ctx.state is None — fsm_dimension_by_id requires a loaded StateConfig"
|
||||||
|
.to_owned(),
|
||||||
})?;
|
})?;
|
||||||
let dimension = state
|
let dimension = state
|
||||||
.dimensions
|
.dimensions
|
||||||
|
|
@ -483,7 +492,8 @@ fn pipeline() {
|
||||||
// paths. Exercises every pipeline step in skeleton form.
|
// paths. Exercises every pipeline step in skeleton form.
|
||||||
#[onto_operation(
|
#[onto_operation(
|
||||||
id = "noop_pipeline_smoke",
|
id = "noop_pipeline_smoke",
|
||||||
description = "Noop op used by the O2 runtime smoke test — no precondition, no validators, no effects, no render paths.",
|
description = "Noop op used by the O2 runtime smoke test — no precondition, no \
|
||||||
|
validators, no effects, no render paths.",
|
||||||
validation_sla = "Synchronous",
|
validation_sla = "Synchronous",
|
||||||
witness_shape = "noop"
|
witness_shape = "noop"
|
||||||
)]
|
)]
|
||||||
|
|
@ -543,7 +553,8 @@ fn pipeline_rejects_disallowed_actor() {
|
||||||
/// Restricted op for actor-policy negative coverage.
|
/// Restricted op for actor-policy negative coverage.
|
||||||
#[onto_operation(
|
#[onto_operation(
|
||||||
id = "restricted_op_smoke",
|
id = "restricted_op_smoke",
|
||||||
description = "Restricted to the admin role — used by O2 to verify actor policy enforcement.",
|
description = "Restricted to the admin role — used by O2 to verify actor policy \
|
||||||
|
enforcement.",
|
||||||
validation_sla = "Synchronous",
|
validation_sla = "Synchronous",
|
||||||
actor_policy = "admin",
|
actor_policy = "admin",
|
||||||
witness_shape = "noop"
|
witness_shape = "noop"
|
||||||
|
|
@ -579,7 +590,8 @@ fn witness_persists_to_oplog_and_verifies_in_fresh_process() {
|
||||||
/// Noop op exercising the full pipeline including witness persistence.
|
/// Noop op exercising the full pipeline including witness persistence.
|
||||||
#[onto_operation(
|
#[onto_operation(
|
||||||
id = "substrate_persist_smoke",
|
id = "substrate_persist_smoke",
|
||||||
description = "Noop op used by the substrate witness-persistence test — dispatches, signs, and appends to the oplog.",
|
description = "Noop op used by the substrate witness-persistence test — dispatches, \
|
||||||
|
signs, and appends to the oplog.",
|
||||||
validation_sla = "Synchronous",
|
validation_sla = "Synchronous",
|
||||||
witness_shape = "noop"
|
witness_shape = "noop"
|
||||||
)]
|
)]
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,8 @@ pub enum TierGuardError {
|
||||||
/// Callers MUST refuse to apply the new config until the migration
|
/// Callers MUST refuse to apply the new config until the migration
|
||||||
/// baseline is clean.
|
/// baseline is clean.
|
||||||
#[error(
|
#[error(
|
||||||
"tier transition {old_tier:?} -> {new_tier:?} blocked: {pending} migration(s) pending: {ids:?}"
|
"tier transition {old_tier:?} -> {new_tier:?} blocked: {pending} migration(s) pending: \
|
||||||
|
{ids:?}"
|
||||||
)]
|
)]
|
||||||
MigrationsPending {
|
MigrationsPending {
|
||||||
/// Tier value declared by the OLD config (or default if absent).
|
/// Tier value declared by the OLD config (or default if absent).
|
||||||
|
|
@ -54,8 +55,8 @@ pub enum TierGuardError {
|
||||||
/// Note: the function does NOT check `ops.phase` — Phase transitions
|
/// Note: the function does NOT check `ops.phase` — Phase transitions
|
||||||
/// (`Progressive` → `Pure` within tier-2) are governed by a separate
|
/// (`Progressive` → `Pure` within tier-2) are governed by a separate
|
||||||
/// constraint (future ADR-033). This guard is scoped to tier transitions
|
/// constraint (future ADR-033). This guard is scoped to tier transitions
|
||||||
/// only, matching the ADR-029 / `tier-transition-requires-clean-migration-state`
|
/// only, matching the ADR-029 /
|
||||||
/// constraint precisely.
|
/// `tier-transition-requires-clean-migration-state` constraint precisely.
|
||||||
pub fn enforce_tier_transition_guard(
|
pub fn enforce_tier_transition_guard(
|
||||||
old: &OpsConfig,
|
old: &OpsConfig,
|
||||||
new: &OpsConfig,
|
new: &OpsConfig,
|
||||||
|
|
|
||||||
|
|
@ -23,17 +23,17 @@
|
||||||
//! - `transition-tier-uses-existing-guard` — [`execute`] invokes
|
//! - `transition-tier-uses-existing-guard` — [`execute`] invokes
|
||||||
//! [`crate::tier_guard::enforce_tier_transition_guard`] as its first
|
//! [`crate::tier_guard::enforce_tier_transition_guard`] as its first
|
||||||
//! validator before any effect runs.
|
//! validator before any effect runs.
|
||||||
//! - `transition-tier-single-rust-function` — this is the single entry
|
//! - `transition-tier-single-rust-function` — this is the single entry that
|
||||||
//! that both the CLI surface and the future catalogued op must call.
|
//! both the CLI surface and the future catalogued op must call.
|
||||||
//! - `transition-tier-transactional-or-noop` — file replace happens via
|
//! - `transition-tier-transactional-or-noop` — file replace happens via
|
||||||
//! tempfile + atomic rename. On any pre-rewrite failure the original
|
//! tempfile + atomic rename. On any pre-rewrite failure the original
|
||||||
//! `.ontoref/config.ncl` is untouched. The atomicity tests in this
|
//! `.ontoref/config.ncl` is untouched. The atomicity tests in this module pin
|
||||||
//! module pin the property.
|
//! the property.
|
||||||
//!
|
//!
|
||||||
//! Hard constraints DEFERRED to substrate integration:
|
//! Hard constraints DEFERRED to substrate integration:
|
||||||
//!
|
//!
|
||||||
//! - `downgrade-preserves-substrate-immutable` — no substrate to preserve
|
//! - `downgrade-preserves-substrate-immutable` — no substrate to preserve yet
|
||||||
//! yet in this module. Will be verified when oplog/keys effects land.
|
//! in this module. Will be verified when oplog/keys effects land.
|
||||||
//! - `tier-2-descent-emits-final-witness` — witness emission requires the
|
//! - `tier-2-descent-emits-final-witness` — witness emission requires the
|
||||||
//! keypair surface and the oplog writer; deferred.
|
//! keypair surface and the oplog writer; deferred.
|
||||||
|
|
||||||
|
|
@ -42,10 +42,9 @@ use std::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use tempfile::NamedTempFile;
|
|
||||||
|
|
||||||
#[cfg(feature = "substrate")]
|
#[cfg(feature = "substrate")]
|
||||||
use ed25519_dalek::SigningKey;
|
use ed25519_dalek::SigningKey;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
use crate::tier::{OpsConfig, Phase, Tier};
|
use crate::tier::{OpsConfig, Phase, Tier};
|
||||||
use crate::tier_guard::{enforce_tier_transition_guard, TierGuardError};
|
use crate::tier_guard::{enforce_tier_transition_guard, TierGuardError};
|
||||||
|
|
@ -155,14 +154,15 @@ fn rewrite_config_tier(config_path: &Path, target: Tier) -> Result<(), Transitio
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let target_atom = format!("'{target:?}");
|
let target_atom = format!("'{target:?}");
|
||||||
let rewritten =
|
let rewritten = replace_tier_value(&original, &target_atom).ok_or_else(|| {
|
||||||
replace_tier_value(&original, &target_atom).ok_or_else(|| TransitionError::OpsBlockAbsent {
|
TransitionError::OpsBlockAbsent {
|
||||||
path: config_path.to_path_buf(),
|
path: config_path.to_path_buf(),
|
||||||
})?;
|
}
|
||||||
|
|
||||||
let parent = config_path.parent().ok_or_else(|| {
|
|
||||||
TransitionError::Io(std::io::Error::other("config path has no parent"))
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let parent = config_path
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| TransitionError::Io(std::io::Error::other("config path has no parent")))?;
|
||||||
let mut tmp = NamedTempFile::new_in(parent)?;
|
let mut tmp = NamedTempFile::new_in(parent)?;
|
||||||
tmp.write_all(rewritten.as_bytes())?;
|
tmp.write_all(rewritten.as_bytes())?;
|
||||||
tmp.flush()?;
|
tmp.flush()?;
|
||||||
|
|
@ -252,20 +252,19 @@ fn now_unix_ms() -> u128 {
|
||||||
/// keys, no secret values). Only the protocol-level fields named by the
|
/// keys, no secret values). Only the protocol-level fields named by the
|
||||||
/// constraint: requested target tier, original tier, failure source,
|
/// constraint: requested target tier, original tier, failure source,
|
||||||
/// timestamp.
|
/// timestamp.
|
||||||
fn log_failed_transition(
|
fn log_failed_transition(project_root: &Path, requested: Tier, original: Tier, source: &str) {
|
||||||
project_root: &Path,
|
let log_dir = project_root
|
||||||
requested: Tier,
|
.join(".ontoref")
|
||||||
original: Tier,
|
.join("artifacts")
|
||||||
source: &str,
|
.join("transition-log");
|
||||||
) {
|
|
||||||
let log_dir = project_root.join(".ontoref").join("artifacts").join("transition-log");
|
|
||||||
if fs::create_dir_all(&log_dir).is_err() {
|
if fs::create_dir_all(&log_dir).is_err() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let ts = now_unix_ms();
|
let ts = now_unix_ms();
|
||||||
let log_path = log_dir.join(format!("{ts}-failed.ncl"));
|
let log_path = log_dir.join(format!("{ts}-failed.ncl"));
|
||||||
let body = format!(
|
let body = format!(
|
||||||
"{{\n timestamp = {ts},\n requested_tier = \"{requested:?}\",\n original_tier = \"{original:?}\",\n source = \"{source}\",\n}}\n"
|
"{{\n timestamp = {ts},\n requested_tier = \"{requested:?}\",\n original_tier = \
|
||||||
|
\"{original:?}\",\n source = \"{source}\",\n}}\n"
|
||||||
);
|
);
|
||||||
let _ = fs::write(log_path, body);
|
let _ = fs::write(log_path, body);
|
||||||
}
|
}
|
||||||
|
|
@ -280,7 +279,8 @@ enum Compensation {
|
||||||
/// Remove an oplog file this transition created, and its parent
|
/// Remove an oplog file this transition created, and its parent
|
||||||
/// `substrate/` directory when this transition created that too. Only
|
/// `substrate/` directory when this transition created that too. Only
|
||||||
/// scheduled for substrate we created — pre-existing substrate is
|
/// scheduled for substrate we created — pre-existing substrate is
|
||||||
/// immutable on rollback (ADR-033 `downgrade-preserves-substrate-immutable`).
|
/// immutable on rollback (ADR-033
|
||||||
|
/// `downgrade-preserves-substrate-immutable`).
|
||||||
RemoveOplog {
|
RemoveOplog {
|
||||||
/// Path of the oplog file to remove.
|
/// Path of the oplog file to remove.
|
||||||
oplog_path: PathBuf,
|
oplog_path: PathBuf,
|
||||||
|
|
@ -488,11 +488,9 @@ pub fn execute(
|
||||||
tier: target_tier,
|
tier: target_tier,
|
||||||
phase: ctx.current_ops.phase,
|
phase: ctx.current_ops.phase,
|
||||||
};
|
};
|
||||||
if let Err(e) = enforce_tier_transition_guard(
|
if let Err(e) =
|
||||||
&ctx.current_ops,
|
enforce_tier_transition_guard(&ctx.current_ops, &target_ops, &ctx.pending_migrations)
|
||||||
&target_ops,
|
{
|
||||||
&ctx.pending_migrations,
|
|
||||||
) {
|
|
||||||
log_failed_transition(
|
log_failed_transition(
|
||||||
&ctx.project_root,
|
&ctx.project_root,
|
||||||
target_tier,
|
target_tier,
|
||||||
|
|
@ -539,7 +537,9 @@ pub fn execute(
|
||||||
comp.run();
|
comp.run();
|
||||||
}
|
}
|
||||||
let e = TransitionError::TierDescentWitness {
|
let e = TransitionError::TierDescentWitness {
|
||||||
reason: "Tier2 descent requires the actor signing key (supply it via the ops surface or the keypair store)".to_owned(),
|
reason: "Tier2 descent requires the actor signing key (supply it via the ops \
|
||||||
|
surface or the keypair store)"
|
||||||
|
.to_owned(),
|
||||||
};
|
};
|
||||||
log_failed_transition(
|
log_failed_transition(
|
||||||
&ctx.project_root,
|
&ctx.project_root,
|
||||||
|
|
@ -764,7 +764,10 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn transition_log_entries(project_root: &Path) -> Vec<PathBuf> {
|
fn transition_log_entries(project_root: &Path) -> Vec<PathBuf> {
|
||||||
let dir = project_root.join(".ontoref").join("artifacts").join("transition-log");
|
let dir = project_root
|
||||||
|
.join(".ontoref")
|
||||||
|
.join("artifacts")
|
||||||
|
.join("transition-log");
|
||||||
if !dir.exists() {
|
if !dir.exists() {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
@ -891,10 +894,12 @@ mod tests {
|
||||||
let between_quotes = body
|
let between_quotes = body
|
||||||
.split("source = \"")
|
.split("source = \"")
|
||||||
.nth(1)
|
.nth(1)
|
||||||
.and_then(|s| s.split("\"")
|
.and_then(|s| s.split("\"").next())
|
||||||
.next())
|
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
assert!(!between_quotes.contains('"'), "source value must not contain bare double-quotes");
|
assert!(
|
||||||
|
!between_quotes.contains('"'),
|
||||||
|
"source value must not contain bare double-quotes"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "substrate")]
|
#[cfg(feature = "substrate")]
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ use crate::validation::verdict::Verdict;
|
||||||
///
|
///
|
||||||
/// Slice envelope: `{ "adrs": [{"id": "…", "status": "…"}],
|
/// Slice envelope: `{ "adrs": [{"id": "…", "status": "…"}],
|
||||||
/// "proofs": [{"evidence_adr": "…"}] }`.
|
/// "proofs": [{"evidence_adr": "…"}] }`.
|
||||||
/// Returns `Unknown` when the slice was not loaded (slice loader not yet wired).
|
/// Returns `Unknown` when the slice was not loaded (slice loader not yet
|
||||||
|
/// wired).
|
||||||
#[onto_validator(
|
#[onto_validator(
|
||||||
id = "adr_accepted_has_proof",
|
id = "adr_accepted_has_proof",
|
||||||
description = "Every Accepted ADR is cited by at least one positioning Proof.",
|
description = "Every Accepted ADR is cited by at least one positioning Proof.",
|
||||||
|
|
@ -91,9 +92,10 @@ fn adr_proposal_requires_description(slice: &Slice, _ctx: &ValidationCtx) -> Ver
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
fn ctx() -> ValidationCtx {
|
fn ctx() -> ValidationCtx {
|
||||||
ValidationCtx::synthetic("test", "op:adr-propose")
|
ValidationCtx::synthetic("test", "op:adr-propose")
|
||||||
}
|
}
|
||||||
|
|
@ -218,8 +220,8 @@ mod tests {
|
||||||
"adrs": [{"id": "adr-001", "status": "Accepted"}],
|
"adrs": [{"id": "adr-001", "status": "Accepted"}],
|
||||||
"proofs": [{"evidence_adr": "adr-001"}]
|
"proofs": [{"evidence_adr": "adr-001"}]
|
||||||
});
|
});
|
||||||
let verdict =
|
let verdict = dispatch::dispatch("adr_accepted_has_proof", &slice, &ctx())
|
||||||
dispatch::dispatch("adr_accepted_has_proof", &slice, &ctx()).expect("must be registered");
|
.expect("must be registered");
|
||||||
assert_eq!(verdict, Verdict::Accept);
|
assert_eq!(verdict, Verdict::Accept);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -240,10 +242,11 @@ mod tests {
|
||||||
/// in NCL but its Rust fn was never registered via `#[onto_validator]`.
|
/// in NCL but its Rust fn was never registered via `#[onto_validator]`.
|
||||||
#[test]
|
#[test]
|
||||||
fn ncl_predicate_refs_all_registered() {
|
fn ncl_predicate_refs_all_registered() {
|
||||||
use crate::validation::dispatch;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
use crate::validation::dispatch;
|
||||||
|
|
||||||
// `CARGO_MANIFEST_DIR` = code/crates/ontoref-ops
|
// `CARGO_MANIFEST_DIR` = code/crates/ontoref-ops
|
||||||
// Three parents up → constellation root (ontoref/)
|
// Three parents up → constellation root (ontoref/)
|
||||||
let catalog = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
let catalog = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
|
@ -293,8 +296,8 @@ mod tests {
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
registered.contains(&predicate_ref),
|
registered.contains(&predicate_ref),
|
||||||
"NCL predicate_ref '{}' (from {}) is not registered in the Rust inventory.\n\
|
"NCL predicate_ref '{}' (from {}) is not registered in the Rust \
|
||||||
Registered ids: {:?}",
|
inventory.\nRegistered ids: {:?}",
|
||||||
predicate_ref,
|
predicate_ref,
|
||||||
path.display(),
|
path.display(),
|
||||||
registered
|
registered
|
||||||
|
|
|
||||||
|
|
@ -78,15 +78,17 @@ where
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use ontoref_derive::onto_validator;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::validation::registry::ValidatorEntry;
|
use crate::validation::registry::ValidatorEntry;
|
||||||
use ontoref_derive::onto_validator;
|
|
||||||
|
|
||||||
/// Dummy validator registered via the macro so the inventory iter
|
/// Dummy validator registered via the macro so the inventory iter
|
||||||
/// has at least one entry to discover during the gate test.
|
/// has at least one entry to discover during the gate test.
|
||||||
#[onto_validator(
|
#[onto_validator(
|
||||||
id = "dummy_always_accept",
|
id = "dummy_always_accept",
|
||||||
description = "Reference validator that accepts every input — exists to verify the registration path.",
|
description = "Reference validator that accepts every input — exists to verify the \
|
||||||
|
registration path.",
|
||||||
category = "Structural",
|
category = "Structural",
|
||||||
predicate_ref = "dummy_always_accept"
|
predicate_ref = "dummy_always_accept"
|
||||||
)]
|
)]
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,11 @@ impl PipelineWitness {
|
||||||
/// reconstruct this from the same inputs to verify the signature.
|
/// reconstruct this from the same inputs to verify the signature.
|
||||||
pub fn canonical_payload(&self) -> Vec<u8> {
|
pub fn canonical_payload(&self) -> Vec<u8> {
|
||||||
let mut buf = Vec::with_capacity(
|
let mut buf = Vec::with_capacity(
|
||||||
self.op_id.len() + self.state_root.len() + self.effects_hash.len() + self.extra_payload.len() + 16,
|
self.op_id.len()
|
||||||
|
+ self.state_root.len()
|
||||||
|
+ self.effects_hash.len()
|
||||||
|
+ self.extra_payload.len()
|
||||||
|
+ 16,
|
||||||
);
|
);
|
||||||
buf.extend_from_slice(b"ontoref-witness-v1");
|
buf.extend_from_slice(b"ontoref-witness-v1");
|
||||||
buf.extend_from_slice(self.op_id.as_bytes());
|
buf.extend_from_slice(self.op_id.as_bytes());
|
||||||
|
|
@ -54,10 +58,9 @@ impl PipelineWitness {
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// - [`SignatureError::InvalidPublicKey`] if the actor's key is
|
/// - [`SignatureError::InvalidPublicKey`] if the actor's key is malformed.
|
||||||
/// malformed.
|
/// - [`SignatureError::BadSignature`] if the signature does not verify the
|
||||||
/// - [`SignatureError::BadSignature`] if the signature does not
|
/// canonical payload.
|
||||||
/// verify the canonical payload.
|
|
||||||
pub fn verify(&self) -> Result<(), SignatureError> {
|
pub fn verify(&self) -> Result<(), SignatureError> {
|
||||||
let payload = self.canonical_payload();
|
let payload = self.canonical_payload();
|
||||||
self.actor.verify(&payload, &self.signature)
|
self.actor.verify(&payload, &self.signature)
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,9 @@ fn propose_adr_writes_adr_file_and_signs_witness() {
|
||||||
assert_eq!(result.witness.witness_shape, "adr_proposed");
|
assert_eq!(result.witness.witness_shape, "adr_proposed");
|
||||||
result.witness.verify().expect("witness verifies");
|
result.witness.verify().expect("witness verifies");
|
||||||
|
|
||||||
let adr_path = tmp.path().join(".ontoref/adrs/adr-099-cross-domain-pilot.ncl");
|
let adr_path = tmp
|
||||||
|
.path()
|
||||||
|
.join(".ontoref/adrs/adr-099-cross-domain-pilot.ncl");
|
||||||
assert!(adr_path.exists(), "ADR file must be written");
|
assert!(adr_path.exists(), "ADR file must be written");
|
||||||
let body = std::fs::read_to_string(&adr_path).expect("read adr");
|
let body = std::fs::read_to_string(&adr_path).expect("read adr");
|
||||||
assert!(body.contains("status = 'Proposed"));
|
assert!(body.contains("status = 'Proposed"));
|
||||||
|
|
@ -89,8 +91,7 @@ fn propose_adr_rejects_empty_ondaod_with_no_rationale() {
|
||||||
"synthesis_state": ""
|
"synthesis_state": ""
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let err = dispatch_op("propose_adr", &inputs, &mut ctx)
|
let err = dispatch_op("propose_adr", &inputs, &mut ctx).expect_err("empty ondaod must reject");
|
||||||
.expect_err("empty ondaod must reject");
|
|
||||||
assert!(matches!(err, OpError::ValidatorRejected { .. }));
|
assert!(matches!(err, OpError::ValidatorRejected { .. }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,8 +107,8 @@ fn propose_adr_rejects_malformed_id() {
|
||||||
"decision": "x",
|
"decision": "x",
|
||||||
"ondaod_evaluation": { "engaged_tensions": ["t"], "synthesis_state": "x" }
|
"ondaod_evaluation": { "engaged_tensions": ["t"], "synthesis_state": "x" }
|
||||||
});
|
});
|
||||||
let err = dispatch_op("propose_adr", &inputs, &mut ctx)
|
let err =
|
||||||
.expect_err("malformed adr id must reject");
|
dispatch_op("propose_adr", &inputs, &mut ctx).expect_err("malformed adr id must reject");
|
||||||
assert!(matches!(err, OpError::OpBody { .. }));
|
assert!(matches!(err, OpError::OpBody { .. }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,8 +154,8 @@ fn update_ontology_node_updates_existing_node() {
|
||||||
});
|
});
|
||||||
let result = dispatch_op("update_ontology_node", &inputs, &mut ctx).expect("dispatch ok");
|
let result = dispatch_op("update_ontology_node", &inputs, &mut ctx).expect("dispatch ok");
|
||||||
assert_eq!(result.effects[0].kind, "Update");
|
assert_eq!(result.effects[0].kind, "Update");
|
||||||
let body = std::fs::read_to_string(tmp.path().join(".ontoref/ontology/core.ncl"))
|
let body =
|
||||||
.expect("read core");
|
std::fs::read_to_string(tmp.path().join(".ontoref/ontology/core.ncl")).expect("read core");
|
||||||
assert!(body.contains("Updated description for the protocol invariant"));
|
assert!(body.contains("Updated description for the protocol invariant"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,8 +190,8 @@ fn manage_backlog_item_add_creates_entry() {
|
||||||
result.witness.verify().expect("witness verifies");
|
result.witness.verify().expect("witness verifies");
|
||||||
assert_eq!(result.effects[0].kind, "Insert");
|
assert_eq!(result.effects[0].kind, "Insert");
|
||||||
|
|
||||||
let body =
|
let body = std::fs::read_to_string(tmp.path().join(".ontoref/reflection/backlog.ncl"))
|
||||||
std::fs::read_to_string(tmp.path().join(".ontoref/reflection/backlog.ncl")).expect("read backlog");
|
.expect("read backlog");
|
||||||
assert!(body.contains("Wire MCP surface to dispatch"));
|
assert!(body.contains("Wire MCP surface to dispatch"));
|
||||||
assert!(body.contains("Feature"));
|
assert!(body.contains("Feature"));
|
||||||
}
|
}
|
||||||
|
|
@ -213,11 +214,10 @@ fn manage_backlog_item_update_mutates_existing() {
|
||||||
"id": "bl-9001",
|
"id": "bl-9001",
|
||||||
"status": "Closed"
|
"status": "Closed"
|
||||||
});
|
});
|
||||||
let result =
|
let result = dispatch_op("manage_backlog_item", &update_inputs, &mut ctx).expect("update ok");
|
||||||
dispatch_op("manage_backlog_item", &update_inputs, &mut ctx).expect("update ok");
|
|
||||||
assert_eq!(result.effects[0].kind, "Update");
|
assert_eq!(result.effects[0].kind, "Update");
|
||||||
let body =
|
let body = std::fs::read_to_string(tmp.path().join(".ontoref/reflection/backlog.ncl"))
|
||||||
std::fs::read_to_string(tmp.path().join(".ontoref/reflection/backlog.ncl")).expect("read backlog");
|
.expect("read backlog");
|
||||||
assert!(body.contains("Closed"));
|
assert!(body.contains("Closed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,10 @@ fn transition_adr_accept_requires_ondaod() {
|
||||||
});
|
});
|
||||||
let err = dispatch_op("transition_adr", &inputs, &mut c)
|
let err = dispatch_op("transition_adr", &inputs, &mut c)
|
||||||
.expect_err("accept without ondaod must reject");
|
.expect_err("accept without ondaod must reject");
|
||||||
assert!(matches!(err, ontoref_ops::OpError::ValidatorRejected { .. }));
|
assert!(matches!(
|
||||||
|
err,
|
||||||
|
ontoref_ops::OpError::ValidatorRejected { .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -75,9 +78,12 @@ fn transition_adr_unsupported_target_rejects() {
|
||||||
"new_status": "Withdrawn",
|
"new_status": "Withdrawn",
|
||||||
"ondaod_evaluation": { "engaged_tensions": ["x"], "synthesis_state": "x" }
|
"ondaod_evaluation": { "engaged_tensions": ["x"], "synthesis_state": "x" }
|
||||||
});
|
});
|
||||||
let err = dispatch_op("transition_adr", &inputs, &mut c)
|
let err =
|
||||||
.expect_err("unsupported target must reject");
|
dispatch_op("transition_adr", &inputs, &mut c).expect_err("unsupported target must reject");
|
||||||
assert!(matches!(err, ontoref_ops::OpError::ValidatorRejected { .. }));
|
assert!(matches!(
|
||||||
|
err,
|
||||||
|
ontoref_ops::OpError::ValidatorRejected { .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── update_ontology_edge ──────────────────────────────────────────────────
|
// ── update_ontology_edge ──────────────────────────────────────────────────
|
||||||
|
|
@ -105,9 +111,12 @@ fn update_ontology_edge_rejects_self_loop() {
|
||||||
"to": "x",
|
"to": "x",
|
||||||
"kind": "DependsOn",
|
"kind": "DependsOn",
|
||||||
});
|
});
|
||||||
let err = dispatch_op("update_ontology_edge", &inputs, &mut c)
|
let err =
|
||||||
.expect_err("self-loop must reject");
|
dispatch_op("update_ontology_edge", &inputs, &mut c).expect_err("self-loop must reject");
|
||||||
assert!(matches!(err, ontoref_ops::OpError::ValidatorRejected { .. }));
|
assert!(matches!(
|
||||||
|
err,
|
||||||
|
ontoref_ops::OpError::ValidatorRejected { .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -124,9 +133,12 @@ fn update_ontology_edge_rejects_1_step_cycle() {
|
||||||
"to": "b",
|
"to": "b",
|
||||||
"kind": "DependsOn",
|
"kind": "DependsOn",
|
||||||
});
|
});
|
||||||
let err = dispatch_op("update_ontology_edge", &inputs, &mut c)
|
let err =
|
||||||
.expect_err("1-step cycle must reject");
|
dispatch_op("update_ontology_edge", &inputs, &mut c).expect_err("1-step cycle must reject");
|
||||||
assert!(matches!(err, ontoref_ops::OpError::ValidatorRejected { .. }));
|
assert!(matches!(
|
||||||
|
err,
|
||||||
|
ontoref_ops::OpError::ValidatorRejected { .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── sync_apply ────────────────────────────────────────────────────────────
|
// ── sync_apply ────────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,11 @@ fn build_ctx(project_root: std::path::PathBuf) -> OpContext {
|
||||||
fn happy_path_full_pipeline() {
|
fn happy_path_full_pipeline() {
|
||||||
let tmp = tempfile::tempdir().expect("tempdir");
|
let tmp = tempfile::tempdir().expect("tempdir");
|
||||||
let project_root = tmp.path().to_path_buf();
|
let project_root = tmp.path().to_path_buf();
|
||||||
std::fs::create_dir_all(ontoref_ontology::layout::resolve_section(&project_root, "ontology")).expect("mkdir .ontology");
|
std::fs::create_dir_all(ontoref_ontology::layout::resolve_section(
|
||||||
|
&project_root,
|
||||||
|
"ontology",
|
||||||
|
))
|
||||||
|
.expect("mkdir .ontology");
|
||||||
|
|
||||||
let mut ctx = build_ctx(project_root.clone());
|
let mut ctx = build_ctx(project_root.clone());
|
||||||
let initial_root = ctx.commit_layer.root();
|
let initial_root = ctx.commit_layer.root();
|
||||||
|
|
@ -97,8 +101,14 @@ fn happy_path_full_pipeline() {
|
||||||
|
|
||||||
// ── substrate ───────────────────────────────────────────────────────────
|
// ── substrate ───────────────────────────────────────────────────────────
|
||||||
let new_root = ctx.commit_layer.root();
|
let new_root = ctx.commit_layer.root();
|
||||||
assert_eq!(new_root, result.witness.state_root, "witness binds to current state_root");
|
assert_eq!(
|
||||||
assert_ne!(new_root, initial_root, "state_root must advance after the op");
|
new_root, result.witness.state_root,
|
||||||
|
"witness binds to current state_root"
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
new_root, initial_root,
|
||||||
|
"state_root must advance after the op"
|
||||||
|
);
|
||||||
|
|
||||||
// ── typed state ────────────────────────────────────────────────────────
|
// ── typed state ────────────────────────────────────────────────────────
|
||||||
let dim = ctx.state.as_ref().unwrap().dimensions.first().unwrap();
|
let dim = ctx.state.as_ref().unwrap().dimensions.first().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,12 @@
|
||||||
//! log from disk and, trusting nothing but the actor's public key:
|
//! log from disk and, trusting nothing but the actor's public key:
|
||||||
//!
|
//!
|
||||||
//! 1. verifies every persisted operation's signature (the oplog body);
|
//! 1. verifies every persisted operation's signature (the oplog body);
|
||||||
//! 2. reconstructs each `PipelineWitness` from its payload and verifies
|
//! 2. reconstructs each `PipelineWitness` from its payload and verifies its
|
||||||
//! its own Ed25519 signature;
|
//! own Ed25519 signature;
|
||||||
//! 3. replays the witness chain — applying each witness's embedded
|
//! 3. replays the witness chain — applying each witness's embedded
|
||||||
//! state-changing op to a fresh commit layer — and confirms the
|
//! state-changing op to a fresh commit layer — and confirms the
|
||||||
//! reconstructed `state_root` reproduces the one the witness attests,
|
//! reconstructed `state_root` reproduces the one the witness attests, at
|
||||||
//! at every step.
|
//! every step.
|
||||||
//!
|
//!
|
||||||
//! This is the executable proof of the `vp-004` provenance claim.
|
//! This is the executable proof of the `vp-004` provenance claim.
|
||||||
|
|
||||||
|
|
@ -41,9 +41,8 @@ fn full_cycle_dispatch_persist_replay_verify() {
|
||||||
// verifier reopens the file — a genuine fresh-process boundary.
|
// verifier reopens the file — a genuine fresh-process boundary.
|
||||||
let attested_roots: Vec<[u8; 32]> = {
|
let attested_roots: Vec<[u8; 32]> = {
|
||||||
let oplog = Arc::new(OpLog::open(&oplog_path).expect("open oplog"));
|
let oplog = Arc::new(OpLog::open(&oplog_path).expect("open oplog"));
|
||||||
let mut ctx =
|
let mut ctx = OpContext::new_in_memory("developer", "", SigningKey::from_bytes(&SEED))
|
||||||
OpContext::new_in_memory("developer", "", SigningKey::from_bytes(&SEED))
|
.with_oplog(Arc::clone(&oplog));
|
||||||
.with_oplog(Arc::clone(&oplog));
|
|
||||||
|
|
||||||
let mut roots = Vec::new();
|
let mut roots = Vec::new();
|
||||||
for topic in ["structure-that-remembers", "witness-as-axis-seam"] {
|
for topic in ["structure-that-remembers", "witness-as-axis-seam"] {
|
||||||
|
|
|
||||||
|
|
@ -76,9 +76,7 @@ impl QueryEngine {
|
||||||
if !visited.insert(node.clone()) {
|
if !visited.insert(node.clone()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let next = self
|
let next = self.triples.entity_attr(&node, ref_attr, as_of, valid_at)?;
|
||||||
.triples
|
|
||||||
.entity_attr(&node, ref_attr, as_of, valid_at)?;
|
|
||||||
if let Some(Value::Ref(target)) = next {
|
if let Some(Value::Ref(target)) = next {
|
||||||
frontier.push(target);
|
frontier.push(target);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,13 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use ed25519_dalek::SigningKey;
|
use ed25519_dalek::SigningKey;
|
||||||
|
use ontoref_query::QueryEngine;
|
||||||
use ontoref_triples::TripleStore;
|
use ontoref_triples::TripleStore;
|
||||||
use ontoref_types::{
|
use ontoref_types::{
|
||||||
operation::{OpBody, OpPayload, Operation},
|
operation::{OpBody, OpPayload, Operation},
|
||||||
AttrId, EntityId, Hlc, PublicKey, Value,
|
AttrId, EntityId, Hlc, PublicKey, Value,
|
||||||
};
|
};
|
||||||
|
|
||||||
use ontoref_query::QueryEngine;
|
|
||||||
|
|
||||||
fn signing_key() -> SigningKey {
|
fn signing_key() -> SigningKey {
|
||||||
SigningKey::from_bytes(&[13u8; 32])
|
SigningKey::from_bytes(&[13u8; 32])
|
||||||
}
|
}
|
||||||
|
|
@ -147,10 +146,8 @@ fn transitive_closure_respects_as_of_horizon() {
|
||||||
Hlc::new(30, 0),
|
Hlc::new(30, 0),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let expected_late: std::collections::HashSet<EntityId> = ["a", "b", "c"]
|
let expected_late: std::collections::HashSet<EntityId> =
|
||||||
.iter()
|
["a", "b", "c"].iter().map(|n| EntityId::new(*n)).collect();
|
||||||
.map(|n| EntityId::new(*n))
|
|
||||||
.collect();
|
|
||||||
assert_eq!(late, expected_late);
|
assert_eq!(late, expected_late);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -268,8 +265,5 @@ fn subscription_delivers_op_payload_to_handler() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let seen = captured.lock().unwrap();
|
let seen = captured.lock().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(*seen, vec![EntityId::new("alpha"), EntityId::new("beta")]);
|
||||||
*seen,
|
|
||||||
vec![EntityId::new("alpha"), EntityId::new("beta")]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,10 @@ impl FilesystemSync {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// IO error when the local peer directory cannot be created.
|
/// IO error when the local peer directory cannot be created.
|
||||||
pub fn new(base_dir: impl Into<PathBuf>, peer_id: impl Into<String>) -> Result<Self, SyncError> {
|
pub fn new(
|
||||||
|
base_dir: impl Into<PathBuf>,
|
||||||
|
peer_id: impl Into<String>,
|
||||||
|
) -> Result<Self, SyncError> {
|
||||||
let base_dir = base_dir.into();
|
let base_dir = base_dir.into();
|
||||||
let peer_id = peer_id.into();
|
let peer_id = peer_id.into();
|
||||||
let local = base_dir.join(&peer_id);
|
let local = base_dir.join(&peer_id);
|
||||||
|
|
@ -120,7 +123,10 @@ impl FilesystemSync {
|
||||||
///
|
///
|
||||||
/// IO error on write.
|
/// IO error on write.
|
||||||
pub fn publish_body(&self, op_id: &str, body: &[u8]) -> Result<(), SyncError> {
|
pub fn publish_body(&self, op_id: &str, body: &[u8]) -> Result<(), SyncError> {
|
||||||
let path = self.base_dir.join(&self.peer_id).join(format!("{op_id}.body"));
|
let path = self
|
||||||
|
.base_dir
|
||||||
|
.join(&self.peer_id)
|
||||||
|
.join(format!("{op_id}.body"));
|
||||||
std::fs::write(&path, body).map_err(|e| SyncError::Io {
|
std::fs::write(&path, body).map_err(|e| SyncError::Io {
|
||||||
context: format!("writing body to {}", path.display()),
|
context: format!("writing body to {}", path.display()),
|
||||||
source: e,
|
source: e,
|
||||||
|
|
@ -254,6 +260,8 @@ fn filesystem_default() {
|
||||||
assert!(missing.is_none());
|
assert!(missing.is_none());
|
||||||
|
|
||||||
// Subscribing to a non-existent peer fails clearly.
|
// Subscribing to a non-existent peer fails clearly.
|
||||||
let err = bob.subscribe_peer("nobody").expect_err("subscribe must fail");
|
let err = bob
|
||||||
|
.subscribe_peer("nobody")
|
||||||
|
.expect_err("subscribe must fail");
|
||||||
assert!(matches!(err, SyncError::Io { .. }));
|
assert!(matches!(err, SyncError::Io { .. }));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ pub enum Error {
|
||||||
/// An identifier exceeded the 65535-byte key encoding limit.
|
/// An identifier exceeded the 65535-byte key encoding limit.
|
||||||
#[error("identifier too long ({0} bytes)")]
|
#[error("identifier too long ({0} bytes)")]
|
||||||
IdentifierTooLong(usize),
|
IdentifierTooLong(usize),
|
||||||
/// A stored EAV row's key bytes did not decode to a valid (attr, hlc) tuple.
|
/// A stored EAV row's key bytes did not decode to a valid (attr, hlc)
|
||||||
|
/// tuple.
|
||||||
#[error("corrupt key in eav table")]
|
#[error("corrupt key in eav table")]
|
||||||
CorruptKey,
|
CorruptKey,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,7 @@ pub(crate) fn hlc_from_be(bytes: &[u8; 12]) -> Hlc {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn value_hash(value: &Value) -> [u8; 32] {
|
pub(crate) fn value_hash(value: &Value) -> [u8; 32] {
|
||||||
let bytes =
|
let bytes = canonical::encode(value).expect("canonical encoding never fails for owned types");
|
||||||
canonical::encode(value).expect("canonical encoding never fails for owned types");
|
|
||||||
*blake3::hash(&bytes).as_bytes()
|
*blake3::hash(&bytes).as_bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,12 +56,7 @@ pub(crate) fn eav_prefix_entity_attr(entity: &str, attr: &str) -> Result<Vec<u8>
|
||||||
Ok(key)
|
Ok(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn vae_key(
|
pub(crate) fn vae_key(attr: &str, value_h: [u8; 32], entity: &str, tx: Hlc) -> Result<Vec<u8>> {
|
||||||
attr: &str,
|
|
||||||
value_h: [u8; 32],
|
|
||||||
entity: &str,
|
|
||||||
tx: Hlc,
|
|
||||||
) -> Result<Vec<u8>> {
|
|
||||||
let mut key = Vec::with_capacity(2 + attr.len() + 32 + 2 + entity.len() + 12);
|
let mut key = Vec::with_capacity(2 + attr.len() + 32 + 2 + entity.len() + 12);
|
||||||
push_len_str(&mut key, attr)?;
|
push_len_str(&mut key, attr)?;
|
||||||
key.extend_from_slice(&value_h);
|
key.extend_from_slice(&value_h);
|
||||||
|
|
@ -78,7 +72,8 @@ pub(crate) fn vae_prefix_attr_value(attr: &str, value_h: [u8; 32]) -> Result<Vec
|
||||||
Ok(key)
|
Ok(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse the (attr, tx_time) suffix of an EAV key whose entity prefix is `entity`.
|
/// Parse the (attr, tx_time) suffix of an EAV key whose entity prefix is
|
||||||
|
/// `entity`.
|
||||||
pub(crate) fn parse_eav_key_attr_tx(key: &[u8], entity: &str) -> Result<(String, Hlc)> {
|
pub(crate) fn parse_eav_key_attr_tx(key: &[u8], entity: &str) -> Result<(String, Hlc)> {
|
||||||
let entity_prefix_len = 2 + entity.len();
|
let entity_prefix_len = 2 + entity.len();
|
||||||
if key.len() < entity_prefix_len + 2 + 12 {
|
if key.len() < entity_prefix_len + 2 + 12 {
|
||||||
|
|
@ -98,7 +93,8 @@ pub(crate) fn parse_eav_key_attr_tx(key: &[u8], entity: &str) -> Result<(String,
|
||||||
Ok((attr, hlc_from_be(&tx_bytes)))
|
Ok((attr, hlc_from_be(&tx_bytes)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse the (entity, tx_time) suffix of a VAE key whose (attr, value_hash) prefix is fixed.
|
/// Parse the (entity, tx_time) suffix of a VAE key whose (attr, value_hash)
|
||||||
|
/// prefix is fixed.
|
||||||
pub(crate) fn parse_vae_key_entity_tx(key: &[u8], prefix_len: usize) -> Result<(String, Hlc)> {
|
pub(crate) fn parse_vae_key_entity_tx(key: &[u8], prefix_len: usize) -> Result<(String, Hlc)> {
|
||||||
if key.len() < prefix_len + 2 + 12 {
|
if key.len() < prefix_len + 2 + 12 {
|
||||||
return Err(Error::CorruptKey);
|
return Err(Error::CorruptKey);
|
||||||
|
|
@ -117,7 +113,8 @@ pub(crate) fn parse_vae_key_entity_tx(key: &[u8], prefix_len: usize) -> Result<(
|
||||||
Ok((entity, hlc_from_be(&tx_bytes)))
|
Ok((entity, hlc_from_be(&tx_bytes)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract the tx_time from an EAV key whose `(entity, attr)` prefix is `entity_attr_prefix`.
|
/// Extract the tx_time from an EAV key whose `(entity, attr)` prefix is
|
||||||
|
/// `entity_attr_prefix`.
|
||||||
pub(crate) fn parse_eav_key_tx_only(key: &[u8], entity_attr_prefix_len: usize) -> Result<Hlc> {
|
pub(crate) fn parse_eav_key_tx_only(key: &[u8], entity_attr_prefix_len: usize) -> Result<Hlc> {
|
||||||
if key.len() != entity_attr_prefix_len + 12 {
|
if key.len() != entity_attr_prefix_len + 12 {
|
||||||
return Err(Error::CorruptKey);
|
return Err(Error::CorruptKey);
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,12 @@ impl TripleStore {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let candidate = (eav_row.valid_from, tx_time, eav_row.value, eav_row.retracted);
|
let candidate = (
|
||||||
|
eav_row.valid_from,
|
||||||
|
tx_time,
|
||||||
|
eav_row.value,
|
||||||
|
eav_row.retracted,
|
||||||
|
);
|
||||||
let better = match winners.get(&attr) {
|
let better = match winners.get(&attr) {
|
||||||
None => true,
|
None => true,
|
||||||
Some((current_vf, current_tx, _, _)) => {
|
Some((current_vf, current_tx, _, _)) => {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use ed25519_dalek::SigningKey;
|
use ed25519_dalek::SigningKey;
|
||||||
|
use ontoref_triples::TripleStore;
|
||||||
use ontoref_types::{
|
use ontoref_types::{
|
||||||
operation::{OpBody, OpPayload, Operation},
|
operation::{OpBody, OpPayload, Operation},
|
||||||
AttrId, EntityId, Hlc, PublicKey, Value,
|
AttrId, EntityId, Hlc, PublicKey, Value,
|
||||||
};
|
};
|
||||||
|
|
||||||
use ontoref_triples::TripleStore;
|
|
||||||
|
|
||||||
fn signing_key() -> SigningKey {
|
fn signing_key() -> SigningKey {
|
||||||
SigningKey::from_bytes(&[9u8; 32])
|
SigningKey::from_bytes(&[9u8; 32])
|
||||||
}
|
}
|
||||||
|
|
@ -53,9 +52,7 @@ fn forward_index_returns_attrs_for_entity() {
|
||||||
))
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let attrs = store
|
let attrs = store.entity_attrs(&EntityId::new("alice"), tx, tx).unwrap();
|
||||||
.entity_attrs(&EntityId::new("alice"), tx, tx)
|
|
||||||
.unwrap();
|
|
||||||
let map: HashMap<String, Value> = attrs
|
let map: HashMap<String, Value> = attrs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(attr, value)| (attr.as_str().to_string(), value))
|
.map(|(attr, value)| (attr.as_str().to_string(), value))
|
||||||
|
|
@ -205,14 +202,10 @@ fn as_of_filters_to_tx_time_horizon() {
|
||||||
.apply(&assert_op("e", vec![("v", Value::Int(2))], tx2, None))
|
.apply(&assert_op("e", vec![("v", Value::Int(2))], tx2, None))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let at_tx1 = store
|
let at_tx1 = store.entity_attrs(&EntityId::new("e"), tx1, tx1).unwrap();
|
||||||
.entity_attrs(&EntityId::new("e"), tx1, tx1)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(at_tx1[0].1, Value::Int(1));
|
assert_eq!(at_tx1[0].1, Value::Int(1));
|
||||||
|
|
||||||
let at_tx2 = store
|
let at_tx2 = store.entity_attrs(&EntityId::new("e"), tx2, tx2).unwrap();
|
||||||
.entity_attrs(&EntityId::new("e"), tx2, tx2)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(at_tx2[0].1, Value::Int(2));
|
assert_eq!(at_tx2[0].1, Value::Int(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,7 +333,12 @@ fn retract_hides_attribute_in_query_view() {
|
||||||
|
|
||||||
// Assert e.v = 1 at tx=10, then retract e.v at tx=20.
|
// Assert e.v = 1 at tx=10, then retract e.v at tx=20.
|
||||||
store
|
store
|
||||||
.apply(&assert_op("e", vec![("v", Value::Int(1))], Hlc::new(10, 0), None))
|
.apply(&assert_op(
|
||||||
|
"e",
|
||||||
|
vec![("v", Value::Int(1))],
|
||||||
|
Hlc::new(10, 0),
|
||||||
|
None,
|
||||||
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
store
|
store
|
||||||
.apply(&retract_op("e", &["v"], Hlc::new(20, 0)))
|
.apply(&retract_op("e", &["v"], Hlc::new(20, 0)))
|
||||||
|
|
@ -364,15 +362,31 @@ fn reassert_after_retract_restores_attribute() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let store = TripleStore::open(dir.path().join("triples.redb")).unwrap();
|
let store = TripleStore::open(dir.path().join("triples.redb")).unwrap();
|
||||||
store
|
store
|
||||||
.apply(&assert_op("e", vec![("v", Value::Int(1))], Hlc::new(10, 0), None))
|
.apply(&assert_op(
|
||||||
|
"e",
|
||||||
|
vec![("v", Value::Int(1))],
|
||||||
|
Hlc::new(10, 0),
|
||||||
|
None,
|
||||||
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
store.apply(&retract_op("e", &["v"], Hlc::new(20, 0))).unwrap();
|
|
||||||
store
|
store
|
||||||
.apply(&assert_op("e", vec![("v", Value::Int(2))], Hlc::new(30, 0), None))
|
.apply(&retract_op("e", &["v"], Hlc::new(20, 0)))
|
||||||
|
.unwrap();
|
||||||
|
store
|
||||||
|
.apply(&assert_op(
|
||||||
|
"e",
|
||||||
|
vec![("v", Value::Int(2))],
|
||||||
|
Hlc::new(30, 0),
|
||||||
|
None,
|
||||||
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let now = store
|
let now = store
|
||||||
.entity_attrs(&EntityId::new("e"), Hlc::new(30, 0), Hlc::new(30, 0))
|
.entity_attrs(&EntityId::new("e"), Hlc::new(30, 0), Hlc::new(30, 0))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(now, vec![(AttrId::new("v"), Value::Int(2))], "re-assert after retract wins by tx_time");
|
assert_eq!(
|
||||||
|
now,
|
||||||
|
vec![(AttrId::new("v"), Value::Int(2))],
|
||||||
|
"re-assert after retract wins by tx_time"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
//! Determinism depends on two properties:
|
//! Determinism depends on two properties:
|
||||||
//! 1. The encoder MUST NOT emit indefinite-length items.
|
//! 1. The encoder MUST NOT emit indefinite-length items.
|
||||||
//! 2. Map keys MUST appear in lexicographic byte order (CBOR canonical form,
|
//! 2. Map keys MUST appear in lexicographic byte order (CBOR canonical form,
|
||||||
//! RFC 8949 §4.2.1). `ciborium` respects struct field declaration order;
|
//! RFC 8949 §4.2.1). `ciborium` respects struct field declaration order; the
|
||||||
//! the type-level invariant is: every serialized struct declares fields in
|
//! type-level invariant is: every serialized struct declares fields in
|
||||||
//! lexicographic order. Each new struct added must respect this.
|
//! lexicographic order. Each new struct added must respect this.
|
||||||
|
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
//! Content-addressed identifier for operations.
|
//! Content-addressed identifier for operations.
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// 32-byte blake3 content address of a canonically-encoded operation.
|
/// 32-byte blake3 content address of a canonically-encoded operation.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
pub struct OpId(pub [u8; 32]);
|
pub struct OpId(pub [u8; 32]);
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,8 @@ pub enum OpPayload {
|
||||||
/// witness to its DAG position (bl-022): reordering the log mutates
|
/// witness to its DAG position (bl-022): reordering the log mutates
|
||||||
/// the signed body and fails `verify_signature`.
|
/// the signed body and fails `verify_signature`.
|
||||||
Witness {
|
Witness {
|
||||||
/// Blake3 hash over the canonical encoding of the attested op's effects.
|
/// Blake3 hash over the canonical encoding of the attested op's
|
||||||
|
/// effects.
|
||||||
effects_hash: [u8; 32],
|
effects_hash: [u8; 32],
|
||||||
/// Domain payload the op body emitted (canonical bytes for replay).
|
/// Domain payload the op body emitted (canonical bytes for replay).
|
||||||
extra_payload: Vec<u8>,
|
extra_payload: Vec<u8>,
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue