# How the Tool Ledger Scanner in Apache Maka Detects Corruption and Rejections

> Discover how the Apache Maka Tool Ledger scanner detects corruption via duplicate IDs, identity conflicts, and invalid lanes, ensuring data integrity before persistence.

- Repository: [The Apache Software Foundation/maka](https://github.com/apache/maka)
- Tags: internals
- Published: 2026-09-02

---

**The Tool Ledger scanner detects corruption by scanning `RuntimeEvent` sequences for duplicate IDs, identity conflicts, and invalid lanes, then validates transitions before persistence to reject invalid candidate events.**

Apache Maka's **Tool Ledger** provides an immutable audit trail of every tool-related fact—function calls, dispatches, responses, and recovery decisions. The [`tool-ledger-scanner.ts`](https://github.com/apache/maka/blob/main/tool-ledger-scanner.ts) module implements a two-stage validation pipeline that detects **corruption** in existing ledgers and **rejects** invalid write attempts before they reach persistent storage.

---

## Two-Stage Validation Architecture

The scanner operates through distinct but complementary mechanisms:

- **`scanToolLedger`** – Performs a full ledger scan to build operational maps and flag corruption
- **`validateToolLedgerTransition`** – Validates candidate events against ledger rules before persistence

This separation allows Apache Maka to distinguish between **ledger-level corruption** (data store damage) and **application-level rejection** (invalid writes from buggy producers).

---

## Stage 1: Detecting Corruption with `scanToolLedger`

The `scanToolLedger` function in [`packages/core/src/tool-ledger-scanner.ts`](https://github.com/apache/maka/blob/main/packages/core/src/tool-ledger-scanner.ts) (lines 41–250) walks the entire event sequence once, maintains tracking structures, and records any integrity violations.

### Core Detection Mechanisms

| Check | Issue Code | Location |
|-------|-----------|----------|
| Duplicate event IDs | `duplicate_event_id` | L55–58 |
| Invocation spine conflicts | `invocation_identity_conflict` | L60–66 |
| Invalid semantic lanes | Lane-specific codes | L70–73 |
| Duplicate tool calls | `duplicate_call` | L95–103 |
| Duplicate dispatches | `duplicate_dispatch` | L133–141 |
| Identity mismatches | `identity_conflict` | L149–156 |

### Corruption Flag Semantics

The function returns a `ToolLedgerScanResult` where `hasCorruption: issues.length > 0` serves as the definitive indicator:

```ts
return { operations, issues, hasCorruption: issues.length > 0 };

```

When `hasCorruption` is true, the ledger is considered **unusable** for subsequent writes. The `SqliteRuntimeStore` will throw `ToolLedgerCorruptionError` rather than attempt further modifications.

### Key Tracking Structures

The scanner maintains several in-memory collections during traversal:

- `seenEventIds` – Detects duplicate `event.id` values
- `invocationSpines` – Maps `invocationId` to `(sessionId, runId, turnId)` triples
- `byToolCall` and `byOperation` – Indexes events by tool call ID and operation ID
- `operations` – Builds the logical view of completed tool operations

---

## Stage 2: Detecting Rejections with `validateToolLedgerTransition`

The `validateToolLedgerTransition` function validates prospective writes before persistence. It resides in the same source file (lines 10–35) and implements four defensive checks.

### 1. Existing Corruption Fast-Path

If the ledger already contains corruption, the function immediately rejects:

```ts
if (existing.hasCorruption) return issueValidation(existing.issues[0]!);

```

This prevents writes to fundamentally compromised storage.

### 2. Candidate Deduplication

Candidate events sharing IDs with existing events trigger rejection if content differs:

```ts
if (!nodeUtil.isDeepStrictEqual(prior, candidate)) {
  return { ok: false, code: 'duplicate_event_id', eventId: candidate.id };
}

```

### 3. Transition Shape Validation

The scanner validates that candidate events match expected semantic patterns:

| Transition Type | Required Lane Sequence |
|---------------|----------------------|
| `generic_append` | Single valid event |
| `t1_prepare` | `function_call → tool_dispatch` |
| `t2_outcome` | Response or decision following prepared call |
| `recovery_bundle` | Reconciliation-specific sequence |

Violations return `transition_shape_conflict`.

### 4. Prospective Re-Scanning

After shape validation, the scanner runs:

```ts
const prospective = scanToolLedger([...input.existingEvents, ...candidates]);
if (prospective.hasCorruption) return issueValidation(prospective.issues[0]!);

```

This catches issues introduced only by the candidate events—orphan responses, hash mismatches, or cross-event conflicts.

---

## Error Types and Calling Conventions

The scanner distinguishes two failure modes through distinct error classes:

### `ToolLedgerRejectionError`

Thrown when a **healthy ledger** receives an **invalid candidate**:

- Indicates producer-side bugs
- Embedded `code` and `eventId` enable precise diagnosis
- Source: `SqliteRuntimeStore.assertToolLedgerTransition` (L318–329)

### `ToolLedgerCorruptionError`

Thrown when operating on a **pre-corrupted ledger**:

- Indicates storage damage or prior crash
- Requires administrative intervention
- Prevents further writes that could compound damage

Both error classes are defined in [`packages/core/src/tool-ledger-scanner.ts`](https://github.com/apache/maka/blob/main/packages/core/src/tool-ledger-scanner.ts#L68-L90) and include the offending issue code and event ID.

---

## Practical Implementation Example

```ts
import { 
  scanToolLedger, 
  validateToolLedgerTransition,
  ToolLedgerRejectionError 
} from '@maka/core/tool-ledger-scanner';
import type { RuntimeEvent } from '@maka/core/runtime-event';

// Stage 1: Validate ledger health on load
const ledgerEvents: RuntimeEvent[] = await store.fetchLedger(sessionId, runId);
const healthCheck = scanToolLedger(ledgerEvents);

if (healthCheck.hasCorruption) {
  // Log all issues for forensic analysis
  console.error('Corruption detected:', healthCheck.issues);
  // Runtime will throw ToolLedgerCorruptionError on write attempts
}

// Stage 2: Validate before appending new events
const newDispatch: RuntimeEvent = {
  id: generateUUID(),
  lane: 'tool_dispatch',
  invocationId: currentInvocation,
  // ... additional fields
};

const validation = validateToolLedgerTransition({
  existingEvents: ledgerEvents,
  candidateEvents: [newDispatch],
  expectedTransition: 't1_prepare'
});

if (!validation.ok) {
  throw new ToolLedgerRejectionError(validation.code, validation.eventId);
}

// Safe to persist
await store.appendRuntimeEvent(sessionId, runId, newDispatch);

```

---

## Summary

- **Corruption detection** occurs via `scanToolLedger`, which scans the full event sequence and sets `hasCorruption` when any integrity violation is found
- **Rejection detection** occurs via `validateToolLedgerTransition`, which validates candidate events against ledger rules before persistence
- **Fast-fail behavior** prevents writes to corrupted ledgers immediately, avoiding data compounding
- **Precise diagnostics** through embedded issue codes and event IDs enable rapid debugging
- **Two error types**—`ToolLedgerRejectionError` for invalid candidates, `ToolLedgerCorruptionError` for damaged storage—provide clear operational signals

---

## Frequently Asked Questions

### What triggers an `invocation_identity_conflict` issue?

The `invocation_identity_conflict` issue fires when the same `invocationId` appears with different `(sessionId, runId, turnId)` triples across events. According to the source in [`packages/core/src/tool-ledger-scanner.ts`](https://github.com/apache/maka/blob/main/packages/core/src/tool-ledger-scanner.ts#L60-L66), the scanner maintains a `spine` string representation of this triple and compares it against any prior occurrence for the same invocation ID.

### Can a candidate event be rejected even if the ledger is healthy?

Yes. The `validateToolLedgerTransition` function performs four independent checks: existing corruption status, candidate deduplication, transition shape validation, and prospective re-scanning. Any of the last three checks can reject a candidate against a healthy ledger—typically indicating producer-side bugs or race conditions in event generation.

### How does `t1_prepare` transition validation work?

The `t1_prepare` transition requires a specific two-event sequence: a `function_call` followed by a `tool_dispatch` for the same tool call. The `validateTransitionShape` helper checks that candidate events conform to this pattern; violations return `transition_shape_conflict`. This enforces causal ordering between call preparation and actual dispatch.

### Where does the runtime actually throw these errors?

The `SqliteRuntimeStore` class in [`packages/storage/src/sqlite-runtime-store.ts`](https://github.com/apache/maka/blob/main/packages/storage/src/sqlite-runtime-store.ts) (around L318–329) orchestrates the validation calls. It invokes `validateToolLedgerTransition` within `assertToolLedgerTransition` and throws `ToolLedgerRejectionError` or `ToolLedgerCorruptionError` based on the validation result returned from the scanner module.