# memory_lint Tool in ai-memory: Detecting Stale Pages and Dangling Cross-Project Links

> ai-memory's memory_lint tool finds stale pages via front-matter and content checks. It also detects dangling cross-project links by validating markdown targets against the SQLite index.

- Repository: [Fabio Akita/ai-memory](https://github.com/akitaonrails/ai-memory)
- Tags: how-to-guide
- Published: 2026-08-23

---

**The `memory_lint` tool built into ai-memory detects stale or wrong pages through front‑matter validation and content hash checks, while identifying dangling cross‑project links by resolving markdown targets against the SQLite page index.**

The `memory_lint` linter—exposed as the `ai-memory lint` sub-command—safeguards the integrity of the ai-memory wiki by running static analysis over all stored markdown pages. According to the akitaonrails/ai-memory source code, this tool enforces data consistency for LLM‑driven knowledge retrieval and auto‑improvement pipelines.

## How memory_lint Detects Stale and Wrong Pages

Stale pages undermine trust in the knowledge base. The linter catches three specific problems through metadata and content validation.

### Missing or Malformed Front-Matter Metadata

Every wiki page requires YAML front‑matter with `author`, `created_at`, `updated_at`, `project_id`, and `workspace_id` fields. The linter in [`crates/ai-memory-consolidate/src/lint.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-consolidate/src/lint.rs) parses these blocks and flags missing or unparsable entries as **stale‑metadata warnings**. Corrupted metadata causes incorrect attribution, broken search results, and failed permission checks.

### Out‑of‑Sync Content Detection

The linter compares the stored `content_hash` in the database against a fresh hash of the on‑disk file. A mismatch indicates the file was edited outside the normal `Wiki::write_page` pathway, leaving the index stale. This check prevents the system from serving outdated knowledge to LLM recall pipelines.

### Invalid Scope References

Pages referencing non‑existent `project_id` or `workspace_id` values are flagged when the `ScopeResolver` lookup against `ai_memory_store` tables fails. This catches orphaned pages that no longer belong to valid organizational scopes.

## How memory_lint Detects Dangling Cross‑Project Links

Cross‑project links enable knowledge reuse across workspaces. The linter ensures these links actually resolve.

### Link Extraction and Normalization

The linter runs a regex over each markdown body to extract `[text](target)` links. It normalizes relative paths (e.g., [`../other-project/page.md`](https://github.com/akitaonrails/ai-memory/blob/main/../other-project/page.md)) to absolute paths using the source page's scope context.

### Target Existence Verification

For each normalized target, the linter:

- Calls `ScopeResolver::lookup_existing_scope` to validate the target's project/workspace exists
- Queries the `pages` table for the target path
- Reports **dangling cross‑project links** when either lookup fails

This catches both links to never‑indexed files and links to projects that were deleted or renamed.

## Core Implementation Details

The linter architecture lives in [`crates/ai-memory-consolidate/src/lint.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-consolidate/src/lint.rs). The implementation streams every `Page` row from SQLite under single‑writer actor guarantees, ensuring a consistent snapshot without network I/O.

```rust
// Simplified core flow from lint.rs
pub enum LintResult {
    StaleMetadata { page_id: PageId, missing: Vec<String> },
    OutOfDateContent { page_id: PageId },
    DanglingLink { source: PageId, target: String },
}

```

Key validation functions include front‑matter YAML parsing, SHA‑256 content hashing, and `ScopeResolver` integration for cross‑project path resolution.

## Running memory_lint

### Command‑Line Usage

```bash

# From repository root

cargo run --bin ai-memory -- lint

```

Sample output:

```text
[WARN] Stale metadata: page src/wiki/projectA/overview.md – missing author field
[WARN] Out‑of‑date content: page src/wiki/projectB/notes.md – file hash differs from index
[WARN] Dangling link: src/wiki/projectA/guide.md → ../projectC/missing.md (target not indexed)

```

### Programmatic Integration

```rust
use ai_memory_consolidate::lint::{run_lint, LintResult};

fn main() -> anyhow::Result<()> {
    let results: Vec<LintResult> = run_lint(&store)?;
    
    // Filter for specific problem types
    let dangling: Vec<_> = results.into_iter()
        .filter(|r| matches!(r, LintResult::DanglingLink { .. }))
        .collect();
    
    println!("Found {} dangling cross‑project links", dangling.len());
    Ok(())
}

```

## Key Source Files

| File | Purpose |
|------|---------|
| [`crates/ai-memory-consolidate/src/lint.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-consolidate/src/lint.rs) | Core linter logic: page loading, metadata validation, hashing, link resolution |
| [`crates/ai-memory-cli/src/commands/lint.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/commands/lint.rs) | CLI argument parsing and formatted warning output |
| [`crates/ai-memory-store/src/schema.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/schema.rs) | `pages` table definition with `content_hash` and `metadata_json` columns |
| [`crates/ai-memory-wiki/src/wiki.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-wiki/src/wiki.rs) | `Wiki::write_page` atomic write pathway that the linter cross‑checks |
| [`docs/ARCHITECTURE.md`](https://github.com/akitaonrails/ai-memory/blob/main/docs/ARCHITECTURE.md) | Single‑writer SQLite actor design for consistent snapshots |

## Summary

- **Stale metadata detection** validates required front‑matter fields and flags missing or malformed YAML blocks.
- **Content freshness checks** compare stored `content_hash` values against on‑disk files to catch unindexed edits.
- **Dangling link detection** resolves cross‑project markdown links through `ScopeResolver` and verifies target existence in the database.
- All checks run locally without network dependencies, making `memory_lint` suitable for CI/CD integration.

## Frequently Asked Questions

### What makes a page "stale" in memory_lint?

A page becomes stale when its front‑matter metadata is missing required fields, its `content_hash` no longer matches the file on disk, or it references a non‑existent project/workspace scope. The linter flags these conditions through `StaleMetadata` and `OutOfDateContent` result variants.

### How does memory_lint handle relative cross‑project links?

The linter normalizes relative paths using the source page's scope context, then resolves the absolute target through `ScopeResolver::lookup_existing_scope`. If the target project or page row is missing, it reports a `DanglingLink` warning with the source `PageId` and target path string.

### Can memory_lint run without a running ai-memory server?

Yes. The linter operates entirely within the CLI process using a local SQLite snapshot. It requires no network I/O or running server, as implemented in [`crates/ai-memory-consolidate/src/lint.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-consolidate/src/lint.rs) with single‑writer actor guarantees for data consistency.

### What consequences do dangling cross‑project links have for LLM operations?

Dangling links break navigation paths and corrupt the knowledge graph used for LLM‑driven recall and auto‑improvement. When an LLM retrieves context through these links, missing targets cause gaps in the retrieved knowledge, reducing response quality and hallucination resistance.