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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 79 additions & 21 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@ ucon is a dimensional analysis library for engineers building systems where unit
| v0.6.0 | Pydantic + Serialization | Complete |
| v0.6.x | MCP Server | Complete |
| v0.6.x | LogMap + Nines | Complete |
| v0.6.x | Dimensional Type Safety | Planned |
| v0.7.0 | NumPy Array Support | Planned |
| v0.6.x | Dimensional Type Safety | Complete |
| v0.7.0 | MCP Error Suggestions | Complete |
| v0.7.x | MCP Compute + Schema Constraints | Planned |
| v0.8.0 | String Parsing | Planned |
| v0.9.0 | Constants + Logarithmic Units | Planned |
| v0.10.0 | DataFrame Integration | Planned |
| v0.10.0 | Scientific Computing | Planned |
| v1.0.0 | API Stability | Planned |

---
Expand Down Expand Up @@ -221,38 +222,89 @@ Building on v0.5.x baseline:

---

## v0.6.x — Dimensional Type Safety (Planned)
## v0.6.x — Dimensional Type Safety (Complete)

**Theme:** Type-directed validation for AI agents and domain formulas.
**Theme:** Type-directed validation for domain formulas.

- [x] Human-readable derived dimension names (`derived(length^3/time)` not `Vector(...)`)
- [x] `Number[Dimension]` type-safe generics via `typing.Annotated`
- [x] `DimConstraint` marker class for annotation introspection
- [x] `@enforce_dimensions` decorator for runtime validation at function boundaries
- [ ] MCP error suggestions with actionable recovery hints

**Outcomes:**
- Dimension errors caught at function boundaries with clear messages
- AI agents can self-correct via readable error diagnostics
- Domain authors declare dimensional constraints declaratively, not imperatively
- MCP server returns structured errors with fuzzy-matched suggestions
- Foundation for schema-level dimension constraints in MCP tools
- Foundation for MCP error suggestions and schema-level constraints

---

## v0.7.0 — NumPy Array Support
## v0.7.0 — MCP Error Suggestions (Complete)

**Theme:** Scientific computing integration.
**Theme:** AI agent self-correction.

- [ ] `Number` wraps `np.ndarray` values
- [ ] Vectorized conversion
- [ ] Vectorized arithmetic with uncertainty propagation
- [ ] Performance benchmarks
- [x] `ConversionError` response model with `likely_fix` and `hints`
- [x] Fuzzy matching for unknown units with confidence tiers (≥0.7 for `likely_fix`)
- [x] Compatible unit suggestions from graph edges
- [x] Pseudo-dimension isolation explanation
- [x] `ucon/mcp/suggestions.py` module (independently testable)
- [x] Error handling in `convert()`, `check_dimensions()`, `list_units()`

**Outcomes:**
- Seamless integration with NumPy-based scientific workflows
- Efficient batch conversions for large datasets
- Performance characteristics documented and optimized
- MCP tools return structured errors instead of raw exceptions
- High-confidence fixes enable single-retry correction loops
- AI agents can self-correct via readable error diagnostics
- Foundation for schema-level dimension constraints

---

## v0.7.1 — Pre-Compute Foundations (Planned)

**Theme:** Architectural prerequisites for multi-step factor-label chains.

- [ ] SI symbol coverage audit (ensure `A`, `V`, `W`, etc. in case-sensitive registry)
- [ ] Add `step: int | None` field to `ConversionError` for chain error localization
- [ ] Extract `_resolve_unit(name, parameter)` helper to reduce try/except duplication
- [ ] Add `build_parse_error` builder for malformed composite expressions
- [ ] Document priority alias invariant for contributors

**Outcomes:**
- Expressions like `V/mA`, `mA·h`, `µA/cm²` resolve correctly
- Error responses can localize failures to specific steps in a chain
- MCP server code is DRY and ready for compute's N-factor resolution
- `ParseError` wrapped in structured `ConversionError` like other error types

---

## v0.7.2 — Compute Tool (Planned)

**Theme:** Multi-step factor-label calculations for AI agents.

- [ ] `compute` tool for dimensionally-validated factor-label chains
- [ ] `steps` array in response showing intermediate dimensional state
- [ ] Per-step error localization using `ConversionError.step`
- [ ] Multi-factor cancellation tests for `UnitProduct` (medical dosage, stoichiometry)

**Outcomes:**
- AI agents can run factor-label chains with dimensional safety at each step
- Intermediate state visible for debugging and benchmarks (SLM vs LLM comparison)
- Agents can self-correct mid-chain rather than only at the end
- `UnitProduct` cancellation logic validated against realistic compute inputs

---

## v0.7.x — Schema-Level Dimension Constraints (Planned)

