Sprint 1: project skeleton, type system, and all architecture specs

- src/types.hpp: complete UCWM type system in C++20 — 19 enums, 11 facet
  data types, all core structs (CanonicalObject, Constraint, Facet,
  GateSignal, WorldState, etc.) with full JSON round-trip serialization
- src/main.cpp: smoke test — constructs apple-problem WorldState by hand,
  serializes to JSON
- tests/test_types.cpp: 19 tests, 123 assertions, all passing
- CMakeLists.txt: CMake + CPM build with nlohmann/json, spdlog, Catch2
- schemas/: JSON Schema contracts for all UCWM data types
- gates/, specialists/, resolver/, synthesis/: language-agnostic interface
  contracts and domain specs for all pipeline layers
- docs/: architecture, vocabulary, decision matrices, roadmap (6 phases,
  28 sprints), sprint_001, implementation_constraints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 16:09:55 -07:00
commit b758d7ea60
35 changed files with 6344 additions and 0 deletions

436
tests/test_types.cpp Normal file
View File

@@ -0,0 +1,436 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "types.hpp"
using namespace ucwm;
using nlohmann::json;
// ─────────────────────────────────────────────────────────────────────────────
// CanonicalObject
// ─────────────────────────────────────────────────────────────────────────────
TEST_CASE("CanonicalObject — construction and field access", "[types][object]") {
CanonicalObject obj;
obj.object_id = "E1";
obj.object_kind = ObjectKind::Entity;
obj.status = ObjectStatus::Active;
obj.confidence = 0.97;
obj.canonical_label = "Alice";
obj.aliases = {"she", "her"};
obj.surface_span = SurfaceSpan{"Alice", 0, 5};
obj.source_module = "entity_specialist";
REQUIRE(obj.object_id == "E1");
REQUIRE(obj.object_kind == ObjectKind::Entity);
REQUIRE(obj.status == ObjectStatus::Active);
REQUIRE_THAT(obj.confidence, Catch::Matchers::WithinAbs(0.97, 1e-9));
REQUIRE(obj.canonical_label.has_value());
REQUIRE(*obj.canonical_label == "Alice");
REQUIRE(obj.aliases.size() == 2);
REQUIRE(obj.surface_span.has_value());
REQUIRE(obj.surface_span->start == 0);
REQUIRE(obj.surface_span->end == 5);
REQUIRE_FALSE(obj.merged_into.has_value());
REQUIRE(obj.facet_refs.empty());
}
TEST_CASE("CanonicalObject — absent optionals stay absent", "[types][object]") {
CanonicalObject obj;
obj.object_id = "E2";
obj.object_kind = ObjectKind::Event;
obj.confidence = 0.5;
REQUIRE_FALSE(obj.surface_span.has_value());
REQUIRE_FALSE(obj.canonical_label.has_value());
REQUIRE_FALSE(obj.merged_into.has_value());
REQUIRE_FALSE(obj.split_from.has_value());
REQUIRE_FALSE(obj.source_module.has_value());
}
TEST_CASE("CanonicalObject — JSON round-trip", "[types][object][json]") {
CanonicalObject original;
original.object_id = "E1";
original.object_kind = ObjectKind::Entity;
original.status = ObjectStatus::Active;
original.confidence = 0.97;
original.canonical_label = "Alice";
original.aliases = {"she", "her"};
original.surface_span = SurfaceSpan{"Alice", 0, 5};
original.facet_refs = {"F1", "F2"};
original.source_module = "entity_specialist";
json j = original;
CanonicalObject restored = j.get<CanonicalObject>();
REQUIRE(restored.object_id == original.object_id);
REQUIRE(restored.object_kind == original.object_kind);
REQUIRE(restored.status == original.status);
REQUIRE_THAT(restored.confidence, Catch::Matchers::WithinAbs(original.confidence, 1e-9));
REQUIRE(restored.canonical_label == original.canonical_label);
REQUIRE(restored.aliases == original.aliases);
REQUIRE(restored.surface_span.has_value());
REQUIRE(restored.surface_span->text == "Alice");
REQUIRE(restored.facet_refs == original.facet_refs);
REQUIRE(restored.source_module == original.source_module);
}
TEST_CASE("CanonicalObject — absent optional not written to JSON", "[types][object][json]") {
CanonicalObject obj;
obj.object_id = "E3";
obj.object_kind = ObjectKind::Quantity;
obj.confidence = 0.99;
json j = obj;
REQUIRE_FALSE(j.contains("surface_span"));
REQUIRE_FALSE(j.contains("canonical_label"));
REQUIRE_FALSE(j.contains("merged_into"));
}
// ─────────────────────────────────────────────────────────────────────────────
// Constraint
// ─────────────────────────────────────────────────────────────────────────────
TEST_CASE("Constraint — construction and field access", "[types][constraint]") {
Constraint c;
c.constraint_id = "C1";
c.constraint_type = "quantity_difference";
c.argument_refs = {"Q_result", "Q1", "Q2"};
c.polarity = Polarity::Positive;
c.strength = ConstraintStrength::Hard;
c.status = ConstraintStatus::Unresolved;
c.expression = "quantity(Q_result) = quantity(Q1) - quantity(Q2)";
c.confidence = 0.99;
c.source_module = "quantity_specialist";
REQUIRE(c.constraint_id == "C1");
REQUIRE(c.constraint_type == "quantity_difference");
REQUIRE(c.argument_refs.size() == 3);
REQUIRE(c.strength == ConstraintStrength::Hard);
REQUIRE(c.status == ConstraintStatus::Unresolved);
REQUIRE(c.expression.has_value());
REQUIRE(c.confidence.has_value());
REQUIRE_FALSE(c.probability.has_value());
}
TEST_CASE("Constraint — JSON round-trip", "[types][constraint][json]") {
Constraint original;
original.constraint_id = "C3";
original.constraint_type = "before";
original.argument_refs = {"EV1", "EV2"};
original.polarity = Polarity::Positive;
original.strength = ConstraintStrength::Soft;
original.status = ConstraintStatus::Resolved;
original.source_module = "temporal_specialist";
original.expression = "before(EV1, EV2)";
json j = original;
Constraint restored = j.get<Constraint>();
REQUIRE(restored.constraint_id == original.constraint_id);
REQUIRE(restored.constraint_type == original.constraint_type);
REQUIRE(restored.argument_refs == original.argument_refs);
REQUIRE(restored.polarity == original.polarity);
REQUIRE(restored.strength == original.strength);
REQUIRE(restored.status == original.status);
REQUIRE(restored.expression == original.expression);
}
// ─────────────────────────────────────────────────────────────────────────────
// Facets and FacetData variant
// ─────────────────────────────────────────────────────────────────────────────
TEST_CASE("Facet — TemporalFacetData construction and kind detection", "[types][facet]") {
TemporalFacetData data;
data.time_point = "t0";
data.temporal_order_refs = {"C_temporal"};
data.is_recurring = false;
Facet f;
f.facet_id = "F_EV_temporal";
f.object_ref = "EV_initial";
f.confidence = 0.90;
f.source_module = "temporal_specialist";
f.data = data;
REQUIRE(kind_of(f.data) == FacetKind::Temporal);
REQUIRE(std::holds_alternative<TemporalFacetData>(f.data));
auto& d = std::get<TemporalFacetData>(f.data);
REQUIRE(d.time_point.has_value());
REQUIRE(*d.time_point == "t0");
REQUIRE(d.temporal_order_refs.size() == 1);
REQUIRE_FALSE(d.is_recurring);
}
TEST_CASE("Facet — QuantityFacetData with numeric value", "[types][facet]") {
QuantityFacetData data;
data.value = 5.0;
data.unit = "apples";
data.is_exact = true;
Facet f;
f.facet_id = "F_Q1";
f.object_ref = "Q1";
f.confidence = 0.99;
f.source_module = "quantity_specialist";
f.data = data;
REQUIRE(kind_of(f.data) == FacetKind::Quantity);
auto& d = std::get<QuantityFacetData>(f.data);
REQUIRE(d.value.has_value());
REQUIRE(std::holds_alternative<double>(*d.value));
REQUIRE_THAT(std::get<double>(*d.value), Catch::Matchers::WithinAbs(5.0, 1e-9));
REQUIRE(*d.unit == "apples");
REQUIRE(d.is_exact);
}
TEST_CASE("Facet — QuantityFacetData with symbolic value", "[types][facet]") {
QuantityFacetData data;
data.value = std::string{"Q1 - Q2"};
data.unit = "apples";
data.is_exact = false;
Facet f;
f.facet_id = "F_Q_final";
f.object_ref = "Q_alice_final";
f.confidence = 0.90;
f.source_module = "quantity_specialist";
f.data = data;
auto& d = std::get<QuantityFacetData>(f.data);
REQUIRE(std::holds_alternative<std::string>(*d.value));
REQUIRE(std::get<std::string>(*d.value) == "Q1 - Q2");
}
TEST_CASE("Facet — QuantityFacetData with null value", "[types][facet]") {
QuantityFacetData data;
data.unit = "apples";
data.is_exact = false;
// value intentionally left empty
Facet f;
f.facet_id = "F_Q_unresolved";
f.object_ref = "Q_alice_final";
f.confidence = 0.80;
f.source_module = "quantity_specialist";
f.data = data;
auto& d = std::get<QuantityFacetData>(f.data);
REQUIRE_FALSE(d.value.has_value());
}
TEST_CASE("Facet — JSON round-trip with TemporalFacetData", "[types][facet][json]") {
TemporalFacetData data;
data.time_point = "t1";
data.temporal_order_refs = {"C1", "C2"};
data.is_recurring = false;
Facet original;
original.facet_id = "F_temporal";
original.object_ref = "EV_transfer";
original.confidence = 0.88;
original.source_module = "temporal_specialist";
original.data = data;
json j = original;
REQUIRE(j["facet_kind"] == "temporal");
REQUIRE(j["facet_id"] == "F_temporal");
REQUIRE(j["time_point"] == "t1");
Facet restored = j.get<Facet>();
REQUIRE(restored.facet_id == original.facet_id);
REQUIRE(kind_of(restored.data) == FacetKind::Temporal);
auto& d = std::get<TemporalFacetData>(restored.data);
REQUIRE(*d.time_point == "t1");
REQUIRE(d.temporal_order_refs.size() == 2);
}
TEST_CASE("Facet — JSON round-trip with QuantityFacetData numeric value", "[types][facet][json]") {
QuantityFacetData data;
data.value = 3.0;
data.unit = "apples";
data.is_exact = true;
Facet original;
original.facet_id = "F_Q_resolved";
original.object_ref = "Q_alice_final";
original.confidence = 0.99;
original.source_module = "quantity_specialist";
original.data = data;
json j = original;
REQUIRE(j["facet_kind"] == "quantity");
REQUIRE(j["value"] == 3.0);
REQUIRE(j["unit"] == "apples");
REQUIRE(j["is_exact"] == true);
Facet restored = j.get<Facet>();
auto& d = std::get<QuantityFacetData>(restored.data);
REQUIRE(std::holds_alternative<double>(*d.value));
REQUIRE_THAT(std::get<double>(*d.value), Catch::Matchers::WithinAbs(3.0, 1e-9));
REQUIRE(*d.unit == "apples");
REQUIRE(d.is_exact);
}
// ─────────────────────────────────────────────────────────────────────────────
// GateSignal
// ─────────────────────────────────────────────────────────────────────────────
TEST_CASE("GateSignal — construction", "[types][gate]") {
GateSignal g;
g.gate_id = "has_quantity";
g.activated = true;
g.confidence = 0.99;
g.evidence_spans = {EvidenceSpan{"5 apples", 10, 18}};
g.method = GateMethod::Rule;
REQUIRE(g.gate_id == "has_quantity");
REQUIRE(g.activated);
REQUIRE_THAT(g.confidence, Catch::Matchers::WithinAbs(0.99, 1e-9));
REQUIRE(g.evidence_spans.size() == 1);
REQUIRE(g.evidence_spans[0].text == "5 apples");
REQUIRE(g.method.has_value());
REQUIRE(*g.method == GateMethod::Rule);
}
TEST_CASE("GateSignal — JSON round-trip", "[types][gate][json]") {
GateSignal original;
original.gate_id = "has_entity_reference";
original.activated = true;
original.confidence = 0.95;
original.method = GateMethod::Rule;
original.evidence_spans = {EvidenceSpan{"Alice", 0, 5}, EvidenceSpan{"Bob", 29, 32}};
json j = original;
GateSignal restored = j.get<GateSignal>();
REQUIRE(restored.gate_id == original.gate_id);
REQUIRE(restored.activated == original.activated);
REQUIRE_THAT(restored.confidence, Catch::Matchers::WithinAbs(original.confidence, 1e-9));
REQUIRE(restored.evidence_spans.size() == 2);
REQUIRE(restored.evidence_spans[0].text == "Alice");
REQUIRE(restored.method.has_value());
REQUIRE(*restored.method == GateMethod::Rule);
}
// ─────────────────────────────────────────────────────────────────────────────
// WorldState
// ─────────────────────────────────────────────────────────────────────────────
TEST_CASE("WorldState — construction with objects", "[types][worldstate]") {
WorldState ws;
ws.state_id = "ws_001";
ws.stage = WorldStateStage::PostProposal;
ws.input_text = "Alice had 5 apples.";
CanonicalObject alice;
alice.object_id = "E1";
alice.object_kind = ObjectKind::Entity;
alice.confidence = 0.97;
ws.objects["E1"] = alice;
CanonicalObject q1;
q1.object_id = "Q1";
q1.object_kind = ObjectKind::Quantity;
q1.confidence = 0.99;
ws.objects["Q1"] = q1;
REQUIRE(ws.objects.size() == 2);
REQUIRE(ws.objects.count("E1") == 1);
REQUIRE(ws.objects.at("E1").object_kind == ObjectKind::Entity);
REQUIRE(ws.facets.empty());
REQUIRE(ws.constraints.empty());
REQUIRE(ws.input_text.has_value());
REQUIRE(*ws.input_text == "Alice had 5 apples.");
REQUIRE_FALSE(ws.synthesis_answer.has_value());
}
TEST_CASE("WorldState — JSON round-trip", "[types][worldstate][json]") {
WorldState ws;
ws.state_id = "ws_roundtrip";
ws.stage = WorldStateStage::PostProposal;
ws.input_text = "Alice had 5 apples.";
CanonicalObject alice;
alice.object_id = "E1";
alice.object_kind = ObjectKind::Entity;
alice.status = ObjectStatus::Active;
alice.confidence = 0.97;
alice.canonical_label = "Alice";
alice.source_module = "entity_specialist";
ws.objects["E1"] = alice;
Facet fq;
fq.facet_id = "F_Q1";
fq.object_ref = "Q1";
fq.confidence = 0.99;
fq.source_module = "quantity_specialist";
QuantityFacetData qd;
qd.value = 5.0;
qd.unit = "apples";
qd.is_exact = true;
fq.data = qd;
ws.facets["F_Q1"] = fq;
json j = ws;
WorldState restored = j.get<WorldState>();
REQUIRE(restored.state_id == ws.state_id);
REQUIRE(restored.stage == ws.stage);
REQUIRE(restored.input_text == ws.input_text);
REQUIRE(restored.objects.size() == 1);
REQUIRE(restored.objects.count("E1") == 1);
REQUIRE(restored.objects.at("E1").canonical_label == alice.canonical_label);
REQUIRE(restored.facets.size() == 1);
REQUIRE(restored.facets.count("F_Q1") == 1);
REQUIRE(kind_of(restored.facets.at("F_Q1").data) == FacetKind::Quantity);
}
TEST_CASE("WorldState — stage enum JSON values", "[types][worldstate][json]") {
WorldState ws;
ws.state_id = "ws_stage_test";
ws.stage = WorldStateStage::PostGate;
REQUIRE(json(ws)["stage"] == "post_gate");
ws.stage = WorldStateStage::Resolved;
REQUIRE(json(ws)["stage"] == "resolved");
ws.stage = WorldStateStage::Synthesized;
REQUIRE(json(ws)["stage"] == "synthesized");
}
// ─────────────────────────────────────────────────────────────────────────────
// Enum serialization spot checks
// ─────────────────────────────────────────────────────────────────────────────
TEST_CASE("Enum JSON values match schema strings", "[types][enum][json]") {
REQUIRE(json(ObjectKind::Entity) == "entity");
REQUIRE(json(ObjectKind::RelationInstance) == "relation_instance");
REQUIRE(json(ConstraintStrength::Probabilistic) == "probabilistic");
REQUIRE(json(ConstraintStatus::Contradicted) == "contradicted");
REQUIRE(json(Polarity::Negative) == "negative");
REQUIRE(json(TruthStatus::True_) == "true");
REQUIRE(json(TruthStatus::False_) == "false");
REQUIRE(json(TransferType::Initial) == "initial");
REQUIRE(json(ExecutionMode::ConflictChecking) == "conflict_checking");
REQUIRE(json(ErrorKind::InsufficientData) == "insufficient_data");
}
TEST_CASE("SpecialistError — construction and JSON round-trip", "[types][error][json]") {
SpecialistError err;
err.error_kind = ErrorKind::Ambiguous;
err.affected_refs = {"E1", "E2"};
err.description = "Could not resolve coreference between E1 and E2";
json j = err;
REQUIRE(j["error_kind"] == "ambiguous");
SpecialistError restored = j.get<SpecialistError>();
REQUIRE(restored.error_kind == ErrorKind::Ambiguous);
REQUIRE(restored.affected_refs.size() == 2);
REQUIRE(restored.description == err.description);
}