Docs Project
Contributing to Turnframe
Turnframe is a framework for natural conversational applications with deterministic workflows, durable human interactions and verifiable side effects. Models propose meaning. Deterministic reducers decide effects. Committed events decide claims. Every contribution is judged first against that promise and only then against style, performance or convenience.
Development setup #
-
Install Rust through
rustup. The workspace pins thestablechannel inrust-toolchain.tomlwithrustfmtandclippy; the minimum supported Rust version (MSRV) is 1.88, declared asrust-versionin the workspaceCargo.toml. Changing the MSRV is a reviewed decision, not a side effect of picking up a new API. -
Install the auxiliary tools CI uses:
cargo install cargo-deny cargo-audit cargo-semver-checks. -
Store tests need a PostgreSQL 16 instance. The simplest way is Docker:
docker run --name turnframe-pg -e POSTGRES_USER=turnframe -e POSTGRES_PASSWORD=turnframe \ -e POSTGRES_DB=turnframe_test -p 5432:5432 -d postgres:16export TURNFRAME_TEST_DATABASE_URL=postgres://turnframe:turnframe@localhost:5432/turnframe_testWhen the variable is unset, the PostgreSQL store tests are skipped; the in-memory store tests and everything else run without a database. Never point the variable at a database you care about: the test suite owns the
tf_*tables it creates. -
Every variable the tests and
examples/consoleread is listed in.env.dev. Copy it to.env, which git ignores, and uncomment what you need: those tests and the console load the repository's.envthemselves, and a variable already exported in the shell wins over it. The live provider and evaluation tests skip unless their variable is set, and always skip whenCIis set.
Local check sequence #
Run these before opening a pull request. They mirror .github/workflows/ci.yml, including the
RUSTFLAGS="-D warnings" and RUSTDOCFLAGS="-D warnings" that CI exports, so a warning that is
harmless locally is a failure there.
export RUSTFLAGS="-D warnings" RUSTDOCFLAGS="-D warnings"cargo fmt --all -- --checkcargo clippy --workspace --all-targets --all-features -- -D warningscargo test --workspace --all-features --no-fail-fastcargo test --workspace --all-features --doccargo doc --workspace --all-features --no-depscargo +1.88 check --workspace --all-features --all-targets # MSRV build (rustup toolchain install 1.88 once)cargo deny --all-features checkcargo audit --ignore RUSTSEC-2023-0071 # rsa, never compiled: see the audit job in ci.ymlcargo bench --workspace --no-runCI additionally runs cargo-semver-checks on pull requests and release tags for the published
crates (turnframe-core, turnframe-runtime, turnframe-store, turnframe-provider,
turnframe). Until a first release exists on crates.io that job only validates manifests, but a
breaking change to a public type must still be called out in the pull request description.
What a pull request is expected to honour #
These expectations restate the working rules of the master specification. Reviewers apply them in this order.
- Safety invariants first, then APIs, then implementation. A change that touches reduction, command execution, interaction resolution or claims must name the invariant it preserves (I1 to I20 in the architecture guide) in its description. A PR that wraps a provider SDK or adds a generic tool loop without an invariant behind it will be sent back.
- The model is never trusted with operational truth. Model output may read language, propose semantic acts and write the reply around what code decided. Code that lets model output authorize a consequential effect, or that phrases "created", "sent" or "deleted" without a committed domain event or authoritative receipt behind it, is a defect regardless of test status.
- Ambiguity fails closed and model answers are all-or-nothing. An ambiguous target produces a
clarification
Interactionand no dependent mutation; a malformed task answer is rejected whole, never partially used. - No hidden TODOs on safety boundaries. A vertical slice is complete only when its revision
checks, idempotency handling, interaction lifecycle, events, tests and failure behaviour exist.
// TODOon any of those is a blocker, not a note. Put genuinely deferred work in an issue and link it. - Typed, boring code. Prefer explicit types over clever abstraction:
thiserrorerrors, nounwrap/expecton runtime paths (tests may opt in with an explicit lint allowance),#![forbid(unsafe_code)]in generic crates, secrets wrapped and redacted,tracingfor structured logs. - Macros only after three domains. A macro or proc-macro DSL is accepted only when at least three real domain implementations in the repository show the same boilerplate it removes. Until then, write the trait implementation by hand.
- Provider quirks stay outside the core.
turnframe-coremust not depend on provider crates, database crates or a web framework; provider adapters depend onturnframe-provider, not on runtime internals. Feature flags gate optional integration surfaces only; they never create materially different safety semantics. - No performance or reliability numbers in docs. Latency and reliability targets are gates measured by benchmarks and observation; documentation describes mechanisms, not results.
Commit messages #
Use Conventional Commits: type(scope): imperative summary, where type is one of feat, fix,
docs, refactor, test, perf, build, ci or chore, and scope is a crate short name
(core, runtime, store-postgres, provider-openai, ...) or adr. Mark breaking public API
changes with ! after the scope and a BREAKING CHANGE: footer. Keep the subject under 72
characters and explain the why in the body; the diff already shows the what.
Architecture decision records #
Every load-bearing boundary needs an ADR under docs/adr/, numbered sequentially
(ADR-015-short-title.md) and following the existing sections: Status, Context, Decision,
Consequences, Alternatives considered, Enforcement. A boundary is load-bearing when changing it
would alter a fundamental invariant, the trust boundary between model and reducer, the storage
contract, or the public trait surface of turnframe-core. A PR that changes such a boundary must
add or supersede an ADR in the same change; a PR that merely implements an accepted ADR should
reference it. ADRs describe decisions and reasoning, never who made them.
Adding a provider adapter #
- Create
crates/turnframe-provider-<name>depending onturnframe-provider(and onturnframe-corefor shared types), never onturnframe-runtimeinternals. ImplementModelProvider: reportProviderKey,ModelKeyand honestProviderCapabilities, and map the wire protocol to the normalizedModelRequest,ModelResponseandModelStreamtypes. Provider specific extras go into the typed extension map or an adapter builder, never into core types. - Wire the adapter into the provider conformance suite in
turnframe-test. Every adapter must pass the full list: valid structured response, malformed JSON, unknown fields, missing required fields, multiple acts, tool/read request IDs, streaming reconstruction, empty output, refusal, timeout, rate limit, authentication failure, context overflow, cancellation, retry classification, redaction of secrets, and no silent capability downgrade. The suite runs againstwiremockfixtures in CI; live smoke tests are optional and must be gated behind an environment variable so CI never needs real credentials. - Never fall back to prompt-only JSON when a model cannot meet structured-output requirements for a critical stage. Declare the missing capability and let the router reject or reroute.
- Add the adapter behind a feature flag in the
turnframefacade crate, document supported models in the adapter README, and add an ADR only if the adapter forces a new normalized capability.
Adding a store implementation #
Implement the six store traits from turnframe-store: ConversationStore, InteractionStore,
CommandJournal, EventJournal, OutboxStore and ReplayStore. Interaction resolution must be a
compare-and-swap on the stored revision; command idempotency keys must be persisted before or
atomically with execution and must be unique per account; events must be append-only. Run the shared
store contract tests from turnframe-test against the new backend the same way the in-memory and
PostgreSQL stores do, and document the transaction scope your backend offers so adopters know which
atomicity guarantees of the reducer they actually receive.
Review checklist #
- Does the PR name the invariant it preserves, and does a test fail if that invariant is broken?
- Are all model outputs treated as proposals, with no path from model text to a write or a claim?
- Are revision checks, idempotency keys and event emission present on every new mutation?
- Does an ambiguous target or malformed model response end in an
Interactionor a rejection, never a partial effect? - Are errors typed, secrets redacted, and
unwrap/expectabsent from runtime paths? - Does
turnframe-corestill avoid provider, database and web-framework dependencies? - Is a new abstraction justified by existing repeated code rather than anticipated reuse?
- Are ADRs, rustdoc and the changelog updated, with doc examples that compile as tests?
- Does the full local check sequence pass with
-D warnings?