///|
fn indices_json(values : Array[Int32]) -> Json {
Json::array(values.map(fn(i) { Json::number(i.to_double()) }))
}
///|
fn optional_number(value : Double?) -> Json {
match value {
Some(n) => Json::number(n)
None => Json::null()
}
}
///|
fn metrics_json(metrics : RuleMetrics) -> Json {
Json::object({
"objects": Json::number(metrics.object_count.to_double()),
"premise_count": Json::number(metrics.premise_count.to_double()),
"conclusion_count": Json::number(metrics.conclusion_count.to_double()),
"joint_count": Json::number(metrics.joint_count.to_double()),
"counterexamples": Json::number(metrics.counterexample_count.to_double()),
"support": Json::number(metrics.support),
"confidence": optional_number(metrics.confidence),
"lift": optional_number(metrics.lift),
"leverage": Json::number(metrics.leverage),
})
}
///|
/// Indexed output keeps vocabularies once rather than repeating potentially large labels.
pub fn Context::concept_report(
self : Context,
limit~ : Int32 = 4096,
) -> Json raise FcaError {
let nodes = self
.concepts(limit~)
.map(concept => {
Json::object({
"extent": indices_json(concept.extent),
"intent": indices_json(concept.intent),
"support": Json::number(self.support(concept.intent)),
})
})
Json::object({
"schema": Json::string("moonconcept-concepts/v1"),
"context": self.to_json(),
"concepts": Json::array(nodes),
})
}
///|
pub fn Context::lattice_report(
self : Context,
limit~ : Int32 = 256,
) -> Json raise FcaError {
let lattice = self.lattice(limit~)
let nodes : Array[Json] = []
for i = 0; i < lattice.size(); i = i + 1 {
let concept = lattice.concept(i)
nodes.push(
Json::object({
"id": Json::number(i.to_double()),
"extent": indices_json(concept.extent),
"intent": indices_json(concept.intent),
}),
)
}
let covers = lattice
.covers()
.map(fn(edge) {
Json::array([
Json::number(edge.lower.to_double()),
Json::number(edge.upper.to_double()),
])
})
Json::object({
"schema": Json::string("moonconcept-lattice/v1"),
"order": Json::string("extent-subset"),
"context": self.to_json(),
"nodes": Json::array(nodes),
"covers": Json::array(covers),
})
}
///|
/// Safe numeric DOT labels, directed bottom -> top in extent inclusion order.
/// Human label vocabulary remains in the companion lattice_report, never interpolated into DOT.
pub fn Lattice::to_dot(self : Lattice) -> String {
let lines = ["digraph ConceptLattice {", " rankdir=BT;"]
for i = 0; i < self.nodes.length(); i = i + 1 {
let node = self.nodes[i]
let intent = node.intent.map(fn(a) { a.to_string() }).join(",")
lines.push(
" n\{i} [label=\"#\{i} | extent=\{node.extent.length()} | intent={\{intent}}\"];",
)
}
for edge in self.edges {
lines.push(" n\{edge.lower} -> n\{edge.upper};")
}
lines.push("}")
lines.join("\n") + "\n"
}
///|
/// Every supplied rule is audited, including vacuously valid rules with undefined confidence.
pub fn RuleSystem::audit(
self : RuleSystem,
observed : Context,
) -> Json raise FcaError {
if self.attribute_count() != observed.attribute_count() {
raise Invalid("audit vocabulary dimensions differ")
}
let entries = self
.implications()
.map(rule => {
Json::object({
"rule": implication_json(rule),
"valid": Json::boolean(observed.valid_implication(rule)),
"witnesses": indices_json(observed.counterexamples(rule)),
"metrics": metrics_json(observed.rule_metrics(rule)),
})
})
Json::object({
"schema": Json::string("moonconcept-rule-audit/v1"),
"context": observed.to_json(),
"rules": Json::array(entries),
})
}
///|
/// Explanations identify source rule indices and only newly inferred attributes.
pub fn RuleSystem::inference_report(
self : RuleSystem,
seed : Array[Int32],
) -> Json raise FcaError {
let seed = checked_indices(seed, self.attribute_count())
let trace = self.trace(seed)
let steps = trace.steps.map(fn(step) {
Json::object({
"rule_index": Json::number(step.rule_index.to_double()),
"added": indices_json(step.added),
})
})
Json::object({
"schema": Json::string("moonconcept-inference/v1"),
"theory": self.to_json(),
"seed": indices_json(seed),
"result": indices_json(trace.result),
"steps": Json::array(steps),
})
}