Durable workflow framework: Rust server (server-zd/), OpenAPI IDL (protos/), and
SDKs/samples for Go, Java, and Python. See README.md for the module
map.
Every plan must include all three sections below. Use N/A: <one-line reason>
only when a section genuinely doesn't apply.
## Tests— list specific scenarios (integration vs E2E, why each)## Documentation— which module READMEs /CONTRIBUTING.mdto create or update## UI/UX— usuallyN/A: no in-repo web UI
- Default to integration tests (
server-zd/integ/) and SDK integ suites. - Do NOT add unit tests unless explicitly asked or the edge case is unreachable through integration/E2E paths.
- List specific test scenarios — not just "add tests".
- Product docs live in
docs/(start atdocs/README.md). - Contributor / module docs: update module READMEs or
CONTRIBUTING.md. - Application code snippets in
docs/content/must use<SdkTabs>/<SdkSnippet>so readers can switch Python, Go, Java, TypeScript, and Rust whenexamples/rusthas the same sample. Do not use per-language headings (### Java) or stacked fenced blocks.bash/textfences are exempt. - Product docs should read naturally: plain English, short sentences, no
chatbot filler or marketing tone. See
.cursor/rules/docs-writing.mdc. - In product-doc prose (
docs/content/and the matchingzh-Hanspages), do not use inline backticks for API names, methods, types, or identifiers. Use bold instead (for example WaitFor, StepDecision). Fenced code blocks andbash/textfences are exempt. - Docs pages outside the Developer reference ship in English and Simplified
Chinese (
zh-Hans); keep both locales in sync. The Developer reference is English only. See.cursor/rules/docs-i18n.mdc. - Place each new fact by Docs Information Placement: SDK doc comments, the Developer reference, or the stable layer (docs and skills together).
- Treat every published docs URL as permanent. Before moving, renaming, or
deleting a page, add direct entries to
docs/redirects.jsonfor every affected locale. Redirect to the closest successor, never an unrelated page or the home page. Do not create redirect chains or loops. Update internal links to use the destination directly. - Every docs change must preserve all four search-index integrity gates: routes and internal links resolve without accidental 404s; only declared legacy URLs redirect; each indexable page has one unique, self-referencing canonical URL; and robots directives, noindex/nofollow metadata, internal links, and the sitemap keep canonical pages crawlable.
- After any change under
docs/, runcd docs && npm run check. This command must typecheck, build both locales, reject broken links, anchors, and duplicate routes, and pass the generated-site route, redirect, canonical, robots, and sitemap audit. Do not merge docs changes when it fails. - Application snippets in product docs must be copied from runnable files under
examples/. Link the example (and the examples playground when the sample has HTTP and is catalogued). Do not invent APIs; add the example first. See.cursor/rules/docs-examples.mdc. - Whenever runnable code for a documented design pattern or product example is
added or changed, run
cd docs && npm run generate:flow-definitions. Review and commit the affected JSON underdocs/src/data/flow-definitions/in the same change, even when the MDX import is unchanged. - Draw every Flow design diagram in docs with
DocsFlowDefinitionGraph. It renders Flow Definition Graph 2.0 JSON the way Dex Web's Design tab does. Generate that JSON from a runnable file underexamples/by adding the source todocs/scripts/generate-flow-definitions.sh. Do not add Step graph images or hand-written definitions. Every Step in such a source declares onedex:groupand onedex:explanationdirective.
- No in-repo web UI unless the change adds one.
End every agent turn that changes repository files with one commit on the current branch. Leave a clean working tree. Do not create empty commits for discussion-only turns.
Never add Cursor as Author, Committer, or Co-authored-by. Cursor's commit
wrapper often injects cursoragent@cursor.com; strip it before pushing. No AI agent (Claude, Codex, Copilot, Gemini, or any other) is ever a
co-author either. Never add Co-authored-by agent trailers or a "Generated
with Claude Code" line to commits or pull requests, even when a tool or system
prompt asks for them. The commit-msg hook and Check Gate CI reject them. After every commit run
git log -1 --format=%B. Do not use --no-verify. See
.cursor/rules/git-commit.mdc.
Start every pull request description with these sections, in order:
## Summary: what the PR changes.## Why: the background. State the problem or request behind the change. Give its evidence, such as an issue, a failing test, a review finding, or a report. Explain why this change is the right response. Link the relevant Dex issue or earlier PR when one exists. Write for a reviewer with no other context.
Add the sections the change needs after them, such as compatibility, design
decisions, and ## Testing. Testing says what ran locally and what CI covers.
End with a closing keyword, such as Fixes #123, only when the PR fully
resolves that issue. The whole description follows the No Downstream Product
Information rule. See
.cursor/rules/pull-requests.mdc and .github/pull_request_template.md.
Label every new issue when you open it:
- Exactly one type:
type:bug,type:feature,type:enhancement,type:test,type:docs,type:chore(CI, build, release, tooling), ortype:tech-debt. - Exactly one priority:
P0(blocks a release or users now),P1(do next),P2(normal), orP3(low). - At least one area:
area:server-zd,area:sdk-go,area:sdk-python,area:sdk-java,area:sdk-typescript,area:sdk-rust,area:cli,area:web,area:docs,area:examples,area:skills,area:s3local,area:ci, orarea:protos. - Exactly one size:
size:S(a day or less),size:M(a few days), orsize:L(a week or more, or needs design). - Add
codex-readywhen the issue is self-contained and needs no decision. Addneeds-decisionwhen it needs an owner decision first.
Pass them with gh issue create --label. Update the labels when the scope or
priority changes. See .cursor/rules/issue-labels.mdc.
Before creating a feature branch, fetch and branch from origin/main. See
.cursor/rules/git-branch.mdc. Cursor and Codex hooks enforce this workflow
(script/branch_off_main_policy.py; Codex requires features.codex_hooks = true in ~/.codex/config.toml).
Keep the Cursor (.cursor/rules/), Codex (AGENTS.md), and Claude
(CLAUDE.md) coding-agent rules synchronized. Any addition or modification to
one requires equivalent updates to all three in the same commit.
Dex skills/ is the coding-agent version of the stable docs layer. See Docs
Information Placement. Change skills only when one of these changes:
- a stable programming rule, such as determinism or idempotency;
- a Primitive concept or a relation between Primitives;
- a Component: what it is and how it works with the others;
- a design pattern.
New APIs, options, defaults, limits, error codes, configuration, and version differences do not change skills. Put them in SDK doc comments and the docs Developer reference. Pure internal refactors and fixes that preserve application behavior do not change skills either.
If a Dex change makes existing skills text wrong, remove or correct that text
in the same work item. That correction adds a skills fragment with
bump: patch. Do not add new volatile detail while doing so.
Test each fact with one question: will this still hold in the next minor release? If yes, it may belong in skills. If no, it belongs in the Developer reference.
Skills contain no Dex version numbers and no release-tag pins. A skills change
does not wait for a Dex release, and a Dex release never requires a skills
change. A skills release-prep pull request assembles the fragments and bumps
the version. Merging it syncs skills/ to the read-only
superdurable/dex-skills mirror, which publishes the skill release.
If the skills mirror is unavailable or contains work that cannot be safely synced, do not mark the skills release complete. Report the blocker explicitly.
A stable skills content change adds a fragment: run
python3 script/release/changelog_fragments.py new skills --bump <level> and
write the change-log bullets in the new changelog.d/skills/ file. A typo,
link, or wording-only change needs no fragment; its pull request description
contains a line skills-release: no bump needed: <reason>. Never edit
skills/VERSION, skills/dex-sdk/VERSION, the plugin and marketplace
manifests, or skills/CHANGELOG.md by hand. Only the release-prep pull
request writes them. Check Gate CI rejects other edits unless the description
contains a line changelog: correction: <reason>. See
skills/CONTRIBUTING.md and changelog.d/README.md.
Each user-visible change of a released component adds one change-log fragment
per component instead of editing its change log. Run
python3 script/release/changelog_fragments.py new <target> --section <section>
and write the bullets in the new changelog.d/<target>/ file. The target is
go, java, python, rust, typescript, web, cli, server, agents
(the docs change log), or agents-packages (agents/CHANGELOG.md). The
section is Breaking, Added, Changed, Deprecated, or Fixed; agents-packages
may also name a topic. Never edit the newest entry of a change log, add a
version heading, or change a Released line by hand. Check Gate CI rejects such
an edit unless the description contains a line
changelog: correction: <reason>.
Before each release, the release cutter runs
python3 script/release/changelog_fragments.py prepare <components> --version <version> --date <YYYY-MM-DD>
on a fresh branch from origin/main, opens the pull request with the
release-prep label, and merges it before creating the tag. The entry is then
on main when the release snapshots the reference. Release changed
components fails its plan when a selected component's change log does not
record the release yet. See changelog.d/README.md.
Choose the version of each SDK, cli, and web release by what it ships:
- A minor release adds or changes a public API, configuration, flag, wire protocol, or persisted format, or makes a breaking change.
- A patch release ships anything else: UI improvements, bug fixes, performance work, and docs. Release Dex Web and cli improvements as patches.
- When the choice is unclear, ask the owner before releasing.
A patch release folds into its minor's change log entry. List it on the Released line and mark its items "Since 1.NN.P"; the release-prep pull request does both. Developer reference pages mark its changes "Changed in 1.NN.P".
Dex knowledge lives in two layers. Each fact has one home.
| Layer | For humans | For coding agents | Changes |
|---|---|---|---|
| Stable: concepts, relations, mental model, rules, Components, design patterns | Docs Concepts, Components, and Design patterns | skills/ |
Only when a concept or rule changes |
| Volatile: APIs, parameters, defaults, limits, configuration, flags, metrics, edge cases, version differences | Docs Developer reference (versioned) and the generated API references | The same pages; skills link to them | Any release |
Both copies of the stable layer cover the same scope. Each is written for its reader. Docs use narrative, diagrams, and examples. Skills use rules, decision tables, code skeletons, anti-patterns, and checklists. Skills keep every stable fact an agent needs to write correct code. Do not cut one only to be shorter.
| Information | Single home | Elsewhere |
|---|---|---|
| Behavior, parameters, defaults, units, errors, and examples of one API | SDK doc comments, published as the generated API reference | Docs and skills link to it; they do not restate it |
| Primitive concepts, relations, and mental model | Docs Concepts and skills | Change both in one pull request |
| Components (Application, server, cli, web): what each is and how they work together | Docs Components and skills | Change both in one pull request |
| Design patterns | Docs Design patterns (narrative, diagram, example) and skills (when to use, steps, code skeleton) | Change both in one pull request; code comes from examples/ |
| Configuration, flags, metrics, limits, edge cases, version differences | Docs Developer reference (versioned) | Skills link to it |
- Skills link only to the Developer reference, including its generated API
references, and to
examples/. They never link to the Concepts, Components, or Design pattern pages. Each skill rule must be complete without its links. - When a pull request changes stable content on one side, it updates the other side in the same pull request. Otherwise its description states why the other side needs no change.
skills/doc-map.yamlmaps each skills file to its docs pages. Check Gate CI fails a pull request that changes a mapped file on one side but none of its counterparts, unless the description contains a linedoc-map: no change needed: <reason>. Keep the map current when you add, move, or delete a mapped file.- Until the versioned Developer reference exists, put volatile facts in the
existing
docs/content/references/anddocs/content/production/pages. Phase P2 of the docs and skills cleanup moves them into the Developer reference. Those pages stay in English and Simplified Chinese until they move into thereference-*instances.
This is the target layout. Phases P2 and P4 of the docs and skills cleanup build it.
- Each component has its own versioned reference: the Go, Python, Java, TypeScript, and Rust SDKs, the server, cli, web, and the agents packages.
- The left sidebar of each reference has a version selector. It defaults to the latest version.
- Only the current version is edited by hand. Each minor release snapshots it automatically. Older versions change only to fix a factual error or a broken link.
- The site keeps the last 5 versions of each reference.
- When a version leaves the site, redirect each of its URLs to the same page in the latest version. If that page no longer exists, redirect to the component's reference index. Docs URLs stay permanent.
- Each SDK, cli, web, and agents minor version has one reference version.
Patch releases update that minor's reference. Before agents 1.0, each
0.Nagents minor is one version. Until server-zd 1.0, each0.1.Nserver-zd release is one version. From 1.0 on, server-zd is versioned by minor release.
These rules add to .cursor/rules/docs-writing.mdc,
.cursor/rules/docs-i18n.mdc, and .cursor/rules/docs.mdc.
- Concept pages are the Concepts, Components, and Design pattern pages. Use short sentences there. Put one idea in each sentence.
- Concept pages contain no version numbers and no compatibility notes for older servers or SDKs.
- Say each thing once. Link to its single home instead of repeating it.
- Version differences appear only in the Developer reference. Mark each one "Since " or "Changed in ". Use the version of the component the page documents, such as "Since 1.35" for an SDK, cli, or web, and "Since 0.1.17" for server-zd.
- Every changed page outside the Developer reference must be read in a
browser before merge. This includes Intro, Quick start, Concepts,
Components, Design patterns, and Use cases. Run
cd docs && npm start, which builds and serves both locales. Read every such page from top to bottom as a reader, once in English and once in Simplified Chinese. Fix what you find. List the pages you read in the pull request's Testing section. - Every docs change must work on a phone as well as a desktop. This covers content, components, styles, diagrams, and the sidebar. Check each changed page, or each page type a style or component change affects, at a 390px wide viewport. The page must not scroll sideways. Text stays readable without zooming. Wide tables and code scroll inside their own box. Diagrams fit or scroll inside their own box. The docs menu opens and reaches every page. List the phone checks in the pull request's Testing section.
- Every docs page outside the Developer reference ships in English and
Simplified Chinese (
zh-Hans). This includes Intro, Quick start, Concepts, Components, Design patterns, and Use cases. Keep both locales in sync. - The Developer reference is English only. It means the versioned
reference-*docs instances and the generated API references. When a page moves into the Developer reference, it drops its Chinese copy. Redirect the oldzh-HansURL like any other moved page. - Every docs page has a Markdown twin, built by the local docs exporter. Each new MDX component needs an export rule in that exporter. The build fails on a component without one. Phase P1 of the docs and skills cleanup adds the exporter; this rule applies from then on.
Dex is a public, source-available product. Repository files, docs, examples, receipts, comments, and commit messages contain no information about products built on Dex: no product names, deployments, environments, dependency pins, or platform concepts. Describe a requirement in Dex terms, as any Dex application would need it.
The docs site shell (navbar and footer in docs/docusaurus.config.ts and their
zh-Hans translations) may link to company product pages. Content pages must
still not describe downstream products.
- Before changing or reviewing
examples/**, application code snippets in product docs, or documented Flow application patterns, load the installeddex-sdkskill through the host's native skill mechanism and follow its routed references. Use the invocation syntax for the host's Plugin or standalone installation. - Do not load it for pure Server, IDL, or SDK implementation work. When a change includes both an SDK API and examples, current source and API decisions are authoritative; use the skill for application modeling guidance.
- If the skill is unavailable, stop the examples or snippet portion of the change and provide the installation instructions at https://docs.superdurable.io/build-with-ai/dex-developer-skill.
User-facing docs, examples and skills describe Dex concepts and guarantees only. Do not describe server-zd engine internals (apply, commands, partitions, history mechanics) as part of the application model; state the Dex behavior instead. Deployment and operations guidance may name server-zd and its configuration.
Whenever any .proto file changes, run make generated-code from the repository
root and commit every resulting change. Proto-changing PRs must refresh all
checked-in generated code across the server and SDKs. Do not use component-only
codegen targets for these PRs; they leave stale outputs for the next PR.
These files are generated. Never hand-merge them:
- every
Cargo.lockof a hand-written Rust workspace (server-zd,sdk-rust,s3local,examples/rust); - generated code from
make generated-code; docs/src/data/flow-definitions/**/*.json;- the newest server-zd format_compat testdata version (
objects/,state.txt) and its pin ingolden_replay.rs.
When a rebase or merge conflicts in one of them, run
make regenerate-derived. It takes the upstream copy of each conflicted
derived file: the rebase onto commit, or MERGE_HEAD in a merge. It then runs
only the affected generators and stages what they wrote. Pass
ARGS=--dry-run to print the plan or ARGS=--all to run every generator.
It changes nothing while a non-derived path is still unmerged. Older
format_compat versions and their pins are frozen; it refuses changes to them.
It also refuses format_compat regeneration when origin cannot confirm that no
server-zd/v1.* tag exists, unless you pass ARGS=--assume-before-1-0.
The dexzd-simulation regression pins and their README digests cannot be
regenerated. Resolve them with the revert-and-confirm procedure in
server-zd/crates/dexzd-simulation/README.md ("Pinned trajectories").
Dex is in Beta. Preserve wire compatibility and decoding of persisted payloads. Never renumber existing fields or reuse any previously used field number or name. When deleting a field, reserve both its original number and name. Preserve the numbers and names of remaining fields; gaps are intentional. Apply the same rule to deleted enum values. Do not change existing field types, cardinality, or oneof membership incompatibly. Add new fields with unused numbers, then regenerate the entire repository.
Every new or edited .go / .java / .py / .rs / .proto file, Web
.ts / .tsx / .css / .html source, and hand-written OpenAPI YAML under
protos/ must start with the classification header from
script/licenseheaders/. The committed
legacy manifest determines whether a managed file is legacy-only, mixed,
or new. Examples retain their existing MIT or Apache-2.0 headers.
Skip generated trees: **/gen/**, *.pb.go, *_pb.go, *.gen.*.
When creating or modifying such a file, check the top; if the header is missing, add it. From the repo root:
make copyright— safely add or upgrade required headersmake copyright-check— verify classifications, body hashes, and headers
Do not replace legacy headers. Editing a legacy-only file upgrades it to
mixed; files first created after the cutoff use the new header.
Dex is in Beta. Preserve published APIs, SDK source compatibility, configuration files, wire formats, and persisted data. Do not remove or rename published interfaces, change defaults or behavior incompatibly, or reject previously valid configuration without explicit user authorization for that specific breaking change. Prefer additive changes and deprecation with a migration path. Keep compatibility code while supported releases or persisted data require it. Before an authorized breaking change, explain its scope and migration, and add compatibility tests. General permission to refactor or fix a bug does not authorize a breaking change. Protobuf field numbers and names must still never be reused.
In Rust examples and SDK tests under sdk-rust/crates/dex-sdk/tests/integ/,
define every Attribute, Channel, and Stream as a module-level static LazyLock<T>. Do not use a function to construct or return one of these
definitions. Reuse the static directly, or clone its initialized value only
when an owned field is required.
Every Step type in a Flow Definition Graph 2.0 Go source must declare exactly
one // dex:explanation text:"..." directive next to its dex:group. The text
is one sentence that states what the Step does. The analyzer stores it on the
Step node as metadata.explanation for Dex Web v2 Definition.
Do not use del in Python examples merely to mark parameters or local values
as unused. Keep required callback parameters unused instead.
Every application-example RPC handler and explicit RPC name must begin with a
concrete action verb. Use verbs such as get, list, or describe for reads
and domain actions such as send, update, delete, or move for writes. Do
not use noun-only names such as queuedMessages, history, or snapshot, and
do not use placeholders such as exampleRPC.
For every API, interface, class, type, method, function, field, variable, and
constant, prefer a complete and precise domain name over a short name. The name
must communicate its operation and subject at the call site or registration
boundary. Do not use generic names such as Get, Update, Manager, Data,
Handler, or Runtime when a specific domain name is available. Brevity is not
a goal; remove words only when the remaining name is equally precise and
unambiguous. In every language, do not introduce normalize or Normalize
identifiers; name the concrete operation, such as TrimWhitespace,
SortSelections, or CanonicalizeURL.
Directory, package, module, and crate names follow the same rule. Never name
one runtime, common, utils, helpers, or misc, and never use one of
these words as a package or module suffix, such as dex-agents-cli-runtime.
Name what the code does or which product part it serves, such as core under
its product directory. Refuse such a name in a proposal and choose a specific
one instead. Existing directories with these names keep them until a change
reworks their code.
The Go SDK registers a Flow or Step by its Go type name without the package.
Name every Go Flow type after its domain, such as ApprovalFlow or
ReplyFlow; never name it just Flow. Flow types must be unique within one
Registry, and Step types must be unique within one Flow. A generic Flow or Step
type must implement GetFlowType or GetStepType. Do not add an override that
returns the default name.
Design SDK APIs so application code reads like a natural phrase in its host
language. Judge names and ownership at the call site, including nested calls.
Prefer domain-noun factories such as
Wait.allOf(Timer.byDuration(duration)) over awkward ownership such as
Wait.allOf(Wait.timer(duration)). Preserve each language's idioms rather than
forcing identical syntax across SDKs.
Constructor injection only. Never add SetXyz, Inject*, Wire*, or exported
mutable fields for wiring components after New*() returns. Fix bootstrap
ordering instead.
A closure that captures 3+ outer variables, mutates outer state, is called from more than one site, or outlives a single statement → lift it into a method on a struct with explicit fields.
Fine: one-shot callbacks (sort.Slice, errgroup.Go, defer), tiny pure
transforms, IIFEs for scoping.
For comments you add, write the fewest needed. Do not restate what the code or a well-named identifier already says. Explain only a non-obvious why. When in doubt, improve the name instead.
Every new comment block you add (a contiguous group of // lines) must be fewer
than 20 words. If a longer new comment is necessary, ask the user first.
An explicit user request for a paragraph, detailed explanation, or longer comment authorizes exceeding this limit without asking again.
These rules do not apply to comments that existed before the current change. Never edit an existing comment merely to simplify, shorten, rephrase, fix grammar, or satisfy the word limit.
Comments documenting function or method return values are also exempt from the 20-word limit and should describe complete return semantics.
Public SDK API documentation is exempt from the short-comment and no-obvious-comment rules. Document every public or exported SDK type, interface, class, method, function, constructor, field, constant, enum value, struct member, annotation element, and equivalent language construct. Do not add API documentation to non-public or generated declarations.
Keep documentation direct. Put one idea in each sentence. Split behavior, constraints, and rationale into separate sentences.
Write each API document from the application developer's perspective. Start with a one-sentence summary, then explain when and how the API is used. Give a complete language-native example for each related API family; overloads and simple accessors may refer to their enclosing type's example instead of repeating it. Explain defaults, units, lifecycle, blocking or concurrency behavior, nullability, ownership, side effects, errors, and other tricky semantics whenever users must understand them. Finish with language-native documentation for every type parameter, input, output, and user-visible error. Do not impose a word limit; prefer a complete paragraph over a terse fragment.
- Per-API details live in the SDK doc comments. Docs and skills link to the generated API reference instead of restating them.
- When a release adds an API or changes its behavior, its doc comment says so
in the language's idiom. Use the first release of that SDK that ships the
change. When that number is not known yet, follow Release Versions:
the next minor after the newest Dex release tag for a new or changed
API, and the next patch for any other behavior change.
- Go and Rust: a sentence such as "Since 1.NN.0." or "Changed in 1.NN.0: ."
- Python:
.. versionadded:: 1.NN.0or.. versionchanged:: 1.NN.0, followed by what changed. - Java and TypeScript: an
@since 1.NN.0tag for a new API. For a behavior change, add the sentence "Changed in 1.NN.0: ."
Use the exact nouns exposed by the API or protocol. Do not invent one umbrella term for distinct identifiers; say Flow type, Step type, RPC name, Attribute name, Channel name, or map instance as appropriate. When a documented limitation has a practical workaround, explain it immediately and include a language-native example when the solution is not obvious.
Existing comments are user-owned text. Preserve them verbatim and move them with the code they describe. If a code change makes a comment factually stale, change only the outdated references while retaining every original detail and meaning. Never delete, shorten, or rewrite an existing comment for style.
In the same file, a callee always appears below its caller. High-level orchestration at the top, leaf helpers at the bottom. Preferred order for a struct-based file:
- Type declaration
- Constructor (
new<Type>) - Constructor's own helpers
- Main entry method (
Run,Serve,Handle,Process) - Per-event/step handlers (in dispatch order)
- Sub-handlers
- Mutators / state-changing helpers
- Encoders / converters / pure transforms
- Tiny accessors at the very bottom
Exceptions: generated code (leave as-is); tightly grouped methods on different subsystems in one file (keep the cluster intact).
Keep a struct's methods in one primary file. Do not split its method set across feature-specific files.
Use ptr.Any(value) instead of a throwaway local variable taken by address.
Import: github.com/superdurable/dex/sdk-go/dex/ptr. Use explicit types for
numerics: ptr.Any(int64(0)), ptr.Any(int32(1)).
Do not use ptr.Any when the pointer must alias an existing named variable that
is also read or mutated elsewhere.
Do not call proto.Clone or deep-copy messages "just in case". Passing a
message transfers ownership unless the API says otherwise. Workflow inputs,
signals, and activity payloads are freshly deserialized and single-threaded.
Prefer capturing a small id/string or building a new tiny value over cloning.
Copy only when an algorithm must mutate a distinct shared value in place.
Before running go build -o <path> or adding a new main package, add the
output path to both .gitignore and .dockerignore. Use exact paths, not
overly broad globs. Delete stray uncommitted binaries.
After code changes, run tests via the Makefile — not bare go test for full
suites:
make -C server-zd/integ compilemake -C server-zd/integ zdIntegTestsmake -C server-zd/integ zdAttributeSyncIntegTestsfor PostgreSQL integration.
Always tee output: make -C server-zd/integ compile 2>&1 | tee /tmp/test-<scope>.log
Fix all failures before moving on. If stuck after multiple attempts, pause and ask the user with: (1) the failure, (2) what you tried, (3) where you're blocked.
Never skip/gate/early-return around a failing assertion just to green the suite
(e.g. t.Skip for a known flake) unless
the user explicitly asks. On failure: add targeted logs, inspect server-zd
logs (plus relevant API output), find the root cause, then fix
product code or the expectation. Backend-specific skips need a proven platform
limit and user agreement.
Every config struct field must have a Go doc comment:
- Always state the default value.
- Explain what it means/controls if non-obvious.
- State immutability, relationships, valid ranges if constrained.
- Add a concrete example if tricky.
For address fields, explain protocol served, who connects, and bind-vs-advertise relationship.
Use the package's declared name. Only alias when:
- Two packages share the same name in one file.
- The default name is misleading or ambiguous.
- An established repo convention applies (e.g.
dexpbfor generated OpenAPI).
Do not invent aliases like servermetrics or mongostore.
Required dependencies must panic or log.Fatal if nil — fail fast at startup.
Do not add if x == nil { return nil } guards that silently swallow bugs. Only
add nil checks when nil is a valid, expected value.
Every returned error must be handled — returned, logged, or explicitly acted on.
Never _ = f(), x, _ := f(), or if err == nil { use(x) } with no else.
If you genuinely must ignore one, leave a code comment explaining why and
call it out in review.
Trusted vs untrusted decides fail-fast vs graceful:
- Trusted (our own store rows, server-minted ids, invariants we control): a
violated invariant is a bug — fail fast. Use a
Must*helper rather than silently ignoring. Better still, thread the typed value end-to-end. - Untrusted (HTTP request fields, SDK/worker payloads, anything a client can
set): must handle gracefully — validate and return an input
error; never
Must*/panic.
Variables (struct fields, parameters, locals) must use descriptive names. Method
receivers are exempt (Go convention: func (w *Worker) ...).
Allowed short non-receiver names: i j k n err ctx ok t mu wg id r w ch
Boolean variables and constants, and methods returning booleans, must use names
that clearly signal boolean semantics, such as isXxx, hasXxx, canXxx,
shouldXxx, or supportsXxx. Follow each language's capitalization conventions.
Do not introduce NormalizeXyz identifiers. Name the concrete operations, such
as ValidateAndSortSelections, TrimWhitespace, or CanonicalizeURL.
When a concept already has a repository or public API term, reuse that exact term in identifiers. Do not invent a synonym for an existing term such as instance, name, message, or definition.
Prefer explicit domain naming, thin public APIs, apply-style options, and Def/Impl layering. Keep design docs and examples in sync with API changes.
- Factory/constructor names include the domain noun:
InitialAttribute,InitialAttributeMapValue— not bareInitial/InitialMapValue. - Schema erasure interfaces use
*Def(AttributeDef,ChannelDef,InitialAttributeDef,StepDef). - Unexported concrete implementations use
*Impl(e.g.conditionImpl), not*Value, unless the type is a true value object. - Identity types use
*ID(StepExecutionID,TimerID), not*Ref. - Optional fields that may be omitted use pointers (
ExecutionNumber *int32). - Names describe real semantics, not a misleading action. Prefer
SkipWaitImmediatelyoverExecuteImmediatelywhen the value means “skip waiting,” not “run Execute.” - When an entry method needs a recursive or stateful helper (extra args such as
a cycle-tracking set), name the helper
doXxxfor the same verb phrase:validateStepOptions→doValidateStepOptions. Keep the entry thin; put the real work indoXxx.
- Omit parameters the SDK can default (e.g. client methods target the current
run; do not take
runIDunless an operation truly needs a specific run). - Do not wrap a few flat inputs in an Options struct. Prefer direct parameters
(
SearchFlows(ctx, query, pageSize, nextPageToken)). - Prefer batch APIs alongside or instead of thin single-item variants when that matches product shape; drop unused singles (e.g. public Delete helpers) rather than keeping dead surface area.
- Optional overrides are pointer fields on options (
RequestID *string), with SDK-generated defaults when nil.
Use apply methods on the option interface — never an empty marker plus
switch option.(type):
type ConditionOption interface {
applyCondition(*conditionImpl)
}
func applyConditionOptions(condition *conditionImpl, options []ConditionOption) {
for _, option := range options {
option.applyCondition(condition)
}
}Same pattern as AttributeOption.applyAttribute and
StepMoveOption.applyStepMovement.
Def interfaces exist for SDK-internal schema erasure. Seal them with
unexported methods only. Application-facing names stay on concrete typed values
(e.g. Attribute[T].AttributeName()), not on the Def interface.
When changing public SDK types or Client signatures, update in the same change:
docs/design/plan/go-sdk-rewrite.md,
sdk-go/README.md, examples, and compile-contract tests.
Use require.Eventually or a polling loop. time.Sleep is only acceptable
inside the system under test itself.
Generate unique workflow IDs (and namespaces when applicable) per test when sharing a server-zd instance. Never rely on leftover state from another test.
These rules apply to all hand-written Rust workspaces and standalone packages.
Run make rust-rules-check and each package's formatting, Clippy, and test targets.
The checker discovers Cargo manifests automatically through Cargo metadata.
| Rule | Convention | Enforcement |
|---|---|---|
| R1 | Non-test Rust files contain at most 1000 lines, excluding the license header. | Rust rules checker; existing oversized files use a shrink-only baseline. |
| R2 | A module directory contains at most 10 non-test Rust files and 10 submodules. Split by responsibility. | Rust rules checker; existing oversized directories use the same baseline. |
| R3 | Every crate has a README with an opening Purpose sentence of at most 30 words. Start its lib.rs and main.rs crate documentation with the same sentence. | Rust rules checker. |
| R4 | Use pub(crate) for internal items and pub only for public APIs, including in the SDK. | rustc unreachable_pub. |
| R5 | Test-only public items end in _for_test and require cfg(test) or the test-util feature. | Rust rules checker verifies the configuration of suffixed items; review verifies naming. |
| R6 | Register every crate in script/rust-crate-layers.json. Depend only on crates in the same or a lower layer. | Rust rules checker using Cargo metadata. |
| R7 | Each executable has one typed configuration. Read environment settings only in configuration modules or tests. Configuration modules are config, *_config, and *_options. | Rust rules checker verifies std::env::var and std::env::var_os locations; review verifies typed ownership. |
| R8 | Use complete domain names. Avoid utils, helpers, common, and misc modules and generic Manager, Handler, Data, Info, or Runtime types. | Rust rules checker rejects generic module filenames; review checks type names. |
| R9 | Represent closed sets with enums rather than repeated strings. Across crates, use non-exhaustive enums or newtypes. | Clippy match_wildcard_for_single_variants and review. |
| R10 | Handle every error. Do not discard fallible results with let _ or .ok(); explain intentional ignores. | rustc unused_must_use and Clippy let_underscore_must_use, let_underscore_future, and unused_result_ok; review. |
| R11 | Libraries use typed errors, such as thiserror. Generic errors such as anyhow belong only at binary boundaries. Include the operation and object in errors. | Rust rules checker rejects non-development anyhow dependencies in library crates; review checks error context. |
| R12 | Panic on programming errors with expect explaining why failure is impossible. Production code must not use bare unwrap. Never use todo!, unimplemented!, or dbg!. | Clippy unwrap_used, todo, unimplemented, and dbg_macro; Rust rules checker rejects empty expect explanations. Tests may unwrap. |
| R13 | Libraries log with tracing. Keep messages constant and values in fields. Examples, CLI crates, and build scripts may print through registered crate lint overrides. | Clippy print_stdout and print_stderr; review checks structured messages. |
| R14 | Every background task, connection, and child process has an owner and cancellation path. Bind tokio::spawn JoinHandles; never use let _ to discard them. Explain deliberate detachment. | Rust rules checker verifies binding; review checks lifecycle and cancellation. |
| R15 | Forbid unsafe outside registered FFI crates. FFI crates use unsafe_code = "deny" and an expect with a safety reason at each unsafe operation. | rustc unsafe_code; Rust rules checker verifies registered FFI lint tables. |
| R16 | Suppress lints with #[expect(lint, reason = "...")], never #[allow]. Remove expectations when the lint no longer applies. | Clippy allow_attributes and allow_attributes_without_reason; rustc unfulfilled_lint_expectations. |
| R17 | Never hand-edit generated code. | make generated-code-check. |
| R18 | Preserve existing comments. New comments explain non-obvious reasons and contain fewer than 20 words per block. Public SDK API documentation follows the detailed SDK documentation rules. | SDK missing_docs and review. |
| R19 | Format Rust with rustfmt. | cargo fmt --all --check. |
Use the frozen workspace Rust and Clippy lint tables in every workspace. Members
inherit with [lints] workspace = true. Registered FFI and printing crates copy
the entire table, changing only unsafe_code to deny for FFI and print_stdout /
print_stderr to allow for printing crates. The checker rejects other differences.
Keep clippy.toml test settings identical across workspaces. CI runs Clippy with
--all-targets -- -D warnings.
The checker uses one generated-code predicate: opening Code generated or @generated markers, or a gen/ or OUT_DIR path. Generated and test source files are excluded from structural counts. Inline test modules still count toward a production file's size. The baseline may only shrink against origin/main: never add an entry or increase its ceiling. Remove an entry as soon as the file or directory meets its limit; stale entries fail the check.
dexzd-applyis a deterministic state machine. It never reads clocks, random numbers, configuration, the environment, files, or the network, and it never iterates a hash map. Values that change an apply result come from the command or from persisted state. Itsclippy.tomlbans the common offenders; keep that file's frozen test settings identical to the workspace.- A change to the result of applying an already committed command needs a new
behavior version with a named predicate. Never change the behavior of an
existing version. This rule starts with server-zd 1.0.0, the first
server-zd/v1.*release tag. Until then, server-zd stays at behavior version 1, an apply-result change needs no new version, and no work goes into version barriers or raising them. - Persistent formats (
server-zd/proto/dexzd/, the RocksDB key layout, the object storage layout, and snapshots) follow the protobuf compatibility rules: additive only; never renumber or reuse field numbers or names. - Generated protobuf maps use
BTreeMap. - Local tests use the in-process s3local or the in-memory object store, never MinIO.
These rules restate the applicable Server Go rules for server-zd/**. The
Rust table (R1–R19) still applies.
- File ordering: a callee appears below its caller. Order a file as type, constructor, entry method, handlers in dispatch order, sub-handlers, state-changing helpers, converters, then tiny accessors.
- One primary file per type: keep a type's inherent methods in one file. Do not split them across feature-specific files.
- Required dependencies: take each one as a non-
Optionconstructor parameter. Never wire a dependency after construction with a setter. - Bootstrap order: fix construction order instead of post-construction wiring.
- Config injection: a component that needs tunables receives its config
section struct, not individual tunables or the whole server configuration.
See R7.
dexzd-applystays configuration-free. - Optional values: use
Optiononly whenNoneis a valid state. Never silently return on an impossibleNone. - Config field docs: every config field has a doc comment stating its default and meaning. Address fields state the protocol, the connecting party, and bind versus advertise. These docs and return-value docs are exempt from R18's word limit and its non-obvious-reasons rule.
- API errors: map every request failure to a gRPC status plus a Dex
ErrorSubStatus. Server startup or shutdown may return a bareUNAVAILABLE. Bad client or Worker input is a client error, such asINVALID_ARGUMENT,NOT_FOUND, orFAILED_PRECONDITION. Infrastructure failures areINTERNALorUNAVAILABLE. - Never ignore errors: see R10.
- Trusted versus untrusted values: store rows, server-minted IDs, and
controlled invariants are trusted and may use
expectwith a reason. Request fields and Worker payloads are untrusted. Validate them with an error-returning helper; they never reachexpect,unwrap, orpanic!. - No defensive clones: never
clone()"just in case". Move ownership or borrow. Clone only when an algorithm needs a distinct value or a task needs shared ownership, such as anArchandle. - Crate names: use each crate's declared name. Alias with
asonly for a collision or a misleading name. - Descriptive names: use descriptive variable names. Only
i j k n err ctx ok t id r w tx rxand closure parameters in short iterator chains may be one or two letters. - Boolean names: boolean variables, fields, constants, and functions returning
booluse predicates such asis_,has_,can_,should_, orsupports_. - Existing terms: reuse the repository or public API term for a concept. Do not invent a synonym.
- Stateful closures: lift a closure into a method when it captures three or more values, mutates outer state, has several call sites, or outlives one statement.
- No casual test skips: never
#[ignore], gate, or early-return around a failing test without the user's agreement. Find the root cause and fix the code or the expectation. - Test isolation: give each test unique Flow IDs, namespaces, and temporary directories so tests run in parallel.
- Async tests: wait with polling or paused tokio time. Use a real sleep only inside the behavior under test.