From b758d7ea60e2b522651e3af60f176a84d858f77e Mon Sep 17 00:00:00 2001 From: bill Date: Fri, 1 May 2026 16:09:55 -0700 Subject: [PATCH] Sprint 1: project skeleton, type system, and all architecture specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .gitignore | 4 + CMakeLists.txt | 48 + docs/architecture.md | 222 +++++ docs/decision_matrices.md | 165 ++++ docs/implementation_constraints.md | 279 ++++++ docs/roadmap.md | 140 +++ docs/sprint_001.md | 204 ++++ docs/vocabulary.md | 90 ++ examples/temporal_001.json | 166 ++++ examples/word_problem_001.json | 296 ++++++ gates/contract.md | 112 +++ gates/registry.md | 70 ++ gates/simple_rules.md | 223 +++++ resolver/contract.md | 133 +++ resolver/operations.md | 256 +++++ schemas/constraints.json | 155 +++ schemas/facets.json | 331 +++++++ schemas/objects.json | 128 +++ schemas/provenance.json | 80 ++ schemas/routing.json | 196 ++++ schemas/world_state.json | 98 ++ specialists/contract.md | 136 +++ specialists/entity.md | 146 +++ specialists/logic.md | 123 +++ specialists/quantity.md | 132 +++ specialists/spatial.md | 107 ++ specialists/temporal.md | 117 +++ src/main.cpp | 118 +++ src/types.hpp | 980 +++++++++++++++++++ synthesis/contract.md | 123 +++ synthesis/language_bindings.md | 156 +++ tests/contracts/gate_contract_tests.md | 119 +++ tests/contracts/resolver_contract_tests.md | 120 +++ tests/contracts/specialist_contract_tests.md | 135 +++ tests/test_types.cpp | 436 +++++++++ 35 files changed, 6344 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 docs/architecture.md create mode 100644 docs/decision_matrices.md create mode 100644 docs/implementation_constraints.md create mode 100644 docs/roadmap.md create mode 100644 docs/sprint_001.md create mode 100644 docs/vocabulary.md create mode 100644 examples/temporal_001.json create mode 100644 examples/word_problem_001.json create mode 100644 gates/contract.md create mode 100644 gates/registry.md create mode 100644 gates/simple_rules.md create mode 100644 resolver/contract.md create mode 100644 resolver/operations.md create mode 100644 schemas/constraints.json create mode 100644 schemas/facets.json create mode 100644 schemas/objects.json create mode 100644 schemas/provenance.json create mode 100644 schemas/routing.json create mode 100644 schemas/world_state.json create mode 100644 specialists/contract.md create mode 100644 specialists/entity.md create mode 100644 specialists/logic.md create mode 100644 specialists/quantity.md create mode 100644 specialists/spatial.md create mode 100644 specialists/temporal.md create mode 100644 src/main.cpp create mode 100644 src/types.hpp create mode 100644 synthesis/contract.md create mode 100644 synthesis/language_bindings.md create mode 100644 tests/contracts/gate_contract_tests.md create mode 100644 tests/contracts/resolver_contract_tests.md create mode 100644 tests/contracts/specialist_contract_tests.md create mode 100644 tests/test_types.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b7d4e25 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +build/ +.cache/ +*.o +*.a diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..a64fdf5 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,48 @@ +cmake_minimum_required(VERSION 3.20) +project(ucwm VERSION 0.1.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +# Auto-download CPM into the build directory +set(CPM_DOWNLOAD_VERSION 0.40.0) +if(NOT EXISTS "${CMAKE_CURRENT_BINARY_DIR}/CPM.cmake") + file(DOWNLOAD + "https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/CPM.cmake" + SHOW_PROGRESS + ) +endif() +include("${CMAKE_CURRENT_BINARY_DIR}/CPM.cmake") + +CPMAddPackage("gh:nlohmann/json@3.11.3") +CPMAddPackage("gh:gabime/spdlog@1.13.0") +CPMAddPackage( + NAME Catch2 + VERSION 3.5.4 + GITHUB_REPOSITORY catchorg/Catch2 +) + +list(APPEND CMAKE_MODULE_PATH "${Catch2_SOURCE_DIR}/extras") + +# Interface library — everything links against this +add_library(ucwm_types INTERFACE) +target_include_directories(ucwm_types INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/src") +target_link_libraries(ucwm_types INTERFACE nlohmann_json::nlohmann_json spdlog::spdlog) +target_compile_features(ucwm_types INTERFACE cxx_std_20) + +# Main executable +add_executable(ucwm src/main.cpp) +target_link_libraries(ucwm PRIVATE ucwm_types) + +# Tests +enable_testing() +add_executable(ucwm_tests tests/test_types.cpp) +target_link_libraries(ucwm_tests PRIVATE ucwm_types Catch2::Catch2WithMain) +include(Catch) +catch_discover_tests(ucwm_tests) diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..af07d76 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,222 @@ +# UCWM Architecture — Unified Constraint World Model + +## Core thesis + +Current LLMs mostly do: + + tokens → latent vectors → tokens + +UCWM does: + + tokens → objects → facets → constraints → resolved state → answer + +Language is messy at the surface. Much of reasoning is not inherently messy once input has been decomposed into typed structures. UCWM uses neural perception where input is messy, and structured/deterministic/specialist systems once state has been converted into contract-based representations. + +**Central invariant:** tokens are not meaning. Tokens are *evidence pointers* toward entities, events, concepts, propositions, relations, quantities, and procedures. The architecture moves away from raw tokens as soon as practical. + +--- + +## Pipeline overview + +``` +User input + │ + ▼ +Relation Gate Array ← small recognizers, high-recall, many parallel + │ + ▼ +Perception / Object Proposal ← broad model, emits CanonicalObject proposals + │ + ▼ +Composite Routing Decision ← relay decides which specialists run, on what, with what budget + │ + ▼ +Specialists (parallel/seq) ← each attaches typed Facets + Constraints to objects + │ + ▼ +UCWM Resolver ← merges, detects contradictions, stabilizes world state + │ + ▼ +Synthesis ← produces answer from resolved state, not from token patterns +``` + +--- + +## Component responsibilities + +### 1. Relation Gate Array + +A set of small, independent recognizers that detect which relation families are probably relevant to the input. Gates are the cheapest possible front-end filter. + +**Key property:** high-recall over high-precision. Activating an unnecessary specialist costs budget. Missing a needed one corrupts the answer. + +Gate outputs are `GateSignal` objects (see `schemas/routing.json`). They do not solve anything. They only inform the router. + +Standard gates (not exhaustive): + +| Gate ID | Detects | +|---|---| +| `is_math` | Numeric reasoning needed | +| `is_code` | Code generation or analysis | +| `has_temporal_relation` | Before/after/during/recurring | +| `has_spatial_relation` | Location, containment, proximity | +| `has_causal_relation` | Cause/effect/enabling/blocking | +| `has_entity_reference` | Named or implied entities | +| `has_coreference` | Pronoun or alias chains | +| `has_quantity` | Counts, amounts, measures | +| `has_logical_negation` | Not, unless, except | +| `has_comparison` | More/less/same/different | +| `has_planning` | Steps, goals, prerequisites | +| `has_constraint` | Requirements, restrictions | +| `has_state_change` | Possession, status transitions | +| `has_ownership_transfer` | Give, sell, take, lose | +| `has_social_intent` | Belief, desire, obligation, deception | +| `requires_external_knowledge` | World facts not in input | +| `requires_synthesis_only` | No structure needed, direct answer | + +Gates may be implemented as: regex rules, feature classifiers, small neural nets, or hybrids. The interface contract is identical regardless of implementation. + +--- + +### 2. Router / Relay (RSA) + +The relay (Relay Specialist Architecture) consumes gate outputs and initial extraction signals, then emits a `RoutingDecision` (see `schemas/routing.json`). + +A `RoutingDecision` activates multiple specialists for the same target simultaneously. The relay is a *dispatcher*, not a solver. RSA is the routing mechanism; UCWM is the constraint-based reasoning substrate. + +Relay responsibilities: +- Which specialists should run +- Which objects or spans they should inspect +- Budget per specialist +- Execution mode (parallel, sequential, iterative, conflict-checking) +- Priority order for constraint merging + +--- + +### 3. Canonical Object Layer + +A `CanonicalObject` (see `schemas/objects.json`) is the primary reusable referent inside UCWM. + +Objects are stable anchors. They are not overloaded with every possible field. Typed descriptions attach to objects via Facets. + +Object kinds: `entity`, `event`, `concept`, `proposition`, `relation_instance`, `procedure_step`, `code_object`, `quantity`, `claim`, `state` + +--- + +### 4. Facet Bundles + +A single object can carry multiple typed descriptions simultaneously. This is a `FacetBundle`. + +Facets are optional and additive. Not every object exists in time or space. A physical entity may have temporal and spatial facets. An abstract concept may not. A proposition may have logical facets but no spatial facet. + +Facet kinds: `temporal`, `spatial`, `causal`, `logical`, `syntactic`, `semantic`, `social`, `ownership`, `code_structure`, `quantity`, `planning` + +See `schemas/facets.json` for schemas of each type. + +--- + +### 5. Schema Plurality + +The same underlying thing may be describable by multiple schemas simultaneously. An event could simultaneously be: +- A predicate-argument structure +- A temporal interval +- A causal node +- A state transition +- A social action + +UCWM does not force one schema too early. Multiple specialists attach schema-shaped facets to the same canonical object. The resolver reconciles them. + +--- + +### 6. Constraint System + +Constraints are the core reasoning currency (see `schemas/constraints.json`). + +A constraint is an explicit relation, restriction, incompatibility, requirement, or preference involving objects and/or facets. + +Strength levels: `hard`, `soft`, `probabilistic`, `defeasible` + +Status lifecycle: `unresolved` → `resolved` | `contradicted` | `suspended` + +Examples: +``` +before(E1, E2) +causes(E1, E3) +same_entity(O4, O9) +quantity(Q1) = quantity(Q2) - 3 +truth(P1) = false +inside(O1, O2) +owner(O3) = Entity_Alice +``` + +--- + +### 7. UCWM Resolver + +The resolver maintains `WorldState` and stabilizes it by: +- Combining constraints from multiple specialists +- Detecting contradictions +- Reinforcing agreements +- Maintaining multiple hypotheses when ambiguous +- Resolving object merges and splits +- Reducing uncertainty to a stabilized state for synthesis + +See `resolver/contract.md` and `resolver/operations.md`. + +--- + +### 8. Specialists + +Specialists are contract-bound modules that attach `Facet` and `Constraint` objects to `CanonicalObject` refs. They may be neural, statistical, symbolic, deterministic, graph-based, or hybrid. + +**Key rule:** specialists emit bounded structured outputs, not unconstrained free text. + +See `specialists/contract.md` for the interface and individual specialist files for domain logic. + +--- + +## Contractual layers and testability + +Every layer has a typed boundary that can be independently tested: + +| Layer | Input | Output | +|---|---|---| +| Gate | text span | `GateSignal[]` | +| Object proposal | text + gate signals | `CanonicalObject[]` | +| Routing | objects + gate signals | `RoutingDecision` | +| Specialist | object refs + routing context | `Facet[]` + `Constraint[]` | +| Resolver | `WorldState` | resolved `WorldState` | +| Synthesis | resolved state + query | answer + derivation trace | + +This enables unit tests, regression tests, swappable implementations, local debugging, and failure localization — advantages not available with monolithic LLMs. + +--- + +## Compute strategy + +Target profile: +- One ~1B perception/routing/object-proposal model +- Many 10M–50M specialist models +- Deterministic solvers where state is already structured +- Structured state instead of long token chains +- Bounded outputs rather than free-form generation + +Goal: comparable usefulness to large dense models via decomposition, sparse activation, explicit structure, and constraint resolution — not by scaling a single model. + +--- + +## First vertical slice target + +Prove the pipeline on a narrow domain before expanding. + +Candidate domains: +- Math word problems +- Temporal + entity tracking +- State-change / ownership reasoning +- Code generation with AST constraints + +Success criteria for first prototype: +1. Structured intermediate state is produced and inspectable +2. Answer is derivable from resolved constraints, not from surface patterns +3. Each layer has at least one passing contract test +4. A failure in one layer is localizable without inspecting other layers diff --git a/docs/decision_matrices.md b/docs/decision_matrices.md new file mode 100644 index 0000000..1cda89c --- /dev/null +++ b/docs/decision_matrices.md @@ -0,0 +1,165 @@ +# UCWM Decision Matrices + +Use this format for every major architecture fork. A decision that is too complex to settle in a sentence deserves a matrix. + +--- + +## Matrix format + +**Rows** = options under consideration +**Columns** = evaluation factors +**Each row also includes** a structured summary block + +### Standard columns + +| Column | What it measures | +|---|---| +| `compute_efficiency` | Ops and memory per inference pass | +| `memory_efficiency` | State size and cache pressure | +| `latency_cost` | Wall-clock time sensitivity | +| `implementation_complexity` | Engineering effort to build correctly | +| `training_complexity` | Data, infra, and iteration cost | +| `scaling_behavior` | How quality changes with more data/compute | +| `mechanistic_interpretability` | Can you understand what the model is doing internally? | +| `state_inspectability` | Can intermediate state be read and debugged? | +| `constraint_explicitness` | Are constraints visible as typed objects? | +| `failure_localizability` | Can a failure be traced to a specific module? | +| `update_localizability` | Can a fix be applied without retraining everything? | +| `swappability` | Can this component be replaced without cascading changes? | +| `extensibility` | How hard is it to add new capability? | +| `backward_compatibility` | Does a change break existing contracts? | +| `migration_cost` | Cost to shift if this option turns out wrong | +| `reasoning_power` | Quality of structured reasoning output | +| `generalization_potential` | Expected performance outside training distribution | +| `precision` | False positive rate on structured outputs | +| `robustness` | Graceful degradation under noisy/adversarial input | +| `cognitive_load` | Mental overhead for a solo builder to hold this in mind | +| `solo_builder_feasibility` | Can one person build and maintain this? | +| `prototype_speed` | Time from decision to working test | +| `dependency_risk` | Risk from external libraries, APIs, models | + +Rating scale: `low` / `med` / `high` or numeric where useful. Unknown cells should say `?` not be left blank. + +### Per-row summary block + +``` +key_benefit: One sentence on the strongest argument for this option +key_risk: One sentence on the most likely failure mode +unknowns: What you don't know yet that would change this rating +what_it_unlocks: What becomes possible if this works +what_it_blocks: What becomes harder or impossible if you choose this +reversibility: easy / partial / hard — cost of switching away later +recommended_now: yes / no / conditional +``` + +**Note:** Unknown is not the same as bad. Prototype choice is not always final architecture choice. + +--- + +## Matrix 001 — Object Proposal Strategy + +**Decision:** How should the system produce initial CanonicalObject proposals from raw input? + +| Option | compute_efficiency | memory_efficiency | implementation_complexity | training_complexity | state_inspectability | failure_localizability | prototype_speed | solo_builder_feasibility | +|---|---|---|---|---|---|---|---|---| +| A. Rule-based NER + heuristics | high | high | low | none | high | high | high | high | +| B. Small fine-tuned span extractor (~100M) | med | med | med | med | med | high | med | med | +| C. Prompted general LLM | low | low | low | none | low | low | high | high | +| D. Full encoder-decoder proposal model | low | low | high | high | med | med | low | low | + +``` +Option A — Rule-based NER + heuristics +key_benefit: Zero training cost, fully inspectable, fast to iterate contracts against +key_risk: Low recall on implicit or unusual entities; won't generalize to new domains +unknowns: How many object kinds can be reliably detected with rules alone? +what_it_unlocks: Fast contract stabilization; deterministic test fixtures +what_it_blocks: Realistic handling of ambiguous or implicit entities +reversibility: easy — rules are swappable +recommended_now: yes — for first vertical slice + +Option B — Small fine-tuned span extractor +key_benefit: Better recall than rules, bounded output, can be contract-tested +key_risk: Requires labeled data; adds training loop before contracts are stable +unknowns: Minimum labeled data needed for acceptable recall on target domains? +what_it_unlocks: Realistic prototype; near-production object proposal +what_it_blocks: Nothing significant +reversibility: partial — retraining needed to change object kinds +recommended_now: conditional — after contracts stabilize + +Option C — Prompted general LLM +key_benefit: Zero training, fast to prototype +key_risk: Output is not contract-typed; failure modes are opaque and inconsistent +unknowns: ? +what_it_unlocks: Fast exploration +what_it_blocks: All downstream contract-testing; structured state guarantees +reversibility: easy +recommended_now: no — undermines UCWM's core value proposition + +Option D — Full encoder-decoder proposal model +key_benefit: Highest potential recall and precision +key_risk: Massive training complexity before any contract is validated +unknowns: ? +what_it_unlocks: Full generality +what_it_blocks: Fast iteration; solo buildability +reversibility: hard +recommended_now: no +``` + +--- + +## Matrix 002 — Constraint Representation Format + +**Decision:** What format should constraints use internally? + +| Option | state_inspectability | constraint_explicitness | swappability | implementation_complexity | reasoning_power | prototype_speed | +|---|---|---|---|---|---|---| +| A. Typed record structs (per schema) | high | high | high | med | med | med | +| B. Predicate logic strings | high | med | med | low | high | med | +| C. Graph edges in a knowledge graph | med | med | low | high | high | low | +| D. Embedding vectors | low | low | low | low | low | high | + +``` +Option A — Typed record structs +key_benefit: Directly contract-testable; every field is named and typed +key_risk: Requires upfront schema design for each constraint type +unknowns: How many constraint types are needed before the schema stabilizes? +what_it_unlocks: Unit testing of individual constraints; specialist contract validation +what_it_blocks: Nothing significant for structured reasoning +reversibility: partial — schema changes require migration +recommended_now: yes + +Option B — Predicate logic strings +key_benefit: Human-readable; can feed formal solvers +key_risk: String parsing adds fragility; harder to type-check +unknowns: How to handle probabilistic and defeasible constraints in logic strings? +what_it_unlocks: Integration with SMT solvers, Prolog-style inference +what_it_blocks: Clean contract testing without a parser layer +reversibility: partial +recommended_now: conditional — after typed structs prove insufficient + +Option C — Knowledge graph edges +key_benefit: Natural representation for relation-heavy domains +key_risk: Graph traversal adds latency; harder to express soft constraints +unknowns: Which graph library and query interface? +what_it_unlocks: Graph-native reasoning; visualization +what_it_blocks: Simple sequential constraint resolution +reversibility: hard +recommended_now: no + +Option D — Embedding vectors +key_benefit: Fast similarity; compatible with neural downstream +key_risk: Zero inspectability; no contract-testing possible +unknowns: ? +what_it_unlocks: Neural constraint matching +what_it_blocks: Everything UCWM stands for +reversibility: easy +recommended_now: no +``` + +--- + +## How to add a new matrix + +Copy the template above. Give it the next sequential ID and a clear decision statement. Fill in relevant columns only — not every column applies to every decision. Mark unknowns as `?` rather than guessing. Add the per-row summary block for every option. + +Archive old matrices as decisions are made, but do not delete them. Past reasoning is provenance. diff --git a/docs/implementation_constraints.md b/docs/implementation_constraints.md new file mode 100644 index 0000000..cbaa7e9 --- /dev/null +++ b/docs/implementation_constraints.md @@ -0,0 +1,279 @@ +# Implementation Constraints (v1.1) + +These constraints apply to every module in the system without exception. They are architectural rules, not guidelines. A module that violates any of them is not a valid UCWM module, regardless of whether it produces correct answers. + +--- + +## 1. No hidden state between modules + +Every module must: +- Accept only typed inputs defined by its contract +- Produce only typed outputs defined by its contract +- Not rely on hidden global context +- Not rely on implicit latent carryover from prior calls + +If a module needs information, it must be passed explicitly through the WorldState or arguments. A module that reads from a global variable or internal cache that is not visible in WorldState is invalid. + +--- + +## 2. WorldState is the source of truth + +All state lives in WorldState. Modules must not: +- Store their own internally evolving state across calls +- Cache interpretations across calls unless that cache is explicitly registered as a field in WorldState +- Mutate shared objects directly — all changes must be returned as typed outputs and applied by the receiving layer + +All changes take the form: +``` +Operation { + type: "propose" | "merge" | "refine" | "link" | "suppress" + target_refs: string[] + payload: typed payload per operation type + confidence_delta: float? +} +``` + +--- + +## 3. Every operation must be logged + +All module outputs must include provenance. The provenance record must be sufficient to reconstruct: + +``` +final_answer → constraints → facets → objects → source text +``` + +Every Constraint, Facet, and Object created or modified must have a corresponding ProvenanceRecord with: +``` +ProvenanceRecord { + source_module: string + input_refs: string[] + output_refs: string[] + reasoning_tag: string -- short label for what operation was performed +} +``` + +See `schemas/provenance.json` for the normative schema. + +--- + +## 4. Bounded output enforcement + +Every module must declare and enforce explicit output caps in its manifest. No module may produce unbounded output. + +Default caps (may be overridden per module manifest): +``` +max_objects_proposed: 8 +max_constraints_emitted: 16 +max_facets_emitted: 8 +max_merges: 4 +max_splits: 2 +``` + +If a module reaches a cap, it must stop and return what it has, not raise an error and discard all output. Partial output is valid. Unbounded output is not. + +Gates have separate caps: a gate emits exactly 1 GateSignal. No cap needed — it is structurally enforced. + +--- + +## 5. No free-form text past the perception layer + +After the perception / extraction stage: +- No module may output free-form natural language as a primary output +- All outputs must be structured, typed, and schema-compliant +- Diagnostic `notes` fields in logs may contain free text, but these are not part of the reasoning chain + +Only the final synthesis layer may produce language as output. + +This rule applies to constraint `expression` fields too — they are informational annotations, not authoritative outputs. + +--- + +## 6. First-class error states + +Every module must be able to return explicit error states. Forcing a resolution when insufficient information exists is a contract violation. + +Valid first-class output states for any object, facet, or constraint: + +| State | Meaning | +|---|---| +| `unknown` | The module could not determine a value | +| `ambiguous` | Multiple valid interpretations exist | +| `conflicting` | Two or more inputs contradict each other | +| `insufficient_data` | Not enough evidence to propose anything | + +These states must be returned as typed fields, not inferred from missing values or nulls. + +Example — SpecialistError type: +``` +SpecialistError { + error_kind: "unknown" | "ambiguous" | "conflicting" | "insufficient_data" + affected_refs: string[] + description: string +} +``` + +The resolver preserves and propagates error states rather than collapsing them. + +--- + +## 7. Constraint-first thinking + +Specialists should prefer emitting constraints over conclusions. + +Wrong: +``` +Q_result.value = 3 +``` + +Right: +``` +quantity_difference(Q_result, Q1, Q2) -- status: unresolved +``` + +Resolution happens in the UCWM resolver, not inside specialists. A specialist that resolves arithmetic or logical conclusions itself before emitting constraints is bypassing the resolver contract. + +The exception: arithmetic that is provably deterministic with all operands present may be resolved inline by the quantity specialist, with the resolution logged in provenance. + +--- + +## 8. Test-driven module design + +For every module, the following test classes must exist before the module is considered complete: + +| Test class | Validates | +|---|---| +| Input → output correctness | Expected outputs for known inputs | +| Schema compliance | All outputs validate against declared schemas | +| Bounded output | Output never exceeds declared caps | +| Deterministic behavior | Same input → same output (where determinism is expected) | +| Failure modes | Module returns valid error states on ambiguous/missing data | +| Domain adherence | Module does not emit facet/constraint kinds outside its manifest | + +See `tests/contracts/` for templates. + +--- + +## 9. Minimal vertical slice priority + +Do not build everything before validating anything. + +The first working pipeline must support: +``` +Input sentence + → gate signals + → object extraction (1–3 objects) + → 1–2 specialists + → constraints + → resolution + → correct answer +``` + +Suggested first domain: math word problems or simple transfer/state-change problems. + +Expand coverage only after the vertical slice produces correct, inspectable, testable output. + +--- + +## 10. Language binding stubs + +Two interface stubs must exist from the start, even if naive: + +``` +to_ucwm(text_input: string) → WorldState +from_ucwm(world_state: WorldState) → string +``` + +See `synthesis/language_bindings.md` for the full interface specification. + +These stubs enforce that translation and generation remain separable from reasoning. The reasoning core must never depend on how language enters or exits the system. + +--- + +## 11. Composite routing must be multi-label + +The router must activate multiple specialists simultaneously. Single-label routing is not valid for real-world inputs. + +A valid RoutingDecision activates: +``` +temporal: true +causal: true +quantity: true +``` + +Not: +``` +category: "quantity" ← wrong; exactly one specialist selected +``` + +See `schemas/routing.json#/$defs/RoutingDecision` — the `assignments[]` array is the mechanism. + +--- + +## 12. Facets must be independent + +A facet describes only its own dimension. It must not modify or depend on another facet's fields. + +A TemporalFacet must not modify ownership state. A QuantityFacet must not set truth status. Cross-facet interaction happens only through constraints, never through direct facet-to-facet references. + +--- + +## 13. Resolver must be replaceable + +The resolver is defined by an interface, not an implementation: + +``` +resolve(world_state: WorldState) → WorldState +``` + +The rest of the pipeline must not depend on resolver internals. Any implementation that satisfies `tests/contracts/resolver_contract_tests.md` is a valid resolver. This makes it possible to swap: + +- Simple rule-based resolver +- Graph-based constraint solver +- Probabilistic constraint solver +- SAT-style system + +--- + +## 14. Keep implementations simple first + +Prefer clarity over performance. Prefer correctness over cleverness. This system will become complex naturally — do not add complexity early. + +Guidelines: +- Use simple, flat data structures where possible +- Avoid premature abstraction +- An obvious implementation that is slow is better than a clever one that is wrong +- Performance optimization begins after contract tests pass + +Language choice is not prescribed. Whatever language is chosen, it must support schema validation of WorldState at every module boundary. + +--- + +## 15. Debug mode + +Every implementation must support a debug mode that emits a step-by-step trace: + +``` +[Step 1] Gate outputs: {gate_id: activated, confidence} +[Step 2] Objects proposed: {object_id, kind, label, confidence} +[Step 3] Routing decision: {specialist_id, target_refs, budget} +[Step 4] Specialist outputs: {specialist_id, new_facets[], new_constraints[]} +[Step 5] Constraints: {constraint_id, type, args, strength, status} +[Step 6] Resolution log: {operation, affected_refs, result} +[Step 7] Final answer: {answer, confidence, derivation_trace} +``` + +Debug mode must write to a structured log (JSON or equivalent), not only to stdout. The log must be sufficient to replay and inspect any inference. + +--- + +## Priority order + +When any two implementation choices conflict, resolve by this priority: + +1. Contracts (schema compliance, interface adherence) +2. Testability (can this behavior be verified independently?) +3. Inspectability (can intermediate state be read and debugged?) +4. Correctness (does it produce right answers?) +5. Performance (how fast?) +6. Scale (how big?) diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000..ed1bf3c --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,140 @@ +# UCWM Roadmap + +## Goal + +A working, inspectable, contract-tested language reasoning system that converts natural language into structured world state, resolves constraints, and produces answers from that structure — not from token pattern matching. + +## Guiding principle + +Build the thinnest possible vertical slice first. Prove the pipeline shape. Replace rule-based components with trained models only after contracts are stable and the slice works end-to-end. + +--- + +## Phase 1 — Vertical Slice + +**Goal:** One input in, one correct answer out, with a full inspectable trace at every stage. +**Target input:** `"Alice had 5 apples. She gave Bob 2 apples. How many apples does Alice have now?"` +**Target output:** `"Alice has 3 apples."` with derivation trace. + +| Sprint | Name | Goal | +|---|---|---| +| 1 | Skeleton | C++ project compiles. All core types defined. WorldState round-trips to/from JSON. | +| 2 | Gates + Proposal | 5 keyword gates fire correctly. Object proposal extracts entities, quantities, events from apple problem. | +| 3 | Specialists | Entity specialist resolves Alice/She. Quantity specialist emits arithmetic constraint and resolves it. | +| 4 | Resolver + Synthesis + Pipeline | Full pipeline runs. Apple problem produces correct answer with debug trace. All contract tests pass. | + +**Phase 1 exit criteria:** +- Apple problem works end-to-end +- Each layer has passing contract tests +- Debug trace shows full derivation +- A failure in one layer is diagnosable without inspecting others + +--- + +## Phase 2 — Domain Expansion + +**Goal:** Expand to more input types. Stress-test contracts. Add the specialists the apple problem skipped. + +| Sprint | Name | Goal | +|---|---|---| +| 5 | Ownership Specialist | Track possession, transfers, state at time T. Multi-step ownership chains. | +| 6 | Temporal Specialist | Before/after/during. Temporal chain example (temporal_001.json) works end-to-end. | +| 7 | Causal Specialist | Cause/effect/enabling/blocking. First causal example works. | +| 8 | Logic Specialist | Negation, implication, contradiction detection. | +| 9 | Multi-step Problems | 3+ step word problems. Resolver handles chains of arithmetic and ownership constraints. | +| 10 | Robustness Sprint | Edge cases: empty input, ambiguous entities, contradicting constraints. Error states propagate correctly. | + +**Phase 2 exit criteria:** +- All 5 core specialist domains covered (entity, quantity, temporal, causal, logic) +- At least 10 distinct example fixtures with passing end-to-end tests +- `open_contradictions` and error states tested explicitly +- Decision matrix completed for gate implementation (regex vs. transformer) + +--- + +## Phase 3 — Neural Gates + +**Goal:** Replace keyword gates with a trained multi-label transformer classifier. Gate contract unchanged. + +| Sprint | Name | Goal | +|---|---|---| +| 11 | Training Data | Generate labeled gate training examples (synthetic + manual). At least 500 examples per gate. | +| 12 | Gate Model | Fine-tune small encoder (DistilBERT-scale) with 17 sigmoid heads on labeled data. | +| 13 | ONNX Integration | Export gate model to ONNX. Load and run via ONNX Runtime in C++. Pass gate contract tests. | +| 14 | Calibration | Confidence scores calibrated against held-out set. Recall >= 0.90 on each gate. | + +**Phase 3 exit criteria:** +- Gate model passes all contract tests from `tests/contracts/gate_contract_tests.md` +- Recall >= 0.90 per gate +- Latency per gate array pass <= 20ms on CPU +- Keyword gates retained as fallback (togglable) +- All downstream behavior unchanged (contract held) + +--- + +## Phase 4 — Neural Specialists + +**Goal:** Replace rule-based specialists with trained neural modules, one at a time. + +| Sprint | Name | Goal | +|---|---|---| +| 15 | Entity Specialist (Neural) | Small NER + coreference model. Passes entity contract tests. Replaces rule-based version. | +| 16 | Quantity Specialist (Neural) | Numeric extraction + arithmetic constraint emission. Handles implicit quantities. | +| 17 | Temporal Specialist (Neural) | Temporal ordering under ambiguity. Handles implicit sequence without explicit connectives. | +| 18 | Resolver Upgrade | Add probabilistic constraint handling. Soft constraint weighting. Multiple hypotheses. | +| 19 | Synthesis Upgrade | Move from template-based to generative synthesis. Answer quality evaluation. | + +**Phase 4 exit criteria:** +- Each specialist passes its contract tests with the neural implementation +- Performance equal to or better than rule-based on held-out examples +- Rule-based versions retained as reference implementations for debugging + +--- + +## Phase 5 — Perception Model + +**Goal:** Build or integrate the broad ~1B parameter perception model that handles object proposal for novel domains. + +| Sprint | Name | Goal | +|---|---|---| +| 20 | Architecture Decision | Decision matrix: train from scratch vs. fine-tune existing. Encoder-only vs. encoder-decoder. | +| 21 | Perception Prototype | Small prototype perception model producing CanonicalObject proposals on unseen inputs. | +| 22 | Shared Encoder | Evaluate sharing the perception encoder with the gate classifier heads. | +| 23 | Integration | Wire perception model into pipeline. Replace rule-based object proposal. | + +--- + +## Phase 6 — Scale and Specialized Domains + +**Goal:** Expand to code, planning, social reasoning, and multi-turn conversation. + +| Sprint | Name | Goal | +|---|---|---| +| 24 | Code Specialist | AST-constrained generation. Code object proposals. Dependency graph constraints. | +| 25 | Planning Specialist | Step/goal/prerequisite tracking. Multi-step planning problems. | +| 26 | Social Specialist | Belief, intention, obligation, deception. Theory of mind examples. | +| 27 | Multi-turn | WorldState carries over across turns. Coreference across sentences. | +| 28 | Performance | Parallel specialist execution. Profiling. Latency targets. | + +--- + +## Key decision points + +These are forks in the road that should be decided with a decision matrix (see `docs/decision_matrices.md`) rather than ad hoc: + +| Decision | When | +|---|---| +| Gate implementation: keyword vs. neural | End of Phase 2 | +| Specialist implementation: rule vs. neural (per specialist) | After each rule-based specialist is validated | +| Perception model: train from scratch vs. fine-tune | Start of Phase 5 | +| Resolver: deterministic vs. probabilistic vs. SAT | End of Phase 3 | +| Shared encoder: gate + perception vs. separate models | Start of Phase 5 | +| Synthesis: template vs. generative | End of Phase 4 | + +--- + +## What this is not + +- Not a chatbot. Multi-turn comes late (Sprint 27) because single-turn must be solid first. +- Not a 30B dense model. The goal is comparable usefulness via decomposition and structure. +- Not a research paper. The goal is a working system. Publish after it works. diff --git a/docs/sprint_001.md b/docs/sprint_001.md new file mode 100644 index 0000000..fd33f24 --- /dev/null +++ b/docs/sprint_001.md @@ -0,0 +1,204 @@ +# Sprint 1 — Skeleton + +## Goal + +The C++ project compiles cleanly. All core UCWM types are defined as C++ structs. A `WorldState` can be constructed in code and serialized to JSON. No logic runs yet — this sprint is purely about getting the type system right before anything else is built on top of it. + +**If the types are wrong here, everything built on them will be wrong. Take the time to get them right.** + +--- + +## Deliverables + +| File | Description | +|---|---| +| `CMakeLists.txt` | Build system. CPM dependencies. Targets for main and tests. | +| `src/types.hpp` | All core UCWM structs: CanonicalObject, all Facet types, Constraint, GateSignal, RoutingDecision, WorldState, Provenance, SpecialistError. | +| `src/main.cpp` | Smoke test: constructs a minimal WorldState for the apple problem by hand, serializes to JSON, prints debug output. | +| `tests/test_types.cpp` | Unit tests: construct each type, verify field access, verify JSON round-trip. | + +--- + +## Acceptance criteria + +- [ ] `cmake -B build && cmake --build build` succeeds with zero errors and zero warnings +- [ ] `./build/ucwm` runs and prints a JSON-serialized WorldState to stdout +- [ ] `./build/tests` runs and all type tests pass +- [ ] Every field from `schemas/objects.json`, `schemas/facets.json`, `schemas/constraints.json`, `schemas/routing.json`, `schemas/provenance.json`, and `schemas/world_state.json` has a corresponding C++ field +- [ ] No raw owning pointers — use `std::optional`, `std::variant`, `std::vector`, `std::unordered_map` +- [ ] All string IDs typed as `std::string` (upgrade to a newtype wrapper in a later sprint if needed) + +--- + +## Out of scope + +- Any gate logic +- Any specialist logic +- Any resolver logic +- Any synthesis logic +- JSON Schema validation (structs mirror the schemas, but no runtime validation yet) +- Neural models of any kind +- Performance optimization + +--- + +## Dependencies (pulled via CPM) + +```cmake +CPMAddPackage("gh:nlohmann/json@3.11.3") +CPMAddPackage("gh:gabime/spdlog@1.13.0") +CPMAddPackage("gh:catchorg/Catch2@3.5.4") +``` + +--- + +## Type design notes + +### Use `std::variant` for sum types + +Object kind, facet kind, constraint status, object status — these are closed enumerations in the schema. Use `enum class` for simple enums, `std::variant` when the type carries different data per variant. + +```cpp +enum class ObjectKind { + Entity, Event, Concept, Proposition, + RelationInstance, ProcedureStep, CodeObject, + Quantity, Claim, State +}; + +enum class ObjectStatus { Proposed, Active, Merged, Split, Invalidated }; +enum class ConstraintStrength { Hard, Soft, Probabilistic, Defeasible }; +enum class ConstraintStatus { Unresolved, Resolved, Contradicted, Suspended }; +enum class Polarity { Positive, Negative }; +``` + +### Use `std::optional` for nullable fields + +Schema fields that are not in `required` should be `std::optional` in C++. Never use raw nulls or sentinel values. + +```cpp +std::optional surface_span; +std::optional canonical_label; +std::optional merged_into; +std::optional probability; // only when strength=Probabilistic +``` + +### Facets as a variant + +All facet types share a base set of fields, then add domain-specific fields. Represent as a `std::variant` or a struct with a `std::variant` payload. + +```cpp +struct TemporalFacetData { + std::optional time_point; + std::optional interval_start; + std::optional interval_end; + bool is_recurring = false; + std::optional recurrence_pattern; + std::vector temporal_order_refs; +}; + +// ... one struct per facet kind ... + +using FacetData = std::variant< + TemporalFacetData, + SpatialFacetData, + CausalFacetData, + LogicalFacetData, + OwnershipFacetData, + QuantityFacetData, + SyntacticFacetData, + SocialFacetData, + PlanningFacetData +>; + +struct Facet { + std::string facet_id; + std::string object_ref; + float confidence; + std::string source_module; + std::vector provenance_refs; + FacetData data; // kind is implicit in the variant index +}; +``` + +### WorldState as maps + +```cpp +struct WorldState { + std::string state_id; + WorldStateStage stage; + std::optional input_text; + std::unordered_map objects; + std::unordered_map facets; + std::unordered_map constraints; + std::unordered_map provenance; + std::optional routing_decision; + std::vector gate_signals; + std::vector resolution_log; + std::vector open_contradictions; + std::optional synthesis_answer; + std::optional synthesis_confidence; + std::vector derivation_trace; +}; +``` + +### JSON serialization + +Use `nlohmann/json`'s `NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE` macro or manual `to_json`/`from_json` overloads. Every type needs both directions for the debug trace and for loading fixtures. + +```cpp +// Example pattern for each type: +void to_json(nlohmann::json& j, const CanonicalObject& o); +void from_json(const nlohmann::json& j, CanonicalObject& o); +``` + +--- + +## File layout after Sprint 1 + +``` +ucwm/ + CMakeLists.txt + cmake/ + CPM.cmake + src/ + types.hpp ← all structs and enums + main.cpp ← smoke test + tests/ + test_types.cpp ← type construction + JSON round-trip tests +``` + +--- + +## Smoke test behavior (`main.cpp`) + +`main.cpp` should: +1. Construct the post-proposal WorldState from `examples/word_problem_001.json` by hand (hardcoded, not parsed — parsing the fixture comes in Sprint 2) +2. Serialize it to JSON using nlohmann/json +3. Print the JSON to stdout +4. Print a simple debug header showing object count, constraint count + +Expected output: +``` +=== UCWM Smoke Test === +WorldState stage: post_proposal +Objects: 7 +Facets: 0 +Constraints: 0 + +[full JSON follows] +``` + +--- + +## Definition of done + +Sprint 1 is done when: +1. All acceptance criteria checkboxes are checked +2. A second person (or the same person on a fresh read) can understand every field in `types.hpp` without referring back to the schemas +3. The JSON output of `main.cpp` matches the structure of `examples/word_problem_001.json` for the post-proposal stage + +--- + +## Next sprint preview + +Sprint 2 builds the keyword gates and rule-based object proposal on top of these types. None of that can start until `types.hpp` is locked. diff --git a/docs/vocabulary.md b/docs/vocabulary.md new file mode 100644 index 0000000..09c8c4d --- /dev/null +++ b/docs/vocabulary.md @@ -0,0 +1,90 @@ +# UCWM Vocabulary + +## Core terms + +**UCWM — Unified Constraint World Model** +The whole structured reasoning substrate. Converts messy natural language into a typed world state of objects, facets, and constraints, then resolves that state to produce answers. + +**RSA — Relay Specialist Architecture** +The routing and dispatch mechanism inside UCWM. RSA decides which specialists run, on which objects, with what budget. RSA is not the model core; UCWM is. + +**CanonicalObject** +The primary reusable referent. A stable anchor that facets attach to. Objects are not overloaded — they carry identity, kind, status, confidence, and refs to facets, constraints, and provenance. + +**FacetBundle** +The collection of typed descriptions attached to a single canonical object. A single object may have temporal, causal, spatial, logical, and other facets simultaneously. + +**Facet** +A partial, typed view of a canonical object. Examples: TemporalFacet, SpatialFacet, CausalFacet, QuantityFacet. Facets are optional — not every object exists in every relation type. + +**Schema Plurality** +The principle that the same underlying thing may be validly described by multiple schema systems simultaneously. UCWM does not force a single schema early. Specialists attach schema-shaped facets; the resolver reconciles. + +**Constraint** +An explicit relation, restriction, incompatibility, requirement, or preference involving objects and/or facets. Constraints are the core reasoning currency. Examples: `before(E1, E2)`, `owns(Alice, apples, count=5)`, `quantity(Q1) = quantity(Q2) - 3`. + +**Constraint Profile** +The active set of constraints touching a given object and its facets. + +**Composite Routing** +A relay decision that activates multiple specialists on the same target simultaneously. + +**Relation Gate Array** +The front-end set of small recognizers that detect which relation families are probably relevant to the input. High-recall by design — prefer false activation over missed activation. + +**GateSignal** +The output of a single gate. Contains: gate_id, activated flag, confidence score, and evidence spans. + +**RoutingDecision** +The output of the relay. Contains: active specialists, target object refs, execution mode, budget, and priority order. + +**WorldState** +The active UCWM state at a given processing stage. Contains all current objects, facets, constraints, and their statuses. + +**Contractual Layer** +A module boundary with typed input/output schemas and testable behavior. Every UCWM layer is a contractual layer. + +**Specialist** +A contract-bound module that takes canonical objects and routing context, and emits typed facets and constraints. May be neural, symbolic, statistical, or hybrid. Emits bounded structured output — never unconstrained free text. + +**Resolver** +The UCWM component that combines constraints from specialists, detects contradictions, reinforces agreements, and stabilizes WorldState into a resolved form ready for synthesis. + +**Synthesis** +The final stage. Produces a natural language answer and derivation trace from resolved WorldState. The answer must be derivable from constraints, not from surface token patterns. + +**Provenance** +A record of where a fact, object, facet, or constraint came from: which input span, which module, which inference step, at what confidence. + +## Constraint strength levels + +| Strength | Meaning | +|---|---| +| `hard` | Must hold; contradiction is an error | +| `soft` | Preferred; can be overridden by hard constraints | +| `probabilistic` | Holds with a stated probability | +| `defeasible` | Default assumption; can be retracted by new evidence | + +## Constraint status lifecycle + +``` +unresolved → resolved + → contradicted + → suspended +``` + +## Object kinds + +`entity`, `event`, `concept`, `proposition`, `relation_instance`, `procedure_step`, `code_object`, `quantity`, `claim`, `state` + +## Object status lifecycle + +``` +proposed → active → merged + → split + → invalidated +``` + +## Facet kinds + +`temporal`, `spatial`, `causal`, `logical`, `syntactic`, `semantic`, `social`, `ownership`, `code_structure`, `quantity`, `planning` diff --git a/examples/temporal_001.json b/examples/temporal_001.json new file mode 100644 index 0000000..cf5eeff --- /dev/null +++ b/examples/temporal_001.json @@ -0,0 +1,166 @@ +{ + "_comment": "Temporal chain example: multi-event sequence with explicit and implicit ordering.", + "input_text": "The meeting started at 9am. Alice arrived after it began. She left before noon.", + + "stage_post_gate": { + "gate_signals": [ + { "gate_id": "has_entity_reference", "activated": true, "confidence": 0.97 }, + { "gate_id": "has_coreference", "activated": true, "confidence": 0.88 }, + { "gate_id": "has_temporal_relation", "activated": true, "confidence": 0.99, + "evidence_spans": [{"text": "at 9am"}, {"text": "after"}, {"text": "before noon"}] }, + { "gate_id": "has_state_change", "activated": true, "confidence": 0.85 } + ] + }, + + "stage_post_proposal": { + "objects": { + "E1": { + "object_id": "E1", + "object_kind": "entity", + "status": "active", + "confidence": 0.96, + "canonical_label": "Alice", + "aliases": ["She", "her"], + "source_module": "entity_specialist" + }, + "EV_meeting": { + "object_id": "EV_meeting", + "object_kind": "event", + "status": "active", + "confidence": 0.97, + "surface_span": { "text": "meeting", "start": 4, "end": 11 }, + "canonical_label": "meeting", + "source_module": "entity_specialist" + }, + "EV_meeting_start": { + "object_id": "EV_meeting_start", + "object_kind": "event", + "status": "active", + "confidence": 0.97, + "surface_span": { "text": "started", "start": 12, "end": 19 }, + "canonical_label": "meeting_start", + "source_module": "entity_specialist" + }, + "EV_arrival": { + "object_id": "EV_arrival", + "object_kind": "event", + "status": "active", + "confidence": 0.95, + "surface_span": { "text": "arrived", "start": 33, "end": 40 }, + "canonical_label": "alice_arrival", + "source_module": "entity_specialist" + }, + "EV_departure": { + "object_id": "EV_departure", + "object_kind": "event", + "status": "active", + "confidence": 0.94, + "surface_span": { "text": "left", "start": 57, "end": 61 }, + "canonical_label": "alice_departure", + "source_module": "entity_specialist" + }, + "T_9am": { + "object_id": "T_9am", + "object_kind": "quantity", + "status": "active", + "confidence": 0.99, + "surface_span": { "text": "9am", "start": 23, "end": 26 }, + "canonical_label": "09:00", + "source_module": "temporal_specialist" + }, + "T_noon": { + "object_id": "T_noon", + "object_kind": "quantity", + "status": "active", + "confidence": 0.99, + "surface_span": { "text": "noon", "start": 69, "end": 73 }, + "canonical_label": "12:00", + "source_module": "temporal_specialist" + } + } + }, + + "stage_post_specialist": { + "constraints": { + "C_t1": { + "constraint_id": "C_t1", + "constraint_type": "starts_at", + "argument_refs": ["EV_meeting_start", "T_9am"], + "polarity": "positive", + "strength": "hard", + "status": "unresolved", + "expression": "starts_at(meeting_start, 09:00)", + "confidence": 0.97, + "source_module": "temporal_specialist" + }, + "C_t2": { + "constraint_id": "C_t2", + "constraint_type": "after", + "argument_refs": ["EV_arrival", "EV_meeting_start"], + "polarity": "positive", + "strength": "hard", + "status": "unresolved", + "expression": "after(alice_arrival, meeting_start)", + "confidence": 0.96, + "source_module": "temporal_specialist" + }, + "C_t3": { + "constraint_id": "C_t3", + "constraint_type": "before", + "argument_refs": ["EV_departure", "T_noon"], + "polarity": "positive", + "strength": "hard", + "status": "unresolved", + "expression": "before(alice_departure, 12:00)", + "confidence": 0.95, + "source_module": "temporal_specialist" + }, + "C_t4": { + "constraint_id": "C_t4", + "constraint_type": "before", + "argument_refs": ["EV_arrival", "EV_departure"], + "polarity": "positive", + "strength": "soft", + "status": "unresolved", + "expression": "before(alice_arrival, alice_departure)", + "confidence": 0.88, + "source_module": "temporal_specialist" + }, + "C_coref": { + "constraint_id": "C_coref", + "constraint_type": "same_entity", + "argument_refs": ["E1", "E_she"], + "polarity": "positive", + "strength": "hard", + "status": "unresolved", + "expression": "same_entity(Alice, She)", + "confidence": 0.90, + "source_module": "entity_specialist" + } + } + }, + + "stage_resolved": { + "_notes": "All temporal constraints resolved. Ordering chain: meeting_start (09:00) → alice_arrival → alice_departure (< 12:00).", + "open_contradictions": [], + "ordering_chain": [ + "EV_meeting_start at T_9am", + "EV_arrival after EV_meeting_start", + "EV_departure before T_noon", + "EV_arrival before EV_departure (soft, consistent)" + ] + }, + + "stage_synthesized": { + "synthesis_mode": "explain", + "answer": "The meeting started at 9am. Alice arrived after it began and left before noon.", + "confidence": 0.93, + "derivation_trace": [ + "C_t1: starts_at(meeting_start, 09:00) — resolved", + "C_t2: after(alice_arrival, meeting_start) — resolved", + "C_t3: before(alice_departure, 12:00) — resolved", + "C_t4: before(alice_arrival, alice_departure) — resolved (consistent with C_t2, C_t3)", + "C_coref: same_entity(Alice, She) — merged" + ] + } +} diff --git a/examples/word_problem_001.json b/examples/word_problem_001.json new file mode 100644 index 0000000..409b9b3 --- /dev/null +++ b/examples/word_problem_001.json @@ -0,0 +1,296 @@ +{ + "_comment": "Vertical slice example: apple transfer word problem. Shows complete pipeline state at each stage.", + "input_text": "Alice had 5 apples. She gave Bob 2 apples. How many apples does Alice have now?", + + "stage_post_gate": { + "gate_signals": [ + { "gate_id": "has_entity_reference", "activated": true, "confidence": 0.99, "evidence_spans": [{"text": "Alice", "start": 0, "end": 5}, {"text": "Bob", "start": 29, "end": 32}] }, + { "gate_id": "has_coreference", "activated": true, "confidence": 0.90, "evidence_spans": [{"text": "She", "start": 17, "end": 20}] }, + { "gate_id": "has_quantity", "activated": true, "confidence": 0.99, "evidence_spans": [{"text": "5 apples", "start": 10, "end": 18}, {"text": "2 apples", "start": 33, "end": 41}] }, + { "gate_id": "has_ownership_transfer", "activated": true, "confidence": 0.97, "evidence_spans": [{"text": "gave", "start": 21, "end": 25}] }, + { "gate_id": "has_temporal_relation", "activated": true, "confidence": 0.85, "evidence_spans": [{"text": "had", "start": 6, "end": 9}, {"text": "gave", "start": 21, "end": 25}] }, + { "gate_id": "is_math", "activated": true, "confidence": 0.95, "evidence_spans": [{"text": "5", "start": 10, "end": 11}, {"text": "2", "start": 33, "end": 34}] } + ] + }, + + "stage_post_proposal": { + "objects": { + "E1": { + "object_id": "E1", + "object_kind": "entity", + "status": "active", + "confidence": 0.97, + "surface_span": { "text": "Alice", "start": 0, "end": 5 }, + "canonical_label": "Alice", + "aliases": ["She", "her"], + "facet_refs": [], + "constraint_refs": [], + "evidence_refs": [], + "provenance_refs": ["P1"], + "source_module": "entity_specialist" + }, + "E2": { + "object_id": "E2", + "object_kind": "entity", + "status": "active", + "confidence": 0.97, + "surface_span": { "text": "Bob", "start": 29, "end": 32 }, + "canonical_label": "Bob", + "aliases": ["him"], + "facet_refs": [], + "constraint_refs": [], + "evidence_refs": [], + "provenance_refs": ["P2"], + "source_module": "entity_specialist" + }, + "O_apples": { + "object_id": "O_apples", + "object_kind": "concept", + "status": "active", + "confidence": 0.99, + "surface_span": { "text": "apples", "start": 12, "end": 18 }, + "canonical_label": "apples", + "aliases": [], + "facet_refs": [], + "constraint_refs": [], + "evidence_refs": [], + "provenance_refs": ["P3"], + "source_module": "entity_specialist" + }, + "Q1": { + "object_id": "Q1", + "object_kind": "quantity", + "status": "active", + "confidence": 0.99, + "surface_span": { "text": "5", "start": 10, "end": 11 }, + "canonical_label": "5", + "facet_refs": ["F_Q1"], + "constraint_refs": [], + "evidence_refs": [], + "provenance_refs": ["P4"], + "source_module": "quantity_specialist" + }, + "Q2": { + "object_id": "Q2", + "object_kind": "quantity", + "status": "active", + "confidence": 0.99, + "surface_span": { "text": "2", "start": 33, "end": 34 }, + "canonical_label": "2", + "facet_refs": ["F_Q2"], + "constraint_refs": [], + "evidence_refs": [], + "provenance_refs": ["P5"], + "source_module": "quantity_specialist" + }, + "Q_alice_final": { + "object_id": "Q_alice_final", + "object_kind": "quantity", + "status": "proposed", + "confidence": 0.80, + "canonical_label": "Alice's final apple count", + "facet_refs": ["F_Q_final"], + "constraint_refs": [], + "evidence_refs": [], + "provenance_refs": ["P6"], + "source_module": "quantity_specialist" + }, + "EV_initial": { + "object_id": "EV_initial", + "object_kind": "event", + "status": "active", + "confidence": 0.90, + "surface_span": { "text": "had", "start": 6, "end": 9 }, + "canonical_label": "initial_possession", + "facet_refs": [], + "constraint_refs": [], + "evidence_refs": [], + "provenance_refs": ["P7"], + "source_module": "entity_specialist" + }, + "EV_transfer": { + "object_id": "EV_transfer", + "object_kind": "event", + "status": "active", + "confidence": 0.95, + "surface_span": { "text": "gave", "start": 21, "end": 25 }, + "canonical_label": "transfer", + "facet_refs": [], + "constraint_refs": [], + "evidence_refs": [], + "provenance_refs": ["P8"], + "source_module": "entity_specialist" + } + } + }, + + "stage_post_specialist": { + "facets": { + "F_Q1": { + "facet_id": "F_Q1", + "facet_kind": "quantity", + "object_ref": "Q1", + "confidence": 0.99, + "source_module": "quantity_specialist", + "value": 5, + "unit": "apples", + "is_exact": true, + "derived_from_refs": [] + }, + "F_Q2": { + "facet_id": "F_Q2", + "facet_kind": "quantity", + "object_ref": "Q2", + "confidence": 0.99, + "source_module": "quantity_specialist", + "value": 2, + "unit": "apples", + "is_exact": true, + "derived_from_refs": [] + }, + "F_Q_final": { + "facet_id": "F_Q_final", + "facet_kind": "quantity", + "object_ref": "Q_alice_final", + "confidence": 0.90, + "source_module": "quantity_specialist", + "value": null, + "unit": "apples", + "is_exact": false, + "derived_from_refs": ["Q1", "Q2"] + }, + "F_EV_initial_temporal": { + "facet_id": "F_EV_initial_temporal", + "facet_kind": "temporal", + "object_ref": "EV_initial", + "confidence": 0.90, + "source_module": "temporal_specialist", + "time_point": "t0", + "temporal_order_refs": ["C_temporal"] + }, + "F_EV_transfer_temporal": { + "facet_id": "F_EV_transfer_temporal", + "facet_kind": "temporal", + "object_ref": "EV_transfer", + "confidence": 0.90, + "source_module": "temporal_specialist", + "time_point": "t1", + "temporal_order_refs": ["C_temporal"] + }, + "F_E1_ownership_t0": { + "facet_id": "F_E1_ownership_t0", + "facet_kind": "ownership", + "object_ref": "O_apples", + "confidence": 0.97, + "source_module": "ownership_specialist", + "owner_ref": "E1", + "owned_ref": "O_apples", + "quantity_ref": "Q1", + "temporal_ref": "F_EV_initial_temporal", + "transfer_type": "initial" + } + }, + "constraints": { + "C1": { + "constraint_id": "C1", + "constraint_type": "owns", + "argument_refs": ["E1", "O_apples", "Q1"], + "polarity": "positive", + "strength": "hard", + "status": "unresolved", + "expression": "owns(Alice, apples, count=5) at t0", + "confidence": 0.97, + "source_module": "ownership_specialist", + "provenance_refs": ["P_C1"] + }, + "C2": { + "constraint_id": "C2", + "constraint_type": "transfers", + "argument_refs": ["E1", "E2", "O_apples", "Q2"], + "polarity": "positive", + "strength": "hard", + "status": "unresolved", + "expression": "transfers(Alice, Bob, apples, count=2) at t1", + "confidence": 0.96, + "source_module": "ownership_specialist", + "provenance_refs": ["P_C2"] + }, + "C3": { + "constraint_id": "C3", + "constraint_type": "quantity_difference", + "argument_refs": ["Q_alice_final", "Q1", "Q2"], + "polarity": "positive", + "strength": "hard", + "status": "unresolved", + "expression": "quantity(Q_alice_final) = quantity(Q1) - quantity(Q2)", + "confidence": 0.99, + "source_module": "quantity_specialist", + "provenance_refs": ["P_C3"] + }, + "C_temporal": { + "constraint_id": "C_temporal", + "constraint_type": "before", + "argument_refs": ["EV_initial", "EV_transfer"], + "polarity": "positive", + "strength": "hard", + "status": "unresolved", + "expression": "before(EV_initial, EV_transfer)", + "confidence": 0.90, + "source_module": "temporal_specialist", + "provenance_refs": ["P_Ct"] + }, + "C_coreference": { + "constraint_id": "C_coreference", + "constraint_type": "same_entity", + "argument_refs": ["E1", "E_she"], + "polarity": "positive", + "strength": "hard", + "status": "unresolved", + "expression": "same_entity(Alice, She)", + "confidence": 0.90, + "source_module": "entity_specialist", + "provenance_refs": ["P_Cc"] + }, + "C_distinct": { + "constraint_id": "C_distinct", + "constraint_type": "not_same_entity", + "argument_refs": ["E1", "E2"], + "polarity": "positive", + "strength": "hard", + "status": "unresolved", + "expression": "not_same_entity(Alice, Bob)", + "confidence": 0.99, + "source_module": "entity_specialist", + "provenance_refs": ["P_Cd"] + } + } + }, + + "stage_resolved": { + "_notes": "After resolver runs: C3 arithmetic resolved (Q_alice_final=3), C_coreference merged E_she into E1, all hard constraints resolved, no contradictions.", + "Q_alice_final_value": 3, + "open_contradictions": [], + "resolution_log": [ + { "operation": "apply_hard", "affected_refs": ["C1"], "result": "resolved" }, + { "operation": "apply_hard", "affected_refs": ["C2"], "result": "resolved" }, + { "operation": "apply_hard", "affected_refs": ["C_temporal"], "result": "resolved" }, + { "operation": "apply_hard", "affected_refs": ["C_distinct"], "result": "resolved" }, + { "operation": "resolve_arithmetic", "affected_refs": ["Q_alice_final"], "result": "resolved", "note": "computed 5-2=3" }, + { "operation": "merge_coreference", "affected_refs": ["E1", "E_she"], "result": "merged", "note": "merged E_she into E1" } + ] + }, + + "stage_synthesized": { + "answer": "Alice has 3 apples.", + "confidence": 0.97, + "derivation_trace": [ + "E1 (Alice) — active entity, confidence 0.97", + "Q1 = 5 apples — quantity, confidence 0.99", + "Q2 = 2 apples — quantity, confidence 0.99", + "C1: owns(Alice, apples, count=5) at t0 — resolved", + "C2: transfers(Alice, Bob, apples, count=2) at t1 — resolved", + "C3: quantity_difference(Q_alice_final, Q1, Q2) — resolved, 5-2=3", + "Q_alice_final = 3 apples" + ] + } +} diff --git a/gates/contract.md b/gates/contract.md new file mode 100644 index 0000000..ea1cbcc --- /dev/null +++ b/gates/contract.md @@ -0,0 +1,112 @@ +# Gate Interface Contract + +A gate is a small, independent recognizer that detects whether a particular relation family is likely relevant to the input. Gates are the cheapest component in the pipeline and run first, in parallel. + +--- + +## Design principle + +Gates are **high-recall by design**. It is better to activate an unnecessary specialist than to miss a needed one. A false positive costs budget. A false negative corrupts the answer. + +Gates do not solve the task. They do not produce objects, facets, or constraints. They only emit a `GateSignal`. + +--- + +## Input + +``` +GateInput { + text: string -- raw input text + context: string? -- optional prior context + span_start: integer? -- if operating on a specific span + span_end: integer? + gate_id: string -- which gate is being invoked +} +``` + +--- + +## Output + +``` +GateSignal { + gate_id: string -- must match the invoking gate_id + activated: boolean -- did this gate fire? + confidence: float [0, 1] -- confidence in the activation decision + evidence_spans: Span[]? -- text spans that triggered the gate + method: string -- "regex" | "rule" | "classifier" | "neural" | "hybrid" +} +``` + +See `schemas/routing.json#/$defs/GateSignal` for the normative schema. + +--- + +## Invariants + +Every gate implementation MUST satisfy all of the following: + +1. **Bounded output.** A gate emits exactly one `GateSignal`. No additional text, no secondary outputs. + +2. **Idempotent.** Calling a gate twice with the same input produces the same output. + +3. **No side effects.** A gate does not modify WorldState. It only reads input text. + +4. **Declared method.** The `method` field must accurately reflect how the gate works (regex, rule, classifier, neural, hybrid). This is used for diagnostic filtering. + +5. **High-recall posture.** When uncertain, a gate should prefer `activated: true` with lower confidence over `activated: false`. A gate should only return `activated: false, confidence: 0.99` if the signal is clearly and definitively absent. + +6. **Self-contained.** A gate may not depend on the outputs of other gates. Gates run in parallel with no inter-gate dependencies. + +--- + +## Standard gate IDs + +Gates are identified by string IDs. The following IDs are reserved and have defined semantics. Additional domain-specific gates may be added to the registry. + +| gate_id | Fires when input probably contains... | +|---|---| +| `is_math` | Numeric computation, arithmetic, algebra | +| `is_code` | Source code, pseudocode, program structures | +| `has_temporal_relation` | Before/after/during/recurring/sequence | +| `has_spatial_relation` | Location, containment, direction, proximity | +| `has_causal_relation` | Cause/effect/enable/block | +| `has_entity_reference` | Named or implied entities | +| `has_coreference` | Pronouns or aliases that refer back | +| `has_quantity` | Counts, amounts, measures, percentages | +| `has_logical_negation` | Not, unless, except, never | +| `has_comparison` | More/less/same/different/equal | +| `has_planning` | Steps, goals, prerequisites, sequencing | +| `has_constraint` | Requirements, restrictions, conditions | +| `has_state_change` | Status transitions, possession changes | +| `has_ownership_transfer` | Give, sell, take, receive, lose | +| `has_social_intent` | Belief, desire, obligation, deception, trust | +| `requires_external_knowledge` | World facts not inferable from input alone | +| `requires_synthesis_only` | Input is direct and needs no decomposition | + +--- + +## Testing a gate implementation + +A gate implementation is valid if it passes all of the following test classes: + +**Positive recall tests:** Given input known to contain the relation, the gate fires. +**True negative tests:** Given input that clearly lacks the relation, the gate does not fire. +**Confidence calibration tests:** Confidence scores are higher for clear positives than for ambiguous cases. +**Boundary tests:** Very short input, empty input, non-linguistic input (numbers only, code only) produce valid GateSignal objects. +**Idempotency tests:** Same input, same output on repeated calls. + +See `tests/contracts/gate_contract_tests.md` for test case templates. + +--- + +## Implementation guide + +A gate may be implemented in any language or framework, as long as it satisfies the contract above. Common approaches: + +- **Regex / keyword rules:** fast, zero training cost, fragile on unusual phrasing +- **Feature classifier:** small model trained on labeled examples, better generalization +- **Small neural classifier:** ~1M–10M parameter binary classifier, highest recall potential +- **Hybrid:** rules for high-confidence cases, classifier for uncertain cases + +The gate registry (`gates/registry.md`) specifies how to register a new gate implementation. diff --git a/gates/registry.md b/gates/registry.md new file mode 100644 index 0000000..344b7ab --- /dev/null +++ b/gates/registry.md @@ -0,0 +1,70 @@ +# Gate Registry + +The gate registry maps gate IDs to their implementations and metadata. The registry is the authoritative list of which gates are active in the system. + +--- + +## Registry entry format + +``` +GateRegistryEntry { + gate_id: string -- must match a standard gate_id or be a registered extension + display_name: string -- human-readable label + description: string -- one sentence on what this gate detects + method: string -- "regex" | "rule" | "classifier" | "neural" | "hybrid" + implementation: string -- reference to implementation module or file + version: string -- semver + enabled: boolean -- false to disable without removing from registry + recall_threshold: float [0, 1] -- minimum recall this gate is expected to maintain + activates_specialists: string[] -- hint: which specialists this gate commonly triggers + notes: string? -- optional +} +``` + +--- + +## Active gate registry (initial) + +These are the initial gate implementations. All use rule/regex methods as starting points. They will be upgraded to classifiers or neural models as the contract tests stabilize. + +| gate_id | method | activates_specialists | enabled | +|---|---|---|---| +| `is_math` | rule | quantity, logic | true | +| `is_code` | rule | code | true | +| `has_temporal_relation` | rule | temporal | true | +| `has_spatial_relation` | rule | spatial | true | +| `has_causal_relation` | rule | causal | true | +| `has_entity_reference` | rule | entity | true | +| `has_coreference` | rule | entity | true | +| `has_quantity` | rule | quantity | true | +| `has_logical_negation` | rule | logic | true | +| `has_comparison` | rule | quantity, logic | true | +| `has_planning` | rule | planning | true | +| `has_constraint` | rule | logic, planning | true | +| `has_state_change` | rule | ownership, temporal | true | +| `has_ownership_transfer` | rule | ownership, entity | true | +| `has_social_intent` | rule | social | true | +| `requires_external_knowledge` | rule | — | true | +| `requires_synthesis_only` | rule | — | true | + +--- + +## Adding a new gate + +1. Define a gate ID. Use `has_{relation_family}` for relation detectors and `is_{domain}` for domain detectors. +2. Write the implementation satisfying `gates/contract.md`. +3. Write at least 5 positive recall tests and 5 true negative tests per `tests/contracts/gate_contract_tests.md`. +4. Add an entry to this registry. +5. Update `activates_specialists` with which specialists this gate commonly triggers. This is a hint to the router, not a constraint. + +--- + +## Upgrading a gate implementation + +A gate's contract (gate_id, output schema, invariants) does not change when the implementation method changes. A regex gate may be replaced by a neural gate without any other component knowing. The registry `method` field and `version` field must be updated. + +--- + +## Disabling a gate + +Set `enabled: false`. Do not remove entries — removal loses the `activates_specialists` hint and breaks the audit trail. diff --git a/gates/simple_rules.md b/gates/simple_rules.md new file mode 100644 index 0000000..20c63f8 --- /dev/null +++ b/gates/simple_rules.md @@ -0,0 +1,223 @@ +# Simple Rule-Based Gate Definitions + +This file specifies the rule logic for the initial rule-based gate implementations. These are starter gates — high-recall, zero training cost, acceptable false positive rate. + +Each gate spec below is language-agnostic. Any implementation must produce identical `GateSignal` outputs for the given inputs. + +--- + +## Gate: `is_math` + +**Detects:** numeric computation, arithmetic, algebra, equations. + +**Rule logic:** + +1. Activate if input contains any digit sequence (regex: `\d+`) +2. Activate if input contains math keywords: `how many`, `total`, `sum`, `difference`, `product`, `divided by`, `minus`, `plus`, `equals`, `more than`, `less than`, `percent`, `ratio` +3. Activate if input contains operator symbols: `+`, `-`, `*`, `/`, `=`, `%`, `^` + +**Confidence assignment:** +- Multiple signals present → 0.95 +- One signal, multiple instances → 0.85 +- One signal, one instance → 0.70 + +--- + +## Gate: `has_entity_reference` + +**Detects:** named or implied entities (people, organizations, places, objects, artifacts). + +**Rule logic:** + +1. Activate if any token starts with a capital letter and is not the first word of the sentence +2. Activate if input contains proper-noun indicators: `Mr.`, `Ms.`, `Dr.`, `the`, followed by capitalized noun +3. Activate if input contains pronouns: `he`, `she`, `they`, `it`, `him`, `her`, `them` +4. Activate if input contains possessive markers: `Alice's`, `Bob's`, `its` + +**Confidence assignment:** +- Capital-letter token found → 0.90 +- Pronoun only → 0.75 +- Neither, but possessive marker found → 0.80 + +--- + +## Gate: `has_quantity` + +**Detects:** counts, amounts, measures, percentages, ordinals. + +**Rule logic:** + +1. Activate if input contains a digit sequence +2. Activate if input contains number words: `one`, `two`, `three`, ... `ten`, `hundred`, `thousand`, `million` +3. Activate if input contains measure words: `apples`, `dollars`, `meters`, `kilograms`, `minutes`, `hours`, `days` +4. Activate if input contains ordinals: `first`, `second`, `third`, `last` + +**Confidence assignment:** +- Digit + measure word → 0.99 +- Digit only → 0.85 +- Number word only → 0.80 + +--- + +## Gate: `has_ownership_transfer` + +**Detects:** give, sell, take, receive, lose, transfer of possession. + +**Rule logic:** + +1. Activate if input contains transfer verbs: `give`, `gave`, `gives`, `take`, `took`, `takes`, `sell`, `sold`, `sells`, `receive`, `received`, `receives`, `lose`, `lost`, `loses`, `transfer`, `transferred`, `hand`, `handed`, `pass`, `passed` +2. Activate if input contains indirect-object patterns: `gave [entity] [object]`, `[entity] sold [object] to [entity]` + +**Confidence assignment:** +- Transfer verb present → 0.92 +- Indirect-object pattern confirmed → 0.97 + +--- + +## Gate: `has_temporal_relation` + +**Detects:** before/after/during/recurring/sequential ordering. + +**Rule logic:** + +1. Activate if input contains temporal connectives: `before`, `after`, `during`, `when`, `then`, `next`, `later`, `previously`, `first`, `second`, `finally`, `since`, `until`, `while` +2. Activate if input contains time expressions: `yesterday`, `tomorrow`, `at noon`, `on Monday`, day/month/year patterns +3. Activate if input contains sequence markers: `step 1`, `then`, `afterward`, `subsequently` + +**Confidence assignment:** +- Temporal connective + time expression → 0.97 +- Temporal connective only → 0.82 +- Sequence marker only → 0.75 + +--- + +## Gate: `has_logical_negation` + +**Detects:** not, never, unless, except, no, without, deny, false. + +**Rule logic:** + +1. Activate if input contains negation words: `not`, `no`, `never`, `none`, `neither`, `nor`, `without`, `unless`, `except`, `deny`, `denied`, `false`, `incorrect`, `wrong` +2. Activate if input contains negative contractions: `isn't`, `aren't`, `wasn't`, `weren't`, `doesn't`, `don't`, `didn't`, `can't`, `won't`, `wouldn't`, `couldn't`, `shouldn't` + +**Confidence assignment:** +- Negation word found → 0.92 + +--- + +## Gate: `has_causal_relation` + +**Detects:** cause, effect, enable, block, because, therefore, so. + +**Rule logic:** + +1. Activate if input contains causal connectives: `because`, `therefore`, `so`, `thus`, `hence`, `consequently`, `as a result`, `due to`, `caused by`, `leads to`, `results in`, `enables`, `prevents`, `blocks`, `allows` + +**Confidence assignment:** +- Causal connective found → 0.90 + +--- + +## Gate: `has_coreference` + +**Detects:** pronouns and aliases that refer to previously mentioned entities. + +**Rule logic:** + +1. Activate if input contains personal pronouns: `he`, `she`, `they`, `it`, `him`, `her`, `them`, `his`, `hers`, `their`, `its` +2. Activate if input contains demonstrative references: `this`, `that`, `these`, `those` followed by a noun that was mentioned earlier +3. Activate if multiple entities are named AND pronouns are present + +**Confidence assignment:** +- Pronoun present + multiple named entities → 0.90 +- Pronoun only, single entity → 0.70 + +--- + +## Gate: `has_spatial_relation` + +**Detects:** location, containment, direction, proximity. + +**Rule logic:** + +1. Activate if input contains spatial prepositions: `in`, `at`, `on`, `above`, `below`, `inside`, `outside`, `near`, `far`, `left`, `right`, `north`, `south`, `east`, `west`, `between`, `next to`, `adjacent to` +2. Activate if input contains place names (capitalized geographics) or location indicators: `street`, `city`, `country`, `room`, `building`, `floor` + +**Confidence assignment:** +- Spatial preposition + place indicator → 0.92 +- Spatial preposition only → 0.72 + +--- + +## Gate: `has_planning` + +**Detects:** goals, steps, prerequisites, sequencing of actions. + +**Rule logic:** + +1. Activate if input contains planning vocabulary: `plan`, `goal`, `step`, `procedure`, `prerequisite`, `depends on`, `requires`, `in order to`, `so that`, `to achieve` +2. Activate if input contains numbered or ordered list markers + +**Confidence assignment:** +- Planning vocabulary present → 0.88 + +--- + +## Gate: `has_social_intent` + +**Detects:** beliefs, desires, obligations, deception, trust, social roles. + +**Rule logic:** + +1. Activate if input contains mental state verbs: `believe`, `believes`, `think`, `thinks`, `want`, `wants`, `know`, `knows`, `intend`, `intends`, `expect`, `expects`, `hope`, `hopes` +2. Activate if input contains obligation/permission words: `must`, `should`, `ought`, `may`, `allowed`, `permitted`, `required`, `obligated` +3. Activate if input contains social verbs: `promise`, `lie`, `deceive`, `trust`, `cooperate`, `compete`, `agree`, `refuse` + +**Confidence assignment:** +- Mental state verb present → 0.85 +- Obligation/permission word → 0.80 +- Social verb → 0.90 + +--- + +## Gate: `requires_external_knowledge` + +**Detects:** queries that require facts not present in the input text. + +**Rule logic:** + +1. Activate if input contains knowledge-seeking patterns: `who is`, `what is`, `where is`, `when did`, `how does`, `why does` combined with named entities +2. Activate if input references historical events, scientific facts, or world geography without providing context + +**Note:** This gate has higher false-positive tolerance than most. When uncertain, fire. + +**Confidence assignment:** +- Knowledge-seeking pattern with named entity → 0.85 +- Named entity without provided context → 0.70 + +--- + +## Gate: `requires_synthesis_only` + +**Detects:** inputs that are complete, self-contained, and require no structural decomposition. + +**Rule logic:** + +1. Activate only if all of the following hold: + - No other gates fire with confidence > 0.70 + - Input is a direct factual statement with no quantities, entities, or relations to track + - Input length < 15 tokens + +**Note:** This gate is a low-priority catch-all. It should rarely fire when other gates fire. + +**Confidence assignment:** +- All conditions met → 0.80 + +--- + +## Implementation notes + +- All keyword lists should be treated as seed lists. Implementors should extend them for their target domain. +- Case-insensitive matching is preferred. +- Lemmatization or stemming is recommended for verb forms. +- Do not hardcode language; design for swappable tokenizers and keyword tables. diff --git a/resolver/contract.md b/resolver/contract.md new file mode 100644 index 0000000..1a8070e --- /dev/null +++ b/resolver/contract.md @@ -0,0 +1,133 @@ +# Resolver Interface Contract + +The resolver is the UCWM core. It receives WorldState after specialists have attached their facets and constraints, and it produces a stabilized WorldState ready for synthesis. + +--- + +## Design principles + +1. **Never adds meaning.** The resolver does not infer new facts beyond what constraint propagation and arithmetic require. It resolves existing constraints; it does not guess. + +2. **Contradiction-explicit.** When constraints contradict, the resolver records the contradiction in `open_contradictions` rather than silently discarding either side. + +3. **Multiple hypotheses.** When ambiguity is unresolvable, the resolver maintains multiple hypotheses in WorldState rather than forcing a single resolution. + +4. **Monotonic confidence.** Constraint reinforcement raises confidence. Contradiction lowers confidence. Confidence never increases without supporting evidence. + +5. **Traceable.** Every operation the resolver performs is recorded in `resolution_log` with the affected refs and the reason. + +--- + +## Input + +``` +ResolverInput { + world_state: WorldState -- post-specialist state, stage = "post_specialist" + resolution_mode: string -- "single_pass" | "iterative" | "conflict_checking" + max_iterations: integer? -- for iterative mode (default: 5) +} +``` + +--- + +## Output + +``` +ResolverOutput { + resolved_state: WorldState -- stage = "resolved" + resolution_log: ResolutionEntry[] + open_contradictions: ContradictionRecord[] + confidence_summary: ConfidenceSummary +} + +ResolutionEntry { + operation: string -- see Operations below + affected_refs: string[] + result: string -- "resolved" | "contradicted" | "suspended" | "merged" | "split" + confidence_after: float? + note: string? +} + +ContradictionRecord { + constraint_refs: string[] -- constraints that contradict each other + description: string + resolution_attempt: string? -- what the resolver tried before giving up +} +``` + +--- + +## Operations + +The resolver applies operations in this order: + +### 1. Validate all refs + +Before any operation, verify all `argument_refs` and `object_refs` resolve to objects in WorldState. Emit warnings for any dangling refs. Do not proceed with a constraint that has dangling refs. + +### 2. Apply hard constraints + +Apply all constraints with `strength: hard`. If two hard constraints contradict, record the contradiction immediately and mark both as `contradicted`. Do not attempt to resolve hard-vs-hard contradictions silently. + +### 3. Resolve arithmetic + +Apply `quantity_difference`, `quantity_sum`, `quantity_product`, `quantity_quotient`, `quantity_equals` where all operands are known exact values. Update the result quantity object's QuantityFacet `value` field. + +### 4. Propagate truth values + +Apply `truth_value` constraints. Propagate via `implies` chains. If `implies(P1, P2)` and P1 is true, mark P2 as true. If P1 is false, no propagation (modus tollens requires explicit encoding). + +### 5. Resolve coreference + +Apply `same_entity` constraints. For each pair (A, B) with `same_entity(A, B)`: +- Merge B into A (or A into B, by priority: higher confidence object survives) +- Set the lower-confidence object to `status: merged`, `merged_into: ` +- Transfer all facet_refs and constraint_refs to the surviving object + +### 6. Detect contradictions from soft/defeasible constraints + +Apply soft and defeasible constraints. When a soft constraint contradicts a hard constraint, suspend the soft constraint. When two soft constraints contradict, mark both with lower confidence and record in `open_contradictions`. + +### 7. Confidence propagation + +For each constraint that was resolved: +- Increase confidence on objects/facets that the constraint reinforces +- Decrease confidence on objects/facets that the constraint contradicts + +Confidence deltas must be bounded: a single constraint may not move confidence by more than 0.15 in either direction. + +### 8. Produce resolved WorldState + +Update all constraint statuses. Objects with `status: proposed` that have at least one resolved constraint referencing them may be upgraded to `status: active`. Mark WorldState stage as `resolved`. + +--- + +## Resolution modes + +**single_pass:** Apply all operations once. Fast, suitable for simple inputs. + +**iterative:** Apply operations repeatedly until no new resolutions occur or `max_iterations` is reached. Use for inputs with chains of implications. + +**conflict_checking:** Extra pass after resolution: verify that no two hard constraints in `resolved` status contradict each other. Emit detailed contradiction records if found. Use when input may contain adversarial or self-contradictory statements. + +--- + +## Invariants + +1. **No new objects.** The resolver does not propose new objects. It may merge or split existing ones, but may not create new objects from thin air. + +2. **Idempotent application.** Applying a resolved constraint again does not change WorldState. + +3. **Contradiction is never silent.** Every detected contradiction must appear in `open_contradictions` or in `resolution_log` with `result: contradicted`. + +4. **Log completeness.** Every state change must have a corresponding entry in `resolution_log`. + +--- + +## Contract test requirements + +- Given worldstate with `quantity_difference(Q3, Q1, Q2)`, Q1.value=5, Q2.value=2: resolver must set Q3.value=3 and mark C3 resolved +- Given `same_entity(E1, E3)`, E1.confidence=0.97, E3.confidence=0.72: E3 must be merged into E1 +- Given `before(EV1, EV2)` and `before(EV2, EV1)` (cycle): must emit a ContradictionRecord, not silently resolve +- `resolution_log` must be non-empty after any state change +- All constraints must have updated `status` after resolver runs diff --git a/resolver/operations.md b/resolver/operations.md new file mode 100644 index 0000000..cda6248 --- /dev/null +++ b/resolver/operations.md @@ -0,0 +1,256 @@ +# Resolver Operations Reference + +Detailed pseudocode for each resolver operation. These are language-agnostic specifications; any implementation must produce equivalent results. + +--- + +## Op 1: ValidateRefs + +``` +procedure ValidateRefs(world_state): + warnings = [] + for each constraint C in world_state.constraints: + for each ref in C.argument_refs: + if ref not in world_state.objects: + warnings.append(f"Dangling ref {ref} in constraint {C.constraint_id}") + mark C.status = suspended + for each facet F in world_state.facets: + if F.object_ref not in world_state.objects: + warnings.append(f"Dangling object_ref {F.object_ref} in facet {F.facet_id}") + return warnings +``` + +--- + +## Op 2: ApplyHardConstraints + +``` +procedure ApplyHardConstraints(world_state): + hard_constraints = [C for C in world_state.constraints where C.strength = "hard" and C.status = "unresolved"] + + for each C in hard_constraints: + conflicts = [C2 for C2 in hard_constraints + where C2 ≠ C + and C2.status = "unresolved" + and DirectlyContradicts(C, C2)] + if conflicts: + for C2 in conflicts: + mark C.status = "contradicted" + mark C2.status = "contradicted" + add ContradictionRecord(constraint_refs=[C.id, C2.id]) + else: + mark C.status = "resolved" + log ResolutionEntry(operation="apply_hard", affected_refs=C.argument_refs, result="resolved") +``` + +``` +function DirectlyContradicts(C1, C2): + -- Same type, same arguments, opposite polarity + if C1.constraint_type = C2.constraint_type + and C1.argument_refs = C2.argument_refs + and C1.polarity ≠ C2.polarity: + return true + -- Known incompatible pairs + if {C1.constraint_type, C2.constraint_type} = {"before", "after"} + and C1.argument_refs = reverse(C2.argument_refs): + return true + return false +``` + +--- + +## Op 3: ResolveArithmetic + +``` +procedure ResolveArithmetic(world_state): + arithmetic_types = { + "quantity_difference": (result, minuend, subtrahend) -> minuend - subtrahend, + "quantity_sum": (result, *addends) -> sum(addends), + "quantity_product": (result, *factors) -> product(factors), + "quantity_quotient": (result, dividend, divisor) -> dividend / divisor, + "quantity_equals": (result, value) -> value + } + + for each C in world_state.constraints where C.constraint_type in arithmetic_types: + if C.status ≠ "unresolved": + continue + + arg_objects = [world_state.objects[ref] for ref in C.argument_refs] + result_obj = arg_objects[0] + operands = arg_objects[1:] + + operand_values = [GetQuantityValue(O) for O in operands] + + if all(V is not None and is_exact(V) for V in operand_values): + fn = arithmetic_types[C.constraint_type] + computed = fn(*operand_values) + + SetQuantityValue(result_obj, computed) + result_facet = GetOrCreateQuantityFacet(result_obj) + result_facet.value = computed + result_facet.is_exact = true + result_facet.derived_from_refs = [O.object_id for O in operands] + + mark C.status = "resolved" + log ResolutionEntry(operation="resolve_arithmetic", + affected_refs=[result_obj.object_id], + result="resolved", + note=f"computed {computed}") + else: + -- operands not yet resolved; defer + pass +``` + +--- + +## Op 4: PropagateTruth + +``` +procedure PropagateTruth(world_state): + implies_constraints = [C for C in world_state.constraints + where C.constraint_type = "implies" and C.status = "resolved"] + + changed = true + while changed: + changed = false + for each C in implies_constraints: + antecedent_id, consequent_id = C.argument_refs + antecedent = world_state.objects[antecedent_id] + consequent = world_state.objects[consequent_id] + + if GetTruthStatus(antecedent) = "true": + if GetTruthStatus(consequent) ≠ "true": + SetTruthStatus(consequent, "true") + log ResolutionEntry(operation="propagate_truth", + affected_refs=[consequent_id], + result="resolved", + note=f"derived from {antecedent_id} via implies constraint {C.id}") + changed = true + + if GetTruthStatus(antecedent) = "false": + -- modus tollens: do not propagate without explicit negation constraint + pass +``` + +--- + +## Op 5: ResolveCoreference + +``` +procedure ResolveCoreference(world_state): + same_entity_constraints = [C for C in world_state.constraints + where C.constraint_type = "same_entity" and C.status = "resolved"] + + for each C in same_entity_constraints: + id_a, id_b = C.argument_refs + obj_a = world_state.objects[id_a] + obj_b = world_state.objects[id_b] + + if obj_a.confidence >= obj_b.confidence: + survivor = obj_a + deprecated = obj_b + else: + survivor = obj_b + deprecated = obj_a + + -- Transfer all refs from deprecated to survivor + for ref in deprecated.facet_refs: + world_state.facets[ref].object_ref = survivor.object_id + survivor.facet_refs.append(ref) + + for ref in deprecated.constraint_refs: + constraint = world_state.constraints[ref] + replace deprecated.object_id with survivor.object_id in constraint.argument_refs + survivor.constraint_refs.append(ref) + + -- Transfer aliases + survivor.aliases += deprecated.aliases + [deprecated.canonical_label] + + deprecated.status = "merged" + deprecated.merged_into = survivor.object_id + + log ResolutionEntry(operation="merge_coreference", + affected_refs=[survivor.object_id, deprecated.object_id], + result="merged", + note=f"merged {deprecated.object_id} into {survivor.object_id}") +``` + +--- + +## Op 6: DetectSoftContradictions + +``` +procedure DetectSoftContradictions(world_state): + soft_constraints = [C for C in world_state.constraints + where C.strength in {"soft", "defeasible"} and C.status = "unresolved"] + + for each C in soft_constraints: + -- Check against hard resolved constraints first + hard_conflicts = [C2 for C2 in world_state.constraints + where C2.strength = "hard" and C2.status = "resolved" + and DirectlyContradicts(C, C2)] + if hard_conflicts: + mark C.status = "suspended" + log ResolutionEntry(operation="suspend_soft", + affected_refs=C.argument_refs, + result="suspended", + note=f"overridden by hard constraint {hard_conflicts[0].id}") + continue + + -- Check against other soft constraints + soft_conflicts = [C2 for C2 in soft_constraints + where C2 ≠ C and DirectlyContradicts(C, C2)] + if soft_conflicts: + C.confidence *= 0.7 + for C2 in soft_conflicts: + C2.confidence *= 0.7 + add ContradictionRecord(constraint_refs=[C.id] + [C2.id for C2 in soft_conflicts], + description="Soft constraint conflict, confidence reduced") + else: + mark C.status = "resolved" +``` + +--- + +## Op 7: PropagateConfidence + +``` +procedure PropagateConfidence(world_state): + REINFORCE_DELTA = 0.05 + CONTRADICT_DELTA = 0.10 + MAX_DELTA = 0.15 + + for each constraint C where C.status = "resolved": + for ref in C.argument_refs: + obj = world_state.objects.get(ref) + if obj: + delta = min(REINFORCE_DELTA * C.confidence, MAX_DELTA) + obj.confidence = min(1.0, obj.confidence + delta) + + for each constraint C where C.status = "contradicted": + for ref in C.argument_refs: + obj = world_state.objects.get(ref) + if obj: + delta = min(CONTRADICT_DELTA, MAX_DELTA) + obj.confidence = max(0.0, obj.confidence - delta) +``` + +--- + +## Op 8: FinalizeState + +``` +procedure FinalizeState(world_state): + -- Promote high-confidence proposed objects + for each obj in world_state.objects where obj.status = "proposed": + resolved_constraints = [C for C in world_state.constraints + where obj.object_id in C.argument_refs + and C.status = "resolved"] + if obj.confidence >= 0.75 and len(resolved_constraints) >= 1: + obj.status = "active" + log ResolutionEntry(operation="promote_object", + affected_refs=[obj.object_id], + result="resolved") + + world_state.stage = "resolved" +``` diff --git a/schemas/constraints.json b/schemas/constraints.json new file mode 100644 index 0000000..9ee7ac6 --- /dev/null +++ b/schemas/constraints.json @@ -0,0 +1,155 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "ucwm/schemas/constraints.json", + "title": "Constraint", + "description": "The core reasoning currency of UCWM. An explicit relation, restriction, incompatibility, requirement, or preference involving objects and/or facets.", + "type": "object", + "required": ["constraint_id", "constraint_type", "argument_refs", "polarity", "strength", "status", "source_module"], + "additionalProperties": false, + "properties": { + "constraint_id": { + "type": "string", + "description": "Unique identifier. Format: C{sequence} e.g. C1, C14" + }, + "constraint_type": { + "type": "string", + "description": "Named relation type. Implementors should define allowed values per domain.", + "examples": [ + "before", + "after", + "during", + "causes", + "enables", + "blocks", + "same_entity", + "not_same_entity", + "owns", + "transfers", + "inside", + "outside", + "adjacent", + "quantity_equals", + "quantity_sum", + "quantity_difference", + "quantity_product", + "quantity_greater", + "quantity_less", + "truth_value", + "implies", + "negates", + "contradicts", + "prerequisite", + "goal", + "has_role" + ] + }, + "argument_refs": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "description": "Ordered list of object_ids or facet_ids this constraint relates. Order is significant for asymmetric relations." + }, + "polarity": { + "type": "string", + "enum": ["positive", "negative"], + "description": "Whether this constraint asserts or denies the relation." + }, + "strength": { + "type": "string", + "enum": ["hard", "soft", "probabilistic", "defeasible"], + "description": "hard=must hold; soft=preferred; probabilistic=holds with stated probability; defeasible=default, retractable by new evidence" + }, + "probability": { + "type": "number", + "minimum": 0.0, + "maximum": 1.0, + "description": "Required when strength=probabilistic." + }, + "status": { + "type": "string", + "enum": ["unresolved", "resolved", "contradicted", "suspended"], + "description": "Resolver lifecycle status.", + "default": "unresolved" + }, + "expression": { + "type": "string", + "description": "Optional human-readable or formal logic expression of the constraint. Informational; not authoritative.", + "examples": [ + "before(E1, E2)", + "quantity(Q1) = quantity(Q2) - 3", + "owner(O3) = Entity_Alice" + ] + }, + "confidence": { + "type": "number", + "minimum": 0.0, + "maximum": 1.0, + "description": "Confidence that this constraint is correctly extracted." + }, + "contradicted_by_refs": { + "type": "array", + "items": { "type": "string" }, + "description": "constraint_ids that contradict this one. Populated by the resolver.", + "default": [] + }, + "supports_refs": { + "type": "array", + "items": { "type": "string" }, + "description": "constraint_ids that reinforce this one.", + "default": [] + }, + "source_module": { + "type": "string", + "description": "Specialist or module that emitted this constraint." + }, + "provenance_refs": { + "type": "array", + "items": { "type": "string" }, + "default": [] + } + }, + "examples": [ + { + "constraint_id": "C1", + "constraint_type": "owns", + "argument_refs": ["E1", "O_apples", "Q1"], + "polarity": "positive", + "strength": "hard", + "status": "resolved", + "expression": "owns(Alice, apples, count=5) at t0", + "confidence": 0.98, + "source_module": "ownership_specialist", + "contradicted_by_refs": [], + "supports_refs": [], + "provenance_refs": ["P1"] + }, + { + "constraint_id": "C2", + "constraint_type": "before", + "argument_refs": ["EV_initial_possession", "EV_transfer"], + "polarity": "positive", + "strength": "hard", + "status": "resolved", + "expression": "before(E_initial_possession, E_transfer)", + "confidence": 0.99, + "source_module": "temporal_specialist", + "contradicted_by_refs": [], + "supports_refs": [], + "provenance_refs": ["P2"] + }, + { + "constraint_id": "C3", + "constraint_type": "quantity_difference", + "argument_refs": ["Q_alice_final", "Q1", "Q2"], + "polarity": "positive", + "strength": "hard", + "status": "resolved", + "expression": "quantity(Q_alice_final) = quantity(Q1) - quantity(Q2)", + "confidence": 0.99, + "source_module": "quantity_specialist", + "contradicted_by_refs": [], + "supports_refs": [], + "provenance_refs": ["P3"] + } + ] +} diff --git a/schemas/facets.json b/schemas/facets.json new file mode 100644 index 0000000..f5f1483 --- /dev/null +++ b/schemas/facets.json @@ -0,0 +1,331 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "ucwm/schemas/facets.json", + "title": "Facets", + "description": "Typed partial views that attach to CanonicalObjects. Facets are optional and additive — not every object exists in every relation type.", + "$defs": { + "BaseFacet": { + "type": "object", + "required": ["facet_id", "facet_kind", "object_ref", "confidence", "source_module"], + "properties": { + "facet_id": { + "type": "string", + "description": "Unique identifier for this facet instance." + }, + "facet_kind": { + "type": "string", + "enum": [ + "temporal", + "spatial", + "causal", + "logical", + "syntactic", + "semantic", + "social", + "ownership", + "code_structure", + "quantity", + "planning" + ] + }, + "object_ref": { + "type": "string", + "description": "The object_id this facet is attached to." + }, + "confidence": { + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + }, + "source_module": { + "type": "string", + "description": "Specialist module that produced this facet." + }, + "provenance_refs": { + "type": "array", + "items": { "type": "string" }, + "default": [] + } + } + }, + + "TemporalFacet": { + "allOf": [ + { "$ref": "#/$defs/BaseFacet" }, + { + "type": "object", + "description": "Temporal description: when this object exists, begins, ends, or recurs.", + "properties": { + "facet_kind": { "const": "temporal" }, + "time_point": { + "type": "string", + "description": "Absolute or relative time point. ISO 8601 when known." + }, + "interval_start": { "type": "string" }, + "interval_end": { "type": "string" }, + "temporal_order_refs": { + "type": "array", + "items": { "type": "string" }, + "description": "IDs of constraint records expressing before/after/during relations." + }, + "is_recurring": { "type": "boolean", "default": false }, + "recurrence_pattern": { + "type": "string", + "description": "Natural language or ISO 8601 recurrence expression." + }, + "duration": { "type": "string" } + } + } + ] + }, + + "SpatialFacet": { + "allOf": [ + { "$ref": "#/$defs/BaseFacet" }, + { + "type": "object", + "description": "Spatial description: where this object is, was, or will be.", + "properties": { + "facet_kind": { "const": "spatial" }, + "location_label": { "type": "string" }, + "coordinates": { + "type": "object", + "properties": { + "x": { "type": "number" }, + "y": { "type": "number" }, + "z": { "type": "number" } + } + }, + "containment_refs": { + "type": "array", + "items": { "type": "string" }, + "description": "Constraint IDs expressing inside/outside/adjacent relations." + }, + "reference_frame": { + "type": "string", + "description": "The coordinate system or reference object." + } + } + } + ] + }, + + "CausalFacet": { + "allOf": [ + { "$ref": "#/$defs/BaseFacet" }, + { + "type": "object", + "description": "Causal description: what caused or was caused by this object.", + "properties": { + "facet_kind": { "const": "causal" }, + "cause_refs": { + "type": "array", + "items": { "type": "string" }, + "description": "IDs of objects or events that caused this one." + }, + "effect_refs": { + "type": "array", + "items": { "type": "string" }, + "description": "IDs of objects or events caused by this one." + }, + "enabling_refs": { + "type": "array", + "items": { "type": "string" } + }, + "blocking_refs": { + "type": "array", + "items": { "type": "string" } + }, + "causal_strength": { + "type": "string", + "enum": ["necessary", "sufficient", "contributing", "correlational"] + } + } + } + ] + }, + + "LogicalFacet": { + "allOf": [ + { "$ref": "#/$defs/BaseFacet" }, + { + "type": "object", + "description": "Logical description: truth value, negation, quantification, implication.", + "properties": { + "facet_kind": { "const": "logical" }, + "truth_status": { + "type": "string", + "enum": ["true", "false", "unknown", "hypothetical", "negated", "asserted"] + }, + "quantifier": { + "type": "string", + "enum": ["universal", "existential", "none"] + }, + "implies_refs": { + "type": "array", + "items": { "type": "string" }, + "description": "IDs of propositions this object implies." + }, + "negates_refs": { + "type": "array", + "items": { "type": "string" } + }, + "contradicts_refs": { + "type": "array", + "items": { "type": "string" } + } + } + } + ] + }, + + "OwnershipFacet": { + "allOf": [ + { "$ref": "#/$defs/BaseFacet" }, + { + "type": "object", + "description": "Ownership and possession description: who holds what, and when.", + "properties": { + "facet_kind": { "const": "ownership" }, + "owner_ref": { + "type": "string", + "description": "object_id of the current owner." + }, + "owned_ref": { + "type": "string", + "description": "object_id of the thing owned." + }, + "quantity_ref": { + "type": "string", + "description": "object_id of a quantity object if ownership is of a counted resource." + }, + "temporal_ref": { + "type": "string", + "description": "Facet ID providing temporal scope for this ownership." + }, + "transfer_type": { + "type": "string", + "enum": ["give", "sell", "take", "lose", "receive", "initial", "final"] + } + } + } + ] + }, + + "QuantityFacet": { + "allOf": [ + { "$ref": "#/$defs/BaseFacet" }, + { + "type": "object", + "description": "Quantity description: numeric value, unit, precision.", + "properties": { + "facet_kind": { "const": "quantity" }, + "value": { + "oneOf": [ + { "type": "number" }, + { "type": "string", "description": "Symbolic expression e.g. 'Q1 - 2'" } + ] + }, + "unit": { "type": "string" }, + "precision": { "type": "number" }, + "is_exact": { "type": "boolean" }, + "derived_from_refs": { + "type": "array", + "items": { "type": "string" }, + "description": "object_ids of quantities this was computed from." + } + } + } + ] + }, + + "SyntacticFacet": { + "allOf": [ + { "$ref": "#/$defs/BaseFacet" }, + { + "type": "object", + "description": "Syntactic description: role in sentence structure.", + "properties": { + "facet_kind": { "const": "syntactic" }, + "grammatical_role": { + "type": "string", + "enum": ["subject", "object", "indirect_object", "adjunct", "predicate", "modifier", "other"] + }, + "head_token": { "type": "string" }, + "dependency_arc": { "type": "string" } + } + } + ] + }, + + "SocialFacet": { + "allOf": [ + { "$ref": "#/$defs/BaseFacet" }, + { + "type": "object", + "description": "Social and agentive description: beliefs, intentions, obligations, roles.", + "properties": { + "facet_kind": { "const": "social" }, + "agent_ref": { "type": "string" }, + "social_act": { + "type": "string", + "enum": ["assert", "request", "promise", "threaten", "deceive", "inform", "ask", "other"] + }, + "belief_about_ref": { "type": "string" }, + "belief_truth_status": { + "type": "string", + "enum": ["true", "false", "unknown"] + }, + "obligation_refs": { + "type": "array", + "items": { "type": "string" } + }, + "role": { "type": "string" } + } + } + ] + }, + + "PlanningFacet": { + "allOf": [ + { "$ref": "#/$defs/BaseFacet" }, + { + "type": "object", + "description": "Planning description: steps, goals, prerequisites, dependencies.", + "properties": { + "facet_kind": { "const": "planning" }, + "step_index": { "type": "integer" }, + "prerequisite_refs": { + "type": "array", + "items": { "type": "string" } + }, + "goal_refs": { + "type": "array", + "items": { "type": "string" } + }, + "blocks_refs": { + "type": "array", + "items": { "type": "string" } + }, + "status": { + "type": "string", + "enum": ["pending", "in_progress", "complete", "blocked", "skipped"] + } + } + } + ] + } + }, + + "oneOf": [ + { "$ref": "#/$defs/TemporalFacet" }, + { "$ref": "#/$defs/SpatialFacet" }, + { "$ref": "#/$defs/CausalFacet" }, + { "$ref": "#/$defs/LogicalFacet" }, + { "$ref": "#/$defs/OwnershipFacet" }, + { "$ref": "#/$defs/QuantityFacet" }, + { "$ref": "#/$defs/SyntacticFacet" }, + { "$ref": "#/$defs/SocialFacet" }, + { "$ref": "#/$defs/PlanningFacet" } + ] +} diff --git a/schemas/objects.json b/schemas/objects.json new file mode 100644 index 0000000..2d28137 --- /dev/null +++ b/schemas/objects.json @@ -0,0 +1,128 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "ucwm/schemas/objects.json", + "title": "CanonicalObject", + "description": "The primary reusable referent inside UCWM. A stable anchor that facets and constraints attach to.", + "type": "object", + "required": ["object_id", "object_kind", "status", "confidence"], + "additionalProperties": false, + "properties": { + "object_id": { + "type": "string", + "description": "Unique identifier. Format: {kind_prefix}_{sequence} e.g. E1, O4, Q7", + "pattern": "^[A-Z][A-Z0-9]*_[0-9]+$|^[A-Z][0-9]+$" + }, + "object_kind": { + "type": "string", + "enum": [ + "entity", + "event", + "concept", + "proposition", + "relation_instance", + "procedure_step", + "code_object", + "quantity", + "claim", + "state" + ], + "description": "The ontological kind of this object." + }, + "status": { + "type": "string", + "enum": ["proposed", "active", "merged", "split", "invalidated"], + "description": "Lifecycle status. Proposed objects are unconfirmed. Merged objects were unified with another. Split objects produced multiple descendants.", + "default": "proposed" + }, + "confidence": { + "type": "number", + "minimum": 0.0, + "maximum": 1.0, + "description": "Aggregate confidence that this object is correctly proposed." + }, + "surface_span": { + "type": "object", + "description": "The text span(s) that evidence this object.", + "properties": { + "text": { "type": "string" }, + "start": { "type": "integer" }, + "end": { "type": "integer" } + }, + "additionalProperties": false + }, + "canonical_label": { + "type": "string", + "description": "Human-readable canonical name for this object." + }, + "aliases": { + "type": "array", + "items": { "type": "string" }, + "description": "Alternative names, pronouns, or references that may corefer to this object." + }, + "merged_into": { + "type": "string", + "description": "object_id of the object this was merged into. Populated only when status=merged." + }, + "split_from": { + "type": "string", + "description": "object_id of the object this was split from. Populated only when status=split." + }, + "facet_refs": { + "type": "array", + "items": { "type": "string" }, + "description": "IDs of Facet objects attached to this canonical object.", + "default": [] + }, + "constraint_refs": { + "type": "array", + "items": { "type": "string" }, + "description": "IDs of Constraints that reference this object.", + "default": [] + }, + "evidence_refs": { + "type": "array", + "items": { "type": "string" }, + "description": "IDs of evidence or provenance records supporting this object.", + "default": [] + }, + "provenance_refs": { + "type": "array", + "items": { "type": "string" }, + "description": "IDs of Provenance records tracking how this object was created.", + "default": [] + }, + "source_module": { + "type": "string", + "description": "Module ID that proposed this object." + } + }, + "examples": [ + { + "object_id": "E1", + "object_kind": "entity", + "status": "active", + "confidence": 0.97, + "surface_span": { "text": "Alice", "start": 0, "end": 5 }, + "canonical_label": "Alice", + "aliases": ["she", "her"], + "facet_refs": ["F_E1_ownership", "F_E1_temporal"], + "constraint_refs": ["C1", "C3"], + "evidence_refs": [], + "provenance_refs": ["P1"], + "source_module": "entity_specialist" + }, + { + "object_id": "Q1", + "object_kind": "quantity", + "status": "active", + "confidence": 0.99, + "surface_span": { "text": "5 apples", "start": 10, "end": 17 }, + "canonical_label": "5", + "facet_refs": ["F_Q1_quantity"], + "constraint_refs": ["C1"], + "evidence_refs": [], + "provenance_refs": ["P2"], + "source_module": "quantity_specialist" + } + ] +} diff --git a/schemas/provenance.json b/schemas/provenance.json new file mode 100644 index 0000000..4086017 --- /dev/null +++ b/schemas/provenance.json @@ -0,0 +1,80 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "ucwm/schemas/provenance.json", + "title": "Provenance", + "description": "Tracks where every fact, object, facet, and constraint came from. Enables failure localization and explainability.", + "type": "object", + "required": ["provenance_id", "record_type", "source_module", "timestamp"], + "additionalProperties": false, + "properties": { + "provenance_id": { + "type": "string", + "description": "Unique identifier. Format: P{sequence}" + }, + "record_type": { + "type": "string", + "enum": ["object_proposal", "facet_attachment", "constraint_emission", "resolver_operation", "object_merge", "object_split", "synthesis_step"], + "description": "What kind of operation created this provenance record." + }, + "source_module": { + "type": "string", + "description": "Module ID that triggered this event." + }, + "input_refs": { + "type": "array", + "items": { "type": "string" }, + "description": "IDs of objects, facets, or constraints that were inputs to the operation.", + "default": [] + }, + "output_refs": { + "type": "array", + "items": { "type": "string" }, + "description": "IDs of objects, facets, or constraints produced by the operation.", + "default": [] + }, + "evidence_span": { + "type": "object", + "description": "Text span that provided evidence for this operation.", + "properties": { + "text": { "type": "string" }, + "start": { "type": "integer" }, + "end": { "type": "integer" } + } + }, + "confidence": { + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + }, + "timestamp": { + "type": "string", + "description": "ISO 8601 timestamp of when this operation occurred." + }, + "notes": { + "type": "string", + "description": "Optional free-text note. For debugging only — not part of the reasoning chain." + } + }, + "examples": [ + { + "provenance_id": "P1", + "record_type": "object_proposal", + "source_module": "entity_specialist", + "input_refs": [], + "output_refs": ["E1"], + "evidence_span": { "text": "Alice", "start": 0, "end": 5 }, + "confidence": 0.97, + "timestamp": "2026-05-01T00:00:00Z" + }, + { + "provenance_id": "P4", + "record_type": "resolver_operation", + "source_module": "resolver", + "input_refs": ["C1", "C2", "C3"], + "output_refs": ["Q_alice_final"], + "confidence": 0.99, + "timestamp": "2026-05-01T00:00:01Z", + "notes": "Arithmetic resolution: 5 - 2 = 3" + } + ] +} diff --git a/schemas/routing.json b/schemas/routing.json new file mode 100644 index 0000000..547db50 --- /dev/null +++ b/schemas/routing.json @@ -0,0 +1,196 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "ucwm/schemas/routing.json", + "title": "Routing Schemas", + "description": "GateSignal and RoutingDecision schemas for the RSA relay layer.", + "$defs": { + "GateSignal": { + "type": "object", + "title": "GateSignal", + "description": "Output of a single relation gate. Does not solve anything — only informs the router.", + "required": ["gate_id", "activated", "confidence"], + "additionalProperties": false, + "properties": { + "gate_id": { + "type": "string", + "description": "Identifier of the gate that produced this signal.", + "examples": [ + "is_math", + "has_temporal_relation", + "has_entity_reference", + "has_ownership_transfer", + "has_quantity" + ] + }, + "activated": { + "type": "boolean", + "description": "Whether the gate fired. High-recall design: prefer true when uncertain." + }, + "confidence": { + "type": "number", + "minimum": 0.0, + "maximum": 1.0, + "description": "Confidence in the activation decision." + }, + "evidence_spans": { + "type": "array", + "items": { + "type": "object", + "properties": { + "text": { "type": "string" }, + "start": { "type": "integer" }, + "end": { "type": "integer" } + } + }, + "description": "Text spans that triggered this gate." + }, + "method": { + "type": "string", + "enum": ["regex", "rule", "classifier", "neural", "hybrid"], + "description": "Implementation method used by this gate instance." + } + } + }, + + "BudgetSpec": { + "type": "object", + "title": "BudgetSpec", + "description": "Resource budget granted to a specialist by the relay.", + "properties": { + "level": { + "type": "string", + "enum": ["minimal", "bounded", "full"], + "description": "minimal=one fast pass; bounded=normal depth; full=exhaust all evidence" + }, + "max_objects": { + "type": "integer", + "description": "Maximum number of objects the specialist may inspect." + }, + "max_constraints": { + "type": "integer", + "description": "Maximum number of constraints the specialist may emit." + }, + "max_iterations": { + "type": "integer", + "description": "Maximum processing iterations for iterative specialists." + } + } + }, + + "SpecialistAssignment": { + "type": "object", + "title": "SpecialistAssignment", + "description": "Activation of a single specialist within a RoutingDecision.", + "required": ["specialist_id", "target_refs", "budget"], + "properties": { + "specialist_id": { + "type": "string", + "description": "Registered ID of the specialist to activate.", + "examples": [ + "temporal", + "spatial", + "entity", + "quantity", + "logic", + "causal", + "ownership", + "social", + "planning", + "code" + ] + }, + "target_refs": { + "type": "array", + "items": { "type": "string" }, + "description": "object_ids the specialist should inspect." + }, + "budget": { "$ref": "#/$defs/BudgetSpec" }, + "priority": { + "type": "integer", + "description": "Relative priority for constraint merging. Lower number = higher priority." + } + } + }, + + "RoutingDecision": { + "type": "object", + "title": "RoutingDecision", + "description": "Composite routing decision emitted by the RSA relay. Activates multiple specialists on the same targets.", + "required": ["decision_id", "assignments", "mode"], + "additionalProperties": false, + "properties": { + "decision_id": { + "type": "string", + "description": "Unique ID for this routing decision." + }, + "assignments": { + "type": "array", + "items": { "$ref": "#/$defs/SpecialistAssignment" }, + "minItems": 1, + "description": "Ordered list of specialist assignments." + }, + "mode": { + "type": "string", + "enum": ["parallel", "sequential", "iterative", "conflict_checking"], + "description": "Execution mode for specialists in this decision." + }, + "gate_signals": { + "type": "array", + "items": { "$ref": "#/$defs/GateSignal" }, + "description": "The gate signals that informed this routing decision." + }, + "input_object_refs": { + "type": "array", + "items": { "type": "string" }, + "description": "All canonical object IDs available at routing time." + }, + "provenance_refs": { + "type": "array", + "items": { "type": "string" }, + "default": [] + } + } + } + }, + + "examples": [ + { + "decision_id": "RD_001", + "mode": "parallel", + "gate_signals": [ + { "gate_id": "has_entity_reference", "activated": true, "confidence": 0.99 }, + { "gate_id": "has_quantity", "activated": true, "confidence": 0.99 }, + { "gate_id": "has_ownership_transfer", "activated": true, "confidence": 0.96 }, + { "gate_id": "is_math", "activated": true, "confidence": 0.94 }, + { "gate_id": "has_temporal_relation", "activated": true, "confidence": 0.88 } + ], + "input_object_refs": ["E1", "E2", "O_apples", "Q1", "Q2", "EV_transfer"], + "assignments": [ + { + "specialist_id": "entity", + "target_refs": ["E1", "E2"], + "budget": { "level": "bounded" }, + "priority": 1 + }, + { + "specialist_id": "quantity", + "target_refs": ["Q1", "Q2"], + "budget": { "level": "full" }, + "priority": 2 + }, + { + "specialist_id": "ownership", + "target_refs": ["E1", "E2", "O_apples"], + "budget": { "level": "bounded" }, + "priority": 3 + }, + { + "specialist_id": "temporal", + "target_refs": ["EV_transfer"], + "budget": { "level": "minimal" }, + "priority": 4 + } + ] + } + ] +} diff --git a/schemas/world_state.json b/schemas/world_state.json new file mode 100644 index 0000000..57670d9 --- /dev/null +++ b/schemas/world_state.json @@ -0,0 +1,98 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "ucwm/schemas/world_state.json", + "title": "WorldState", + "description": "The active UCWM state at a given processing stage. Passed between pipeline stages. The resolver stabilizes it; synthesis reads from it.", + "type": "object", + "required": ["state_id", "stage", "objects", "facets", "constraints"], + "additionalProperties": false, + "properties": { + "state_id": { + "type": "string", + "description": "Unique ID for this state snapshot." + }, + "stage": { + "type": "string", + "enum": [ + "post_gate", + "post_proposal", + "post_routing", + "post_specialist", + "post_resolver", + "resolved", + "synthesized" + ], + "description": "Which pipeline stage produced this state snapshot." + }, + "input_text": { + "type": "string", + "description": "Original user input text." + }, + "objects": { + "type": "object", + "description": "Map of object_id to CanonicalObject.", + "additionalProperties": { "$ref": "objects.json" } + }, + "facets": { + "type": "object", + "description": "Map of facet_id to Facet.", + "additionalProperties": { "$ref": "facets.json" } + }, + "constraints": { + "type": "object", + "description": "Map of constraint_id to Constraint.", + "additionalProperties": { "$ref": "constraints.json" } + }, + "provenance": { + "type": "object", + "description": "Map of provenance_id to Provenance record.", + "additionalProperties": { "$ref": "provenance.json" } + }, + "routing_decision": { + "$ref": "routing.json#/$defs/RoutingDecision" + }, + "gate_signals": { + "type": "array", + "items": { "$ref": "routing.json#/$defs/GateSignal" } + }, + "resolution_log": { + "type": "array", + "items": { + "type": "object", + "properties": { + "operation": { "type": "string" }, + "affected_refs": { "type": "array", "items": { "type": "string" } }, + "result": { "type": "string" }, + "note": { "type": "string" } + } + }, + "description": "Ordered log of resolver operations applied to reach this state." + }, + "open_contradictions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "constraint_refs": { "type": "array", "items": { "type": "string" } }, + "description": { "type": "string" } + } + }, + "description": "Contradictions that the resolver could not resolve. Synthesis must account for these." + }, + "synthesis_answer": { + "type": "string", + "description": "Final synthesized answer. Populated only at stage=synthesized." + }, + "synthesis_confidence": { + "type": "number", + "minimum": 0.0, + "maximum": 1.0, + "description": "Confidence in the synthesis answer." + }, + "derivation_trace": { + "type": "array", + "items": { "type": "string" }, + "description": "Ordered list of constraint_ids and resolver operations used to derive the synthesis answer." + } + } +} diff --git a/specialists/contract.md b/specialists/contract.md new file mode 100644 index 0000000..ba4a1c3 --- /dev/null +++ b/specialists/contract.md @@ -0,0 +1,136 @@ +# Specialist Interface Contract + +A specialist is a contract-bound module that inspects canonical objects within a specific relation domain and emits typed facets and constraints. Specialists are the workhorses of UCWM — they build the structured world state that the resolver stabilizes. + +--- + +## Design principles + +1. **Bounded output.** A specialist emits a finite list of Facets and Constraints. It does not emit free text, tokens, or embeddings as primary output. + +2. **Contract-typed.** Every output conforms to the schemas in `schemas/facets.json` and `schemas/constraints.json`. + +3. **Domain-limited.** A specialist operates only in its declared domain. The temporal specialist does not emit ownership constraints. + +4. **Implementation-neutral.** A specialist may be neural, statistical, symbolic, rule-based, graph-based, or hybrid. The contract is identical regardless. + +5. **Replaceable.** Any specialist can be replaced with a different implementation as long as the new implementation satisfies this contract. No other component should be affected. + +--- + +## Input + +``` +SpecialistInput { + specialist_id: string -- which specialist is being invoked + target_refs: string[] -- object_ids to inspect + world_state: WorldState -- current state (read-only for this specialist) + routing_context: SpecialistAssignment -- from the RoutingDecision + budget: BudgetSpec -- resource limits +} +``` + +The specialist receives a read-only view of WorldState. It MUST NOT modify WorldState directly. It only returns outputs; the resolver applies them. + +--- + +## Output + +``` +SpecialistOutput { + specialist_id: string + new_facets: Facet[] + new_constraints: Constraint[] + updated_objects: ObjectUpdate[] -- confidence or alias updates to existing objects + provenance: Provenance[] + budget_used: BudgetUsed +} + +ObjectUpdate { + object_id: string + confidence_delta: float? -- adjustment to existing confidence + new_aliases: string[]? + new_status: string? -- only "proposed" → "active" transitions +} + +BudgetUsed { + objects_inspected: integer + constraints_emitted: integer + iterations_used: integer +} +``` + +--- + +## Invariants + +Every specialist implementation MUST satisfy all of the following: + +1. **Domain adherence.** A specialist only emits facets and constraints of kinds it declares in its manifest. A temporal specialist emits `temporal` facets and temporal constraints (`before`, `after`, `during`). It does not emit `ownership` facets. + +2. **Valid refs.** Every `argument_ref` in a constraint and every `object_ref` in a facet must reference an ID that exists in the input WorldState or in `new_objects` returned in the same output. No dangling references. + +3. **Bounded by budget.** The specialist respects its `BudgetSpec`. If `max_constraints` is 10, it returns at most 10 constraints. + +4. **Confidence required.** Every Facet and Constraint emitted must have a `confidence` value. + +5. **Source module declared.** Every Facet, Constraint, and Provenance record must have `source_module` set to the specialist's ID. + +6. **Provenance required.** For every Constraint and Facet emitted, there must be a corresponding Provenance record in `provenance[]`. + +7. **No WorldState mutation.** The specialist only returns outputs. It does not write to WorldState. + +8. **Idempotent.** Calling a specialist twice with identical input produces outputs that are logically equivalent (same constraint set, possibly different IDs if IDs are randomly generated — use deterministic IDs where possible). + +--- + +## Specialist manifest format + +Each specialist declares its capabilities in a manifest: + +``` +SpecialistManifest { + specialist_id: string + display_name: string + description: string + declared_facet_kinds: string[] -- facet kinds this specialist may emit + declared_constraint_types: string[] -- constraint types this specialist may emit + trigger_gates: string[] -- gate_ids that commonly activate this specialist + implementation: string -- reference to implementation + version: string +} +``` + +--- + +## Specialist output validation + +Before the resolver applies specialist outputs, a contract validator MUST check: + +- All emitted facets have `facet_kind` in `declared_facet_kinds` +- All emitted constraints have `constraint_type` in `declared_constraint_types` +- All `argument_refs` and `object_refs` resolve to existing or co-emitted objects +- All `confidence` values are in [0, 1] +- Budget limits were not exceeded +- `source_module` is correct on all records + +A specialist that emits invalid output is rejected. The resolver logs the rejection and continues without that specialist's output. + +--- + +## Registered specialists + +| specialist_id | Declared facet kinds | Key constraint types | +|---|---|---| +| `entity` | `syntactic`, `semantic` | `same_entity`, `not_same_entity` | +| `temporal` | `temporal` | `before`, `after`, `during` | +| `spatial` | `spatial` | `inside`, `outside`, `adjacent` | +| `causal` | `causal` | `causes`, `enables`, `blocks` | +| `quantity` | `quantity` | `quantity_equals`, `quantity_sum`, `quantity_difference` | +| `logic` | `logical` | `truth_value`, `implies`, `negates`, `contradicts` | +| `ownership` | `ownership` | `owns`, `transfers` | +| `social` | `social` | `has_role`, `believes`, `obligated` | +| `planning` | `planning` | `prerequisite`, `goal` | +| `code` | `code_structure` | domain-specific | + +See individual specialist files for full manifests and domain logic. diff --git a/specialists/entity.md b/specialists/entity.md new file mode 100644 index 0000000..8292045 --- /dev/null +++ b/specialists/entity.md @@ -0,0 +1,146 @@ +# Entity Specialist + +**specialist_id:** `entity` +**trigger_gates:** `has_entity_reference`, `has_coreference` + +--- + +## Responsibility + +The entity specialist finds named and implied entities, proposes CanonicalObjects for them, resolves aliases, and detects coreference chains. It upgrades `proposed` objects to `active` when evidence is sufficient, and emits `same_entity` or `not_same_entity` constraints when coreference is resolved. + +--- + +## Manifest + +``` +declared_facet_kinds: + - syntactic + - semantic + +declared_constraint_types: + - same_entity + - not_same_entity + - has_role + +budget_caps: + max_objects_proposed: 10 + max_facets_emitted: 8 + max_constraints_emitted: 12 + max_merges: 4 + max_splits: 0 +``` + +--- + +## Input focus + +Given `target_refs`, the entity specialist inspects: +- Surface text spans that evidence each target object +- Pronouns and alias spans in the same context window +- Prior objects already in WorldState with overlapping spans or labels + +--- + +## Output: CanonicalObject proposals + +The entity specialist may propose new objects of kind `entity`. New entity proposals use `status: proposed`. Proposals are confirmed (`active`) only if confidence exceeds 0.75. + +Required fields on all proposed entities: +- `surface_span` +- `canonical_label` +- `aliases` (may be empty) +- `confidence` +- `source_module: entity` + +--- + +## Output: SyntacticFacet + +For each confirmed entity, emit a `SyntacticFacet` describing its grammatical role in the input. + +``` +SyntacticFacet fields used by entity specialist: + - grammatical_role: "subject" | "object" | "indirect_object" | "adjunct" | "other" + - head_token: string +``` + +--- + +## Output: Constraints + +### `same_entity` + +Emit when two object proposals likely refer to the same real-world entity. Common cases: +- Pronoun resolution: `she` → `Alice` +- Alias resolution: `the company` → `Acme Corp` +- Repeated reference: `Alice ... Alice` (same span, different positions) + +``` +Constraint example: +{ + "constraint_type": "same_entity", + "argument_refs": ["E1", "E3"], + "polarity": "positive", + "strength": "hard", + "expression": "same_entity(E1, E3)" +} +``` + +### `not_same_entity` + +Emit when two object proposals are clearly different entities that might otherwise be confused. + +``` +Constraint example: +{ + "constraint_type": "not_same_entity", + "argument_refs": ["E1", "E2"], + "polarity": "positive", + "strength": "hard", + "expression": "not_same_entity(Alice, Bob)" +} +``` + +### `has_role` + +Emit when an entity is playing a specific functional role in the input context (giver, receiver, buyer, seller, etc.). + +``` +Constraint example: +{ + "constraint_type": "has_role", + "argument_refs": ["E1", "EV_transfer"], + "polarity": "positive", + "strength": "soft", + "expression": "has_role(Alice, giver, EV_transfer)" +} +``` + +--- + +## Coreference resolution algorithm (rule-based baseline) + +``` +for each pronoun P in input: + candidates = all active entity objects in WorldState + rank candidates by: + 1. recency (closest preceding entity) + 2. gender agreement (he/she/they match) + 3. number agreement (they/them = plural) + 4. animacy (personal pronouns prefer animate entities) + if top candidate confidence > 0.7: + emit same_entity(pronoun_object, top_candidate) + else: + propose new entity with surface_span = P, status = proposed +``` + +--- + +## Contract test requirements + +- Given `Alice gave Bob 2 apples`, must emit at least two distinct entity objects (Alice, Bob) +- Must emit `not_same_entity(Alice, Bob)` +- Given `She gave him 2 apples` with prior context establishing Alice and Bob, must resolve `she → Alice` and `him → Bob` +- Must not emit quantity or temporal constraints +- All emitted objects must have `source_module: entity` diff --git a/specialists/logic.md b/specialists/logic.md new file mode 100644 index 0000000..c0481ca --- /dev/null +++ b/specialists/logic.md @@ -0,0 +1,123 @@ +# Logic Specialist + +**specialist_id:** `logic` +**trigger_gates:** `has_logical_negation`, `has_comparison`, `has_constraint` + +--- + +## Responsibility + +The logic specialist handles negation, implication, contradiction, quantifiers, and truth status. It attaches LogicalFacets to propositions and claims, and emits logical constraints that the resolver uses to detect contradictions and propagate truth values. + +--- + +## Manifest + +``` +declared_facet_kinds: + - logical + +declared_constraint_types: + - truth_value + - implies + - negates + - contradicts + - requires + - exclusive_or + - universally_quantified + - existentially_quantified + +budget_caps: + max_objects_proposed: 4 + max_facets_emitted: 8 + max_constraints_emitted: 10 + max_merges: 0 + max_splits: 0 +``` + +--- + +## Output: LogicalFacet + +Attach to objects of kind `proposition`, `claim`, or `event` with a truth dimension. + +``` +LogicalFacet fields used: + - truth_status: "true" | "false" | "unknown" | "hypothetical" | "negated" | "asserted" + - quantifier: "universal" | "existential" | "none" + - implies_refs: [proposition_ids this implies] + - negates_refs: [proposition_ids this negates] + - contradicts_refs: [proposition_ids this contradicts] +``` + +--- + +## Output: Constraints + +### `truth_value(P, status)` + +Sets the explicit truth status of a proposition. + +``` +{ + "constraint_type": "truth_value", + "argument_refs": ["P1"], + "polarity": "positive", + "strength": "hard", + "expression": "truth(P1) = false" +} +``` + +### `negates(P1, P2)` + +P1 is the logical negation of P2. If P1 is true, P2 is false, and vice versa. + +### `implies(P1, P2)` + +If P1 is true, P2 must be true. Defeasible by default unless marked hard. + +### `contradicts(P1, P2)` + +P1 and P2 cannot both be true. Strength is always hard. + +### `requires(A, B)` + +A requires B to hold. Equivalent to `implies(A, B)` but semantically marks a dependency rather than a logical implication. + +### `exclusive_or(P1, P2)` + +Exactly one of P1 or P2 is true. + +--- + +## Negation handling + +Negation is the most error-prone logical operation. Rules: + +1. Surface negation (e.g., `not`, `isn't`) does not always mean logical negation of a proposition — it may be just semantic modification. +2. When `has_logical_negation` fires, the logic specialist inspects whether the negation applies to: + - A predicate (`Alice is NOT happy` → LogicalFacet truth_status = negated) + - A quantifier (`NOT all → some are not`) + - A condition (`unless X` → implies(NOT X, consequence)) +3. Emit `truth_value` constraints conservatively — only when confident the scope of negation is correctly identified. + +--- + +## Contradiction detection support + +The logic specialist does not resolve contradictions. It emits `contradicts` constraints when it detects that two propositions in WorldState logically oppose each other. The resolver handles contradiction resolution. + +``` +if P1 has truth_status = true AND P2 = negation_of(P1): + emit contradicts(P1, P2) with strength = hard +``` + +--- + +## Contract test requirements + +- Given `Alice is not happy`, must emit a LogicalFacet on `P_alice_happy` with `truth_status: negated` +- Given `If it rains, the ground is wet`, must emit `implies(E_rain, S_ground_wet)` +- Must not emit temporal or quantity constraints +- Contradiction constraints must reference two objects that exist in WorldState +- All emitted constraints must have `source_module: logic` diff --git a/specialists/quantity.md b/specialists/quantity.md new file mode 100644 index 0000000..4dadfb6 --- /dev/null +++ b/specialists/quantity.md @@ -0,0 +1,132 @@ +# Quantity Specialist + +**specialist_id:** `quantity` +**trigger_gates:** `has_quantity`, `is_math` + +--- + +## Responsibility + +The quantity specialist extracts numeric values, attaches QuantityFacets to quantity objects, and emits arithmetic and comparison constraints. When arithmetic can be resolved deterministically, it resolves it. When resolution requires a solver, it emits a structured constraint for the resolver. + +--- + +## Manifest + +``` +declared_facet_kinds: + - quantity + +declared_constraint_types: + - quantity_equals + - quantity_sum + - quantity_difference + - quantity_product + - quantity_quotient + - quantity_greater + - quantity_less + - quantity_at_least + - quantity_at_most + - quantity_bound + +budget_caps: + max_objects_proposed: 6 + max_facets_emitted: 6 + max_constraints_emitted: 10 + max_merges: 0 + max_splits: 0 +``` + +--- + +## Output: QuantityFacet + +Attach to any object of kind `quantity`. + +``` +QuantityFacet fields used: + - value: number or symbolic expression + - unit: string (apples, dollars, meters, etc.) + - is_exact: true/false + - derived_from_refs: quantity object IDs this was computed from +``` + +--- + +## Output: Constraints + +### `quantity_equals(Q_result, expression)` + +Emit when a quantity is defined as equal to an expression. + +``` +{ + "constraint_type": "quantity_equals", + "argument_refs": ["Q_alice_final", "Q_expr_5_minus_2"], + "expression": "quantity(Q_alice_final) = quantity(Q1) - quantity(Q2)", + "strength": "hard" +} +``` + +### `quantity_difference(Q_result, Q_minuend, Q_subtrahend)` + +Preferred form when the specific operation is subtraction. + +``` +argument_refs order: [result, minuend, subtrahend] +``` + +### `quantity_sum(Q_result, Q_addend1, Q_addend2, ...)` + +Result equals the sum of all argument quantities. + +### `quantity_greater(Q_a, Q_b)` + +Q_a > Q_b. + +### `quantity_less(Q_a, Q_b)` + +Q_a < Q_b. + +--- + +## Arithmetic resolution + +When all inputs to an arithmetic constraint are exact numeric values, the quantity specialist SHOULD resolve the arithmetic inline and set the result quantity's `value` directly. + +``` +if quantity_difference(Q_result, Q1, Q2): + if Q1.value is exact numeric AND Q2.value is exact numeric: + Q_result.value = Q1.value - Q2.value + Q_result.is_exact = true + mark constraint as resolved + else: + emit constraint with status = unresolved + resolver handles it +``` + +This avoids unnecessary round-trips through the resolver for simple arithmetic. + +--- + +## Unit tracking + +Units must be tracked and propagated. Emitting a `quantity_difference` constraint without units is not sufficient — the result unit must be declared or inferred. + +Rules: +- Same unit on both operands: result inherits unit +- Different units on operands: emit a `quantity_bound` constraint marking the result as unit-ambiguous until a converter is applied +- Unitless operands: result is unitless + +--- + +## Contract test requirements + +- Given `Alice had 5 apples. She gave Bob 2 apples.`: + - Must emit Q1 with value=5, unit="apples" + - Must emit Q2 with value=2, unit="apples" + - Must emit `quantity_difference(Q_alice_final, Q1, Q2)` or `quantity_equals(Q_alice_final, "5-2")` + - Must set Q_alice_final.value = 3 (inline resolution) +- Must not emit temporal or entity constraints +- All quantity facets must have `unit` set or explicitly `"unitless"` +- All emitted constraints must have `source_module: quantity` diff --git a/specialists/spatial.md b/specialists/spatial.md new file mode 100644 index 0000000..a1222d4 --- /dev/null +++ b/specialists/spatial.md @@ -0,0 +1,107 @@ +# Spatial Specialist + +**specialist_id:** `spatial` +**trigger_gates:** `has_spatial_relation` + +--- + +## Responsibility + +The spatial specialist adds location, containment, direction, proximity, and topology constraints to objects. It attaches SpatialFacets to entities and events with spatial dimension, and emits spatial constraints between objects. + +--- + +## Manifest + +``` +declared_facet_kinds: + - spatial + +declared_constraint_types: + - inside + - outside + - adjacent + - above + - below + - north_of + - south_of + - east_of + - west_of + - near + - far + - at_location + - between + - on_path + +budget_caps: + max_objects_proposed: 4 + max_facets_emitted: 8 + max_constraints_emitted: 12 + max_merges: 0 + max_splits: 0 +``` + +--- + +## Output: SpatialFacet + +Attach to objects with a spatial dimension (entities, events, states, code objects with file location). + +``` +SpatialFacet fields used: + - location_label: string (natural language location) + - coordinates: {x, y, z} if numeric coords available + - containment_refs: constraint IDs for inside/outside relations + - reference_frame: coordinate system or reference object +``` + +--- + +## Output: Constraints + +### `inside(A, B)` + +Object A is contained within object B. Example: `inside(book, shelf)`, `inside(Alice, building)`. + +### `adjacent(A, B)` + +A and B are next to each other without containment. + +### `at_location(A, L)` + +Object A is at named location L. L should be a canonical object of kind `entity` (a place). + +### `between(A, B, C)` + +Object A is between objects B and C. + +### Directional constraints + +`north_of`, `south_of`, `east_of`, `west_of`, `above`, `below` — all take two argument_refs in the form `(subject, reference)`. + +### `near(A, B)` / `far(A, B)` + +Relative proximity. Strength is always `soft` since "near" is inherently vague without a defined scale. + +--- + +## Reference frame handling + +Spatial constraints are only meaningful relative to a reference frame. When no frame is explicit in the input, use `"world"` as the default frame label. + +``` +SpatialFacet.reference_frame: + - "world" -- global geographic frame (default when unspecified) + - "local" -- relative to a locally established reference + - "{object_id}" -- relative to a specific object in WorldState +``` + +--- + +## Contract test requirements + +- Given `Alice is in the kitchen`, must emit `at_location(E_alice, O_kitchen)` or `inside(E_alice, O_kitchen)` +- Given `The box is on the shelf`, must emit `above(O_box, O_shelf)` or `on(O_box, O_shelf)` +- Must not emit temporal or quantity constraints +- All spatial facets must have `location_label` set +- All emitted constraints must have `source_module: spatial` diff --git a/specialists/temporal.md b/specialists/temporal.md new file mode 100644 index 0000000..7348051 --- /dev/null +++ b/specialists/temporal.md @@ -0,0 +1,117 @@ +# Temporal Specialist + +**specialist_id:** `temporal` +**trigger_gates:** `has_temporal_relation`, `has_state_change` + +--- + +## Responsibility + +The temporal specialist adds before/after/during/recurring constraints to events and states. It identifies time points, intervals, and orderings, then attaches TemporalFacets to relevant objects and emits temporal ordering constraints. + +--- + +## Manifest + +``` +declared_facet_kinds: + - temporal + +declared_constraint_types: + - before + - after + - during + - overlaps + - starts_at + - ends_at + - recurring + +budget_caps: + max_objects_proposed: 4 + max_facets_emitted: 8 + max_constraints_emitted: 12 + max_merges: 0 + max_splits: 0 +``` + +--- + +## Output: TemporalFacet + +Attach to any object of kind `event`, `state`, or `procedure_step`. + +``` +TemporalFacet fields used: + - time_point: absolute or relative (if known) + - interval_start: if event has a duration + - interval_end: if event has a duration + - temporal_order_refs: constraint_ids expressing ordering + - is_recurring: true/false + - recurrence_pattern: if recurring +``` + +--- + +## Output: Constraints + +### `before(A, B)` + +Event or state A occurs before event or state B. + +Emit when: +- Explicit connective: `before`, `prior to`, `earlier`, `first ... then` +- Narrative sequence: events described in causal or sequential order +- Completed event followed by a subsequent event + +Strength: `hard` for explicit connectives, `soft` for narrative inference. + +### `after(A, B)` + +Event A occurs after event B. Equivalent to `before(B, A)` but retains the original linguistic direction. + +### `during(A, B)` + +Event A occurs within the span of event B. + +### `overlaps(A, B)` + +Events A and B share some time without one being fully within the other. + +### `starts_at(E, time_expression)` + +Argument_refs: [event_id, time_object_id]. Emit when a specific start time is mentioned. + +### `ends_at(E, time_expression)` + +Argument_refs: [event_id, time_object_id]. Emit when a specific end time is mentioned. + +### `recurring(E, pattern)` + +Emit when an event is described as habitual or recurring. Argument_refs: [event_id, pattern_string_as_object]. + +--- + +## Ordering baseline algorithm + +``` +events = [objects in target_refs where kind = event | state | procedure_step] + +for each pair (A, B) in events × events where A ≠ B: + check for explicit temporal connective between A and B + if found: + emit corresponding constraint with strength = hard + else: + check narrative order (A mentioned before B in text): + if A is a precondition or cause of B: + emit before(A, B) with strength = soft +``` + +--- + +## Contract test requirements + +- Given `Alice had 5 apples. She gave Bob 2 apples.`, must emit `before(EV_initial_possession, EV_transfer)` or equivalent +- Given explicit `before X, Y happened`, must emit `before(Y, X)` with `strength: hard` +- Must not emit entity, quantity, or ownership constraints +- All temporal facets must reference at least one constraint +- All emitted constraints must have `source_module: temporal` diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..1c5c0fc --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,118 @@ +#include "types.hpp" +#include + +// Constructs the post-proposal WorldState for the apple problem by hand. +// This is the Sprint 1 smoke test — no logic runs, just type construction +// and JSON serialization. Sprint 2 will load this from the fixture file. +int main() { + using namespace ucwm; + + WorldState ws; + ws.state_id = "ws_apple_001"; + ws.stage = WorldStateStage::PostProposal; + ws.input_text = "Alice had 5 apples. She gave Bob 2 apples. How many apples does Alice have now?"; + + // ── Entities ────────────────────────────────────────────────────────────── + + CanonicalObject alice; + alice.object_id = "E1"; + alice.object_kind = ObjectKind::Entity; + alice.status = ObjectStatus::Active; + alice.confidence = 0.97; + alice.surface_span = SurfaceSpan{"Alice", 0, 5}; + alice.canonical_label = "Alice"; + alice.aliases = {"She", "her"}; + alice.source_module = "entity_specialist"; + ws.objects["E1"] = alice; + + CanonicalObject bob; + bob.object_id = "E2"; + bob.object_kind = ObjectKind::Entity; + bob.status = ObjectStatus::Active; + bob.confidence = 0.97; + bob.surface_span = SurfaceSpan{"Bob", 29, 32}; + bob.canonical_label = "Bob"; + bob.aliases = {"him"}; + bob.source_module = "entity_specialist"; + ws.objects["E2"] = bob; + + CanonicalObject apples; + apples.object_id = "O_apples"; + apples.object_kind = ObjectKind::Concept; + apples.status = ObjectStatus::Active; + apples.confidence = 0.99; + apples.surface_span = SurfaceSpan{"apples", 12, 18}; + apples.canonical_label = "apples"; + apples.source_module = "entity_specialist"; + ws.objects["O_apples"] = apples; + + // ── Quantities ──────────────────────────────────────────────────────────── + + CanonicalObject q1; + q1.object_id = "Q1"; + q1.object_kind = ObjectKind::Quantity; + q1.status = ObjectStatus::Active; + q1.confidence = 0.99; + q1.surface_span = SurfaceSpan{"5", 10, 11}; + q1.canonical_label = "5"; + q1.facet_refs = {"F_Q1"}; + q1.source_module = "quantity_specialist"; + ws.objects["Q1"] = q1; + + CanonicalObject q2; + q2.object_id = "Q2"; + q2.object_kind = ObjectKind::Quantity; + q2.status = ObjectStatus::Active; + q2.confidence = 0.99; + q2.surface_span = SurfaceSpan{"2", 33, 34}; + q2.canonical_label = "2"; + q2.facet_refs = {"F_Q2"}; + q2.source_module = "quantity_specialist"; + ws.objects["Q2"] = q2; + + CanonicalObject q_final; + q_final.object_id = "Q_alice_final"; + q_final.object_kind = ObjectKind::Quantity; + q_final.status = ObjectStatus::Proposed; + q_final.confidence = 0.80; + q_final.canonical_label = "Alice's final apple count"; + q_final.facet_refs = {"F_Q_final"}; + q_final.source_module = "quantity_specialist"; + ws.objects["Q_alice_final"] = q_final; + + // ── Events ──────────────────────────────────────────────────────────────── + + CanonicalObject ev_initial; + ev_initial.object_id = "EV_initial"; + ev_initial.object_kind = ObjectKind::Event; + ev_initial.status = ObjectStatus::Active; + ev_initial.confidence = 0.90; + ev_initial.surface_span = SurfaceSpan{"had", 6, 9}; + ev_initial.canonical_label = "initial_possession"; + ev_initial.source_module = "entity_specialist"; + ws.objects["EV_initial"] = ev_initial; + + CanonicalObject ev_transfer; + ev_transfer.object_id = "EV_transfer"; + ev_transfer.object_kind = ObjectKind::Event; + ev_transfer.status = ObjectStatus::Active; + ev_transfer.confidence = 0.95; + ev_transfer.surface_span = SurfaceSpan{"gave", 21, 25}; + ev_transfer.canonical_label = "transfer"; + ev_transfer.source_module = "entity_specialist"; + ws.objects["EV_transfer"] = ev_transfer; + + // ── Print ───────────────────────────────────────────────────────────────── + + std::cout << "=== UCWM Smoke Test ===\n"; + std::cout << "WorldState stage: post_proposal\n"; + std::cout << "Objects: " << ws.objects.size() << "\n"; + std::cout << "Facets: " << ws.facets.size() << "\n"; + std::cout << "Constraints: " << ws.constraints.size() << "\n"; + std::cout << "\n"; + + nlohmann::json j = ws; + std::cout << j.dump(2) << "\n"; + + return 0; +} diff --git a/src/types.hpp b/src/types.hpp new file mode 100644 index 0000000..79869b5 --- /dev/null +++ b/src/types.hpp @@ -0,0 +1,980 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace ucwm { + +// ───────────────────────────────────────────────────────────────────────────── +// Enums +// ───────────────────────────────────────────────────────────────────────────── + +enum class ObjectKind { + Entity, Event, Concept, Proposition, RelationInstance, + ProcedureStep, CodeObject, Quantity, Claim, State +}; + +enum class ObjectStatus { Proposed, Active, Merged, Split, Invalidated }; + +enum class ConstraintStrength { Hard, Soft, Probabilistic, Defeasible }; + +enum class ConstraintStatus { Unresolved, Resolved, Contradicted, Suspended }; + +enum class Polarity { Positive, Negative }; + +enum class FacetKind { + Temporal, Spatial, Causal, Logical, Ownership, + Quantity, Syntactic, Social, Planning, Semantic, CodeStructure +}; + +enum class WorldStateStage { + PostGate, PostProposal, PostRouting, PostSpecialist, + PostResolver, Resolved, Synthesized +}; + +enum class BudgetLevel { Minimal, Bounded, Full }; + +enum class ExecutionMode { Parallel, Sequential, Iterative, ConflictChecking }; + +enum class GateMethod { Regex, Rule, Classifier, Neural, Hybrid }; + +enum class ErrorKind { Unknown, Ambiguous, Conflicting, InsufficientData }; + +enum class ProvenanceRecordType { + ObjectProposal, FacetAttachment, ConstraintEmission, + ResolverOperation, ObjectMerge, ObjectSplit, SynthesisStep +}; + +// Facet-specific enums +enum class TruthStatus { True_, False_, Unknown, Hypothetical, Negated, Asserted }; +enum class Quantifier { Universal, Existential, None }; +enum class CausalStrength { Necessary, Sufficient, Contributing, Correlational }; +enum class GrammaticalRole { Subject, Object, IndirectObject, Adjunct, Predicate, Modifier, Other }; +enum class SocialAct { Assert, Request, Promise, Threaten, Deceive, Inform, Ask, Other }; +enum class PlanningStatus { Pending, InProgress, Complete, Blocked, Skipped }; +enum class TransferType { Give, Sell, Take, Lose, Receive, Initial, Final }; + +// ───────────────────────────────────────────────────────────────────────────── +// JSON enum serialization +// ───────────────────────────────────────────────────────────────────────────── + +NLOHMANN_JSON_SERIALIZE_ENUM(ObjectKind, { + {ObjectKind::Entity, "entity"}, + {ObjectKind::Event, "event"}, + {ObjectKind::Concept, "concept"}, + {ObjectKind::Proposition, "proposition"}, + {ObjectKind::RelationInstance, "relation_instance"}, + {ObjectKind::ProcedureStep, "procedure_step"}, + {ObjectKind::CodeObject, "code_object"}, + {ObjectKind::Quantity, "quantity"}, + {ObjectKind::Claim, "claim"}, + {ObjectKind::State, "state"}, +}) + +NLOHMANN_JSON_SERIALIZE_ENUM(ObjectStatus, { + {ObjectStatus::Proposed, "proposed"}, + {ObjectStatus::Active, "active"}, + {ObjectStatus::Merged, "merged"}, + {ObjectStatus::Split, "split"}, + {ObjectStatus::Invalidated, "invalidated"}, +}) + +NLOHMANN_JSON_SERIALIZE_ENUM(ConstraintStrength, { + {ConstraintStrength::Hard, "hard"}, + {ConstraintStrength::Soft, "soft"}, + {ConstraintStrength::Probabilistic, "probabilistic"}, + {ConstraintStrength::Defeasible, "defeasible"}, +}) + +NLOHMANN_JSON_SERIALIZE_ENUM(ConstraintStatus, { + {ConstraintStatus::Unresolved, "unresolved"}, + {ConstraintStatus::Resolved, "resolved"}, + {ConstraintStatus::Contradicted, "contradicted"}, + {ConstraintStatus::Suspended, "suspended"}, +}) + +NLOHMANN_JSON_SERIALIZE_ENUM(Polarity, { + {Polarity::Positive, "positive"}, + {Polarity::Negative, "negative"}, +}) + +NLOHMANN_JSON_SERIALIZE_ENUM(FacetKind, { + {FacetKind::Temporal, "temporal"}, + {FacetKind::Spatial, "spatial"}, + {FacetKind::Causal, "causal"}, + {FacetKind::Logical, "logical"}, + {FacetKind::Ownership, "ownership"}, + {FacetKind::Quantity, "quantity"}, + {FacetKind::Syntactic, "syntactic"}, + {FacetKind::Social, "social"}, + {FacetKind::Planning, "planning"}, + {FacetKind::Semantic, "semantic"}, + {FacetKind::CodeStructure, "code_structure"}, +}) + +NLOHMANN_JSON_SERIALIZE_ENUM(WorldStateStage, { + {WorldStateStage::PostGate, "post_gate"}, + {WorldStateStage::PostProposal, "post_proposal"}, + {WorldStateStage::PostRouting, "post_routing"}, + {WorldStateStage::PostSpecialist, "post_specialist"}, + {WorldStateStage::PostResolver, "post_resolver"}, + {WorldStateStage::Resolved, "resolved"}, + {WorldStateStage::Synthesized, "synthesized"}, +}) + +NLOHMANN_JSON_SERIALIZE_ENUM(BudgetLevel, { + {BudgetLevel::Minimal, "minimal"}, + {BudgetLevel::Bounded, "bounded"}, + {BudgetLevel::Full, "full"}, +}) + +NLOHMANN_JSON_SERIALIZE_ENUM(ExecutionMode, { + {ExecutionMode::Parallel, "parallel"}, + {ExecutionMode::Sequential, "sequential"}, + {ExecutionMode::Iterative, "iterative"}, + {ExecutionMode::ConflictChecking, "conflict_checking"}, +}) + +NLOHMANN_JSON_SERIALIZE_ENUM(GateMethod, { + {GateMethod::Regex, "regex"}, + {GateMethod::Rule, "rule"}, + {GateMethod::Classifier, "classifier"}, + {GateMethod::Neural, "neural"}, + {GateMethod::Hybrid, "hybrid"}, +}) + +NLOHMANN_JSON_SERIALIZE_ENUM(ErrorKind, { + {ErrorKind::Unknown, "unknown"}, + {ErrorKind::Ambiguous, "ambiguous"}, + {ErrorKind::Conflicting, "conflicting"}, + {ErrorKind::InsufficientData, "insufficient_data"}, +}) + +NLOHMANN_JSON_SERIALIZE_ENUM(ProvenanceRecordType, { + {ProvenanceRecordType::ObjectProposal, "object_proposal"}, + {ProvenanceRecordType::FacetAttachment, "facet_attachment"}, + {ProvenanceRecordType::ConstraintEmission, "constraint_emission"}, + {ProvenanceRecordType::ResolverOperation, "resolver_operation"}, + {ProvenanceRecordType::ObjectMerge, "object_merge"}, + {ProvenanceRecordType::ObjectSplit, "object_split"}, + {ProvenanceRecordType::SynthesisStep, "synthesis_step"}, +}) + +NLOHMANN_JSON_SERIALIZE_ENUM(TruthStatus, { + {TruthStatus::True_, "true"}, + {TruthStatus::False_, "false"}, + {TruthStatus::Unknown, "unknown"}, + {TruthStatus::Hypothetical,"hypothetical"}, + {TruthStatus::Negated, "negated"}, + {TruthStatus::Asserted, "asserted"}, +}) + +NLOHMANN_JSON_SERIALIZE_ENUM(Quantifier, { + {Quantifier::Universal, "universal"}, + {Quantifier::Existential, "existential"}, + {Quantifier::None, "none"}, +}) + +NLOHMANN_JSON_SERIALIZE_ENUM(CausalStrength, { + {CausalStrength::Necessary, "necessary"}, + {CausalStrength::Sufficient, "sufficient"}, + {CausalStrength::Contributing, "contributing"}, + {CausalStrength::Correlational,"correlational"}, +}) + +NLOHMANN_JSON_SERIALIZE_ENUM(GrammaticalRole, { + {GrammaticalRole::Subject, "subject"}, + {GrammaticalRole::Object, "object"}, + {GrammaticalRole::IndirectObject, "indirect_object"}, + {GrammaticalRole::Adjunct, "adjunct"}, + {GrammaticalRole::Predicate, "predicate"}, + {GrammaticalRole::Modifier, "modifier"}, + {GrammaticalRole::Other, "other"}, +}) + +NLOHMANN_JSON_SERIALIZE_ENUM(SocialAct, { + {SocialAct::Assert, "assert"}, + {SocialAct::Request, "request"}, + {SocialAct::Promise, "promise"}, + {SocialAct::Threaten, "threaten"}, + {SocialAct::Deceive, "deceive"}, + {SocialAct::Inform, "inform"}, + {SocialAct::Ask, "ask"}, + {SocialAct::Other, "other"}, +}) + +NLOHMANN_JSON_SERIALIZE_ENUM(PlanningStatus, { + {PlanningStatus::Pending, "pending"}, + {PlanningStatus::InProgress, "in_progress"}, + {PlanningStatus::Complete, "complete"}, + {PlanningStatus::Blocked, "blocked"}, + {PlanningStatus::Skipped, "skipped"}, +}) + +NLOHMANN_JSON_SERIALIZE_ENUM(TransferType, { + {TransferType::Give, "give"}, + {TransferType::Sell, "sell"}, + {TransferType::Take, "take"}, + {TransferType::Lose, "lose"}, + {TransferType::Receive, "receive"}, + {TransferType::Initial, "initial"}, + {TransferType::Final, "final"}, +}) + +// ───────────────────────────────────────────────────────────────────────────── +// Support structs +// ───────────────────────────────────────────────────────────────────────────── + +struct SurfaceSpan { + std::string text; + int start = 0; + int end = 0; +}; + +struct Coordinates { + std::optional x; + std::optional y; + std::optional z; +}; + +struct EvidenceSpan { + std::string text; + std::optional start; + std::optional end; +}; + +// ───────────────────────────────────────────────────────────────────────────── +// Facet data types (one per FacetKind) +// ───────────────────────────────────────────────────────────────────────────── + +struct TemporalFacetData { + std::optional time_point; + std::optional interval_start; + std::optional interval_end; + std::vector temporal_order_refs; + bool is_recurring = false; + std::optional recurrence_pattern; + std::optional duration; +}; + +struct SpatialFacetData { + std::optional location_label; + std::optional coordinates; + std::vector containment_refs; + std::optional reference_frame; +}; + +struct CausalFacetData { + std::vector cause_refs; + std::vector effect_refs; + std::vector enabling_refs; + std::vector blocking_refs; + std::optional causal_strength; +}; + +struct LogicalFacetData { + std::optional truth_status; + std::optional quantifier; + std::vector implies_refs; + std::vector negates_refs; + std::vector contradicts_refs; +}; + +struct OwnershipFacetData { + std::optional owner_ref; + std::optional owned_ref; + std::optional quantity_ref; + std::optional temporal_ref; + std::optional transfer_type; +}; + +struct QuantityFacetData { + // value is a resolved number or an unresolved symbolic expression + std::optional> value; + std::optional unit; + std::optional precision; + bool is_exact = false; + std::vector derived_from_refs; +}; + +struct SyntacticFacetData { + std::optional grammatical_role; + std::optional head_token; + std::optional dependency_arc; +}; + +struct SocialFacetData { + std::optional agent_ref; + std::optional social_act; + std::optional belief_about_ref; + std::optional belief_truth_status; + std::vector obligation_refs; + std::optional role; +}; + +struct PlanningFacetData { + std::optional step_index; + std::vector prerequisite_refs; + std::vector goal_refs; + std::vector blocks_refs; + std::optional status; +}; + +struct SemanticFacetData { + std::optional semantic_role; + std::optional frame; +}; + +struct CodeStructureFacetData { + std::optional language; + std::optional node_type; + std::vector dependency_refs; +}; + +using FacetData = std::variant< + TemporalFacetData, + SpatialFacetData, + CausalFacetData, + LogicalFacetData, + OwnershipFacetData, + QuantityFacetData, + SyntacticFacetData, + SocialFacetData, + PlanningFacetData, + SemanticFacetData, + CodeStructureFacetData +>; + +// ───────────────────────────────────────────────────────────────────────────── +// Core structs +// ───────────────────────────────────────────────────────────────────────────── + +struct CanonicalObject { + std::string object_id; + ObjectKind object_kind = ObjectKind::Entity; + ObjectStatus status = ObjectStatus::Proposed; + double confidence = 0.0; + std::optional surface_span; + std::optional canonical_label; + std::vector aliases; + std::optional merged_into; + std::optional split_from; + std::vector facet_refs; + std::vector constraint_refs; + std::vector evidence_refs; + std::vector provenance_refs; + std::optional source_module; +}; + +struct Facet { + std::string facet_id; + std::string object_ref; + double confidence = 0.0; + std::string source_module; + std::vector provenance_refs; + FacetData data; // kind is implicit in the variant index +}; + +struct Constraint { + std::string constraint_id; + std::string constraint_type; // open string — not an enum + std::vector argument_refs; + Polarity polarity = Polarity::Positive; + ConstraintStrength strength = ConstraintStrength::Hard; + std::optional probability; // only when strength=Probabilistic + ConstraintStatus status = ConstraintStatus::Unresolved; + std::optional expression; + std::optional confidence; + std::vector contradicted_by_refs; + std::vector supports_refs; + std::string source_module; + std::vector provenance_refs; +}; + +struct GateSignal { + std::string gate_id; + bool activated = false; + double confidence = 0.0; + std::vector evidence_spans; + std::optional method; +}; + +struct BudgetSpec { + BudgetLevel level = BudgetLevel::Bounded; + std::optional max_objects; + std::optional max_constraints; + std::optional max_iterations; +}; + +struct SpecialistAssignment { + std::string specialist_id; + std::vector target_refs; + BudgetSpec budget; + std::optional priority; +}; + +struct RoutingDecision { + std::string decision_id; + std::vector assignments; + ExecutionMode mode = ExecutionMode::Parallel; + std::vector gate_signals; + std::vector input_object_refs; + std::vector provenance_refs; +}; + +struct Provenance { + std::string provenance_id; + ProvenanceRecordType record_type; + std::string source_module; + std::vector input_refs; + std::vector output_refs; + std::optional evidence_span; + std::optional confidence; + std::string timestamp; + std::optional notes; +}; + +struct ResolutionEntry { + std::string operation; + std::vector affected_refs; + std::string result; + std::optional confidence_after; + std::optional note; +}; + +struct ContradictionRecord { + std::vector constraint_refs; + std::string description; + std::optional resolution_attempt; +}; + +struct SpecialistError { + ErrorKind error_kind; + std::vector affected_refs; + std::string description; +}; + +struct WorldState { + std::string state_id; + WorldStateStage stage = WorldStateStage::PostProposal; + std::optional input_text; + std::unordered_map objects; + std::unordered_map facets; + std::unordered_map constraints; + std::unordered_map provenance; + std::optional routing_decision; + std::vector gate_signals; + std::vector resolution_log; + std::vector open_contradictions; + std::optional synthesis_answer; + std::optional synthesis_confidence; + std::vector derivation_trace; +}; + +// ───────────────────────────────────────────────────────────────────────────── +// Facet helpers +// ───────────────────────────────────────────────────────────────────────────── + +inline FacetKind kind_of(const FacetData& data) { + return std::visit([](auto&& d) -> FacetKind { + using T = std::decay_t; + if constexpr (std::is_same_v) return FacetKind::Temporal; + else if constexpr (std::is_same_v) return FacetKind::Spatial; + else if constexpr (std::is_same_v) return FacetKind::Causal; + else if constexpr (std::is_same_v) return FacetKind::Logical; + else if constexpr (std::is_same_v) return FacetKind::Ownership; + else if constexpr (std::is_same_v) return FacetKind::Quantity; + else if constexpr (std::is_same_v) return FacetKind::Syntactic; + else if constexpr (std::is_same_v) return FacetKind::Social; + else if constexpr (std::is_same_v) return FacetKind::Planning; + else if constexpr (std::is_same_v) return FacetKind::Semantic; + else return FacetKind::CodeStructure; + }, data); +} + +// ───────────────────────────────────────────────────────────────────────────── +// JSON serialization — support structs +// ───────────────────────────────────────────────────────────────────────────── + +inline void to_json(nlohmann::json& j, const SurfaceSpan& s) { + j = {{"text", s.text}, {"start", s.start}, {"end", s.end}}; +} +inline void from_json(const nlohmann::json& j, SurfaceSpan& s) { + j.at("text").get_to(s.text); + j.at("start").get_to(s.start); + j.at("end").get_to(s.end); +} + +inline void to_json(nlohmann::json& j, const Coordinates& c) { + j = nlohmann::json::object(); + if (c.x) j["x"] = *c.x; + if (c.y) j["y"] = *c.y; + if (c.z) j["z"] = *c.z; +} +inline void from_json(const nlohmann::json& j, Coordinates& c) { + if (j.contains("x")) c.x = j["x"].get(); + if (j.contains("y")) c.y = j["y"].get(); + if (j.contains("z")) c.z = j["z"].get(); +} + +inline void to_json(nlohmann::json& j, const EvidenceSpan& e) { + j = {{"text", e.text}}; + if (e.start) j["start"] = *e.start; + if (e.end) j["end"] = *e.end; +} +inline void from_json(const nlohmann::json& j, EvidenceSpan& e) { + j.at("text").get_to(e.text); + if (j.contains("start")) e.start = j["start"].get(); + if (j.contains("end")) e.end = j["end"].get(); +} + +// ───────────────────────────────────────────────────────────────────────────── +// JSON serialization — facet data types +// ───────────────────────────────────────────────────────────────────────────── + +inline void to_json(nlohmann::json& j, const TemporalFacetData& d) { + j = {{"temporal_order_refs", d.temporal_order_refs}, {"is_recurring", d.is_recurring}}; + if (d.time_point) j["time_point"] = *d.time_point; + if (d.interval_start) j["interval_start"] = *d.interval_start; + if (d.interval_end) j["interval_end"] = *d.interval_end; + if (d.recurrence_pattern) j["recurrence_pattern"] = *d.recurrence_pattern; + if (d.duration) j["duration"] = *d.duration; +} +inline void from_json(const nlohmann::json& j, TemporalFacetData& d) { + if (j.contains("time_point")) d.time_point = j["time_point"].get(); + if (j.contains("interval_start")) d.interval_start = j["interval_start"].get(); + if (j.contains("interval_end")) d.interval_end = j["interval_end"].get(); + if (j.contains("temporal_order_refs")) j["temporal_order_refs"].get_to(d.temporal_order_refs); + if (j.contains("is_recurring")) d.is_recurring = j["is_recurring"].get(); + if (j.contains("recurrence_pattern")) d.recurrence_pattern = j["recurrence_pattern"].get(); + if (j.contains("duration")) d.duration = j["duration"].get(); +} + +inline void to_json(nlohmann::json& j, const SpatialFacetData& d) { + j = {{"containment_refs", d.containment_refs}}; + if (d.location_label) j["location_label"] = *d.location_label; + if (d.coordinates) j["coordinates"] = *d.coordinates; + if (d.reference_frame) j["reference_frame"] = *d.reference_frame; +} +inline void from_json(const nlohmann::json& j, SpatialFacetData& d) { + if (j.contains("location_label")) d.location_label = j["location_label"].get(); + if (j.contains("coordinates")) d.coordinates = j["coordinates"].get(); + if (j.contains("containment_refs")) j["containment_refs"].get_to(d.containment_refs); + if (j.contains("reference_frame")) d.reference_frame = j["reference_frame"].get(); +} + +inline void to_json(nlohmann::json& j, const CausalFacetData& d) { + j = { + {"cause_refs", d.cause_refs}, + {"effect_refs", d.effect_refs}, + {"enabling_refs", d.enabling_refs}, + {"blocking_refs", d.blocking_refs}, + }; + if (d.causal_strength) j["causal_strength"] = *d.causal_strength; +} +inline void from_json(const nlohmann::json& j, CausalFacetData& d) { + if (j.contains("cause_refs")) j["cause_refs"].get_to(d.cause_refs); + if (j.contains("effect_refs")) j["effect_refs"].get_to(d.effect_refs); + if (j.contains("enabling_refs")) j["enabling_refs"].get_to(d.enabling_refs); + if (j.contains("blocking_refs")) j["blocking_refs"].get_to(d.blocking_refs); + if (j.contains("causal_strength")) d.causal_strength = j["causal_strength"].get(); +} + +inline void to_json(nlohmann::json& j, const LogicalFacetData& d) { + j = { + {"implies_refs", d.implies_refs}, + {"negates_refs", d.negates_refs}, + {"contradicts_refs", d.contradicts_refs}, + }; + if (d.truth_status) j["truth_status"] = *d.truth_status; + if (d.quantifier) j["quantifier"] = *d.quantifier; +} +inline void from_json(const nlohmann::json& j, LogicalFacetData& d) { + if (j.contains("truth_status")) d.truth_status = j["truth_status"].get(); + if (j.contains("quantifier")) d.quantifier = j["quantifier"].get(); + if (j.contains("implies_refs")) j["implies_refs"].get_to(d.implies_refs); + if (j.contains("negates_refs")) j["negates_refs"].get_to(d.negates_refs); + if (j.contains("contradicts_refs")) j["contradicts_refs"].get_to(d.contradicts_refs); +} + +inline void to_json(nlohmann::json& j, const OwnershipFacetData& d) { + j = nlohmann::json::object(); + if (d.owner_ref) j["owner_ref"] = *d.owner_ref; + if (d.owned_ref) j["owned_ref"] = *d.owned_ref; + if (d.quantity_ref) j["quantity_ref"] = *d.quantity_ref; + if (d.temporal_ref) j["temporal_ref"] = *d.temporal_ref; + if (d.transfer_type) j["transfer_type"] = *d.transfer_type; +} +inline void from_json(const nlohmann::json& j, OwnershipFacetData& d) { + if (j.contains("owner_ref")) d.owner_ref = j["owner_ref"].get(); + if (j.contains("owned_ref")) d.owned_ref = j["owned_ref"].get(); + if (j.contains("quantity_ref")) d.quantity_ref = j["quantity_ref"].get(); + if (j.contains("temporal_ref")) d.temporal_ref = j["temporal_ref"].get(); + if (j.contains("transfer_type")) d.transfer_type = j["transfer_type"].get(); +} + +inline void to_json(nlohmann::json& j, const QuantityFacetData& d) { + j = {{"is_exact", d.is_exact}, {"derived_from_refs", d.derived_from_refs}}; + if (d.value) { + std::visit([&j](auto&& v) { j["value"] = v; }, *d.value); + } + if (d.unit) j["unit"] = *d.unit; + if (d.precision) j["precision"] = *d.precision; +} +inline void from_json(const nlohmann::json& j, QuantityFacetData& d) { + if (j.contains("value") && !j["value"].is_null()) { + if (j["value"].is_number()) d.value = j["value"].get(); + else d.value = j["value"].get(); + } + if (j.contains("unit")) d.unit = j["unit"].get(); + if (j.contains("precision")) d.precision = j["precision"].get(); + if (j.contains("is_exact")) d.is_exact = j["is_exact"].get(); + if (j.contains("derived_from_refs")) j["derived_from_refs"].get_to(d.derived_from_refs); +} + +inline void to_json(nlohmann::json& j, const SyntacticFacetData& d) { + j = nlohmann::json::object(); + if (d.grammatical_role) j["grammatical_role"] = *d.grammatical_role; + if (d.head_token) j["head_token"] = *d.head_token; + if (d.dependency_arc) j["dependency_arc"] = *d.dependency_arc; +} +inline void from_json(const nlohmann::json& j, SyntacticFacetData& d) { + if (j.contains("grammatical_role")) d.grammatical_role = j["grammatical_role"].get(); + if (j.contains("head_token")) d.head_token = j["head_token"].get(); + if (j.contains("dependency_arc")) d.dependency_arc = j["dependency_arc"].get(); +} + +inline void to_json(nlohmann::json& j, const SocialFacetData& d) { + j = {{"obligation_refs", d.obligation_refs}}; + if (d.agent_ref) j["agent_ref"] = *d.agent_ref; + if (d.social_act) j["social_act"] = *d.social_act; + if (d.belief_about_ref) j["belief_about_ref"] = *d.belief_about_ref; + if (d.belief_truth_status) j["belief_truth_status"] = *d.belief_truth_status; + if (d.role) j["role"] = *d.role; +} +inline void from_json(const nlohmann::json& j, SocialFacetData& d) { + if (j.contains("agent_ref")) d.agent_ref = j["agent_ref"].get(); + if (j.contains("social_act")) d.social_act = j["social_act"].get(); + if (j.contains("belief_about_ref")) d.belief_about_ref = j["belief_about_ref"].get(); + if (j.contains("belief_truth_status")) d.belief_truth_status = j["belief_truth_status"].get(); + if (j.contains("obligation_refs")) j["obligation_refs"].get_to(d.obligation_refs); + if (j.contains("role")) d.role = j["role"].get(); +} + +inline void to_json(nlohmann::json& j, const PlanningFacetData& d) { + j = { + {"prerequisite_refs", d.prerequisite_refs}, + {"goal_refs", d.goal_refs}, + {"blocks_refs", d.blocks_refs}, + }; + if (d.step_index) j["step_index"] = *d.step_index; + if (d.status) j["status"] = *d.status; +} +inline void from_json(const nlohmann::json& j, PlanningFacetData& d) { + if (j.contains("step_index")) d.step_index = j["step_index"].get(); + if (j.contains("prerequisite_refs")) j["prerequisite_refs"].get_to(d.prerequisite_refs); + if (j.contains("goal_refs")) j["goal_refs"].get_to(d.goal_refs); + if (j.contains("blocks_refs")) j["blocks_refs"].get_to(d.blocks_refs); + if (j.contains("status")) d.status = j["status"].get(); +} + +inline void to_json(nlohmann::json& j, const SemanticFacetData& d) { + j = nlohmann::json::object(); + if (d.semantic_role) j["semantic_role"] = *d.semantic_role; + if (d.frame) j["frame"] = *d.frame; +} +inline void from_json(const nlohmann::json& j, SemanticFacetData& d) { + if (j.contains("semantic_role")) d.semantic_role = j["semantic_role"].get(); + if (j.contains("frame")) d.frame = j["frame"].get(); +} + +inline void to_json(nlohmann::json& j, const CodeStructureFacetData& d) { + j = {{"dependency_refs", d.dependency_refs}}; + if (d.language) j["language"] = *d.language; + if (d.node_type) j["node_type"] = *d.node_type; +} +inline void from_json(const nlohmann::json& j, CodeStructureFacetData& d) { + if (j.contains("language")) d.language = j["language"].get(); + if (j.contains("node_type")) d.node_type = j["node_type"].get(); + if (j.contains("dependency_refs")) j["dependency_refs"].get_to(d.dependency_refs); +} + +// ───────────────────────────────────────────────────────────────────────────── +// JSON serialization — core structs +// ───────────────────────────────────────────────────────────────────────────── + +inline void to_json(nlohmann::json& j, const CanonicalObject& o) { + j = { + {"object_id", o.object_id}, + {"object_kind", o.object_kind}, + {"status", o.status}, + {"confidence", o.confidence}, + {"aliases", o.aliases}, + {"facet_refs", o.facet_refs}, + {"constraint_refs", o.constraint_refs}, + {"evidence_refs", o.evidence_refs}, + {"provenance_refs", o.provenance_refs}, + }; + if (o.surface_span) j["surface_span"] = *o.surface_span; + if (o.canonical_label) j["canonical_label"] = *o.canonical_label; + if (o.merged_into) j["merged_into"] = *o.merged_into; + if (o.split_from) j["split_from"] = *o.split_from; + if (o.source_module) j["source_module"] = *o.source_module; +} +inline void from_json(const nlohmann::json& j, CanonicalObject& o) { + j.at("object_id").get_to(o.object_id); + j.at("object_kind").get_to(o.object_kind); + j.at("status").get_to(o.status); + j.at("confidence").get_to(o.confidence); + if (j.contains("surface_span")) o.surface_span = j["surface_span"].get(); + if (j.contains("canonical_label")) o.canonical_label = j["canonical_label"].get(); + if (j.contains("aliases")) j["aliases"].get_to(o.aliases); + if (j.contains("merged_into")) o.merged_into = j["merged_into"].get(); + if (j.contains("split_from")) o.split_from = j["split_from"].get(); + if (j.contains("facet_refs")) j["facet_refs"].get_to(o.facet_refs); + if (j.contains("constraint_refs")) j["constraint_refs"].get_to(o.constraint_refs); + if (j.contains("evidence_refs")) j["evidence_refs"].get_to(o.evidence_refs); + if (j.contains("provenance_refs")) j["provenance_refs"].get_to(o.provenance_refs); + if (j.contains("source_module")) o.source_module = j["source_module"].get(); +} + +inline void to_json(nlohmann::json& j, const Facet& f) { + j = { + {"facet_id", f.facet_id}, + {"facet_kind", kind_of(f.data)}, + {"object_ref", f.object_ref}, + {"confidence", f.confidence}, + {"source_module", f.source_module}, + {"provenance_refs", f.provenance_refs}, + }; + // Merge facet-specific fields into the same object + nlohmann::json data_j; + std::visit([&data_j](auto&& d) { to_json(data_j, d); }, f.data); + for (auto& [k, v] : data_j.items()) j[k] = v; +} +inline void from_json(const nlohmann::json& j, Facet& f) { + j.at("facet_id").get_to(f.facet_id); + j.at("object_ref").get_to(f.object_ref); + j.at("confidence").get_to(f.confidence); + j.at("source_module").get_to(f.source_module); + if (j.contains("provenance_refs")) j["provenance_refs"].get_to(f.provenance_refs); + + FacetKind kind; + j.at("facet_kind").get_to(kind); + switch (kind) { + case FacetKind::Temporal: f.data = j.get(); break; + case FacetKind::Spatial: f.data = j.get(); break; + case FacetKind::Causal: f.data = j.get(); break; + case FacetKind::Logical: f.data = j.get(); break; + case FacetKind::Ownership: f.data = j.get(); break; + case FacetKind::Quantity: f.data = j.get(); break; + case FacetKind::Syntactic: f.data = j.get(); break; + case FacetKind::Social: f.data = j.get(); break; + case FacetKind::Planning: f.data = j.get(); break; + case FacetKind::Semantic: f.data = j.get(); break; + case FacetKind::CodeStructure: f.data = j.get(); break; + } +} + +inline void to_json(nlohmann::json& j, const Constraint& c) { + j = { + {"constraint_id", c.constraint_id}, + {"constraint_type", c.constraint_type}, + {"argument_refs", c.argument_refs}, + {"polarity", c.polarity}, + {"strength", c.strength}, + {"status", c.status}, + {"contradicted_by_refs", c.contradicted_by_refs}, + {"supports_refs", c.supports_refs}, + {"source_module", c.source_module}, + {"provenance_refs", c.provenance_refs}, + }; + if (c.probability) j["probability"] = *c.probability; + if (c.expression) j["expression"] = *c.expression; + if (c.confidence) j["confidence"] = *c.confidence; +} +inline void from_json(const nlohmann::json& j, Constraint& c) { + j.at("constraint_id").get_to(c.constraint_id); + j.at("constraint_type").get_to(c.constraint_type); + j.at("argument_refs").get_to(c.argument_refs); + j.at("polarity").get_to(c.polarity); + j.at("strength").get_to(c.strength); + j.at("status").get_to(c.status); + j.at("source_module").get_to(c.source_module); + if (j.contains("probability")) c.probability = j["probability"].get(); + if (j.contains("expression")) c.expression = j["expression"].get(); + if (j.contains("confidence")) c.confidence = j["confidence"].get(); + if (j.contains("contradicted_by_refs")) j["contradicted_by_refs"].get_to(c.contradicted_by_refs); + if (j.contains("supports_refs")) j["supports_refs"].get_to(c.supports_refs); + if (j.contains("provenance_refs")) j["provenance_refs"].get_to(c.provenance_refs); +} + +inline void to_json(nlohmann::json& j, const GateSignal& g) { + j = { + {"gate_id", g.gate_id}, + {"activated", g.activated}, + {"confidence", g.confidence}, + {"evidence_spans", g.evidence_spans}, + }; + if (g.method) j["method"] = *g.method; +} +inline void from_json(const nlohmann::json& j, GateSignal& g) { + j.at("gate_id").get_to(g.gate_id); + j.at("activated").get_to(g.activated); + j.at("confidence").get_to(g.confidence); + if (j.contains("evidence_spans")) j["evidence_spans"].get_to(g.evidence_spans); + if (j.contains("method")) g.method = j["method"].get(); +} + +inline void to_json(nlohmann::json& j, const BudgetSpec& b) { + j = {{"level", b.level}}; + if (b.max_objects) j["max_objects"] = *b.max_objects; + if (b.max_constraints) j["max_constraints"] = *b.max_constraints; + if (b.max_iterations) j["max_iterations"] = *b.max_iterations; +} +inline void from_json(const nlohmann::json& j, BudgetSpec& b) { + j.at("level").get_to(b.level); + if (j.contains("max_objects")) b.max_objects = j["max_objects"].get(); + if (j.contains("max_constraints")) b.max_constraints = j["max_constraints"].get(); + if (j.contains("max_iterations")) b.max_iterations = j["max_iterations"].get(); +} + +inline void to_json(nlohmann::json& j, const SpecialistAssignment& a) { + j = { + {"specialist_id", a.specialist_id}, + {"target_refs", a.target_refs}, + {"budget", a.budget}, + }; + if (a.priority) j["priority"] = *a.priority; +} +inline void from_json(const nlohmann::json& j, SpecialistAssignment& a) { + j.at("specialist_id").get_to(a.specialist_id); + j.at("target_refs").get_to(a.target_refs); + j.at("budget").get_to(a.budget); + if (j.contains("priority")) a.priority = j["priority"].get(); +} + +inline void to_json(nlohmann::json& j, const RoutingDecision& r) { + j = { + {"decision_id", r.decision_id}, + {"assignments", r.assignments}, + {"mode", r.mode}, + {"gate_signals", r.gate_signals}, + {"input_object_refs", r.input_object_refs}, + {"provenance_refs", r.provenance_refs}, + }; +} +inline void from_json(const nlohmann::json& j, RoutingDecision& r) { + j.at("decision_id").get_to(r.decision_id); + j.at("assignments").get_to(r.assignments); + j.at("mode").get_to(r.mode); + if (j.contains("gate_signals")) j["gate_signals"].get_to(r.gate_signals); + if (j.contains("input_object_refs")) j["input_object_refs"].get_to(r.input_object_refs); + if (j.contains("provenance_refs")) j["provenance_refs"].get_to(r.provenance_refs); +} + +inline void to_json(nlohmann::json& j, const Provenance& p) { + j = { + {"provenance_id", p.provenance_id}, + {"record_type", p.record_type}, + {"source_module", p.source_module}, + {"input_refs", p.input_refs}, + {"output_refs", p.output_refs}, + {"timestamp", p.timestamp}, + }; + if (p.evidence_span) j["evidence_span"] = *p.evidence_span; + if (p.confidence) j["confidence"] = *p.confidence; + if (p.notes) j["notes"] = *p.notes; +} +inline void from_json(const nlohmann::json& j, Provenance& p) { + j.at("provenance_id").get_to(p.provenance_id); + j.at("record_type").get_to(p.record_type); + j.at("source_module").get_to(p.source_module); + j.at("timestamp").get_to(p.timestamp); + if (j.contains("input_refs")) j["input_refs"].get_to(p.input_refs); + if (j.contains("output_refs")) j["output_refs"].get_to(p.output_refs); + if (j.contains("evidence_span")) p.evidence_span = j["evidence_span"].get(); + if (j.contains("confidence")) p.confidence = j["confidence"].get(); + if (j.contains("notes")) p.notes = j["notes"].get(); +} + +inline void to_json(nlohmann::json& j, const ResolutionEntry& e) { + j = { + {"operation", e.operation}, + {"affected_refs", e.affected_refs}, + {"result", e.result}, + }; + if (e.confidence_after) j["confidence_after"] = *e.confidence_after; + if (e.note) j["note"] = *e.note; +} +inline void from_json(const nlohmann::json& j, ResolutionEntry& e) { + j.at("operation").get_to(e.operation); + j.at("affected_refs").get_to(e.affected_refs); + j.at("result").get_to(e.result); + if (j.contains("confidence_after")) e.confidence_after = j["confidence_after"].get(); + if (j.contains("note")) e.note = j["note"].get(); +} + +inline void to_json(nlohmann::json& j, const ContradictionRecord& c) { + j = {{"constraint_refs", c.constraint_refs}, {"description", c.description}}; + if (c.resolution_attempt) j["resolution_attempt"] = *c.resolution_attempt; +} +inline void from_json(const nlohmann::json& j, ContradictionRecord& c) { + j.at("constraint_refs").get_to(c.constraint_refs); + j.at("description").get_to(c.description); + if (j.contains("resolution_attempt")) c.resolution_attempt = j["resolution_attempt"].get(); +} + +inline void to_json(nlohmann::json& j, const SpecialistError& e) { + j = { + {"error_kind", e.error_kind}, + {"affected_refs", e.affected_refs}, + {"description", e.description}, + }; +} +inline void from_json(const nlohmann::json& j, SpecialistError& e) { + j.at("error_kind").get_to(e.error_kind); + j.at("affected_refs").get_to(e.affected_refs); + j.at("description").get_to(e.description); +} + +inline void to_json(nlohmann::json& j, const WorldState& w) { + j = { + {"state_id", w.state_id}, + {"stage", w.stage}, + {"objects", w.objects}, + {"facets", w.facets}, + {"constraints", w.constraints}, + {"provenance", w.provenance}, + {"gate_signals", w.gate_signals}, + {"resolution_log", w.resolution_log}, + {"open_contradictions", w.open_contradictions}, + {"derivation_trace", w.derivation_trace}, + }; + if (w.input_text) j["input_text"] = *w.input_text; + if (w.routing_decision) j["routing_decision"] = *w.routing_decision; + if (w.synthesis_answer) j["synthesis_answer"] = *w.synthesis_answer; + if (w.synthesis_confidence) j["synthesis_confidence"] = *w.synthesis_confidence; +} +inline void from_json(const nlohmann::json& j, WorldState& w) { + j.at("state_id").get_to(w.state_id); + j.at("stage").get_to(w.stage); + if (j.contains("input_text")) w.input_text = j["input_text"].get(); + if (j.contains("objects")) j["objects"].get_to(w.objects); + if (j.contains("facets")) j["facets"].get_to(w.facets); + if (j.contains("constraints")) j["constraints"].get_to(w.constraints); + if (j.contains("provenance")) j["provenance"].get_to(w.provenance); + if (j.contains("routing_decision")) w.routing_decision = j["routing_decision"].get(); + if (j.contains("gate_signals")) j["gate_signals"].get_to(w.gate_signals); + if (j.contains("resolution_log")) j["resolution_log"].get_to(w.resolution_log); + if (j.contains("open_contradictions")) j["open_contradictions"].get_to(w.open_contradictions); + if (j.contains("synthesis_answer")) w.synthesis_answer = j["synthesis_answer"].get(); + if (j.contains("synthesis_confidence")) w.synthesis_confidence = j["synthesis_confidence"].get(); + if (j.contains("derivation_trace")) j["derivation_trace"].get_to(w.derivation_trace); +} + +} // namespace ucwm diff --git a/synthesis/contract.md b/synthesis/contract.md new file mode 100644 index 0000000..c6d9875 --- /dev/null +++ b/synthesis/contract.md @@ -0,0 +1,123 @@ +# Synthesis Interface Contract + +The synthesis module is the final stage. It reads resolved WorldState and produces a natural language answer plus a derivation trace. The answer must be derivable from resolved constraints — not generated from surface token patterns. + +--- + +## Design principle + +Synthesis is translation, not reasoning. All reasoning was done by specialists and the resolver. Synthesis converts the stabilized structured state into human-readable output. + +If the answer cannot be derived from resolved constraints, synthesis should say so rather than confabulate. + +--- + +## Input + +``` +SynthesisInput { + world_state: WorldState -- must have stage = "resolved" + original_query: string -- the user's original input text + synthesis_mode: string -- "direct" | "explain" | "trace" +} +``` + +**Synthesis modes:** + +- `direct`: produce the answer as a natural sentence. Minimal explanation. +- `explain`: produce the answer with a short explanation of how it was derived. +- `trace`: produce the answer plus a full derivation trace listing every constraint and resolver operation used. + +--- + +## Output + +``` +SynthesisOutput { + answer: string + confidence: float [0, 1] + derivation_trace: string[] -- ordered list of constraint_ids / operations used + open_questions: string[]? -- aspects the system could not resolve + synthesis_mode: string -- echoes input mode +} +``` + +--- + +## Derivation trace + +The `derivation_trace` must list, in order: +1. The objects and facts the answer is based on +2. The constraints that were applied +3. The resolver operations that stabilized the relevant values + +The trace must be sufficient for a reader to reconstruct the answer without the system. + +--- + +## Invariants + +1. **Grounded answer.** Every factual claim in the answer must correspond to a resolved constraint or a confirmed object in WorldState. Synthesis does not add new facts. + +2. **Uncertainty acknowledgment.** If a relevant constraint has `status: contradicted` or is in `open_contradictions`, the answer must acknowledge the uncertainty rather than silently pick a side. + +3. **Trace completeness.** `derivation_trace` must list all constraints used. If a claim appears in the answer, the constraint supporting it must appear in the trace. + +4. **Stage check.** Synthesis must reject WorldState where `stage ≠ "resolved"` with an error, not a guess. + +--- + +## Answer generation patterns + +### Simple factual answer + +When WorldState contains a single resolved quantity or state that directly answers the query: + +``` +Template: "{Subject} {verb} {value} {unit}." +Example: "Alice has 3 apples." + +Derivation: + 1. E1 (Alice) — active entity + 2. Q_alice_final — quantity, value=3, unit="apples" + 3. C1: owns(Alice, apples, count=5) at t0 — resolved + 4. C2: transfers(Alice, Bob, apples, count=2) at t1 — resolved + 5. C3: quantity_difference(Q_alice_final, Q1, Q2) — resolved, computed 5-2=3 + 6. Resolver Op: merge_coreference (she → Alice) + 7. Resolver Op: resolve_arithmetic (Q_alice_final.value = 3) +``` + +### Uncertain answer + +When `open_contradictions` is non-empty or relevant objects are low-confidence: + +``` +Template: "{Subject} likely {verb} {value}, though {uncertainty description}." +``` + +### No answer available + +When the query asks for something not covered by any resolved constraint: + +``` +Template: "The available information does not determine {query_topic}." +``` + +--- + +## Synthesis does not do + +- Infer new facts beyond resolved constraints +- Choose between contradicted constraints without flagging the contradiction +- Generate fluent text that goes beyond what WorldState supports +- Add caveats, hedges, or stylistic embellishments not grounded in confidence values + +--- + +## Contract test requirements + +- Given resolved WorldState with Q_alice_final.value=3 and owns constraint resolved: answer must contain "3" and "Alice" +- Given WorldState with `open_contradictions` non-empty: answer must not state a definitive value for the contradicted quantity +- `derivation_trace` must be non-empty for any non-trivial answer +- `stage` check: must raise error given WorldState where stage = "post_specialist" +- All emitted `derivation_trace` entries must reference valid IDs in WorldState diff --git a/synthesis/language_bindings.md b/synthesis/language_bindings.md new file mode 100644 index 0000000..932c694 --- /dev/null +++ b/synthesis/language_bindings.md @@ -0,0 +1,156 @@ +# Language Binding Interface + +The language binding layer is the boundary between messy natural language and the structured UCWM reasoning core. It consists of exactly two interface stubs: + +``` +to_ucwm(text_input) → WorldState +from_ucwm(world_state) → text_output +``` + +These must remain separable. The reasoning core must never depend on how language enters or exits. + +--- + +## `to_ucwm` — Input binding + +``` +to_ucwm { + input: + text: string -- raw natural language input + context: WorldState? -- optional prior world state (multi-turn) + options: BindingOptions? + + output: + world_state: WorldState -- stage = "post_gate" with gate_signals populated + -- and initial object proposals if perception runs here + + BindingOptions { + run_gates: boolean -- default: true + run_perception: boolean -- default: true (runs object proposal) + language: string? -- ISO 639-1 language code, default "en" + debug: boolean -- emit step-by-step trace + } +} +``` + +### Responsibilities + +`to_ucwm` is responsible for: +1. Running the Relation Gate Array on the input text, producing `GateSignal[]` +2. Running the perception / object proposal stage, producing initial `CanonicalObject[]` +3. Packaging both into a `WorldState` with `stage = "post_gate"` or `"post_proposal"` +4. Returning a valid WorldState — never raw text, never embeddings as primary output + +### What `to_ucwm` must NOT do + +- Run specialists +- Emit constraints +- Produce answers +- Modify a prior WorldState in-place (multi-turn must pass context as a separate field, not mutate) + +### Naive implementation (first slice) + +A minimal `to_ucwm` implementation: +1. Run all gates using rule-based implementations from `gates/simple_rules.md` +2. For each activated gate, tag relevant spans +3. Propose one `CanonicalObject` per clearly identified entity, quantity, or event span +4. Return WorldState with these proposals + +This is sufficient for the first vertical slice. Upgrade to a neural perception model later. + +--- + +## `from_ucwm` — Output binding + +``` +from_ucwm { + input: + world_state: WorldState -- must have stage = "resolved" + original_query: string -- original user input text + mode: string -- "direct" | "explain" | "trace" + options: OutputOptions? + + output: + text: string -- natural language answer + confidence: float + derivation_trace: string[] + open_questions: string[]? + + OutputOptions { + max_length: integer? -- max characters in output text + language: string? -- ISO 639-1, default "en" + debug: boolean + } +} +``` + +### Responsibilities + +`from_ucwm` is responsible for: +1. Checking WorldState.stage = "resolved" (reject otherwise with an error) +2. Delegating to the synthesis module (see `synthesis/contract.md`) +3. Returning a typed `SynthesisOutput` — never directly generating text from tokens + +### What `from_ucwm` must NOT do + +- Access raw input tokens +- Run specialists or the resolver +- Add facts not present in resolved WorldState + +--- + +## Contract invariants + +1. **Round-trip transparency.** For any resolved WorldState W: + ``` + from_ucwm(W).derivation_trace must reference only IDs that exist in W + ``` + +2. **Stage enforcement.** `to_ucwm` always produces a WorldState with stage at or before `post_proposal`. `from_ucwm` always requires stage = `resolved`. The pipeline between them is the responsibility of the caller. + +3. **Stateless bindings.** Neither binding holds state. Multi-turn context is passed as an argument, not stored inside the binding. + +4. **Language is at the edges only.** Raw language appears only at the input of `to_ucwm` and the output of `from_ucwm`. No other module may receive or emit raw language as a primary typed output. + +--- + +## Multi-turn handling + +For conversational multi-turn: + +``` +turn_1: + ws_1 = to_ucwm(text="Alice had 5 apples") + ws_1_resolved = pipeline(ws_1) + +turn_2: + ws_2 = to_ucwm(text="She gave Bob 2.", context=ws_1_resolved) + -- ws_2 inherits E1 (Alice), O_apples, Q1 from ws_1_resolved + -- entity specialist resolves "She" → E1 via existing alias +``` + +The context WorldState is read-only in `to_ucwm`. New proposals may reference objects from context but do not modify them directly. The resolver handles merges. + +--- + +## Debug trace format + +When `options.debug = true`, both bindings emit a structured log: + +```json +{ + "stage": "to_ucwm", + "input_length": 42, + "gates_fired": ["has_entity_reference", "has_quantity", "has_ownership_transfer"], + "objects_proposed": 5, + "world_state_stage": "post_proposal" +} + +{ + "stage": "from_ucwm", + "world_state_stage": "resolved", + "answer": "Alice has 3 apples.", + "confidence": 0.97, + "derivation_steps": 7 +} +``` diff --git a/tests/contracts/gate_contract_tests.md b/tests/contracts/gate_contract_tests.md new file mode 100644 index 0000000..2b1ce84 --- /dev/null +++ b/tests/contracts/gate_contract_tests.md @@ -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. diff --git a/tests/contracts/resolver_contract_tests.md b/tests/contracts/resolver_contract_tests.md new file mode 100644 index 0000000..d812d35 --- /dev/null +++ b/tests/contracts/resolver_contract_tests.md @@ -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. +``` diff --git a/tests/contracts/specialist_contract_tests.md b/tests/contracts/specialist_contract_tests.md new file mode 100644 index 0000000..160b323 --- /dev/null +++ b/tests/contracts/specialist_contract_tests.md @@ -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 | diff --git a/tests/test_types.cpp b/tests/test_types.cpp new file mode 100644 index 0000000..106ca8b --- /dev/null +++ b/tests/test_types.cpp @@ -0,0 +1,436 @@ +#include +#include +#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(); + + 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(); + + 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(f.data)); + + auto& d = std::get(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(f.data); + REQUIRE(d.value.has_value()); + REQUIRE(std::holds_alternative(*d.value)); + REQUIRE_THAT(std::get(*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(f.data); + REQUIRE(std::holds_alternative(*d.value)); + REQUIRE(std::get(*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(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(); + REQUIRE(restored.facet_id == original.facet_id); + REQUIRE(kind_of(restored.data) == FacetKind::Temporal); + + auto& d = std::get(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(); + auto& d = std::get(restored.data); + REQUIRE(std::holds_alternative(*d.value)); + REQUIRE_THAT(std::get(*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(); + + 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(); + + 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(); + REQUIRE(restored.error_kind == ErrorKind::Ambiguous); + REQUIRE(restored.affected_refs.size() == 2); + REQUIRE(restored.description == err.description); +}