triblespace 0.24.0

The Triblespace: A lightweight knowledge base for rust.
Documentation
# Patterns & Recipes

This chapter collects idiomatic solutions to common problems. Each recipe is
self-contained — jump to the one that matches your situation.

## Modeling relationships

### One-to-one

An entity links to exactly one other entity via a `GenId` attribute:

```rust
attributes! {
    "..." as author: GenId;
}

let book = fucid();
let writer = fucid();
change += entity! { &book @ literature::author: &writer };
```

Query both directions: the attribute stores the forward link, and the query
engine can traverse it in reverse by binding the value and querying for the
entity:

```rust
// Forward: who wrote this book?
find!(author: Id, pattern!(&catalog, [{ book_id @ literature::author: ?author }]))

// Reverse: what did this author write?
find!(book: Id, pattern!(&catalog, [{ ?book @ literature::author: author_id }]))
```

### Many-to-many

Use a **repeated attribute** — the same entity can have multiple values for one
attribute. The `entity!` macro supports this with the `*` spread syntax:

```rust
let paper = fucid();
let tag_ml = fucid();
let tag_neuro = fucid();
change += entity! { &paper @ metadata::tag: &tag_ml };
change += entity! { &paper @ metadata::tag: &tag_neuro };
```

Or in a single entity expression:

```rust
let tags = vec![tag_ml, tag_neuro];
change += entity! { &paper @ metadata::tag*: tags.iter() };
```

Query all tags for an entity, or all entities with a tag:

```rust
// All tags on this paper
find!(tag: Id, pattern!(&catalog, [{ paper_id @ metadata::tag: ?tag }]))

// All papers with this tag
find!(paper: Id, pattern!(&catalog, [{ ?paper @ metadata::tag: tag_id }]))
```

### Hierarchies (parent/child)

Model with a `parent` attribute. Children point up to their parent:

```rust
attributes! {
    "..." as parent: GenId;
}
change += entity! { &child @ tree::parent: &parent_node };
```

For recursive traversal (all ancestors, all descendants), use `path!`:

```rust
// All ancestors of this node
find!(ancestor: Id, path!(&catalog, node_id tree::parent+ ancestor))

// All descendants (reverse: who has me as ancestor?)
find!(desc: Id, path!(&catalog, desc tree::parent+ node_id))
```

## Entity classification with tags

Use `metadata::tag` with minted `GenId` tag entities. Give tags human-readable
names via `metadata::name`:

```rust
// Mint a tag once
let kind_paper = genid!("A1B2C3...");  // or use trible genid
change += entity! { &kind_paper @ metadata::name: ws.put("paper".to_owned()) };

// Tag an entity
change += entity! { &my_paper @ metadata::tag: &kind_paper };

// Find all papers
find!(paper: Id, pattern!(&catalog, [{ ?paper @ metadata::tag: kind_paper }]))
```

This is the pattern used by wiki.rs (KIND_VERSION_ID), compass.rs (KIND_GOAL_ID),
and files.rs (KIND_FILE, KIND_DIRECTORY, KIND_IMPORT). Tags are entities, not
strings — they can carry metadata, participate in queries, and be shared across
systems.

## Working with blobs

Values larger than 32 bytes live in blobs. The workspace manages their lifecycle:

```rust
// Write a blob (returns a Handle you can store in an entity)
let text_handle = ws.put("A very long string...".to_owned());
change += entity! { &doc @ article::body: text_handle };

// Read a blob back
let view: View<str> = ws.get(text_handle)?;
println!("{}", view.as_ref());
```

**When to use blobs vs values:**
- If you need to **join or filter** on the data → value (inline, 32 bytes)
- If it's **opaque content** you just store and retrieve → blob (Handle)
- Rule of thumb: names and tags are values, content and payloads are blobs

## Building entities from optional attributes

When some attributes are always present and others are conditional, build the
required part first, then conditionally extend:

```rust
let id = ufoid();
let mut change = TribleSet::new();

// Required attributes — always written together
change += entity! { &id @
    metadata::tag: &KIND_REQUEST,
    request::command: command_handle,
    request::created_at: now,
};

// Optional attributes — only if present
if let Some(cwd) = default_cwd {
    let handle = ws.put(cwd.to_owned());
    change += entity! { &id @ request::cwd: handle };
}
```

When querying, use a multi-attribute pattern for the required fields (the query
engine proves they exist), and separate queries for optional fields:

```rust
// Required: one pattern, no Option<> needed
for (id, command, created_at) in find!(
    (id: Id, cmd: TextHandle, at: Value<NsTAIInterval>),
    pattern!(&catalog, [{
        ?id @
        metadata::tag: &KIND_REQUEST,
        request::command: ?cmd,
        request::created_at: ?at,
    }])
) {
    // Optional: separate query
    let cwd = find!(
        handle: TextHandle,
        pattern!(&catalog, [{ id @ request::cwd: ?handle }])
    ).next();
}
```

## Schema evolution

Adding a new attribute to existing entities is free — just start writing it.
Existing entities that lack the attribute are unaffected; queries that require
it simply won't match them. This is the monotonic property at work.

```rust
// V1: papers have title and author
change += entity! { &paper @ literature::title: "Dune", literature::author: &herbert };

// V2: add page_count to new papers — old papers still work fine
change += entity! { &new_paper @
    literature::title: "Foundation",
    literature::author: &asimov,
    literature::page_count: 255u64.to_value(),
};

// Query: papers with page_count (only new ones match)
find!((paper: Id, pages: Value<U256BE>),
    pattern!(&catalog, [{ ?paper @ literature::page_count: ?pages }]))

// Query: all papers (both old and new match)
find!(paper: Id,
    pattern!(&catalog, [{ ?paper @ literature::title: _?t }]))
```

**Removing** an attribute is not directly supported — triblesets are monotonic.
Instead, add a new attribute that supersedes the old one, and update your queries
to prefer it. The old data remains but stops being used.

## Multi-dataset queries

Queries can span multiple `TribleSet`s and even native Rust collections in a
single `find!` call using `and!`:

```rust
let local_facts = TribleSet::new();
let remote_facts = TribleSet::new();

find!((entity: Id, name: String),
    and!(
        pattern!(&local_facts, [{ ?entity @ schema::tag: &KIND_PERSON }]),
        pattern!(&remote_facts, [{ ?entity @ schema::name: ?name }])
    ))
```

The engine handles the join — it doesn't matter which dataset holds which
attributes.