# Cookbook Implementation Plan (#190)

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Write four wiki cookbook pages covering graph traversal, time-travel audit idioms, bitemporal modeling, and application workflow patterns, plus navigation updates to Home.md and _Sidebar.md.

**Architecture:** Four new `.wiki/Cookbook-*.md` pages using the Problem / Pattern / Notes recipe format. Two existing wiki files updated for navigation. All changes committed to the `.wiki/` git repo (separate from the main repo). No changes to `src/` or `tests/`.

**Tech Stack:** Markdown, Minigraf Datalog dialect, Rust API snippets (prepared statements only, in Page 4). Wiki repo at `.wiki/`.

---

## Syntax reference (read before writing recipes)

- **`:as-of N`** — tx-count snapshot (sequential integer 1, 2, 3…)
- **`:as-of "2025-01-15T10:00:00Z"`** — wall-clock snapshot (ISO 8601 UTC)
- **`:valid-at "2023-06-01"`** — valid-time filter (ISO 8601 date or datetime)
- **`:valid-at :any-valid-time`** — disable valid-time filter; return all historical facts
- **`[?e :db/valid-from ?vf]`** — bind valid-from (Unix ms) of the fact matched by the preceding pattern for entity `?e`
- **`[?e :db/valid-to ?vt]`** — bind valid-to (Unix ms); `9223372036854775807` = open-ended (forever)
- **`[?e :db/tx-count ?tc]`** — bind tx-count of the fact for entity `?e`
- **`[?e :db/tx-id ?ti]`** — bind Unix ms tx-id of the fact for entity `?e`
- **`[(<= ?vf 1704067200000)]`** — expr clause comparing Unix ms integers
- **Pseudo-attributes correlate with the preceding real-attribute pattern** on the same entity — `[:alice :salary ?s] [:alice :db/tx-count ?tx]` binds the tx-count of the salary fact
- **`VALID_TIME_FOREVER = 9223372036854775807`** (i64::MAX) — sentinel for open-ended facts

---

## File map

| Action | Path |
|---|---|
| Create | `.wiki/Cookbook-Graph-Traversal.md` |
| Create | `.wiki/Cookbook-Time-Travel.md` |
| Create | `.wiki/Cookbook-Bitemporal-Modeling.md` |
| Create | `.wiki/Cookbook-Application-Workflows.md` |
| Modify | `.wiki/Home.md` |
| Modify | `.wiki/_Sidebar.md` |

---

## Task 1: Graph Traversal Patterns page

**Files:**
- Create: `.wiki/Cookbook-Graph-Traversal.md`

- [ ] **Step 1: Write the file**

Create `.wiki/Cookbook-Graph-Traversal.md` with this exact content:

```markdown
# Graph Traversal Patterns

Recipes for navigating graph-structured data. Each recipe is self-contained: re-define any
rules it needs even if they appear elsewhere on this page.

← [Home](Home) | [Time-Travel Idioms →](Cookbook-Time-Travel)

---

## Recipe 1 — Direct neighbors (1-hop)

**Problem:** Find all entities directly connected to a given node via a specific edge attribute.

```datalog
;; Org chart: direct reports of :ceo
(transact [[:alice :reports-to :ceo]
           [:bob   :reports-to :ceo]
           [:carol :reports-to :alice]])

(query [:find ?person
        :where [?person :reports-to :ceo]])
;; => :alice, :bob
```

**Notes:**
- Join with `:person/name` to project human-readable names: add `[?person :person/name ?name]` and include `?name` in `:find`
- Works for any edge attribute: `:follows`, `:depends-on`, `:connected`, `:linked-to`
- Reverse direction — who does `:alice` report to: `[:alice :reports-to ?manager]`

---

## Recipe 2 — Transitive closure (all reachable nodes)

**Problem:** Find all nodes reachable from a root by following an edge attribute recursively.

```datalog
;; All reports under :ceo (direct and indirect)
(transact [[:alice :reports-to :ceo]
           [:bob   :reports-to :ceo]
           [:carol :reports-to :alice]
           [:dave  :reports-to :carol]])

(rule [(all-reports ?manager ?report)
       [?report :reports-to ?manager]])
(rule [(all-reports ?manager ?report)
       [?mid :reports-to ?manager]
       (all-reports ?mid ?report)])

(query [:find ?report
        :where (all-reports :ceo ?report)])
;; => :alice, :bob, :carol, :dave
```

**Notes:**
- Minigraf uses semi-naive fixed-point evaluation; terminates correctly on graphs with cycles
- The base case catches direct edges; the recursive case handles any depth
- Rule names are session-global; use descriptive names to avoid collisions across queries in the same session

---

## Recipe 3 — Reachability check

**Problem:** Test whether a path exists between two specific nodes.

```datalog
;; Social graph
(transact [[:alice :follows :bob]
           [:bob   :follows :carol]
           [:carol :follows :dave]])

(rule [(reachable ?a ?b) [?a :follows ?b]])
(rule [(reachable ?a ?b) [?a :follows ?m] (reachable ?m ?b)])

;; Non-empty result = :alice can reach :dave; empty = cannot
(query [:find ?dst
        :where (reachable :alice ?dst)
               [(= ?dst :dave)]])
;; => :dave
```

**Notes:**
- Only presence or absence of results matters; the returned value is the destination itself
- To count all reachable nodes from a root: `(count ?dst)` — returns 0 if unreachable
- Substitute both entity arguments with variables to check reachability across all node pairs

---

## Recipe 4 — Leaf nodes (no outgoing edges)

**Problem:** Find nodes that have no outgoing edges of a given type.

```datalog
;; Org chart: employees with no direct reports
(transact [[:alice :reports-to :ceo]
           [:bob   :reports-to :ceo]
           [:carol :reports-to :alice]])

(query [:find ?person
        :where [?person :reports-to _]
               (not-join [?person] [_ :reports-to ?person])])
;; => :bob, :carol (nobody reports to them)
```

**Notes:**
- `not-join [?person]` declares `?person` as the shared variable; the inner pattern checks for the absence of incoming edges
- For "nodes with no outgoing edges" (sources): flip to `(not-join [?person] [?person :reports-to _])`
- Works equally well for dependency graphs, follower graphs, and any directed edge attribute

---

## Recipe 5 — Common ancestors of two nodes

**Problem:** Find all nodes that are ancestors of both a given pair of nodes.

```datalog
;; Org chart: find shared managers of :carol and :dave
(transact [[:alice :reports-to :vp]
           [:bob   :reports-to :vp]
           [:carol :reports-to :alice]
           [:dave  :reports-to :bob]
           [:vp    :reports-to :ceo]])

(rule [(ancestor ?person ?anc) [?person :reports-to ?anc]])
(rule [(ancestor ?person ?anc) [?person :reports-to ?mid] (ancestor ?mid ?anc)])

(query [:find ?common
        :where (ancestor :carol ?common)
               (ancestor :dave ?common)])
;; => :vp, :ceo (shared ancestors of both)
```

**Notes:**
- Result includes all shared ancestors, not just the nearest common ancestor (LCA)
- To find only the LCA: add `(not-join [?common] [?closer] (ancestor :carol ?closer) (ancestor :dave ?closer) (ancestor ?closer ?common))` — complex but expressible
- The two `ancestor` sub-queries are independent; Minigraf evaluates each and intersects

---

## Recipe 6 — Descendants of multiple roots

**Problem:** Find all nodes reachable from any node in a set of starting points.

```datalog
;; All reports under either :alice or :bob (union of their subtrees)
(transact [[:carol :reports-to :alice]
           [:dave  :reports-to :alice]
           [:eve   :reports-to :bob]])

(rule [(all-reports ?manager ?report)
       [?report :reports-to ?manager]])
(rule [(all-reports ?manager ?report)
       [?mid :reports-to ?manager]
       (all-reports ?mid ?report)])

(query [:find ?report
        :where (or-join [?report]
                 (all-reports :alice ?report)
                 (all-reports :bob ?report))])
;; => :carol, :dave (under :alice) and :eve (under :bob)
```

**Notes:**
- `or-join [?report]` declares `?report` as the variable shared across branches; branch-private intermediates are allowed
- Each branch is a complete sub-query; results are deduplicated across branches
- Equivalent to two separate queries + set union, but resolved in a single round-trip

---

## Recipe 7 — Neighbor count (out-degree)

**Problem:** Count how many outgoing edges each node has.

```datalog
;; Social graph: how many people does each user follow?
(transact [[:alice :follows :bob]
           [:alice :follows :carol]
           [:bob   :follows :carol]])

(query [:find ?person (count ?followed)
        :with ?followed
        :where [?person :follows ?followed]])
;; => [(:alice, 2), (:bob, 1)]
```

**Notes:**
- `:with ?followed` keeps each followed-entity row distinct before counting; without it, duplicate `(?person, ?followed)` pairs would collapse before the aggregate
- For in-degree (how many edges point *to* each node): `[?follower :follows ?person]`, count `?follower`
- Use `count-distinct ?followed` if your model can have duplicate edges between the same pair

---

## Recipe 8 — Type-filtered traversal

**Problem:** Traverse edges of a specific type, stopping at nodes that match a predicate.

```datalog
;; Call graph: find all core functions reachable from :main — skip I/O layer nodes
(transact [[:main    :calls :init]
           [:main    :calls :process]
           [:process :calls :save]
           [:save    :layer :io]
           [:init    :layer :core]
           [:process :layer :core]])

(rule [(core-reachable ?from ?to)
       [?from :calls ?to]
       (not [?to :layer :io])])
(rule [(core-reachable ?from ?to)
       [?from :calls ?mid]
       (not [?mid :layer :io])
       (core-reachable ?mid ?to)])

(query [:find ?fn
        :where (core-reachable :main ?fn)])
;; => :init, :process  (not :save, which is :io)
```

**Notes:**
- The `not` on `?to` excludes io-layer destinations from results; the `not` on `?mid` prevents traversal *through* io-layer nodes
- Replace `(not [?to :layer :io])` with any predicate: `[(= ?depth 0)]`, `(not [?node :status :deprecated])`, etc.
- Combine with `:as-of` to traverse the graph as it existed at a past snapshot

---

## Recipe 9 — Edge reification (property graphs)

**Problem:** Attach properties (weight, label, timestamp) to individual edges.

```datalog
;; Model weighted, labelled edges as first-class entities
(transact [[:edge-ab :edge/from   :alice]
           [:edge-ab :edge/to     :bob]
           [:edge-ab :edge/label  :knows]
           [:edge-ab :edge/weight 5]
           [:edge-ab :edge/since  "2020-01-01"]
           [:edge-ac :edge/from   :alice]
           [:edge-ac :edge/to     :carol]
           [:edge-ac :edge/label  :knows]
           [:edge-ac :edge/weight 2]])

;; Find Alice's connections with weight ≥ 3
(query [:find ?neighbor ?weight
        :where [?edge :edge/from   :alice]
               [?edge :edge/to     ?neighbor]
               [?edge :edge/weight ?weight]
               [(>= ?weight 3)]])
;; => [:bob 5]

;; Traverse reified edges recursively
(rule [(prop-reachable ?from ?to)
       [?e :edge/from ?from]
       [?e :edge/to   ?to]
       [?e :edge/label :knows]])
(rule [(prop-reachable ?from ?to)
       [?e  :edge/from  ?from]
       [?e  :edge/to    ?mid]
       [?e  :edge/label :knows]
       (prop-reachable ?mid ?to)])

(query [:find ?node
        :where (prop-reachable :alice ?node)])
;; => :bob, :carol (and deeper if they have :knows edges)
```

**Notes:**
- The edge entity (`:edge-ab`) is a stable identifier; its properties can be updated over time with full bitemporal history — e.g., "what was the weight of this connection in 2023?"
- Reification trades simplicity (direct `:follows` triples) for expressiveness (queryable edge metadata)
- Each edge entity can carry an arbitrary number of attributes; they are all queryable and indexable

---

← [Home](Home) | [Time-Travel Idioms →](Cookbook-Time-Travel)

Reference: [Recursive rules](Datalog-Reference#recursive-rules), [Negation](Datalog-Reference#negation), [Aggregation](Datalog-Reference#aggregation)
```

- [ ] **Step 2: Verify**

Check that the file has exactly 9 `## Recipe` sections. Run:
```bash
grep -c "^## Recipe" .wiki/Cookbook-Graph-Traversal.md
```
Expected output: `9`

- [ ] **Step 3: Commit**

```bash
cd .wiki
git add Cookbook-Graph-Traversal.md
git commit -m "docs(cookbook): add graph traversal patterns page (#190)"
cd ..
```

---

## Task 2: Time-Travel Idioms page

**Files:**
- Create: `.wiki/Cookbook-Time-Travel.md`

- [ ] **Step 1: Write the file**

Create `.wiki/Cookbook-Time-Travel.md` with this exact content:

```markdown
# Audit and Time-Travel Idioms

Recipes for querying Minigraf's bi-temporal history. Organised by the four temporal query
classes from the [RecallGraph taxonomy](https://recallgraph.hashnode.dev/temporal-query-types).

← [Graph Traversal](Cookbook-Graph-Traversal) | [Bitemporal Modeling →](Cookbook-Bitemporal-Modeling)

---

## Point-in-Time queries

A point-in-time query returns the state of the database at a single frozen moment. Both time
axes (transaction time and valid time) support point-in-time queries independently or combined.

---

### Recipe 1 — Transaction-time snapshot by tx count

**Problem:** Query the database exactly as it existed after transaction N.

```datalog
;; Config table updated three times
(transact [[:cfg :app/max-conn 10]])   ;; tx 1
(transact [[:cfg :app/max-conn 50]])   ;; tx 2
(transact [[:cfg :app/max-conn 100]])  ;; tx 3

;; What was max-conn after tx 1?
(query [:find ?v
        :as-of 1
        :valid-at :any-valid-time
        :where [:cfg :app/max-conn ?v]])
;; => 10
```

**Notes:**
- `:as-of N` uses the sequential tx counter (1, 2, 3…), not the Unix ms tx-id shown by the REPL's "Transacted (tx: …)" output
- Each `(transact [...])` increments the counter once regardless of how many facts it contains
- `:valid-at :any-valid-time` suppresses the default valid-time filter; omit it only if your facts carry explicit valid-time ranges that you want applied

---

### Recipe 2 — Transaction-time snapshot by wall-clock time

**Problem:** Query the database as it existed at a specific wall-clock timestamp.

```datalog
(query [:find ?v
        :as-of "2025-06-01T12:00:00Z"
        :valid-at :any-valid-time
        :where [:cfg :app/max-conn ?v]])
```

**Notes:**
- Matches the latest transaction whose `tx_id` (Unix ms) is ≤ the given timestamp
- Timestamps must be ISO 8601 UTC; Minigraf rejects local-timezone or offset timestamps
- If no transaction existed before the given timestamp, the query returns empty

---

### Recipe 3 — Valid-time snapshot (standalone `:valid-at`)

**Problem:** Query what was true in the real world on a specific date, regardless of when it was recorded.

```datalog
;; Employment history with explicit valid-time ranges
(transact {:valid-from "2020-01-01" :valid-to "2023-06-01"}
          [[:alice :works-at :techcorp]])
(transact {:valid-from "2023-06-01"}
          [[:alice :works-at :startupco]])

;; Where did Alice work on 2021-07-01?
(query [:find ?company
        :valid-at "2021-07-01"
        :where [:alice :works-at ?company]])
;; => :techcorp

;; Where does Alice work today?
(query [:find ?company
        :where [:alice :works-at ?company]])
;; => :startupco  (no :valid-at = currently valid facts only)
```

**Notes:**
- `:valid-at "date"` filters to facts where `valid-from ≤ date < valid-to`
- Omitting `:valid-at` returns only facts currently valid (`valid-to = forever` and `valid-from ≤ now`)
- `:valid-at :any-valid-time` returns all facts regardless of valid-time — use this when you need the full history

---

### Recipe 4 — Bi-temporal snapshot

**Problem:** Ask "what did the database *know* at transaction N about what was *true* on date D?"

```datalog
;; As-of tx 1 (only TechCorp recorded): where was Alice in 2024?
(query [:find ?company
        :as-of 1
        :valid-at "2024-01-01"
        :where [:alice :works-at ?company]])
;; => empty — TechCorp expired before 2024; StartupCo not yet in DB at tx 1

;; As-of tx 2 (both recorded): where was Alice in 2024?
(query [:find ?company
        :as-of 2
        :valid-at "2024-01-01"
        :where [:alice :works-at ?company]])
;; => :startupco
```

**Notes:**
- `:as-of` controls what was *recorded*; `:valid-at` controls what was *true in the real world*
- Use this for auditing decisions: "what information did we have available when we made choice X?"
- Both axes are fully independent; combining them narrows the result to the intersection

---

## Time Interval queries

A time interval query gathers data across a range of the timeline rather than at a single point.

---

### Recipe 5 — Full history of an attribute

**Problem:** List every value ever recorded for an entity/attribute pair, including retracted values.

```datalog
;; All salary values ever recorded for :alice (including retracted ones)
(transact [[:alice :salary 75000]])  ;; tx 1
(retract  [[:alice :salary 75000]])
(transact [[:alice :salary 80000]])  ;; tx 3

(query [:find ?salary ?tx
        :valid-at :any-valid-time
        :where [:alice :salary ?salary]
               [:alice :db/tx-count ?tx]])
;; => [(75000, 1), (80000, 3)]
```

**Notes:**
- `:valid-at :any-valid-time` with no `:as-of` returns all facts ever asserted, including retracted ones
- `[:alice :db/tx-count ?tx]` binds the tx-count of each salary fact — the pseudo-attribute correlates with the preceding real-attribute pattern on the same entity
- Sort by `?tx` to read changes in chronological order

---

### Recipe 6 — Changes between two tx counts

**Problem:** Find all values asserted for an attribute within a specific transaction range.

```datalog
;; Price changes between tx 3 and tx 7
(query [:find ?price ?tx
        :valid-at :any-valid-time
        :where [:product-x :product/price ?price]
               [:product-x :db/tx-count   ?tx]
               [(>= ?tx 3)]
               [(<= ?tx 7)]])
```

**Notes:**
- `?tx` is an integer (sequential counter); compare with `>=` and `<=` expression predicates
- To find changes across *all* entities in a tx range, replace the entity with `?e`: `[?e :product/price ?price] [?e :db/tx-count ?tx]`
- Combine with `:db/tx-id ?ti` to get the wall-clock time of each change: `[(/ ?ti 1000) ?unix-sec]`

---

### Recipe 7 — Facts valid during a date range (VT overlap)

**Problem:** Find all facts that were valid at any point during a given date range.

```datalog
;; Insurance policies valid at any point during 2023
;; Overlap condition: fact started before range-end AND fact ended after range-start

