crystflux 0.23.12
A type-safe immutable JSON toolkit with chainable method access.
Requires Python: >=3.11

# CrystFlux

![Python Version](https://img.shields.io/badge/python-3.11%2B-blue)
![License](https://img.shields.io/badge/license-MIT-green)
![PyPI Version](https://img.shields.io/pypi/v/crystflux)
![Immutability](https://img.shields.io/badge/immutability-deep%20recursive-orange)
![JSON Access](https://img.shields.io/badge/JSON-type%20safe%20access-yellow)

CrystFlux is a type-safe immutable JSON toolkit with chainable method access.

## Related Documents

- [Essential Files Map](ESSENTIAL_FILES_MAP.md)
- [Structure Guide](src/crystflux/v1/STRUCTURE.md)
- [Concepts](docs/01-concepts/README.md)

## Table of Contents

- [Introduction](#introduction)
- [Installation](#installation)
- [Getting Started](#getting-started)
- [Usage Example](#usage-example)
  - [Navigating Data](#navigating-data)
  - [Filtering and Mapping](#filtering-and-mapping)
  - [Handling Missing Structures](#handling-missing-structures)
- [as / expected](#as-expected)
- [Latent / Strict](#latent-strict)
  - [Value Object Types](#value-object-types)
- [Dream Mode](#dream-mode)
  - [Hallucination Tolerance](#hallucination-tolerance)
  - [Observed Logging](#observed-logging)
- [License](#license)

## Introduction

CrystFlux is a library. It was born from exploring better approaches to solve AI assistants' struggles with JSON handling.

AI tends to have difficulty generating robust code for handling dynamic, recursive data structures. As a result, it often produces code that induces runtime errors (`KeyError`, `IndexError`, `AttributeError`, etc.) due to missing keys or type mismatches, or code where the core logic is buried under excessive defensive checks (frequent `isinstance` checks, `None` checks, `cast`).

To minimize AI inference costs and guide more stable implementations, CrystFlux provides a JSON accessor with the following characteristics:

- **Deep Immutability:** Recursively transforms JSON data into immutable structures, eliminating state changes and side effects from the logical flow.
- **Fluent Interface:** Method chains that support `map` and `filter` enable linear, structured data extraction without complex nesting or conditional branching.
- **Type-Specific Accessors:** Values can be retrieved with type specifications using `as_str()` / `expected_str()`, etc. On type mismatch, `as_*` returns `None`, while `expected_*` raises `TypeError`.
- **Multi-Mode Safety:** Behavior can be switched according to requirements.

| Mode | Description |
|---|---|
| **Strict** | Strict mode that raises exceptions immediately on schema violations |
| **Latent (Safe)** | Safe mode that treats `KeyError`, `IndexError` as `None` (Void) and continues the chain |
| **Dream** | Research mode that even tolerates hallucinated (non-existent) method calls, continues the chain, and observes/records them (disabled by default) |

---

## Installation

### pip

```bash
# Using pip
pip install crystflux
# Verify installation and version
python -c "import crystflux.v1; print(crystflux.__version__)"
```

### uv

```bash
# Using uv
uv pip install crystflux
# Verify installation and version
uv run python -c "import crystflux.v1; print(crystflux.__version__)"
```

For detailed setup instructions, refer to the following files:

- [INSTALL.md](INSTALL.md)
- [INSTALL_ja.md](INSTALL_ja.md)

---

## Getting Started

A simple usage example that you can run immediately after installation to verify that it works.

```bash
python -c "
from crystflux.v1 import Crystallizer;
val = Crystallizer.latent({'name': 'Alice', 'age': 30});
print(val.get('name').as_str());
print(val.get('age').as_int());
print(val.get('missing').as_str());
"
```
**Result:**

```bash
Alice
30
None
```

---

## Usage Example

An implementation example handling semi-structured data containing missing values and type mismatches in `Latent (Safe)` mode.

### Target Data

Prepare sample data to be used in the following examples.

```python
data = {
    "users": [
        {"id": 1, "active": True,  "profile": {"name": "Alice", "age": 30}},
        {"id": 2, "active": True,  "profile": {"name": "Anna",  "age": 17}},
        {"id": 3, "active": 0,     "profile": None},
        {},
        {"id": 5, "active": False, "profile": {"name": "Bob"}},
        {"id": 6, "active": True,  "profile": {"name": "Carol"}}
    ]
}

from crystflux.v1 import Crystallizer, it

# Wrap the data (Latent mode)
val = Crystallizer.latent(data)
```

### Navigating Data

Use `get()` and `at()` to navigate nested data structures.

These methods return wrapped `Value` objects rather than raw Python values.
This enables method chaining and safe handling of missing data.
To extract the actual value (`str`, `int`, etc.), use accessor methods like `as_str()` or `as_int()`.

```python
val.get("name")           # -> Value object
val.get("name").as_str()  # -> "Alice"
```

See [Latent / Strict](#latent--strict) for `Value` object types and mode-specific behavior.

#### get() - Object Key Access

Retrieve values by specifying object keys.

```python
val.get("users")                      # Value of "users" key (array)
val.get("users").get(0)               # NG: get() only accepts string keys
```

Missing keys return `None` in Latent mode.

```python
val.get("missing").as_str()
# -> None
```

#### at() - Array Index Access

Retrieve elements by specifying array indices.

```python
val.get("users").at(0)                # First user
val.get("users").at(2)                # Third user (id=3)
```

Out-of-range indices return `None` in Latent mode.

```python
val.get("users").at(999).get("id").as_int()
# -> None
```

#### Combining get() and at()

For nested structures of objects and arrays, use `get()` and `at()` alternately.

```python
# Get the name of the second user (index=1)
val.get("users").at(1).get("profile").get("name").as_str()
# -> "Anna"

# Safe chaining even for missing paths (Latent mode)
val.get("users").at(3).get("profile").get("name").as_str()
# -> None (user at id=4 has no profile key)
```

#### Comparison

| Method | Target | Argument | Example |
|--------|--------|----------|--------|
| `get(key)` | Object | `str` | `val.get("users")` |
| `at(index)` | Array | `int` | `val.get("users").at(0)` |

Both methods raise exceptions on missing data in Strict mode.
See [Latent / Strict](#latent--strict) for details.

### Basic Extraction

Access array elements and extract nested values.
Missing keys or type mismatches along the path are treated as `None` (Void).

```python
names = (
    val.get("users")
       .map(lambda u: u.get("profile").get("name"))
       .as_str_array()
)
# Result: ('Alice', 'Anna', 'Bob', 'Carol')
# (None values are excluded by .as_str_array())
```

### Filtering and Mapping

Instead of lambda, you can also use `it` (Projection) to declaratively describe value type conversions and condition evaluations. Property access (`.`) and `get()` are treated equivalently.
Type conversions (`as_bool`, `as_int`) safely return `None` (treated as False) when the target value is incompatible.

```python
active_adults = (
    val.get("users")
       .filter(it.active.as_bool().is_true())  # Only those where active is True (bool)
       .filter(it.profile.age.as_int() >= 20)  # Only those where age is 20 or above
       .map(it.profile.name)                   # Extract names
       .as_str_array()
)
# Result: ('Alice',)
```

### Handling Missing Structures

Structural missing (Void state) can also be explicitly detected.

**Detecting missing keys:**
```python
missing_key = (
    val.get("users")
       .filter(lambda u: u.get("profile").is_void())
)
# Result: Wraps object for id=4 (empty dict - no profile key)
```

**Detecting when value is None or key is missing:**
```python
profile_is_none = (
    val.get("users")
       .filter(lambda u: u.get("profile").as_json_value() is None)
)
# Result: Wraps objects for id=3 (profile is None) and id=4 (empty dict)
```

> [!NOTE]
> In Latent mode, `profile: None` is treated as a valid value (`is_void()` returns False). Only missing keys are considered Void.

## as / expected {#as-expected}

The difference between `as_` methods and `expected_` methods lies in their behavior when type mismatches or missing values occur.
`as_` returns `None`, while `expected_` raises an exception (`TypeError`).

### Same Result (Type Matches)

When types match, both return the same value.

```python
from crystflux.v1 import Crystallizer

data = {
    "age": 30,
    "name": "Alice",
}

val = Crystallizer.latent(data)

val.get("age").as_int()
# -> 30
val.get("age").expected_int()
# -> 30

val.get("name").as_str()
# -> "Alice"
val.get("name").expected_str()
# -> "Alice"
```

### Different Result (Type Mismatch or Missing)

When types differ or keys are missing, behavior diverges.

```python
# Case 1: Type Mismatch (String is not Int)
val.get("name").as_int()
# -> None

val.get("name").expected_int()
# -> raises TypeError: Expected an integer, but got str

# Case 2: Missing Key
val.get("missing").as_str()
# -> None

val.get("missing").expected_str()
# -> raises TypeError: VOID has no string value
```

### Primitive Type Accessors

Introduction of main accessors. Many more accessors exist. For details, refer to:

- [`value_api.py`](src/crystflux/v1/core/value_api.py)
- [`base_value.py`](src/crystflux/v1/core/base_value.py)

#### As Type Accessors

Returns `None` when value is missing or type mismatches

| Method | Return Type |
|---------|----------|
| `as_str()` | `str \| None` |
| `as_int()` | `int \| None` |
| `as_float()` | `float \| None` |
| `as_bool()` | `bool \| None` |

#### Expected Type Accessors

Raises error when expected type value is missing

| Method | Return Type |
|---------|----------|
| `expected_str()` | `str` |
| `expected_int()` | `int` |
| `expected_float()` | `float` |
| `expected_bool()` | `bool` |

---

## Latent / Strict {#latent-strict}

Both modes share the same API (`ValueAPI`), but differ in runtime behavior when schema violations (missing keys, out-of-range indices) occur.

Reference:
- [`value_modes.py`](src/crystflux/v1/core/value_modes.py)

### Value Object Types

CrystFlux wraps JSON data as `Value` objects.
Upon wrapping, data is recursively converted into an immutable structure—the original data is never modified by any subsequent operation.
While the mode specified at creation determines behavior on missing data, all types share a common interface: `get()`, `at()`, `as_str()`, and more.

| Type | Creation Method | Behavior on Missing |
|------|-----------------|---------------------|
| `StrictValue` | `Crystallizer.strict()` | Raises exception |
| `LatentValue` | `Crystallizer.latent()` | Returns `VOID` |
| `DreamValue` | `Crystallizer.dream()` | Returns `VOID` (observable) |

`VOID`, generated when data is missing or an error occurs, also implements the same interface to keep chains alive.
Calling accessors like `as_str()` on `VOID` returns `None`.

> [!NOTE]
> Changing the mode at wrap time does not change the `as_*` / `expected_*` implementations themselves. The differing behavior is solely due to how each mode handles schema violations (missing keys, out-of-range indices).

### Same Code, Different Behavior

Consider executing the same code on data with missing keys in both `Latent` and `Strict` modes.

```python
from crystflux.v1 import Crystallizer
from crystflux.v1.core.value_api import ValueAPI

data = {"id": 1}  # 'name' is missing

def extract_name(val: ValueAPI) -> str | None:
    return val.get("name").as_str()
```

### Latent Mode

`Latent` mode absorbs structural errors, propagates Void state, and ultimately returns `None`.

```python
val = Crystallizer.latent(data)

print(extract_name(val))
# -> None
```

### Strict Mode

`Strict` mode enforces schema immediately and raises exceptions at the point of failure.

```python
val = Crystallizer.strict(data)

print(extract_name(val))
# -> raises KeyError: 'name'
```

### Summary

| Feature | Latent Mode | Strict Mode |
| :--- | :--- | :--- |
| **Missing Key/Index** | Returns `Void` (evaluates to `None`) | Raises `KeyError` / `IndexError` |
| **Chain Execution** | Continues (absorbs failures) | Stops immediately (Fail-fast) |

---

## Dream Mode

Dream mode is a research-focused mode for studying AI behavior during long-term autonomous operation. It observes and records hallucinations (undefined method calls) instead of ignoring them, providing insights 
into AI reasoning processes.

As an experimental implementation, it is disabled by default.

**Features**

- Chains containing hallucinated (non-existent) method calls from AI coders continue processing while their behavior is observed and logged
- Any hallucinated method call irreversibly transitions processing to a `VOID` chain  
- VOID chains produce `None` for final value retrieval

> [!NOTE]
> Hallucinations are observed rather than eliminated—treated as "alternative worldlines" the AI considers more probable than the current state. With suitable feedback mechanisms, this approach could lead to hallucination-driven development methods.

> [!WARNING]
> Regardless of whether this library is used, AI agents with self-referential and self-modifying structures (Ouroboros type) can form recursive self-evolutionary loops where observation influences their own behavior, posing risks of unexpected outcomes.
> Experiments must be conducted in isolated environments with proper containment, audit log preservation, and fail-safe mechanisms, under your own management and responsibility. 
> This library is provided as-is under the MIT License, and assumes no liability for any damages.

### Opt-in Required

To use Dream mode, explicit enabling is required.

```python
from crystflux.v1 import Crystallizer
from crystflux.v1.adapters import enable_dream_mode

with enable_dream_mode():
    val = Crystallizer.dream({"name": "Alice"})
```

### Hallucination Tolerance

Calling non-existent methods returns a DreamValue void instead of raising exceptions.

Reference:
- [`value_modes.py`](src/crystflux/v1/core/value_modes.py)

```python
val.some_nonexistent_method()
# -> DreamValue void (no exception)

# Chain continuation
val.some_method().another_method()
# -> DreamValue void (chain is preserved)
```

### Observed Logging

In Dream mode, both hallucination paths and structural missing can be observed and recorded.

#### Undefined Method Calls (Desire Logger)

Calling non-existent methods records hallucination paths (desire paths).

```python
from crystflux.v1.adapters import StdoutDesireLogger

logger = StdoutDesireLogger()
val = Crystallizer.dream({"name": "Alice"}, desire_logger=logger)

val.some_nonexistent_method()
# Output: [Desire] method=some_nonexistent_method target=mappingproxy repr=mappingproxy({'name': 'Alice'})
```

Method chains are also observed.
```python
val.method_a().method_b()
# Output:
# [Desire] method=method_a target=mappingproxy ...
# [Desire] method=method_b target=void ...
```

#### Structural Missing (Missing Logger)

Access to missing keys or indices can be observed with `MissingLogger`.

```python
from crystflux.v1.adapters import StdoutMissingLogger

logger = StdoutMissingLogger()
val = Crystallizer.dream({"name": "Alice"}, missing_logger=logger)

val.get("profile").get("age")
# Output: [Missing] reason=VoidReason.MISSING_KEY key='profile' target=mappingproxy repr=mappingproxy({'name': 'Alice'})
```

You can create custom loggers by implementing the `DesireLogger` and `MissingLogger` protocols.

> [!IMPORTANT]
> In the current implementation, property chain notation like `val.profile.age` results in `AttributeError`.

### Summary

| Feature | Dream Mode |
| :--- | :--- |
| **Hallucination Tolerance** | Even non-existent method calls return void |
| **Chain Execution** | Continues |
| **Opt-in** | Explicitly enabled with `enable_dream_mode()` |
| **Observation & Recording** | Tracks desire paths with `DesireLogger` |

---

## License

MIT License - See [LICENSE](LICENSE) for details.