# How the Board Card's Audit Counter Persists Through Lane Changes and Approvals

> Learn how the board card audit counter persists through lane changes and approvals. Discover its immutable data map, atomic increment, and serialization process in Swarm Forge.

- Repository: [Robert C. Martin/swarm-forge](https://github.com/unclebob/swarm-forge)
- Tags: internals
- Published: 2026-08-30

---

**The board card's audit counter survives lane changes and approvals because it lives inside the card's immutable data map, gets incremented atomically during every mutation, and is serialized to disk via [`pack_board.sh`](https://github.com/unclebob/swarm-forge/blob/main/pack_board.sh) after each state change.**

Every card in the **swarm-forge** Kanban system carries an `:audit-counter` key that tracks how many actions have been performed on it. Unlike client-side session state, this counter is durable—it persists across browser refreshes, server restarts, and concurrent edits by multiple users. Here's exactly how the mechanism works.

## Card Data Structure and the Audit Counter

Each board card is represented as a Clojure map stored in a central board atom. The map includes standard fields like `:lane`, `:title`, and `:approved`, plus the critical `:audit-counter` key.

When the board is persisted, this entire structure is written to JSON. The [[`pack_board.sh`](https://github.com/unclebob/swarm-forge/blob/main/pack_board.sh)](https://github.com/unclebob/swarm-forge/blob/main/pack_board.sh) script dumps the current board state to [`board.json`](https://github.com/unclebob/swarm-forge/blob/main/board.json), capturing the audit counter value for every card. This file becomes the single source of truth.

```clojure
;; Typical card map structure
{:id "card-42"
 :title "Implement authentication"
 :lane "in-progress"
 :approved false
 :audit-counter 3}  ;; increments on each move or approval

```

## Lane Changes Increment the Counter

When a user drags a card to a new lane, the frontend dispatches a `move-card` command defined in [[`handoff-protocol.md`](https://github.com/unclebob/swarm-forge/blob/main/handoff-protocol.md)](https://github.com/unclebob/swarm-forge/blob/main/handoff-protocol.md). The backend handler executes the move inside a `swap!` operation that atomically updates three things: the card's lane, its audit counter, and the board's timestamp.

```clojure
(defn move-card [board card-id new-lane]
  (swap! board
    (fn [state]
      (update-in state [:cards card-id]
        (fn [card]
          (-> card
              (assoc :lane new-lane)
              (update :audit-counter inc)))))))

```

The `swap!` ensures that concurrent lane changes from multiple users never lose increments. After the atom updates, the system triggers [`pack_board.sh`](https://github.com/unclebob/swarm-forge/blob/main/pack_board.sh) to serialize the new state to disk.

## Approvals Also Bump the Counter

Approving a card follows the same pattern. The `approve-card` command—also specified in [[`handoff-protocol.md`](https://github.com/unclebob/swarm-forge/blob/main/handoff-protocol.md)](https://github.com/unclebob/swarm-forge/blob/main/handoff-protocol.md)—increments the audit counter before marking the card as approved. This design choice treats approvals as auditable events, not silent state flips.

```clojure
(defn approve-card [board card-id]
  (swap! board
    (fn [state]
      (update-in state [:cards card-id]
        (fn [card]
          (-> card
              (assoc :approved true)
              (update :audit-counter inc)))))))

```

Because approvals and lane changes both use the same atomic update mechanism, the audit counter remains accurate regardless of which action triggered it.

## Persistence via pack_board.sh

Durability comes from [[`swarmforge/scripts/pack_board.sh`](https://github.com/unclebob/swarm-forge/blob/main/swarmforge/scripts/pack_board.sh)](https://github.com/unclebob/swarm-forge/blob/main/swarmforge/scripts/pack_board.sh), which runs after every state mutation. The script extracts the board atom's current value as JSON and writes it to the data directory.

```bash
#!/usr/bin/env bash

# pack_board.sh – serializes board state including all audit counters

board_json=$(clojure -M -e "(prn (json/write-str @board))")
echo "$board_json" > ./swarmforge/data/board.json

```

When the system restarts or another client loads the board, it reads [`board.json`](https://github.com/unclebob/swarm-forge/blob/main/board.json) and restores every card's audit counter to its last persisted value. No counter is ever lost because no operation completes without triggering this serialization step.

## Atomic Updates Prevent Race Conditions

The board state lives in a Clojure atom, which provides **compare-and-swap semantics**. If two users simultaneously move the same card, `swap!` retries the update function with the latest state until it succeeds. The audit counter increments exactly once per action, even under contention.

This atomicity is critical for audit integrity. Without it, simultaneous approvals could overwrite each other's counter increments, corrupting the audit trail.

## Key Files in the Persistence Flow

| File | Purpose |
|------|---------|
| [[`handoff-protocol.md`](https://github.com/unclebob/swarm-forge/blob/main/handoff-protocol.md)](https://github.com/unclebob/swarm-forge/blob/main/handoff-protocol.md) | Defines `move-card` and `approve-card` command formats |
| [[`pack_board.sh`](https://github.com/unclebob/swarm-forge/blob/main/pack_board.sh)](https://github.com/unclebob/swarm-forge/blob/main/pack_board.sh) | Root-level script entry point for board serialization |
| [[`swarmforge/scripts/pack_board.sh`](https://github.com/unclebob/swarm-forge/blob/main/swarmforge/scripts/pack_board.sh)](https://github.com/unclebob/swarm-forge/blob/main/swarmforge/scripts/pack_board.sh) | Implementation that writes [`board.json`](https://github.com/unclebob/swarm-forge/blob/main/board.json) |
| [`board.json`](https://github.com/unclebob/swarm-forge/blob/main/board.json) (generated) | Persisted board state with audit counters |

## Summary

- **The audit counter lives in the card map**, ensuring it travels with the card data everywhere.
- **Every mutation wraps in `swap!`**, guaranteeing atomic increments even during concurrent access.
- **Lane changes and approvals both increment the counter**, creating a unified audit trail.
- **[`pack_board.sh`](https://github.com/unclebob/swarm-forge/blob/main/pack_board.sh) serializes after every change**, making counters durable across sessions.
- **JSON restoration on load** brings counters back exactly where they left off.

## Frequently Asked Questions

### Where is the audit counter stored between browser sessions?

The counter persists in [`board.json`](https://github.com/unclebob/swarm-forge/blob/main/board.json), a JSON file generated by [`pack_board.sh`](https://github.com/unclebob/swarm-forge/blob/main/pack_board.sh) after every board mutation. When the application restarts, it reloads this file and restores all card states including their audit counters.

### What prevents two users from overwriting each other's counter increments?

Clojure's `swap!` operation on the board atom provides compare-and-swap semantics. If the underlying state changes during an update, `swap!` automatically retries with fresh state until the operation succeeds, ensuring no increments are lost.

### Does deleting a card preserve its audit history?

The audit counter exists only while the card remains in the board. Deletion removes the card map entirely from the board state, erasing the counter with it. For permanent audit logs, you would need a separate append-only event log outside the current board snapshot.