;; 2023-01-01 = 1672531200000 ms  |  2024-01-01 = 1704067200000 ms
(query [:find ?policy ?name
        :valid-at :any-valid-time
        :where [?policy :policy/name  ?name]
               [?policy :db/valid-from ?vf]
               [?policy :db/valid-to   ?vt]
               [(< ?vf 1704067200000)]   ;; started before end of range
               [(>= ?vt 1672531200000)]])  ;; ended on or after start of range
;; => all policies with any overlap with 2023
```

**Notes:**
- Valid-time values are stored as Unix milliseconds; pre-compute range boundaries before querying
- Open-ended facts have `valid-to = 9223372036854775807` (i64::MAX) and always satisfy `>= range_start`
- The overlap condition is: `fact_start < range_end AND fact_end >= range_start`

---

## Time-Point Lookup queries

A time-point lookup finds the point(s) on the timeline where given criteria were satisfied —
the inverse of point-in-time.

---

### Recipe 8 — When was X first asserted?

**Problem:** Find the transaction at which a specific fact was first recorded.

```datalog
;; When was :alice's name first recorded?
(query [:find (min ?tx)
        :valid-at :any-valid-time
        :where [:alice :person/name _]
               [:alice :db/tx-count ?tx]])
;; => the earliest tx count for any name fact for :alice
```

**Notes:**
- `(min ?tx)` returns the earliest; `(max ?tx)` returns the most recent assertion
- To get the wall-clock time: replace `:db/tx-count` with `:db/tx-id` — the value is Unix milliseconds
- To find the first assertion across all entities for an attribute, use `?e` instead of `:alice` and add `?e` to `:find`

---

### Recipe 9 — When did X become valid?

**Problem:** Find the valid-time at which a specific fact started being true in the real world.

```datalog
;; When did Alice's employment at TechCorp begin (valid-time)?
(query [:find ?vf
        :valid-at :any-valid-time
        :where [:alice :works-at :techcorp]
               [:alice :db/valid-from ?vf]])
;; => Unix ms timestamp corresponding to 2020-01-01
```

**Notes:**
- `[:alice :db/valid-from ?vf]` binds the `valid-from` of the fact matched by the preceding pattern
- If the fact was asserted without an explicit `valid-from`, the value equals `tx_id` (the wall-clock time of the transaction)
- Divide the result by 1000 to get Unix seconds; divide by 86400000 for days since epoch

---

### Recipe 10 — When did X expire?

**Problem:** Find the valid-time at which a specific fact stopped being true.

```datalog
;; When did Alice's TechCorp employment end?
(query [:find ?vt
        :valid-at :any-valid-time
        :where [:alice :works-at :techcorp]
               [:alice :db/valid-to ?vt]
               [(< ?vt 9223372036854775807)]])  ;; exclude open-ended facts
;; => Unix ms timestamp corresponding to 2023-06-01
```

**Notes:**
- `9223372036854775807` is `i64::MAX` — the sentinel for "valid forever"; filter it out to find only expired facts
- Retracted facts may carry `valid-to = i64::MAX` at the moment of retraction; use `:as-of` to distinguish retracted-then-replaced from genuinely expired
- To find all facts that expired before a given date: add `[(<= ?vt <date-ms>)]`

---

## Time-Interval Lookup queries

A time-interval lookup finds the interval(s) during which given criteria held — the inverse of time-interval.

---

### Recipe 11 — Duration of a valid period

**Problem:** Compute how long a specific fact was continuously valid.

```datalog
;; Duration of Alice's TechCorp employment in milliseconds
(query [:find ?duration
        :valid-at :any-valid-time
        :where [:alice :works-at :techcorp]
               [:alice :db/valid-from ?vf]
               [:alice :db/valid-to   ?vt]
               [(< ?vt 9223372036854775807)]  ;; exclude open-ended
               [(- ?vt ?vf) ?duration]])
;; => duration in ms; divide by 86400000 for days
```

**Notes:**
- `[(- ?vt ?vf) ?duration]` is an arithmetic binding — it computes and binds the result
- Filter open-ended facts (`valid-to = i64::MAX`) first; subtracting i64::MAX overflows
- For open-ended facts, substitute the current Unix ms timestamp as the upper bound before computing duration

---

### Recipe 12 — All valid periods for an attribute

**Problem:** List every time interval during which a given entity/attribute pair held a value.

```datalog
;; All employment periods for :alice (all employers, all intervals)
(query [:find ?company ?vf ?vt
        :valid-at :any-valid-time
        :where [:alice :works-at ?company]
               [:alice :db/valid-from ?vf]
               [:alice :db/valid-to   ?vt]])
;; => [(:techcorp, <vf-ms>, <vt-ms>), (:startupco, <vf-ms>, 9223372036854775807)]
```

**Notes:**
- Results include all historical intervals; open-ended ones show `valid-to = 9223372036854775807`
- Order by `?vf` to read the history chronologically
- Add `[(< ?vt 9223372036854775807)]` to show only closed (expired) periods; remove it to include the current open interval

---

## Supporting patterns

---

### Recipe 13 — Audit trail with actor

**Problem:** Record which actor made each change, and later find all changes made by a specific actor.

```datalog
;; Transact actor metadata alongside the data change — same tx-count ties them together
(transact [[:tx-meta-1 :tx/actor  "alice@example.com"]
           [:tx-meta-1 :tx/reason "quarterly-review"]
           [:alice      :salary    80000]])

;; Find all salary changes made by alice@example.com
(query [:find ?entity ?salary ?tx
        :valid-at :any-valid-time
        :where [?meta :tx/actor "alice@example.com"]
               [?meta :db/tx-count ?tx]
               [?entity :salary ?salary]
               [?entity :db/tx-count ?tx]])
;; => [(:alice, 80000, N)] — all salary facts recorded in the same tx as the actor fact
```

**Notes:**
- Correlate the actor entity's `:db/tx-count` with data entities' `:db/tx-count` to find all facts written in the same transaction
- Use a naming convention for tx-meta entities (e.g., `:tx-meta-<uuid>`) to avoid collisions
- Actor facts are themselves bitemporal — query them with `:as-of` to reconstruct who was responsible for a change at a past tx

---

← [Graph Traversal](Cookbook-Graph-Traversal) | [Bitemporal Modeling →](Cookbook-Bitemporal-Modeling)

Reference: [Bi-temporal queries](Datalog-Reference#bi-temporal-queries), [Pseudo-attributes](Datalog-Reference#pseudo-attributes)
```

- [ ] **Step 2: Verify**

```bash
grep -c "^### Recipe" .wiki/Cookbook-Time-Travel.md
```
Expected output: `13`

- [ ] **Step 3: Commit**

```bash
cd .wiki
git add Cookbook-Time-Travel.md
git commit -m "docs(cookbook): add time-travel audit idioms page (#190)"
cd ..
```

---

## Task 3: Bitemporal Modeling page

**Files:**
- Create: `.wiki/Cookbook-Bitemporal-Modeling.md`

- [ ] **Step 1: Write the file**

Create `.wiki/Cookbook-Bitemporal-Modeling.md` with this exact content:

```markdown
# Bitemporal Modeling

Recipes for structuring data to take full advantage of Minigraf's bi-temporal model.
These are the "write side" — how to assert facts — complementary to the query patterns
in [Audit and Time-Travel Idioms](Cookbook-Time-Travel).

← [Time-Travel Idioms](Cookbook-Time-Travel) | [Application Workflows →](Cookbook-Application-Workflows)

---

## Recipe 1 — Point-in-time facts (default valid-time)

**Problem:** Assert a fact that becomes true "now" with no modeled expiry.

```datalog
;; Email address recorded at the moment of the transaction
(transact [[:alice :person/email "alice@example.com"]])

;; valid-from = tx_id (wall-clock time of the transaction, Unix ms)
;; valid-to   = 9223372036854775807 (i64::MAX — open-ended)
```

**Notes:**
- No `{:valid-from ...}` map needed; Minigraf sets `valid-from` to the transaction's Unix ms timestamp automatically
- The default query filter (no `:valid-at`) returns these facts as long as they have not been retracted
- Use this for simple current-state storage: contact info, settings, labels — anything that becomes true "now"

---

## Recipe 2 — Bounded facts (known start and end)

**Problem:** Assert a fact that is only true for a specific, fully known time period.

```datalog
;; Annual contract active for all of 2024
(transact {:valid-from "2024-01-01" :valid-to "2024-12-31"}
          [[:contract-99 :contract/status :active]
           [:contract-99 :contract/holder :alice]])

;; Per-fact 5-element form for mixed periods in one transact
(transact [[:policy-a :policy/status :active "2024-01-01" "2024-06-30"]
           [:policy-b :policy/status :active "2024-03-01" "2024-12-31"]])
;; [entity attribute value valid-from valid-to]
```

**Notes:**
- Transaction-level `{:valid-from ... :valid-to ...}` applies to all facts in the batch
- The 5-element per-fact form overrides the transaction-level range for that individual fact
- Bounded facts model contracts, subscriptions, approvals, and any period with a known start and end

---

## Recipe 3 — Open-ended current facts (known start, no end)

**Problem:** Assert a fact that started on a known date and is still true today.

```datalog
;; Employment beginning on a specific date, no known end date
(transact {:valid-from "2023-06-01"}
          [[:alice :works-at :startupco]])
;; valid-from = 2023-06-01; valid-to = i64::MAX (open-ended)

;; Query: who currently works at :startupco?
(query [:find ?person
        :where [?person :works-at :startupco]])
;; Returns :alice — the default filter sees open-ended facts as currently valid ✓
```

**Notes:**
- Omitting `:valid-to` leaves the fact open-ended (valid forever from `valid-from`)
- The default query filter (no `:valid-at`) returns facts where `valid-from ≤ now`; an open-ended fact set in the past is visible
- To close this fact when the situation changes, use Recipe 7

---

## Recipe 4 — Retroactive correction

**Problem:** Correct a fact that was recorded with a wrong value, while preserving the original erroneous record in history.

```datalog
;; Wrong salary was recorded
(transact {:valid-from "2024-01-01"}
          [[:alice :salary 75000]])  ;; tx 1 — incorrect

;; Correction: Alice's actual salary was 80000 from the same date
(retract [[:alice :salary 75000]])   ;; tx 2 — marks old value as retracted
(transact {:valid-from "2024-01-01"}
          [[:alice :salary 80000]])  ;; tx 3 — correct value, same valid-from

;; Current state: corrected salary
(query [:find ?s :where [:alice :salary ?s]])
;; => 80000

;; History preserved: original wrong value visible at tx 1
(query [:find ?s
        :as-of 1
        :valid-at :any-valid-time
        :where [:alice :salary ?s]])
;; => 75000
```

**Notes:**
- `retract` marks the fact as no longer asserted; it does **not** delete it from history
- The re-asserted fact carries the **same `valid-from`** as the original — this is what makes it retroactive
- The full audit trail (wrong value at tx 1, correction at tx 3) is always preserved and queryable

---

## Recipe 5 — Future-dated assertion

**Problem:** Record a change that takes effect at a future date.

```datalog
;; Promotion effective 2026-01-01, recorded today
(transact {:valid-from "2026-01-01"}
          [[:alice :title :senior-engineer]])

;; Query today (before 2026-01-01): title is not yet visible
(query [:find ?title :where [:alice :title ?title]])
;; => empty

;; Query at or after 2026-01-01
(query [:find ?title
        :valid-at "2026-01-01"
        :where [:alice :title ?title]])
;; => :senior-engineer
```

**Notes:**
- The fact is stored immediately but invisible to queries without an explicit `:valid-at` (since `valid-from > now`)
- Useful for scheduling promotions, pre-approvals, contract renewals, and "effective date" workflows
- To list all future-dated facts: query with `:valid-at :any-valid-time` and filter `[(> ?vf <now-ms>)]` using `:db/valid-from`

---

## Recipe 6 — Modeling overlapping valid periods

**Problem:** Represent a situation where an entity holds two concurrent values for the same attribute.

```datalog
;; Alice holds two roles simultaneously during 2024
(transact {:valid-from "2024-01-01" :valid-to "2024-06-30"}
          [[:alice :role :project-lead]])
(transact {:valid-from "2024-03-01"}
          [[:alice :role :technical-advisor]])

;; Query on 2024-04-15: both roles are active
(query [:find ?role
        :valid-at "2024-04-15"
        :where [:alice :role ?role]])
;; => :project-lead, :technical-advisor
```

**Notes:**
- Minigraf places no uniqueness constraint on attribute values across valid-time ranges — overlapping periods are fully supported
- To enforce "at most one active value at a time", validate at the application layer before transacting
- Use `:valid-at :any-valid-time` to see all periods including non-overlapping historical ones

---

## Recipe 7 — Closing an open-ended fact

**Problem:** Record the real-world end of a situation that was modeled as open-ended.

```datalog
;; Alice left StartupCo on 2025-12-31
;; Step 1: retract the open-ended fact
(retract [[:alice :works-at :startupco]])

