How the Board Card's Audit Counter Persists Through Lane Changes and Approvals
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 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) script dumps the current board state to board.json, capturing the audit counter value for every card. This file becomes the single source of truth.
;; 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). 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.
(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 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)—increments the audit counter before marking the card as approved. This design choice treats approvals as auditable events, not silent state flips.
(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), which runs after every state mutation. The script extracts the board atom's current value as JSON and writes it to the data directory.
#!/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 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) |
Defines move-card and approve-card command formats |
[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) |
Implementation that writes board.json |
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.shserializes 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, a JSON file generated by 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →