# How Cross-Project Links Work in AI Memory: Syntax, Resolution, and the Dangling Link Curator Check

> Understand cross-project links in AI Memory. Learn the `[[project:path.md]]` syntax and how the dangling link curator check finds broken references to ensure your AI Memory remains organized and accurate.

- Repository: [Fabio Akita/ai-memory](https://github.com/akitaonrails/ai-memory)
- Tags: deep-dive
- Published: 2026-08-25

---

**Cross-project links connect markdown pages across different projects using `[[project:path.md]]` syntax, and the dangling link curator check identifies broken references to non-existent projects by querying the store for unresolved target IDs.**

AI Memory treats each workspace as a unified directed graph where individual project wikis form interconnected nodes. These **cross-project links** enable navigation between distinct codebases or documentation sets, but they require careful validation to prevent broken references. The system implements a deferred resolution mechanism and automated curator checks to maintain graph integrity across the akitaonrails/ai-memory repository.

## Cross-Project Link Syntax and Parsing

AI Memory extends standard markdown wikilinks with syntax that explicitly targets pages in other projects or workspaces.

### Basic Syntax Patterns

The parser recognizes two distinct formats for cross-project navigation:

- `[[project:path.md]]` – Links to a sibling project within the **same workspace**
- `[[workspace/project:path.md]]` – Links to a project in a **different workspace**

These patterns allow authors to reference architecture decisions, API documentation, or context stored in separate repositories without duplicating content.

### How Links Are Parsed and Stored

The `ai-memory-wiki::extract_links` function processes markdown content and builds a `LinkTarget` structure containing `workspace`, `project`, and `path` fields. When stored in the `links` table, the system writes the target's identifiers into `to_workspace` and `to_project` columns, using `NULL` for references within the source's own project according to the schema defined in [`docs/ARCHITECTURE.md`](https://github.com/akitaonrails/ai-memory/blob/main/docs/ARCHITECTURE.md).

Because `to_page_id` functions as a **global identifier**, queries joining on this column automatically aggregate backlinks across project boundaries. This design enables `RelatedPage` objects to carry source `workspace` and `project` metadata, presenting the entire workspace as a unified dependency graph accessible via the `/api/v1/graph` endpoint.

## Deferred Resolution and Backlink Synchronization

Resolution is **deferred-safe**: if the target page does not exist when the link is created, the `to_page_id` remains `NULL` temporarily. When the target page is subsequently created, the `refresh_incoming_links_for_path` function automatically updates the foreign key relationship. This ensures backlinks appear on the target page without requiring manual intervention or re-indexing of the source document.

This mechanism guarantees that the global graph remains consistent even when pages are created, moved, or renamed across different projects.

## The Dangling Cross-Project Link Curator Check

Cross-project links can point to projects that do not exist due to typos, deletions, or workspace reorganization. The **dangling link curator check** surfaces these broken references as warnings.

### Detection Implementation

The check executes in `curator::run` within [`crates/ai-memory-consolidate/src/curator.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-consolidate/src/curator.rs). It invokes the store reader method `dangling_cross_project_links(workspace_id, project_id)` defined in [`crates/ai-memory-store/src/reader.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/reader.rs), which returns every link whose project name cannot resolve to an existing project record.

For each dangling link, the curator generates a `CuratorFinding` structure:

```rust
let target = format!(
    "{}/{}/{}",
    link.workspace.as_deref().unwrap_or(workspace_name),
    link.project,
    link.path
);
CuratorFinding {
    kind: "dangling_cross_project_link".into(),
    severity: "warning".into(),
    message: format!("{} links to missing cross‑project target {target}", link.from_path),
    pages: vec![link.from_path],
    detail: Some(json!({
        "target": target,
        "project_exists": link.project_exists,
    })),
}

```

### Running the Curator and Linting Tools

To surface dangling links in your current workspace and project:

```bash
ai-memory curator --max-findings-per-kind 10

```

To generate a persistent markdown report within your wiki:

```bash
ai-memory lint
ai-memory read-page --path wiki/_lint/dangling_cross_project_link.md

```

These findings list each source page, the malformed target path, and whether the target project exists. Dangling links break graph integrity and hinder automated dependency analysis, so the system categorizes them as warnings to encourage maintenance without blocking workflows.

## Summary

- Cross-project links use `[[project:path.md]]` syntax to reference pages across project boundaries within a unified workspace graph.
- The `ai-memory-wiki::extract_links` function parses these into `LinkTarget` structures stored in the `links` table with `to_workspace` and `to_project` identifiers.
- Deferred resolution via `refresh_incoming_links_for_path` ensures backlinks update automatically when target pages are created.
- The `dangling_cross_project_links` method in [`crates/ai-memory-store/src/reader.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/reader.rs) detects references to non-existent projects.
- The curator generates `CuratorFinding` warnings for broken links, accessible via the CLI or lint reports in `wiki/_lint/`.

## Frequently Asked Questions

### What happens if I link to a page that doesn't exist yet?

The link remains valid but unresolved. The `to_page_id` field stays `NULL` in the database until the target page is created, at which point `refresh_incoming_links_for_path` automatically populates the foreign key and establishes the backlink relationship.

### How do I identify and fix dangling cross-project links?

Run `ai-memory curator` to see warnings in your terminal, or execute `ai-memory lint` to write findings to [`wiki/_lint/dangling_cross_project_link.md`](https://github.com/akitaonrails/ai-memory/blob/main/wiki/_lint/dangling_cross_project_link.md). Fix the link by correcting the project name, creating the missing target project, or removing the obsolete reference.

### Can I link to projects in completely separate workspaces?

Yes. Use the `[[workspace/project:path.md]]` syntax to reference projects outside your current workspace. The resolver validates these against the global workspace registry when the link is stored.

### Where does the system store cross-project link metadata?

Link targets are stored in the `links` table with columns `to_workspace` and `to_project` distinguishing cross-project references from internal links. The global `to_page_id` enables efficient backlink queries across the entire workspace graph.