Separate generated from handwritten code: generated/ subfolders + gitignore #96
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_rpc#96
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
Feedback from a contributor reviewing
hero_service: it's hard to tell at a glance what's code-generated vs what a human wrote. Today the generator scatters*_generated.rsfiles into the samesrc/<domain>/directory as handwritten code (rpc.rs,mod.rs, etc.). All of it gets committed to git, so:The fix is to physically separate generated artifacts and gitignore them so the working tree only shows what humans own.
Solution
Folder layout
Generated files move into a dedicated
generated/subfolder under each consumer. Example forhero_service:Note: the
_generated.rssuffix becomes redundant once the folder name carries the signal. Drop it —generated/types.rsreads cleanly.Gitignore strategy
Per-crate
.gitignoreentries the scaffolder emits + the contributor never edits:Repo-root
.gitignore(also scaffolded):Exact wildcards to be tuned during implementation — the principle is: anything code-emitted is ignored; anything scaffolded once and edited by humans is committed.
What stays committed (do not gitignore)
service.tomlCargo.toml, workspaceCargo.lockschemas/**/*.oschema(the input to codegen)crates/<name>_server/src/<domain>/handlers.rs(the preserved trait impl)crates/<name>_admin/**(admin UI is hand-written)crates/<name>/src/<domain>/mod.rs— scaffolded once, declarespub mod generated;, owned by the contributor afterwardsdk/js/package.json,sdk/python/pyproject.toml, README files, etc.docs/other than the codegen-emitted openrpc files (the directory must still exist in tree to satisfy crateinclude_str!paths if any — verify)mod.rsstrategyThe parent
mod.rs(scaffolded once) declares the generated submodule + handles any re-exports:The
generated/mod.rsis itself codegen-emitted and re-exports its siblings. Scaffolder writes the parentmod.rsonce; the generator writes everything insidegenerated/on every build.What to do
crates/generator/src/generate/*.rsmodules: rust_types, rust_server, rust_rpc, openrpc, js, rhai, python). Tag each: "generated → moves togenerated/" vs "scaffolded once → stays where it is."generated/subfolder. Drop the_generated.rssuffix from the resulting filename since the folder name carries the signal.mod.rswithpub mod generated; pub use generated::*;.gitignoreignoringgenerated/.gitignoreignoringdocs/openrpc.json+docs/*/openrpc.json+sdk/rust/src/generated/+sdk/rhai/src/generated/+sdk/js/src/+sdk/python/<package>/hero_servicetemplate. Regenerate it so the contributor can clone it and immediately see the new layout. (git statusaftercargo buildshould be clean — every generated file is gitignored.)recipe_serverexample. Same thing — in-tree reference matches the new shape.hero_service_scaffold.mdskill so it documents the new layout + gitignore convention.Acceptance
git statusaftercargo buildis empty (every generated file is gitignored).crates/<name>/src/<domain>/contains at most:mod.rs+ scaffolded human files + thegenerated/subfolder. No mixed-purpose dir.hero_servicerepo on Forgejo can tell what's code vs cruft within 30 seconds.cargo build --workspaceclean on bothrecipe_serverandhero_serviceafter regen.lab infocheckclean.hero_service_scaffold.mdreflects the new layout.Out of scope
generated/was actually generated by the generator (cute idea, lots of friction).cargo gen/ standalone codegen step (we're keeping codegen insidecargo buildviabuild.rs)._generated.rsfiles in repos other thanhero_service+recipe_server— those services need migration but it's a sweep in hero_rpc#90 phase 3, not this issue.Related
hero_servicetemplate's readability._admin+_webthat drive the generated SDK end-to-end #98Design proposal — request for sign-off before implementation
Working in worktree
issue-96-generated-foldersoffdevelopment. Audited the seven generator modules (crates/generator/src/generate/{rust_types,rust_server,rust_rpc,openrpc,js,rhai,python}.rs) plus the build-script scaffolders (crates/generator/src/build/emit/{domain,rust_server,rust_rpc2,python_sdk}.rs). Below is the exact final shape per emitter and the gitignore wording. Naming follows the issue body:generated/subfolder + drop_generatedsuffix everywhere.1. Folder layout per emitter
A. Consumer crate (core types) —
crates/<name>/src/<domain>/Was (emitter:
generate/rust_types.rs):Now:
The current
types.rsinclude!()wrapper trick is dropped — custom impls live in a separate handwrittenextensions.rs(or inline in parentmod.rs) sitting besidegenerated/. Recipe_server's existingtypes.rsonly has commented-out example impls, so deleting it loses nothing.B. Server crate —
crates/<name>_server/src/<domain>/Was (emitters:
generate/rust_server.rs+build/emit/rust_server.rs):Now:
Rename note: the existing handwritten
rpc.rs→handlers.rsto disambiguate it from the codegengenerated/rpc.rs. Recipe_server'srpc.rsis the only file that needs hand-migrating; hero_service template doesn't have one yet (scaffolder writes it fresh).C. OpenRPC specs —
<workspace_root>/docs/Was (emitter:
generate/openrpc.rs):Now: same paths, gitignored. No emitter changes — the contributor reads these from the on-disk path the admin UI /
<hero-api-docs>widget already loads. Thedocs/directory itself stays in tree (placeholder.gitkeepif needed soinclude_str!("../core/openrpc.json")resolves before first build — verified during impl).D. Rust SDK trait crate —
sdk/rust/src/Was (emitter:
build/emit/rust_rpc2.rs):Now:
The
@sdk-feature:marker auto-discovery loop inemit_rpc2_lib_rsmoves into thegenerated/mod.rswriter (handwritten sibling modules still get picked up — they just sit besidegenerated/, not inside it).E. Rhai SDK crate —
sdk/rhai/src/Was:
lib.rsis currently a handwritten 3-line stub; nogenerate/rhai.rsemit targets it (only the per-domainrhai_types.rsin the consumer crate). No-op for this issue beyond adding the gitignore line — once a Rhai emitter for the SDK lands, it'll naturally write intosdk/rhai/src/generated/.F. JS SDK —
sdk/js/Was (emitter:
generate/js.rs):Now: same paths,
src/gitignored.package.jsonstays committed; everything undersrc/is codegen output.G. Python SDK —
sdk/python/Was (emitter:
build/emit/python_sdk.rs):Now:
The Python package layout (
hero_<service>_sdk/...) is the import contract — moving it under agenerated/subdirectory would change every consumer's import path. Per the issue body, easier to gitignore the whole package dir and keeppyproject.toml+ README at the parent level.2. Gitignore entries
Per-domain
.gitignorefiles (emitted by scaffolder)Each
crates/<name>/src/<domain>/.gitignoreandcrates/<name>_server/src/<domain>/.gitignore:These are scaffolded once alongside the parent
mod.rsand never touched again.Repo-root
.gitignore(scaffolder appends; merges with existing entries)Appended block (idempotent — scaffolder skips if marker is already present):
sdk/python/*/ignores every direct subdir ofsdk/python/(i.e. the per-service package dirs likehero_recipes_sdk/) while keepingpyproject.toml+ README + the gitignore file itself in tree.3. What stays committed (recap from the issue body, made concrete)
service.toml, rootCargo.toml, workspaceCargo.lockschemas/**/*.oschema(codegen input)crates/<name>{,_server}/src/<domain>/mod.rs— scaffolded once, contributor-owned afterwardcrates/<name>_server/src/<domain>/handlers.rs— preserved handwritten trait implcrates/<name>_server/src/<domain>/tests_*.rs— handwritten test extras (e.g.tests_error_category.rs)crates/<name>_admin/**— admin UI is hand-writtensdk/rust/src/lib.rs— scaffolded oncesdk/rhai/src/lib.rs— handwritten (Rhai SDK emitter doesn't exist yet)sdk/js/package.jsonsdk/python/pyproject.toml, optionalsdk/python/README.mddocs/.gitkeep(so the directory exists pre-first-build for anyinclude_str!paths — verified during impl)4. Scope clarifications
mod.rsfiles, (b) per-domain.gitignore, (c) repo-root.gitignoreblock, (d)handlers.rsstubs (renamed fromrpc.rs). All once-only.generated/(anddocs/), every build, unconditionally.examples/rust/and per-domain e2e examples: out of scope here. They're currently committed; leaving them as-is. Could move toexamples/generated/in a follow-up if friction shows up.5. Open questions / call-outs
types.rswrapper drop: confirming theinclude!("types_generated.rs")pattern goes away. Custom impls move to a siblingextensions.rs(handwritten, declared from parentmod.rs). Recipe_server'stypes.rsonly has commented examples, so safe.rpc.rs→handlers.rsrename: in the server crate, the handwritten preserved file. Only one repo (recipe_server) needs the hand-migration; hero_service scaffolder gets it right from the start.docs/placeholder: ifinclude_str!("../core/openrpc.json")(or the per-domain variant inrust_server.rs) needs the file at compile time, we need it generated before rustc sees the consuming file. build.rs ordering should handle this, but I'll verify with a clean clone +cargo buildduring impl. If broken, fall back to scaffolding a.gitkeepand ensuring the codegen step runs first viacargo:rerun-if-changed.Requesting sign-off on the layout + gitignore wording before I start changing emit paths. If the rename
rpc.rs→handlers.rsis contentious (e.g. you'd rather keeprpc.rsas the handwritten file and name the generated dispatcher something else), happy to swap — the only constraint is one of the two names has to change._admin+_webthat drive the generated SDK end-to-end #98Design finalized — proceeding with implementation
Two adjustments from the previous comment based on offline sign-off:
1. Skip the
rpc.rs→handlers.rsrenameThe handwritten preserved file stays
rpc.rs. The codegen dispatcher moves togenerated/rpc.rs— different module paths (recipes::rpcvsrecipes::generated::rpc), no collision. This preserves the convention every existing service already uses and avoids a hand-migration on each service when #90 phase 3 sweeps the broader codebase.Server crate final shape:
2. Keep the
include!()wrapper pattern fortypes.rsBetter dev ergonomics: contributor adds
impl Recipe { … }directly below theinclude!()and it lives in the same module scope as the generated struct. Less ceremony than a siblingextensions.rsthat has touse super::generated::*;first.Consumer crate final shape:
Parent
mod.rs(scaffolded once):The
types.rswrapper (scaffolded once):generated/mod.rsdeliberately does NOT re-exporttypes— the wrapper owns the native re-export. Onlytypes_wasmandrhai_typesgetpub mod …lines from the regenerated barrel.Everything else from the previous comment stands
.gitignoreignoringgenerated/(scaffolded once per domain)..gitignoreblock coveringdocs/openrpc.json,docs/*/openrpc.json,sdk/rust/src/generated/,sdk/rhai/src/generated/,sdk/js/src/,sdk/python/*/with!sdk/python/pyproject.toml/!sdk/python/README.mdexceptions.sdk/rust/src/generated/; JS gitignoressdk/js/src/(onlypackage.jsonstays); Python gitignores the per-service package dir.<workspace>/docs/, gitignored.examples/rust/and per-domain e2e examples out of scope.example/recipe_server+hero_servicetemplate only (per the issue body).Starting implementation now in
issue-96-generated-folders. Will commit + push in small steps and report back whencargo build --workspaceon recipe_server leavesgit statusempty.PR #101 merged into development. Acceptance criteria met for recipe_server: cargo build clean, git status empty post-build, no _generated.rs filenames remain. Next: hero_service template PR + hero_service_scaffold.md skill update.
Done. All acceptance criteria met across three merged PRs:
developmentdevelopmentdevelopmentAcceptance check
cargo build --workspaceclean on bothexample/recipe_serverandhero_servicefrom a fresh clone.git statusaftercargo build --workspaceis empty in both repos.*_generated.rsfilenames remain anywhere outsidetarget//crates/osis/examples/(legacy fixtures, out of scope per the issue body).hero_service_scaffold.mdreflects the new layout — the layout tree calls out thegenerated/subfolder, theinclude!()wrapper pattern fortypes.rs, the build-ordering edges for SDK/admin crates, and the per-domain + repo-root gitignore shape.What landed (recap of the design)
Per-domain
generated/subfolders under every consumer crate, server crate, and the Rust SDK. The codegen writes everything insidegenerated/; the contributor-facing parentmod.rs,types.rs(include!()wrapper for custom impls), andrpc.rs(handwritten trait impl) sit besidegenerated/and are scaffolded once._generated.rssuffix dropped everywhere — folder name carries the signal.docs/and the JS/Python SDK trees are gitignored at the repo root; per-domain.gitignorefiles cover the per-crategenerated/dirs.sdk/rust/and<svc>_admin/get a two-linebuild.rsshim plus a[build-dependencies] hero_<name>entry so cargo runs the core crate'sbuild.rs(which produces both crates' codegen) before either compiles.Out of scope (deferred to other issues per the issue body)
recipe_server+ thehero_servicetemplate — handled in hero_rpc#90 phase 3 sweep.generated/was generated by the generator — out of scope, lots of friction, low value.cargo genstep — codegen stays insidecargo buildviabuild.rs.