;; Step 2: re-assert with an explicit valid-to
(transact {:valid-from "2023-06-01" :valid-to "2025-12-31"}
          [[:alice :works-at :startupco]])
```

**Notes:**
- Minigraf facts are immutable; you cannot add `valid-to` to an existing open-ended fact in place
- Retract + re-assert is the correct pattern; the original open-ended fact is preserved in history (visible via `:as-of <tx before retraction>`)
- The re-asserted fact carries the **original `valid-from`** so the full employment period (2023-06-01 to 2025-12-31) is correctly modeled
- Follow immediately with a new open-ended fact if Alice starts a new role: `(transact {:valid-from "2026-01-15"} [[:alice :works-at :nextcorp]])`

---

## Recipe 8 — Correction vs. lifecycle end

**Problem:** Know when to use error correction (Recipe 4) vs. valid-time bounding (Recipe 7).

```datalog
;; ERROR CORRECTION — the original value was WRONG
;; Preserve the original valid-from in the replacement
(retract [[:alice :salary 75000]])
(transact {:valid-from "2024-01-01"}  ;; same start date as the wrong fact
          [[:alice :salary 80000]])

;; LIFECYCLE END — the original value was CORRECT but the situation changed
;; Close the fact at the real-world end date; add a new fact for the next period
(retract [[:alice :works-at :startupco]])
(transact {:valid-from "2023-06-01" :valid-to "2025-12-31"}
          [[:alice :works-at :startupco]])
(transact {:valid-from "2026-01-15"}
          [[:alice :works-at :nextcorp]])
```

**Notes:**
- The distinction is semantic, not mechanical — both use retract + re-assert
- **Error correction:** preserve the original `valid-from` in the replacement; the wrong value was never correct in the real world
- **Lifecycle end:** close the fact at the real-world end date; assert a new fact for the new period
- Both preserve the full history of what was recorded and when; `:as-of` lets you see the database state before either change

---

← [Time-Travel Idioms](Cookbook-Time-Travel) | [Application Workflows →](Cookbook-Application-Workflows)

Reference: [Bi-temporal queries](Datalog-Reference#bi-temporal-queries), [Transact](Datalog-Reference#transact), [Retract](Datalog-Reference#retract)
```

- [ ] **Step 2: Verify**

```bash
grep -c "^## Recipe" .wiki/Cookbook-Bitemporal-Modeling.md
```
Expected output: `8`

- [ ] **Step 3: Commit**

```bash
cd .wiki
git add Cookbook-Bitemporal-Modeling.md
git commit -m "docs(cookbook): add bitemporal modeling page (#190)"
cd ..
```

---

## Task 4: Application Workflows page

**Files:**
- Create: `.wiki/Cookbook-Application-Workflows.md`

- [ ] **Step 1: Write the file**

Create `.wiki/Cookbook-Application-Workflows.md` with this exact content:

```markdown
# Application Workflow Patterns

End-to-end recipes for common Minigraf deployment scenarios. Rust API snippets appear
where the native API is required; all other recipes use the Datalog REPL syntax.

← [Bitemporal Modeling](Cookbook-Bitemporal-Modeling) | [Home →](Home)

---

## Agent memory

---

### Recipe 1 — Store, update, and retract beliefs

**Problem:** Implement an agent belief lifecycle — assert, correct, and retract beliefs while preserving the full decision audit trail.

```datalog
;; Agent records a belief
(transact [[:agent-session-1 :belief/sky-color "blue"]])  ;; tx 1

;; Belief is corrected
(retract [[:agent-session-1 :belief/sky-color "blue"]])
(transact [[:agent-session-1 :belief/sky-color "red"]])   ;; tx 3

;; Current belief
(query [:find ?color
        :where [:agent-session-1 :belief/sky-color ?color]])
;; => "red"

;; What did the agent believe before the correction?
(query [:find ?color
        :as-of 1
        :valid-at :any-valid-time
        :where [:agent-session-1 :belief/sky-color ?color]])
;; => "blue"
```

**Notes:**
- Assert multiple beliefs in a single transaction: `[[:agent :belief/A "x"] [:agent :belief/B "y"]]` — all receive the same tx-count
- Store the tx-count alongside decisions: `(transact [[:decision-42 :decision/belief-tx 1]])` — use it later as the `:as-of` handle for audits
- One `.graph` file per agent instance; each agent's memory is private (see Recipe 9)

---

### Recipe 2 — Audit a past decision

**Problem:** Reconstruct exactly what an agent knew at the moment it made a specific decision.

```datalog
;; Store the tx-count when a decision was made
(transact [[:decision-42 :decision/made-at-tx 7]
           [:decision-42 :decision/choice     :route-A]])

;; Retrieve the tx-count for decision-42
(query [:find ?tx
        :where [:decision-42 :decision/made-at-tx ?tx]])
;; => 7

;; Reconstruct the agent's belief state at tx 7
(query [:find ?topic ?belief
        :as-of 7
        :valid-at :any-valid-time
        :where [:agent :belief/?topic ?belief]])
```

**Notes:**
- `:as-of 7` freezes the database to what was known after tx 7 — this is the agent's exact knowledge state at decision time
- Combine with `:valid-at` to also restrict which real-world facts were considered valid at that moment
- The tx-count stored in `:decision/made-at-tx` is the durable link between a decision record and its knowledge context

---

### Recipe 3 — High-frequency belief queries with prepared statements

**Problem:** Run the same query shape thousands of times per session with different entity IDs and tx snapshots without re-parsing each time.

```rust
use minigraf::{Minigraf, OpenOptions, BindValue};

let db = OpenOptions::new().open("agent.graph")?;

// Prepare once at session startup — parse and plan happen here
let pq = db.prepare(
    "(query [:find ?belief
             :as-of $tx
             :where [$entity :belief/value ?belief]])"
)?;

