9.0 KiB
AGENTS.md
Guidance for AI coding agents. Follow exactly; overrides model defaults. CLAUDE.md is a symlink.
Verification Checklist
After making Rust code changes, always run in order:
cargo check --workspace
cargo test --workspace --no-fail-fast --all-features
cargo doc --workspace --no-deps
cargo clippy --workspace --all-targets --fix --allow-dirty --allow-staged --no-deps # Auto-fixes lint suggestions
cargo +nightly fmt --all
- If
cargo check,cargo test, orcargo docfails: Fix and re-run. - If
cargo clippyorcargo +nightly fmtchanges files: Do not re-run the checklist. This is expected and intentional. - If
cargo clippyfails: Fix and re-run.
Proofread new documentation and comments against the tone and style rules before declaring done.
Architecture
OpenAPI code generator for polymorphic specs (allOf/oneOf/anyOf): Parse → IR → Codegen.
Workspace Crates
| Crate | Purpose |
|---|---|
| ploidy | CLI entrypoint (keep thin) |
| ploidy-core | Language-agnostic IR and type graph |
| ploidy-codegen-rust | AST-based Rust code generator (syn/quote) |
| ploidy-pointer | RFC 6901 JSON Pointer for $ref resolution |
| ploidy-pointer-derive | #[derive(JsonPointee, JsonPointerTarget)] proc-macros |
| ploidy-util | Runtime support for generated clients |
Key Abstractions
Arena: Bump allocator that owns long-lived data. Types hold&Arena-allocated references, slices, and strings, making them cheaply copyable.Spec: Raw IR data (schemas, operations). Created bySpec::from_doc(&arena, &doc).RawGraph/CookedGraph: WrapSpecwith type graph for traversal, transitive closure, and cycle detection. Created byRawGraph::new(&arena, &spec)thenraw.cook(). UseRawGraphfor transformations;CookedGraphfor traversal and view types.- View types (e.g.,
StructView,TaggedView,EnumView,OperationView): Graph-aware wrappers for traversal, inline expansion, usage queries, boxing and derive decisions. - Inline type paths: Anonymous schemas get semantic paths like
Type/Field/MapValuefor stable naming.
Polymorphic Type Mapping
| OpenAPI | IR Type | Rust Output |
|---|---|---|
allOf |
Struct with inherited fields |
Single struct with linearized ancestor fields |
oneOf + discriminator |
Tagged |
#[serde(tag = "...")] enum |
oneOf without discriminator |
Untagged |
#[serde(untagged)] enum |
anyOf |
Struct with flattened optionals |
Struct with optional flattened fields |
Coding Style
Requirements, not suggestions. When rules conflict: consistency wins, more specific rules apply, ask if genuinely unclear.
Helper Functions
Do not introduce helper functions unless the user explicitly approves that specific helper in the current task.
This is intentional.
Do not add private support functions, test helpers, fixture builders, single-purpose convenience functions, local extraction functions, or functions whose purpose is to organize, shorten, deduplicate, or name a block of logic. Do not infer approval from context. Keep new behavior inline in existing functions or methods unless the user explicitly authorizes the helper before it is written.
Type Design
| Pattern | Rule |
|---|---|
| Context objects | Bundle related data in structs instead of free functions with many params |
| Newtypes | Use to enforce invariants (e.g., SchemaIdent(String) for uniquified names) |
| Enums with data | Carry data in variants directly (e.g., SpecType, GraphType) |
| Symmetry | Similar types follow similar patterns, even if slightly redundant |
Ownership and Lifetimes
// ✅ Borrow from source
struct MyView<'a> {
name: &'a str,
items: &'a [Item],
}
// ❌ Unnecessary allocation
struct MyView {
name: String,
items: Vec<Item>,
}
- Use semantic names (
'viewfor views,'graphfor graphs) when multiple lifetimes coexist;'ais fine for single-lifetime cases. - Never elide lifetimes that distinguish borrowed sources.
Data Structures
IndexMapwhere insertion order matters.FxHash{Map, Set}instead ofstd::collections::Hash{Map, Set}(HashDoS not a concern)..collect_vec()(importitertools) instead oflet v: Vec<_> = … .collect()or.collect::<Vec<_>>().
Documentation (/// and //!)
Tone:
- Match Rust stdlib's tone: precise, terse, declarative. No filler, no hedging, no marketing.
- Proofread with Strunk & White's Elements of Style: omit needless words, use active voice, prefer positive statements.
Style rules:
- Complete sentences, indicative mood ("Returns", not "Return"), backticks for code items.
- Describe args/returns in prose, never separate sections.
- Document interface (what/why), not implementation details (how).
- Wrap at 80 chars.
// ✅ Indicative mood, inline prose
/// Creates and returns a representation of a feature-gated `impl Client` block
/// for a resource, with all its operations.
pub fn new(resource: &'a str, operations: &'a [OperationView<'a>]) -> Self { ... }
// ❌ Imperative mood, separate sections
/// Create a representation of a feature-gated `impl Client` block.
///
/// # Arguments
/// - resource (string): The resource name
/// - operations (list): The operations
///
/// # Returns
/// The representation
pub fn new(resource: &'a str, operations: &'a [OperationView<'a>]) -> Self { ... }
Comments (//)
- Only for non-obvious logic
- Always complete sentences with periods; backticks for code items
// MARK:for sections (under 50 chars, no period)
// ✅ Explains why, complete sentence, backticks
// Skip `f.discriminator`; it's handled separately in tagged unions.
if f.discriminator() { continue; }
// ❌ Restates code, sentence fragment, no backticks around `f`
// Check if f is discriminator
if f.discriminator() { continue; }
Strings
- Raw strings (
r#"..."#) for strings with quotes .to_owned()for&str→String.to_string()only when formatting (numbers,Displaytypes)
Imports
- Always add
useimports; never use inline qualified paths - Rename conflicting imports:
use std::{fmt::Result as FmtResult, io::Result as IoResult} - Ordered groups (blank lines between):
std::→ external crates (alphabetical) →crate::→super:: - No globs except re-exports in
mod.rs,use super::*in tests
Miscellaneous
#[inline]for smallpubfunctionspub(in crate::path)for internal constructors- Define module-level error types with
thiserror; usemiettefor user-facing diagnostics panic!/unwrap()for violated internal invariants- Justify lint suppressions with comments
Testing
- Naming:
test_<behavior>_<condition>, grouped with// MARK:comments. - Use existing test helpers; never create new ones without asking. Inline all fixtures directly.
- YAML fixtures: Always use
Document::from_yaml(indoc::indoc! { ... })for OpenAPI documents. Never constructDocumentdirectly. - Assertions: Prefer one structural pattern match with
assert_matches!over multi-step match/let-elsechains. Only uselet-elsewhen the bound variable is needed for subsequent method calls. Include actual value inlet-elsepanic messages:panic!("expected X; got{ty:?}"). - Throwaway tests: When behavior is unclear, write a quick test to prove it rather than theorizing. Delete or convert once done.
- Debugging
synnode mismatches: Whenassert_eq!(actual, expected)fails, useprintln!("{}", actual.to_token_stream())to compare expectations.
Crate-Specific Rules
- ploidy-core: Language-agnostic IR, but Rust-flavored concepts are fine when they simplify codegen (e.g.,
Required,needs_box()). The boundary is: core must not depend on codegen types (syn,quote, token streams). Use view types for graph queries. Tests insrc/**/tests/*.rs. - ploidy-codegen-rust: All types implement
ToTokens. Usequote!for tokens, never string-format. Tests must compare AST nodes usingparse_quote!, never string matching (e.g.,tokens.contains("...")). - ploidy-pointer: Follows RFC 6901. Tests in
src/lib.rsandtests/. Simpler assertions OK. - ploidy-pointer-derive: Proc-macro constraints apply. Test via
ploidy-pointer/tests/. - ploidy-util: Keep minimal. All data types must impl
Serialize/Deserialize. Key types:AbsentOr<T>,QuerySerializer,UnixSeconds.
Process
- Dependencies: Prefer
[workspace.dependencies]. Justify new deps. - Breaking changes: Do not preserve backward compatibility unless the user explicitly asks for it.
- Design: Push back or propose alternatives. Keep changes modular for partial reverts.
- Reverts: Don't
git checkout --. Manually restore to avoid data loss. - Ask for help when: requirements ambiguous, multiple valid approaches, tests fail for unclear reasons, scope larger than expected, new workspace crate needed, or approach seems wrong.