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:
119
tests/contracts/gate_contract_tests.md
Normal file
119
tests/contracts/gate_contract_tests.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# Gate Contract Tests
|
||||
|
||||
Template for testing any gate implementation. These tests must pass regardless of implementation method (regex, classifier, neural, hybrid).
|
||||
|
||||
---
|
||||
|
||||
## Test format
|
||||
|
||||
```
|
||||
GateContractTest {
|
||||
test_id: string
|
||||
gate_id: string
|
||||
input_text: string
|
||||
expected: {
|
||||
activated: boolean
|
||||
min_confidence: float -- activated=true: confidence must be >= this
|
||||
max_confidence: float -- activated=false: confidence must be <= this
|
||||
}
|
||||
test_class: "positive_recall" | "true_negative" | "boundary" | "idempotency"
|
||||
rationale: string
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `is_math` gate tests
|
||||
|
||||
| test_id | input | expected activated | min_conf | test_class |
|
||||
|---|---|---|---|---|
|
||||
| G_MATH_001 | "Alice had 5 apples. She gave Bob 2." | true | 0.80 | positive_recall |
|
||||
| G_MATH_002 | "What is 3 plus 7?" | true | 0.90 | positive_recall |
|
||||
| G_MATH_003 | "How many apples does she have left?" | true | 0.70 | positive_recall |
|
||||
| G_MATH_004 | "The cat sat on the mat." | false | — | true_negative |
|
||||
| G_MATH_005 | "Alice went to the store." | false | — | true_negative |
|
||||
| G_MATH_006 | "" | false | — | boundary |
|
||||
| G_MATH_007 | "42" | true | 0.70 | boundary |
|
||||
|
||||
---
|
||||
|
||||
## `has_entity_reference` gate tests
|
||||
|
||||
| test_id | input | expected activated | min_conf | test_class |
|
||||
|---|---|---|---|---|
|
||||
| G_ENT_001 | "Alice gave Bob 2 apples." | true | 0.90 | positive_recall |
|
||||
| G_ENT_002 | "She gave him the book." | true | 0.70 | positive_recall |
|
||||
| G_ENT_003 | "Dr. Smith visited the clinic." | true | 0.90 | positive_recall |
|
||||
| G_ENT_004 | "2 plus 3 equals 5." | false | — | true_negative |
|
||||
| G_ENT_005 | "run the program" | false | — | true_negative |
|
||||
| G_ENT_006 | "" | false | — | boundary |
|
||||
|
||||
---
|
||||
|
||||
## `has_ownership_transfer` gate tests
|
||||
|
||||
| test_id | input | expected activated | min_conf | test_class |
|
||||
|---|---|---|---|---|
|
||||
| G_OWN_001 | "Alice gave Bob 2 apples." | true | 0.90 | positive_recall |
|
||||
| G_OWN_002 | "She sold the car to him." | true | 0.90 | positive_recall |
|
||||
| G_OWN_003 | "He received a package." | true | 0.85 | positive_recall |
|
||||
| G_OWN_004 | "The sun rose at 6am." | false | — | true_negative |
|
||||
| G_OWN_005 | "Alice is happy." | false | — | true_negative |
|
||||
| G_OWN_006 | "gave" | true | 0.70 | boundary |
|
||||
|
||||
---
|
||||
|
||||
## `has_temporal_relation` gate tests
|
||||
|
||||
| test_id | input | expected activated | min_conf | test_class |
|
||||
|---|---|---|---|---|
|
||||
| G_TMP_001 | "She arrived after the meeting started." | true | 0.90 | positive_recall |
|
||||
| G_TMP_002 | "Before noon, Alice left." | true | 0.90 | positive_recall |
|
||||
| G_TMP_003 | "Step 1: open the file. Step 2: read it." | true | 0.80 | positive_recall |
|
||||
| G_TMP_004 | "Alice is 5 feet tall." | false | — | true_negative |
|
||||
| G_TMP_005 | "x equals y." | false | — | true_negative |
|
||||
|
||||
---
|
||||
|
||||
## `has_logical_negation` gate tests
|
||||
|
||||
| test_id | input | expected activated | min_conf | test_class |
|
||||
|---|---|---|---|---|
|
||||
| G_NEG_001 | "Alice did not give Bob any apples." | true | 0.90 | positive_recall |
|
||||
| G_NEG_002 | "She never arrived." | true | 0.90 | positive_recall |
|
||||
| G_NEG_003 | "Unless it rains, the picnic continues." | true | 0.85 | positive_recall |
|
||||
| G_NEG_004 | "Alice gave Bob 2 apples." | false | — | true_negative |
|
||||
|
||||
---
|
||||
|
||||
## Idempotency tests
|
||||
|
||||
Every gate must pass these for every gate_id:
|
||||
|
||||
| test_id | procedure | expected |
|
||||
|---|---|---|
|
||||
| G_IDEM_001 | Call gate(X) twice with identical input | Both calls return identical GateSignal |
|
||||
| G_IDEM_002 | Call gate(X) after unrelated text processed | Same result as fresh call |
|
||||
|
||||
---
|
||||
|
||||
## Boundary tests (all gates)
|
||||
|
||||
| test_id | input | expected |
|
||||
|---|---|---|
|
||||
| G_BOUND_001 | "" (empty string) | Valid GateSignal with activated=false, confidence > 0 |
|
||||
| G_BOUND_002 | Single character "a" | Valid GateSignal, no crash |
|
||||
| G_BOUND_003 | 10,000 character string | Valid GateSignal, no crash, returns within 100ms |
|
||||
| G_BOUND_004 | String with only symbols "!@#$%^&*()" | Valid GateSignal, no crash |
|
||||
|
||||
---
|
||||
|
||||
## Recall vs. precision tolerance
|
||||
|
||||
Gates are high-recall by design. The following tolerances apply to implementation evaluation:
|
||||
|
||||
- Recall must be >= 0.90 on positive test cases
|
||||
- Precision on clearly negative test cases must be >= 0.80
|
||||
- Precision on ambiguous test cases: no threshold (false positives are acceptable)
|
||||
|
||||
A gate that achieves 0.95 recall with 0.70 precision is preferred over one with 0.85 recall and 0.95 precision.
|
||||
120
tests/contracts/resolver_contract_tests.md
Normal file
120
tests/contracts/resolver_contract_tests.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Resolver Contract Tests
|
||||
|
||||
Tests that any resolver implementation must pass.
|
||||
|
||||
---
|
||||
|
||||
## RT_001 — Arithmetic resolution
|
||||
```
|
||||
Input WorldState:
|
||||
Objects: Q1 (value=5, unit=apples), Q2 (value=2, unit=apples), Q_result (value=null)
|
||||
Constraints: C1 = quantity_difference(Q_result, Q1, Q2), status=unresolved
|
||||
|
||||
Expected resolved WorldState:
|
||||
Q_result.QuantityFacet.value = 3
|
||||
Q_result.QuantityFacet.is_exact = true
|
||||
Q_result.QuantityFacet.unit = "apples"
|
||||
C1.status = "resolved"
|
||||
resolution_log contains entry for "resolve_arithmetic" affecting Q_result
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## RT_002 — Coreference merge
|
||||
```
|
||||
Input WorldState:
|
||||
Objects: E1 (Alice, confidence=0.97), E_she (She, confidence=0.72)
|
||||
Constraints: C_coref = same_entity(E1, E_she), status=resolved
|
||||
|
||||
Expected resolved WorldState:
|
||||
E_she.status = "merged"
|
||||
E_she.merged_into = "E1"
|
||||
E1.aliases contains "She"
|
||||
All facet_refs and constraint_refs from E_she transferred to E1
|
||||
resolution_log contains entry for "merge_coreference"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## RT_003 — Contradiction detected and logged
|
||||
```
|
||||
Input WorldState:
|
||||
Constraints:
|
||||
C1 = before(EV1, EV2), strength=hard, status=unresolved
|
||||
C2 = before(EV2, EV1), strength=hard, status=unresolved
|
||||
|
||||
Expected:
|
||||
C1.status = "contradicted"
|
||||
C2.status = "contradicted"
|
||||
open_contradictions contains record with constraint_refs = ["C1", "C2"]
|
||||
NO silent resolution
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## RT_004 — Soft constraint suspended by hard conflict
|
||||
```
|
||||
Input WorldState:
|
||||
C1 = quantity_equals(Q1, 5), strength=hard, status=resolved
|
||||
C2 = quantity_equals(Q1, 7), strength=soft, status=unresolved
|
||||
|
||||
Expected:
|
||||
C2.status = "suspended"
|
||||
resolution_log contains "suspend_soft" entry for C2
|
||||
Q1.value = 5 (unchanged)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## RT_005 — No new objects created
|
||||
```
|
||||
Input WorldState: N objects
|
||||
Expected: resolved WorldState has <= N objects (merges reduce count; splits would increase but must be explicitly supported)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## RT_006 — Resolution log completeness
|
||||
```
|
||||
For any non-trivial WorldState:
|
||||
Every object status change must have a log entry
|
||||
Every constraint status change must have a log entry
|
||||
resolution_log must be non-empty
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## RT_007 — Stage check
|
||||
```
|
||||
Input: WorldState with stage = "post_gate"
|
||||
Expected: resolver raises error or returns error state, does not produce resolved output
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## RT_008 — Idempotency
|
||||
```
|
||||
Call resolver(WorldState) → resolved_1
|
||||
Call resolver(resolved_1) → resolved_2
|
||||
Expected: resolved_1 and resolved_2 are logically equivalent (no new changes on second pass)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## RT_009 — Confidence propagation bounds
|
||||
```
|
||||
Given any resolved constraint affecting object O:
|
||||
O.confidence must not increase by more than 0.15 from a single constraint
|
||||
O.confidence must not decrease by more than 0.15 from a single contradiction
|
||||
O.confidence must remain in [0.0, 1.0]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## RT_010 — Resolver interface replaceability
|
||||
```
|
||||
Given a resolver interface: resolve(world_state) → world_state
|
||||
|
||||
Any implementation that passes RT_001 through RT_009 is a valid resolver.
|
||||
The rest of the pipeline must not depend on resolver internals.
|
||||
```
|
||||
135
tests/contracts/specialist_contract_tests.md
Normal file
135
tests/contracts/specialist_contract_tests.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Specialist Contract Tests
|
||||
|
||||
Template for testing any specialist implementation against its contract.
|
||||
|
||||
---
|
||||
|
||||
## Entity Specialist
|
||||
|
||||
### ES_001 — Two named entities detected
|
||||
```
|
||||
Input: "Alice gave Bob 2 apples."
|
||||
Expected:
|
||||
- new_objects contains at least two entities
|
||||
- E_alice.canonical_label = "Alice"
|
||||
- E_bob.canonical_label = "Bob"
|
||||
- Constraint not_same_entity(E_alice, E_bob) emitted
|
||||
- All objects have source_module = "entity"
|
||||
```
|
||||
|
||||
### ES_002 — Coreference resolved
|
||||
```
|
||||
Input: "Alice arrived. She sat down."
|
||||
WorldState contains E_alice from prior proposal.
|
||||
Expected:
|
||||
- same_entity(E_alice, E_she) constraint emitted
|
||||
- Confidence >= 0.80
|
||||
```
|
||||
|
||||
### ES_003 — Domain adherence
|
||||
```
|
||||
Input: "Alice gave Bob 5 apples."
|
||||
Expected:
|
||||
- No quantity facets emitted
|
||||
- No temporal constraints emitted
|
||||
- Only syntactic/semantic facets and entity constraints
|
||||
```
|
||||
|
||||
### ES_004 — Budget respected
|
||||
```
|
||||
BudgetSpec: max_constraints = 3
|
||||
Input: very long text with many entities
|
||||
Expected:
|
||||
- len(new_constraints) <= 3
|
||||
```
|
||||
|
||||
### ES_005 — Valid refs
|
||||
```
|
||||
Expected for any input:
|
||||
- All argument_refs in constraints resolve to IDs in new_objects or input WorldState.objects
|
||||
- No dangling refs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quantity Specialist
|
||||
|
||||
### QS_001 — Exact arithmetic resolved inline
|
||||
```
|
||||
Input context: Q1.value=5, Q2.value=2
|
||||
Constraint: quantity_difference(Q_result, Q1, Q2) with status=unresolved
|
||||
Expected:
|
||||
- Q_result QuantityFacet.value = 3
|
||||
- Q_result QuantityFacet.is_exact = true
|
||||
- Constraint status = resolved
|
||||
```
|
||||
|
||||
### QS_002 — Units propagated
|
||||
```
|
||||
Input: Q1.unit="apples", Q2.unit="apples"
|
||||
Expected:
|
||||
- Q_result.unit = "apples"
|
||||
```
|
||||
|
||||
### QS_003 — Domain adherence
|
||||
```
|
||||
Input: any text
|
||||
Expected:
|
||||
- No temporal facets emitted
|
||||
- No entity constraints emitted
|
||||
- Only quantity facets and quantity constraints
|
||||
```
|
||||
|
||||
### QS_004 — Unresolvable emits unresolved constraint
|
||||
```
|
||||
Input context: Q1.value=5, Q2.value=null (not yet known)
|
||||
Expected:
|
||||
- quantity_difference(Q_result, Q1, Q2) emitted with status=unresolved
|
||||
- Q_result.value = null
|
||||
- No error, no crash
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Temporal Specialist
|
||||
|
||||
### TS_001 — Explicit ordering
|
||||
```
|
||||
Input: "Alice arrived before the meeting started."
|
||||
Expected:
|
||||
- before(EV_arrival, EV_meeting) emitted with strength=hard
|
||||
- Both objects referenced exist in WorldState or new_objects
|
||||
```
|
||||
|
||||
### TS_002 — Implicit sequence
|
||||
```
|
||||
Input: "The light turned green. The car moved."
|
||||
Expected:
|
||||
- before(EV_light_green, EV_car_move) emitted with strength=soft
|
||||
```
|
||||
|
||||
### TS_003 — Time point attachment
|
||||
```
|
||||
Input: "The meeting started at 9am."
|
||||
Expected:
|
||||
- TemporalFacet on EV_meeting with time_point set
|
||||
- starts_at(EV_meeting, T_9am) constraint emitted
|
||||
```
|
||||
|
||||
### TS_004 — Domain adherence
|
||||
```
|
||||
Expected: no quantity constraints, no entity constraints emitted
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cross-specialist invariants (apply to all specialists)
|
||||
|
||||
| invariant | check |
|
||||
|---|---|
|
||||
| Bounded output | len(new_constraints) <= BudgetSpec.max_constraints |
|
||||
| Valid refs | all argument_refs resolve |
|
||||
| Source module | all records have correct source_module |
|
||||
| Provenance | len(provenance) >= len(new_facets) + len(new_constraints) |
|
||||
| No WorldState mutation | input WorldState identical before and after call |
|
||||
| Schema compliance | all outputs validate against schemas/facets.json and schemas/constraints.json |
|
||||
436
tests/test_types.cpp
Normal file
436
tests/test_types.cpp
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user