# Design: the `fact` asset type — durable bundle-level knowledge

Status: accepted (phase 1 implemented)
Author: akm
Date: 2026-06-20

## Problem

akm has no home for **durable, mostly-static facts about a user, team, or
project**. Concretely, users want a bundle to carry things like:

- **Personal** — name, email, address, timezone, favorite blogs, writing/working style.
- **Team** — tool stacks, primary languages, CI/CD platform, deployment targets, web addresses.
- **Conventions / constitution** — coding conventions, architecture principles, lint rules.
- **Bundle meta** — folder organization, naming conventions, expected frontmatter, the active-projects list.

Today these have nowhere first-class to live:

- `memory` is **episodic** — recency-decayed, belief-state ranked, captured ad-hoc via `akm remember`. It models "what I observed in a session," not "what is durably true about this bundle."
- `knowledge` is curated **reference material** — documents an agent reads on demand, not bundle-level identity/conventions injected as context.
- `lesson` is a **`when_to_use` trigger** distilled from feedback.
- The `.meta/` convention (`src/core/asset/stash-meta.ts`, see also `docs/guides/concepts.md`) is **documentary only**: not indexed, not searchable, not ranked, and not surfaced to agents automatically.

## Conceptual grounding

The agent-memory literature splits long-term memory three ways (CoALA,
*Cognitive Architectures for Language Agents*, arXiv:2309.02427; echoed by
LangMem and Letta/MemGPT). akm already covers two of the three:

| Memory class | "answers" | akm |
| --- | --- | --- |
| **Episodic** — timestamped events | "what happened" | `session` (#561) |
| **Procedural** — executable how-to | "how to do X" | `skill`, `command`, `workflow` |
| **Semantic** — durable facts/identity | "what is true" | **`fact` (this doc)** |

Karpathy's operating-system analogy frames the design: model weights are the
CPU/ROM (can't retrain), the context window is scarce RAM, and everything else
is disk that must be *paged in*. A fact is **disk-resident knowledge selectively
loaded into RAM** — neither baked into weights nor permanently pinned in the
system prompt. Anthropic's *Effective context engineering for AI agents*
reinforces this: curate aggressively, prefer just-in-time retrieval, and pin
only a small high-signal core (cf. CLAUDE.md, kept short).

## Design

### Type: `fact`

A first-class, indexed, searchable markdown asset type. Stored under
`facts/` in a bundle; ref form `fact:<name>` (nesting allowed, e.g.
`fact:team/tool-stack`). Singular name matches akm convention
(`skill`, `memory`, `lesson`, `task`, `session`).

```
<stash>/facts/
  personal/identity.md         # fact:personal/identity
  personal/writing-style.md
  team/tool-stack.md
  conventions/coding.md         # the "constitution"
  meta/naming-conventions.md
  meta/active-projects.md
```

### One type, categorized — not a type explosion

Rather than separate `fact` / `constitution` / `profile` types, a single
`fact` type carries a `category` frontmatter dimension. This keeps akm's
minimalist type set and matches the LangMem "namespace/scope" approach.
Recommended values: `personal`, `team`, `project`, `convention`, `meta`.
Normative facts (the "constitution") are stored as facts and phrased as
guidance at injection time.

### Frontmatter

```yaml
---
description: <one-line, indexed for search>   # like other markdown types
category: personal|team|project|convention|meta
pinned: false        # true → part of the always-injected core (keep this set small)
updated: 2026-06-20
---
```

`description` and `category` are the curation surface; `pinned` selects the
small always-on core. Additional keys (`source`, `status`, …) are accepted
but not required in phase 1.

### Retrieval & injection model: pinned core + JIT retrieval

Two tiers, matching the context-engineering guidance:

1. **`pinned: true` facts** form the small always-injected core (CLAUDE.md
   style — keep it short). Phase 1 makes `pinned` a real, captured, query-
   surfaceable property (tag + search hint + ranking boost); **phase 2** wires
   the assembled pinned-core into harness system prompts.
2. **Everything else** stays on "disk" and is surfaced through normal
   `akm search` / `akm curate` / `akm show` (JIT retrieval). This works the
   day the type ships — no new retrieval path required.

### Ranking

`fact` gets a high `TYPE_BOOST` (authoritative, like `knowledge`), plus a
modest additional boost for `pinned` facts so the core outranks ordinary
facts on otherwise-equal queries.

## Implementation (phase 1)

Following the `session`/`task` template, metadata is encoded as tags +
search hints (no new DB columns or `StashEntry` fields):

| Concern | File |
| --- | --- |
| Type spec | `PLACEMENT_SPECS` in `src/core/asset/asset-placement.ts` — `fact: { stashDir: "facts", ...markdownSpec }` |
| Renderer + action | `TYPE_PRESENTATION` in `src/core/type-presentation.ts` (supersedes the earlier `TYPE_TO_RENDERER`/`ACTION_BUILDERS` split) |
| Renderer + metadata | `src/output/renderers.ts` — `factMdRenderer` (frontmatter `category`/`pinned` parsed inline in `buildShowResponse`) |
| File classification | `src/indexer/walk/matchers.ts` — `DIR_TYPE_MAP` `facts/` |
| Ranking | `src/indexer/search/ranking-contributors.ts` — `TYPE_BOOST` + `pinnedFactRankingContributor` |
| Lint | `factDiagnostics` in `src/core/adapter/adapters/akm-lint.ts`, called from `src/commands/lint/index.ts` (warns `missing-category` on absent/unrecognized `category`) |
| Authoring hint | `src/integrations/agent/prompts.ts` — `TYPE_HINTS.fact` |
| Graph extraction (opt-in) | not currently wired: `graphExtractionIncludeTypes` (`src/core/config/schema/index-config.ts`) takes any string, but the runtime consumer's `SUPPORTED_GRAPH_EXTRACTION_INCLUDE_TYPES` set (`src/indexer/graph/graph-extraction.ts`) does not include `fact` — configuring it is a silent no-op today |
| Type union | `KNOWN_TYPES` in `src/core/recognition-util.ts`, which includes `fact` |

`fact:` refs resolve automatically — the ref resolver derives its type set
from `PLACEMENT_SPECS`, not `KNOWN_TYPES`: `typeNameFromConceptId`
(`src/core/asset/resolve-ref.ts`) calls `typeForStashDir`
(`src/core/asset/asset-placement.ts`), and `fact` has a `PLACEMENT_SPECS`
entry (`stashDir: "facts"`). The ref-resolver contract note in
`src/commands/lint/base-linter.ts` confirms the same split: `refToRelPath` is
"DERIVED FROM THE PLACEMENT SPECS ... rather than hand-encoded."

## Phase 2 (follow-up, not in this change)

- Assemble the pinned-fact core and inject it into harness system prompts
  (`src/integrations/agent/**`, per-harness builders).
- Optional `akm fact` CLI surface and a hot-capture path (à la `akm remember`).
- Staleness/conflict handling (`status: active|stale|superseded`), since the
  documented failure mode of fact stores is update, not storage.

## Relationship to `.meta/`

`.meta/` stays the human-oriented, non-indexed orientation convention.
`fact` is its machine-facing sibling: indexed, searchable, rankable, and
(phase 2) agent-injected.
