Skip to content

Latest commit

 

History

History
916 lines (730 loc) · 47.7 KB

AGENTS.md

File metadata and controls

916 lines (730 loc) · 47.7 KB

Dex — Codex Instructions

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.

Plan Mode

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.md to create or update
  • ## UI/UX — usually N/A: no in-repo web UI

Tests

  • 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".

Documentation

  • Product docs live in docs/ (start at docs/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 when examples/rust has the same sample. Do not use per-language headings (### Java) or stacked fenced blocks. bash / text fences 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 matching zh-Hans pages), do not use inline backticks for API names, methods, types, or identifiers. Use bold instead (for example WaitFor, StepDecision). Fenced code blocks and bash / text fences 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.json for 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/, run cd 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 under docs/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 under examples/ by adding the source to docs/scripts/generate-flow-definitions.sh. Do not add Step graph images or hand-written definitions. Every Step in such a source declares one dex:group and one dex:explanation directive.

UI/UX

  • No in-repo web UI unless the change adds one.

Code Quality Rules

Commit Every Changing Turn

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.

Pull Request Descriptions

Start every pull request description with these sections, in order:

  1. ## Summary: what the PR changes.
  2. ## 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.

Issue Labels

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), or type:tech-debt.
  • Exactly one priority: P0 (blocks a release or users now), P1 (do next), P2 (normal), or P3 (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, or area:protos.
  • Exactly one size: size:S (a day or less), size:M (a few days), or size:L (a week or more, or needs design).
  • Add codex-ready when the issue is self-contained and needs no decision. Add needs-decision when 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.

Branch From Latest Main

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).

Agent Rule Synchronization

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.

Skills Change Only When Stable Rules or Concepts Change

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.

Change Logs

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.

Release Versions

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".

Docs Information Placement

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.yaml maps 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 line doc-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/ and docs/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 the reference-* instances.

Developer Reference Versions

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.N agents minor is one version. Until server-zd 1.0, each 0.1.N server-zd release is one version. From 1.0 on, server-zd is versioned by minor release.

Docs Writing, Languages, and Markdown Export

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 old zh-Hans URL 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.

No Downstream Product Information

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.

Dex SDK Skill Routing

  • Before changing or reviewing examples/**, application code snippets in product docs, or documented Flow application patterns, load the installed dex-sdk skill 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 Concept Boundary

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.

Regenerate the Entire Repository After Proto Changes

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.

Derived Files

These files are generated. Never hand-merge them:

  • every Cargo.lock of 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 in golden_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").

Preserve Protobuf Compatibility

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.

License Headers

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 headers
  • make 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.

Beta Compatibility

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.

Rust Workflow Schema Definitions

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.

FDG 2.0 Step Explanations

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.

Python Examples

Do not use del in Python examples merely to mark parameters or local values as unused. Keep required callback parameters unused instead.

RPC Names Start With Verbs

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.

Prefer Complete, Precise Names

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.

Go Flow and Step Type Names

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.

Fluent SDK Call Sites

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.

No Setter Injection

Constructor injection only. Never add SetXyz, Inject*, Wire*, or exported mutable fields for wiring components after New*() returns. Fix bootstrap ordering instead.

No Stateful Closures — Use Methods on Structs

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.

No Obvious Comments — New Comments Only

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.

Short New Comments — Under 20 Words

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.

Detailed Public SDK API Documentation

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.0 or .. versionchanged:: 1.NN.0, followed by what changed.
    • Java and TypeScript: an @since 1.NN.0 tag 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.

Preserve Comments During Refactoring

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.

Top-Down File Ordering (Go files)

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:

  1. Type declaration
  2. Constructor (new<Type>)
  3. Constructor's own helpers
  4. Main entry method (Run, Serve, Handle, Process)
  5. Per-event/step handlers (in dispatch order)
  6. Sub-handlers
  7. Mutators / state-changing helpers
  8. Encoders / converters / pure transforms
  9. 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.

ptr.Any(...) for Pointer Literals (Go)

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.

No Defensive Cloning (Go)

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.

Update Ignore Files When Producing Binaries

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.

Run Tests After Every Change

After code changes, run tests via the Makefile — not bare go test for full suites:

  • make -C server-zd/integ compile
  • make -C server-zd/integ zdIntegTests
  • make -C server-zd/integ zdAttributeSyncIntegTests for 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.

Do Not Casually Skip Failing Tests

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.

Go-Specific Rules

Config Field Comments

Every config struct field must have a Go doc comment:

  1. Always state the default value.
  2. Explain what it means/controls if non-obvious.
  3. State immutability, relationships, valid ranges if constrained.
  4. Add a concrete example if tricky.

For address fields, explain protocol served, who connects, and bind-vs-advertise relationship.

Go Package Aliases

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. dexpb for generated OpenAPI).

Do not invent aliases like servermetrics or mongostore.

No Unnecessary Nil Checks

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.

Never Silently Ignore Errors

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.

Naming — No 1-2 Letter Variable Names

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 Names Are Predicates

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.

No NormalizeXyz Names

Do not introduce NormalizeXyz identifiers. Name the concrete operations, such as ValidateAndSortSelections, TrimWhitespace, or CanonicalizeURL.

Reuse Existing Domain Terms

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.

Go SDK Conventions (sdk-go/)

Prefer explicit domain naming, thin public APIs, apply-style options, and Def/Impl layering. Keep design docs and examples in sync with API changes.

Naming

  • Factory/constructor names include the domain noun: InitialAttribute, InitialAttributeMapValue — not bare Initial / 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 SkipWaitImmediately over ExecuteImmediately when 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 doXxx for the same verb phrase: validateStepOptions → doValidateStepOptions. Keep the entry thin; put the real work in doXxx.

API shape

  • Omit parameters the SDK can default (e.g. client methods target the current run; do not take runID unless 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.

Functional options

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.

Sealed Def interfaces

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.

Documentation

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.

Test Isolation Rules

No time.Sleep for Async Convergence

Use require.Eventually or a polling loop. time.Sleep is only acceptable inside the system under test itself.

Unique IDs Per Test

Generate unique workflow IDs (and namespaces when applicable) per test when sharing a server-zd instance. Never rely on leftover state from another test.

Rust Code Conventions

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.

server-zd Engine Rules (server-zd/**)

  • dexzd-apply is 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. Its clippy.toml bans 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.

server-zd Rust conventions ported from the Go rules

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-Option constructor 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-apply stays configuration-free.
  • Optional values: use Option only when None is a valid state. Never silently return on an impossible None.
  • 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 bare UNAVAILABLE. Bad client or Worker input is a client error, such as INVALID_ARGUMENT, NOT_FOUND, or FAILED_PRECONDITION. Infrastructure failures are INTERNAL or UNAVAILABLE.
  • Never ignore errors: see R10.
  • Trusted versus untrusted values: store rows, server-minted IDs, and controlled invariants are trusted and may use expect with a reason. Request fields and Worker payloads are untrusted. Validate them with an error-returning helper; they never reach expect, unwrap, or panic!.
  • 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 an Arc handle.
  • Crate names: use each crate's declared name. Alias with as only 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 rx and closure parameters in short iterator chains may be one or two letters.
  • Boolean names: boolean variables, fields, constants, and functions returning bool use predicates such as is_, has_, can_, should_, or supports_.
  • 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.