Development¶
Provider-plugin authoring uses the three GitTPL repositories directly.
The parent template composes reviewed source and integration modules, including an initially empty plugin.
Each generated provider owns its descriptor, module builds, tests and CI/release workflows.
Core CI uses plugins/example as a packaging-tooling fixture.
Move that project to a standalone repository without changing the stable plugin and module IDs.
Working on a standalone provider¶
Build the provider's own source and integration modules with Cargo and Bun before packaging their declared artifacts.
From the provider root, run memcastle plugin inspect --json, memcastle plugin package, then
memcastle plugin test dist/ID-VERSION.tar.gz against the exact resulting archive.
The descriptor plugin.toml stays unchanged; the archive alone carries generated versioned manifest.toml metadata
and an external .sha256sum. Use a tagged Git commit, an SCM development version, or an explicit --version override.
Review the archive inventory and checksum and commit source Cargo.lock files with source changes.
The smoke test uses a disposable palace and validates preview, consent, installation and source activation;
it never uses the configured palace or agent installation.
Agent registration and legacy adoption remain provider-specific checks.
Standalone provider CI checks source conformance and archive installation against the successful main CI prerelease
and selected plugin-capable release trees (latest stable patch), recording the exact Core revision or resolved tag.
Core CI builds and checks only the plugins/example fixture on each pull request and main push.
See Plugin packages for the release assets, SHA verification, reusable
workflow inputs and compatibility policy.
Prerequisites¶
- Node and bun, only for
web/andintegrations/(mise run web:checkandintegrations:checkinstall them for the task; the Rust build needs neither). - Rust, via
rustup, which installs the channel pinned inrust-toolchain.toml(mise does not manage Rust itself). - The remaining tools (nextest, prek, ast-grep, typos, ...) via
mise— runmise installonce.
That's it — SurrealKV, the embedded storage engine, is pure Rust,
so the storage engine itself needs no C/C++ toolchain.
(A transitive TLS dependency, aws-lc-sys, may use cmake on some targets; that is unrelated to storage.)
Everyday tasks¶
mise run build # cargo build
mise run test # the basic suite: cargo nextest run, without the WebAssembly tests (accepts nextest selectors)
mise run test:wasm # the WebAssembly suite: every tests/wasm_*.rs binary (slow; needs the wasm32-wasip2 target)
mise run sources:check # build and conformance-test every source under sources/
mise run sources:test -- directory # the same for one source, which is what each CI leg runs
mise run lint # cargo clippy --all-targets --all-features -- -D warnings
mise run format # cargo fmt --all
mise run guards # the architecture guard, including its rule fixtures, described below
mise run integrations:check # test the Core-shipped integration and install its bundle
mise run integrations:build # bundle integrations/*/src into integrations/*/dist, for `--assets-dir "$PWD"`
mise run integrations:package # lay out what a release ships in target/bundled-integrations
mise run web:check # typecheck, test, build and package the web UI, then serve it from a real daemon (needs bun and node)
mise run web:build # build web/ into web/dist, for `--assets-dir "$PWD"` with `web.enable`
mise run web:dev # the dashboard's dev server, proxying /api to the running daemon
mise run web:package # lay out what a release ships in target/bundled-web
mise run eval # measure retrieval quality and latency on the bundled dataset (docs/retrieval-evaluation.md)
mise run eval:baseline # rewrite the committed retrieval baseline, after a change meant to move quality
mise run eval:compare -- a.json b.json # what changed between two retrieval reports
mise run eval:longmemeval -- file.json # session retrieval on a downloaded LongMemEval file
mise run check # every lint, the guards, both test suites, the sources, the integrations and the dashboard, without modifying the tree
mise run ci # check plus the docs build: the local equivalent of CI's lint,
# test and docs steps
mise cli <args> # run memcastle from source, e.g. `mise cli status`
mise run docs # serve this documentation locally
mise run docs:build # build it into site/
The project-owned web:build, web:package, integrations:build and integrations:package tasks skip their build
when their inputs have not changed and their required outputs still exist.
Changing a source or removing an output makes the corresponding task run again; use mise run --force web:build
(or the other task name) to rebuild regardless of freshness.
The test and check tasks still run on every invocation, including when used through mise run check or mise run ci.
prek install (once) wires formatting, spelling, actionlint, markdownlint, commitlint, basic file hygiene,
and the architecture guard described below into git commit as hooks.
Full-project Clippy runs in CI and with mise run lint, rather than on every local commit.
Running the daemon locally¶
mise cli serve
# in another terminal:
mise cli status
mise cli mine ./src
mise cli job list
mise cli job demo --steps 5 # exercise the scheduler without mining anything
mise cli search "job scheduler"
mise cli daemon stop
In this repository mise.toml sets development defaults in its [env] table:
MEMCASTLE_ASSETS_DIR is the checkout and MEMCASTLE_WEB_ENABLE is true.
They apply to mise run, to mise cli and to any shell with mise activated
once you are in the project directory, so mise cli serve already serves the dashboard from web/dist (after
mise run web:build) and finds the integrations and sources of the checkout, with no flag.
Because the variables follow the shell, any memcastle you run from the project directory uses them, including an
installed one: memcastle integration install codex there installs the assets prepared in the checkout
(mise run integrations:build) and not the package's.
Run it from another directory, or env -u MEMCASTLE_ASSETS_DIR memcastle ..., to use the installed assets.
A flag still wins over the variable (--assets-dir), and your own mise.local.toml can override or drop either.
The suites that start a daemon or run the installer are not affected:
mise run integrations:check and mise run web:check unset every MEMCASTLE_* variable before they start,
and the Rust and bun harnesses build their own environment, so a test never depends on where it was run from.
By default the palace lives under ~/.local/share/memcastle/default and the daemon listens on 127.0.0.1 port 8420.
Every path, environment variable, flag and the precedence between them is in Configuration.
config::Config is the source of truth if that page ever drifts.
Testing¶
- Unit tests live next to the code they test (
domain::job's state machine,config's validation,store's migrations/persistence — the storage tests use SurrealDB's in-memory engine for speed, plus one test against a real SurrealKV directory; survival across a restart is proven at the process boundary intests/persistence.rs, because SurrealKV's file lock is not released within one process). - Three suites. Core test binaries that build or run a WebAssembly component are named
tests/wasm_*.rs, and that prefix is the whole definition of the WebAssembly suite:.config/nextest.tomlselects it withbinary(/^wasm_/), andmise run test:wasmand CI'swasmjob pass the same prefix to Cargo as--test 'wasm_*', so that the other test binaries are not compiled for nothing. A newwasm_file therefore joins the suite with no configuration. Most other tests are the basic suite, which is whatmise run test:fastand thedefaultandcinextest profiles run; thewasmandci-wasmprofiles run Core's component tests.mise run testuses the same execution filter but may still compile excluded component test binaries; usetest:fastto avoid linking them. Process-boundary durability, MCP and auth tests and the committed retrieval baseline form the Linux-only slow suite, selected with theslow-testsCargo feature bymise run test:slowand CI'sci-slowprofile.mise run check:allcombines the template'schecktask with this project-specific slow gate. The basic suite runs in CI on Linux, macOS and Windows and needs no WebAssembly target. The WebAssembly suite runs in CI on Linux only, in its ownwasmjob with its own Codecov flag, and itssourcesjob explicitly tests thedirectoryreference source withmise run sources:test -- directory. That job runs conformance, plus any source-ownedcore-tests.rscases not expressible as conformance. - Integration tests (
tests/) run against a tempdir palace and an OS-assigned port. Those that start the daemon inside the test process are modules of one binary,tests/in_process/main.rs: eachtests/binary links the whole server, about 600 MB with debug info, so sixteen of them were most of what compiling the tests took, and one links it once. To add one, createtests/in_process/<name>.rsand list it inmain.rs. nextest still runs every test in a process of its own, and a test is namedin_process <module>::<test>, somise run test -- -E 'binary(in_process) & test(auth::)'selects one file's tests. A test that needs a process boundary (a daemon that is killed, a CLI with no daemon) stays a binary of its own. Most start the daemon in-process (tests/common'sTestDaemon); the ones that need a real process boundary (SurrealKV's file lock is not released within one process) spawn thememcastlebinary instead: tests/in_process/server.rs— health, status, graceful shutdown, retry (in-process).tests/in_process/concurrency.rs— many simulated clients submitting jobs and reading status at once, proving the shared store stays consistent (in-process).tests/in_process/memory_mode.rs,tests/in_process/mcp_memory_mode.rs— per-request and per-MCP-session memory modes (in-process).tests/in_process/audit.rs,tests/in_process/repair.rs— the audit and repair job kinds end to end (in-process).tests/persistence.rs— data and job state survive a daemon restart, including a SIGKILL mid-job and a pause or cancel requested just before it,memcastle daemon startandmemcastle daemon restart --bind --port(subprocess).tests/mcp_smoke.rs— the MCP surface end to end: a real MCP client over streamable HTTP against amemcastle servesubprocess, covering read and write tools, migrations before serving, a second session, and persistence across a restart with a reconnecting session (subprocess).tests/in_process/auth.rs— optional bearer-token authentication over real HTTP: every route guarded, the open health probe, a configured secret, generate, rotate and revoke, and MCP with a token and with no credential-management tool (in-process).tests/auth_lifecycle.rs— the authentication lifecycle through the real CLI: generate, restart, enable, a rotated and a revoked token, the daemon refusing to start with nothing to check against, and no token ever reaching the log, the database or any file (subprocess).tests/credential_isolation.rs— OAuth credentials stay in one module that reaches no palace, no jobs and no WebAssembly runtime, that onlyappand the daemon's root name, and that never logs or reads a token outside where it is stored or sent to its provider.tests/wasm_oauth.rs— a source that signs in with OAuth against a real daemon and the real CLI, with a fake provider: consent to the sign-in,source auththrough the device flow, the token a run is given, renewal, a revoked credential, a miner that needs the sign-in, and no refresh token anywhere but the credentials file (needs thewasm32-wasip2target).tests/config_assets.rs— the assets override is honoured and checked, and a standalone binary starts with no assets (subprocess).tests/dependencies.rs— the lockfile never pulls a second storage engine into the binary (ADR-001), nor a source-specific SDK or native database binding (ADR-026), and the WebAssembly runtime stays built without the features a source host never uses.tests/config_bind.rs— the listener itself: bind address and port precedence, and real bind failures (subprocess).tests/config_paths.rs— the XDG config, data and state locations, resolved through the real binary (subprocess).tests/migrate.rs—memcastle migrateand its--check/--statusmodes (subprocess).tests/cli.rs— the binary's argument parsing and its behaviour with no daemon reachable (subprocess), including colour,completionsand the absence of ahelpsubcommand.tests/in_process/retrieval_eval/— the retrieval evaluation harness (Retrieval evaluation): the bundled dataset against its committed baseline, no result outside its scope or time, and the metric, report and LongMemEval converter units (in-process). Its explicit runs (mise run eval*) are#[ignore]d.tests/in_process/palace.rs— the/api/wings/...hierarchy routes: lifecycle, error contract and memory-mode gates (in-process).tests/shutdown.rs— a stopping process lets the embedded datastore finish stopping before it exits (subprocess).tests/in_process/db_endpoint.rs— the database admin endpoint: opt-in, loopback by default, refused origins, and sharing the daemon's data (in-process).tests/in_process/miners.rs— persistent miner configuration end to end: several miners with their own options, creation and changes validated before anything is written, a change never broadening silently, hand edits noticed and an invalid one keeping the last good miners, a cursor surviving every change that keeps the source, a credential never shown, and REST, the CLI and MCP agreeing (in-process, with the CLI as a subprocess client).tests/in_process/triggers.rs— source triggers end to end: disabled by default and nothing started by defining a trigger, a miner or a webhook; enabling refused with what is missing; poll, watch and webhook firing; a webhook's listener opening only while one is enabled and refusing every unauthentic delivery alike; repeated deliveries running once; a burst of firings and deliveries becoming one waiting run; a restart keeping the user's choice and the remembered timetable; a vanished watch path reported and recovered; hand edits; and REST and the CLI agreeing (in-process, with the CLI as a subprocess client).tests/in_process/cli_daemon.rs— CLI flags that change what the daemon is asked:--mode, and relativeminepaths against an in-process daemon (subprocess client).tests/in_process/integration_contract.rs— the daemon half of the integration contract: a real MCP session replays the language-neutral fixtures intests/fixtures/integration/(modes, checkpoint payloads, failure classes), and fails when the contract page, the capability manifest and the test names disagree (in-process).src/project.rs— the project file and theMEMCASTLE_WING/MEMCASTLE_ROOMoverrides, replaying the cases intests/fixtures/project-config/(unit tests): discovery, nesting, the git and home boundaries, the environment, and what is refused. The integrations replay the same cases in TypeScript, see Project configuration.- The external Pi and OpenCode plugins run their own agent-client suites against a real daemon in their provider repositories, replaying the integration contract's language-neutral fixtures.
tests/integration_cli.rs—memcastle integration list|install|update|removethrough the real binary, with fake fakepiandopencodeprograms and a throwaway home: the lifecycle, idempotence, compatibility refusals, preservation of the user's other plugins and settings, and resolution from--assets-dir, the environment and the binary's own prefix. The installer's unit tests are insrc/integration/(manifest, discovery, adapters, install, update, remove, rollback).tests/integration_bundle.rs— checks the bundles built byintegrations:checkwithpackaging/integrations/build.sh: the package tree (manifests accepted by this MemCastle, no sources ornode_modules, the skills) and the same lifecycle from the packaged tree and from the checkout. It is#[ignore]d in the basic suite so thatmise run integrations:checkcan build the checkout assets without a second suite racing it.tests/in_process/events.rs—GET /api/eventsagainst a real daemon: a note, a wing and a job's progress arrive as identifiers and never as content, adisabledsession is refused and aread_onlyone served, the token is required, and an open stream does not keep the daemon from shutting down.tests/in_process/web.rs— the dashboard's routes against a real daemon: opt-in, the static files and their headers, the refusal of any path that leavesweb/dist, the page for a build that is missing, the public shell with authentication on,/api/config, and the jobs listing'skindandlimit(in-process).src/assetsunit-tests that a packaged layout and a worktree resolveweb/dist/index.htmlthrough the same lookup.tests/web_bundle.rs— builds the dashboard withpackaging/web/build.sh, checks the package tree (no sources, maps ornode_modules) and serves it from an installed prefix and from the checkout. It needs bun and node, so it is#[ignore]d in the basic suite andmise run web:checkruns it.web/test/— the dashboard's client, login, route guard, event-stream parser and live loader (vitest, no daemon), and the same client, login and event stream against a realmemcastle serve(web/test/daemon, bun), see Web UI.tests/in_process/native_conformance.rs— conformance for Core's built-indirectoryadapter in the basic suite. WebAssembly provider conformance runs in each source's own CI leg, includingsources/directory/.- The Pi history source and lifecycle extension have their own tests and fixtures in
memcastle-pi, run against a released or locally built Core binary. - The OpenCode history source's fixtures, conformance and installation/mining smoke checks live in
memcastle-opencode. - The GitHub repository source's fixtures, conformance and installation/mining smoke checks live in
memcastle-github. - The Codex and ChatGPT modules' fixtures, conformance and independent plugin-install/adoption smokes live in
memcastle-openai. tests/wasm_registry.rs— registries against a real daemon (a directory registry written with the same callssource indexmakes): search, install by name with consent, an archive that is not the one published, a package that is not the one the index says, version choice, the trust policy, update with and without new permissions, a bundled source that a registry never installs, updates or replaces, a source registered by GitHub repository (against a stand-in for the API, with a digest that does not match and a repository that cannot be read), an unreadable registry, and the CLI publishing, signing, searching, installing and updating (in-process and subprocess, WebAssembly suite, using the synthetic Core mock source).tests/wasm_bundle.rs—packaging/sources/build.shremoves stale bundled components and archives from the now-empty Core source package tree (WebAssembly suite, Unix only).tests/wasm_runtime.rs,tests/wasm_plugin.rs— installable mock sources against a real daemon: consent, independent module lifecycles, mining through the shared pipeline, retained data, and refusing a component altered on disk (in-process, WebAssembly suite).tests/wasm_projects.rs—memcastle source init,build,testandpackagefor real, the sandbox (a source cannot read outside its grant, see the environment, outrun its time limit or exceed its memory, or run an unlisted program) and installing through the CLI. It builds components with Cargo forwasm32-wasip2(subprocess, WebAssembly suite); see Building components in tests.tests/in_process/skills.rs— the shared agent skills: every skill is discoverable, and every tool, CLI command and REST route it names exists in this release (in-process daemon, plus the real binary's help).
Run a subset with nextest's filter syntax, e.g.:
A filter on mise run test still excludes the wasm_ binaries; use mise run test:wasm -- <filter> for those.
mise run cover runs the basic suite with coverage.
The web TypeScript suite reports coverage too: with COVERAGE=1, mise run web:check runs its test:cover script,
which writes lcov under web/coverage/; CI uploads it to Codecov under the web flag.
Standalone agent plugins run their own client suites in their provider repositories.
.github/codecov.yml also groups every report into components (core, surfaces, app, memory, sources, web, integrations)
by path, so the Codecov page shows each area whatever flag covered it.
Each component reflects the merged execution coverage from every reporting suite.
Its required status permits the same 0.5% fluctuation as the project status,
so platform and timing variation does not fail a build.
Core and provider test boundaries¶
Generic Core WebAssembly lifecycle and contract tests use tests/fixtures/mock-source/, a synthetic WIT component.
Its two fixed candidates and locator-derived text exercise installation, consent, cursors, mining, registry updates and
independent plugin modules without testing any provider's discovery rules or calling an external service.
The mock is a test fixture, not a releasable source: it lives outside sources/ and is built only by the generic tests.
tests/source_isolation.rs guards against reintroducing a concrete provider fixture into those tests.
Each real WebAssembly source's own mise run sources:test -- <name> CI leg runs its conformance fixtures and source-owned
behavior tests; sources/directory/ retains the directory conformance cases.
Core's built-in directory adapter has its separate native conformance test.
Scaffolding and sandbox tests (tests/wasm_projects.rs), OAuth tests (tests/wasm_oauth.rs) and the release bundle test
use purpose-built components because their behavior is what those tests verify.
Where the CI time goes¶
Measured on GitHub's runners after the suites were split, so that a change aimed at speed has a baseline to beat.
Compiling is the test binaries built instrumented for coverage; running is nextest's own summary line.
The figures are from runs that restored their caches; the first run of a new job, or one whose cache was evicted,
compiles wasmtime and Cranelift from scratch and takes several minutes longer.
| Job | Compiling | Running | Whole job |
|---|---|---|---|
| Tests, Linux (basic, 1119 tests) | 46 s | 224 s | 5.4 min |
| Tests, macOS (basic) | 25 s | 67 s | 2.5 min |
| Tests, Windows (basic) | 75 s | 215 s | 7.3 min |
| WebAssembly tests, Linux (38 tests) | 10 s with warm caches, 2.8 min cold | 38 s | 1.5 to 4.3 min |
Source, one leg per sources/<name>/ |
10 s | seconds | about 1 min |
The WebAssembly tests run in under a minute in CI, and their job finishes well before the basic Linux one.
Both suites build only the test binaries they run: the wasm job passes --test 'wasm_*' to Cargo,
and the daemon tests that start a server in the test process are one binary, tests/in_process, rather than sixteen.
The time left in the basic suite is its roughly 1100 tests, not compiling or WebAssembly.
Building components in tests¶
Under nextest every test is its own process, so a component built by one test is invisible to the next, and the cost
is how many times Cargo has to compile what.
The helpers in tests/common/wasm.rs keep that to one small crate per build:
- Every
wasm_*test builds into one shared debug target directory, sowit-bindgenandserde_jsoncompile once. - Every scaffolded project is seeded with the mock source's
Cargo.lock. A scaffold ships none, and without it each build updated the crates.io index and re-resolved 35 packages while holding Cargo's package-cache lock, which every other build then waited for. - The mock source is built in place, in debug, into that shared directory, so generic tests never need a provider build or wait for the release build with LTO that source-owned CI legs make.
- A test that drives
memcastle source initmust callshare_target_ofafterwards. Without it the project builds in release into atarget/of its own and recompiles every dependency, which used to be the two slowest tests in the suite.
Locally this took the WebAssembly suite from about 225 s to 95 s on a cold target directory and from about 92 s to 38 s on a warm one; in CI, on a warm cache, the tests went from 49 s to 31 s and the slowest from 25 s to 14 s. What is left of the job is compiling the instrumented test binaries.
The architecture guard¶
The non-negotiable invariant in AGENTS.md — "the CLI has no business logic MCP/HTTP can't reuse" —
is enforced by the architecture prek hook's ast-grep store-isolation rules.
They inspect Rust paths in src/main.rs, src/cli.rs, src/client/, src/mcp/ and src/api/
for direct store or jobs use (including multiline grouped use crate::{store::..} imports).
The one allowed exception is main.rs's SurrealStore import, which memcastle migrate needs.
A diagnostic-code string such as memcastle::jobs::not_found still trips the rule, but a comment does not:
tests should read a code from the error (Error::body().code) rather than spell it out in these directories.
If you find yourself wanting to import store from one of those,
the fix is almost always to add a method to app::AppServices instead,
so the same capability becomes available to every interface at once.
The single-writer rule enforces the companion invariant, one daemon and one writer per embedded palace:
SurrealStore::connect may only be called from src/server/, src/store/ and src/main.rs (for migrate).
The integrations-http-only-* and shared clients-http-only-* rules keep an integration to MCP and HTTP:
they fail on surrealdb, surrealkv, SurrealStore, a store or jobs path, or /api/db in supported source files
under integrations/ (Markdown and node_modules are skipped), see ADR-019.
Comments and unsupported file types do not trip these rules.
The authentication invariant has no hook, because a route or an MCP tool is not something a grep can recognise.
It is enforced by tests instead: tests/in_process/auth.rs walks a list of the REST routes
(and a route that does not exist) without a token, and fails if an MCP tool's name mentions credentials.
The database admin endpoint (ADR-015) is guarded the same way:
a daemon that was only started has no second listener, a non-loopback bind needs the opt-in and authentication,
a page from another origin is refused, and no MCP tool mentions the database.
Those are tests/in_process/db_endpoint.rs, the /api/db entries in tests/in_process/auth.rs
and a unit test that serve has no flag that starts it.
surrealdb-core and surrealdb-rpc are pinned to the exact surrealdb version: they are SurrealDB internal API,
so bump all three together.
Invariant 9 (source-specific code stays out of the pipeline) and invariant 10 (a source package has no ambient authority)
have no hook, because what they forbid is not something a grep over the whole tree can recognise.
tests/source_isolation.rs reads the source text instead: the pipeline, the chunker and the adapter contract name no
adapter, no file access and no WebAssembly runtime; adapters, the WebAssembly host and src/source/ never reach the store
or the jobs (at any depth); only src/mining/wasm/ names the engine; and the host calls nothing that hands a guest the
daemon's environment, standard streams, arguments or a writable directory.
Building the reference WebAssembly sources needs the wasm32-wasip2 target (rustup target add wasm32-wasip2, which
rust-toolchain.toml requests); mise run sources:check builds and tests every source under sources/.
tests/source_isolation.rs builds nothing and runs in the basic suite on every OS.
Invariant 6 and 10's rule that MCP never touches credentials covers OAuth sign-ins too (ADR-039):
tests/in_process/auth.rs guards both /api/source-packages/{name}/auth routes and fails on any MCP tool that mentions
auth, credential, oauth or sign, and tests/credential_isolation.rs reads the source text so that the credential
module reaches neither the palace nor the WebAssembly runtime,
and nothing outside app and the daemon's root can name it.
Invariant 12 (changing a miner is administrative, and the daemon is the only writer of the [[miners]] section) has no
hook either.
tests/in_process/auth.rs guards every /api/miners route and fails when any MCP tool other than memcastle_miner_list
and memcastle_miner_get mentions miners; tests/in_process/miners.rs holds the rest, and config::miners_file's unit
tests hold that an edit keeps the file's comments and other tables (ADR-037).
Invariant 13 (triggers are opt-in, administrative, and only ask for a run) has no hook either.
tests/trigger_isolation.rs reads the source text so that src/trigger reaches neither the palace, the jobs nor any
source, only app and the daemon's root can name it, MCP calls nothing that defines, enables or fires a trigger, the
main router serves no /hooks route, the webhook listener never logs or passes on a body or a secret, and a trigger
queues work only through the request miner run makes.
tests/in_process/auth.rs guards every /api/triggers route and fails when any MCP tool other than
memcastle_trigger_list and memcastle_trigger_get mentions triggers
(ADR-043).
The integration installer is local tooling like the source tooling, and is held to the same rule in the same way.
tests/integration_isolation.rs fails when src/integration/ mentions the store, the jobs, the daemon client, the
network or an agent's settings file or a credential, and when anything under src/mcp, src/api, src/app or
src/server (or any module but the binary) calls it.
tests/integration_cli.rs exercises the installer with synthetic integrations;
tests/integration_bundle.rs checks the Core-shipped package tree on Linux.
The web UI is held to HTTP as an integration is: the web-http-only-* and shared clients-http-only-* rules fail on
surrealdb, surrealkv,
SurrealStore, a store or jobs path, or /api/db in supported source files under web/
(Markdown, node_modules, dist and bun.lock are skipped), see ADR-035.
That the dashboard's static files are the only thing besides the liveness probe that needs no token is held by
tests/in_process/auth.rs and tests/in_process/web.rs, which walk the routes with the dashboard off and on.
Two more rules guard the remaining invariants.
job-status-only-via-apply fails on any .status = assignment outside src/domain/job.rs,
so a job's status only ever changes through Job::apply.
no-hand-rolled-ddl fails on schema DDL in Rust code, because the schema is SurrealKit's (database/schema/*.surql).
The pinned ast-grep rules live under ast-grep/rules/, with discovery and Vue-as-HTML parsing in sgconfig.yml.
The architecture hook scans the whole checkout even if only one relevant file was staged;
architecture-rule-tests runs the native YAML rule fixtures, and mise run guards runs both prek hooks.
To inspect diagnostics directly, run ast-grep scan --report-style short;
to iterate on a rule, run ast-grep test --skip-snapshot-tests.
Vue <script> blocks use HTML's built-in JavaScript injection, including imports from lang="ts" blocks;
HTML rules check attributes and interpolations separately.
The rules target Rust, TypeScript, TSX, JavaScript, Vue, HTML and JSON as specified by each rule's file globs;
other file types are outside their scope.
The web guard skips Markdown, node_modules, dist and bun.lock, and the integration guard skips Markdown and
node_modules.
These syntax rules cannot resolve renamed imports, computed strings or runtime behavior.
Documentation¶
The documentation site is built by Zensical from docs/, configured in zensical.toml.
mise run docs serves it with live reload, and mise run docs:build (part of mise run ci) builds it.
The documentation workflow builds pull requests without deployment and publishes merged main builds into the root of
memcastle/memcastle.github.io's gh-pages branch, where Pages serves https://memcastle.github.io/.
After the first publish creates the branch (if the repository is empty), configure the destination repository's Pages
source as Deploy from a branch (gh-pages, /).
The source repository's github-pages environment needs APP_CLIENT_ID (variable) and APP_PRIVATE_KEY (secret) for a
GitHub App installed on memcastle/memcastle.github.io with Contents: write; its token is scoped to that repository.
The first publish creates the branch when the destination is empty, and subsequent identical builds make no commit.
Documentation is part of a change, not a follow-up: a pull request that changes a flag, a setting, a tool
or a user-visible behaviour updates the page that describes it, in the same commit series.
- Where things go. Task-oriented pages (installation, quickstart, guides) and references (configuration, CLI, MCP and
REST) are for users;
architecture.md, this page andadr/are for contributors. Say a thing once and link to it from elsewhere instead of repeating it. - New pages must be added to the
navinzensical.toml, or they are built but not reachable. - Describe what exists. Document the implementation as it is, and put anything not built yet under a "not implemented" or "non-goals" heading rather than describing it as available. Run the command and paste real output when a page shows some.
- Diagrams use Mermaid in fenced
mermaidblocks, next to the text they explain. GitHub and the site both render them; stick to flowcharts, sequence, state, class and entity-relationship diagrams, and keep them small enough to read on a phone. - Prose style. One sentence per line, wrapped at a clause boundary if a sentence would pass 120 columns
(
mise run lint:mdenforces the width onAGENTS.md,CONTRIBUTING.md,README.md,docs/andskills/*/SKILL.md).mise run spellrunstypos; a legitimate new term goes intotypos.toml'sextend-words.
This repository is generated from a template¶
See CONTRIBUTING.md for the git tpl workflow (mise run tpl:diff, mise run tpl:update),
and AGENTS.md for where project-specific content goes so template updates keep merging cleanly.