// Execute thousands of times — query plan is reused, only bind values differ
for (entity_id, tx) in agent_queries {
    let results = pq.execute(&[
        ("tx",     BindValue::TxCount(tx)),
        ("entity", BindValue::Entity(entity_id)),
    ])?;
    // process results...
}
```

**Notes:**
- `$tx` and `$entity` are named bind slots; `prepare()` parses and plans once; `execute()` substitutes values and runs
- `BindValue::TxCount(u64)` for `:as-of`-style tx bindings; `BindValue::Entity(uuid::Uuid)` for entity IDs; `BindValue::String(String)` for string values
- `PreparedQuery` is `Send + Sync`; safe to share across threads — each `execute()` call is independent

---

### Recipe 4 — GraphRAG pattern (architecture)

**Problem:** Combine a vector store for fuzzy retrieval with Minigraf for structured graph and bitemporal navigation.

The two layers serve complementary roles:

| Layer | Tool | Job |
|---|---|---|
| Fuzzy retrieval | Vector store (Chroma, Qdrant, Pinecone…) | Find entities similar to the query embedding |
| Graph + temporal backbone | Minigraf | Follow relationships, audit facts, rewind to past states |

**Pattern:**
1. Store the Minigraf entity UUID alongside its embedding in the vector store
2. On a query, retrieve top-k matching entity UUIDs from the vector store
3. Use those UUIDs as entry points into Minigraf for graph traversal and temporal queries

```datalog
;; Entry-point UUID retrieved from vector store
;; Follow relationships and fetch bitemporal metadata from Minigraf
(query [:find ?title ?approver ?approved-at
        :where [#uuid "550e8400-e29b-41d4-a716-446655440000" :doc/title      ?title]
               [#uuid "550e8400-e29b-41d4-a716-446655440000" :doc/approved-by ?approver-id]
               [?approver-id :person/name ?approver]
               [#uuid "550e8400-e29b-41d4-a716-446655440000" :doc/approved-at ?approved-at]])
```

**Notes:**
- Minigraf has no vector search — the vector store does fuzzy retrieval; Minigraf handles graph traversal and temporal queries
- Entity UUIDs are stable across time; they are the durable link between the two systems
- The vector store answers "which entities are relevant?"; Minigraf answers "what are their relationships and history?"

---

## Offline-first

---

### Recipe 5 — Record facts offline, correct on sync

**Problem:** Record a fact on a device offline, then retroactively correct it after the user reviews the data on sync.

```datalog
;; Device records a health measurement on 2025-06-01 (recorded offline)
(transact {:valid-from "2025-06-01"}
          [[:user :health/weight-kg 82.5]])  ;; tx 1

;; After sync, user notices a mis-entry and corrects it
(retract [[:user :health/weight-kg 82.5]])
(transact {:valid-from "2025-06-01"}
          [[:user :health/weight-kg 80.5]])  ;; tx 3 — corrected, same valid-from

;; Current value: corrected
(query [:find ?w :where [:user :health/weight-kg ?w]])
;; => 80.5

;; What was originally recorded (before correction)?
(query [:find ?w
        :as-of 1
        :valid-at :any-valid-time
        :where [:user :health/weight-kg ?w]])
;; => 82.5
```

**Notes:**
- The original measurement is preserved in tx history — the correction is auditable
- `valid-from "2025-06-01"` reflects when the measurement was *taken*, not when the device synced — this is valid-time modeling
- This pattern works identically whether the device was online or offline when recording; Minigraf does not distinguish

---

### Recipe 6 — Detect what changed since last sync

**Problem:** Find all facts added after a known sync checkpoint so they can be pushed to a central store.

```datalog
;; Store the tx-count of the last successful sync
(transact [[:app/state :sync/last-tx-count 12]])

;; Retrieve it before each sync
(query [:find ?last-tx
        :where [:app/state :sync/last-tx-count ?last-tx]])
;; => 12

;; Find all weight facts recorded since tx 12
(query [:find ?value ?tx
        :valid-at :any-valid-time
        :where [:user :health/weight-kg ?value]
               [:user :db/tx-count      ?tx]
               [(> ?tx 12)]])
```

**Notes:**
- Store the last-sync tx-count as a fact; update it after each successful sync with `retract` + `transact`
- Replace the hard-coded `12` with the retrieved `?last-tx` value (or use a prepared query with a bind slot — see Recipe 3)
- To detect changes across multiple attributes, enumerate them with `or`: `(or [?e :health/weight-kg ?v] [?e :health/steps ?v])`

---

## Task and dependency graphs

---

### Recipe 7 — Model a task DAG

**Problem:** Model a project task graph and find all tasks that are transitively blocked by a given task.

```datalog
;; Task graph: tasks and their blocking relationships
(transact [[:task-a :task/name "Design API"]
           [:task-b :task/name "Implement endpoint"]
           [:task-c :task/name "Write tests"]
           [:task-d :task/name "Deploy"]
           [:task-a :task/blocks :task-b]
           [:task-b :task/blocks :task-c]
           [:task-b :task/blocks :task-d]])

;; Recursive rule: all tasks transitively blocked by a given task
(rule [(blocked-by ?blocker ?blocked)
       [?blocker :task/blocks ?blocked]])
(rule [(blocked-by ?blocker ?blocked)
       [?blocker :task/blocks ?mid]
       (blocked-by ?mid ?blocked)])

;; What does completing :task-a unblock (transitively)?
(query [:find ?name
        :where (blocked-by :task-a ?t)
               [?t :task/name ?name]])
;; => "Implement endpoint", "Write tests", "Deploy"
```

**Notes:**
- Model blocking as `:task/blocks` (A blocks B = B cannot start until A completes)
- The recursive rule finds all transitive dependents; Minigraf detects fixed points and terminates safely on cycles
- Add a `:task/status` attribute and filter with `(not [?t :task/status :complete])` to find only currently-blocked tasks

---

### Recipe 8 — Query task DAG at a historical tx snapshot

**Problem:** Reconstruct which tasks were unblocked at a specific point in the project's history.

```datalog
;; At tx 5, :task-a was marked complete
;; What tasks became unblocked at that point that are not yet done?
(query [:find ?name
        :as-of 5
        :where [:task-a :task/status :complete]
               (blocked-by :task-a ?t)
               (not [?t :task/status :complete])
               [?t :task/name ?name]])
```

**Notes:**
- `:as-of 5` ensures the query sees the database state after tx 5, including the `:complete` status update
- The `blocked-by` rule is evaluated within the `:as-of 5` snapshot — only edges that existed at tx 5 are traversed
- Use this for retrospective sprint planning: "why was :task-d still blocked at the end of sprint 3?"

---

## Multi-tenant and fleet patterns

---

### Recipe 9 — One `.graph` file per agent or user (architecture)

**Problem:** Scale an agent fleet or multi-user application without a shared database.

**Pattern:** Each agent instance or user session maintains its own `.graph` file. There is no shared database.

```
agent-instance-1/  →  session-abc.graph
agent-instance-2/  →  session-def.graph
agent-instance-3/  →  session-ghi.graph
```

**When this is the right model:**
- Each agent handles one session, task, or conversation
- Memory is private and does not need to be shared across instances
- The `.graph` file travels with the agent (container volume, device storage, cloud object store)

```rust
// Each session gets its own file — no coordination needed
let db = OpenOptions::new().open(&format!("sessions/{session_id}.graph"))?;
```

**When to use a different approach:**
- Multiple agents need shared state → use a distributed database for that layer; Minigraf remains the per-agent L1 reasoning cache
- See the [Worker-local reasoning (L1 cache) pattern](Use-Cases#ai-agents) in Use-Cases for the fleet architecture

**Notes:**
- Each `.graph` file is completely self-contained; no server, no coordination, no network calls
- `OpenOptions::new().open(path)` creates the file if it doesn't exist, opens it otherwise
- `MiniGrafDb` is `Arc<Mutex<…>>` internally — the handle is `Send + Sync` but concurrent calls are serialized

---

← [Bitemporal Modeling](Cookbook-Bitemporal-Modeling) | [Home →](Home)

Reference: [Prepared queries](Datalog-Reference#prepared-queries), [Bi-temporal queries](Datalog-Reference#bi-temporal-queries), [Use Cases](Use-Cases)
```

- [ ] **Step 2: Verify**

```bash
grep -c "^### Recipe\|^## Recipe" .wiki/Cookbook-Application-Workflows.md
```
Expected output: `9`

- [ ] **Step 3: Commit**

```bash
cd .wiki
git add Cookbook-Application-Workflows.md
git commit -m "docs(cookbook): add application workflow patterns page (#190)"
cd ..
```

---

## Task 5: Navigation updates (Home.md and _Sidebar.md)

**Files:**
- Modify: `.wiki/Home.md`
- Modify: `.wiki/_Sidebar.md`

- [ ] **Step 1: Read both files to understand exact current content**

```bash
cat .wiki/Home.md
cat .wiki/_Sidebar.md
```

- [ ] **Step 2: Add Cookbook section to Home.md**

Insert the following block **after** the closing `- [11. Marketplace…](Tutorial-11-Marketplace)` line and **before** the `## Pages` heading:

```markdown
## Cookbook

Problem-oriented recipes for common Minigraf patterns. Each recipe is self-contained:
a one-line problem statement, the Datalog (or Rust API where needed), and brief notes.

- [Graph Traversal Patterns](Cookbook-Graph-Traversal) — neighbors, transitive closure,
  reachability, leaf detection, degree, property graphs, edge reification
- [Audit and Time-Travel Idioms](Cookbook-Time-Travel) — point-in-time, time-interval,
  time-point lookup, and time-interval lookup across both time axes
- [Bitemporal Modeling](Cookbook-Bitemporal-Modeling) — bounded facts, retroactive
  corrections, future-dating, overlapping periods, lifecycle end
- [Application Workflow Patterns](Cookbook-Application-Workflows) — agent memory,
  offline-first state, task DAGs, GraphRAG, multi-tenant fleet patterns

```

Use the Edit tool to make this change precisely. The resulting `Home.md` section order must be:
1. `## Tutorials`
2. `## Cookbook`   ← new
3. `## Pages`
4. `## Packages`
5. `## Quick links`

- [ ] **Step 3: Add Cookbook section to _Sidebar.md**

Insert the following block **after** the `- [11. Marketplace](Tutorial-11-Marketplace)` line and **before** the `---` separator:

```markdown

## Cookbook

- [Graph Traversal](Cookbook-Graph-Traversal)
- [Time Travel](Cookbook-Time-Travel)
- [Bitemporal Modeling](Cookbook-Bitemporal-Modeling)
- [App Workflows](Cookbook-Application-Workflows)
```

The resulting `_Sidebar.md` section order must be:
1. `## Tutorial` (existing)
2. `## Cookbook`  ← new
3. `---`
4. `## Reference` (existing)

- [ ] **Step 4: Verify Home.md structure**

```bash
grep "^## " .wiki/Home.md
```
Expected output (in order):
```
## Tutorials
## Cookbook
## Pages
## Packages
## Quick links
```

- [ ] **Step 5: Verify _Sidebar.md structure**

```bash
grep "^## \|^---" .wiki/_Sidebar.md
```
Expected output (in order):
```
## Tutorial
## Cookbook
---
## Reference
```

- [ ] **Step 6: Verify cookbook links appear in both files**

```bash
grep "Cookbook-" .wiki/Home.md | wc -l
grep "Cookbook-" .wiki/_Sidebar.md | wc -l
```
Both expected: `4`

- [ ] **Step 7: Commit**

```bash
cd .wiki
git add Home.md _Sidebar.md
git commit -m "docs(cookbook): add cookbook ToC to Home and Sidebar (#190)"
cd ..
```

---

## Self-review

**Spec coverage:**

| Spec requirement | Task |
|---|---|
| Graph traversal patterns (9 recipes) | Task 1 |
| Audit and time-travel idioms — point-in-time (4) | Task 2 |
| Audit and time-travel idioms — time interval (3) | Task 2 |
| Audit and time-travel idioms — time-point lookup (3) | Task 2 |
| Audit and time-travel idioms — time-interval lookup (2) | Task 2 |
| Audit and time-travel idioms — supporting (1) | Task 2 |
| Bitemporal modeling (8 recipes) | Task 3 |
| Application workflows — agent memory (4) | Task 4 |
| Application workflows — offline-first (2) | Task 4 |
| Application workflows — task DAGs (2) | Task 4 |
| Application workflows — multi-tenant (1) | Task 4 |
| Home.md Cookbook section | Task 5 |
| _Sidebar.md Cookbook section | Task 5 |

All 39 recipes and both navigation files are covered. No gaps.

**Placeholder scan:** No TBD, TODO, or incomplete steps. All recipe content is written in full. Verify commands include expected output.

**Type consistency:** No function names or variables referenced across tasks. Each recipe is self-contained. Pseudo-attribute syntax (`[:e :db/valid-from ?vf]`) used consistently throughout.
