Skip to content

paths

What placementFor decides from. path is the caller’s explicit override.

Every optional field also admits undefined explicitly. Under exactOptionalPropertyTypes a bare path?: string would refuse placementFor({ path: params.path, … }) for a string | undefined param, forcing every tool adapter to strip absent fields by hand. The contract therefore accepts the shape the MCP and CLI layers actually hold.

readonly optional entities?: readonly string[];
readonly memoryType: string;
readonly optional path?: string;
readonly optional tags?: readonly string[];
readonly optional workspace?: string;
const ARCHIVE_BUCKET: "archive" = "archive";

The bucket eviction moves into, partitioned by year.


const ARCS_DIR: "areas/arcs" = "areas/arcs";

Behavioral arcs. Under areas/ because PARA is fixed at four buckets.


const INBOX_DIR: "areas/inbox" = "areas/inbox";

Where a memory lands when no rule claims it. memhtml doctor reports inbox depth as a health signal, so an unplaceable memory is visible rather than lost.


const MEMORY_EXTENSION: ".html" = ".html";

The file extension every memory carries.


const PEOPLE_DIR: "resources/people" = "resources/people";

The person plane, folded into resources/ rather than given its own bucket.


const TASKS_SUBDIR: "tasks" = "tasks";

The directory segment every task file sits under, appended to its workspace’s project directory or to the inbox.

A subdirectory rather than a fifth bucket: PARA is fixed at four, and a task belongs to whatever the memory beside it belongs to. Keeping tasks in one named segment is what makes ls projects/<slug>/tasks the list operation, which is the design’s CRUDL-without-retrieval contract. The segment name is therefore part of the contract.

function archivePathFor(path, year): string;

The archive path a memory moves to on eviction: archive/<YYYY>/<original-path>, with the original path mirrored exactly beneath the year.

year is a calendar year (a label, not an offset). Mirroring the whole original path is what makes the mapping injective and invertible, so git log --follow reads through the move and diff -M reports it as R100 rather than a delete plus an add.

string

number

string


function archiveYearOf(path): number | undefined;

The archive year of an archive path, or undefined when the path is not archived.

string

number | undefined


function isArchivePath(path): boolean;

True when a path sits under the archive bucket with a year partition.

string

boolean


function isValidMemoryPath(path): boolean;

True when a path is a usable memory path: rooted in a PARA bucket, ending in .html, carrying no . or .. segment.

memoryPathViolation asked as a boolean, so the predicate and the refusal’s reason cannot disagree about which paths are usable.

string

boolean


function memoryPathFor(input): string;

The full path for a new memory. An explicit valid path is authoritative and returned verbatim in canonical form; otherwise the directory comes from placementFor and the filename from filenameFor, which date-prefixes an episodic entry.

PlacementInput & object

string


function memoryPathViolation(path): string | undefined;

Why a path is not a usable memory path, or undefined when it is one.

The RULE and its explanation in one function, because a refusal that restated the rule in its own words would be a second copy of it, free to name a clause this function does not check. A caller that refuses an unusable path quotes this string; isValidMemoryPath is the same question asked as a boolean.

Each clause names the input it saw rather than the rule in the abstract, so a caller holding the message can act without re-reading the format doc. The traversal clause is the security one: it is what keeps a caller-supplied path from escaping the memory repo.

string

string | undefined


function normalizePath(path): string;

Reduce a caller-supplied path to the canonical git-tree form: leading slashes dropped (callers may pass the <link href> document-reference form), repeated slashes collapsed, trailing slash dropped.

The trailing slash is removed by endsWith/slice rather than by //+$/, which is not a style choice: an unanchored-left /+$ is quadratic on a long run of slashes followed by a non-slash, measured 2026-08-18 at 4 ms / 57 ms / 769 ms / 3049 ms for n = 2k / 8k / 32k / 64k. The collapse above happens to defuse it — after it, no run of two slashes survives, so the pattern can only ever match one character — but that made the cost of this function depend on the ORDER of three chained calls, with nothing stating it and nothing checking it. Reordering them, or dropping the collapse, would reintroduce a multi-second stall on 64 KB of input across this function’s callers, all of which sit on the write path that accepts agent-supplied paths. A single-character slice cannot be reordered into a hazard. packages/contracts/tests/paths.test.ts asserts the cost curve at adversarial sizes.

string

string


function originalPathFor(archivePath): string | undefined;

The pre-eviction path behind an archive path, or undefined when the path is not an archive path. Strips exactly one archive/<YYYY>/ prefix, so it is the left inverse of archivePathFor even for a memory archived twice.

string

string | undefined


function paraBucketOf(path): "projects" | "areas" | "resources" | "archive" | undefined;

The PARA bucket a path sits in, or undefined when it sits outside all four.

string

"projects" | "areas" | "resources" | "archive" | undefined


function placementFor(input): string;

The directory a memory belongs in, following design §2.1’s six rules in order, with the task rule sitting between the arc rule and the person rule. Total: it always returns a directory rooted in a PARA bucket, so the write path never guesses twice and never fails.

Returns the directory, not the full path, because the filename needs a title this input does not carry. memoryPathFor composes the two. An explicit path contributes its directory; when that path is unusable it is ignored rather than propagated, so the return stays a valid bucket and this function stays total.

Totality is why the refusal cannot live here. A caller that wants an unusable path refused rather than re-derived asks the store for it (strictPath on a WriteInput), which gates on memoryPathViolation before any of this runs and quotes its reason. Refusing here instead would make placement fallible for every caller, including the sleep phases that place a synthesized arc and have no caller path to be wrong about.

PlacementInput

string