**Theme:** Pre-call validation for AI agents.

- [ ] Expose `DimConstraint` in MCP tool schemas
- [ ] Schema generator introspects dimension constraints from `@enforce_dimensions` functions
- [ ] Formula registration/discovery mechanism for domain packages

**Outcomes:**
- MCP schemas declare expected dimensions per parameter
- LLMs can validate inputs before calling, reducing round-trips
- Completes the type-directed correction loop
- Foundation for ucon.dev marketplace of domain formula packages

---

Expand Down Expand Up @@ -290,19 +342,25 @@ Building on v0.5.x baseline:

---

## v0.10.0 — DataFrame Integration
## v0.10.0 — Scientific Computing

**Theme:** Data science workflows.
**Theme:** NumPy and DataFrame integration.

- [ ] `Number` wraps `np.ndarray` values
- [ ] Vectorized conversion and arithmetic
- [ ] Vectorized uncertainty propagation
- [ ] Polars integration: `NumberColumn` type
- [ ] Pandas integration: `NumberSeries` type
- [ ] Column-wise conversion
- [ ] Unit-aware arithmetic on columns
- [ ] Performance benchmarks

**Outcomes:**
- Seamless integration with NumPy-based scientific workflows
- Efficient batch conversions for large datasets
- First-class support for data science workflows
- Unit-safe transformations on tabular data
- Interoperability with modern DataFrame ecosystems
- Performance characteristics documented and optimized

---

Expand Down
196 changes: 184 additions & 12 deletions tests/ucon/mcp/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ class TestConvertToolErrors(unittest.TestCase):
def setUpClass(cls):
try:
from ucon.mcp.server import convert
from ucon.mcp.suggestions import ConversionError
cls.convert = staticmethod(convert)
cls.ConversionError = ConversionError
cls.skip_tests = False
except ImportError:
cls.skip_tests = True
Expand All @@ -88,22 +90,24 @@ def setUp(self):
self.skipTest("mcp not installed")

def test_unknown_source_unit(self):
"""Test that unknown source unit raises error."""
from ucon.units import UnknownUnitError
with self.assertRaises(UnknownUnitError):
self.convert(1, "foobar", "m")
"""Test that unknown source unit returns ConversionError."""
result = self.convert(1, "foobar", "m")
self.assertIsInstance(result, self.ConversionError)
self.assertEqual(result.error_type, "unknown_unit")
self.assertEqual(result.parameter, "from_unit")

def test_unknown_target_unit(self):
"""Test that unknown target unit raises error."""
from ucon.units import UnknownUnitError
with self.assertRaises(UnknownUnitError):
self.convert(1, "m", "bazqux")
"""Test that unknown target unit returns ConversionError."""
result = self.convert(1, "m", "bazqux")
self.assertIsInstance(result, self.ConversionError)
self.assertEqual(result.error_type, "unknown_unit")
self.assertEqual(result.parameter, "to_unit")

def test_dimension_mismatch(self):
"""Test that incompatible dimensions raise error."""
from ucon.graph import DimensionMismatch
with self.assertRaises(DimensionMismatch):
self.convert(1, "m", "s")
"""Test that incompatible dimensions return ConversionError."""
result = self.convert(1, "m", "s")
self.assertIsInstance(result, self.ConversionError)
self.assertEqual(result.error_type, "dimension_mismatch")


class TestListUnitsTool(unittest.TestCase):
Expand Down Expand Up @@ -347,5 +351,173 @@ def test_sorted(self):
self.assertEqual(result, sorted(result))


class TestConvertToolSuggestions(unittest.TestCase):
"""Test suggestion features in the convert tool."""

@classmethod
def setUpClass(cls):
try:
from ucon.mcp.server import convert
from ucon.mcp.suggestions import ConversionError
cls.convert = staticmethod(convert)
cls.ConversionError = ConversionError
cls.skip_tests = False
except ImportError:
cls.skip_tests = True

def setUp(self):
if self.skip_tests:
self.skipTest("mcp not installed")

def test_typo_single_match(self):
"""Test that typo with single high-confidence match gets likely_fix."""
result = self.convert(100, "meetr", "ft")
self.assertIsInstance(result, self.ConversionError)
self.assertEqual(result.error_type, "unknown_unit")
self.assertEqual(result.parameter, "from_unit")
self.assertIsNotNone(result.likely_fix)
self.assertIn("meter", result.likely_fix)

def test_bad_to_unit(self):
"""Test that typo in to_unit position is detected."""
result = self.convert(100, "meter", "feeet")
self.assertIsInstance(result, self.ConversionError)
self.assertEqual(result.parameter, "to_unit")
# Should suggest "foot"
self.assertTrue(
(result.likely_fix and "foot" in result.likely_fix) or
any("foot" in h for h in result.hints)
)

