blob: 01edfadcbb36f7869b85162a0428ce038d8f0d59 [file]
# Faster builds when the tools are there: sccache caches compiled crates across clean
# builds and worktrees, nextest runs tests in parallel processes. Neither is required.
SCCACHE := $(shell command -v sccache 2>/dev/null)
ifneq ($(SCCACHE),)
export RUSTC_WRAPPER := $(SCCACHE)
endif
NEXTEST := $(shell cargo nextest --version 2>/dev/null)
TEST_CMD := $(if $(NEXTEST),cargo nextest run --workspace --no-fail-fast,cargo test --workspace)
.PHONY: demo-ready podman-ready e2e hosted install init check verify fmt fmt-check clippy test guard-test docs-only-test no-image-push-test conformance-test conformance deny machete coverage wasm-check msrv-check run build wslcsdk-test wslcsdk unsafe-check windows-target \
local-up local-down local-logs local-key local-memory launch usage golden corpus-check drift taps-check taps-build wasip2-target
# ---- the local stack: LiteLLM and Postgres in containers, the memory root on disk.
# Reads local/.env. See local/README.md.
COMPOSE ?= podman compose
LOCAL_ENV = local/.env
local-up: podman-ready
@test -f $(LOCAL_ENV) || { echo "copy local/.env.example to local/.env and fill it in"; exit 1; }
$(COMPOSE) --env-file $(LOCAL_ENV) -f local/compose.yaml up -d
@echo "LiteLLM on http://127.0.0.1:4000; next: make local-key"
# Rootless podman keeps its state under XDG_RUNTIME_DIR, /run/user/<uid>, which systemd
# makes for a login session. A WSL shell may have none, and podman then fails with
# "mkdir /run/user/1000: permission denied"; lingering makes the directory for good.
podman-ready:
@d="$${XDG_RUNTIME_DIR:-}"; if [ -n "$$d" ] && [ ! -d "$$d" ]; then \
echo "podman cannot run in this shell: $$d is missing, since no login session made it." >&2; \
echo "Once, then open a new terminal: sudo loginctl enable-linger $$(id -un)" >&2; \
echo "The demo does not need podman: make demo-ready" >&2; exit 1; fi
# LiteLLM shares Postgres's network namespace, so it goes first or down trips over it.
local-down: podman-ready
$(COMPOSE) --env-file $(LOCAL_ENV) -f local/compose.yaml rm -sf litellm
$(COMPOSE) --env-file $(LOCAL_ENV) -f local/compose.yaml down
local-logs:
$(COMPOSE) --env-file $(LOCAL_ENV) -f local/compose.yaml logs -f litellm
local-key:
sh scripts/local-key.sh
# Run a harness confined, with the memory server attached and model calls through the
# egress. HARNESS=claude|codex, AUTH=subscription|key|gateway, ARGS passed to the harness.
# make launch HARNESS=claude claude on your own login
# make launch HARNESS=codex AUTH=key codex with OPENAI_API_KEY from local/.env
# make launch HARNESS=claude AUTH=gateway claude through the gateway
# No default harness: say which one, so a run is never the wrong harness by omission.
HARNESS ?=
AUTH ?= subscription
PROVIDER ?=
# The memory's own calls (the recall deputy, mining) on a credential of their own, so a
# harness on a personal login leaves them to the gateway: SERVER_AUTH=gateway (or key,
# with SERVER_PROVIDER naming where).
SERVER_AUTH ?=
SERVER_PROVIDER ?=
SERVER_FLAGS = $(if $(SERVER_AUTH),--server-auth $(SERVER_AUTH),) $(if $(SERVER_PROVIDER),--server-provider $(SERVER_PROVIDER),)
PROMPT ?=
LOOP ?=
BACKEND ?= auto
launch: build
@test -n "$(HARNESS)" || { echo "HARNESS= is required: claude, codex, gemini, antigravity, qwen, grok, muse, pi, copilot, opencode or command" >&2; exit 2; }
@set -a; . ./$(LOCAL_ENV); set +a; \
LLMAO_KEY="$$(cat local/.virtual-key 2>/dev/null)" \
GH_TOKEN="$${GH_TOKEN:-$$(gh auth token 2>/dev/null)}" \
target/release/amem --root "$$MEMORY_ROOT" launch --harness $(HARNESS) --auth $(AUTH) \
--backend $(BACKEND) $(if $(PROVIDER),--provider $(PROVIDER),) $(SERVER_FLAGS) $(if $(MODEL),--model $(MODEL),) \
$(if $(SUBAGENTS),--subagents $(SUBAGENTS),) $(if $(PROMPT),--prompt '$(PROMPT)',) -- $(ARGS)
# The harness image, for the wslc backend (and any OCI runtime): the release binary is
# copied in beside the Dockerfile, then built with wslc on Windows/WSL or podman elsewhere.
IMAGE ?= durable-memory/harness:dev
image: build
@cp target/release/amem images/harness/memory
@if command -v wslc.exe >/dev/null 2>&1; then \
wslc.exe build -t $(IMAGE) "$$(wslpath -w images/harness)"; \
elif command -v wslc >/dev/null 2>&1; then wslc build -t $(IMAGE) images/harness; \
else podman build -t $(IMAGE) images/harness; fi
# Unpack the harness image under the record, for the Linux backend to pivot into. After
# this, `make launch` on Linux runs in the image by default.
unpack: build
@set -a; . ./$(LOCAL_ENV); set +a; \
target/release/amem --root "$$MEMORY_ROOT" image unpack --image $(IMAGE)
# The web launcher, opened in the browser, with local/.env and the virtual key in its
# environment. Outside this checkout, `amem` alone does this from credentials.env.
web: build
@set -a; . ./$(LOCAL_ENV); set +a; \
LLMAO_KEY="$$(cat local/.virtual-key 2>/dev/null)" \
GH_TOKEN="$${GH_TOKEN:-$$(gh auth token 2>/dev/null)}" \
target/release/amem --root "$$MEMORY_ROOT" web $(if $(HARNESS),--harness $(HARNESS),) --auth $(AUTH) --backend $(BACKEND)
# One curation cycle on the record: the survey filed, the checker on a sample through
# the gateway, the canaries checked, the telemetry's alarms filed. EVERY=<seconds> loops.
curate: build
@set -a; . ./$(LOCAL_ENV); set +a; \
LLMAO_KEY="$$(cat local/.virtual-key 2>/dev/null)" \
target/release/amem --root "$$MEMORY_ROOT" curate --auth $(AUTH) $(if $(MODEL),--model $(MODEL),) \
--contradictions $(if $(EVERY),--every $(EVERY),)
# Vendor drift: the probe set (SET, default probes/drift.toml) run against MODEL through
# the gateway (or AUTH=key with PROVIDER), appended under state/drift, then the latest
# run checked against the first. FILE=1 files confirmed drift as a task for the owner.
# make drift MODEL=qwen3.8-27b
DRIFT_AUTH = $(if $(filter subscription,$(AUTH)),gateway,$(AUTH))
DRIFT_PROVIDER = $(or $(PROVIDER),$(if $(filter gateway,$(DRIFT_AUTH)),llmao,openai))
drift: build
@test -n "$(MODEL)" || { echo "MODEL= is required: the model whose drift is watched; amem models --auth gateway lists them" >&2; exit 2; }
@set -a; . ./$(LOCAL_ENV); set +a; \
LLMAO_KEY="$$(cat local/.virtual-key 2>/dev/null)" \
target/release/amem --root "$$MEMORY_ROOT" drift probe --auth $(DRIFT_AUTH) --provider $(DRIFT_PROVIDER) \
--model $(MODEL) $(if $(SET),--set $(SET),) $(if $(REPEAT),--repeat $(REPEAT),) >/dev/null && \
target/release/amem --root "$$MEMORY_ROOT" drift check --provider $(DRIFT_PROVIDER) --model $(MODEL) \
$(if $(BASELINE),--baseline $(BASELINE),) $(if $(FILE),--file,)
# Claim the next task from the ledger and run the harness on it. LOOP=1 keeps claiming.
work: build
@test -n "$(HARNESS)" || { echo "HARNESS= is required: claude, codex, gemini, antigravity, qwen, grok, muse, pi, copilot, opencode or command" >&2; exit 2; }
@set -a; . ./$(LOCAL_ENV); set +a; \
LLMAO_KEY="$$(cat local/.virtual-key 2>/dev/null)" \
GH_TOKEN="$${GH_TOKEN:-$$(gh auth token 2>/dev/null)}" \
target/release/amem --root "$$MEMORY_ROOT" work --harness $(HARNESS) --auth $(AUTH) \
--backend $(BACKEND) $(if $(PROVIDER),--provider $(PROVIDER),) $(SERVER_FLAGS) $(if $(MODEL),--model $(MODEL),) \
$(if $(SUBAGENTS),--subagents $(SUBAGENTS),) $(if $(LOOP),--loop,)
# The weekend demo in a record of its own: DEMO_ROOT (default ~/memory-demo) seeded from
# DEMO_FROM (Markdown directories), one task worked by the harness, the savings measured.
DEMO_ROOT ?= $(HOME)/memory-demo
DEMO_FROM ?= examples/decisions docs
demo: build
@test -n "$(HARNESS)" || { echo "HARNESS= is required: claude, codex, gemini, antigravity, qwen, grok, muse, pi, copilot or opencode" >&2; exit 2; }
@set -a; [ -f $(LOCAL_ENV) ] && . ./$(LOCAL_ENV); set +a; \
LLMAO_KEY="$${LLMAO_KEY:-$$(cat local/.virtual-key 2>/dev/null)}" \
scripts/demo.sh "$(DEMO_ROOT)" $(DEMO_FROM)
# Get the demo to a state it can run: the record seeded (RESET=1 starts it over), the
# harness image at its pins unpacked into it, the gateway's key checked, the logins
# shown. Says what is missing and how to fix it; HARNESS= checks that harness's login.
demo-ready: build
@set -a; [ -f $(LOCAL_ENV) ] && . ./$(LOCAL_ENV); set +a; \
LLMAO_KEY="$${LLMAO_KEY:-$$(cat local/.virtual-key 2>/dev/null)}" \
RESET="$(RESET)" HARNESS="$(HARNESS)" IMAGE="$(IMAGE)" \
scripts/demo-ready.sh "$(DEMO_ROOT)" $(DEMO_FROM)
# The whole demo in acts on one record: ACTS="1 4" chooses a subset (all eleven by default);
# TRANSCRIPT=<file> also writes everything shown there, as plain text.
demo-all: build
@test -n "$(HARNESS)" || { echo "HARNESS= is required: claude, codex, gemini, antigravity, qwen, grok, muse, pi, copilot or opencode" >&2; exit 2; }
@set -a; [ -f $(LOCAL_ENV) ] && . ./$(LOCAL_ENV); set +a; \
LLMAO_KEY="$${LLMAO_KEY:-$$(cat local/.virtual-key 2>/dev/null)}" \
ACTS="$(ACTS)" DEMO_TRANSCRIPT="$(TRANSCRIPT)" scripts/demo-all.sh "$(DEMO_ROOT)" $(DEMO_FROM)
# The paper's read-path claims measured on this record.
savings: build
@set -a; . ./$(LOCAL_ENV); set +a; target/release/amem --root "$$MEMORY_ROOT" savings $(if $(QUERY),--query '$(QUERY)',)
# A golden suite scored against a record: SUITE (default golden/docs.toml, which fits the
# demo corpus) on ROOT (default DEMO_ROOT). PROVIDER adds the deputy's brief to the score.
SUITE ?= golden/docs.toml
GOLDEN_AUTH ?= key
golden: build
@set -a; [ -f $(LOCAL_ENV) ] && . ./$(LOCAL_ENV); set +a; \
LLMAO_KEY="$${LLMAO_KEY:-$$(cat local/.virtual-key 2>/dev/null)}" \
target/release/amem --root "$${ROOT:-$(DEMO_ROOT)}" golden $(SUITE) $(if $(PROVIDER),--provider $(PROVIDER) --auth $(GOLDEN_AUTH),)
usage:
@set -a; . ./$(LOCAL_ENV); set +a; target/release/amem --root "$$MEMORY_ROOT" usage
local-memory:
@set -a; . ./$(LOCAL_ENV); set +a; \
if [ -f "$$MEMORY_ROOT/grants.toml" ]; then echo "memory root at $$MEMORY_ROOT"; \
else target/release/amem --root "$$MEMORY_ROOT" init --domain dom-me; fi
# `make check` is the pre-commit gate: fast, and what every commit must pass.
check: wasip2-target fmt-check clippy test guard-test docs-only-test no-image-push-test wslcsdk-test conformance-test unsafe-check taps-check
# The demo's Markdown, checked as the golden suites, the evals and the acts read it.
corpus-check:
cargo build -p amem
sh scripts/corpus-check.sh target/debug/amem $(DEMO_FROM)
# `make verify` is what CI runs: the gate plus dependencies, coverage, and the wasm build.
verify: check deny machete coverage wasm-check
init:
git config core.hooksPath .githooks
@echo "pre-commit hook installed: .githooks/pre-commit runs make check"
fmt:
cargo fmt --all
fmt-check:
cargo fmt --all -- --check
clippy:
cargo clippy --workspace --all-targets -- -D warnings
test:
$(TEST_CMD)
guard-test:
sh .claude/hooks/guard-bash.test.sh
docs-only-test:
sh scripts/docs-only.test.sh
no-image-push-test:
sh scripts/no-image-push.test.sh
wslcsdk-test:
sh scripts/fetch-wslcsdk.test.sh
conformance-test:
sh scripts/conformance.test.sh
# The seven M0 steps against a server exposing the surface: a fresh record served by this
# build, or URL=<mcp endpoint> for another server, with OPERATOR=<command> taking
# `export <dir>`, `import <dir>` and `reindex` for steps 6 and 7. The default run names
# this build's own index file so step 7 shows the rebuild from a lost one.
conformance: build
@if [ -n "$(URL)" ]; then \
python3 scripts/conformance.py --url "$(URL)" $(if $(OPERATOR),--operator '$(OPERATOR)',); \
else \
root="$$(mktemp -d)/memory"; \
target/release/amem --root "$$root" init --domain dom-conf >/dev/null; \
python3 scripts/conformance.py --stdio "target/release/amem --root $$root serve" \
--operator "target/release/amem --root $$root" \
--drop-index "python3 -c \"import os; os.remove('$$root/state/index.db')\""; \
fi
# `unsafe` only where AGENTS.md allows it, and every block there documented: the C API
# binding is Windows only, so its clippy runs for the Windows target, which needs no
# linker to check.
unsafe-check: windows-target
sh scripts/unsafe-crates.sh
cargo clippy -p dm-wslc-api --target x86_64-pc-windows-gnu --all-targets -- -D warnings
windows-target:
@rustup target list --installed 2>/dev/null | grep -q '^x86_64-pc-windows-gnu$$' || \
{ echo "the C API binding is linted for Windows; run: rustup target add x86_64-pc-windows-gnu" >&2; exit 1; }
# wslcsdk.dll, the WSL containers C API, beside the Windows amem.exe: pinned and checked.
wslcsdk:
sh scripts/fetch-wslcsdk.sh "$(or $(DEST),target/release)"
# Advisories, licences, duplicate versions, sources. Configured in deny.toml.
deny:
cargo deny check
# Dependencies declared and never used.
machete:
cargo machete
# Line coverage over the workspace. COVERAGE_MIN is a ratchet: it may be raised when
# coverage grows and may not be lowered to make a reading pass. Change it in the commit
# that earns it.
COVERAGE_MIN ?= 90
# crates/sandbox/src/linux.rs runs between fork and exec, where a profile is never written,
# so it is measured by the confined launch test rather than by line coverage.
# crates/memory/src/wslc.rs drives wslc.exe, which CI does not have; its argument builders
# are unit-tested and the rest is proven by hand on a WSL machine.
# crates/wslc-api binds a Windows-only DLL; its layout test runs everywhere
# and the rest only where the DLL is.
# crates/taps/src/bindings.rs is generated by wasmtime's bindgen from wit/tap.wit.
COVERAGE_IGNORE = 'sandbox/src/linux\.rs|memory/src/wslc\.rs|wslc-api/src/lib\.rs|taps/src/bindings\.rs'
coverage:
cargo llvm-cov --workspace --ignore-filename-regex $(COVERAGE_IGNORE) --fail-under-lines $(COVERAGE_MIN) --lcov --output-path target/lcov.info
cargo llvm-cov report --ignore-filename-regex $(COVERAGE_IGNORE) --summary-only
# The core crate must build for the browser target, or the reader and the plugins cannot.
# The reader itself is built and bound by crates/web/build.rs, into the web crate's
# OUT_DIR, every time its sources change; nothing generated is committed.
wasm-check:
cargo check -p dm-core --target wasm32-unknown-unknown
cargo check -p dm-policy --target wasm32-unknown-unknown
cargo check -p dm-reader --target wasm32-unknown-unknown
# The rust-version in Cargo.toml is the oldest Rust the workspace builds with: the highest
# rust-version among the locked dependencies (`cargo metadata` lists them), unless the
# workspace's own code needs a newer one. The first step reads every locked crate, for
# every platform, since a build on Linux never sees the Windows-only or macOS-only ones; the
# rest builds with that release for Linux and wasm32, so the declared value cannot fall
# behind the lock. It needs that toolchain, which `check` and `verify` do not, so CI runs it
# as a job of its own:
# rustup toolchain install <rust-version> --profile minimal --target wasm32-unknown-unknown
MSRV = $(shell sed -n 's/^rust-version = "\(.*\)"$$/\1/p' Cargo.toml)
msrv-check:
@test -n "$(MSRV)" || { echo "no rust-version in Cargo.toml" >&2; exit 1; }
mkdir -p target
cargo metadata --locked --format-version 1 > target/msrv-metadata.json
python3 -c 'import json, sys; ver = lambda s: tuple(int(x) for x in (s.split(".") + ["0", "0"])[:3]); msrv = sys.argv[1]; need = sorted({(p["rust_version"], p["name"]) for p in json.load(open(sys.argv[2]))["packages"] if p.get("rust_version") and ver(p["rust_version"]) > ver(msrv)}); sys.exit("rust-version " + msrv + " is older than these locked crates need: " + ", ".join(n + " " + v for v, n in need) if need else 0)' "$(MSRV)" target/msrv-metadata.json
cargo +$(MSRV) check --locked --workspace --all-targets
cargo +$(MSRV) check --locked -p dm-core --target wasm32-unknown-unknown
cargo +$(MSRV) check --locked -p dm-policy --target wasm32-unknown-unknown
cargo +$(MSRV) check --locked -p dm-reader --target wasm32-unknown-unknown
# The taps are a workspace of their own, built as wasm32-wasip2 components. The host's
# tests build them; the gate also formats and lints them.
taps-check: wasip2-target
cargo fmt --manifest-path taps/Cargo.toml --all -- --check
cargo clippy --manifest-path taps/Cargo.toml --target wasm32-wasip2 --all-targets -- -D warnings
# The taps need the component target; say so plainly instead of failing deep in rustc.
wasip2-target:
@rustup target list --installed 2>/dev/null | grep -q '^wasm32-wasip2$$' || \
{ echo "the taps build for wasm32-wasip2; run: rustup target add wasm32-wasip2" >&2; exit 1; }
taps-build:
cargo build --manifest-path taps/Cargo.toml --target wasm32-wasip2 --release
build:
cargo build --release
# The browser tests, in the Playwright image: the launch form, a session's terminal and
# pane, the reader, the token, every page at three widths in both themes. Not part of
# `check` or `verify`; run it for a change to the pages. FILTER= narrows to a spec or -g.
e2e:
sh scripts/e2e.sh $(FILTER)
# The pages as plain files in target/hosted, for a static host; see "A hosted page" in
# local/README.md. `amem --hosted <url>` opens that copy paired with this machine's server.
hosted:
sh scripts/hosted.sh
# `amem` on the PATH, from this checkout.
install:
cargo install --path crates/memory --locked
run:
cargo run -q -p amem -- $(ARGS)