Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,18 @@ jobs:
name: cargo check + test
runs-on: ubuntu-latest
timeout-minutes: 30
env:
# Tests read OGIT from a checkout of AdaWorldAPI/OGIT, never from a
# copy in this repo (`crates/ogar-from-schema/src/ogit_checkout.rs`).
OGIT_FORK_PATH: ${{ github.workspace }}/ogit
steps:
- uses: actions/checkout@v4
- name: Check out AdaWorldAPI/OGIT at its moving master
uses: actions/checkout@v4
with:
repository: AdaWorldAPI/OGIT
ref: master
path: ogit
- name: Install Rust toolchain (per rust-toolchain.toml)
uses: dtolnay/rust-toolchain@master
with:
Expand Down
53 changes: 24 additions & 29 deletions crates/ogar-from-schema/src/do_arm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,30 +412,28 @@ pub fn assemble_action_handler(entities: &[EntityDecl]) -> Option<ActionHandlerS
// ───────────────────────────────────────────────────────────── tests ──
//
// The schema-level half of PROBE-OGAR-DO-ARM-LIFT: prove the §4 field mapping
// lifts from the real vendored Automation TTL (no fixtures — the bytes are the
// frozen OGIT source, same calibration discipline as the MARS structural arm).
// lifts from the real Automation TTL in the OGIT checkout (no fixtures, same
// calibration discipline as the MARS structural arm).