def test_unrecognizable_no_spurious_matches(self):
"""Test that completely unknown unit doesn't produce spurious matches."""
result = self.convert(100, "xyzzy", "kg")
self.assertIsInstance(result, self.ConversionError)
self.assertIsNone(result.likely_fix)
self.assertTrue(any("list_units" in h for h in result.hints))

def test_dimension_mismatch_readable(self):
"""Test that dimension mismatch error uses readable names."""
result = self.convert(100, "meter", "second")
self.assertIsInstance(result, self.ConversionError)
self.assertEqual(result.error_type, "dimension_mismatch")
self.assertEqual(result.got, "length")
self.assertIn("length", result.error)
self.assertIn("time", result.error)
self.assertNotIn("Vector", result.error)

def test_derived_dimension_readable(self):
"""Test that derived dimension uses readable name in error."""
result = self.convert(1, "m/s", "kg")
self.assertIsInstance(result, self.ConversionError)
self.assertIn("velocity", result.error)
self.assertNotIn("Vector", result.error)

def test_unnamed_derived_dimension(self):
"""Test that unnamed derived dimension doesn't show Vector."""
result = self.convert(1, "m^3/s", "kg")
self.assertIsInstance(result, self.ConversionError)
# Should show readable format, not Vector(...)
self.assertNotIn("Vector", result.error)
# Should have some dimension info
self.assertTrue("length" in result.error or "derived(" in result.error)

def test_pseudo_dimension_explains_isolation(self):
"""Test that pseudo-dimension isolation is explained."""
result = self.convert(1, "radian", "percent")
self.assertIsInstance(result, self.ConversionError)
self.assertEqual(result.error_type, "no_conversion_path")
self.assertEqual(result.got, "angle")
self.assertEqual(result.expected, "ratio")
self.assertTrue(
any("cannot interconvert" in h or "isolated" in h for h in result.hints)
)

def test_compatible_units_in_hints(self):
"""Test that dimension mismatch includes compatible units."""
result = self.convert(100, "meter", "second")
self.assertIsInstance(result, self.ConversionError)
# Should suggest compatible length units
hints_str = str(result.hints)
self.assertTrue(
"ft" in hints_str or "in" in hints_str or
"foot" in hints_str or "inch" in hints_str
)

def test_no_vector_in_any_error(self):
"""Test that no error response contains raw Vector representation."""
cases = [
("m^3/s", "kg"),
("kg*m/s^2", "A"),
]
for from_u, to_u in cases:
result = self.convert(1, from_u, to_u)
if isinstance(result, self.ConversionError):
self.assertNotIn("Vector(", result.error)
for h in result.hints:
self.assertNotIn("Vector(", h)


class TestCheckDimensionsErrors(unittest.TestCase):
"""Test error handling in the check_dimensions tool."""

@classmethod
def setUpClass(cls):
try:
from ucon.mcp.server import check_dimensions
from ucon.mcp.suggestions import ConversionError
cls.check_dimensions = staticmethod(check_dimensions)
cls.ConversionError = ConversionError
cls.skip_tests = False
except ImportError:
cls.skip_tests = True

def setUp(self):
if self.skip_tests:
self.skipTest("mcp not installed")

def test_bad_unit_a(self):
"""Test that bad unit_a returns ConversionError."""
result = self.check_dimensions("meetr", "foot")
self.assertIsInstance(result, self.ConversionError)
self.assertEqual(result.parameter, "unit_a")

def test_bad_unit_b(self):
"""Test that bad unit_b returns ConversionError."""
result = self.check_dimensions("meter", "fooot")
self.assertIsInstance(result, self.ConversionError)
self.assertEqual(result.parameter, "unit_b")


class TestListUnitsErrors(unittest.TestCase):
"""Test error handling in the list_units tool."""

@classmethod
def setUpClass(cls):
try:
from ucon.mcp.server import list_units
from ucon.mcp.suggestions import ConversionError
cls.list_units = staticmethod(list_units)
cls.ConversionError = ConversionError
cls.skip_tests = False
except ImportError:
cls.skip_tests = True

def setUp(self):
if self.skip_tests:
self.skipTest("mcp not installed")

def test_bad_dimension_filter(self):
"""Test that bad dimension filter returns ConversionError."""
result = self.list_units(dimension="lenth")
self.assertIsInstance(result, self.ConversionError)
self.assertEqual(result.parameter, "dimension")
# Should suggest "length"
self.assertTrue(
(result.likely_fix and "length" in result.likely_fix) or
any("length" in h for h in result.hints)
)


if __name__ == '__main__':
unittest.main()
Loading