Application Bootstrap
Spice’s preferred application declaration is the ordinary Go process entrypoint. It contains annotations, command arguments, and exit conversion—no framework assembly:
package main
import ( "os"
_ "example.com/shop/orders" _ "example.com/shop/payments" _ "example.com/shop/platform" spiceapp "example.com/shop/internal/spicegen/shop")
// @import { Application } from "github.com/spice-framework/spice/annotation/core"// @import { Enable } from "github.com/spice-framework/spice/annotation/management"// @import { Logging } from "github.com/spice-framework/spice/annotation/observability"
// @Application// @Enable(expose=["health", "liveness", "readiness", "info", "metrics"], access="loopback")// @Loggingfunc main() { os.Exit(spiceapp.Main(os.Args[1:]))}The generated target package is an explicit ordinary Go dependency. Its
Main returns a stable exit code and never calls os.Exit. This makes the
process boundary, generated implementation, Go-to-definition behavior, and
debugger transition visible without writing generated declarations beside
handwritten source.
Compile-time discovery
For generation, Spice loads one selected command package through the standard
package driver and its existing typed compiler pipeline. Direct blank imports
of packages in the same Go module explicitly compose the application. Those
already type-checked dependencies are promoted into the same immutable program;
Spice does not perform a second package load. Named imports are ordinary code
dependencies, external blank imports retain ordinary Go side-effect semantics,
and */autoconfigure imports use the separate explicit library-default
contract. Within the composed scope Spice discovers:
- package-documentation
@Moduleroots and ownership; @Beanproviders and their exact-type dependencies;- typed configuration declarations;
- controllers, routes, authorization, and transaction boundaries;
- lifecycle hooks, jobs, asynchronous methods, caches, and events;
- explicitly imported library auto-configuration defaults and application features.
Every generated import and call is ordinary inspectable Go. Discovery does not
use reflection, runtime package scanning, init, a service locator, a global
registry, provider execution, or dependency presence.
A normal single-application module can run:
go tool github.com/spice-framework/toolchain/cmd/spice generatego tool github.com/spice-framework/toolchain/cmd/spice generate --checkgo tool github.com/spice-framework/toolchain/cmd/spice buildWhen a module has multiple application targets, pass only the command package
and select the command unambiguously. For example, in the standalone
commerce repository:
cd commercego tool github.com/spice-framework/toolchain/cmd/spice generate --target Commerce .--target accepts the derived target name, command import path, or stable
marker symbol ID. Package patterns are analysis scope, not module imports and
not runtime activation.
Run
spice run is the first-class development execution path:
cd commercego tool github.com/spice-framework/toolchain/cmd/spice run --target Commerce . -- -checkArguments before -- select the application and compile-time package scope;
arguments after it belong to the generated application command. Spice applies
guarded generation, builds only the selected package-main import path with
-trimpath into a unique temporary artifact, and starts that exact candidate.
Application standard input, output, error output, and nonzero exit codes are
preserved.
The child runs in an isolated process group. Interrupt and termination are relayed on Windows and Unix so the generated command can drain HTTP and execute its bounded lifecycle shutdown. A second interrupt or an unresponsive process after the relay deadline is terminated. The temporary artifact is removed after exit. Legacy parameter-root markers remain generatable and buildable but are deliberately not runnable because they do not identify a package-main process.
Generated layout and ownership
The preferred target owns:
internal/spicegen/<target>/spice_contracts_gen.gointernal/spicegen/<target>/spice_configuration_gen.gointernal/spicegen/<target>/spice_providers_gen.gointernal/spicegen/<target>/spice_assembly_gen.gointernal/spicegen/<target>/spice_features_gen.go # when neededinternal/spicegen/<target>/spice_http_gen.go # when HTTP is enabledinternal/spicegen/<target>/spice_http_route_<symbol>_<id>_gen.gointernal/spicegen/<target>/spice_lifecycle_gen.gointernal/spicegen/<target>/spice_command_gen.gointernal/spicegen/<target>/sources/<source-directory>/<source>_spice_gen.gointernal/spicegen/<target>/artifacts/openapi.json # when controllers exist.spice/<target>.manifest.jsonThere is deliberately no catch-all generated file. Contracts, configuration, provider-graph construction, phase assembly, optional features, HTTP coordination, lifecycle methods, and process commands each have one named boundary. Every HTTP route has a readable, stable symbol-and-hash-derived file, so a breakpoint or route edit does not require navigating an application-sized renderer output. The small assembly unit invokes these bounded phases in validated order. The repository quality gate caps target-level generated units in the reference applications at 400 lines so future features must add a semantic shard instead of rebuilding the monolith.
The handwritten command imports the target package directly. Every contributing
handwritten file—including the application marker—owns one mirrored source
unit; providers, configuration binders, application metadata, and
conventional blank-identifier @Implements assertions derived from that file
live together there. Source
units use nested generated packages rather than appearing beside handwritten
Go, and target provider wiring calls their typed exported adapters.
The schema-5 manifest records each file’s concern role—including a distinct
target-http-route role—primary source, related source declarations, exact
generated ranges, and SHA-256 ownership. Every regular file below a generated
target must appear in that manifest; the repository gates reject handwritten
tests, helper files, stale targets, and other unowned artifacts. Application
acceptance tests live outside internal/spicegen and import the generated
package as an ordinary black-box dependency. Generation
preserves unchanged files, refuses manual edits and unowned collisions, and
supports read-only check and bounded diff modes. Migration removes legacy
schema-4 monoliths and adjacent schema-3 shards only when their recorded hash
still matches.
Generated files have standard Go source positions and direct calls into
handwritten functions, so stepping from wiring into user code uses the normal
Go debugger. spice generated --source path.go --line n and the reverse
--generated form query the manifest without compiling or changing files.
Generated dependency variables, source adapter imports, and route functions
use stable semantic names. Short deterministic suffixes disambiguate exported
cross-file helpers; opaque ordinal names such as provider17 are not the
ordinary wiring contract.
Generated source is excluded only from regeneration analysis with the reserved
spice_generate build tag. During analysis, Spice verifies that annotated
func main imports the exact generated target package and calls its Main
function. If that package is not available under the analysis tag, the loader
adds a pure in-memory stub package at that exact import path. It does not write
a bridge, suppress an undefined identifier, or accept any other load error.
This permits safe first generation while ordinary Go commands remain strict.
Process and reusable ownership
The generated target package’s Main owns conventional SPICE_ environment loading and
SIGINT/SIGTERM because it is the process boundary. It creates a fresh
bounded shutdown context and returns zero for success, one for runtime failure,
or two for invalid command usage.
The generated NewApplication, NewApplicationWithOptions, Start, Stop,
Run, Components, RunCommand, and Main seams are exported directly by
the generated target package for tests and embedded policies. Components is
a generated typed snapshot of singleton beans, not a reflection container or
string lookup. BeanOverrides is a generated compile-time-typed test and
embedding seam for public singleton beans. bean.Replace supplies an exact
value; bean.ReplaceFactory supplies a value plus lifecycle cleanup. Generated
construction uses the replacement at the original provider position, so
dependencies, rollback, module cleanup ownership, and shutdown ordering remain
unchanged. Disabled zero values preserve production behavior; there is no
mutable application context or string-addressed bean replacement.
Generated BeanOverrideLayer and ComposeBeanOverrides additionally compose
named library, embedding, and test layers before construction. Layers are
validated in order and later enabled exact-type fields deliberately replace
earlier fields; the resulting BeanOverrides value still enters the same
construction path.
The reusable APIs accept caller-owned contexts, configuration sources, overrides, observers, middleware, writers, loggers, and shutdown policy. They never capture process signals.
Legacy marker compatibility
During the pre-1.0 period, a package-level marker may still enumerate exact provider roots as parameters:
// @Applicationfunc Commerce(*platform.Server, *orders.Service) {}Legacy parameter-root markers also retain internal/spicegen/<target>. During
ownership migration, guarded generation removes old same-package bridges only
when their manifest hash still matches. Manual edits fail closed.