#[cfg(test)]
mod tests {
use super::*;
use crate::{TtlDeclaration, ttl::parse_file};

const KNOWLEDGE_ITEM_TTL: &str =
include_str!("../../../vocab/imports/ogit/NTO/Automation/entities/KnowledgeItem.ttl");
const ACTION_HANDLER_TTL: &str =
include_str!("../../../vocab/imports/ogit/NTO/Automation/entities/ActionHandler.ttl");
const ACTION_APPLICABILITY_TTL: &str =
include_str!("../../../vocab/imports/ogit/NTO/Automation/entities/ActionApplicability.ttl");
const TRIGGER_TTL: &str =
include_str!("../../../vocab/imports/ogit/NTO/Automation/entities/Trigger.ttl");

fn entity(src: &str) -> EntityDecl {
match parse_file(src).expect("parses") {
TtlDeclaration::Entity(e) => e,
other => panic!("expected entity, got {other:?}"),
}
}

/// An `NTO/Automation` entity, read from the OGIT checkout.
fn automation(name: &str) -> EntityDecl {
entity(&crate::ogit_checkout::read(&format!(
"NTO/Automation/entities/{name}.ttl"
)))
}

#[test]
fn snake_handles_acronym_runs() {
assert_eq!(snake("MARSNodeTemplate"), "mars_node_template");
Expand All @@ -462,7 +460,7 @@ mod tests {
/// asserted against the real OGIT TTL bytes.
#[test]
fn knowledge_item_lifts_to_action_def() {
let ki = entity(KNOWLEDGE_ITEM_TTL);
let ki = automation("KnowledgeItem");
assert!(
is_actionable(&ki),
"KnowledgeItem must be an action carrier"
Expand Down Expand Up @@ -509,7 +507,7 @@ mod tests {
/// recorded by name; `body_source` stays `None` at the schema level.
#[test]
fn knowledge_item_payload_is_pointed_to_never_inlined() {
let ki = entity(KNOWLEDGE_ITEM_TTL);
let ki = automation("KnowledgeItem");
assert_eq!(
payload_attribute(&ki).as_deref(),
Some("knowledge_item_formal_representation"),
Expand All @@ -527,7 +525,7 @@ mod tests {
/// (its `environmentFilter`), not to a standalone `ActionDef`.
#[test]
fn action_applicability_maps_to_state_guard_not_a_def() {
let app = entity(ACTION_APPLICABILITY_TTL);
let app = automation("ActionApplicability");
assert!(
into_action_def(&app).is_none(),
"applicability is not a def"
Expand All @@ -544,15 +542,15 @@ mod tests {
/// `ActionHandler` is the adapter/membrane (§4) — never a standalone def.
#[test]
fn action_handler_is_not_a_standalone_def() {
let handler = entity(ACTION_HANDLER_TTL);
let handler = automation("ActionHandler");
assert!(!is_actionable(&handler));
assert!(into_action_def(&handler).is_none());
}

/// `Trigger` is the kausal source, not an action carrier itself.
#[test]
fn trigger_is_not_a_standalone_def() {
let trigger = entity(TRIGGER_TTL);
let trigger = automation("Trigger");
assert!(!is_actionable(&trigger));
assert!(into_action_def(&trigger).is_none());
}
Expand All @@ -561,10 +559,10 @@ mod tests {
#[test]
fn lift_action_defs_keeps_only_carriers() {
let entities = [
entity(KNOWLEDGE_ITEM_TTL),
entity(ACTION_HANDLER_TTL),
entity(ACTION_APPLICABILITY_TTL),
entity(TRIGGER_TTL),
automation("KnowledgeItem"),
automation("ActionHandler"),
automation("ActionApplicability"),
automation("Trigger"),
];
let defs = lift_action_defs(&entities);
assert_eq!(
Expand All @@ -577,17 +575,14 @@ mod tests {

// ── arago ActionHandler contract parity (the `provides` graph) ──

const ACTION_CAPABILITY_TTL: &str =
include_str!("../../../vocab/imports/ogit/NTO/Automation/entities/ActionCapability.ttl");

/// The full arago ActionHandler contract assembles from the vendored OGIT
/// `provides` graph: ActionHandler → ActionApplicability → ActionCapability.
/// The full arago ActionHandler contract assembles from OGIT's `provides`
/// graph: ActionHandler → ActionApplicability → ActionCapability.
#[test]
fn assembles_the_full_action_handler_contract() {
let entities = [
entity(ACTION_HANDLER_TTL),
entity(ACTION_APPLICABILITY_TTL),
entity(ACTION_CAPABILITY_TTL),
automation("ActionHandler"),
automation("ActionApplicability"),
automation("ActionCapability"),
];
let spec = assemble_action_handler(&entities).expect("ActionHandler present");

Expand Down Expand Up @@ -620,7 +615,7 @@ mod tests {
/// No ActionHandler in the set ⇒ no contract to assemble.
#[test]
fn assemble_returns_none_without_a_handler() {
let entities = [entity(KNOWLEDGE_ITEM_TTL), entity(TRIGGER_TTL)];
let entities = [automation("KnowledgeItem"), automation("Trigger")];
assert!(assemble_action_handler(&entities).is_none());
}

Expand Down
4 changes: 3 additions & 1 deletion crates/ogar-from-schema/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
//! # v0 scope
//!
//! - `ttl` front-end: reads the line-oriented OGIT TTL dialect into
//! [`Class`]. Demo target: `vocab/imports/ogit/MARS/`.
//! [`Class`]. Demo target: OGIT's `NTO/MARS/`.
//! - Cross-check: the **fixed-enum agreement test** asserts the TTL
//! `ogit:validation-parameter` set matches the XSD oracle's extracted
//! classifications (the chess-grade bijection, applied at the schema
Expand All @@ -59,6 +59,8 @@ use ogar_vocab::{Attribute, Class, EnumDecl, EnumSource, Language};

pub mod action_ws;
pub mod do_arm;
#[cfg(test)]
mod ogit_checkout;
pub mod registration;
pub mod sgo;
pub mod ttl;
Expand Down
39 changes: 39 additions & 0 deletions crates/ogar-from-schema/src/ogit_checkout.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//! Where OGAR's tests read OGIT: a checkout of `AdaWorldAPI/OGIT` at its
//! moving `master`. OGAR keeps no copy of it. `OGIT_FORK_PATH` names the
//! checkout; without it, `OGIT` next to this repository. lance-graph-ontology
//! reads OGIT the same way.
//!
//! A missing checkout is a setup error, not a skip: a test that reads OGIT
//! must not pass by not running.

use std::path::{Path, PathBuf};

/// The checkout's root, the directory that holds `NTO/` and `SGO/`.
///
/// # Panics
/// If the named checkout, or `../OGIT` when none is named, has no `NTO/`.
pub(crate) fn root() -> PathBuf {
let (root, source) = match std::env::var_os("OGIT_FORK_PATH") {
Some(path) => (PathBuf::from(path), "OGIT_FORK_PATH"),
None => (
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../OGIT"),
"the sibling checkout",
),
};
assert!(
root.join("NTO").is_dir(),
"no AdaWorldAPI/OGIT checkout at {} ({source}); clone \
https://github.com/AdaWorldAPI/OGIT next to OGAR or set OGIT_FORK_PATH",
root.display()
);
root
}

/// One file of the checkout, by its path inside OGIT.
///
/// # Panics
/// If the checkout is missing or the file does not read.
pub(crate) fn read(path: &str) -> String {
let full = root().join(path);
std::fs::read_to_string(&full).unwrap_or_else(|e| panic!("{}: {e}", full.display()))
}
24 changes: 11 additions & 13 deletions crates/ogar-from-schema/src/sgo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
//! SGO is OGIT's upper ontology — the layer above NTO that declares
//! the canonical entities (`ogit:Entity`, `ogit:Node`) and **the
//! canonical verb vocabulary** (`ogit:dependsOn`, `ogit:contains`,
//! `ogit:generates`, `ogit:relates`, `ogit:causes`, …). 176 verb TTLs
//! at `vocab/imports/ogit/SGO/sgo/verbs/`.
//! `ogit:generates`, `ogit:relates`, `ogit:causes`, …), one verb TTL each
//! under OGIT's `SGO/sgo/verbs/`.
//!
//! **Why this matters for OGAR's AST.** Every NTO entity's
//! `ogit:allowed ([ verb target ])` block references SGO verbs by
Expand Down Expand Up @@ -116,14 +116,13 @@ pub fn emit_verb(verb: &VerbDecl) -> String {
mod tests {
use super::*;

const DEPENDSON_TTL: &str =
include_str!("../../../vocab/imports/ogit/SGO/sgo/verbs/dependsOn.ttl");
const CONTAINS_TTL: &str =
include_str!("../../../vocab/imports/ogit/SGO/sgo/verbs/contains.ttl");
fn verb_ttl(name: &str) -> String {
crate::ogit_checkout::read(&format!("SGO/sgo/verbs/{name}.ttl"))
}

#[test]
fn parses_dependson() {
let v = parse_verb(DEPENDSON_TTL).expect("parses");
let v = parse_verb(&verb_ttl("dependsOn")).expect("parses");
assert_eq!(v.curie, "ogit:dependsOn");
assert_eq!(v.name, "dependsOn");
assert_eq!(v.label, "dependsOn");
Expand All @@ -134,7 +133,7 @@ mod tests {

#[test]
fn parses_contains() {
let v = parse_verb(CONTAINS_TTL).expect("parses");
let v = parse_verb(&verb_ttl("contains")).expect("parses");
assert_eq!(v.name, "contains");
// Description uses the triple-quoted form because it contains
// an embedded literal quote (`see also "includes"`).
Expand All @@ -143,20 +142,19 @@ mod tests {

#[test]
fn dependson_roundtrip() {
let once = parse_verb(DEPENDSON_TTL).expect("parses");
let once = parse_verb(&verb_ttl("dependsOn")).expect("parses");
let emitted = emit_verb(&once);
let twice = parse_verb(&emitted).expect("re-parses");
assert_eq!(once, twice, "verb round-trip lost a predicate");
}

/// Walk every verb TTL under `SGO/sgo/verbs/` and assert round-trip.
/// One of OGIT's strengths is that the verb vocabulary is small and
/// stable; this test catches drift on any of the 176 verbs the
/// stable; this test catches drift on any verb in the checkout the
/// moment a predicate slips out of `VerbDecl`.
#[test]
fn all_sgo_verbs_roundtrip() {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../vocab/imports/ogit/SGO/sgo/verbs");
let dir = crate::ogit_checkout::root().join("SGO/sgo/verbs");
let mut checked = 0usize;
for entry in std::fs::read_dir(&dir).expect("read sgo/verbs/") {
let entry = entry.expect("entry");
Expand All @@ -178,7 +176,7 @@ mod tests {
}
assert!(
checked >= 170,
"expected ≥ 170 SGO verbs at this SHA, got {checked}"
"expected ≥ 170 SGO verbs in the OGIT checkout, got {checked}"
);
}
}
51 changes: 26 additions & 25 deletions crates/ogar-from-schema/src/ttl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,18 +248,14 @@ fn extract_allowed_block(src: &str) -> Vec<(String, String)> {
mod tests {
use super::*;

const MACHINE_TTL: &str =
include_str!("../../../vocab/imports/ogit/NTO/MARS/entities/Machine.ttl");
const APPLICATION_TTL: &str =
include_str!("../../../vocab/imports/ogit/NTO/MARS/entities/Application.ttl");
const APP_CLASS_TTL: &str =
include_str!("../../../vocab/imports/ogit/NTO/MARS/Application/attributes/class.ttl");
const APP_SUBCLASS_TTL: &str =
include_str!("../../../vocab/imports/ogit/NTO/MARS/Application/attributes/subClass.ttl");
fn mars(path: &str) -> String {
crate::ogit_checkout::read(&format!("NTO/MARS/{path}"))
}

#[test]
fn parses_machine_entity() {
let TtlDeclaration::Entity(e) = parse_file(MACHINE_TTL).expect("parses") else {
let TtlDeclaration::Entity(e) = parse_file(&mars("entities/Machine.ttl")).expect("parses")
else {
panic!("expected entity");
};
assert_eq!(e.curie, "ogit.MARS:Machine");
Expand All @@ -285,7 +281,9 @@ mod tests {

#[test]
fn parses_application_entity_with_dependson_chain() {
let TtlDeclaration::Entity(e) = parse_file(APPLICATION_TTL).expect("parses") else {
let TtlDeclaration::Entity(e) =
parse_file(&mars("entities/Application.ttl")).expect("parses")
else {
panic!("expected entity");
};
assert_eq!(e.name, "Application");
Expand All @@ -301,7 +299,8 @@ mod tests {

#[test]
fn parses_fixed_enum_attribute() {
let TtlDeclaration::DatatypeAttribute(a) = parse_file(APP_CLASS_TTL).expect("parses")
let TtlDeclaration::DatatypeAttribute(a) =
parse_file(&mars("Application/attributes/class.ttl")).expect("parses")
else {
panic!("expected datatype attribute");
};
Expand All @@ -325,22 +324,22 @@ mod tests {
.map(str::to_owned)
.collect::<Vec<_>>()
);
// Matches the XSD oracle's ApplicationClass count (7) per
// PROVENANCE.md.
// Matches the XSD oracle's ApplicationClass count (7).
assert_eq!(values.len(), 7);
}

#[test]
fn parses_fixed_enum_subclass_attribute() {
let TtlDeclaration::DatatypeAttribute(a) = parse_file(APP_SUBCLASS_TTL).expect("parses")
let TtlDeclaration::DatatypeAttribute(a) =
parse_file(&mars("Application/attributes/subClass.ttl")).expect("parses")
else {
panic!("expected datatype attribute");
};
let values = a.fixed_enum_values().expect("fixed enum");
// From the XSD oracle ApplicationSubClass extraction: 50
// distinct (Class, SubClass) PAIRS, but the SubClass *set*
// is 50 values too (verified by the run cached in
// `vocab/imports/ogit/MARS/_oracle/classifications.adoc`).
// `vocab/oracles/mars/classifications.adoc`).
assert_eq!(
values.len(),
50,
Expand All @@ -350,19 +349,20 @@ mod tests {

/// The bijective oracle test: every Application classification
/// in the OGIT TTL is also present in the XSD-extracted set
/// (cached at `_oracle/classifications.adoc`). This is the
/// (cached at `vocab/oracles/mars/classifications.adoc`). This is the
/// chess-grade calibration applied to a frozen schema — same
/// shape as `shakmaty::Position::play` round-tripping for chess.
///
/// We assert membership here; the full bijection (TTL set ==
/// XSD set, no missing, no extra) lives in the calibration
/// doc under `docs/calibration/mars/README.md` so consumers
/// can re-run it manually with `python3 _oracle/extract_classes_py3.py`.
/// can re-run it manually with
/// `python3 vocab/oracles/mars/extract_classes_py3.py`.
#[test]
fn application_class_values_appear_in_xsd_oracle() {
const ORACLE: &str =
include_str!("../../../vocab/imports/ogit/NTO/MARS/_oracle/classifications.adoc");
let TtlDeclaration::DatatypeAttribute(a) = parse_file(APP_CLASS_TTL).expect("parses")
const ORACLE: &str = include_str!("../../../vocab/oracles/mars/classifications.adoc");
let TtlDeclaration::DatatypeAttribute(a) =
parse_file(&mars("Application/attributes/class.ttl")).expect("parses")
else {
panic!("expected datatype attribute");
};
Expand All @@ -377,13 +377,14 @@ mod tests {
#[test]
fn into_class_carries_attribute_set_and_enums() {
use super::super::into_class;
let TtlDeclaration::Entity(machine) = parse_file(MACHINE_TTL).expect("parses") else {
let TtlDeclaration::Entity(machine) =
parse_file(&mars("entities/Machine.ttl")).expect("parses")
else {
panic!("expected entity");
};
let TtlDeclaration::DatatypeAttribute(class_attr) = parse_file(include_str!(
"../../../vocab/imports/ogit/NTO/MARS/Machine/attributes/class.ttl"
))
.expect("parses") else {
let TtlDeclaration::DatatypeAttribute(class_attr) =
parse_file(&mars("Machine/attributes/class.ttl")).expect("parses")
else {
panic!("expected datatype attribute");
};
let attrs = vec![("class", &class_attr)];
Expand Down
Loading
Loading